@oh-my-pi/omp-stats 16.0.4 → 16.0.5
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.
- package/CHANGELOG.md +15 -0
- package/build.ts +11 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +11 -0
- package/dist/client/index.js +108 -108
- package/dist/client/styles.css +1070 -631
- package/dist/types/client/api.d.ts +19 -10
- package/dist/types/client/app/AppLayout.d.ts +16 -0
- package/dist/types/client/app/NavRail.d.ts +7 -0
- package/dist/types/client/app/RangeControl.d.ts +7 -0
- package/dist/types/client/app/SyncButton.d.ts +14 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/TopBar.d.ts +15 -0
- package/dist/types/client/app/routes.d.ts +12 -0
- package/dist/types/client/components/chart-shared.d.ts +26 -40
- package/dist/types/client/components/models-table-shared.d.ts +20 -40
- package/dist/types/client/data/charts.d.ts +1 -0
- package/dist/types/client/data/formatters.d.ts +7 -0
- package/dist/types/client/data/useHashRoute.d.ts +8 -0
- package/dist/types/client/data/useResource.d.ts +13 -0
- package/dist/types/client/data/view-models.d.ts +37 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
- package/dist/types/client/routes/CostsRoute.d.ts +7 -0
- package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
- package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
- package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
- package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
- package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
- package/dist/types/client/routes/index.d.ts +7 -0
- package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
- package/dist/types/client/ui/DataTable.d.ts +17 -0
- package/dist/types/client/ui/EmptyState.d.ts +7 -0
- package/dist/types/client/ui/ErrorState.d.ts +6 -0
- package/dist/types/client/ui/JsonBlock.d.ts +7 -0
- package/dist/types/client/ui/MetricCluster.d.ts +5 -0
- package/dist/types/client/ui/Panel.d.ts +7 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
- package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
- package/dist/types/client/ui/Skeleton.d.ts +8 -0
- package/dist/types/client/ui/StatusPill.d.ts +7 -0
- package/dist/types/client/ui/index.d.ts +11 -0
- package/dist/types/client/useSystemTheme.d.ts +9 -0
- package/package.json +4 -4
- package/src/aggregator.ts +4 -3
- package/src/client/App.tsx +89 -207
- package/src/client/api.ts +55 -37
- package/src/client/app/AppLayout.tsx +93 -0
- package/src/client/app/NavRail.tsx +44 -0
- package/src/client/app/RangeControl.tsx +39 -0
- package/src/client/app/SyncButton.tsx +75 -0
- package/src/client/app/ThemeToggle.tsx +37 -0
- package/src/client/app/TopBar.tsx +73 -0
- package/src/client/app/routes.ts +50 -0
- package/src/client/components/chart-shared.tsx +28 -91
- package/src/client/components/models-table-shared.tsx +9 -29
- package/src/client/components/range-meta.ts +3 -2
- package/src/client/data/charts.ts +14 -0
- package/src/client/data/formatters.ts +38 -0
- package/src/client/data/useHashRoute.ts +85 -0
- package/src/client/data/useResource.ts +154 -0
- package/src/client/data/view-models.ts +178 -0
- package/src/client/index.tsx +4 -0
- package/src/client/routes/BehaviorRoute.tsx +623 -0
- package/src/client/routes/CostsRoute.tsx +234 -0
- package/src/client/routes/ErrorsRoute.tsx +118 -0
- package/src/client/routes/ModelsRoute.tsx +430 -0
- package/src/client/routes/OverviewRoute.tsx +332 -0
- package/src/client/routes/ProjectsRoute.tsx +163 -0
- package/src/client/routes/RequestsRoute.tsx +123 -0
- package/src/client/routes/index.ts +7 -0
- package/src/client/styles.css +1242 -225
- package/src/client/ui/AsyncBoundary.tsx +54 -0
- package/src/client/ui/DataTable.tsx +122 -0
- package/src/client/ui/EmptyState.tsx +16 -0
- package/src/client/ui/ErrorState.tsx +25 -0
- package/src/client/ui/JsonBlock.tsx +75 -0
- package/src/client/ui/MetricCluster.tsx +67 -0
- package/src/client/ui/Panel.tsx +24 -0
- package/src/client/ui/RequestDrawer.tsx +208 -0
- package/src/client/ui/SegmentedControl.tsx +36 -0
- package/src/client/ui/Skeleton.tsx +17 -0
- package/src/client/ui/StatusPill.tsx +15 -0
- package/src/client/ui/index.ts +11 -0
- package/src/client/useSystemTheme.ts +73 -17
- package/dist/types/client/components/BehaviorChart.d.ts +0 -6
- package/dist/types/client/components/BehaviorModelsTable.d.ts +0 -7
- package/dist/types/client/components/BehaviorSummary.d.ts +0 -7
- package/dist/types/client/components/ChartsContainer.d.ts +0 -7
- package/dist/types/client/components/CostChart.d.ts +0 -6
- package/dist/types/client/components/CostSummary.d.ts +0 -6
- package/dist/types/client/components/Header.d.ts +0 -12
- package/dist/types/client/components/ModelsTable.d.ts +0 -8
- package/dist/types/client/components/RequestDetail.d.ts +0 -6
- package/dist/types/client/components/RequestList.d.ts +0 -8
- package/dist/types/client/components/StatsGrid.d.ts +0 -6
- package/src/client/components/BehaviorChart.tsx +0 -189
- package/src/client/components/BehaviorModelsTable.tsx +0 -342
- package/src/client/components/BehaviorSummary.tsx +0 -95
- package/src/client/components/ChartsContainer.tsx +0 -221
- package/src/client/components/CostChart.tsx +0 -171
- package/src/client/components/CostSummary.tsx +0 -53
- package/src/client/components/Header.tsx +0 -72
- package/src/client/components/ModelsTable.tsx +0 -265
- package/src/client/components/RequestDetail.tsx +0 -172
- package/src/client/components/RequestList.tsx +0 -73
- package/src/client/components/StatsGrid.tsx +0 -135
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { format } from "date-fns";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { Line } from "react-chartjs-2";
|
|
4
|
+
import { getOverviewStats, getRecentRequests } from "../api";
|
|
5
|
+
import { CHART_THEMES } from "../components/chart-shared";
|
|
6
|
+
import { formatCost, formatDurationMs, formatInteger, formatRelativeTime } from "../data/formatters";
|
|
7
|
+
import { useResource } from "../data/useResource";
|
|
8
|
+
import type { MessageStats, TimeRange } from "../types";
|
|
9
|
+
import { AsyncBoundary, DataTable, MetricCluster, Panel, Skeleton, StatusPill } from "../ui";
|
|
10
|
+
import { useSystemTheme } from "../useSystemTheme";
|
|
11
|
+
|
|
12
|
+
export interface OverviewRouteProps {
|
|
13
|
+
active: boolean;
|
|
14
|
+
range: TimeRange;
|
|
15
|
+
refreshTrigger: number;
|
|
16
|
+
onRequestClick: (id: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function OverviewRoute({ active, range, refreshTrigger, onRequestClick }: OverviewRouteProps) {
|
|
20
|
+
const {
|
|
21
|
+
data: overview,
|
|
22
|
+
error: overviewError,
|
|
23
|
+
loading: overviewLoading,
|
|
24
|
+
} = useResource(["overview", range, refreshTrigger], signal => getOverviewStats(range, signal), {
|
|
25
|
+
pollMs: 30000,
|
|
26
|
+
enabled: active,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
data: recentRequests,
|
|
31
|
+
error: requestsError,
|
|
32
|
+
loading: requestsLoading,
|
|
33
|
+
} = useResource(["recent-requests", refreshTrigger], signal => getRecentRequests(50, signal), {
|
|
34
|
+
pollMs: 30000,
|
|
35
|
+
enabled: active,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const theme = useSystemTheme();
|
|
39
|
+
const chartTheme = CHART_THEMES[theme];
|
|
40
|
+
|
|
41
|
+
const chartData = useMemo(() => {
|
|
42
|
+
if (!overview?.timeSeries) return { labels: [], datasets: [] };
|
|
43
|
+
const labels = overview.timeSeries.map(pt =>
|
|
44
|
+
format(new Date(pt.timestamp), range === "1h" || range === "24h" ? "HH:mm" : "MMM d"),
|
|
45
|
+
);
|
|
46
|
+
// Show point markers when the series is sparse (e.g. a quiet 1h window)
|
|
47
|
+
// so a 1-2 point line is still visible instead of an empty plot.
|
|
48
|
+
const pointRadius = overview.timeSeries.length <= 2 ? 3 : 0;
|
|
49
|
+
return {
|
|
50
|
+
labels,
|
|
51
|
+
datasets: [
|
|
52
|
+
{
|
|
53
|
+
label: "Requests",
|
|
54
|
+
data: overview.timeSeries.map(pt => pt.requests),
|
|
55
|
+
borderColor: "#5ad8e6",
|
|
56
|
+
backgroundColor: "rgba(90, 216, 230, 0.12)",
|
|
57
|
+
tension: 0.2,
|
|
58
|
+
borderWidth: 2,
|
|
59
|
+
pointRadius,
|
|
60
|
+
pointHoverRadius: 4,
|
|
61
|
+
fill: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Errors",
|
|
65
|
+
data: overview.timeSeries.map(pt => pt.errors),
|
|
66
|
+
borderColor: "#ff6b7d",
|
|
67
|
+
backgroundColor: "rgba(255, 107, 125, 0.12)",
|
|
68
|
+
tension: 0.2,
|
|
69
|
+
borderWidth: 2,
|
|
70
|
+
pointRadius,
|
|
71
|
+
pointHoverRadius: 4,
|
|
72
|
+
fill: true,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}, [overview?.timeSeries, range]);
|
|
77
|
+
|
|
78
|
+
const chartOptions = useMemo(() => {
|
|
79
|
+
return {
|
|
80
|
+
responsive: true,
|
|
81
|
+
maintainAspectRatio: false,
|
|
82
|
+
interaction: {
|
|
83
|
+
mode: "index" as const,
|
|
84
|
+
intersect: false,
|
|
85
|
+
},
|
|
86
|
+
plugins: {
|
|
87
|
+
legend: {
|
|
88
|
+
display: true,
|
|
89
|
+
position: "top" as const,
|
|
90
|
+
align: "end" as const,
|
|
91
|
+
labels: {
|
|
92
|
+
color: chartTheme.legendLabel,
|
|
93
|
+
boxWidth: 8,
|
|
94
|
+
usePointStyle: true,
|
|
95
|
+
font: { size: 11 },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
tooltip: {
|
|
99
|
+
backgroundColor: chartTheme.tooltipBackground,
|
|
100
|
+
titleColor: chartTheme.tooltipTitle,
|
|
101
|
+
bodyColor: chartTheme.tooltipBody,
|
|
102
|
+
borderColor: chartTheme.tooltipBorder,
|
|
103
|
+
borderWidth: 1,
|
|
104
|
+
cornerRadius: 8,
|
|
105
|
+
padding: 10,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
scales: {
|
|
109
|
+
x: {
|
|
110
|
+
grid: {
|
|
111
|
+
color: chartTheme.grid,
|
|
112
|
+
drawBorder: false,
|
|
113
|
+
},
|
|
114
|
+
ticks: {
|
|
115
|
+
color: chartTheme.tick,
|
|
116
|
+
font: { size: 10 },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
y: {
|
|
120
|
+
grid: {
|
|
121
|
+
color: chartTheme.grid,
|
|
122
|
+
drawBorder: false,
|
|
123
|
+
},
|
|
124
|
+
ticks: {
|
|
125
|
+
color: chartTheme.tick,
|
|
126
|
+
font: { size: 10 },
|
|
127
|
+
},
|
|
128
|
+
min: 0,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}, [chartTheme]);
|
|
133
|
+
|
|
134
|
+
const columns = useMemo(
|
|
135
|
+
() => [
|
|
136
|
+
{
|
|
137
|
+
key: "model",
|
|
138
|
+
header: "Model",
|
|
139
|
+
render: (item: MessageStats) => (
|
|
140
|
+
<div>
|
|
141
|
+
<div className="stats-font-medium stats-text-primary">{item.model}</div>
|
|
142
|
+
<div className="stats-text-xs stats-text-muted">{item.provider}</div>
|
|
143
|
+
</div>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: "timestamp",
|
|
148
|
+
header: "Time",
|
|
149
|
+
render: (item: MessageStats) => formatRelativeTime(item.timestamp),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: "tokens",
|
|
153
|
+
header: "Tokens",
|
|
154
|
+
numeric: true,
|
|
155
|
+
render: (item: MessageStats) => formatInteger(item.usage.totalTokens),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: "cost",
|
|
159
|
+
header: "Cost",
|
|
160
|
+
numeric: true,
|
|
161
|
+
render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: "duration",
|
|
165
|
+
header: "Duration",
|
|
166
|
+
numeric: true,
|
|
167
|
+
render: (item: MessageStats) => formatDurationMs(item.duration),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: "status",
|
|
171
|
+
header: "Status",
|
|
172
|
+
className: "stats-text-center",
|
|
173
|
+
render: (item: MessageStats) => (
|
|
174
|
+
<StatusPill variant={item.errorMessage ? "danger" : "success"}>
|
|
175
|
+
{item.errorMessage ? "Failed" : "Success"}
|
|
176
|
+
</StatusPill>
|
|
177
|
+
),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
[],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const renderMobileCard = (item: MessageStats, onClick?: () => void) => (
|
|
184
|
+
<div className="stats-mobile-card" onClick={onClick}>
|
|
185
|
+
<div className="stats-mobile-card-header">
|
|
186
|
+
<div>
|
|
187
|
+
<div className="stats-font-semibold stats-text-primary">{item.model}</div>
|
|
188
|
+
<div className="stats-text-xs stats-text-muted">{item.provider}</div>
|
|
189
|
+
</div>
|
|
190
|
+
<StatusPill variant={item.errorMessage ? "danger" : "success"}>
|
|
191
|
+
{item.errorMessage ? "Failed" : "Success"}
|
|
192
|
+
</StatusPill>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="stats-mobile-card-grid">
|
|
195
|
+
<div>
|
|
196
|
+
<div className="stats-mobile-card-label">Time</div>
|
|
197
|
+
<div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
|
|
198
|
+
</div>
|
|
199
|
+
<div>
|
|
200
|
+
<div className="stats-mobile-card-label">Cost</div>
|
|
201
|
+
<div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
|
|
202
|
+
</div>
|
|
203
|
+
<div>
|
|
204
|
+
<div className="stats-mobile-card-label">Tokens</div>
|
|
205
|
+
<div className="stats-mobile-card-value">{formatInteger(item.usage.totalTokens)}</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div>
|
|
208
|
+
<div className="stats-mobile-card-label">Duration</div>
|
|
209
|
+
<div className="stats-mobile-card-value">{formatDurationMs(item.duration)}</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
{item.errorMessage && <div className="stats-mobile-card-error truncate mt-2">{item.errorMessage}</div>}
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const previewRequests = useMemo(() => {
|
|
217
|
+
if (!recentRequests) return [];
|
|
218
|
+
return recentRequests.slice(0, 10);
|
|
219
|
+
}, [recentRequests]);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="stats-route-container space-y-6">
|
|
223
|
+
<AsyncBoundary loading={overviewLoading} error={overviewError} data={overview}>
|
|
224
|
+
{overview && <MetricCluster stats={overview.overall} />}
|
|
225
|
+
</AsyncBoundary>
|
|
226
|
+
|
|
227
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
228
|
+
<div className="lg:col-span-2">
|
|
229
|
+
<Panel title="System Throughput" subtitle="Request volume and errors over time">
|
|
230
|
+
<AsyncBoundary loading={overviewLoading} error={overviewError} data={overview}>
|
|
231
|
+
<div className="h-[280px]">
|
|
232
|
+
{overview?.timeSeries && overview.timeSeries.length > 0 ? (
|
|
233
|
+
<Line data={chartData} options={chartOptions} />
|
|
234
|
+
) : (
|
|
235
|
+
<div className="h-full flex items-center justify-center text-stats-muted text-sm">
|
|
236
|
+
No time-series data available
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</AsyncBoundary>
|
|
241
|
+
</Panel>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div>
|
|
245
|
+
<Panel title="Operational Feed" subtitle="Real-time request log">
|
|
246
|
+
<AsyncBoundary
|
|
247
|
+
loading={requestsLoading}
|
|
248
|
+
error={requestsError}
|
|
249
|
+
data={recentRequests}
|
|
250
|
+
fallback={
|
|
251
|
+
<div className="space-y-4">
|
|
252
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
253
|
+
<div key={i} className="flex items-center gap-3">
|
|
254
|
+
<Skeleton variant="circle" width={10} height={10} />
|
|
255
|
+
<div className="flex-1">
|
|
256
|
+
<Skeleton variant="text" width="60%" height={16} />
|
|
257
|
+
<Skeleton variant="text" width="40%" height={12} />
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
}
|
|
263
|
+
>
|
|
264
|
+
<div className="stats-feed-ledger overflow-y-auto max-h-[280px] pr-2">
|
|
265
|
+
{previewRequests.map(req => {
|
|
266
|
+
const isError = !!req.errorMessage;
|
|
267
|
+
return (
|
|
268
|
+
<div
|
|
269
|
+
key={req.id || `${req.sessionFile}-${req.entryId}`}
|
|
270
|
+
className="stats-feed-item flex items-start gap-3 p-2 rounded hover:bg-stats-surface-2 cursor-pointer transition-colors"
|
|
271
|
+
onClick={() => req.id && onRequestClick(req.id)}
|
|
272
|
+
>
|
|
273
|
+
<div
|
|
274
|
+
className={`w-2 h-2 mt-1.5 rounded-full flex-shrink-0 ${
|
|
275
|
+
isError ? "bg-stats-danger" : "bg-stats-success"
|
|
276
|
+
}`}
|
|
277
|
+
/>
|
|
278
|
+
<div className="flex-1 min-w-0">
|
|
279
|
+
<div className="flex justify-between items-baseline gap-2">
|
|
280
|
+
<div className="stats-font-medium stats-text-primary text-sm truncate">
|
|
281
|
+
{req.model}
|
|
282
|
+
</div>
|
|
283
|
+
<div className="stats-text-xs stats-text-muted whitespace-nowrap">
|
|
284
|
+
{formatRelativeTime(req.timestamp)}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="flex justify-between items-center text-xs stats-text-muted mt-0.5">
|
|
288
|
+
<div>{req.provider}</div>
|
|
289
|
+
<div>
|
|
290
|
+
{req.duration ? formatDurationMs(req.duration) : ""}{" "}
|
|
291
|
+
{req.usage?.cost?.total ? `· ${formatCost(req.usage.cost.total, 4)}` : ""}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
{isError && (
|
|
295
|
+
<div className="text-xs text-stats-danger truncate mt-1">{req.errorMessage}</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
})}
|
|
301
|
+
{previewRequests.length === 0 && (
|
|
302
|
+
<div className="py-8 text-center stats-text-muted text-sm">No recent requests found</div>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
</AsyncBoundary>
|
|
306
|
+
</Panel>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<Panel
|
|
311
|
+
title="Recent Requests Preview"
|
|
312
|
+
subtitle="Latest transactions processed by the proxy"
|
|
313
|
+
actions={
|
|
314
|
+
<a href={`#/requests?range=${range}`} className="stats-button stats-button-secondary text-xs">
|
|
315
|
+
View All Requests
|
|
316
|
+
</a>
|
|
317
|
+
}
|
|
318
|
+
>
|
|
319
|
+
<AsyncBoundary loading={requestsLoading} error={requestsError} data={recentRequests}>
|
|
320
|
+
<DataTable
|
|
321
|
+
columns={columns}
|
|
322
|
+
data={previewRequests}
|
|
323
|
+
keyExtractor={item => item.id || `${item.sessionFile}-${item.entryId}`}
|
|
324
|
+
onRowClick={item => item.id && onRequestClick(item.id)}
|
|
325
|
+
renderMobileCard={renderMobileCard}
|
|
326
|
+
emptyText="No recent requests found"
|
|
327
|
+
/>
|
|
328
|
+
</AsyncBoundary>
|
|
329
|
+
</Panel>
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { getFolderStats } from "../api";
|
|
3
|
+
import { formatCost, formatDurationMs, formatInteger, formatPercent } from "../data/formatters";
|
|
4
|
+
import { useResource } from "../data/useResource";
|
|
5
|
+
import { buildFolderRows, type FolderRowView } from "../data/view-models";
|
|
6
|
+
import type { TimeRange } from "../types";
|
|
7
|
+
import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
|
|
8
|
+
|
|
9
|
+
export interface ProjectsRouteProps {
|
|
10
|
+
active: boolean;
|
|
11
|
+
range: TimeRange;
|
|
12
|
+
refreshTrigger: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ProjectsRoute({ active, range, refreshTrigger }: ProjectsRouteProps) {
|
|
16
|
+
const {
|
|
17
|
+
data: foldersData,
|
|
18
|
+
error,
|
|
19
|
+
loading,
|
|
20
|
+
} = useResource(["projects", range, refreshTrigger], signal => getFolderStats(range, signal), {
|
|
21
|
+
pollMs: 30000,
|
|
22
|
+
enabled: active,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const folderRows = useMemo(() => {
|
|
26
|
+
if (!foldersData) return [];
|
|
27
|
+
return buildFolderRows(foldersData);
|
|
28
|
+
}, [foldersData]);
|
|
29
|
+
|
|
30
|
+
const columns = useMemo(
|
|
31
|
+
() => [
|
|
32
|
+
{
|
|
33
|
+
key: "folder",
|
|
34
|
+
header: "Project/Folder",
|
|
35
|
+
render: (item: FolderRowView) => (
|
|
36
|
+
<div
|
|
37
|
+
className="stats-font-medium stats-text-primary truncate max-w-[440px]"
|
|
38
|
+
title={item.folder || "(root)"}
|
|
39
|
+
>
|
|
40
|
+
{item.folder || "(root)"}
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "totalRequests",
|
|
46
|
+
header: "Requests",
|
|
47
|
+
numeric: true,
|
|
48
|
+
render: (item: FolderRowView) => (
|
|
49
|
+
<div className="stats-text-right">
|
|
50
|
+
<div className="font-mono">{formatInteger(item.totalRequests)}</div>
|
|
51
|
+
<div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
|
|
52
|
+
<div
|
|
53
|
+
className="stats-progress-bar-fill"
|
|
54
|
+
data-variant="link"
|
|
55
|
+
style={{ width: `${item.requestsPercentage}%` }}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "totalCost",
|
|
63
|
+
header: "Cost",
|
|
64
|
+
numeric: true,
|
|
65
|
+
render: (item: FolderRowView) => (
|
|
66
|
+
<div className="stats-text-right">
|
|
67
|
+
<div className="font-mono">{formatCost(item.totalCost)}</div>
|
|
68
|
+
<div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
|
|
69
|
+
<div
|
|
70
|
+
className="stats-progress-bar-fill"
|
|
71
|
+
data-variant="success"
|
|
72
|
+
style={{ width: `${item.costPercentage}%` }}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "totalTokens",
|
|
80
|
+
header: "Tokens",
|
|
81
|
+
numeric: true,
|
|
82
|
+
render: (item: FolderRowView) => (
|
|
83
|
+
<div className="font-mono">{formatInteger(item.totalInputTokens + item.totalOutputTokens)}</div>
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "cacheRate",
|
|
88
|
+
header: "Cache Rate",
|
|
89
|
+
numeric: true,
|
|
90
|
+
render: (item: FolderRowView) => (
|
|
91
|
+
<span className="stats-text-success font-medium">{formatPercent(item.cacheRate)}</span>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "errorRate",
|
|
96
|
+
header: "Error Rate",
|
|
97
|
+
numeric: true,
|
|
98
|
+
render: (item: FolderRowView) => (
|
|
99
|
+
<StatusPill variant={item.errorRate > 0.1 ? "danger" : item.errorRate > 0 ? "warning" : "success"}>
|
|
100
|
+
{formatPercent(item.errorRate)}
|
|
101
|
+
</StatusPill>
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "avgDuration",
|
|
106
|
+
header: "Avg Duration",
|
|
107
|
+
numeric: true,
|
|
108
|
+
render: (item: FolderRowView) => formatDurationMs(item.avgDuration),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
[],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const renderMobileCard = (item: FolderRowView) => (
|
|
115
|
+
<div className="stats-mobile-card">
|
|
116
|
+
<div className="stats-mobile-card-header mb-2">
|
|
117
|
+
<div className="stats-font-semibold stats-text-primary">{item.folder || "(root)"}</div>
|
|
118
|
+
<StatusPill variant={item.errorRate > 0.1 ? "danger" : item.errorRate > 0 ? "warning" : "success"}>
|
|
119
|
+
{formatPercent(item.errorRate)} Err
|
|
120
|
+
</StatusPill>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="stats-mobile-card-grid">
|
|
123
|
+
<div>
|
|
124
|
+
<div className="stats-mobile-card-label">Requests</div>
|
|
125
|
+
<div className="stats-mobile-card-value font-mono">{formatInteger(item.totalRequests)}</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div>
|
|
128
|
+
<div className="stats-mobile-card-label">Cost</div>
|
|
129
|
+
<div className="stats-mobile-card-value font-mono">{formatCost(item.totalCost)}</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div>
|
|
132
|
+
<div className="stats-mobile-card-label">Cache</div>
|
|
133
|
+
<div className="stats-mobile-card-value">{formatPercent(item.cacheRate)}</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<div className="stats-mobile-card-label">Duration</div>
|
|
137
|
+
<div className="stats-mobile-card-value">{formatDurationMs(item.avgDuration)}</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="stats-route-container">
|
|
145
|
+
<Panel title="Projects & Folders" subtitle="Aggregate proxy metrics grouped by folder path">
|
|
146
|
+
<AsyncBoundary
|
|
147
|
+
loading={loading}
|
|
148
|
+
error={error}
|
|
149
|
+
data={foldersData}
|
|
150
|
+
emptyText="No project folders recorded for this range."
|
|
151
|
+
>
|
|
152
|
+
<DataTable
|
|
153
|
+
columns={columns}
|
|
154
|
+
data={folderRows}
|
|
155
|
+
keyExtractor={item => item.folder}
|
|
156
|
+
renderMobileCard={renderMobileCard}
|
|
157
|
+
emptyText="No project folders recorded for this range."
|
|
158
|
+
/>
|
|
159
|
+
</AsyncBoundary>
|
|
160
|
+
</Panel>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { getRecentRequests } from "../api";
|
|
3
|
+
import { formatCost, formatDurationMs, formatInteger, formatRelativeTime } from "../data/formatters";
|
|
4
|
+
import { useResource } from "../data/useResource";
|
|
5
|
+
import type { MessageStats, TimeRange } from "../types";
|
|
6
|
+
import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
|
|
7
|
+
|
|
8
|
+
export interface RequestsRouteProps {
|
|
9
|
+
active: boolean;
|
|
10
|
+
range: TimeRange;
|
|
11
|
+
refreshTrigger: number;
|
|
12
|
+
onRequestClick: (id: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function RequestsRoute({ active, refreshTrigger, onRequestClick }: RequestsRouteProps) {
|
|
16
|
+
const {
|
|
17
|
+
data: recentRequests,
|
|
18
|
+
error,
|
|
19
|
+
loading,
|
|
20
|
+
} = useResource(["recent-requests-dense", refreshTrigger], signal => getRecentRequests(50, signal), {
|
|
21
|
+
pollMs: 30000,
|
|
22
|
+
enabled: active,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const columns = useMemo(
|
|
26
|
+
() => [
|
|
27
|
+
{
|
|
28
|
+
key: "model",
|
|
29
|
+
header: "Model",
|
|
30
|
+
render: (item: MessageStats) => (
|
|
31
|
+
<div>
|
|
32
|
+
<div className="stats-font-medium stats-text-primary">{item.model}</div>
|
|
33
|
+
<div className="stats-text-xs stats-text-muted">{item.provider}</div>
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "timestamp",
|
|
39
|
+
header: "Time",
|
|
40
|
+
render: (item: MessageStats) => formatRelativeTime(item.timestamp),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "tokens",
|
|
44
|
+
header: "Tokens",
|
|
45
|
+
numeric: true,
|
|
46
|
+
render: (item: MessageStats) => formatInteger(item.usage.totalTokens),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "cost",
|
|
50
|
+
header: "Cost",
|
|
51
|
+
numeric: true,
|
|
52
|
+
render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "duration",
|
|
56
|
+
header: "Duration",
|
|
57
|
+
numeric: true,
|
|
58
|
+
render: (item: MessageStats) => formatDurationMs(item.duration),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "status",
|
|
62
|
+
header: "Status",
|
|
63
|
+
className: "stats-text-center",
|
|
64
|
+
render: (item: MessageStats) => (
|
|
65
|
+
<StatusPill variant={item.errorMessage ? "danger" : "success"}>
|
|
66
|
+
{item.errorMessage ? "Failed" : "Success"}
|
|
67
|
+
</StatusPill>
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
[],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const renderMobileCard = (item: MessageStats, onClick?: () => void) => (
|
|
75
|
+
<div className="stats-mobile-card" onClick={onClick}>
|
|
76
|
+
<div className="stats-mobile-card-header">
|
|
77
|
+
<div>
|
|
78
|
+
<div className="stats-font-semibold stats-text-primary">{item.model}</div>
|
|
79
|
+
<div className="stats-text-xs stats-text-muted">{item.provider}</div>
|
|
80
|
+
</div>
|
|
81
|
+
<StatusPill variant={item.errorMessage ? "danger" : "success"}>
|
|
82
|
+
{item.errorMessage ? "Failed" : "Success"}
|
|
83
|
+
</StatusPill>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="stats-mobile-card-grid">
|
|
86
|
+
<div>
|
|
87
|
+
<div className="stats-mobile-card-label">Time</div>
|
|
88
|
+
<div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<div className="stats-mobile-card-label">Cost</div>
|
|
92
|
+
<div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<div className="stats-mobile-card-label">Tokens</div>
|
|
96
|
+
<div className="stats-mobile-card-value">{formatInteger(item.usage.totalTokens)}</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div>
|
|
99
|
+
<div className="stats-mobile-card-label">Duration</div>
|
|
100
|
+
<div className="stats-mobile-card-value">{formatDurationMs(item.duration)}</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
{item.errorMessage && <div className="stats-mobile-card-error truncate mt-2">{item.errorMessage}</div>}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="stats-route-container">
|
|
109
|
+
<Panel title="All Recent Requests" subtitle="Up to 50 most recent requests processed by OMP">
|
|
110
|
+
<AsyncBoundary loading={loading} error={error} data={recentRequests}>
|
|
111
|
+
<DataTable
|
|
112
|
+
columns={columns}
|
|
113
|
+
data={recentRequests || []}
|
|
114
|
+
keyExtractor={item => item.id || `${item.sessionFile}-${item.entryId}`}
|
|
115
|
+
onRowClick={item => item.id && onRequestClick(item.id)}
|
|
116
|
+
renderMobileCard={renderMobileCard}
|
|
117
|
+
emptyText="No recent requests found"
|
|
118
|
+
/>
|
|
119
|
+
</AsyncBoundary>
|
|
120
|
+
</Panel>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|