11import { useState , useMemo } from 'react' ;
2- import { ChevronDown , ChevronRight , Clock , Zap , Database , Globe , Bot } from 'lucide-react' ;
2+ import { ChevronDown , ChevronRight , Clock , Zap , Database , Globe , Bot , X } from 'lucide-react' ;
33import type { Span } from '../../types' ;
44import { Card , CardContent } from '../ui/card' ;
55import { Badge } from '../ui/badge' ;
6+ import { Button } from '../ui/button' ;
67
78interface TraceTimelineProps {
89 spans : Span [ ] ;
@@ -53,6 +54,7 @@ export function TraceTimeline({ spans }: TraceTimelineProps) {
5354 const [ expandedSpans , setExpandedSpans ] = useState < Set < string > > ( ( ) => {
5455 return new Set ( spans . filter ( s => ! s . parentSpanId || s . parentSpanId === '' ) . map ( s => s . spanId ) ) ;
5556 } ) ;
57+ const [ selectedSpan , setSelectedSpan ] = useState < Span | null > ( null ) ;
5658
5759 const expandAll = ( ) => {
5860 const allSpanIds = new Set ( spans . map ( s => s . spanId ) ) ;
@@ -194,7 +196,7 @@ export function TraceTimeline({ spans }: TraceTimelineProps) {
194196
195197 return (
196198 < div
197- className = { `${ statusColor } absolute h-6 rounded flex items-center px-1 text-white text-xs font-medium overflow-hidden transition-opacity hover:opacity-90 cursor-pointer shadow-sm` }
199+ className = { `${ statusColor } absolute h-5 rounded flex items-center px-1 text-white text-xs font-medium overflow-hidden transition-opacity hover:opacity-90 cursor-pointer shadow-sm` }
198200 style = { {
199201 left : `${ leftPercent } %` ,
200202 width : `${ Math . max ( widthPercent , 0.5 ) } %` , // Minimum width for visibility
@@ -218,10 +220,19 @@ export function TraceTimeline({ spans }: TraceTimelineProps) {
218220 return (
219221 < div key = { span . spanId } >
220222 { /* Span Row */ }
221- < div className = "flex items-center border-b border-border hover:bg-muted/50 transition-colors" >
223+ < div
224+ className = { `flex items-center border-b border-border hover:bg-muted/50 transition-colors cursor-pointer ${
225+ selectedSpan ?. spanId === span . spanId ? 'bg-accent' : ''
226+ } `}
227+ onClick = { ( e ) => {
228+ // Don't trigger if clicking expand button
229+ if ( ( e . target as HTMLElement ) . closest ( 'button' ) ) return ;
230+ setSelectedSpan ( span ) ;
231+ } }
232+ >
222233 { /* Left: Span info with expand button */ }
223234 < div
224- className = "flex-shrink-0 flex items-center gap-2 py-2 pr-2 min-w-0"
235+ className = "flex-shrink-0 flex items-center gap-2 py-1.5 pr-2 min-w-0"
225236 style = { { width : '350px' , paddingLeft : `${ depth * 12 + 8 } px` } }
226237 >
227238 { /* Tree connector line */ }
@@ -283,15 +294,10 @@ export function TraceTimeline({ spans }: TraceTimelineProps) {
283294 < span className = "text-muted-foreground/40" > —</ span >
284295 ) }
285296 </ div >
286-
287- { /* Status indicator */ }
288- < div className = "flex items-center justify-center flex-shrink-0" style = { { width : '50px' } } >
289- < div className = { `w-2 h-2 rounded-full ${ STATUS_COLORS [ span . statusCode ] || STATUS_COLORS [ 'UNSET' ] } ` } title = { span . statusCode } />
290- </ div >
291297 </ div >
292298
293299 { /* Right: Timeline bar */ }
294- < div className = "flex-1 relative h-10 px-2 min-w-0" >
300+ < div className = "flex-1 relative h-8 px-2 min-w-0 flex items-center " >
295301 { renderSpanBar ( node ) }
296302 </ div >
297303 </ div >
@@ -312,11 +318,149 @@ export function TraceTimeline({ spans }: TraceTimelineProps) {
312318 ) ;
313319 }
314320
321+ const renderSpanDetails = ( span : Span ) => {
322+ const spanType = getSpanType ( span ) ;
323+ const attrs = span . spanAttributes || { } ;
324+
325+ // Extract model parameters
326+ const model = attrs [ 'gen_ai.request.model' ] || attrs [ 'gen_ai.response.model' ] ;
327+ const temperature = attrs [ 'gen_ai.request.temperature' ] ;
328+ const maxTokens = attrs [ 'gen_ai.request.max_tokens' ] ;
329+ const topP = attrs [ 'gen_ai.request.top_p' ] ;
330+
331+ // Extract prompts
332+ const prompts : Array < { role : string ; content : string } > = [ ] ;
333+ let i = 0 ;
334+ while ( attrs [ `gen_ai.prompt.${ i } .role` ] ) {
335+ prompts . push ( {
336+ role : attrs [ `gen_ai.prompt.${ i } .role` ] as string ,
337+ content : attrs [ `gen_ai.prompt.${ i } .content` ] as string ,
338+ } ) ;
339+ i ++ ;
340+ }
341+
342+ // Extract completions
343+ const completions : Array < { role : string ; content : string ; finishReason ?: string } > = [ ] ;
344+ i = 0 ;
345+ while ( attrs [ `gen_ai.completion.${ i } .role` ] ) {
346+ completions . push ( {
347+ role : attrs [ `gen_ai.completion.${ i } .role` ] as string ,
348+ content : attrs [ `gen_ai.completion.${ i } .content` ] as string ,
349+ finishReason : attrs [ `gen_ai.completion.${ i } .finish_reason` ] as string ,
350+ } ) ;
351+ i ++ ;
352+ }
353+
354+ return (
355+ < Card className = "mb-3" >
356+ < CardContent className = "p-3" >
357+ < div className = "flex items-start justify-between mb-3" >
358+ < div className = "flex items-center gap-2" >
359+ < Badge variant = "outline" className = { `${ spanType . badgeColor } flex items-center gap-1 px-1.5 py-0.5 text-xs` } >
360+ { spanType . icon }
361+ { spanType . label }
362+ </ Badge >
363+ < h4 className = "font-semibold text-sm" > { span . spanName } </ h4 >
364+ </ div >
365+ < Button
366+ variant = "ghost"
367+ size = "sm"
368+ onClick = { ( ) => setSelectedSpan ( null ) }
369+ className = "h-5 w-5 p-0"
370+ >
371+ < X className = "h-3 w-3" />
372+ </ Button >
373+ </ div >
374+
375+ { /* Model Parameters */ }
376+ { model && (
377+ < div className = "mb-3" >
378+ < h5 className = "text-xs font-medium mb-1.5 text-muted-foreground" > Model Configuration</ h5 >
379+ < div className = "grid grid-cols-2 gap-2 text-xs border rounded p-2 bg-muted/50" >
380+ < div className = "col-span-2" >
381+ < span className = "text-muted-foreground" > Model:</ span >
382+ < span className = "ml-2 font-mono" > { model } </ span >
383+ </ div >
384+ { temperature !== undefined && (
385+ < div >
386+ < span className = "text-muted-foreground" > Temperature:</ span >
387+ < span className = "ml-2 font-mono" > { temperature } </ span >
388+ </ div >
389+ ) }
390+ { maxTokens && (
391+ < div >
392+ < span className = "text-muted-foreground" > Max Tokens:</ span >
393+ < span className = "ml-2 font-mono" > { maxTokens } </ span >
394+ </ div >
395+ ) }
396+ { topP !== undefined && (
397+ < div >
398+ < span className = "text-muted-foreground" > Top P:</ span >
399+ < span className = "ml-2 font-mono" > { topP } </ span >
400+ </ div >
401+ ) }
402+ </ div >
403+ </ div >
404+ ) }
405+
406+ { /* Prompts */ }
407+ { prompts . length > 0 && (
408+ < div className = "mb-3" >
409+ < h5 className = "text-xs font-medium mb-1.5 text-muted-foreground" > Input</ h5 >
410+ < div className = "space-y-1.5" >
411+ { prompts . map ( ( prompt , idx ) => (
412+ < div key = { idx } className = "border rounded p-2 bg-muted/50" >
413+ < div className = "text-xs font-medium text-muted-foreground mb-1 uppercase" >
414+ { prompt . role }
415+ </ div >
416+ < div className = "text-xs whitespace-pre-wrap leading-relaxed" > { prompt . content } </ div >
417+ </ div >
418+ ) ) }
419+ </ div >
420+ </ div >
421+ ) }
422+
423+ { /* Completions */ }
424+ { completions . length > 0 && (
425+ < div className = "mb-3" >
426+ < h5 className = "text-xs font-medium mb-1.5 text-muted-foreground" > Output</ h5 >
427+ < div className = "space-y-1.5" >
428+ { completions . map ( ( completion , idx ) => (
429+ < div key = { idx } className = "border rounded p-2 bg-muted/50" >
430+ < div className = "text-xs font-medium text-muted-foreground mb-1 uppercase" >
431+ { completion . role }
432+ </ div >
433+ < div className = "text-xs whitespace-pre-wrap leading-relaxed" > { completion . content } </ div >
434+ </ div >
435+ ) ) }
436+ </ div >
437+ </ div >
438+ ) }
439+
440+ { /* Show all attributes (collapsible) */ }
441+ < details className = "mt-2" >
442+ < summary className = "text-xs font-medium cursor-pointer hover:text-foreground text-muted-foreground py-1" >
443+ All Attributes ({ Object . keys ( attrs ) . length } )
444+ </ summary >
445+ < div className = "mt-1.5 text-xs space-y-0.5 bg-muted/50 rounded p-2 max-h-48 overflow-auto" >
446+ { Object . entries ( attrs ) . map ( ( [ key , value ] ) => (
447+ < div key = { key } className = "grid grid-cols-3 gap-2" >
448+ < span className = "text-muted-foreground truncate" title = { key } > { key } :</ span >
449+ < span className = "col-span-2 font-mono break-all text-xs" > { String ( value ) } </ span >
450+ </ div >
451+ ) ) }
452+ </ div >
453+ </ details >
454+ </ CardContent >
455+ </ Card >
456+ ) ;
457+ } ;
458+
315459 return (
316460 < Card >
317- < CardContent className = "p-4 " >
461+ < CardContent className = "p-3 " >
318462 { /* Header */ }
319- < div className = "mb-4 flex items-center justify-between" >
463+ < div className = "mb-3 flex items-center justify-between" >
320464 < div className = "flex items-center gap-3" >
321465 < div className = "flex items-center gap-2" >
322466 < Clock className = "h-4 w-4 text-muted-foreground" />
@@ -346,7 +490,6 @@ export function TraceTimeline({ spans }: TraceTimelineProps) {
346490
347491 { /* Legend */ }
348492 < div className = "flex items-center gap-3 text-xs" >
349- < span className = "text-muted-foreground mr-1" > Status:</ span >
350493 < div className = "flex items-center gap-1" >
351494 < div className = "w-2 h-2 rounded-full bg-green-500" />
352495 < span className = "text-muted-foreground" > OK</ span >
@@ -366,22 +509,28 @@ export function TraceTimeline({ spans }: TraceTimelineProps) {
366509 < div className = "border rounded-lg overflow-hidden bg-background" >
367510 { /* Column headers */ }
368511 < div className = "flex items-center bg-muted/50 border-b border-border font-medium text-xs text-muted-foreground" >
369- < div className = "flex-shrink-0 px-3 py-2 " style = { { width : '350px' } } >
512+ < div className = "flex-shrink-0 px-3 py-1.5 " style = { { width : '350px' } } >
370513 Span Name
371514 </ div >
372- < div className = "flex items-center px-3 py-2 flex-shrink-0" >
515+ < div className = "flex items-center px-3 py-1.5 flex-shrink-0" >
373516 < span style = { { width : '80px' } } > Duration</ span >
374517 < span style = { { width : '170px' } } > Tokens</ span >
375- < span style = { { width : '50px' , textAlign : 'center' } } > Status</ span >
376518 </ div >
377- < div className = "flex-1 px-2 py-2 " >
519+ < div className = "flex-1 px-2 py-1.5 " >
378520 Timeline
379521 </ div >
380522 </ div >
381523
382524 { /* Span rows */ }
383525 { spanTree . map ( node => renderSpanNode ( node ) ) }
384526 </ div >
527+
528+ { /* Span Detail Panel */ }
529+ { selectedSpan && (
530+ < div className = "mt-3" >
531+ { renderSpanDetails ( selectedSpan ) }
532+ </ div >
533+ ) }
385534 </ CardContent >
386535 </ Card >
387536 ) ;
0 commit comments