/** * 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} ); })} ))}
); }