"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 } | { type: "thinking"; content: string } | { type: "text_delta"; content: string } | { type: "tool_use"; id: string; name: string; input: Record; confidence?: number; } | { type: "tool_result"; id: string; name: string; output: Record; reward?: number; done?: boolean; } | { type: "decision"; decision: string; confidence: number; reason_codes: string[]; policy_checks: Record; notes?: string; } | { type: "result"; final_score: number; reward: number; info: Record; observation: Record; trust_graph?: Record; } | { type: "error"; error: string } | { type: "done" }; interface AgentStreamProps { events: StreamEvent[]; isStreaming?: boolean; onResultRevealed?: (result: { final_score: number; reward: number; info: Record; }) => 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; output?: Record; text?: string; reward?: number; confidence?: number; decision?: { decision: string; confidence: number; reason_codes: string[]; policy_checks: Record; notes?: string; }; result?: { final_score: number; reward: number; info: Record; }; error?: string; observation?: Record; } function ShimmerText({ children, className }: { children: React.ReactNode; className?: string }) { return ( {children} ); } 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 (
{open && (
          {text}
        
)}
); } const decisionStyles: Record = { 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 ( conf {safe.toFixed(2)} ); } const REVEAL_DELAY_MS: Record = { 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>(new Map()); const containerRef = useRef(null); const scrollParentRef = useRef(null); const pinnedRef = useRef(true); const programmaticScrollRef = useRef(false); const resultFiredRef = useRef(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 (
0 ? 1 : 0 }} transition={{ duration: 0.4, ease: "easeOut" }} />
{items.map((item) => (
))}
{streamingText && (
                  {streamingText}
                  {showCursor && (
                    
                  )}
                
)}
{!showCursor && items.length === 0 && (
Waiting for agent to start...
)}
); } function TimelineMarker({ item }: { item: TimelineItem }) { return (
{item.status === "pending" ? ( ) : item.status === "error" ? ( ) : item.kind === "decision" ? ( ) : item.kind === "result" ? ( ) : ( )}
); } function TimelineBody({ item }: { item: TimelineItem }) { const labelIcon = (() => { switch (item.kind) { case "tool_use": case "tool_result": return ; case "thinking": return ; case "error": return ; case "decision": return ; case "result": return ; case "observation": return ; default: return
; } })(); return (
{labelIcon} {item.kind === "tool_use" || item.kind === "tool_result" ? ( {item.toolName || item.title} ) : ( {item.title} )} {(item.kind === "tool_use" || item.kind === "tool_result") && typeof item.confidence === "number" && ( )} {typeof item.reward === "number" && item.reward !== 0 && ( 0 ? "text-emerald-400 bg-emerald-500/10" : "text-red-400 bg-red-500/10" }`} > {item.reward > 0 ? "+" : ""} {item.reward.toFixed(2)} )}
{item.kind === "tool_use" && item.input && ( )} {item.kind === "tool_result" && item.output && ( )} {item.kind === "observation" && item.observation && ( )} {item.kind === "thinking" && item.text && (

{item.text}

)} {item.kind === "error" && item.error && (

{item.error}

)} {item.kind === "decision" && item.decision && (
{item.decision.decision} · conf {item.decision.confidence?.toFixed?.(2)}
{item.decision.reason_codes?.length > 0 && (
{item.decision.reason_codes.map((code) => ( {code} ))}
)} {item.decision.notes && (

{item.decision.notes}

)} {item.decision.policy_checks && Object.keys(item.decision.policy_checks).length > 0 && ( )}
)} {item.kind === "result" && item.result && (
Final score:{" "} {item.result.final_score.toFixed(3)} {" · "} Reward:{" "} {item.result.reward.toFixed(3)}
)}
); } function summarizeObs(obs: Record): Record { 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) || {}; 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;