/** 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) => (
))}
setPreset(e.target.value)} sx={{ minWidth: 280 }}>
{Object.entries(FAULT_PRESETS).map(([k, v]) => (
))}
: }
onClick={() => run('replay')}
sx={{ textTransform: 'none' }}
>
Run closed-loop
: }
onClick={() => run('causal')}
sx={{ textTransform: 'none' }}
>
Causal replay (actual vs corrected)
{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)}
))}
>
)}
);
}