Skip to content

Commit 1846a83

Browse files
authored
Support trace detail (#162)
* Add explanation to trace Signed-off-by: kerthcet <kerthcet@gmail.com> * Add explanation to trace Signed-off-by: kerthcet <kerthcet@gmail.com> * Optimize the layout Signed-off-by: kerthcet <kerthcet@gmail.com> * centrate the timelinle Signed-off-by: kerthcet <kerthcet@gmail.com> --------- Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent 602db7d commit 1846a83

7 files changed

Lines changed: 705 additions & 556 deletions

File tree

dashboard/src/components/traces/trace-timeline.tsx

Lines changed: 166 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { 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';
33
import type { Span } from '../../types';
44
import { Card, CardContent } from '../ui/card';
55
import { Badge } from '../ui/badge';
6+
import { Button } from '../ui/button';
67

78
interface 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
);

dashboard/static/assets/index-BbcDnBPG.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)