@gajae-code/stats 0.1.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.
Files changed (67) hide show
  1. package/README.md +82 -0
  2. package/build.ts +84 -0
  3. package/dist/client/index.css +1 -0
  4. package/dist/client/index.html +13 -0
  5. package/dist/client/index.js +257 -0
  6. package/dist/client/styles.css +1159 -0
  7. package/dist/types/aggregator.d.ts +65 -0
  8. package/dist/types/client/App.d.ts +1 -0
  9. package/dist/types/client/api.d.ts +10 -0
  10. package/dist/types/client/components/BehaviorChart.d.ts +6 -0
  11. package/dist/types/client/components/BehaviorModelsTable.d.ts +7 -0
  12. package/dist/types/client/components/BehaviorSummary.d.ts +7 -0
  13. package/dist/types/client/components/ChartsContainer.d.ts +7 -0
  14. package/dist/types/client/components/CostChart.d.ts +6 -0
  15. package/dist/types/client/components/CostSummary.d.ts +6 -0
  16. package/dist/types/client/components/Header.d.ts +12 -0
  17. package/dist/types/client/components/ModelsTable.d.ts +8 -0
  18. package/dist/types/client/components/RequestDetail.d.ts +6 -0
  19. package/dist/types/client/components/RequestList.d.ts +8 -0
  20. package/dist/types/client/components/StatsGrid.d.ts +6 -0
  21. package/dist/types/client/components/chart-shared.d.ts +187 -0
  22. package/dist/types/client/components/models-table-shared.d.ts +195 -0
  23. package/dist/types/client/components/range-meta.d.ts +21 -0
  24. package/dist/types/client/index.d.ts +1 -0
  25. package/dist/types/client/types.d.ts +62 -0
  26. package/dist/types/client/useSystemTheme.d.ts +2 -0
  27. package/dist/types/db.d.ts +93 -0
  28. package/dist/types/index.d.ts +5 -0
  29. package/dist/types/parser.d.ts +40 -0
  30. package/dist/types/server.d.ts +7 -0
  31. package/dist/types/shared-types.d.ts +192 -0
  32. package/dist/types/sync-worker.d.ts +31 -0
  33. package/dist/types/types.d.ts +120 -0
  34. package/dist/types/user-metrics.d.ts +72 -0
  35. package/package.json +91 -0
  36. package/src/aggregator.ts +454 -0
  37. package/src/client/App.tsx +221 -0
  38. package/src/client/api.ts +65 -0
  39. package/src/client/components/BehaviorChart.tsx +189 -0
  40. package/src/client/components/BehaviorModelsTable.tsx +342 -0
  41. package/src/client/components/BehaviorSummary.tsx +95 -0
  42. package/src/client/components/ChartsContainer.tsx +221 -0
  43. package/src/client/components/CostChart.tsx +171 -0
  44. package/src/client/components/CostSummary.tsx +53 -0
  45. package/src/client/components/Header.tsx +72 -0
  46. package/src/client/components/ModelsTable.tsx +265 -0
  47. package/src/client/components/RequestDetail.tsx +172 -0
  48. package/src/client/components/RequestList.tsx +73 -0
  49. package/src/client/components/StatsGrid.tsx +135 -0
  50. package/src/client/components/chart-shared.tsx +320 -0
  51. package/src/client/components/models-table-shared.tsx +275 -0
  52. package/src/client/components/range-meta.ts +72 -0
  53. package/src/client/css.d.ts +1 -0
  54. package/src/client/index.tsx +6 -0
  55. package/src/client/styles.css +306 -0
  56. package/src/client/types.ts +78 -0
  57. package/src/client/useSystemTheme.ts +31 -0
  58. package/src/db.ts +1100 -0
  59. package/src/embedded-client.generated.txt +7 -0
  60. package/src/index.ts +182 -0
  61. package/src/parser.ts +334 -0
  62. package/src/server.ts +325 -0
  63. package/src/shared-types.ts +204 -0
  64. package/src/sync-worker.ts +40 -0
  65. package/src/types.ts +125 -0
  66. package/src/user-metrics.ts +686 -0
  67. package/tailwind.config.js +40 -0
