/** Tab 2: closed-loop behavioral run + causal counterfactual replay diff view. */ import { useEffect, useState } from 'react'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import CircularProgress from '@mui/material/CircularProgress'; import MenuItem from '@mui/material/MenuItem'; import Paper from '@mui/material/Paper'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { GitCompareArrows, Play } from 'lucide-react'; import * as api from '../../services/nextgen'; import type { BehavioralAssessment, CausalReplayResult } from '../../types/nextgen'; import { DataLabelChip, LineChart, MetricCard, PANEL_SX } from './common'; function fmt(v: number | null | undefined, digits = 2): string | null { return v === null || v === undefined ? null : v.toFixed(digits); } function MetricsPanel({ a }: { a: BehavioralAssessment }) { const m = a.metrics; return ( ); } const FAULT_PRESETS: Record[] }> = { default: { label: 'Demo faults (miss emergent pedestrian)' }, none: { label: 'No faults (nominal perception)', faults: [] }, cosmetic: { label: 'Cosmetic class flip (pedestrian → cyclist)', faults: [{ type: 'misclassify', instance_id: 'demo-crossing-ped', as_class: 'cyclist' }], }, }; export default function ClosedLoopTab() { const [scenarioIds, setScenarioIds] = useState(['demo']); const [scenarioId, setScenarioId] = useState('demo'); const [preset, setPreset] = useState('default'); const [assessment, setAssessment] = useState(null); const [causal, setCausal] = useState(null); const [busy, setBusy] = useState<'replay' | 'causal' | null>(null); const [error, setError] = useState(''); useEffect(() => { api .listCounterfactuals() .then((r) => setScenarioIds(['demo', ...r.scenarios.map((s) => s.scenario_id)])) .catch(() => {}); }, []); const run = async (kind: 'replay' | 'causal') => { setBusy(kind); setError(''); try { const faults = FAULT_PRESETS[preset].faults; const body = { scenario_id: scenarioId, ...(faults !== undefined ? { faults } : {}) }; if (kind === 'replay') setAssessment(await api.runReplay(body)); else setCausal(await api.runCausalReplay(body)); } catch (e) { setError(String((e as Error).message ?? e)); } finally { setBusy(null); } }; const behavioral = causal?.verdict === 'BEHAVIORALLY_CONSEQUENTIAL'; return ( setScenarioId(e.target.value)} sx={{ minWidth: 220 }}> {scenarioIds.map((s) => ( {s === 'demo' ? 'demo (occluded emergence testbed)' : s} ))} setPreset(e.target.value)} sx={{ minWidth: 280 }}> {Object.entries(FAULT_PRESETS).map(([k, v]) => ( {v.label} ))} {error && {error}} {assessment && ( Behavioral metrics — {assessment.perception_mode} perception ({ x: p.t, y: p.v })), }, ]} /> )} {causal && ( <> {behavioral ? 'Correcting perception changes the safety outcome — this regression matters behaviorally.' : 'Perception error is visible in open-loop metrics but does not change behavior or safety outcome.'} Trajectory diff — ego speed ({ x: p.t, y: p.v })) }, { name: 'corrected perception', color: '#66bb6a', points: causal.corrected.trajectory.map((p) => ({ x: p.t, y: p.v })) }, ]} /> Trajectory diff — ego position ({ x: p.t, y: p.x })) }, { name: 'corrected perception', color: '#66bb6a', points: causal.corrected.trajectory.map((p) => ({ x: p.t, y: p.x })) }, ]} /> Causal chain {causal.causal_chain.map((c, i) => ( {c.question} {c.evidence} ))} Outcome diffs (actual − corrected) {Object.entries(causal.diffs).map(([k, v]) => ( {k} {typeof v === 'boolean' ? String(v) : v === null ? '—' : v.toFixed(3)} ))} )} ); }