File size: 4,800 Bytes
f6a5256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/** Shared building blocks for the Closed-Loop Lab page. */
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import type { DataLabel } from '../../types/nextgen';

export const PANEL_SX = {
  p: 1.5,
  bgcolor: '#161c22',
  border: '1px solid #232a31',
  borderRadius: 1.5,
} as const;

const LABEL_COLORS: Record<DataLabel, string> = {
  REAL: '#2e7d32',
  REPLAYED: '#0277bd',
  SIMULATED: '#6a1b9a',
  GENERATED: '#ef6c00',
  COUNTERFACTUAL: '#c62828',
};

export function DataLabelChip({ label }: { label: DataLabel | string }) {
  const color = LABEL_COLORS[label as DataLabel] ?? '#455a64';
  return (
    <Chip
      size="small"
      label={label}
      sx={{ height: 18, fontSize: 10, fontWeight: 700, bgcolor: color, color: '#fff' }}
    />
  );
}

export function ScoreChip({ name, value }: { name: string; value: number }) {
  const color = value >= 0.8 ? '#2e7d32' : value >= 0.5 ? '#e65100' : '#b71c1c';
  return (
    <Chip
      size="small"
      label={`${name} ${(value * 100).toFixed(0)}%`}
      sx={{ height: 20, fontSize: 10.5, fontWeight: 700, bgcolor: '#12171d', color, border: `1px solid ${color}` }}
    />
  );
}

export function MetricCard({
  label,
  value,
  unit,
  tone,
}: {
  label: string;
  value: string | number | null | undefined;
  unit?: string;
  tone?: 'good' | 'bad' | 'neutral';
}) {
  const color = tone === 'good' ? '#81c784' : tone === 'bad' ? '#ef9a9a' : '#e0e3e7';
  return (
    <Paper sx={{ ...PANEL_SX, minWidth: 130, flex: '1 1 130px' }}>
      <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', fontSize: 10.5 }}>
        {label}
      </Typography>
      <Typography variant="h6" sx={{ fontWeight: 800, fontSize: 18, color }}>
        {value === null || value === undefined ? '—' : value}
        {unit && value !== null && value !== undefined ? (
          <Typography component="span" variant="caption" sx={{ color: '#8a949e', ml: 0.5 }}>
            {unit}
          </Typography>
        ) : null}
      </Typography>
    </Paper>
  );
}

export interface Series {
  name: string;
  color: string;
  points: { x: number; y: number }[];
}

/** Dependency-free SVG line chart (time series comparison). */
export function LineChart({
  series,
  height = 160,
  xLabel,
  yLabel,
}: {
  series: Series[];
  height?: number;
  xLabel?: string;
  yLabel?: string;
}) {
  const all = series.flatMap((s) => s.points);
  if (!all.length) return null;
  const w = 560;
  const pad = { l: 42, r: 10, t: 10, b: 24 };
  const xs = all.map((p) => p.x);
  const ys = all.map((p) => p.y);
  const xMin = Math.min(...xs);
  const xMax = Math.max(...xs) || 1;
  const yMin = Math.min(0, ...ys);
  const yMax = Math.max(...ys) * 1.08 || 1;
  const sx = (x: number) => pad.l + ((x - xMin) / (xMax - xMin || 1)) * (w - pad.l - pad.r);
  const sy = (y: number) => height - pad.b - ((y - yMin) / (yMax - yMin || 1)) * (height - pad.t - pad.b);
  const yTicks = [yMin, (yMin + yMax) / 2, yMax];
  const xTicks = [xMin, (xMin + xMax) / 2, xMax];
  return (
    <Box>
      <svg viewBox={`0 0 ${w} ${height}`} style={{ width: '100%', maxWidth: w }}>
        {yTicks.map((t, i) => (
          <g key={`y${i}`}>
            <line x1={pad.l} x2={w - pad.r} y1={sy(t)} y2={sy(t)} stroke="#232a31" strokeDasharray="3 3" />
            <text x={pad.l - 6} y={sy(t) + 3} fill="#8a949e" fontSize={9} textAnchor="end">
              {t.toFixed(1)}
            </text>
          </g>
        ))}
        {xTicks.map((t, i) => (
          <text key={`x${i}`} x={sx(t)} y={height - 8} fill="#8a949e" fontSize={9} textAnchor="middle">
            {t.toFixed(1)}
          </text>
        ))}
        {series.map((s) => (
          <polyline
            key={s.name}
            fill="none"
            stroke={s.color}
            strokeWidth={1.8}
            points={s.points.map((p) => `${sx(p.x)},${sy(p.y)}`).join(' ')}
          />
        ))}
        {yLabel ? (
          <text x={10} y={pad.t + 4} fill="#8a949e" fontSize={9}>
            {yLabel}
          </text>
        ) : null}
        {xLabel ? (
          <text x={w - pad.r} y={height - 8} fill="#8a949e" fontSize={9} textAnchor="end">
            {xLabel}
          </text>
        ) : null}
      </svg>
      <Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
        {series.map((s) => (
          <Box key={s.name} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
            <Box sx={{ width: 14, height: 3, bgcolor: s.color, borderRadius: 1 }} />
            <Typography variant="caption" sx={{ color: '#c7ccd1', fontSize: 10.5 }}>
              {s.name}
            </Typography>
          </Box>
        ))}
      </Box>
    </Box>
  );
}