@oh-my-pi/omp-stats 15.0.0 → 15.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,46 +9,31 @@ import {
9
9
  Tooltip,
10
10
  } from "chart.js";
11
11
  import { format } from "date-fns";
12
- import { ChevronDown, ChevronUp } from "lucide-react";
13
12
  import { useMemo, useState } from "react";
14
13
  import { Line } from "react-chartjs-2";
15
14
  import type { ModelPerformancePoint, ModelStats } from "../types";
16
15
  import { useSystemTheme } from "../useSystemTheme";
16
+ import {
17
+ DetailChartEmpty,
18
+ detailChartPlugins,
19
+ detailChartScalesDualAxis,
20
+ ExpandableModelRow,
21
+ lineSeriesStyle,
22
+ MiniSparkline,
23
+ MODEL_COLORS,
24
+ ModelNameCell,
25
+ ModelTableBody,
26
+ ModelTableHeader,
27
+ ModelTableShell,
28
+ TABLE_CHART_THEMES,
29
+ type TableChartTheme,
30
+ TrendEmpty,
31
+ } from "./models-table-shared";
17
32
 
18
33
  ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
19
34
 
20
- const MODEL_COLORS = [
21
- "#a78bfa", // violet
22
- "#22d3ee", // cyan
23
- "#ec4899", // pink
24
- "#4ade80", // green
25
- "#fbbf24", // amber
26
- "#f87171", // red
27
- "#60a5fa", // blue
28
- ];
29
-
30
- const CHART_THEMES = {
31
- dark: {
32
- legendLabel: "#cbd5e1",
33
- tooltipBackground: "#16161e",
34
- tooltipTitle: "#f8fafc",
35
- tooltipBody: "#94a3b8",
36
- tooltipBorder: "rgba(255, 255, 255, 0.1)",
37
- grid: "rgba(255, 255, 255, 0.06)",
38
- tick: "#94a3b8",
39
- },
40
- light: {
41
- legendLabel: "#334155",
42
- tooltipBackground: "#ffffff",
43
- tooltipTitle: "#0f172a",
44
- tooltipBody: "#334155",
45
- tooltipBorder: "rgba(15, 23, 42, 0.18)",
46
- grid: "rgba(15, 23, 42, 0.08)",
47
- tick: "#475569",
48
- },
49
- } as const;
35
+ const GRID_TEMPLATE = "2fr 0.9fr 0.9fr 1fr 0.8fr 0.8fr 140px 40px";
50
36
 
