Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useState } from "react"; | |
| import { | |
| Pulse, | |
| Gauge, | |
| Coins, | |
| WarningCircle, | |
| Calculator, | |
| ChartLine, | |
| Eye, | |
| } from "@phosphor-icons/react"; | |
| import type { StepMath } from "@/lib/agent/diagnostics"; | |
| import { DocumentPreviewModal, type PreviewDoc } from "@/components/DocumentPreviewModal"; | |
| function asRecord(v: unknown): Record<string, unknown> | null { | |
| return v && typeof v === "object" && !Array.isArray(v) | |
| ? (v as Record<string, unknown>) | |
| : null; | |
| } | |
| function asString(v: unknown): string { | |
| return typeof v === "string" ? v : ""; | |
| } | |
| function asNumber(v: unknown): number | null { | |
| return typeof v === "number" && Number.isFinite(v) ? v : null; | |
| } | |
| const COMPONENT_PRIORITY = [ | |
| "voi_reward", | |
| "information_value", | |
| "cost_penalty", | |
| "failure_penalty", | |
| "potential_delta", | |
| "milestone_bonus", | |
| "reward_machine_bonus", | |
| ]; | |
| function sortComponentEntries(entries: [string, number][]): [string, number][] { | |
| const rank = (k: string) => { | |
| const i = COMPONENT_PRIORITY.indexOf(k); | |
| return i === -1 ? 999 : i; | |
| }; | |
| return [...entries].sort((a, b) => rank(a[0]) - rank(b[0]) || a[0].localeCompare(b[0])); | |
| } | |
| export function EnvironmentPanel({ | |
| observation, | |
| cumulativeReward, | |
| stepReward, | |
| stepMath, | |
| isRunning, | |
| }: { | |
| observation: Record<string, unknown> | null; | |
| cumulativeReward: number; | |
| stepReward: number | null; | |
| stepMath: StepMath | null; | |
| isRunning: boolean; | |
| }) { | |
| if (!observation) { | |
| return ( | |
| <div className="rounded-xl border border-white/10 bg-black/40 p-4 text-xs text-zinc-500"> | |
| <p className="font-mono uppercase tracking-[0.2em] text-[10px] text-zinc-600 mb-2"> | |
| Environment | |
| </p> | |
| <p>Start a run to stream live state from LedgerShield.</p> | |
| </div> | |
| ); | |
| } | |
| const caseId = asString(observation.case_id); | |
| const taskType = asString(observation.task_type); | |
| const instruction = asString(observation.instruction); | |
| const step = asNumber(observation.step_count) ?? 0; | |
| const maxSteps = asNumber(observation.max_steps) ?? 0; | |
| const budgetRem = asNumber(observation.budget_remaining); | |
| const budgetTot = asNumber(observation.budget_total); | |
| const readiness = asNumber(observation.decision_readiness); | |
| const meta = asRecord(observation.case_metadata); | |
| const trackMode = meta ? asString(meta.track_mode) : ""; | |
| const risk = asRecord(observation.risk_snapshot); | |
| const signals = Array.isArray(risk?.observed_risk_signals) | |
| ? (risk!.observed_risk_signals as unknown[]) | |
| : []; | |
| const rankings = observation.tool_rankings; | |
| const inv = asRecord(observation.investigation_status); | |
| const docs = Array.isArray(observation.visible_documents) | |
| ? observation.visible_documents | |
| : []; | |
| const lastTool = asRecord(observation.last_tool_result); | |
| const lastToolName = lastTool ? asString(lastTool.tool_name) : ""; | |
| const sprt = asRecord(observation.sprt_state); | |
| const posteriors = sprt?.posterior_probabilities; | |
| const posteriorEntries = | |
| posteriors && typeof posteriors === "object" | |
| ? Object.entries(posteriors as Record<string, unknown>).filter( | |
| ([, v]) => typeof v === "number", | |
| ) | |
| : []; | |
| const sprtRecommend = sprt ? asString(sprt.recommended_decision) : ""; | |
| const sprtStop = sprt ? Boolean(sprt.optimal_stopping_reached) : false; | |
| const rmState = asRecord(observation.reward_machine); | |
| const rmProgress = rmState ? asNumber(rmState.progress_fraction) : null; | |
| const rmStateId = rmState ? asNumber(rmState.state_id) : null; | |
| const [previewDoc, setPreviewDoc] = useState<PreviewDoc | null>(null); | |
| const rankingEntries = | |
| rankings && typeof rankings === "object" && !Array.isArray(rankings) | |
| ? Object.entries(rankings as Record<string, unknown>).filter( | |
| ([, v]) => typeof v === "number", | |
| ) | |
| : []; | |
| return ( | |
| <div className="rounded-xl border border-white/10 bg-black/40 overflow-hidden"> | |
| <div className="flex items-center justify-between px-4 py-3 border-b border-white/10 bg-white/[0.02]"> | |
| <div className="flex items-center gap-2"> | |
| <Pulse | |
| size={16} | |
| className={isRunning ? "text-emerald-400 animate-pulse" : "text-zinc-500"} | |
| weight="fill" | |
| /> | |
| <span className="text-[10px] font-mono uppercase tracking-[0.2em] text-zinc-400"> | |
| Environment | |
| </span> | |
| </div> | |
| {trackMode && ( | |
| <span className="text-[9px] font-mono px-2 py-0.5 rounded border border-white/10 text-zinc-500"> | |
| {trackMode} | |
| </span> | |
| )} | |
| </div> | |
| <div className="p-4 space-y-4 max-h-[min(72vh,640px)] overflow-y-auto text-xs"> | |
| <div> | |
| <p className="text-[10px] font-mono text-zinc-500 mb-1">Case</p> | |
| <p className="font-mono text-emerald-300/90">{caseId || "—"}</p> | |
| <p className="text-zinc-400 mt-0.5">{taskType}</p> | |
| {instruction && ( | |
| <p className="text-zinc-500 mt-2 leading-snug line-clamp-3">{instruction}</p> | |
| )} | |
| </div> | |
| <div className="grid grid-cols-2 gap-2"> | |
| <div className="rounded-lg border border-white/10 bg-white/[0.03] p-2.5"> | |
| <div className="flex items-center gap-1 text-[10px] text-zinc-500 mb-1"> | |
| <Gauge size={12} /> Step | |
| </div> | |
| <p className="font-mono text-zinc-200"> | |
| {step} / {maxSteps} | |
| </p> | |
| </div> | |
| <div className="rounded-lg border border-white/10 bg-white/[0.03] p-2.5"> | |
| <div className="flex items-center gap-1 text-[10px] text-zinc-500 mb-1"> | |
| <Coins size={12} /> Budget | |
| </div> | |
| <p className="font-mono text-zinc-200"> | |
| {budgetRem != null && budgetTot != null | |
| ? `${budgetRem.toFixed(2)} / ${budgetTot.toFixed(2)}` | |
| : "—"} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="rounded-lg border border-white/10 bg-white/[0.03] p-2.5"> | |
| <p className="text-[10px] text-zinc-500 mb-1">Live reward (env)</p> | |
| <div className="flex items-baseline gap-2 flex-wrap"> | |
| <span className="font-mono text-amber-300 text-sm"> | |
| Σ {cumulativeReward.toFixed(3)} | |
| </span> | |
| {stepReward != null && ( | |
| <span className="text-[10px] font-mono text-zinc-500"> | |
| last Δ {stepReward >= 0 ? "+" : ""} | |
| {stepReward.toFixed(3)} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| {(stepMath?.reward_model || stepMath?.rl_plane) && ( | |
| <div className="rounded-lg border border-cyan-500/20 bg-cyan-500/[0.06] p-2.5 space-y-2"> | |
| <div className="flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-[0.15em] text-cyan-400/90"> | |
| <Calculator size={14} /> | |
| VoI & step math | |
| </div> | |
| {stepMath.reward_model && ( | |
| <> | |
| <div className="flex justify-between gap-2 text-[10px]"> | |
| <span className="text-zinc-500">Reward model value</span> | |
| <span className="font-mono text-cyan-200"> | |
| {stepMath.reward_model.value.toFixed(4)} | |
| {stepMath.reward_model.terminal ? ( | |
| <span className="text-zinc-500 ml-1">· terminal</span> | |
| ) : null} | |
| </span> | |
| </div> | |
| {sortComponentEntries( | |
| Object.entries(stepMath.reward_model.components), | |
| ).length > 0 && ( | |
| <div className="space-y-1 max-h-36 overflow-y-auto"> | |
| {sortComponentEntries( | |
| Object.entries(stepMath.reward_model.components), | |
| ).map(([k, v]) => ( | |
| <div | |
| key={k} | |
| className="flex justify-between gap-2 text-[10px] font-mono border border-white/5 rounded px-2 py-1 bg-black/20" | |
| > | |
| <span | |
| className={ | |
| k === "voi_reward" || k === "information_value" | |
| ? "text-cyan-300 truncate" | |
| : "text-zinc-400 truncate" | |
| } | |
| > | |
| {k} | |
| </span> | |
| <span | |
| className={ | |
| k === "voi_reward" | |
| ? "text-emerald-300 shrink-0" | |
| : "text-zinc-200 shrink-0" | |
| } | |
| > | |
| {v.toFixed(4)} | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {Object.keys(stepMath.reward_model.metadata).length > 0 && ( | |
| <div className="flex flex-wrap gap-1"> | |
| {Object.entries(stepMath.reward_model.metadata).map( | |
| ([k, v]) => ( | |
| <span | |
| key={k} | |
| className="text-[9px] font-mono px-1.5 py-0.5 rounded bg-zinc-900/80 border border-white/10 text-zinc-400 max-w-full truncate" | |
| title={`${k}: ${String(v)}`} | |
| > | |
| {k}: {String(v)} | |
| </span> | |
| ), | |
| )} | |
| </div> | |
| )} | |
| </> | |
| )} | |
| {stepMath.rl_plane && stepMath.rl_plane.state_dim > 0 && ( | |
| <div className="pt-1 border-t border-white/10 space-y-1.5"> | |
| <div className="flex items-center gap-1 text-[10px] text-zinc-500"> | |
| <ChartLine size={12} /> RL state vector | |
| </div> | |
| <p className="text-[10px] font-mono text-zinc-400"> | |
| dim {stepMath.rl_plane.state_dim} | |
| {stepMath.rl_plane.best_tool_voi != null && ( | |
| <span className="text-cyan-300 ml-2"> | |
| best_tool_voi (vec):{" "} | |
| {stepMath.rl_plane.best_tool_voi.toFixed(4)} | |
| </span> | |
| )} | |
| </p> | |
| {(stepMath.rl_plane.watchdog != null || | |
| stepMath.rl_plane.calibration != null) && ( | |
| <div className="flex flex-wrap gap-2 text-[9px] font-mono text-zinc-500"> | |
| {stepMath.rl_plane.watchdog != null && ( | |
| <span> | |
| watchdog:{" "} | |
| <span className="text-zinc-300"> | |
| {stepMath.rl_plane.watchdog.toFixed(4)} | |
| </span> | |
| </span> | |
| )} | |
| {stepMath.rl_plane.calibration != null && ( | |
| <span> | |
| calibration:{" "} | |
| <span className="text-zinc-300"> | |
| {stepMath.rl_plane.calibration.toFixed(4)} | |
| </span> | |
| </span> | |
| )} | |
| </div> | |
| )} | |
| {stepMath.rl_plane.preview.length > 0 && ( | |
| <p className="text-[9px] font-mono text-zinc-600 break-all leading-relaxed"> | |
| head: [{stepMath.rl_plane.preview.map((x) => x.toFixed(3)).join(", ")}] | |
| </p> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {posteriorEntries.length > 0 && ( | |
| <div className="rounded-lg border border-violet-500/20 bg-violet-500/[0.06] p-2.5"> | |
| <p className="text-[10px] font-mono text-violet-300/90 mb-2 uppercase tracking-[0.15em]"> | |
| SPRT posteriors | |
| </p> | |
| <div className="space-y-1 max-h-32 overflow-y-auto"> | |
| {posteriorEntries.map(([k, v]) => ( | |
| <div | |
| key={k} | |
| className="flex justify-between gap-2 text-[10px] font-mono" | |
| > | |
| <span className="text-zinc-400 truncate">{k}</span> | |
| <span className="text-violet-200 shrink-0"> | |
| {((v as number) * 100).toFixed(1)}% | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| {(sprtRecommend || sprtStop) && ( | |
| <p className="text-[9px] text-zinc-500 mt-2 font-mono"> | |
| {sprtRecommend && <>rec: {sprtRecommend} · </>} | |
| {sprtStop ? "stop reached" : "stop open"} | |
| </p> | |
| )} | |
| </div> | |
| )} | |
| {(rmProgress != null || rmStateId != null) && ( | |
| <div className="rounded-lg border border-amber-500/20 bg-amber-500/[0.06] p-2.5"> | |
| <p className="text-[10px] font-mono text-amber-200/90 mb-1 uppercase tracking-[0.15em]"> | |
| Reward machine | |
| </p> | |
| {rmProgress != null && ( | |
| <div className="h-1.5 rounded-full bg-white/10 overflow-hidden mb-1"> | |
| <div | |
| className="h-full rounded-full bg-amber-500/70" | |
| style={{ | |
| width: `${Math.min(100, rmProgress * 100)}%`, | |
| }} | |
| /> | |
| </div> | |
| )} | |
| <p className="text-[10px] font-mono text-zinc-400"> | |
| progress {(rmProgress != null ? rmProgress * 100 : 0).toFixed(1)}% | |
| {rmStateId != null && ( | |
| <span className="text-zinc-500"> · state {rmStateId}</span> | |
| )} | |
| </p> | |
| </div> | |
| )} | |
| {readiness != null && ( | |
| <div> | |
| <p className="text-[10px] text-zinc-500 mb-1">Decision readiness</p> | |
| <div className="h-1.5 rounded-full bg-white/10 overflow-hidden"> | |
| <div | |
| className="h-full rounded-full bg-cyan-500/80 transition-all duration-300" | |
| style={{ width: `${Math.min(100, readiness * 100)}%` }} | |
| /> | |
| </div> | |
| <p className="text-[10px] font-mono text-zinc-500 mt-1"> | |
| {(readiness * 100).toFixed(1)}% | |
| </p> | |
| </div> | |
| )} | |
| <div> | |
| <p className="text-[10px] text-zinc-500 mb-1 flex items-center gap-1"> | |
| <WarningCircle size={12} /> Risk signals ({signals.length}) | |
| </p> | |
| {signals.length === 0 ? ( | |
| <p className="text-zinc-600 italic">None observed yet</p> | |
| ) : ( | |
| <ul className="space-y-1 max-h-24 overflow-y-auto"> | |
| {signals.slice(0, 12).map((s, i) => ( | |
| <li | |
| key={i} | |
| className="text-[10px] font-mono text-amber-200/80 bg-amber-500/10 border border-amber-500/20 rounded px-2 py-1" | |
| > | |
| {String(s)} | |
| </li> | |
| ))} | |
| </ul> | |
| )} | |
| </div> | |
| {rankingEntries.length > 0 ? ( | |
| <div> | |
| <p className="text-[10px] text-zinc-500 mb-1">Tool rankings (instrumented)</p> | |
| <div className="space-y-1 max-h-32 overflow-y-auto"> | |
| {rankingEntries | |
| .sort((a, b) => Number(b[1]) - Number(a[1])) | |
| .slice(0, 8) | |
| .map(([k, v]) => ( | |
| <div | |
| key={k} | |
| className="flex justify-between gap-2 text-[10px] font-mono border border-white/5 rounded px-2 py-1" | |
| > | |
| <span className="text-zinc-400 truncate">{k}</span> | |
| <span className="text-cyan-300 shrink-0"> | |
| {Number(v).toFixed(3)} | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ) : ( | |
| trackMode === "blind" && ( | |
| <p className="text-[10px] text-zinc-600 italic"> | |
| Tool rankings / SPRT are hidden in blind benchmark mode. | |
| </p> | |
| ) | |
| )} | |
| {inv && Object.keys(inv).length > 0 && ( | |
| <div> | |
| <p className="text-[10px] text-zinc-500 mb-1">Investigation</p> | |
| <pre className="text-[9px] font-mono text-zinc-500 bg-zinc-950/80 rounded p-2 max-h-28 overflow-auto border border-white/5"> | |
| {JSON.stringify(inv, null, 2)} | |
| </pre> | |
| </div> | |
| )} | |
| <div> | |
| <p className="text-[10px] text-zinc-500 mb-1"> | |
| Visible documents ({docs.length}) | |
| </p> | |
| <ul className="space-y-1 max-h-36 overflow-y-auto"> | |
| {docs.slice(0, 8).map((d, i) => { | |
| const doc = asRecord(d) || {}; | |
| const row = doc as PreviewDoc; | |
| return ( | |
| <li key={i} className="border border-white/5 rounded overflow-hidden"> | |
| <button | |
| type="button" | |
| onClick={() => setPreviewDoc(row)} | |
| className="w-full flex items-center justify-between gap-2 text-left px-2 py-1.5 text-[10px] font-mono text-zinc-400 hover:bg-white/[0.06] hover:text-zinc-200 transition-colors" | |
| > | |
| <span className="truncate"> | |
| {asString(doc.doc_id)}{" "} | |
| <span className="text-zinc-600">({asString(doc.doc_type)})</span> | |
| </span> | |
| <Eye size={14} className="shrink-0 text-zinc-500" aria-hidden /> | |
| </button> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| </div> | |
| {lastToolName && ( | |
| <div className="text-[10px] text-zinc-500 border-t border-white/10 pt-3"> | |
| Last tool:{" "} | |
| <span className="font-mono text-zinc-300">{lastToolName}</span> | |
| </div> | |
| )} | |
| </div> | |
| <DocumentPreviewModal | |
| open={previewDoc !== null} | |
| onClose={() => setPreviewDoc(null)} | |
| doc={previewDoc} | |
| caseId={caseId || undefined} | |
| instruction={instruction || undefined} | |
| lastToolResult={ | |
| lastTool && typeof lastTool === "object" | |
| ? (lastTool as Record<string, unknown>) | |
| : undefined | |
| } | |
| /> | |
| </div> | |
| ); | |
| } | |