/**
* Multi-model compare panel (A vs B vs C) — wraps /api/models/compare.
*/
import { useMemo } from 'react';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import { compareModels } from '../../services/platform';
import { usePoll } from '../../services/labeleval';
import { ErrorNote, LoadingBox, SectionCard, fmtPct } from '../labeleval/shared';
export default function ModelComparePanel({
runId,
baselineRunId,
extraRunIds = [],
refreshKey = 0,
}: {
runId: string | null;
baselineRunId: string | null;
extraRunIds?: string[];
refreshKey?: number;
}) {
const runIds = useMemo(() => {
const ids = [baselineRunId, runId, ...extraRunIds].filter(Boolean) as string[];
return [...new Set(ids)];
}, [runId, baselineRunId, extraRunIds]);
const cmp = usePoll(
() =>
runIds.length >= 2
? compareModels(runIds, baselineRunId)
: Promise.resolve(null),
null,
[runIds.join(','), baselineRunId, refreshKey]
);
if (runIds.length < 2) {
return (
Select a baseline run (and optionally more candidates) to compare models A / B / C.
);
}
if (cmp.error && !cmp.data) return ;
if (cmp.loading && !cmp.data) return ;
const data = cmp.data;
if (!data) return null;
const modelNames = data.models.map((m) => m.model_version);
return (
Baseline {data.baseline_run_id.slice(0, 8)}…
}
>
{data.recommendations.map((r) => (
{r.candidate}: {r.recommendation}
{r.blocker_count ? ` (${r.blocker_count} blockers)` : ''}
))}
Metric
{modelNames.map((m) => (
{m}
))}
{data.metric_matrix.map((row) => (
{row.metric}
{modelNames.map((m) => {
const v = row[m] as number | null | undefined;
const d = row.deltas_vs_baseline?.[m];
return (
{fmtPct(v ?? null)}
{d !== undefined && d !== null ? (
{d >= 0 ? '+' : ''}
{(d * 100).toFixed(1)}pp
) : null}
);
})}
))}
);
}