@linxiraos/pi-stats 1.1.26 → 1.2.0
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 +12 -5
- package/build.ts +4 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +1 -0
- package/dist/client/index.js +96 -96
- package/dist/types/aggregator.d.ts +4 -1
- package/dist/types/client/api.d.ts +5 -1
- package/dist/types/client/app/nav.d.ts +5 -2
- package/dist/types/client/data/formatters.d.ts +7 -2
- package/dist/types/client/data/live.d.ts +16 -11
- package/dist/types/client/data/query-store.d.ts +39 -0
- package/dist/types/client/data/query.d.ts +10 -5
- package/dist/types/client/data/range.d.ts +4 -3
- package/dist/types/client/i18n/format.d.ts +9 -0
- package/dist/types/client/i18n/index.d.ts +18 -0
- package/dist/types/client/i18n/messages/charts.d.ts +8 -0
- package/dist/types/client/i18n/messages/common.d.ts +153 -0
- package/dist/types/client/i18n/messages/costs.d.ts +92 -0
- package/dist/types/client/i18n/messages/en.d.ts +1255 -0
- package/dist/types/client/i18n/messages/errors.d.ts +78 -0
- package/dist/types/client/i18n/messages/frustration.d.ts +191 -0
- package/dist/types/client/i18n/messages/gain.d.ts +54 -0
- package/dist/types/client/i18n/messages/models.d.ts +94 -0
- package/dist/types/client/i18n/messages/nav.d.ts +46 -0
- package/dist/types/client/i18n/messages/overview.d.ts +80 -0
- package/dist/types/client/i18n/messages/projects.d.ts +84 -0
- package/dist/types/client/i18n/messages/providers.d.ts +142 -0
- package/dist/types/client/i18n/messages/requests.d.ts +72 -0
- package/dist/types/client/i18n/messages/tools.d.ts +119 -0
- package/dist/types/client/i18n/messages/traces.d.ts +144 -0
- package/dist/types/client/i18n/messages/ui.d.ts +44 -0
- package/dist/types/client/i18n/messages/zh-CN.d.ts +13 -0
- package/dist/types/client/i18n/types.d.ts +15 -0
- package/dist/types/client/traces/Minimap.d.ts +2 -0
- package/dist/types/client/traces/TimelineCanvas.d.ts +29 -8
- package/dist/types/client/traces/TranscriptList.d.ts +3 -1
- package/dist/types/db.d.ts +14 -21
- package/dist/types/embedded-client.d.ts +5 -0
- package/dist/types/frustration.d.ts +18 -9
- package/dist/types/live.d.ts +5 -3
- package/dist/types/locale.d.ts +16 -0
- package/dist/types/parser.d.ts +0 -17
- package/dist/types/rollup.d.ts +9 -1
- package/dist/types/server.d.ts +6 -0
- package/dist/types/shared-types.d.ts +7 -0
- package/dist/types/trace.d.ts +18 -6
- package/dist/types/types.d.ts +13 -1
- package/dist/types/user-metrics.d.ts +23 -1
- package/package.json +5 -5
- package/src/aggregator.ts +31 -11
- package/src/client/api.ts +40 -5
- package/src/client/app/LiveChip.tsx +14 -13
- package/src/client/app/Shell.tsx +19 -13
- package/src/client/app/ThemeToggle.tsx +4 -3
- package/src/client/app/nav.ts +19 -16
- package/src/client/data/formatters.ts +80 -13
- package/src/client/data/live.tsx +85 -45
- package/src/client/data/query-store.ts +108 -0
- package/src/client/data/query.ts +47 -56
- package/src/client/data/range.ts +34 -14
- package/src/client/data/series.ts +7 -1
- package/src/client/favicon.ico +0 -0
- package/src/client/i18n/format.ts +28 -0
- package/src/client/i18n/index.ts +53 -0
- package/src/client/i18n/messages/charts.ts +8 -0
- package/src/client/i18n/messages/common.ts +157 -0
- package/src/client/i18n/messages/costs.ts +104 -0
- package/src/client/i18n/messages/en.ts +40 -0
- package/src/client/i18n/messages/errors.ts +80 -0
- package/src/client/i18n/messages/frustration.ts +198 -0
- package/src/client/i18n/messages/gain.ts +54 -0
- package/src/client/i18n/messages/models.ts +94 -0
- package/src/client/i18n/messages/nav.ts +46 -0
- package/src/client/i18n/messages/overview.ts +90 -0
- package/src/client/i18n/messages/projects.ts +84 -0
- package/src/client/i18n/messages/providers.ts +165 -0
- package/src/client/i18n/messages/requests.ts +73 -0
- package/src/client/i18n/messages/tools.ts +120 -0
- package/src/client/i18n/messages/traces.ts +160 -0
- package/src/client/i18n/messages/ui.ts +46 -0
- package/src/client/i18n/messages/zh-CN.ts +43 -0
- package/src/client/i18n/types.ts +19 -0
- package/src/client/index.tsx +4 -0
- package/src/client/routes/CostsRoute.tsx +61 -48
- package/src/client/routes/ErrorsRoute.tsx +71 -47
- package/src/client/routes/FrustrationRoute.tsx +131 -116
- package/src/client/routes/GainRoute.tsx +42 -30
- package/src/client/routes/ModelsRoute.tsx +90 -73
- package/src/client/routes/OverviewRoute.tsx +109 -63
- package/src/client/routes/ProjectsRoute.tsx +73 -46
- package/src/client/routes/ProvidersRoute.tsx +151 -114
- package/src/client/routes/RequestsRoute.tsx +69 -46
- package/src/client/routes/ToolsRoute.tsx +108 -94
- package/src/client/routes/TracesRoute.tsx +25 -23
- package/src/client/styles.css +5 -2
- package/src/client/traces/AggregatesPanel.tsx +19 -8
- package/src/client/traces/Minimap.tsx +65 -27
- package/src/client/traces/SpanDrawer.tsx +46 -21
- package/src/client/traces/SummaryStrip.tsx +27 -11
- package/src/client/traces/TimelineCanvas.tsx +245 -116
- package/src/client/traces/TraceView.tsx +51 -36
- package/src/client/traces/TranscriptList.tsx +20 -11
- package/src/client/traces/traces.css +26 -0
- package/src/client/ui/JsonBlock.tsx +3 -2
- package/src/client/ui/Modal.tsx +3 -2
- package/src/client/ui/RequestDrawer.tsx +33 -30
- package/src/client/ui/SearchInput.tsx +2 -1
- package/src/client/ui/States.tsx +3 -2
- package/src/client/ui/Table.tsx +7 -3
- package/src/db.ts +150 -135
- package/src/embedded-client.ts +9 -0
- package/src/frustration.ts +9 -30
- package/src/index.ts +2 -1
- package/src/live.ts +36 -11
- package/src/locale.ts +29 -0
- package/src/parser.ts +37 -72
- package/src/port-conflict.ts +1 -1
- package/src/prompts/judge/frustration.json +19 -0
- package/src/rollup.ts +41 -20
- package/src/server.ts +67 -27
- package/src/shared-types.ts +7 -0
- package/src/trace.ts +54 -40
- package/src/types.ts +13 -1
- package/src/user-metrics.ts +220 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "./costs.css";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { getCostDashboardStats } from "../api";
|
|
4
|
+
import { t } from "../i18n";
|
|
4
5
|
import { Chart, type ChartSeries, Legend, ShareBar, useHiddenSeries } from "../charts";
|
|
5
6
|
import { buildModelColorLookup, modelKey, OTHER_COLOR, SERIES_COLORS } from "../data/colors";
|
|
6
7
|
import { formatCost, formatEstimatedCost, formatInteger, formatPercent } from "../data/formatters";
|
|
@@ -35,8 +36,8 @@ export interface CostsRouteProps {
|
|
|
35
36
|
type SplitMode = "model" | "component";
|
|
36
37
|
|
|
37
38
|
const SPLIT_OPTIONS = [
|
|
38
|
-
{ value: "model" as const, label: "
|
|
39
|
-
{ value: "component" as const, label: "
|
|
39
|
+
{ value: "model" as const, label: t("costs.by-model") },
|
|
40
|
+
{ value: "component" as const, label: t("costs.by-component") },
|
|
40
41
|
];
|
|
41
42
|
|
|
42
43
|
/** Models stacked individually in the daily chart; the rest fold into "Other". */
|
|
@@ -46,10 +47,10 @@ const MODEL_LIMIT = 6;
|
|
|
46
47
|
const DAY_MS = 86_400_000;
|
|
47
48
|
|
|
48
49
|
const COMPONENTS = [
|
|
49
|
-
{ key: "costInput", label: "
|
|
50
|
-
{ key: "costOutput", label: "
|
|
51
|
-
{ key: "costCacheRead", label: "
|
|
52
|
-
{ key: "costCacheWrite", label: "
|
|
50
|
+
{ key: "costInput", label: t("common.input"), color: "#5b8cff" },
|
|
51
|
+
{ key: "costOutput", label: t("common.output"), color: "var(--chart-secondary)" },
|
|
52
|
+
{ key: "costCacheRead", label: t("common.cache-read"), color: "var(--chart-primary)" },
|
|
53
|
+
{ key: "costCacheWrite", label: t("common.cache-write"), color: "#f5b54a" },
|
|
53
54
|
] as const satisfies readonly { key: keyof CostComponents; label: string; color: string }[];
|
|
54
55
|
|
|
55
56
|
const UTC_DAY = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", timeZone: "UTC" });
|
|
@@ -62,20 +63,19 @@ const UTC_DAY_LONG = new Intl.DateTimeFormat(undefined, {
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
65
|
-
const query = useQuery(["costs", range], () => getCostDashboardStats(range), { enabled: active });
|
|
66
|
+
const query = useQuery(["costs", range], ({ signal }) => getCostDashboardStats(range, signal), { enabled: active });
|
|
66
67
|
const meta = rangeMeta(range);
|
|
67
68
|
const [split, setSplit] = useState<SplitMode>("model");
|
|
68
69
|
const [hidden, toggleSeries] = useHiddenSeries();
|
|
69
70
|
const view = useMemo(() => (query.data ? buildCostsView(query.data, range) : null), [query.data, range]);
|
|
71
|
+
// Stable columns: a fresh array would make Table re-sort on every render.
|
|
72
|
+
const columns = useMemo(() => (view ? buildCostColumns(view) : []), [view]);
|
|
70
73
|
const isEmpty = (data: CostDashboardStats) => data.costSeries.length === 0;
|
|
71
|
-
const empty = <EmptyState title="
|
|
74
|
+
const empty = <EmptyState title={t("costs.no-usage-in-range")} hint={t("costs.empty-hint")} />;
|
|
72
75
|
|
|
73
76
|
return (
|
|
74
77
|
<div className="page">
|
|
75
|
-
<PageHeader
|
|
76
|
-
title="Costs"
|
|
77
|
-
description={`What ${meta.windowLabel} of usage would cost at public API rates. Subscription usage without a public price is counted as unpriced, not free.`}
|
|
78
|
-
/>
|
|
78
|
+
<PageHeader title={t("costs.title")} description={t("costs.description", { window: t(meta.windowKey) })} />
|
|
79
79
|
|
|
80
80
|
<QueryView
|
|
81
81
|
query={query}
|
|
@@ -89,17 +89,15 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
89
89
|
<div className="grid grid-main-side">
|
|
90
90
|
<Card
|
|
91
91
|
index={1}
|
|
92
|
-
title="
|
|
93
|
-
description={
|
|
94
|
-
split === "model" ? "Per UTC day, stacked by model" : "Per UTC day, stacked by billing component"
|
|
95
|
-
}
|
|
92
|
+
title={t("costs.daily-estimate")}
|
|
93
|
+
description={split === "model" ? t("costs.daily-by-model") : t("costs.daily-by-component")}
|
|
96
94
|
actions={
|
|
97
95
|
<Segmented
|
|
98
96
|
size="sm"
|
|
99
97
|
options={SPLIT_OPTIONS}
|
|
100
98
|
value={split}
|
|
101
99
|
onChange={setSplit}
|
|
102
|
-
aria-label="
|
|
100
|
+
aria-label={t("costs.split-by")}
|
|
103
101
|
/>
|
|
104
102
|
}
|
|
105
103
|
stale={query.stale}
|
|
@@ -119,11 +117,11 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
119
117
|
height={280}
|
|
120
118
|
format={v => formatCost(v, Number.isInteger(v) ? 0 : 2)}
|
|
121
119
|
formatTooltip={v => formatCost(v)}
|
|
122
|
-
emptyLabel="
|
|
120
|
+
emptyLabel={t("costs.no-priced-usage")}
|
|
123
121
|
tooltipExtra={i =>
|
|
124
122
|
view.unpricedPerDay[i] > 0 ? (
|
|
125
123
|
<div className="chart-tooltip-row chart-tooltip-total">
|
|
126
|
-
<span className="chart-tooltip-label">
|
|
124
|
+
<span className="chart-tooltip-label">{t("costs.unpriced-requests")}</span>
|
|
127
125
|
<span className="chart-tooltip-value">
|
|
128
126
|
{formatInteger(view.unpricedPerDay[i])}
|
|
129
127
|
</span>
|
|
@@ -147,7 +145,12 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
147
145
|
</QueryView>
|
|
148
146
|
</Card>
|
|
149
147
|
|
|
150
|
-
<Card
|
|
148
|
+
<Card
|
|
149
|
+
index={2}
|
|
150
|
+
title={t("costs.where-it-went")}
|
|
151
|
+
description={t("costs.estimate-by-component")}
|
|
152
|
+
stale={query.stale}
|
|
153
|
+
>
|
|
151
154
|
<QueryView query={query} skeleton={<ChartSkeleton height={280} />} isEmpty={isEmpty} empty={empty}>
|
|
152
155
|
{() => view && <ComponentBreakdown summary={view.summary} />}
|
|
153
156
|
</QueryView>
|
|
@@ -156,8 +159,8 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
156
159
|
|
|
157
160
|
<Card
|
|
158
161
|
index={3}
|
|
159
|
-
title="
|
|
160
|
-
description="
|
|
162
|
+
title={t("costs.by-model")}
|
|
163
|
+
description={t("costs.by-model-description")}
|
|
161
164
|
stale={query.stale}
|
|
162
165
|
flush
|
|
163
166
|
>
|
|
@@ -167,7 +170,7 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
167
170
|
<Table
|
|
168
171
|
rows={view.summary.models}
|
|
169
172
|
rowKey={row => row.key}
|
|
170
|
-
columns={
|
|
173
|
+
columns={columns}
|
|
171
174
|
initialSort={{ key: "cost", dir: "desc" }}
|
|
172
175
|
limit={20}
|
|
173
176
|
/>
|
|
@@ -245,36 +248,45 @@ function CostStats({ view, stale }: { view: CostsView; stale: boolean }) {
|
|
|
245
248
|
<div data-stale={stale}>
|
|
246
249
|
<StatGrid min={180}>
|
|
247
250
|
<Stat
|
|
248
|
-
label="
|
|
249
|
-
title="
|
|
251
|
+
label={t("costs.api-equivalent-estimate")}
|
|
252
|
+
title={t("costs.api-estimate-title")}
|
|
250
253
|
value={formatEstimatedCost(summary.totalCost, summary.unpricedRequests)}
|
|
251
|
-
hint={
|
|
254
|
+
hint={t("costs.requests-n", { count: formatInteger(summary.requests) })}
|
|
252
255
|
spark={view.dailyTotals}
|
|
253
256
|
sparkColor="var(--chart-secondary)"
|
|
254
257
|
/>
|
|
255
258
|
<Stat
|
|
256
|
-
label="
|
|
257
|
-
title="
|
|
259
|
+
label={t("costs.average-per-day")}
|
|
260
|
+
title={t("costs.average-per-day-title")}
|
|
258
261
|
value={formatEstimatedCost(summary.avgDailyCost, summary.unpricedRequests)}
|
|
259
|
-
hint={
|
|
262
|
+
hint={t(summary.activeDays === 1 ? "costs.active-days-one" : "costs.active-days-n", {
|
|
263
|
+
count: formatInteger(summary.activeDays),
|
|
264
|
+
})}
|
|
260
265
|
/>
|
|
261
266
|
<Stat
|
|
262
|
-
label="
|
|
267
|
+
label={t("costs.top-model")}
|
|
263
268
|
title={top ? `${top.model} (${top.provider})` : undefined}
|
|
264
269
|
value={top ? top.model : "–"}
|
|
265
|
-
hint={
|
|
270
|
+
hint={
|
|
271
|
+
top
|
|
272
|
+
? t("costs.top-model-hint", {
|
|
273
|
+
cost: formatCost(top.cost),
|
|
274
|
+
share: formatPercent(top.share),
|
|
275
|
+
})
|
|
276
|
+
: t("costs.nothing-priced-yet")
|
|
277
|
+
}
|
|
266
278
|
/>
|
|
267
279
|
<Stat
|
|
268
|
-
label="
|
|
269
|
-
title="
|
|
280
|
+
label={t("costs.per-priced-request")}
|
|
281
|
+
title={t("costs.per-priced-request-title")}
|
|
270
282
|
value={pricedRequests > 0 ? formatUnitCost(summary.totalCost / pricedRequests) : "–"}
|
|
271
|
-
hint={
|
|
283
|
+
hint={t("costs.priced-n", { count: formatInteger(pricedRequests) })}
|
|
272
284
|
/>
|
|
273
285
|
<Stat
|
|
274
|
-
label="
|
|
275
|
-
title="
|
|
286
|
+
label={t("costs.unpriced-requests")}
|
|
287
|
+
title={t("costs.unpriced-title")}
|
|
276
288
|
value={formatInteger(summary.unpricedRequests)}
|
|
277
|
-
hint={summary.unpricedRequests > 0 ? "
|
|
289
|
+
hint={summary.unpricedRequests > 0 ? t("costs.unpriced-hint") : t("costs.all-priced")}
|
|
278
290
|
/>
|
|
279
291
|
</StatGrid>
|
|
280
292
|
</div>
|
|
@@ -302,15 +314,16 @@ function ComponentBreakdown({ summary }: { summary: CostSummaryView }) {
|
|
|
302
314
|
</div>
|
|
303
315
|
))}
|
|
304
316
|
<div className="costs-component-row costs-component-total">
|
|
305
|
-
<span>
|
|
317
|
+
<span>{t("common.total")}</span>
|
|
306
318
|
<span className="num">{formatEstimatedCost(summary.totalCost, summary.unpricedRequests)}</span>
|
|
307
319
|
<span className="costs-component-share" />
|
|
308
320
|
</div>
|
|
309
321
|
</div>
|
|
310
322
|
{summary.unpricedRequests > 0 && (
|
|
311
323
|
<p className="micro dim">
|
|
312
|
-
{
|
|
313
|
-
|
|
324
|
+
{t(summary.unpricedRequests === 1 ? "costs.unpriced-note-one" : "costs.unpriced-note-n", {
|
|
325
|
+
count: formatInteger(summary.unpricedRequests),
|
|
326
|
+
})}
|
|
314
327
|
</p>
|
|
315
328
|
)}
|
|
316
329
|
</div>
|
|
@@ -328,7 +341,7 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
328
341
|
return [
|
|
329
342
|
{
|
|
330
343
|
key: "model",
|
|
331
|
-
header: "
|
|
344
|
+
header: t("common.model"),
|
|
332
345
|
sort: row => row.model,
|
|
333
346
|
render: row => (
|
|
334
347
|
<LabelCell
|
|
@@ -340,15 +353,15 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
340
353
|
},
|
|
341
354
|
{
|
|
342
355
|
key: "requests",
|
|
343
|
-
header: "
|
|
356
|
+
header: t("common.requests"),
|
|
344
357
|
align: "right",
|
|
345
358
|
sort: row => row.requests,
|
|
346
359
|
render: row => <span className="num">{formatInteger(row.requests)}</span>,
|
|
347
360
|
},
|
|
348
361
|
{
|
|
349
362
|
key: "cost",
|
|
350
|
-
header: "
|
|
351
|
-
title: "
|
|
363
|
+
header: t("costs.estimate"),
|
|
364
|
+
title: t("costs.estimate-title"),
|
|
352
365
|
align: "right",
|
|
353
366
|
sort: row => row.cost,
|
|
354
367
|
render: row => (
|
|
@@ -362,14 +375,14 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
362
375
|
},
|
|
363
376
|
{
|
|
364
377
|
key: "share",
|
|
365
|
-
header: "
|
|
378
|
+
header: t("common.share"),
|
|
366
379
|
align: "right",
|
|
367
380
|
sort: row => row.share,
|
|
368
381
|
render: row => <span className="num muted">{row.cost > 0 ? formatPercent(row.share) : "–"}</span>,
|
|
369
382
|
},
|
|
370
383
|
{
|
|
371
384
|
key: "split",
|
|
372
|
-
header: "
|
|
385
|
+
header: t("costs.split"),
|
|
373
386
|
title: "Input · output · cache read · cache write",
|
|
374
387
|
width: 150,
|
|
375
388
|
render: row =>
|
|
@@ -384,7 +397,7 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
384
397
|
...componentColumns,
|
|
385
398
|
{
|
|
386
399
|
key: "perRequest",
|
|
387
|
-
header: "
|
|
400
|
+
header: t("costs.per-request"),
|
|
388
401
|
title: "Estimate ÷ priced requests",
|
|
389
402
|
align: "right",
|
|
390
403
|
sort: row => (row.requests > row.unpricedRequests ? row.cost / (row.requests - row.unpricedRequests) : -1),
|
|
@@ -398,7 +411,7 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
398
411
|
},
|
|
399
412
|
{
|
|
400
413
|
key: "unpriced",
|
|
401
|
-
header: "
|
|
414
|
+
header: t("costs.unpriced"),
|
|
402
415
|
title: "Requests with no public-equivalent price",
|
|
403
416
|
align: "right",
|
|
404
417
|
sort: row => row.unpricedRequests,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ArrowUpRight, X } from "lucide-react";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { getRecentErrors } from "../api";
|
|
4
|
+
import { t } from "../i18n";
|
|
4
5
|
import { BarList } from "../charts";
|
|
5
6
|
import { modelKey } from "../data/colors";
|
|
6
7
|
import {
|
|
@@ -44,7 +45,9 @@ const LOAD_STEPS = [50, 200, 1_000] as const;
|
|
|
44
45
|
export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps) {
|
|
45
46
|
const [step, setStep] = useState(0);
|
|
46
47
|
const limit = LOAD_STEPS[step];
|
|
47
|
-
const errors = useQuery(["errors", range, limit], () => getRecentErrors(range, limit), {
|
|
48
|
+
const errors = useQuery(["errors", range, limit], ({ signal }) => getRecentErrors(range, limit, signal), {
|
|
49
|
+
enabled: active,
|
|
50
|
+
});
|
|
48
51
|
const [selectedSignature, setSelectedSignature] = useState<string | null>(null);
|
|
49
52
|
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
|
50
53
|
const [search, setSearch] = useState("");
|
|
@@ -97,9 +100,7 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
97
100
|
const footer =
|
|
98
101
|
errors.data !== null && !complete ? (
|
|
99
102
|
<>
|
|
100
|
-
<span>
|
|
101
|
-
Showing the latest {formatInteger(loaded)} failures in {meta.windowLabel}; older ones are not loaded.
|
|
102
|
-
</span>
|
|
103
|
+
<span>{t("errors.footer", { count: formatInteger(loaded), window: t(meta.windowKey) })}</span>
|
|
103
104
|
{nextStep !== null && (
|
|
104
105
|
<button
|
|
105
106
|
type="button"
|
|
@@ -108,7 +109,9 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
108
109
|
disabled={errors.refreshing}
|
|
109
110
|
onClick={() => setStep(step + 1)}
|
|
110
111
|
>
|
|
111
|
-
{errors.refreshing && errors.stale
|
|
112
|
+
{errors.refreshing && errors.stale
|
|
113
|
+
? t("errors.loading")
|
|
114
|
+
: t("errors.load-latest", { count: formatInteger(nextStep) })}
|
|
112
115
|
</button>
|
|
113
116
|
)}
|
|
114
117
|
</>
|
|
@@ -120,35 +123,44 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
120
123
|
|
|
121
124
|
return (
|
|
122
125
|
<div className="page">
|
|
123
|
-
<PageHeader
|
|
124
|
-
title="Errors"
|
|
125
|
-
description={`Failed model requests in ${meta.windowLabel}, grouped by error signature. Open a failure for its full payload.`}
|
|
126
|
-
/>
|
|
126
|
+
<PageHeader title={t("errors.title")} description={t("errors.description", { window: t(meta.windowKey) })} />
|
|
127
127
|
|
|
128
128
|
<QueryView query={errors} skeleton={<Skeleton height={96} style={{ borderRadius: 12 }} />}>
|
|
129
129
|
{() => (
|
|
130
130
|
<div data-stale={errors.stale}>
|
|
131
131
|
<StatGrid min={170}>
|
|
132
132
|
<Stat
|
|
133
|
-
label="
|
|
133
|
+
label={t("errors.failures")}
|
|
134
134
|
value={formatInteger(loaded)}
|
|
135
|
-
hint={
|
|
135
|
+
hint={
|
|
136
|
+
complete
|
|
137
|
+
? t("errors.hint-in-window", { window: t(meta.windowKey) })
|
|
138
|
+
: t("errors.hint-latest-loaded", { count: formatInteger(loaded) })
|
|
139
|
+
}
|
|
136
140
|
/>
|
|
137
141
|
<Stat
|
|
138
|
-
label="
|
|
139
|
-
title="
|
|
142
|
+
label={t("errors.signatures")}
|
|
143
|
+
title={t("errors.signatures-hint")}
|
|
140
144
|
value={formatInteger(view.groups.length)}
|
|
141
|
-
hint={
|
|
145
|
+
hint={
|
|
146
|
+
view.groups[0]
|
|
147
|
+
? t("errors.signatures-top", { count: formatInteger(view.groups[0].count) })
|
|
148
|
+
: undefined
|
|
149
|
+
}
|
|
142
150
|
/>
|
|
143
151
|
<Stat
|
|
144
|
-
label="
|
|
152
|
+
label={t("errors.affected-models")}
|
|
145
153
|
value={formatInteger(view.models.length)}
|
|
146
154
|
hint={view.models[0]?.model}
|
|
147
155
|
/>
|
|
148
156
|
<Stat
|
|
149
|
-
label="
|
|
157
|
+
label={t("errors.last-failure")}
|
|
150
158
|
value={view.newest ? formatRelativeTime(view.newest.timestamp) : "–"}
|
|
151
|
-
hint={
|
|
159
|
+
hint={
|
|
160
|
+
view.newest
|
|
161
|
+
? formatTimestamp(view.newest.timestamp)
|
|
162
|
+
: t("errors.none-in-window", { window: t(meta.windowKey) })
|
|
163
|
+
}
|
|
152
164
|
/>
|
|
153
165
|
</StatGrid>
|
|
154
166
|
</div>
|
|
@@ -158,8 +170,8 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
158
170
|
<div className="grid grid-main-side">
|
|
159
171
|
<Card
|
|
160
172
|
index={1}
|
|
161
|
-
title="
|
|
162
|
-
description="
|
|
173
|
+
title={t("errors.error-signatures")}
|
|
174
|
+
description={t("errors.signatures-card-description")}
|
|
163
175
|
flush
|
|
164
176
|
stale={errors.stale}
|
|
165
177
|
>
|
|
@@ -169,8 +181,8 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
169
181
|
isEmpty={rows => rows.length === 0}
|
|
170
182
|
empty={
|
|
171
183
|
<EmptyState
|
|
172
|
-
title={
|
|
173
|
-
hint="
|
|
184
|
+
title={t("errors.empty-no-failures", { window: t(meta.windowKey) })}
|
|
185
|
+
hint={t("errors.empty-all-succeeded")}
|
|
174
186
|
/>
|
|
175
187
|
}
|
|
176
188
|
>
|
|
@@ -196,15 +208,15 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
196
208
|
|
|
197
209
|
<Card
|
|
198
210
|
index={2}
|
|
199
|
-
title="
|
|
200
|
-
description="
|
|
211
|
+
title={t("errors.by-model")}
|
|
212
|
+
description={t("errors.by-model-description")}
|
|
201
213
|
stale={errors.stale}
|
|
202
214
|
>
|
|
203
215
|
<QueryView
|
|
204
216
|
query={errors}
|
|
205
217
|
skeleton={<Skeleton height={180} />}
|
|
206
218
|
isEmpty={rows => rows.length === 0}
|
|
207
|
-
empty={<EmptyState title="
|
|
219
|
+
empty={<EmptyState title={t("errors.empty-no-affected-models")} />}
|
|
208
220
|
>
|
|
209
221
|
{() => (
|
|
210
222
|
<BarList
|
|
@@ -228,11 +240,14 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
228
240
|
|
|
229
241
|
<Card
|
|
230
242
|
index={3}
|
|
231
|
-
title="
|
|
243
|
+
title={t("errors.failures")}
|
|
232
244
|
description={
|
|
233
245
|
errors.data === null
|
|
234
|
-
? "
|
|
235
|
-
:
|
|
246
|
+
? t("errors.loading-failures")
|
|
247
|
+
: t("errors.filtered-of-total", {
|
|
248
|
+
shown: formatInteger(filtered.length),
|
|
249
|
+
total: formatInteger(loaded),
|
|
250
|
+
})
|
|
236
251
|
}
|
|
237
252
|
actions={
|
|
238
253
|
<>
|
|
@@ -259,7 +274,7 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
259
274
|
<X size={12} />
|
|
260
275
|
</button>
|
|
261
276
|
)}
|
|
262
|
-
<SearchInput value={search} onChange={setSearch} placeholder="
|
|
277
|
+
<SearchInput value={search} onChange={setSearch} placeholder={t("errors.search-placeholder")} />
|
|
263
278
|
</>
|
|
264
279
|
}
|
|
265
280
|
flush
|
|
@@ -270,7 +285,7 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
270
285
|
query={errors}
|
|
271
286
|
skeleton={<TableSkeleton rows={10} />}
|
|
272
287
|
isEmpty={rows => rows.length === 0}
|
|
273
|
-
empty={<EmptyState title={
|
|
288
|
+
empty={<EmptyState title={t("errors.empty-no-failures", { window: t(meta.windowKey) })} />}
|
|
274
289
|
>
|
|
275
290
|
{() => (
|
|
276
291
|
<Table
|
|
@@ -281,7 +296,7 @@ export function ErrorsRoute({ active, range, onRequestClick }: ErrorsRouteProps)
|
|
|
281
296
|
initialSort={{ key: "time", dir: "desc" }}
|
|
282
297
|
limit={50}
|
|
283
298
|
dense
|
|
284
|
-
empty={<EmptyState title="
|
|
299
|
+
empty={<EmptyState title={t("errors.no-match")} hint={t("errors.no-match-hint")} />}
|
|
285
300
|
/>
|
|
286
301
|
)}
|
|
287
302
|
</QueryView>
|
|
@@ -294,7 +309,7 @@ function buildGroupColumns(maxCount: number): Column<ErrorGroupView>[] {
|
|
|
294
309
|
return [
|
|
295
310
|
{
|
|
296
311
|
key: "signature",
|
|
297
|
-
header: "
|
|
312
|
+
header: t("errors.signature"),
|
|
298
313
|
wrap: true,
|
|
299
314
|
sort: group => group.signature,
|
|
300
315
|
render: group => (
|
|
@@ -305,18 +320,22 @@ function buildGroupColumns(maxCount: number): Column<ErrorGroupView>[] {
|
|
|
305
320
|
},
|
|
306
321
|
{
|
|
307
322
|
key: "models",
|
|
308
|
-
header: "
|
|
323
|
+
header: t("common.models"),
|
|
309
324
|
sort: group => group.models.length,
|
|
310
325
|
render: group => (
|
|
311
326
|
<LabelCell
|
|
312
327
|
primary={<span className="mono">{group.models[0]?.model}</span>}
|
|
313
|
-
secondary={
|
|
328
|
+
secondary={
|
|
329
|
+
group.models.length > 1
|
|
330
|
+
? t("errors.more-models", { count: formatInteger(group.models.length - 1) })
|
|
331
|
+
: group.models[0]?.provider
|
|
332
|
+
}
|
|
314
333
|
/>
|
|
315
334
|
),
|
|
316
335
|
},
|
|
317
336
|
{
|
|
318
337
|
key: "last",
|
|
319
|
-
header: "
|
|
338
|
+
header: t("errors.last-seen"),
|
|
320
339
|
sort: group => group.lastSeen,
|
|
321
340
|
render: group => (
|
|
322
341
|
<span className="muted" title={formatTimestamp(group.lastSeen)}>
|
|
@@ -326,7 +345,7 @@ function buildGroupColumns(maxCount: number): Column<ErrorGroupView>[] {
|
|
|
326
345
|
},
|
|
327
346
|
{
|
|
328
347
|
key: "count",
|
|
329
|
-
header: "
|
|
348
|
+
header: t("errors.failures"),
|
|
330
349
|
align: "right",
|
|
331
350
|
width: 120,
|
|
332
351
|
sort: group => group.count,
|
|
@@ -341,15 +360,20 @@ function SignatureDetail({ group, onRequestClick }: { group: ErrorGroupView; onR
|
|
|
341
360
|
const latestId = group.latest.id;
|
|
342
361
|
return (
|
|
343
362
|
<div className="errors-detail">
|
|
344
|
-
<pre className="code-block errors-detail-message">
|
|
363
|
+
<pre className="code-block errors-detail-message">
|
|
364
|
+
{group.latest.errorMessage ?? t("errors.unknown-error")}
|
|
365
|
+
</pre>
|
|
345
366
|
<div className="errors-detail-meta">
|
|
346
367
|
<span className="muted">
|
|
347
|
-
{
|
|
348
|
-
|
|
368
|
+
{t("errors.group-meta", {
|
|
369
|
+
count: formatInteger(group.count),
|
|
370
|
+
first: formatTimestamp(group.firstSeen),
|
|
371
|
+
last: formatTimestamp(group.lastSeen),
|
|
372
|
+
})}
|
|
349
373
|
</span>
|
|
350
374
|
{latestId !== undefined && (
|
|
351
375
|
<button type="button" className="btn" data-size="sm" onClick={() => onRequestClick(latestId)}>
|
|
352
|
-
|
|
376
|
+
{t("errors.open-latest")} <ArrowUpRight size={13} />
|
|
353
377
|
</button>
|
|
354
378
|
)}
|
|
355
379
|
</div>
|
|
@@ -368,7 +392,7 @@ function SignatureDetail({ group, onRequestClick }: { group: ErrorGroupView; onR
|
|
|
368
392
|
const FAILURE_COLUMNS: readonly Column<MessageStats>[] = [
|
|
369
393
|
{
|
|
370
394
|
key: "time",
|
|
371
|
-
header: "
|
|
395
|
+
header: t("errors.when"),
|
|
372
396
|
width: 120,
|
|
373
397
|
sort: row => row.timestamp,
|
|
374
398
|
render: row => (
|
|
@@ -379,23 +403,23 @@ const FAILURE_COLUMNS: readonly Column<MessageStats>[] = [
|
|
|
379
403
|
},
|
|
380
404
|
{
|
|
381
405
|
key: "model",
|
|
382
|
-
header: "
|
|
406
|
+
header: t("common.model"),
|
|
383
407
|
sort: row => row.model,
|
|
384
408
|
render: row => <LabelCell primary={<span className="mono">{row.model}</span>} secondary={row.provider} />,
|
|
385
409
|
},
|
|
386
410
|
{
|
|
387
411
|
key: "error",
|
|
388
|
-
header: "
|
|
412
|
+
header: t("common.error"),
|
|
389
413
|
sort: row => row.errorMessage ?? "",
|
|
390
414
|
render: row => (
|
|
391
415
|
<span className="errors-message" title={row.errorMessage ?? undefined}>
|
|
392
|
-
{row.errorMessage ?? "
|
|
416
|
+
{row.errorMessage ?? t("errors.unknown-error")}
|
|
393
417
|
</span>
|
|
394
418
|
),
|
|
395
419
|
},
|
|
396
420
|
{
|
|
397
421
|
key: "project",
|
|
398
|
-
header: "
|
|
422
|
+
header: t("common.project"),
|
|
399
423
|
sort: row => row.folder,
|
|
400
424
|
render: row => (
|
|
401
425
|
<span className="mono muted" title={row.folder}>
|
|
@@ -405,15 +429,15 @@ const FAILURE_COLUMNS: readonly Column<MessageStats>[] = [
|
|
|
405
429
|
},
|
|
406
430
|
{
|
|
407
431
|
key: "tokens",
|
|
408
|
-
header: "
|
|
432
|
+
header: t("common.tokens"),
|
|
409
433
|
align: "right",
|
|
410
434
|
sort: row => row.usage.totalTokens,
|
|
411
435
|
render: row => <span className="num">{formatInteger(row.usage.totalTokens)}</span>,
|
|
412
436
|
},
|
|
413
437
|
{
|
|
414
438
|
key: "cost",
|
|
415
|
-
header: "
|
|
416
|
-
title: "
|
|
439
|
+
header: t("common.cost"),
|
|
440
|
+
title: t("errors.cost-hint"),
|
|
417
441
|
align: "right",
|
|
418
442
|
sort: row => row.usage.cost.total,
|
|
419
443
|
render: row => <span className="num">{formatMessageCost(row, 4)}</span>,
|