File size: 3,899 Bytes
f271df0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
 * 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 (
      <Alert severity="info" variant="outlined">
        Select a baseline run (and optionally more candidates) to compare models A / B / C.
      </Alert>
    );
  }
  if (cmp.error && !cmp.data) return <ErrorNote error={cmp.error} />;
  if (cmp.loading && !cmp.data) return <LoadingBox label="Comparing models…" />;
  const data = cmp.data;
  if (!data) return null;

  const modelNames = data.models.map((m) => m.model_version);

  return (
    <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
      <SectionCard
        title="Model compare (A / B / C)"
        action={
          <Typography variant="caption" color="text.secondary">
            Baseline {data.baseline_run_id.slice(0, 8)}…
          </Typography>
        }
      >
        <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
          {data.recommendations.map((r) => (
            <Alert
              key={r.candidate}
              severity={r.recommendation === 'PROMOTE' ? 'success' : 'warning'}
              variant="outlined"
              sx={{ py: 0 }}
            >
              {r.candidate}: {r.recommendation}
              {r.blocker_count ? ` (${r.blocker_count} blockers)` : ''}
            </Alert>
          ))}
        </Box>

        <Table size="small">
          <TableHead>
            <TableRow>
              <TableCell>Metric</TableCell>
              {modelNames.map((m) => (
                <TableCell key={m} align="right">
                  {m}
                </TableCell>
              ))}
            </TableRow>
          </TableHead>
          <TableBody>
            {data.metric_matrix.map((row) => (
              <TableRow key={row.metric}>
                <TableCell>{row.metric}</TableCell>
                {modelNames.map((m) => {
                  const v = row[m] as number | null | undefined;
                  const d = row.deltas_vs_baseline?.[m];
                  return (
                    <TableCell key={m} align="right">
                      {fmtPct(v ?? null)}
                      {d !== undefined && d !== null ? (
                        <Typography component="span" variant="caption" sx={{ ml: 0.5, color: d < 0 ? '#c62828' : '#2e7d32' }}>
                          {d >= 0 ? '+' : ''}
                          {(d * 100).toFixed(1)}pp
                        </Typography>
                      ) : null}
                    </TableCell>
                  );
                })}
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </SectionCard>
    </Box>
  );
}