IntegraChat / frontend /components /tool-timeline.tsx
nothingworry's picture
feat: Add real-time reasoning visualizer, tool timeline, and tenant heatmap components
1d2a779
raw
history blame
7.25 kB
"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>
);
}