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

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,24 +9,29 @@ 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 { BehaviorModelStats, BehaviorTimeSeriesPoint } from "../types";
16
15
  import { useSystemTheme } from "../useSystemTheme";
16
+ import {
17
+ DetailChartEmpty,
18
+ detailChartPlugins,
19
+ detailChartScalesSingleAxis,
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
35
  const SERIES_COLORS = {
31
36
  yelling: "#fbbf24", // amber
32
37
  profanity: "#f87171", // red
@@ -34,29 +39,6 @@ const SERIES_COLORS = {
34
39
  frustration: "#22d3ee", // cyan - new semantic signals
35
40
  } as const;
36
41
 
37
- const CHART_THEMES = {
38
- dark: {
39
- legendLabel: "#cbd5e1",
40
- tooltipBackground: "#16161e",
41
- tooltipTitle: "#f8fafc",
42
- tooltipBody: "#94a3b8",
43
- tooltipBorder: "rgba(255, 255, 255, 0.1)",
44
- grid: "rgba(255, 255, 255, 0.06)",
45
- tick: "#94a3b8",
46
- },
47
- light: {
48
- legendLabel: "#334155",
49
- tooltipBackground: "#ffffff",
50
- tooltipTitle: "#0f172a",
51
- tooltipBody: "#334155",
52
- tooltipBorder: "rgba(15, 23, 42, 0.18)",
53
- grid: "rgba(15, 23, 42, 0.08)",
54
- tick: "#475569",
55
- },
56
- } as const;
57
-
58
- type ChartTheme = (typeof CHART_THEMES)[keyof typeof CHART_THEMES];
59
-
60
42
  interface BehaviorModelsTableProps {
61
43
  models: BehaviorModelStats[];
62
44
  behaviorSeries: BehaviorTimeSeriesPoint[];
@@ -107,7 +89,7 @@ function formatRate(total: number, messages: number): string {
107
89
  export function BehaviorModelsTable({ models, behaviorSeries }: BehaviorModelsTableProps) {
108
90
  const [expandedKey, setExpandedKey] = useState<string | null>(null);
109
91
  const theme = useSystemTheme();
110
- const chartTheme = CHART_THEMES[theme];
92
+ const chartTheme = TABLE_CHART_THEMES[theme];
111
93
 
112
94
  const trendByKey = useMemo(() => buildTrendLookup(behaviorSeries), [behaviorSeries]);
113
95
 
@@ -119,153 +101,137 @@ export function BehaviorModelsTable({ models, behaviorSeries }: BehaviorModelsTa
119
101
  });
120
102
 
121
103
  return (
122
- <div className="surface overflow-hidden">
123
- <div className="px-5 py-4 border-b border-[var(--border-subtle)]">
124
- <h3 className="text-sm font-semibold text-[var(--text-primary)]">Behavior by Model</h3>
125
- <p className="text-xs text-[var(--text-muted)] mt-1">
126
- How often each model elicited a tantrum — rates are per user message
127
- </p>
128
- </div>
129
-
130
- <div className="overflow-x-auto">
131
- <div
132
- className="grid gap-3 px-5 py-3 text-[var(--text-muted)] text-xs uppercase tracking-wider font-semibold"
133
- style={{ gridTemplateColumns: GRID_TEMPLATE }}
134
- >
135
- <div>Model</div>
136
- <div className="text-right">Messages</div>
137
- <div className="text-right">CAPS %</div>
138
- <div className="text-right">Profanity %</div>
139
- <div className="text-right">Anguish %</div>
140
- <div className="text-right">Frustration %</div>
141
- <div className="text-right">Hits %</div>
142
- <div className="text-center">Trend</div>
143
- <div />
144
- </div>
104
+ <ModelTableShell
105
+ title="Behavior by Model"
106
+ subtitle="How often each model elicited a tantrum — rates are per user message"
107
+ >
108
+ <ModelTableHeader
109
+ gridTemplate={GRID_TEMPLATE}
110
+ columns={[
111
+ { label: "Model" },
112
+ { label: "Messages", align: "right" },
113
+ { label: "CAPS %", align: "right" },
114
+ { label: "Profanity %", align: "right" },
115
+ { label: "Anguish %", align: "right" },
116
+ { label: "Frustration %", align: "right" },
117
+ { label: "Hits %", align: "right" },
118
+ { label: "Trend", align: "center" },
119
+ ]}
120
+ />
145
121
 
146
- <div className="max-h-[calc(100vh-300px)] overflow-y-auto">
147
- {sortedModels.map((model, index) => {
148
- const key = `${model.model}::${model.provider}`;
149
- const trend = trendByKey.get(key)?.data ?? [];
150
- const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
151
- const isExpanded = expandedKey === key;
152
- const totalFrustration = model.totalNegation + model.totalRepetition + model.totalBlame;
153
- const totalHits = model.totalYelling + model.totalProfanity + model.totalAnguish + totalFrustration;
122
+ <ModelTableBody>
123
+ {sortedModels.map((model, index) => {
124
+ const key = `${model.model}::${model.provider}`;
125
+ const trend = trendByKey.get(key)?.data ?? [];
126
+ const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
127
+ const isExpanded = expandedKey === key;
128
+ const totalFrustration = model.totalNegation + model.totalRepetition + model.totalBlame;
129
+ const totalHits = model.totalYelling + model.totalProfanity + model.totalAnguish + totalFrustration;
154
130
 
155
- return (
156
- <div key={key} className="border-t border-[var(--border-subtle)]">
157
- <button
158
- type="button"
159
- onClick={() => setExpandedKey(isExpanded ? null : key)}
160
- className="w-full bg-transparent border-none text-left px-5 py-3 cursor-pointer hover:bg-[var(--bg-hover)] transition-colors"
161
- >
162
- <div className="grid gap-3 items-center" style={{ gridTemplateColumns: GRID_TEMPLATE }}>
163
- <div>
164
- <div className="font-medium text-[var(--text-primary)]">{model.model}</div>
165
- <div className="text-xs text-[var(--text-muted)]">{model.provider}</div>
166
- </div>
167
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
168
- {formatInt(model.totalMessages)}
169
- </div>
170
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
171
- {formatRate(model.totalYelling, model.totalMessages)}
172
- </div>
173
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
174
- {formatRate(model.totalProfanity, model.totalMessages)}
175
- </div>
176
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
177
- {formatRate(model.totalAnguish, model.totalMessages)}
178
- </div>
179
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
180
- {formatRate(totalFrustration, model.totalMessages)}
181
- </div>
182
- <div className="text-right text-[var(--text-secondary)] font-mono text-sm">
183
- {formatRate(totalHits, model.totalMessages)}
184
- </div>
185
- <div className="h-10">
186
- {trend.length === 0 ? (
187
- <div className="text-[var(--text-muted)] text-center text-sm">-</div>
188
- ) : (
189
- <TrendSparkline data={trend} color={trendColor} />
190
- )}
191
- </div>
192
- <div className="flex justify-center text-[var(--text-muted)]">
193
- {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
194
- </div>
131
+ return (
132
+ <ExpandableModelRow
133
+ key={key}
134
+ gridTemplate={GRID_TEMPLATE}
135
+ isExpanded={isExpanded}
136
+ onToggle={() => setExpandedKey(isExpanded ? null : key)}
137
+ cells={[
138
+ <ModelNameCell key="name" model={model.model} provider={model.provider} />,
139
+ <div key="messages" className="text-right text-[var(--text-secondary)] font-mono text-sm">
140
+ {formatInt(model.totalMessages)}
141
+ </div>,
142
+ <div key="caps" className="text-right text-[var(--text-secondary)] font-mono text-sm">
143
+ {formatRate(model.totalYelling, model.totalMessages)}
144
+ </div>,
145
+ <div key="profanity" className="text-right text-[var(--text-secondary)] font-mono text-sm">
146
+ {formatRate(model.totalProfanity, model.totalMessages)}
147
+ </div>,
148
+ <div key="anguish" className="text-right text-[var(--text-secondary)] font-mono text-sm">
149
+ {formatRate(model.totalAnguish, model.totalMessages)}
150
+ </div>,
151
+ <div key="frustration" className="text-right text-[var(--text-secondary)] font-mono text-sm">
152
+ {formatRate(totalFrustration, model.totalMessages)}
153
+ </div>,
154
+ <div key="hits" className="text-right text-[var(--text-secondary)] font-mono text-sm">
155
+ {formatRate(totalHits, model.totalMessages)}
156
+ </div>,
157
+ ]}
158
+ trendCell={
159
+ trend.length === 0 ? (
160
+ <TrendEmpty />
161
+ ) : (
162
+ <MiniSparkline
163
+ timestamps={trend.map(d => d.timestamp)}
164
+ values={trend.map(d => d.total)}
165
+ color={trendColor}
166
+ />
167
+ )
168
+ }
169
+ expandedContent={
170
+ <div className="grid gap-4" style={{ gridTemplateColumns: "220px 1fr" }}>
171
+ <div className="space-y-4 text-sm">
172
+ <DetailRow
173
+ label="Yelling (CAPS)"
174
+ total={model.totalYelling}
175
+ messages={model.totalMessages}
176
+ valueClass="text-[var(--accent-amber,#fbbf24)]"
177
+ />
178
+ <DetailRow
179
+ label="Profanity"
180
+ total={model.totalProfanity}
181
+ messages={model.totalMessages}
182
+ valueClass="text-[var(--accent-red,#f87171)]"
183
+ />
184
+ <DetailRow
185
+ label="Anguish (!!!, nooo, dude, ..)"
186
+ total={model.totalAnguish}
187
+ messages={model.totalMessages}
188
+ valueClass="text-[var(--accent-violet,#a78bfa)]"
189
+ />
190
+ <DetailRow
191
+ label="Negation (no/nope/wrong)"
192
+ total={model.totalNegation}
193
+ messages={model.totalMessages}
194
+ valueClass="text-[var(--accent-cyan,#22d3ee)]"
195
+ />
196
+ <DetailRow
197
+ label="Repetition (i meant, still doesnt)"
198
+ total={model.totalRepetition}
199
+ messages={model.totalMessages}
200
+ valueClass="text-[var(--accent-cyan,#22d3ee)]"
201
+ />
202
+ <DetailRow
203
+ label="Blame (you didnt, stop X-ing)"
204
+ total={model.totalBlame}
205
+ messages={model.totalMessages}
206
+ valueClass="text-[var(--accent-cyan,#22d3ee)]"
207
+ />
208
+ <DetailRow
209
+ label="Avg chars / msg"
210
+ total={model.totalChars}
211
+ messages={model.totalMessages}
212
+ valueClass="text-[var(--text-secondary)]"
213
+ mode="average"
214
+ />
195
215
  </div>
196
- </button>
197
-
198
- {isExpanded && (
199
- <div className="px-5 py-4 bg-[var(--bg-elevated)] border-t border-[var(--border-subtle)]">
200
- <div className="grid gap-4" style={{ gridTemplateColumns: "220px 1fr" }}>
201
- <div className="space-y-4 text-sm">
202
- <DetailRow
203
- label="Yelling (CAPS)"
204
- total={model.totalYelling}
205
- messages={model.totalMessages}
206
- valueClass="text-[var(--accent-amber,#fbbf24)]"
207
- />
208
- <DetailRow
209
- label="Profanity"
210
- total={model.totalProfanity}
211
- messages={model.totalMessages}
212
- valueClass="text-[var(--accent-red,#f87171)]"
213
- />
214
- <DetailRow
215
- label="Anguish (!!!, nooo, dude, ..)"
216
- total={model.totalAnguish}
217
- messages={model.totalMessages}
218
- valueClass="text-[var(--accent-violet,#a78bfa)]"
219
- />
220
- <DetailRow
221
- label="Negation (no/nope/wrong)"
222
- total={model.totalNegation}
223
- messages={model.totalMessages}
224
- valueClass="text-[var(--accent-cyan,#22d3ee)]"
225
- />
226
- <DetailRow
227
- label="Repetition (i meant, still doesnt)"
228
- total={model.totalRepetition}
229
- messages={model.totalMessages}
230
- valueClass="text-[var(--accent-cyan,#22d3ee)]"
231
- />
232
- <DetailRow
233
- label="Blame (you didnt, stop X-ing)"
234
- total={model.totalBlame}
235
- messages={model.totalMessages}
236
- valueClass="text-[var(--accent-cyan,#22d3ee)]"
237
- />
238
- <DetailRow
239
- label="Avg chars / msg"
240
- total={model.totalChars}
241
- messages={model.totalMessages}
242
- valueClass="text-[var(--text-secondary)]"
243
- mode="average"
244
- />
245
- </div>
246
- <div className="h-[200px]">
247
- {trend.length === 0 ? (
248
- <div className="h-full flex items-center justify-center text-[var(--text-muted)] text-sm">
249
- No data available
250
- </div>
251
- ) : (
252
- <BreakdownChart data={trend} chartTheme={chartTheme} />
253
- )}
254
- </div>
255
- </div>
216
+ <div className="h-[200px]">
217
+ {trend.length === 0 ? (
218
+ <DetailChartEmpty />
219
+ ) : (
220
+ <BreakdownChart data={trend} chartTheme={chartTheme} />
221
+ )}
256
222
  </div>
257
- )}
258
- </div>
259
- );
260
- })}
261
- {sortedModels.length === 0 && (
262
- <div className="border-t border-[var(--border-subtle)] px-5 py-8 text-center text-[var(--text-muted)] text-sm">
263
- No user behavior recorded for this range yet.
264
- </div>
265
- )}
266
- </div>
267
- </div>
268
- </div>
223
+ </div>
224
+ }
225
+ />
226
+ );
227
+ })}
228
+ {sortedModels.length === 0 ? (
229
+ <div className="border-t border-[var(--border-subtle)] px-5 py-8 text-center text-[var(--text-muted)] text-sm">
230
+ No user behavior recorded for this range yet.
231
+ </div>
232
+ ) : null}
233
+ </ModelTableBody>
234
+ </ModelTableShell>
269
235
  );
270
236
  }
271
237
 
@@ -302,111 +268,22 @@ function DetailRow({
302
268
  );
303
269
  }
304
270
 
305
- function TrendSparkline({ data, color }: { data: DailyPoint[]; color: string }) {
271
+ function BreakdownChart({ data, chartTheme }: { data: DailyPoint[]; chartTheme: TableChartTheme }) {
306
272
  const chartData = {
307
273
  labels: data.map(d => format(new Date(d.timestamp), "MMM d")),
308
274
  datasets: [
309
- {
310
- data: data.map(d => d.total),
311
- borderColor: color,
312
- backgroundColor: "transparent",
313
- tension: 0.4,
314
- pointRadius: 0,
315
- borderWidth: 2,
316
- },
275
+ { label: "CAPS", data: data.map(d => d.yelling), ...lineSeriesStyle(SERIES_COLORS.yelling) },
276
+ { label: "Profanity", data: data.map(d => d.profanity), ...lineSeriesStyle(SERIES_COLORS.profanity) },
277
+ { label: "Anguish", data: data.map(d => d.anguish), ...lineSeriesStyle(SERIES_COLORS.anguish) },
278
+ { label: "Frustration", data: data.map(d => d.frustration), ...lineSeriesStyle(SERIES_COLORS.frustration) },
317
279
  ],
318
280
  };
319
281
 
320
282
  const options = {
321
283
  responsive: true,
322
284
  maintainAspectRatio: false,
323
- plugins: { legend: { display: false }, tooltip: { enabled: false } },
324
- scales: {
325
- x: { display: false },
326
- y: { display: false, min: 0 },
327
- },
328
- };
329
-
330
- return <Line data={chartData} options={options} />;
331
- }
332
-
333
- function BreakdownChart({ data, chartTheme }: { data: DailyPoint[]; chartTheme: ChartTheme }) {
334
- const chartData = {
335
- labels: data.map(d => format(new Date(d.timestamp), "MMM d")),
336
- datasets: [
337
- {
338
- label: "CAPS",
339
- data: data.map(d => d.yelling),
340
- borderColor: SERIES_COLORS.yelling,
341
- backgroundColor: "transparent",
342
- tension: 0.4,
343
- pointRadius: 0,
344
- borderWidth: 2,
345
- },
346
- {
347
- label: "Profanity",
348
- data: data.map(d => d.profanity),
349
- borderColor: SERIES_COLORS.profanity,
350
- backgroundColor: "transparent",
351
- tension: 0.4,
352
- pointRadius: 0,
353
- borderWidth: 2,
354
- },
355
- {
356
- label: "Anguish",
357
- data: data.map(d => d.anguish),
358
- borderColor: SERIES_COLORS.anguish,
359
- backgroundColor: "transparent",
360
- tension: 0.4,
361
- pointRadius: 0,
362
- borderWidth: 2,
363
- },
364
- {
365
- label: "Frustration",
366
- data: data.map(d => d.frustration),
367
- borderColor: SERIES_COLORS.frustration,
368
- backgroundColor: "transparent",
369
- tension: 0.4,
370
- pointRadius: 0,
371
- borderWidth: 2,
372
- },
373
- ],
374
- };
375
-
376
- const options = {
377
- responsive: true,
378
- maintainAspectRatio: false,
379
- plugins: {
380
- legend: {
381
- display: true,
382
- position: "top" as const,
383
- labels: {
384
- color: chartTheme.legendLabel,
385
- usePointStyle: true,
386
- padding: 16,
387
- font: { size: 12 },
388
- },
389
- },
390
- tooltip: {
391
- backgroundColor: chartTheme.tooltipBackground,
392
- titleColor: chartTheme.tooltipTitle,
393
- bodyColor: chartTheme.tooltipBody,
394
- borderColor: chartTheme.tooltipBorder,
395
- borderWidth: 1,
396
- cornerRadius: 8,
397
- },
398
- },
399
- scales: {
400
- x: {
401
- grid: { color: chartTheme.grid },
402
- ticks: { color: chartTheme.tick, font: { size: 11 } },
403
- },
404
- y: {
405
- grid: { color: chartTheme.grid },
406
- ticks: { color: chartTheme.tick, font: { size: 11 } },
407
- min: 0,
408
- },
409
- },
285
+ plugins: detailChartPlugins(chartTheme),
286
+ scales: detailChartScalesSingleAxis(chartTheme),
410
287
  };
411
288
 
412
289
  return <Line data={chartData} options={options} />;