import { useCallback, useEffect, useMemo, useState } from 'react'; import { ChevronDown, ChevronRight, AlertCircle } from 'lucide-react'; import { api } from '@/api/client'; import type { ToolTrace } from '@/api/types'; interface Props { chatId: string; } // Max latency used as the 100% reference for the bar visualization const MAX_LATENCY_REF = 30_000; // 30s function latencyBarWidth(latencyMs: number | null): number { if (latencyMs == null) return 0; return Math.min(latencyMs / MAX_LATENCY_REF, 1); } function TraceRow({ trace }: { trace: ToolTrace }) { const [expanded, setExpanded] = useState(false); const isError = trace.outcome !== null && trace.outcome !== 'success'; const barWidth = latencyBarWidth(trace.latency_ms); const latencyLabel = trace.latency_ms != null ? trace.latency_ms >= 1000 ? `${(trace.latency_ms / 1000).toFixed(1)}s` : `${trace.latency_ms}ms` : null; return (
{expanded && (
Input
              {JSON.stringify(trace.tool_input, null, 1)}
            
{trace.tool_output != null && (
Output
                {trace.tool_output.length > 2000
                  ? `${trace.tool_output.slice(0, 2000)}…`
                  : trace.tool_output}
              
)} {trace.error != null && (
{trace.error}
)}
)}
); } function TraceGroup({ toolName, traces }: { toolName: string; traces: ToolTrace[] }) { const [collapsed, setCollapsed] = useState(false); const totalLatency = traces.reduce((sum, t) => sum + (t.latency_ms ?? 0), 0); const totalTokens = traces.reduce((sum, t) => sum + (t.tokens_used ?? 0), 0); const errorCount = traces.filter( (t) => t.outcome !== null && t.outcome !== 'success', ).length; return (
{!collapsed && traces.map((trace) => ( ))}
); } export function TraceViewer({ chatId }: Props) { const [open, setOpen] = useState(false); const [traces, setTraces] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchTraces = useCallback(async () => { setLoading(true); setError(null); try { const res = await api.chats.getTraces(chatId); setTraces(res.data); } catch (err) { setError(err instanceof Error ? err.message : 'failed to load traces'); } finally { setLoading(false); } }, [chatId]); useEffect(() => { if (open) { void fetchTraces(); } }, [open, fetchTraces]); const groups = useMemo(() => { const map = new Map(); for (const t of traces) { const existing = map.get(t.tool_name); if (existing) { existing.push(t); } else { map.set(t.tool_name, [t]); } } return map; }, [traces]); const totalCount = traces.length; const errorCount = traces.filter( (t) => t.outcome !== null && t.outcome !== 'success', ).length; return (
{open && (
{loading && traces.length === 0 && (
Loading traces…
)} {error && (
{error}
)} {!loading && !error && traces.length === 0 && (
No tool traces yet.
)} {traces.length > 0 && (
{Array.from(groups.entries()).map(([toolName, groupTraces]) => ( ))}
)}
)}
); }