@@ -0,0 +1,265 @@
1
+ import {
2
+ CategoryScale,
3
+ Chart as ChartJS,
4
+ Legend,
5
+ LinearScale,
6
+ LineElement,
7
+ PointElement,
8
+ Title,
9
+ Tooltip,
10
+ } from "chart.js";
11
+ import { format } from "date-fns";
12
+ import { useMemo, useState } from "react";
13
+ import { Line } from "react-chartjs-2";
14
+ import type { ModelPerformancePoint, ModelStats, TimeRange } from "../types";
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";
32
+ import { rangeMeta } from "./range-meta";
33
+
34
+ ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
35
+
36
+ const GRID_TEMPLATE = "2fr 0.9fr 0.9fr 1fr 0.8fr 0.8fr 140px 40px";
37
+
38
+ interface ModelsTableProps {
39
+ models: ModelStats[];
40
+ performanceSeries: ModelPerformancePoint[];
41
+ timeRange: TimeRange;
42
+ }
43
+
44
+ type ModelPerformanceSeries = {
45
+ label: string;
46
+ data: Array<{
47
+ timestamp: number;
48
+ avgTtftSeconds: number | null;
49
+ avgTokensPerSecond: number | null;
50
+ requests: number;
51
+ }>;
52
+ };
53
+
54
+ export function ModelsTable({ models, performanceSeries, timeRange }: ModelsTableProps) {
55
+ const [expandedKey, setExpandedKey] = useState<string | null>(null);
56
+ const meta = rangeMeta(timeRange);
57
+
58
+ const performanceSeriesByKey = useMemo(
59
+ () => buildModelPerformanceLookup(performanceSeries, meta.bucketCount, meta.bucketMs),
60
+ [performanceSeries, meta.bucketCount, meta.bucketMs],
61
+ );
62
+ const theme = useSystemTheme();
63
+ const chartTheme = TABLE_CHART_THEMES[theme];
64
+ const sortedModels = [...models].sort(
65
+ (a, b) => b.totalInputTokens + b.totalOutputTokens - (a.totalInputTokens + a.totalOutputTokens),
66
+ );
67
+
68
+ return (
69
+ <ModelTableShell title="Model Statistics">
70
+ <ModelTableHeader
71
+ gridTemplate={GRID_TEMPLATE}
72
+ columns={[
73
+ { label: "Model" },
74
+ { label: "Requests", align: "right" },
75
+ { label: "Cost", align: "right" },
76
+ { label: "Tokens", align: "right" },
77
+ { label: "Tokens/s", align: "right" },
78
+ { label: "TTFT", align: "right" },
79
+ { label: meta.trendLabel, align: "center" },
80
+ ]}
81
+ />
82
+
83
+ <ModelTableBody>
84
+ {sortedModels.map((model, index) => {
85
+ const key = `${model.model}::${model.provider}`;
86
+ const performance = performanceSeriesByKey.get(key);
87
+ const trendData = performance?.data ?? [];
88
+ const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
89
+ const isExpanded = expandedKey === key;
90
+ const errorRate = model.errorRate * 100;
91
+
92
+ return (
93
+ <ExpandableModelRow
94
+ key={key}
95
+ gridTemplate={GRID_TEMPLATE}
96
+ isExpanded={isExpanded}
97
+ onToggle={() => setExpandedKey(isExpanded ? null : key)}
98
+ cells={[
99
+ <ModelNameCell key="name" model={model.model} provider={model.provider} />,
100
+ <div key="requests" className="text-right text-[var(--text-secondary)] font-mono text-sm">
101
+ {model.totalRequests.toLocaleString()}
102
+ </div>,
103
+ <div key="cost" className="text-right text-[var(--text-secondary)] font-mono text-sm">
104
+ ${model.totalCost.toFixed(2)}
105
+ </div>,
106
+ <div key="tokens" className="text-right text-[var(--text-secondary)] font-mono text-sm">
107
+ {(model.totalInputTokens + model.totalOutputTokens).toLocaleString()}
108
+ </div>,
109
+ <div key="tps" className="text-right text-[var(--text-secondary)] font-mono text-sm">
110
+ {model.avgTokensPerSecond?.toFixed(1) ?? "-"}
111
+ </div>,
112
+ <div key="ttft" className="text-right text-[var(--text-secondary)] font-mono text-sm">
113
+ {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
114
+ </div>,
115
+ ]}
116
+ trendCell={
117
+ trendData.length === 0 ? (
118
+ <TrendEmpty />
119
+ ) : (
120
+ <MiniSparkline
121
+ timestamps={trendData.map(d => d.timestamp)}
122
+ values={trendData.map(d => d.avgTokensPerSecond ?? 0)}
123
+ color={trendColor}
124
+ />
125
+ )
126
+ }
127
+ expandedContent={
128
+ <div className="grid gap-4" style={{ gridTemplateColumns: "200px 1fr" }}>
129
+ <div className="space-y-4 text-sm">
130
+ <div>
131
+ <div className="text-[var(--text-primary)] font-medium mb-2">Quality</div>
132
+ <div className="space-y-1 text-[var(--text-secondary)]">
133
+ <div className="flex items-center justify-between">
134
+ <span>Error rate</span>
135
+ <span
136
+ className={
137
+ errorRate > 5 ? "text-[var(--accent-red)]" : "text-[var(--accent-green)]"
138
+ }
139
+ >
140
+ {errorRate.toFixed(1)}%
141
+ </span>
142
+ </div>
143
+ <div className="flex items-center justify-between">
144
+ <span>Cache rate</span>
145
+ <span className="text-[var(--accent-cyan)]">
146
+ {(model.cacheRate * 100).toFixed(1)}%
147
+ </span>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ <div>
152
+ <div className="text-[var(--text-primary)] font-medium mb-2">Latency</div>
153
+ <div className="space-y-1 text-[var(--text-secondary)]">
154
+ <div className="flex items-center justify-between">
155
+ <span>Avg duration</span>
156
+ <span className="font-mono">
157
+ {model.avgDuration ? `${(model.avgDuration / 1000).toFixed(2)}s` : "-"}
158
+ </span>
159
+ </div>
160
+ <div className="flex items-center justify-between">
161
+ <span>Avg TTFT</span>
162
+ <span className="font-mono">
163
+ {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
164
+ </span>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ <div className="h-[200px]">
170
+ {trendData.length === 0 ? (
171
+ <DetailChartEmpty />
172
+ ) : (
173
+ <PerformanceChart data={trendData} color={trendColor} chartTheme={chartTheme} />
174
+ )}
175
+ </div>
176
+ </div>
177
+ }
178
+ />
179
+ );
180
+ })}
181
+ </ModelTableBody>
182
+ </ModelTableShell>
183
+ );
184
+ }
185
+
186
+ function PerformanceChart({
187
+ data,
188
+ color,
189
+ chartTheme,
190
+ }: {
191
+ data: Array<{ timestamp: number; avgTtftSeconds: number | null; avgTokensPerSecond: number | null }>;
192
+ color: string;
193
+ chartTheme: TableChartTheme;
194
+ }) {
195
+ const chartData = {
196
+ labels: data.map(d => format(new Date(d.timestamp), "MMM d")),
197
+ datasets: [
198
+ {
199
+ label: "TTFT",
200
+ data: data.map(d => d.avgTtftSeconds ?? null),
201
+ ...lineSeriesStyle("#fbbf24"),
202
+ yAxisID: "y" as const,
203
+ },
204
+ {
205
+ label: "Tokens/s",
206
+ data: data.map(d => d.avgTokensPerSecond ?? null),
207
+ ...lineSeriesStyle(color),
208
+ yAxisID: "y1" as const,
209
+ },
210
+ ],
211
+ };
212
+
213
+ const options = {
214
+ responsive: true,
215
+ maintainAspectRatio: false,
216
+ plugins: detailChartPlugins(chartTheme),
217
+ scales: detailChartScalesDualAxis(chartTheme),
218
+ };
219
+
220
+ return <Line data={chartData} options={options} />;
221
+ }
222
+
223
+ function buildModelPerformanceLookup(
224
+ points: ModelPerformancePoint[],
225
+ bucketCount: number,
226
+ bucketMs: number,
227
+ ): Map<string, ModelPerformanceSeries> {
228
+ const maxTimestamp = points.reduce((max, point) => Math.max(max, point.timestamp), 0);
229
+ const anchor = maxTimestamp > 0 ? maxTimestamp : Math.floor(Date.now() / bucketMs) * bucketMs;
230
+ const uniqueTimestamps = new Set(points.map(p => p.timestamp));
231
+ const effectiveCount = bucketCount > 0 ? bucketCount : Math.max(1, uniqueTimestamps.size);
232
+ const start = anchor - (effectiveCount - 1) * bucketMs;
233
+ const buckets = Array.from({ length: effectiveCount }, (_, index) => start + index * bucketMs);
234
+ const bucketIndex = new Map(buckets.map((timestamp, index) => [timestamp, index]));
235
+ const seriesByKey = new Map<string, ModelPerformanceSeries>();
236
+
237
+ for (const point of points) {
238
+ const key = `${point.model}::${point.provider}`;
239
+ let series = seriesByKey.get(key);
240
+ if (!series) {
241
+ series = {
242
+ label: `${point.model} (${point.provider})`,
243
+ data: buckets.map(timestamp => ({
244
+ timestamp,
245
+ avgTtftSeconds: null,
246
+ avgTokensPerSecond: null,
247
+ requests: 0,
248
+ })),
249
+ };
250
+ seriesByKey.set(key, series);
251
+ }
252
+
253
+ const index = bucketIndex.get(point.timestamp);
254
+ if (index === undefined) continue;
255
+
256
+ series.data[index] = {
257
+ timestamp: point.timestamp,
258
+ avgTtftSeconds: point.avgTtft !== null ? point.avgTtft / 1000 : null,
259
+ avgTokensPerSecond: point.avgTokensPerSecond,
260
+ requests: point.requests,
261
+ };
262
+ }
263
+
264
+ return seriesByKey;
265
+ }
@@ -0,0 +1,172 @@
1
+ import { Clock, Coins, FileJson, Gauge, Hash, Star, X, Zap } from "lucide-react";
2
+ import { useEffect, useState } from "react";
3
+ import { getRequestDetails } from "../api";
4
+ import type { RequestDetails } from "../types";
5
+
6
+ interface RequestDetailProps {
7
+ id: number;
8
+ onClose: () => void;
9
+ }
10
+
11
+ export function RequestDetail({ id, onClose }: RequestDetailProps) {
12
+ const [details, setDetails] = useState<RequestDetails | null>(null);
13
+ const [loading, setLoading] = useState(true);
14
+
15
+ useEffect(() => {
16
+ getRequestDetails(id)
17
+ .then(setDetails)
18
+ .catch(console.error)
19
+ .finally(() => setLoading(false));
20
+ }, [id]);
21
+
22
+ if (!details && loading) {
23
+ return (
24
+ <div className="fixed inset-0 bg-[var(--bg-overlay)] flex justify-center items-center z-[100]">
25
+ <div className="surface px-8 py-6">
26
+ <div className="flex items-center gap-3 text-[var(--text-secondary)]">
27
+ <div className="w-5 h-5 border-2 border-[var(--border-default)] border-t-[var(--accent-cyan)] rounded-full spin" />
28
+ <span>Loading...</span>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ if (!details) return null;
36
+
37
+ return (
38
+ <div
39
+ role="presentation"
40
+ className="fixed inset-0 bg-[var(--bg-overlay)] backdrop-blur-sm flex justify-end z-[100] animate-fade-in"
41
+ onClick={onClose}
42
+ >
43
+ <div
44
+ role="dialog"
45
+ aria-modal="true"
46
+ className="w-[600px] max-w-full bg-[var(--bg-page)] h-full overflow-y-auto border-l border-[var(--border-subtle)] animate-slide-up"
47
+ onClick={e => e.stopPropagation()}
48
+ >
49
+ {/* Header */}
50
+ <div className="sticky top-0 bg-[var(--bg-page)]/95 backdrop-blur border-b border-[var(--border-subtle)] px-6 py-4 flex justify-between items-center z-10">
51
+ <div className="flex items-center gap-3">
52
+ <div className="w-8 h-8 rounded-[var(--radius-sm)] bg-gradient-to-br from-[var(--accent-pink)]/20 to-[var(--accent-cyan)]/20 flex items-center justify-center">
53
+ <FileJson size={16} className="text-[var(--accent-cyan)]" />
54
+ </div>
55
+ <h2 className="text-lg font-semibold text-[var(--text-primary)]">Request Details</h2>
56
+ </div>
57
+ <button
58
+ type="button"
59
+ onClick={onClose}
60
+ className="p-2 rounded-[var(--radius-sm)] text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-colors"
61
+ >
62
+ <X size={20} />
63
+ </button>
64
+ </div>
65
+
66
+ <div className="p-6 space-y-6">
67
+ {/* Model Info */}
68
+ <div className="surface p-5">
69
+ <div className="flex items-center justify-between mb-4">
70
+ <div>
71
+ <div className="text-2xl font-bold text-[var(--text-primary)]">{details.model}</div>
72
+ <div className="text-sm text-[var(--text-muted)]">{details.provider}</div>
73
+ </div>
74
+ {details.errorMessage ? (
75
+ <span className="badge badge-error">Error</span>
76
+ ) : (
77
+ <span className="badge badge-success">Success</span>
78
+ )}
79
+ </div>
80
+ </div>
81
+
82
+ {/* Stats Grid */}
83
+ <div className="grid grid-cols-2 gap-4">
84
+ <div className="surface p-4">
85
+ <div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
86
+ <Coins size={14} />
87
+ <span className="text-xs uppercase tracking-wide">Cost</span>
88
+ </div>
89
+ <div className="text-xl font-semibold text-[var(--text-primary)]">
90
+ ${details.usage.cost.total.toFixed(4)}
91
+ </div>
92
+ </div>
93
+
94
+ <div className="surface p-4">
95
+ <div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
96
+ <Star size={14} />
97
+ <span className="text-xs uppercase tracking-wide">Premium Reqs</span>
98
+ </div>
99
+ <div className="text-xl font-semibold text-[var(--text-primary)]">
100
+ {(details.usage.premiumRequests ?? 0).toLocaleString()}
101
+ </div>
102
+ </div>
103
+ <div className="surface p-4">
104
+ <div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
105
+ <Hash size={14} />
106
+ <span className="text-xs uppercase tracking-wide">Tokens</span>
107
+ </div>
108
+ <div className="text-xl font-semibold text-[var(--text-primary)]">
109
+ {details.usage.totalTokens.toLocaleString()}
110
+ </div>
111
+ <div className="text-xs text-[var(--text-muted)] mt-1">
112
+ {details.usage.input.toLocaleString()} in · {details.usage.output.toLocaleString()} out
113
+ </div>
114
+ </div>
115
+
116
+ <div className="surface p-4">
117
+ <div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
118
+ <Clock size={14} />
119
+ <span className="text-xs uppercase tracking-wide">Duration</span>
120
+ </div>
121
+ <div className="text-xl font-semibold text-[var(--text-primary)]">
122
+ {details.duration ? `${(details.duration / 1000).toFixed(2)}s` : "-"}
123
+ </div>
124
+ </div>
125
+
126
+ <div className="surface p-4">
127
+ <div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
128
+ <Zap size={14} />
129
+ <span className="text-xs uppercase tracking-wide">TTFT</span>
130
+ </div>
131
+ <div className="text-xl font-semibold text-[var(--text-primary)]">
132
+ {details.ttft ? `${(details.ttft / 1000).toFixed(2)}s` : "-"}
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ {/* Tokens/Sec */}
138
+ {details.duration && details.usage.output > 0 && (
139
+ <div className="surface p-4">
140
+ <div className="flex items-center justify-between">
141
+ <div className="flex items-center gap-2 text-[var(--text-muted)]">
142
+ <Gauge size={14} />
143
+ <span className="text-xs uppercase tracking-wide">Throughput</span>
144
+ </div>
145
+ <span className="text-2xl font-bold gradient-text">
146
+ {((details.usage.output * 1000) / details.duration).toFixed(1)}
147
+ </span>
148
+ </div>
149
+ <div className="text-xs text-[var(--text-muted)] mt-1 text-right">tokens/second</div>
150
+ </div>
151
+ )}
152
+
153
+ {/* Output */}
154
+ <div>
155
+ <h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Output</h3>
156
+ <pre className="surface bg-[var(--bg-elevated)] p-4 rounded-[var(--radius-md)] text-sm font-mono text-[var(--text-secondary)] overflow-x-auto">
157
+ {JSON.stringify(details.output, null, 2)}
158
+ </pre>
159
+ </div>
160
+
161
+ {/* Raw Metadata */}
162
+ <div>
163
+ <h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Raw Metadata</h3>
164
+ <pre className="surface bg-[var(--bg-elevated)] p-4 rounded-[var(--radius-md)] text-xs font-mono text-[var(--text-muted)] overflow-x-auto">
165
+ {JSON.stringify(details, null, 2)}
166
+ </pre>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ );
172
+ }
@@ -0,0 +1,73 @@
1
+ import { formatDistanceToNow } from "date-fns";
2
+ import { CheckCircle2, XCircle } from "lucide-react";
3
+ import type { MessageStats } from "../types";
4
+
5
+ interface RequestListProps {
6
+ requests: MessageStats[];
7
+ onSelect: (req: MessageStats) => void;
8
+ title: string;
9
+ }
10
+
11
+ export function RequestList({ requests, onSelect, title }: RequestListProps) {
12
+ return (
13
+ <div className="surface overflow-hidden flex flex-col h-full">
14
+ <div className="px-5 py-4 border-b border-[var(--border-subtle)]">
15
+ <h3 className="text-sm font-semibold text-[var(--text-primary)]">{title}</h3>
16
+ </div>
17
+ <div className="overflow-auto flex-1">
18
+ <table className="w-full">
19
+ <thead className="bg-[var(--bg-elevated)] sticky top-0 z-10">
20
+ <tr>
21
+ <th className="text-left py-3 px-4 table-header">Model</th>
22
+ <th className="text-left py-3 px-4 table-header">Time</th>
23
+ <th className="text-right py-3 px-4 table-header">Tokens</th>
24
+ <th className="text-right py-3 px-4 table-header">Cost</th>
25
+ <th className="text-right py-3 px-4 table-header">Duration</th>
26
+ <th className="text-center py-3 px-4 table-header">Status</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ {requests.map(req => (
31
+ <tr
32
+ key={`${req.sessionFile}-${req.entryId}`}
33
+ onClick={() => onSelect(req)}
34
+ className="table-row cursor-pointer border-b border-[var(--border-subtle)] last:border-b-0"
35
+ >
36
+ <td className="py-3 px-4">
37
+ <div className="font-medium text-[var(--text-primary)] text-sm">{req.model}</div>
38
+ <div className="text-xs text-[var(--text-muted)]">{req.provider}</div>
39
+ </td>
40
+ <td className="py-3 px-4 text-sm text-[var(--text-secondary)]">
41
+ {formatDistanceToNow(req.timestamp, { addSuffix: true })}
42
+ </td>
43
+ <td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
44
+ {req.usage.totalTokens.toLocaleString()}
45
+ </td>
46
+ <td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
47
+ ${req.usage.cost.total.toFixed(4)}
48
+ </td>
49
+ <td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
50
+ {req.duration ? `${(req.duration / 1000).toFixed(1)}s` : "-"}
51
+ </td>
52
+ <td className="py-3 px-4 text-center">
53
+ {req.errorMessage ? (
54
+ <XCircle size={16} className="text-[var(--accent-red)] mx-auto" />
55
+ ) : (
56
+ <CheckCircle2 size={16} className="text-[var(--accent-green)] mx-auto" />
57
+ )}
58
+ </td>
59
+ </tr>
60
+ ))}
61
+ {requests.length === 0 && (
62
+ <tr>
63
+ <td colSpan={6} className="py-12 text-center text-[var(--text-muted)] text-sm">
64
+ No requests found
65
+ </td>
66
+ </tr>
67
+ )}
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,135 @@
1
+ import { Activity, AlertCircle, BarChart3, Database, Download, Server, Star, Upload, Zap } from "lucide-react";
2
+ import type { AggregatedStats } from "../types";
3
+
4
+ interface StatsGridProps {
5
+ stats: AggregatedStats;
6
+ }
7
+
8
+ const compactNumberFormatter = new Intl.NumberFormat(undefined, {
9
+ notation: "compact",
10
+ maximumFractionDigits: 1,
11
+ });
12
+
13
+ function formatCompactNumber(value: number): string {
14
+ return compactNumberFormatter.format(value);
15
+ }
16
+
17
+ function formatExactNumber(value: number): string {
18
+ return value.toLocaleString();
19
+ }
20
+
21
+ const totalPromptCompletionTokens = (stats: AggregatedStats) => stats.totalInputTokens + stats.totalOutputTokens;
22
+
23
+ const statConfig = [
24
+ {
25
+ key: "requests",
26
+ title: "Total Requests",
27
+ icon: Server,
28
+ color: "var(--accent-violet)",
29
+ getValue: (s: AggregatedStats) => s.totalRequests.toLocaleString(),
30
+ getDetail: (s: AggregatedStats) =>
31
+ `${s.successfulRequests.toLocaleString()} success · ${s.failedRequests.toLocaleString()} errors`,
32
+ },
33
+ {
34
+ key: "cost",
35
+ title: "Total Cost",
36
+ icon: Activity,
37
+ color: "var(--accent-pink)",
38
+ getValue: (s: AggregatedStats) => `$${s.totalCost.toFixed(2)}`,
39
+ getDetail: (s: AggregatedStats) =>
40
+ s.totalRequests > 0 ? `$${(s.totalCost / s.totalRequests).toFixed(4)} avg/req` : "-",
41
+ },
42
+ {
43
+ key: "premiumRequests",
44
+ title: "Premium Reqs",
45
+ icon: Star,
46
+ color: "var(--accent-amber)",
47
+ getValue: (s: AggregatedStats) => formatExactNumber(s.totalPremiumRequests),
48
+ getDetail: (s: AggregatedStats) =>
49
+ s.totalRequests > 0 ? `${((s.totalPremiumRequests / s.totalRequests) * 100).toFixed(1)}% of requests` : "-",
50
+ },
51
+ {
52
+ key: "cache",
53
+ title: "Cache Rate",
54
+ icon: Database,
55
+ color: "var(--accent-cyan)",
56
+ getValue: (s: AggregatedStats) => `${(s.cacheRate * 100).toFixed(1)}%`,
57
+ getDetail: (s: AggregatedStats) => `${formatCompactNumber(s.totalCacheReadTokens)} cached tokens`,
58
+ },
59
+ {
60
+ key: "inputTokens",
61
+ title: "Input Tokens",
62
+ icon: Download,
63
+ color: "var(--accent-violet)",
64
+ getValue: (s: AggregatedStats) => formatExactNumber(s.totalInputTokens),
65
+ getDetail: (s: AggregatedStats) =>
66
+ totalPromptCompletionTokens(s) > 0
67
+ ? `${((s.totalInputTokens / totalPromptCompletionTokens(s)) * 100).toFixed(1)}% of prompt+completion`
68
+ : "-",
69
+ },
70
+ {
71
+ key: "outputTokens",
72
+ title: "Output Tokens",
73
+ icon: Upload,
74
+ color: "var(--accent-pink)",
75
+ getValue: (s: AggregatedStats) => formatExactNumber(s.totalOutputTokens),
76
+ getDetail: (s: AggregatedStats) =>
77
+ totalPromptCompletionTokens(s) > 0
78
+ ? `${((s.totalOutputTokens / totalPromptCompletionTokens(s)) * 100).toFixed(1)}% of prompt+completion`
79
+ : "-",
80
+ },
81
+ {
82
+ key: "errors",
83
+ title: "Error Rate",
84
+ icon: AlertCircle,
85
+ color: "var(--accent-red)",
86
+ getValue: (s: AggregatedStats) => `${(s.errorRate * 100).toFixed(1)}%`,
87
+ getDetail: (s: AggregatedStats) => `${s.failedRequests.toLocaleString()} failed requests`,
88
+ },
89
+ {
90
+ key: "tokens",
91
+ title: "Tokens/Sec",
92
+ icon: BarChart3,
93
+ color: "var(--accent-green)",
94
+ getValue: (s: AggregatedStats) => s.avgTokensPerSecond?.toFixed(1) ?? "-",
95
+ getDetail: (s: AggregatedStats) =>
96
+ `${formatCompactNumber(totalPromptCompletionTokens(s))} total prompt+completion`,
97
+ },
98
+ {
99
+ key: "ttft",
100
+ title: "TTFT",
101
+ icon: Zap,
102
+ color: "var(--accent-amber)",
103
+ getValue: (s: AggregatedStats) => (s.avgTtft ? `${(s.avgTtft / 1000).toFixed(2)}s` : "-"),
104
+ getDetail: () => "Time to first token",
105
+ },
106
+ ];
107
+
108
+ export function StatsGrid({ stats }: StatsGridProps) {
109
+ return (
110
+ <div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-9 gap-4 mb-8">
111
+ {statConfig.map(stat => {
112
+ const Icon = stat.icon;
113
+ return (
114
+ <div key={stat.key} className="stat-card group">
115
+ <div className="flex items-center justify-between mb-3">
116
+ <span className="text-sm font-medium text-[var(--text-secondary)]">{stat.title}</span>
117
+ <div
118
+ className="p-2 rounded-[var(--radius-sm)] transition-colors"
119
+ style={{ backgroundColor: `${stat.color}15` }}
120
+ >
121
+ <Icon
122
+ size={18}
123
+ style={{ color: stat.color }}
124
+ className="transition-transform group-hover:scale-110"
125
+ />
126
+ </div>
127
+ </div>
128
+ <div className="text-2xl font-bold text-[var(--text-primary)] mb-1">{stat.getValue(stats)}</div>
129
+ <div className="text-xs text-[var(--text-muted)] truncate">{stat.getDetail(stats)}</div>
130
+ </div>
131
+ );
132
+ })}
133
+ </div>
134
+ );
135
+ }