51
- type ChartTheme = (typeof CHART_THEMES)[keyof typeof CHART_THEMES];
52
37
  interface ModelsTableProps {
53
38
  models: ModelStats[];
54
39
  performanceSeries: ModelPerformancePoint[];
@@ -69,184 +54,129 @@ export function ModelsTable({ models, performanceSeries }: ModelsTableProps) {
69
54
 
70
55
  const performanceSeriesByKey = useMemo(() => buildModelPerformanceLookup(performanceSeries), [performanceSeries]);
71
56
  const theme = useSystemTheme();
72
- const chartTheme = CHART_THEMES[theme];
57
+ const chartTheme = TABLE_CHART_THEMES[theme];
73
58
  const sortedModels = [...models].sort(
74
59
  (a, b) => b.totalInputTokens + b.totalOutputTokens - (a.totalInputTokens + a.totalOutputTokens),
75
60
  );
76
61
 
77
62
  return (
78
- <div className="surface overflow-hidden">
79
- <div className="px-5 py-4 border-b border-[var(--border-subtle)]">
80
- <h3 className="text-sm font-semibold text-[var(--text-primary)]">Model Statistics</h3>
81
- </div>
82
-
83
- <div className="overflow-x-auto">
84
- <div
85
- className="grid gap-3 px-5 py-3 text-[var(--text-muted)] text-xs uppercase tracking-wider font-semibold"
86
- style={{ gridTemplateColumns: "2fr 0.9fr 0.9fr 1fr 0.8fr 0.8fr 140px 40px" }}
87
- >
88
- <div>Model</div>
89
- <div className="text-right">Requests</div>
90
- <div className="text-right">Cost</div>
91
- <div className="text-right">Tokens</div>
92
- <div className="text-right">Tokens/s</div>
93
- <div className="text-right">TTFT</div>
94
- <div className="text-center">14d Trend</div>
95
- <div />
96
- </div>
97
-
98
- <div className="max-h-[calc(100vh-300px)] overflow-y-auto">
99
- {sortedModels.map((model, index) => {
100
- const key = `${model.model}::${model.provider}`;
101
- const performance = performanceSeriesByKey.get(key);
102
- const trendData = performance?.data ?? [];
103
- const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
104
- const isExpanded = expandedKey === key;
105
- const errorRate = model.errorRate * 100;
106
-
107
- return (
108
- <div key={key} className="border-t border-[var(--border-subtle)]">
109
- <button
110
- type="button"
111
- onClick={() => setExpandedKey(isExpanded ? null : key)}
112
- className="w-full bg-transparent border-none text-left px-5 py-3 cursor-pointer hover:bg-[var(--bg-hover)] transition-colors"
113
- >
114
- <div
115
- className="grid gap-3 items-center"
116
- style={{ gridTemplateColumns: "2fr 0.9fr 0.9fr 1fr 0.8fr 0.8fr 140px 40px" }}
117
- >
63
+ <ModelTableShell title="Model Statistics">
64
+ <ModelTableHeader
65
+ gridTemplate={GRID_TEMPLATE}
66
+ columns={[
67
+ { label: "Model" },
68
+ { label: "Requests", align: "right" },
69
+ { label: "Cost", align: "right" },
70
+ { label: "Tokens", align: "right" },
71
+ { label: "Tokens/s", align: "right" },
72
+ { label: "TTFT", align: "right" },
73
+ { label: "14d Trend", align: "center" },
74
+ ]}
75
+ />
76
+
77
+ <ModelTableBody>
78
+ {sortedModels.map((model, index) => {
79
+ const key = `${model.model}::${model.provider}`;
80
+ const performance = performanceSeriesByKey.get(key);
81
+ const trendData = performance?.data ?? [];
82
+ const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
83
+ const isExpanded = expandedKey === key;
84
+ const errorRate = model.errorRate * 100;
85
+
86
+ return (
87
+ <ExpandableModelRow
88
+ key={key}
89
+ gridTemplate={GRID_TEMPLATE}
90
+ isExpanded={isExpanded}
91
+ onToggle={() => setExpandedKey(isExpanded ? null : key)}
92
+ cells={[
93
+ <ModelNameCell key="name" model={model.model} provider={model.provider} />,
94
+ <div key="requests" className="text-right text-[var(--text-secondary)] font-mono text-sm">
95
+ {model.totalRequests.toLocaleString()}
96
+ </div>,
97
+ <div key="cost" className="text-right text-[var(--text-secondary)] font-mono text-sm">
98
+ ${model.totalCost.toFixed(2)}
99
+ </div>,
100
+ <div key="tokens" className="text-right text-[var(--text-secondary)] font-mono text-sm">
101
+ {(model.totalInputTokens + model.totalOutputTokens).toLocaleString()}
102
+ </div>,
103
+ <div key="tps" className="text-right text-[var(--text-secondary)] font-mono text-sm">
104
+ {model.avgTokensPerSecond?.toFixed(1) ?? "-"}
105
+ </div>,
106
+ <div key="ttft" className="text-right text-[var(--text-secondary)] font-mono text-sm">
107
+ {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
108
+ </div>,
109
+ ]}
110
+ trendCell={
111
+ trendData.length === 0 ? (
112
+ <TrendEmpty />
113
+ ) : (
114
+ <MiniSparkline
115
+ timestamps={trendData.map(d => d.timestamp)}
116
+ values={trendData.map(d => d.avgTokensPerSecond ?? 0)}
117
+ color={trendColor}
118
+ />
119
+ )
120
+ }
121
+ expandedContent={
122
+ <div className="grid gap-4" style={{ gridTemplateColumns: "200px 1fr" }}>
123
+ <div className="space-y-4 text-sm">
118
124
  <div>
119
- <div className="font-medium text-[var(--text-primary)]">{model.model}</div>
120
- <div className="text-xs text-[var(--text-muted)]">{model.provider}</div>
121
- </div>
122
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
123
- {model.totalRequests.toLocaleString()}
124
- </div>
125
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
126
- ${model.totalCost.toFixed(2)}
127
- </div>
128
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
129
- {(model.totalInputTokens + model.totalOutputTokens).toLocaleString()}
130
- </div>
131
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
132
- {model.avgTokensPerSecond?.toFixed(1) ?? "-"}
133
- </div>
134
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
135
- {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
136
- </div>
137
- <div className="h-10">
138
- {trendData.length === 0 ? (
139
- <div className="text-[var(--text-muted)] text-center text-sm">-</div>
140
- ) : (
141
- <TrendChart data={trendData} color={trendColor} />
142
- )}
143
- </div>
144
- <div className="flex justify-center text-[var(--text-muted)]">
145
- {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
146
- </div>
147
- </div>
148
- </button>
149
-
150
- {isExpanded && (
151
- <div className="px-5 py-4 bg-[var(--bg-elevated)] border-t border-[var(--border-subtle)]">
152
- <div className="grid gap-4" style={{ gridTemplateColumns: "200px 1fr" }}>
153
- <div className="space-y-4 text-sm">
154
- <div>
155
- <div className="text-[var(--text-primary)] font-medium mb-2">Quality</div>
156
- <div className="space-y-1 text-[var(--text-secondary)]">
157
- <div className="flex items-center justify-between">
158
- <span>Error rate</span>
159
- <span
160
- className={
161
- errorRate > 5
162
- ? "text-[var(--accent-red)]"
163
- : "text-[var(--accent-green)]"
164
- }
165
- >
166
- {errorRate.toFixed(1)}%
167
- </span>
168
- </div>
169
- <div className="flex items-center justify-between">
170
- <span>Cache rate</span>
171
- <span className="text-[var(--accent-cyan)]">
172
- {(model.cacheRate * 100).toFixed(1)}%
173
- </span>
174
- </div>
175
- </div>
125
+ <div className="text-[var(--text-primary)] font-medium mb-2">Quality</div>
126
+ <div className="space-y-1 text-[var(--text-secondary)]">
127
+ <div className="flex items-center justify-between">
128
+ <span>Error rate</span>
129
+ <span
130
+ className={
131
+ errorRate > 5 ? "text-[var(--accent-red)]" : "text-[var(--accent-green)]"
132
+ }
133
+ >
134
+ {errorRate.toFixed(1)}%
135
+ </span>
176
136
  </div>
177
- <div>
178
- <div className="text-[var(--text-primary)] font-medium mb-2">Latency</div>
179
- <div className="space-y-1 text-[var(--text-secondary)]">
180
- <div className="flex items-center justify-between">
181
- <span>Avg duration</span>
182
- <span className="font-mono">
183
- {model.avgDuration ? `${(model.avgDuration / 1000).toFixed(2)}s` : "-"}
184
- </span>
185
- </div>
186
- <div className="flex items-center justify-between">
187
- <span>Avg TTFT</span>
188
- <span className="font-mono">
189
- {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
190
- </span>
191
- </div>
192
- </div>
137
+ <div className="flex items-center justify-between">
138
+ <span>Cache rate</span>
139
+ <span className="text-[var(--accent-cyan)]">
140
+ {(model.cacheRate * 100).toFixed(1)}%
141
+ </span>
193
142
  </div>
194
143
  </div>
195
- <div className="h-[200px]">
196
- {trendData.length === 0 ? (
197
- <div className="h-full flex items-center justify-center text-[var(--text-muted)] text-sm">
198
- No data available
199
- </div>
200
- ) : (
201
- <PerformanceChart data={trendData} color={trendColor} chartTheme={chartTheme} />
202
- )}
144
+ </div>
145
+ <div>
146
+ <div className="text-[var(--text-primary)] font-medium mb-2">Latency</div>
147
+ <div className="space-y-1 text-[var(--text-secondary)]">
148
+ <div className="flex items-center justify-between">
149
+ <span>Avg duration</span>
150
+ <span className="font-mono">
151
+ {model.avgDuration ? `${(model.avgDuration / 1000).toFixed(2)}s` : "-"}
152
+ </span>
153
+ </div>
154
+ <div className="flex items-center justify-between">
155
+ <span>Avg TTFT</span>
156
+ <span className="font-mono">
157
+ {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
158
+ </span>
159
+ </div>
203
160
  </div>
204
161
  </div>
205
162
  </div>
206
- )}
207
- </div>
208
- );
209
- })}
210
- </div>
211
- </div>
212
- </div>
163
+ <div className="h-[200px]">
164
+ {trendData.length === 0 ? (
165
+ <DetailChartEmpty />
166
+ ) : (
167
+ <PerformanceChart data={trendData} color={trendColor} chartTheme={chartTheme} />
168
+ )}
169
+ </div>
170
+ </div>
171
+ }
172
+ />
173
+ );
174
+ })}
175
+ </ModelTableBody>
176
+ </ModelTableShell>
213
177
  );
214
178
  }
215
179
 
216
- function TrendChart({
217
- data,
218
- color,
219
- }: {
220
- data: Array<{ timestamp: number; avgTokensPerSecond: number | null }>;
221
- color: string;
222
- }) {
223
- const chartData = {
224
- labels: data.map(d => format(new Date(d.timestamp), "MMM d")),
225
- datasets: [
226
- {
227
- data: data.map(d => d.avgTokensPerSecond ?? 0),
228
- borderColor: color,
229
- backgroundColor: "transparent",
230
- tension: 0.4,
231
- pointRadius: 0,
232
- borderWidth: 2,
233
- },
234
- ],
235
- };
236
-
237
- const options = {
238
- responsive: true,
239
- maintainAspectRatio: false,
240
- plugins: { legend: { display: false }, tooltip: { enabled: false } },
241
- scales: {
242
- x: { display: false },
243
- y: { display: false, min: 0 },
244
- },
245
- };
246
-
247
- return <Line data={chartData} options={options} />;
248
- }
249
-
250
180
  function PerformanceChart({
251
181
  data,
252
182
  color,
@@ -254,7 +184,7 @@ function PerformanceChart({
254
184
  }: {
255
185
  data: Array<{ timestamp: number; avgTtftSeconds: number | null; avgTokensPerSecond: number | null }>;
256
186
  color: string;
257
- chartTheme: ChartTheme;
187
+ chartTheme: TableChartTheme;
258
188
  }) {
259
189
  const chartData = {
260
190
  labels: data.map(d => format(new Date(d.timestamp), "MMM d")),
@@ -262,21 +192,13 @@ function PerformanceChart({
262
192
  {
263
193
  label: "TTFT",
264
194
  data: data.map(d => d.avgTtftSeconds ?? null),
265
- borderColor: "#fbbf24",
266
- backgroundColor: "transparent",
267
- tension: 0.4,
268
- pointRadius: 0,
269
- borderWidth: 2,
195
+ ...lineSeriesStyle("#fbbf24"),
270
196
  yAxisID: "y" as const,
271
197
  },
272
198
  {
273
199
  label: "Tokens/s",
274
200
  data: data.map(d => d.avgTokensPerSecond ?? null),
275
- borderColor: color,
276
- backgroundColor: "transparent",
277
- tension: 0.4,
278
- pointRadius: 0,
279
- borderWidth: 2,
201
+ ...lineSeriesStyle(color),
280
202
  yAxisID: "y1" as const,
281
203
  },
282
204
  ],
@@ -285,46 +207,8 @@ function PerformanceChart({
285
207
  const options = {
286
208
  responsive: true,
287
209
  maintainAspectRatio: false,
288
- plugins: {
289
- legend: {
290
- display: true,
291
- position: "top" as const,
292
- labels: {
293
- color: chartTheme.legendLabel,
294
- usePointStyle: true,
295
- padding: 16,
296
- font: { size: 12 },
297
- },
298
- },
299
- tooltip: {
300
- backgroundColor: chartTheme.tooltipBackground,
301
- titleColor: chartTheme.tooltipTitle,
302
- bodyColor: chartTheme.tooltipBody,
303
- borderColor: chartTheme.tooltipBorder,
304
- borderWidth: 1,
305
- cornerRadius: 8,
306
- },
307
- },
308
- scales: {
309
- x: {
310
- grid: { color: chartTheme.grid },
311
- ticks: { color: chartTheme.tick, font: { size: 11 } },
312
- },
313
- y: {
314
- type: "linear" as const,
315
- display: true,
316
- position: "left" as const,
317
- grid: { color: chartTheme.grid },
318
- ticks: { color: chartTheme.tick, font: { size: 11 } },
319
- },
320
- y1: {
321
- type: "linear" as const,
322
- display: true,
323
- position: "right" as const,
324
- grid: { drawOnChartArea: false },
325
- ticks: { color: chartTheme.tick, font: { size: 11 } },
326
- },
327
- },
210
+ plugins: detailChartPlugins(chartTheme),
211
+ scales: detailChartScalesDualAxis(chartTheme),
328
212
  };
329
213
 
330
214
  return <Line data={chartData} options={options} />;
@@ -35,13 +35,11 @@ export function RequestDetail({ id, onClose }: RequestDetailProps) {
35
35
  if (!details) return null;
36
36
 
37
37
  return (
38
- // biome-ignore lint/a11y/noStaticElementInteractions: modal backdrop dismissal
39
38
  <div
40
39
  role="presentation"
41
40
  className="fixed inset-0 bg-[var(--bg-overlay)] backdrop-blur-sm flex justify-end z-[100] animate-fade-in"
42
41
  onClick={onClose}
43
42
  >
44
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation for modal content */}
45
43
  <div
46
44
  role="dialog"
47
45
  aria-modal="true"