11'use client' ;
22
3+ import { useState } from 'react' ;
34import {
45 Table ,
56 TableBody ,
@@ -9,6 +10,8 @@ import {
910 TableRow ,
1011} from '@tremor/react' ;
1112import { format } from 'date-fns' ;
13+ import { X } from 'lucide-react' ;
14+ import { useLLMMessages } from '@/hooks/useTinybirdData' ;
1215
1316// Define the shape of the LLM message data
1417interface LLMMessage {
@@ -18,6 +21,7 @@ interface LLMMessage {
1821 environment : string ;
1922 user : string ;
2023 chat_id : string ;
24+ message_id ?: string ;
2125 model : string ;
2226 provider : string ;
2327 prompt_tokens : number ;
@@ -28,6 +32,8 @@ interface LLMMessage {
2832 response_status : string ;
2933 exception : string | null ;
3034 similarity ?: number ;
35+ messages ?: string [ ] ;
36+ response_choices ?: string [ ] ;
3137}
3238
3339interface DataTableProps {
@@ -60,11 +66,194 @@ const MOCK_DATA = {
6066 ]
6167} ;
6268
69+ // Detail view component
70+ function DetailView ( { message, onClose } : { message : LLMMessage , onClose : ( ) => void } ) {
71+ const { data : chatData , isLoading } = useLLMMessages ( {
72+ chat_id : message . chat_id ,
73+ message_id : message . message_id
74+ } ) ;
75+
76+ // Parse messages from JSON string if needed
77+ const parseMessages = ( msg : LLMMessage ) => {
78+ if ( typeof msg . messages === 'string' ) {
79+ try {
80+ return JSON . parse ( msg . messages ) ;
81+ } catch ( e ) {
82+ console . error ( 'Failed to parse messages:' , e ) ;
83+ return [ ] ;
84+ }
85+ }
86+ return msg . messages || [ ] ;
87+ } ;
88+
89+ // Parse response choices from JSON string
90+ const parseResponseChoices = ( msg : LLMMessage ) => {
91+ if ( ! msg . response_choices || msg . response_status !== 'success' ) return [ ] ;
92+
93+ if ( typeof msg . response_choices === 'string' ) {
94+ try {
95+ return JSON . parse ( msg . response_choices ) ;
96+ } catch ( e ) {
97+ try {
98+ // Handle case where response_choices is an array of JSON strings
99+ return ( msg . response_choices as unknown as string [ ] ) . map ( choice => JSON . parse ( choice ) ) ;
100+ } catch ( e2 ) {
101+ console . error ( 'Failed to parse response choices:' , e2 ) ;
102+ return [ ] ;
103+ }
104+ }
105+ }
106+ return msg . response_choices ;
107+ } ;
108+
109+ return (
110+ < div className = "fixed inset-y-0 right-0 w-1/3 bg-gray-800 shadow-xl z-50 overflow-auto transition-transform duration-300 transform translate-x-0" >
111+ < div className = "p-4 border-b border-gray-700 flex justify-between items-center" >
112+ < h2 className = "text-xl font-semibold text-white" > Conversation Details</ h2 >
113+ < button
114+ onClick = { onClose }
115+ className = "p-1 rounded-full hover:bg-gray-700 transition-colors"
116+ >
117+ < X className = "h-6 w-6 text-gray-400" />
118+ </ button >
119+ </ div >
120+
121+ < div className = "p-4" >
122+ < div className = "mb-6 bg-gray-900 rounded-lg p-4" >
123+ < h3 className = "text-lg font-medium text-white mb-2" > Message Info</ h3 >
124+ < div className = "grid grid-cols-2 gap-2 text-sm" >
125+ < div className = "text-gray-400" > Model</ div >
126+ < div className = "text-white" > { message . model } </ div >
127+
128+ < div className = "text-gray-400" > Provider</ div >
129+ < div className = "text-white" > { message . provider } </ div >
130+
131+ < div className = "text-gray-400" > Organization</ div >
132+ < div className = "text-white" > { message . organization } </ div >
133+
134+ < div className = "text-gray-400" > Project</ div >
135+ < div className = "text-white" > { message . project } </ div >
136+
137+ < div className = "text-gray-400" > User</ div >
138+ < div className = "text-white" > { message . user } </ div >
139+
140+ < div className = "text-gray-400" > Timestamp</ div >
141+ < div className = "text-white" > { format ( new Date ( message . timestamp ) , 'MMM d, yyyy HH:mm:ss' ) } </ div >
142+
143+ < div className = "text-gray-400" > Total Tokens</ div >
144+ < div className = "text-white" > { message . total_tokens . toLocaleString ( ) } </ div >
145+
146+ < div className = "text-gray-400" > Duration</ div >
147+ < div className = "text-white" > { message . duration . toFixed ( 2 ) } s</ div >
148+
149+ < div className = "text-gray-400" > Cost</ div >
150+ < div className = "text-white" > ${ message . cost . toFixed ( 4 ) } </ div >
151+
152+ < div className = "text-gray-400" > Status</ div >
153+ < div className = "text-white" >
154+ < span className = { `px-2 py-1 rounded-full text-xs ${
155+ message . response_status === 'success'
156+ ? 'bg-green-100 text-green-800'
157+ : 'bg-red-100 text-red-800'
158+ } `} >
159+ { message . response_status }
160+ </ span >
161+ </ div >
162+ </ div >
163+ </ div >
164+
165+ < h3 className = "text-lg font-medium text-white mb-2" > Conversation</ h3 >
166+
167+ { isLoading ? (
168+ < div className = "text-center py-4 text-gray-400" > Loading conversation...</ div >
169+ ) : (
170+ < div className = "space-y-4" >
171+ { chatData ?. data && chatData . data . length > 0 ? (
172+ chatData . data . map ( ( msg : LLMMessage , idx : number ) => {
173+ const messages = parseMessages ( msg ) ;
174+
175+ // If we have structured messages, render those
176+ if ( messages && messages . length > 0 ) {
177+ return (
178+ < div key = { idx } className = "space-y-2" >
179+ { messages . map ( ( chatMsg : any , msgIdx : number ) => (
180+ < div key = { `${ idx } -${ msgIdx } ` } className = { `rounded-lg p-3 text-white ${
181+ chatMsg . role === 'user' ? 'bg-gray-700' :
182+ chatMsg . role === 'assistant' ? 'bg-blue-900' :
183+ 'bg-purple-900'
184+ } `} >
185+ < div className = "text-xs text-gray-400 mb-1 capitalize" > { chatMsg . role } </ div >
186+ < div className = "whitespace-pre-wrap" > { chatMsg . content } </ div >
187+ </ div >
188+ ) ) }
189+
190+ { /* Show alternative responses if available */ }
191+ { msg . response_status === 'success' && (
192+ < >
193+ { parseResponseChoices ( msg ) . length > 1 && (
194+ < div className = "mt-4" >
195+ < div className = "text-sm text-gray-400 mb-2" > Alternative Responses:</ div >
196+ < div className = "space-y-2" >
197+ { parseResponseChoices ( msg ) . slice ( 1 ) . map ( ( choice : any , choiceIdx : number ) => (
198+ < div key = { `choice-${ choiceIdx } ` } className = "bg-gray-700 rounded-lg p-3 text-white opacity-75" >
199+ < div className = "text-xs text-gray-400 mb-1" > Alternative { choiceIdx + 1 } </ div >
200+ < div className = "whitespace-pre-wrap" >
201+ { choice . message ?. content || choice . content || JSON . stringify ( choice ) }
202+ </ div >
203+ </ div >
204+ ) ) }
205+ </ div >
206+ </ div >
207+ ) }
208+ </ >
209+ ) }
210+ </ div >
211+ ) ;
212+ }
213+
214+ // Fallback to prompt/response if no structured messages
215+ return (
216+ < div key = { idx } className = "space-y-2" >
217+ { msg . prompt && (
218+ < div className = "bg-gray-700 rounded-lg p-3 text-white" >
219+ < div className = "text-xs text-gray-400 mb-1" > User</ div >
220+ < div className = "whitespace-pre-wrap" > { msg . prompt } </ div >
221+ </ div >
222+ ) }
223+
224+ { msg . response && (
225+ < div className = "bg-blue-900 rounded-lg p-3 text-white" >
226+ < div className = "text-xs text-gray-400 mb-1" > Assistant</ div >
227+ < div className = "whitespace-pre-wrap" > { msg . response } </ div >
228+ </ div >
229+ ) }
230+
231+ { msg . response_status !== 'success' && msg . exception && (
232+ < div className = "bg-red-900 rounded-lg p-3 text-white" >
233+ < div className = "text-xs text-gray-400 mb-1" > Error</ div >
234+ < div className = "whitespace-pre-wrap" > { msg . exception } </ div >
235+ </ div >
236+ ) }
237+ </ div >
238+ ) ;
239+ } )
240+ ) : (
241+ < div className = "text-center py-4 text-gray-400" > No conversation data available</ div >
242+ ) }
243+ </ div >
244+ ) }
245+ </ div >
246+ </ div >
247+ ) ;
248+ }
249+
63250export default function DataTable ( {
64251 data = MOCK_DATA ,
65252 isLoading = false ,
66253 searchHighlight = null
67254} : DataTableProps ) {
255+ const [ selectedMessage , setSelectedMessage ] = useState < LLMMessage | null > ( null ) ;
256+
68257 if ( isLoading ) {
69258 return (
70259 < div className = "flex items-center justify-center h-full" >
@@ -73,20 +262,23 @@ export default function DataTable({
73262 ) ;
74263 }
75264
265+ const handleRowClick = ( message : LLMMessage ) => {
266+ setSelectedMessage ( message ) ;
267+ } ;
268+
269+ const handleCloseDetail = ( ) => {
270+ setSelectedMessage ( null ) ;
271+ } ;
272+
76273 return (
77- < div className = "flex flex-col h-full" >
78- < div className = "flex-none" >
79- < div className = "md:flex md:items-center md:justify-between md:space-x-8" >
80- { /* <div>
81- <h3 className="font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
82- Recent Messages
83- </h3>
84- <p className="mt-1 text-tremor-default leading-6 text-tremor-content dark:text-dark-tremor-content">
85- Detailed view of recent LLM interactions
86- </p>
87- </div> */ }
88- </ div >
89- </ div >
274+ < div className = "flex flex-col h-full relative" >
275+ { /* Overlay when detail view is open */ }
276+ { selectedMessage && (
277+ < div
278+ className = "fixed inset-0 bg-black bg-opacity-50 z-40"
279+ onClick = { handleCloseDetail }
280+ />
281+ ) }
90282
91283 < div className = "flex-1 overflow-auto min-h-0" >
92284 < div className = "min-w-[1024px]" >
@@ -111,7 +303,11 @@ export default function DataTable({
111303 < TableBody >
112304 { data . data && data . data . length > 0 ? (
113305 data . data . map ( ( item , idx ) => (
114- < TableRow key = { idx } >
306+ < TableRow
307+ key = { idx }
308+ onClick = { ( ) => handleRowClick ( item ) }
309+ className = "cursor-pointer hover:bg-gray-800 transition-colors"
310+ >
115311 < TableCell > { format ( new Date ( item . timestamp ) , 'MMM d, yyyy HH:mm:ss' ) } </ TableCell >
116312 < TableCell > { item . model } </ TableCell >
117313 < TableCell > { item . provider } </ TableCell >
@@ -154,6 +350,14 @@ export default function DataTable({
154350 </ Table >
155351 </ div >
156352 </ div >
353+
354+ { /* Detail View */ }
355+ { selectedMessage && (
356+ < DetailView
357+ message = { selectedMessage }
358+ onClose = { handleCloseDetail }
359+ />
360+ ) }
157361 </ div >
158362 ) ;
159363}
0 commit comments