/* ── ThinkingPanel - Chain-of-Thought transparency ── */ import { useState, useRef, useEffect } from "react"; export interface ThinkingStep { id: string; timestamp: number; type: "reasoning" | "observation" | "decision" | "tool_call" | "error"; title: string; content: string; duration?: number; } interface ThinkingPanelProps { steps: ThinkingStep[]; collapsed?: boolean; onToggleCollapse?: () => void; } const STEP_ICONS: Record = { reasoning: ( ), observation: ( ), decision: ( ), tool_call: ( ), error: ( ), }; const STEP_COLORS: Record = { reasoning: "var(--accent)", observation: "var(--diff-add-text)", decision: "var(--color-gold-400)", tool_call: "var(--text-subtle)", error: "var(--diff-del-text)", }; function formatTime(ts: number): string { const d = new Date(ts); return d.toLocaleTimeString([], { minute: "2-digit", second: "2-digit", fractionalSecondDigits: 1 }); } export function ThinkingPanel({ steps, collapsed = false, onToggleCollapse }: ThinkingPanelProps) { const [expandedStep, setExpandedStep] = useState(null); const [autoScroll, setAutoScroll] = useState(true); const listRef = useRef(null); // Auto-scroll to bottom when new steps arrive useEffect(() => { if (autoScroll && listRef.current) { listRef.current.scrollTop = listRef.current.scrollHeight; } }, [steps.length, autoScroll]); // Auto-expand the latest step useEffect(() => { if (steps.length > 0) { setExpandedStep(steps[steps.length - 1].id); } }, [steps.length]); if (steps.length === 0) return null; return (
{/* Header */}
{/* Steps */} {!collapsed && (
{ const el = e.currentTarget; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; if (autoScroll !== atBottom) { setAutoScroll(atBottom); } }} > {steps.map((step, idx) => { const isExpanded = expandedStep === step.id; const color = STEP_COLORS[step.type]; const isLast = idx === steps.length - 1; return (
{/* Timeline */}
{STEP_ICONS[step.type]}
{!isLast && (
)}
{/* Content */}
{isExpanded && step.content && (
{step.content}
)}
); })}
)}
); }