Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useMemo } from "react"; | |
| type ToolInvocation = { | |
| tool: string; | |
| startTime: number; | |
| endTime: number; | |
| latency: number; | |
| status: "success" | "error"; | |
| resultCount?: number; | |
| }; | |
| type ToolTimelineProps = { | |
| toolTraces?: Array<Record<string, any>>; | |
| reasoningTrace?: Array<Record<string, any>>; | |
| }; | |
| export function ToolTimeline({ toolTraces = [], reasoningTrace = [] }: ToolTimelineProps) { | |
| const timelineData = useMemo(() => { | |
| if (!toolTraces || toolTraces.length === 0) { | |
| return []; | |
| } | |
| // Extract tool invocations from traces | |
| const invocations: ToolInvocation[] = toolTraces.map((trace, idx) => { | |
| const tool = trace.tool || trace.tool_name || "unknown"; | |
| const startTime = trace.start_time || trace.timestamp || idx * 100; // Fallback timing | |
| const latency = trace.latency_ms || trace.latency || 0; | |
| const endTime = startTime + latency; | |
| const status = trace.status === "error" || trace.error ? "error" : "success"; | |
| const resultCount = trace.result_count || trace.hits || trace.results?.length || 0; | |
| return { | |
| tool, | |
| startTime, | |
| endTime, | |
| latency, | |
| status, | |
| resultCount, | |
| }; | |
| }); | |
| // Sort by start time | |
| invocations.sort((a, b) => a.startTime - b.startTime); | |
| // Calculate timeline bounds | |
| const minTime = Math.min(...invocations.map((i) => i.startTime), 0); | |
| const maxTime = Math.max(...invocations.map((i) => i.endTime), minTime + 1000); | |
| // Normalize times to 0-100% for visualization | |
| const timeRange = maxTime - minTime || 1000; | |
| return invocations.map((inv) => ({ | |
| ...inv, | |
| startPercent: ((inv.startTime - minTime) / timeRange) * 100, | |
| widthPercent: (inv.latency / timeRange) * 100, | |
| })); | |
| }, [toolTraces]); | |
| if (timelineData.length === 0) { | |
| return ( | |
| <div className="rounded-2xl border border-white/10 bg-slate-950/40 p-6"> | |
| <p className="text-sm text-slate-400 text-center"> | |
| No tool invocations yet. Tools will appear here as they are executed. | |
| </p> | |
| </div> | |
| ); | |
| } | |
| const totalLatency = timelineData.reduce((sum, inv) => sum + inv.latency, 0); | |
| const maxLatency = Math.max(...timelineData.map((inv) => inv.latency), 0); | |
| return ( | |
| <div className="rounded-2xl border border-white/10 bg-slate-950/40 p-6"> | |
| <div className="mb-4 flex items-center justify-between"> | |
| <h3 className="text-lg font-semibold text-white">Tool Invocation Timeline</h3> | |
| <div className="text-xs text-slate-400"> | |
| <span className="text-cyan-300">{timelineData.length}</span> tools ·{" "} | |
| <span className="text-emerald-300">{totalLatency}ms</span> total | |
| </div> | |
| </div> | |
| {/* Timeline visualization */} | |
| <div className="relative mb-6 h-32 rounded-xl border border-white/10 bg-slate-900/50 p-4"> | |
| {/* Time axis */} | |
| <div className="absolute inset-x-4 top-2 flex justify-between text-xs text-slate-500"> | |
| <span>0ms</span> | |
| <span>{maxLatency}ms</span> | |
| </div> | |
| {/* Tool bars */} | |
| <div className="relative mt-8 h-full"> | |
| {timelineData.map((inv, idx) => { | |
| const height = 24; | |
| const top = idx * (height + 8); | |
| const color = | |
| inv.status === "error" | |
| ? "bg-red-500/60 border-red-400" | |
| : "bg-cyan-500/60 border-cyan-400"; | |
| return ( | |
| <div key={idx} className="absolute inset-x-0" style={{ top: `${top}px` }}> | |
| {/* Tool label */} | |
| <div className="absolute -left-24 top-0 flex h-6 w-20 items-center justify-end pr-2 text-xs text-slate-300"> | |
| <span className="truncate font-medium">{inv.tool}</span> | |
| </div> | |
| {/* Timeline bar */} | |
| <div | |
| className={`relative h-6 rounded border transition-all hover:opacity-100 ${ | |
| inv.status === "error" ? "opacity-80" : "opacity-70" | |
| } ${color}`} | |
| style={{ | |
| left: `${inv.startPercent}%`, | |
| width: `${Math.max(inv.widthPercent, 2)}%`, | |
| }} | |
| title={`${inv.tool}: ${inv.latency}ms (${inv.status})`} | |
| > | |
| {/* Latency label */} | |
| {inv.widthPercent > 5 && ( | |
| <span className="absolute inset-0 flex items-center justify-center text-[10px] font-semibold text-white"> | |
| {inv.latency}ms | |
| </span> | |
| )} | |
| {/* Result count badge */} | |
| {inv.resultCount > 0 && inv.widthPercent > 8 && ( | |
| <span className="absolute -right-2 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-emerald-500 text-[8px] font-bold text-white"> | |
| {inv.resultCount > 9 ? "9+" : inv.resultCount} | |
| </span> | |
| )} | |
| {/* Error indicator */} | |
| {inv.status === "error" && ( | |
| <span className="absolute -right-1 -top-1 text-red-400">⚠️</span> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Tool list */} | |
| <div className="space-y-2"> | |
| {timelineData.map((inv, idx) => ( | |
| <div | |
| key={idx} | |
| className="flex items-center justify-between rounded-lg border border-white/5 bg-white/5 px-4 py-2" | |
| > | |
| <div className="flex items-center gap-3"> | |
| <div | |
| className={`h-3 w-3 rounded-full ${ | |
| inv.status === "error" ? "bg-red-500" : "bg-emerald-500" | |
| }`} | |
| /> | |
| <span className="font-medium text-white">{inv.tool}</span> | |
| {inv.resultCount > 0 && ( | |
| <span className="text-xs text-slate-400"> | |
| {inv.resultCount} result{inv.resultCount !== 1 ? "s" : ""} | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-4 text-sm"> | |
| <span className="text-slate-400">{inv.latency}ms</span> | |
| {inv.status === "error" && ( | |
| <span className="text-red-400">Error</span> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Summary stats */} | |
| <div className="mt-4 grid grid-cols-3 gap-3 rounded-lg border border-white/5 bg-white/5 p-3"> | |
| <div className="text-center"> | |
| <p className="text-xs text-slate-400">Total Tools</p> | |
| <p className="text-lg font-semibold text-white">{timelineData.length}</p> | |
| </div> | |
| <div className="text-center"> | |
| <p className="text-xs text-slate-400">Total Time</p> | |
| <p className="text-lg font-semibold text-emerald-300">{totalLatency}ms</p> | |
| </div> | |
| <div className="text-center"> | |
| <p className="text-xs text-slate-400">Avg Latency</p> | |
| <p className="text-lg font-semibold text-cyan-300"> | |
| {Math.round(totalLatency / timelineData.length)}ms | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |