shreayas's picture
Sync README judge audit path and training evidence
b35b5a6 verified
Raw
History Blame Contribute Delete
22.1 kB
"use client";
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
CheckCircle,
XCircle,
Brain,
Wrench,
FileText,
Warning,
Gavel,
Sparkle,
CaretDown,
CaretRight,
} from "@phosphor-icons/react";
export type StreamEvent =
| { type: "status"; content: string }
| { type: "observation"; observation: Record<string, unknown> }
| { type: "thinking"; content: string }
| { type: "text_delta"; content: string }
| {
type: "tool_use";
id: string;
name: string;
input: Record<string, unknown>;
confidence?: number;
}
| {
type: "tool_result";
id: string;
name: string;
output: Record<string, unknown>;
reward?: number;
done?: boolean;
}
| {
type: "decision";
decision: string;
confidence: number;
reason_codes: string[];
policy_checks: Record<string, string>;
notes?: string;
}
| {
type: "result";
final_score: number;
reward: number;
info: Record<string, unknown>;
observation: Record<string, unknown>;
trust_graph?: Record<string, unknown>;
}
| { type: "error"; error: string }
| { type: "done" };
interface AgentStreamProps {
events: StreamEvent[];
isStreaming?: boolean;
onResultRevealed?: (result: {
final_score: number;
reward: number;
info: Record<string, unknown>;
}) => void;
}
interface TimelineItem {
id: string;
kind:
| "status"
| "observation"
| "thinking"
| "tool_use"
| "tool_result"
| "text_delta"
| "decision"
| "result"
| "error";
title: string;
status: "pending" | "done" | "error";
toolName?: string;
input?: Record<string, unknown>;
output?: Record<string, unknown>;
text?: string;
reward?: number;
confidence?: number;
decision?: {
decision: string;
confidence: number;
reason_codes: string[];
policy_checks: Record<string, string>;
notes?: string;
};
result?: {
final_score: number;
reward: number;
info: Record<string, unknown>;
};
error?: string;
observation?: Record<string, unknown>;
}
function ShimmerText({ children, className }: { children: React.ReactNode; className?: string }) {
return (
<motion.span
className={`relative inline-block text-transparent bg-clip-text bg-gradient-to-r from-zinc-100 via-white to-zinc-100 ${className || ""}`}
animate={{ backgroundPosition: ["200% center", "-200% center"] }}
transition={{ duration: 2, ease: "linear", repeat: Infinity }}
style={{ backgroundSize: "200% auto" }}
>
{children}
</motion.span>
);
}
function JsonBlock({ value, label }: { value: unknown; label?: string }) {
const [open, setOpen] = useState(false);
const text = JSON.stringify(value, null, 2);
const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
if (text === "{}" || text === "null") return null;
return (
<div className="mt-2">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex items-center gap-1 text-[10px] font-mono text-zinc-500 hover:text-zinc-300 transition-colors"
>
{open ? <CaretDown size={10} /> : <CaretRight size={10} />}
<span>{label || "data"}</span>
{!open && <span className="ml-1 text-zinc-600">{preview}</span>}
</button>
{open && (
<pre className="mt-1.5 text-[10px] font-mono text-zinc-300 bg-zinc-900/60 p-2 rounded border border-white/5 overflow-x-auto max-h-60">
{text}
</pre>
)}
</div>
);
}
const decisionStyles: Record<string, string> = {
PAY: "bg-emerald-500/15 border-emerald-500/30 text-emerald-300",
HOLD: "bg-yellow-500/15 border-yellow-500/30 text-yellow-300",
NEEDS_REVIEW: "bg-blue-500/15 border-blue-500/30 text-blue-300",
ESCALATE_FRAUD: "bg-red-500/15 border-red-500/30 text-red-300",
};
function confidenceTone(value: number): string {
if (value >= 0.85) return "text-emerald-300 bg-emerald-500/10 border-emerald-500/20";
if (value >= 0.6) return "text-cyan-300 bg-cyan-500/10 border-cyan-500/20";
if (value >= 0.4) return "text-amber-300 bg-amber-500/10 border-amber-500/20";
return "text-red-300 bg-red-500/10 border-red-500/20";
}
function ConfidenceChip({ value }: { value: number }) {
const safe = Math.min(1, Math.max(0, value));
const tone = confidenceTone(safe);
return (
<span
title={`Agent confidence in this tool call: ${(safe * 100).toFixed(0)}%`}
className={`inline-flex items-center gap-1 text-[10px] font-mono px-1.5 py-0.5 rounded border ${tone}`}
>
<span className="opacity-70">conf</span>
<span>{safe.toFixed(2)}</span>
</span>
);
}
const REVEAL_DELAY_MS: Record<StreamEvent["type"], number> = {
status: 380,
observation: 720,
thinking: 520,
text_delta: 22,
tool_use: 620,
tool_result: 1100,
decision: 900,
result: 1100,
error: 0,
done: 0,
};
export function AgentStream({
events,
isStreaming = false,
onResultRevealed,
}: AgentStreamProps) {
const [lineTargetY, setLineTargetY] = useState(0);
const [revealedCount, setRevealedCount] = useState(0);
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const containerRef = useRef<HTMLDivElement>(null);
const scrollParentRef = useRef<HTMLElement | null>(null);
const pinnedRef = useRef(true);
const programmaticScrollRef = useRef(false);
const resultFiredRef = useRef<string | null>(null);
useEffect(() => {
if (revealedCount > events.length) {
const reset = setTimeout(() => setRevealedCount(events.length), 0);
return () => clearTimeout(reset);
}
if (revealedCount >= events.length) return;
const nextEvent = events[revealedCount];
const delay = REVEAL_DELAY_MS[nextEvent.type] ?? 200;
const t = setTimeout(() => {
setRevealedCount((c) => Math.min(c + 1, events.length));
}, delay);
return () => clearTimeout(t);
}, [events, revealedCount]);
const visibleEvents = useMemo(
() => events.slice(0, Math.min(revealedCount, events.length)),
[events, revealedCount],
);
const { items, streamingText } = useMemo(() => {
let nextItems: TimelineItem[] = [];
let nextText = "";
for (const event of visibleEvents) {
nextItems = applyEvent(nextItems, event, (text) => {
nextText += text;
});
}
return { items: nextItems, streamingText: nextText };
}, [visibleEvents]);
const isCatchingUp = revealedCount < events.length;
const showCursor = isStreaming || isCatchingUp;
useEffect(() => {
if (items.length > 0 && containerRef.current) {
const lastItem = items[items.length - 1];
const el = itemRefs.current.get(lastItem.id);
if (el) {
const containerRect = containerRef.current.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
setLineTargetY(elRect.top - containerRect.top + elRect.height / 2);
}
}
}, [items.length, items]);
useEffect(() => {
let node: HTMLElement | null = containerRef.current?.parentElement ?? null;
while (node) {
const style = getComputedStyle(node);
if (/auto|scroll|overlay/.test(style.overflowY)) {
scrollParentRef.current = node;
break;
}
node = node.parentElement;
}
const scroller = scrollParentRef.current;
if (!scroller) return;
const onScroll = () => {
if (programmaticScrollRef.current) {
programmaticScrollRef.current = false;
return;
}
const distance =
scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
pinnedRef.current = distance < 48;
};
scroller.addEventListener("scroll", onScroll, { passive: true });
return () => scroller.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
if (!pinnedRef.current) return;
const scroller = scrollParentRef.current;
if (!scroller) return;
programmaticScrollRef.current = true;
scroller.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" });
}, [items.length, streamingText]);
useEffect(() => {
const resultItem = items.find((it) => it.kind === "result");
if (!resultItem) {
resultFiredRef.current = null;
return;
}
if (resultFiredRef.current === resultItem.id) return;
resultFiredRef.current = resultItem.id;
if (resultItem.result) {
onResultRevealed?.(resultItem.result);
}
}, [items, onResultRevealed]);
const setItemRef = useCallback(
(id: string) => (el: HTMLDivElement | null) => {
if (el) itemRefs.current.set(id, el);
},
[],
);
return (
<div className="space-y-0" ref={containerRef}>
<div className="relative">
<motion.div
className="absolute left-[11px] top-0 w-px border-l border-dashed border-zinc-600 origin-top"
animate={{ height: lineTargetY, opacity: items.length > 0 ? 1 : 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
/>
<div className="space-y-0">
{items.map((item) => (
<motion.div
key={item.id}
ref={setItemRef(item.id)}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, ease: "easeOut" }}
className="relative flex items-start gap-3 py-2"
>
<TimelineMarker item={item} />
<div className="flex-1 min-w-0 pt-0.5">
<TimelineBody item={item} />
</div>
</motion.div>
))}
</div>
<AnimatePresence>
{streamingText && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="relative flex items-start gap-3 py-3"
>
<div className="relative z-10 flex-shrink-0 w-6 h-6 flex items-center justify-center">
<FileText size={18} className="text-emerald-400" />
</div>
<div className="flex-1 pt-0.5">
<pre className="text-sm text-zinc-100 font-sans whitespace-pre-wrap leading-relaxed">
{streamingText}
{showCursor && (
<motion.span
animate={{ opacity: [1, 0] }}
transition={{ duration: 0.8, repeat: Infinity }}
className="inline-block w-2 h-4 bg-emerald-400 ml-1 align-middle"
/>
)}
</pre>
</div>
</motion.div>
)}
</AnimatePresence>
{!showCursor && items.length === 0 && (
<div className="text-center py-8 text-sm text-zinc-500">
Waiting for agent to start...
</div>
)}
</div>
</div>
);
}
function TimelineMarker({ item }: { item: TimelineItem }) {
return (
<div className="relative z-10 flex-shrink-0 w-6 h-6 flex items-center justify-center">
<AnimatePresence mode="wait">
{item.status === "pending" ? (
<motion.div
key="pending"
initial={{ scale: 0 }}
animate={{ scale: [0.6, 1, 0.6] }}
transition={{ duration: 1.4, repeat: Infinity }}
className="w-2 h-2 rounded-full bg-zinc-400"
/>
) : item.status === "error" ? (
<motion.div
key="error"
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ duration: 0.3, type: "spring" }}
>
<XCircle size={18} className="text-red-400" weight="fill" />
</motion.div>
) : item.kind === "decision" ? (
<motion.div
key="decision"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, type: "spring" }}
>
<Gavel size={18} className="text-purple-400" weight="fill" />
</motion.div>
) : item.kind === "result" ? (
<motion.div
key="result"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, type: "spring" }}
>
<Sparkle size={18} className="text-amber-300" weight="fill" />
</motion.div>
) : (
<motion.div
key="done"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.25, type: "spring", stiffness: 500 }}
>
<CheckCircle
size={18}
className={
item.kind === "tool_use" || item.kind === "tool_result"
? "text-orange-400"
: "text-emerald-500"
}
weight="fill"
/>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function TimelineBody({ item }: { item: TimelineItem }) {
const labelIcon = (() => {
switch (item.kind) {
case "tool_use":
case "tool_result":
return <Wrench size={12} className="text-orange-400" />;
case "thinking":
return <Brain size={12} className="text-zinc-400" />;
case "error":
return <Warning size={12} className="text-red-400" />;
case "decision":
return <Gavel size={12} className="text-purple-300" />;
case "result":
return <Sparkle size={12} className="text-amber-300" />;
case "observation":
return <FileText size={12} className="text-emerald-300" />;
default:
return <div className="w-1.5 h-1.5 rounded-full bg-zinc-500" />;
}
})();
return (
<div>
<div className="flex items-center gap-2 flex-wrap">
{labelIcon}
{item.kind === "tool_use" || item.kind === "tool_result" ? (
<ShimmerText className="text-sm text-zinc-100">
{item.toolName || item.title}
</ShimmerText>
) : (
<span
className={`text-sm ${
item.kind === "error"
? "text-red-400"
: item.kind === "decision"
? "text-purple-200"
: item.kind === "result"
? "text-amber-200"
: "text-zinc-200"
}`}
>
{item.title}
</span>
)}
{(item.kind === "tool_use" || item.kind === "tool_result") &&
typeof item.confidence === "number" && (
<ConfidenceChip value={item.confidence} />
)}
{typeof item.reward === "number" && item.reward !== 0 && (
<span
className={`text-[10px] font-mono px-1.5 py-0.5 rounded ${
item.reward > 0
? "text-emerald-400 bg-emerald-500/10"
: "text-red-400 bg-red-500/10"
}`}
>
{item.reward > 0 ? "+" : ""}
{item.reward.toFixed(2)}
</span>
)}
</div>
{item.kind === "tool_use" && item.input && (
<JsonBlock label="input" value={item.input} />
)}
{item.kind === "tool_result" && item.output && (
<JsonBlock label="output" value={item.output} />
)}
{item.kind === "observation" && item.observation && (
<JsonBlock label="observation" value={summarizeObs(item.observation)} />
)}
{item.kind === "thinking" && item.text && (
<p className="text-xs text-zinc-400 mt-1 italic leading-relaxed">{item.text}</p>
)}
{item.kind === "error" && item.error && (
<p className="text-xs text-red-400 mt-1 leading-relaxed">{item.error}</p>
)}
{item.kind === "decision" && item.decision && (
<div className="mt-2 space-y-2">
<div
className={`inline-flex items-center gap-2 px-3 py-1 rounded-md border text-xs font-semibold ${
decisionStyles[item.decision.decision] ||
"bg-zinc-500/15 border-zinc-500/30 text-zinc-300"
}`}
>
{item.decision.decision}
<span className="font-mono text-[10px] opacity-70">
· conf {item.decision.confidence?.toFixed?.(2)}
</span>
</div>
{item.decision.reason_codes?.length > 0 && (
<div className="flex flex-wrap gap-1">
{item.decision.reason_codes.map((code) => (
<span
key={code}
className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-zinc-300 border border-white/10"
>
{code}
</span>
))}
</div>
)}
{item.decision.notes && (
<p className="text-xs text-zinc-400 italic leading-relaxed">
{item.decision.notes}
</p>
)}
{item.decision.policy_checks &&
Object.keys(item.decision.policy_checks).length > 0 && (
<JsonBlock label="policy_checks" value={item.decision.policy_checks} />
)}
</div>
)}
{item.kind === "result" && item.result && (
<div className="mt-2 space-y-1">
<div className="text-xs text-zinc-400">
Final score:{" "}
<span className="font-mono text-amber-300">
{item.result.final_score.toFixed(3)}
</span>
{" · "}
Reward:{" "}
<span className="font-mono">
{item.result.reward.toFixed(3)}
</span>
</div>
<JsonBlock label="info" value={item.result.info} />
</div>
)}
</div>
);
}
function summarizeObs(obs: Record<string, unknown>): Record<string, unknown> {
const docs = Array.isArray(obs.visible_documents) ? obs.visible_documents : [];
return {
case_id: obs.case_id,
task_type: obs.task_type,
instruction: obs.instruction,
visible_documents: docs.map((d) => {
const doc = (d as Record<string, unknown>) || {};
return { doc_id: doc.doc_id, doc_type: doc.doc_type };
}),
budget_remaining: obs.budget_remaining,
budget_total: obs.budget_total,
step_count: obs.step_count,
max_steps: obs.max_steps,
allowed_actions: obs.allowed_actions,
};
}
function applyEvent(
prev: TimelineItem[],
event: StreamEvent,
appendText: (text: string) => void,
): TimelineItem[] {
const closePending = (items: TimelineItem[]): TimelineItem[] =>
items.map((it) =>
it.kind === "status" && it.status === "pending"
? { ...it, status: "done" as const }
: it,
);
switch (event.type) {
case "status": {
const next = closePending(prev);
return [
...next,
{
id: `status-${next.length}`,
kind: "status",
title: event.content,
status: "pending",
},
];
}
case "observation": {
const next = closePending(prev);
return [
...next,
{
id: `obs-${next.length}`,
kind: "observation",
title: `Observation · ${event.observation.case_id}`,
status: "done",
observation: event.observation,
},
];
}
case "thinking": {
const next = closePending(prev);
return [
...next,
{
id: `think-${next.length}`,
kind: "thinking",
title: "Thinking",
text: event.content,
status: "done",
},
];
}
case "text_delta": {
appendText(event.content);
return closePending(prev);
}
case "tool_use": {
const next = closePending(prev);
return [
...next,
{
id: event.id,
kind: "tool_use",
title: event.name,
toolName: event.name,
input: event.input,
confidence: event.confidence,
status: "pending",
},
];
}
case "tool_result": {
const idx = prev.findIndex((it) => it.id === event.id);
if (idx === -1) {
return [
...closePending(prev),
{
id: `${event.id}-result`,
kind: "tool_result",
title: event.name,
toolName: event.name,
output: event.output,
reward: event.reward,
status: "done",
},
];
}
const next = [...prev];
next[idx] = {
...next[idx],
kind: "tool_result",
output: event.output,
reward: event.reward,
status: "done",
};
return next;
}
case "decision": {
const next = closePending(prev);
return [
...next,
{
id: `decision-${next.length}`,
kind: "decision",
title: "Decision submitted",
status: "done",
decision: {
decision: event.decision,
confidence: event.confidence,
reason_codes: event.reason_codes,
policy_checks: event.policy_checks,
notes: event.notes,
},
},
];
}
case "result": {
const next = closePending(prev);
return [
...next,
{
id: `result-${next.length}`,
kind: "result",
title: "Episode complete",
status: "done",
result: {
final_score: event.final_score,
reward: event.reward,
info: event.info,
},
},
];
}
case "error": {
const next = closePending(prev);
return [
...next,
{
id: `error-${next.length}`,
kind: "error",
title: "Error",
error: event.error,
status: "error",
},
];
}
case "done":
return closePending(prev);
default:
return prev;
}
}
export default AgentStream;