@linxiraos/pi-stats 1.1.27 → 1.2.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.
- package/CHANGELOG.md +10 -7
- package/README.md +4 -4
- 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 +5 -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 +12 -0
- package/dist/types/user-metrics.d.ts +23 -1
- package/package.json +7 -7
- package/src/aggregator.ts +28 -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 +73 -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 +132 -117
- package/src/client/routes/GainRoute.tsx +42 -30
- package/src/client/routes/ModelsRoute.tsx +87 -71
- package/src/client/routes/OverviewRoute.tsx +107 -62
- package/src/client/routes/ProjectsRoute.tsx +71 -45
- package/src/client/routes/ProvidersRoute.tsx +149 -113
- package/src/client/routes/RequestsRoute.tsx +68 -45
- package/src/client/routes/ToolsRoute.tsx +105 -92
- package/src/client/routes/TracesRoute.tsx +25 -23
- package/src/client/styles.css +4 -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/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 +12 -0
- package/src/user-metrics.ts +220 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { format } from "@linxiraos/pi-utils/dates";
|
|
2
1
|
import { type ReactNode, useMemo, useState } from "react";
|
|
3
2
|
import { getProviderDashboardStats, getProviderWindowStats } from "../api";
|
|
3
|
+
import { t } from "../i18n";
|
|
4
4
|
import { Chart, type ChartSeries, Legend, ShareBar, TimeChart, useHiddenSeries } from "../charts";
|
|
5
5
|
import { buildColorLookup, OTHER_COLOR, SERIES_COLORS } from "../data/colors";
|
|
6
6
|
import {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
formatPercent,
|
|
13
13
|
formatRelativeTime,
|
|
14
14
|
formatTokensPerSecond,
|
|
15
|
+
formatDateTimeShort,
|
|
15
16
|
} from "../data/formatters";
|
|
16
17
|
import { type QueryResult, useQuery } from "../data/query";
|
|
17
18
|
import { bucketAxis, formatBucket, formatTick, rangeMeta } from "../data/range";
|
|
@@ -52,23 +53,22 @@ export interface ProvidersRouteProps {
|
|
|
52
53
|
type BurnMetric = "tokens" | "cost" | "requests";
|
|
53
54
|
|
|
54
55
|
const BURN_OPTIONS = [
|
|
55
|
-
{ value: "tokens" as const, label: "
|
|
56
|
-
{ value: "cost" as const, label: "
|
|
57
|
-
{ value: "requests" as const, label: "
|
|
56
|
+
{ value: "tokens" as const, label: t("common.tokens") },
|
|
57
|
+
{ value: "cost" as const, label: t("common.cost"), title: t("providers.cost-title") },
|
|
58
|
+
{ value: "requests" as const, label: t("common.requests") },
|
|
58
59
|
];
|
|
59
60
|
|
|
60
61
|
/** Providers stacked individually in the burn chart; the rest fold into "Other". */
|
|
61
62
|
const TOP_PROVIDERS = 6;
|
|
62
63
|
|
|
63
64
|
const TOKEN_MIX = [
|
|
64
|
-
{ key: "input", label: "
|
|
65
|
-
{ key: "cacheRead", label: "
|
|
66
|
-
{ key: "cacheWrite", label: "
|
|
67
|
-
{ key: "output", label: "
|
|
65
|
+
{ key: "input", label: t("ui.uncached-input"), color: "#5b8cff" },
|
|
66
|
+
{ key: "cacheRead", label: t("common.cache-read"), color: "var(--chart-primary)" },
|
|
67
|
+
{ key: "cacheWrite", label: t("common.cache-write"), color: "#f5b54a" },
|
|
68
|
+
{ key: "output", label: t("common.output"), color: "var(--chart-secondary)" },
|
|
68
69
|
] as const;
|
|
69
70
|
|
|
70
|
-
const SNAPSHOT_HINT =
|
|
71
|
-
"Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zeta-c usage).";
|
|
71
|
+
const SNAPSHOT_HINT = t("providers.snapshot-hint");
|
|
72
72
|
|
|
73
73
|
interface WindowRef {
|
|
74
74
|
provider: string;
|
|
@@ -76,14 +76,16 @@ interface WindowRef {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
79
|
-
const stats = useQuery(["providers", range], () => getProviderDashboardStats(range), {
|
|
79
|
+
const stats = useQuery(["providers", range], ({ signal }) => getProviderDashboardStats(range, signal), {
|
|
80
|
+
enabled: active,
|
|
81
|
+
});
|
|
80
82
|
const [metric, setMetric] = useState<BurnMetric>("tokens");
|
|
81
83
|
const [hidden, toggleHidden] = useHiddenSeries();
|
|
82
84
|
const meta = rangeMeta(range);
|
|
83
85
|
|
|
84
86
|
// Subscription windows load independently: insights for every provider
|
|
85
87
|
// first, then utilization series for the one provider being charted.
|
|
86
|
-
const windows = useQuery(["provider-windows", range], () => getProviderWindowStats(range, null), {
|
|
88
|
+
const windows = useQuery(["provider-windows", range], ({ signal }) => getProviderWindowStats(range, null, signal), {
|
|
87
89
|
enabled: active,
|
|
88
90
|
});
|
|
89
91
|
const [picked, setPicked] = useState<WindowRef | null>(null);
|
|
@@ -91,7 +93,7 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
91
93
|
const selectedProvider = selected?.provider ?? null;
|
|
92
94
|
const accounts = useQuery(
|
|
93
95
|
["provider-windows", range, selectedProvider],
|
|
94
|
-
() => getProviderWindowStats(range, selectedProvider),
|
|
96
|
+
({ signal }) => getProviderWindowStats(range, selectedProvider, signal),
|
|
95
97
|
{ enabled: active && selectedProvider !== null },
|
|
96
98
|
);
|
|
97
99
|
|
|
@@ -140,13 +142,13 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
140
142
|
|
|
141
143
|
const burnSeries = view.burn[metric];
|
|
142
144
|
const burnFormat = metric === "cost" ? (v: number) => formatCost(v) : formatCompact;
|
|
143
|
-
const
|
|
145
|
+
const totals = view.totals;
|
|
144
146
|
|
|
145
147
|
return (
|
|
146
148
|
<div className="page">
|
|
147
149
|
<PageHeader
|
|
148
|
-
title="
|
|
149
|
-
description={
|
|
150
|
+
title={t("common.providers")}
|
|
151
|
+
description={t("providers.description", { window: t(meta.windowKey) })}
|
|
150
152
|
/>
|
|
151
153
|
|
|
152
154
|
<QueryView query={stats} skeleton={<ChartSkeleton height={112} />}>
|
|
@@ -154,34 +156,42 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
154
156
|
<div data-stale={stats.stale}>
|
|
155
157
|
<StatGrid min={190}>
|
|
156
158
|
<Stat
|
|
157
|
-
label="
|
|
159
|
+
label={t("common.providers")}
|
|
158
160
|
value={formatInteger(providers.length)}
|
|
159
|
-
hint={
|
|
161
|
+
hint={
|
|
162
|
+
view.topProvider
|
|
163
|
+
? t("providers.most-tokens", { provider: view.topProvider.provider })
|
|
164
|
+
: undefined
|
|
165
|
+
}
|
|
160
166
|
/>
|
|
161
167
|
<Stat
|
|
162
|
-
label="
|
|
163
|
-
value={formatInteger(
|
|
164
|
-
hint={
|
|
168
|
+
label={t("common.requests")}
|
|
169
|
+
value={formatInteger(totals.requests)}
|
|
170
|
+
hint={t("providers.failed-n", { count: formatInteger(totals.failed) })}
|
|
165
171
|
spark={view.spark.requests}
|
|
166
172
|
/>
|
|
167
173
|
<Stat
|
|
168
|
-
label="
|
|
169
|
-
title="
|
|
170
|
-
value={formatCompact(
|
|
174
|
+
label={t("common.tokens")}
|
|
175
|
+
title={t("providers.tokens-title")}
|
|
176
|
+
value={formatCompact(totals.tokens)}
|
|
171
177
|
spark={view.spark.tokens}
|
|
172
178
|
/>
|
|
173
179
|
<Stat
|
|
174
|
-
label="
|
|
175
|
-
title="
|
|
176
|
-
value={formatEstimatedCost(
|
|
177
|
-
hint={
|
|
180
|
+
label={t("providers.api-equivalent-cost")}
|
|
181
|
+
title={t("providers.api-cost-title")}
|
|
182
|
+
value={formatEstimatedCost(totals.cost, totals.unpriced)}
|
|
183
|
+
hint={
|
|
184
|
+
totals.unpriced > 0
|
|
185
|
+
? t("providers.unpriced-n", { count: formatInteger(totals.unpriced) })
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
178
188
|
spark={view.spark.cost}
|
|
179
189
|
sparkColor="var(--chart-secondary)"
|
|
180
190
|
/>
|
|
181
191
|
<Stat
|
|
182
|
-
label="
|
|
183
|
-
value={formatErrorRate(
|
|
184
|
-
hint={
|
|
192
|
+
label={t("common.error-rate")}
|
|
193
|
+
value={formatErrorRate(totals.requests > 0 ? totals.failed / totals.requests : 0)}
|
|
194
|
+
hint={t("providers.succeeded-n", { count: formatInteger(totals.requests - totals.failed) })}
|
|
185
195
|
/>
|
|
186
196
|
</StatGrid>
|
|
187
197
|
</div>
|
|
@@ -190,11 +200,16 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
190
200
|
|
|
191
201
|
<Card
|
|
192
202
|
index={1}
|
|
193
|
-
title="
|
|
203
|
+
title={t("providers.totals")}
|
|
194
204
|
description={
|
|
195
|
-
|
|
196
|
-
?
|
|
197
|
-
|
|
205
|
+
totals.unpriced > 0
|
|
206
|
+
? t(
|
|
207
|
+
totals.unpriced === 1
|
|
208
|
+
? "providers.totals-description-unpriced-one"
|
|
209
|
+
: "providers.totals-description-unpriced-n",
|
|
210
|
+
{ count: formatInteger(totals.unpriced) },
|
|
211
|
+
)
|
|
212
|
+
: t("providers.totals-description")
|
|
198
213
|
}
|
|
199
214
|
flush
|
|
200
215
|
stale={stats.stale}
|
|
@@ -207,11 +222,21 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
207
222
|
<div className="grid grid-main-side">
|
|
208
223
|
<Card
|
|
209
224
|
index={2}
|
|
210
|
-
title="
|
|
225
|
+
title={t("providers.burn")}
|
|
211
226
|
description={
|
|
212
|
-
metric === "cost" &&
|
|
213
|
-
?
|
|
214
|
-
|
|
227
|
+
metric === "cost" && totals.unpriced > 0
|
|
228
|
+
? t("providers.burn-estimate-description", {
|
|
229
|
+
bucket: meta.bucketMs < 86_400_000 ? t("providers.bucket-hour") : t("providers.bucket-day"),
|
|
230
|
+
})
|
|
231
|
+
: t("providers.burn-description", {
|
|
232
|
+
bucket:
|
|
233
|
+
meta.bucketMs < 3_600_000
|
|
234
|
+
? t("providers.bucket-5m")
|
|
235
|
+
: meta.bucketMs < 86_400_000
|
|
236
|
+
? t("providers.bucket-hour")
|
|
237
|
+
: t("providers.bucket-day"),
|
|
238
|
+
count: TOP_PROVIDERS,
|
|
239
|
+
})
|
|
215
240
|
}
|
|
216
241
|
actions={<Segmented size="sm" options={BURN_OPTIONS} value={metric} onChange={setMetric} />}
|
|
217
242
|
stale={stats.stale}
|
|
@@ -227,7 +252,7 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
227
252
|
height={260}
|
|
228
253
|
format={burnFormat}
|
|
229
254
|
formatTooltip={metric === "cost" ? v => formatCost(v) : formatInteger}
|
|
230
|
-
emptyLabel="
|
|
255
|
+
emptyLabel={t("providers.no-provider-activity")}
|
|
231
256
|
/>
|
|
232
257
|
<Legend
|
|
233
258
|
items={burnSeries.map(s => ({
|
|
@@ -251,8 +276,8 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
251
276
|
|
|
252
277
|
<Card
|
|
253
278
|
index={4}
|
|
254
|
-
title="
|
|
255
|
-
description="
|
|
279
|
+
title={t("providers.subscription-windows")}
|
|
280
|
+
description={t("providers.subscription-windows-description")}
|
|
256
281
|
flush
|
|
257
282
|
stale={windows.stale}
|
|
258
283
|
>
|
|
@@ -311,7 +336,7 @@ function ProviderTotalsTable({
|
|
|
311
336
|
return [
|
|
312
337
|
{
|
|
313
338
|
key: "provider",
|
|
314
|
-
header: "
|
|
339
|
+
header: t("common.provider"),
|
|
315
340
|
sort: p => p.provider,
|
|
316
341
|
render: p => (
|
|
317
342
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -322,7 +347,7 @@ function ProviderTotalsTable({
|
|
|
322
347
|
},
|
|
323
348
|
{
|
|
324
349
|
key: "requests",
|
|
325
|
-
header: "
|
|
350
|
+
header: t("common.requests"),
|
|
326
351
|
align: "right",
|
|
327
352
|
sort: p => p.totalRequests,
|
|
328
353
|
render: p => (
|
|
@@ -331,7 +356,7 @@ function ProviderTotalsTable({
|
|
|
331
356
|
},
|
|
332
357
|
{
|
|
333
358
|
key: "errors",
|
|
334
|
-
header: "
|
|
359
|
+
header: t("common.error-rate"),
|
|
335
360
|
align: "right",
|
|
336
361
|
sort: p => (p.totalRequests > 0 ? p.failedRequests / p.totalRequests : 0),
|
|
337
362
|
render: p => {
|
|
@@ -348,16 +373,16 @@ function ProviderTotalsTable({
|
|
|
348
373
|
},
|
|
349
374
|
{
|
|
350
375
|
key: "models",
|
|
351
|
-
header: "
|
|
352
|
-
title: "
|
|
376
|
+
header: t("common.models"),
|
|
377
|
+
title: t("providers.models-title"),
|
|
353
378
|
align: "right",
|
|
354
379
|
sort: p => p.models,
|
|
355
380
|
render: p => <span className="num">{formatInteger(p.models)}</span>,
|
|
356
381
|
},
|
|
357
382
|
{
|
|
358
383
|
key: "tokens",
|
|
359
|
-
header: "
|
|
360
|
-
title: "
|
|
384
|
+
header: t("common.tokens"),
|
|
385
|
+
title: t("providers.tokens-title"),
|
|
361
386
|
align: "right",
|
|
362
387
|
sort: p => p.totalTokens,
|
|
363
388
|
render: p => (
|
|
@@ -371,8 +396,8 @@ function ProviderTotalsTable({
|
|
|
371
396
|
},
|
|
372
397
|
{
|
|
373
398
|
key: "share",
|
|
374
|
-
header: "
|
|
375
|
-
title: "
|
|
399
|
+
header: t("common.share"),
|
|
400
|
+
title: t("providers.share-title"),
|
|
376
401
|
align: "right",
|
|
377
402
|
sort: p => p.totalTokens,
|
|
378
403
|
render: p => (
|
|
@@ -381,14 +406,18 @@ function ProviderTotalsTable({
|
|
|
381
406
|
},
|
|
382
407
|
{
|
|
383
408
|
key: "cost",
|
|
384
|
-
header: "
|
|
385
|
-
title: "
|
|
409
|
+
header: t("common.cost"),
|
|
410
|
+
title: t("providers.cost-title"),
|
|
386
411
|
align: "right",
|
|
387
412
|
sort: p => p.totalCost,
|
|
388
413
|
render: p => (
|
|
389
414
|
<span
|
|
390
415
|
className="num"
|
|
391
|
-
title={
|
|
416
|
+
title={
|
|
417
|
+
p.unpricedRequests > 0
|
|
418
|
+
? t("providers.unpriced-n", { count: formatInteger(p.unpricedRequests) })
|
|
419
|
+
: undefined
|
|
420
|
+
}
|
|
392
421
|
>
|
|
393
422
|
{formatEstimatedCost(p.totalCost, p.unpricedRequests)}
|
|
394
423
|
</span>
|
|
@@ -396,16 +425,16 @@ function ProviderTotalsTable({
|
|
|
396
425
|
},
|
|
397
426
|
{
|
|
398
427
|
key: "tps",
|
|
399
|
-
header: "
|
|
400
|
-
title: "
|
|
428
|
+
header: t("common.tokens-per-second"),
|
|
429
|
+
title: t("providers.tps-title"),
|
|
401
430
|
align: "right",
|
|
402
431
|
sort: p => p.avgTokensPerSecond ?? -1,
|
|
403
432
|
render: p => <span className="num">{formatTokensPerSecond(p.avgTokensPerSecond)}</span>,
|
|
404
433
|
},
|
|
405
434
|
{
|
|
406
435
|
key: "premium",
|
|
407
|
-
header: "
|
|
408
|
-
title: "
|
|
436
|
+
header: t("common.premium"),
|
|
437
|
+
title: t("providers.premium-title"),
|
|
409
438
|
align: "right",
|
|
410
439
|
sort: p => p.totalPremiumRequests,
|
|
411
440
|
render: p => (
|
|
@@ -427,7 +456,7 @@ function ProviderTotalsTable({
|
|
|
427
456
|
selectedKey={open}
|
|
428
457
|
onRowClick={p => setOpen(prev => (prev === p.provider ? null : p.provider))}
|
|
429
458
|
expanded={p => (p.provider === open ? <TokenMix provider={p} /> : null)}
|
|
430
|
-
empty={<EmptyState title="
|
|
459
|
+
empty={<EmptyState title={t("providers.no-requests-in-range")} />}
|
|
431
460
|
/>
|
|
432
461
|
);
|
|
433
462
|
}
|
|
@@ -489,13 +518,13 @@ function PeakHoursCard({
|
|
|
489
518
|
const series: ChartSeries[] = [
|
|
490
519
|
{
|
|
491
520
|
key: "tokens",
|
|
492
|
-
label: "
|
|
521
|
+
label: t("common.tokens"),
|
|
493
522
|
color: "var(--chart-primary)",
|
|
494
523
|
values: tokens.map((v, hour) => (hasData && hour === peak ? null : v)),
|
|
495
524
|
},
|
|
496
525
|
{
|
|
497
526
|
key: "peak",
|
|
498
|
-
label: "
|
|
527
|
+
label: t("providers.tokens-peak-hour"),
|
|
499
528
|
color: "var(--chart-secondary)",
|
|
500
529
|
values: tokens.map((v, hour) => (hasData && hour === peak ? v : null)),
|
|
501
530
|
},
|
|
@@ -508,20 +537,20 @@ function PeakHoursCard({
|
|
|
508
537
|
return (
|
|
509
538
|
<Card
|
|
510
539
|
index={3}
|
|
511
|
-
title="
|
|
540
|
+
title={t("providers.peak-burn-hours")}
|
|
512
541
|
description={
|
|
513
542
|
hours.hasData
|
|
514
|
-
?
|
|
515
|
-
: "
|
|
543
|
+
? t("providers.peak-hours-description", { hour: hourLabel(hours.peak) })
|
|
544
|
+
: t("providers.hours-description")
|
|
516
545
|
}
|
|
517
546
|
actions={
|
|
518
547
|
<select
|
|
519
548
|
className="input providers-select"
|
|
520
549
|
value={current}
|
|
521
550
|
onChange={e => setProvider(e.target.value)}
|
|
522
|
-
aria-label="
|
|
551
|
+
aria-label={t("common.provider")}
|
|
523
552
|
>
|
|
524
|
-
<option value={ALL_PROVIDERS}>
|
|
553
|
+
<option value={ALL_PROVIDERS}>{t("providers.all-providers")}</option>
|
|
525
554
|
{providers.map(p => (
|
|
526
555
|
<option key={p.provider} value={p.provider}>
|
|
527
556
|
{p.provider}
|
|
@@ -539,15 +568,15 @@ function PeakHoursCard({
|
|
|
539
568
|
height={260}
|
|
540
569
|
showTotal={false}
|
|
541
570
|
formatTooltip={formatInteger}
|
|
542
|
-
emptyLabel="
|
|
571
|
+
emptyLabel={t("providers.no-activity-in-range")}
|
|
543
572
|
tooltipExtra={hour => (
|
|
544
573
|
<>
|
|
545
574
|
<div className="chart-tooltip-row">
|
|
546
|
-
<span className="chart-tooltip-label">
|
|
575
|
+
<span className="chart-tooltip-label">{t("common.output")}</span>
|
|
547
576
|
<span className="chart-tooltip-value">{formatInteger(hours.output[hour])}</span>
|
|
548
577
|
</div>
|
|
549
578
|
<div className="chart-tooltip-row">
|
|
550
|
-
<span className="chart-tooltip-label">
|
|
579
|
+
<span className="chart-tooltip-label">{t("common.requests")}</span>
|
|
551
580
|
<span className="chart-tooltip-value">{formatInteger(hours.requests[hour])}</span>
|
|
552
581
|
</div>
|
|
553
582
|
</>
|
|
@@ -579,7 +608,7 @@ function WindowInsightsTable({
|
|
|
579
608
|
limit={12}
|
|
580
609
|
selectedKey={selected ? `${selected.provider}::${selected.windowKey}` : null}
|
|
581
610
|
onRowClick={i => onSelect({ provider: i.provider, windowKey: i.windowKey })}
|
|
582
|
-
empty={<EmptyState title="
|
|
611
|
+
empty={<EmptyState title={t("providers.no-snapshots-in-range")} hint={SNAPSHOT_HINT} />}
|
|
583
612
|
/>
|
|
584
613
|
);
|
|
585
614
|
}
|
|
@@ -587,53 +616,53 @@ function WindowInsightsTable({
|
|
|
587
616
|
const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
|
|
588
617
|
{
|
|
589
618
|
key: "window",
|
|
590
|
-
header: "
|
|
591
|
-
sort: i => `${i.provider} ${i.
|
|
592
|
-
render: i => <LabelCell primary={i.
|
|
619
|
+
header: t("providers.window"),
|
|
620
|
+
sort: i => `${i.provider} ${i.windowKey}`,
|
|
621
|
+
render: i => <LabelCell primary={i.windowKey} secondary={<span className="mono">{i.provider}</span>} />,
|
|
593
622
|
},
|
|
594
623
|
{
|
|
595
624
|
key: "accounts",
|
|
596
|
-
header: "
|
|
597
|
-
title: "
|
|
625
|
+
header: t("providers.accounts"),
|
|
626
|
+
title: t("providers.accounts-title"),
|
|
598
627
|
align: "right",
|
|
599
628
|
sort: i => i.accounts,
|
|
600
629
|
render: i => <span className="num">{formatInteger(i.accounts)}</span>,
|
|
601
630
|
},
|
|
602
631
|
{
|
|
603
632
|
key: "cycles",
|
|
604
|
-
header: "
|
|
605
|
-
title: "
|
|
633
|
+
header: t("providers.resets"),
|
|
634
|
+
title: t("providers.resets-title"),
|
|
606
635
|
align: "right",
|
|
607
636
|
sort: i => i.cycles,
|
|
608
637
|
render: i => <span className="num muted">{formatInteger(i.cycles)}</span>,
|
|
609
638
|
},
|
|
610
639
|
{
|
|
611
640
|
key: "consumed",
|
|
612
|
-
header: "
|
|
613
|
-
title: "
|
|
641
|
+
header: t("providers.windows-burned"),
|
|
642
|
+
title: t("providers.windows-burned-title"),
|
|
614
643
|
align: "right",
|
|
615
644
|
sort: i => i.fractionConsumed,
|
|
616
645
|
render: i => <span className="num">{i.fractionConsumed.toFixed(2)}</span>,
|
|
617
646
|
},
|
|
618
647
|
{
|
|
619
648
|
key: "capacity",
|
|
620
|
-
header: "
|
|
621
|
-
title: "
|
|
649
|
+
header: t("providers.tokens-per-window"),
|
|
650
|
+
title: t("providers.tokens-per-window-title"),
|
|
622
651
|
align: "right",
|
|
623
652
|
sort: i => i.estTokensPerWindow ?? -1,
|
|
624
653
|
render: i =>
|
|
625
654
|
i.estTokensPerWindow !== null ? (
|
|
626
655
|
<span className="num">{formatCompact(i.estTokensPerWindow)}</span>
|
|
627
656
|
) : (
|
|
628
|
-
<span className="dim" title="
|
|
657
|
+
<span className="dim" title={t("providers.too-little-consumed")}>
|
|
629
658
|
–
|
|
630
659
|
</span>
|
|
631
660
|
),
|
|
632
661
|
},
|
|
633
662
|
{
|
|
634
663
|
key: "peak",
|
|
635
|
-
header: "
|
|
636
|
-
title: "
|
|
664
|
+
header: t("providers.peak-utilization"),
|
|
665
|
+
title: t("providers.peak-utilization-title"),
|
|
637
666
|
align: "right",
|
|
638
667
|
sort: i => i.peakConcurrentFraction,
|
|
639
668
|
render: i => {
|
|
@@ -650,14 +679,17 @@ const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
|
|
|
650
679
|
},
|
|
651
680
|
{
|
|
652
681
|
key: "ideal",
|
|
653
|
-
header: "
|
|
654
|
-
title: "
|
|
682
|
+
header: t("providers.accounts-needed"),
|
|
683
|
+
title: t("providers.accounts-needed-title"),
|
|
655
684
|
align: "right",
|
|
656
685
|
sort: i => i.idealAccounts - i.accounts,
|
|
657
686
|
render: i =>
|
|
658
687
|
i.idealAccounts > i.accounts ? (
|
|
659
688
|
<Badge tone="warn" mono>
|
|
660
|
-
{
|
|
689
|
+
{t("providers.ideal-accounts", {
|
|
690
|
+
ideal: formatInteger(i.idealAccounts),
|
|
691
|
+
have: formatInteger(i.accounts),
|
|
692
|
+
})}
|
|
661
693
|
</Badge>
|
|
662
694
|
) : (
|
|
663
695
|
<span className="num">{formatInteger(i.idealAccounts)}</span>
|
|
@@ -665,14 +697,14 @@ const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
|
|
|
665
697
|
},
|
|
666
698
|
{
|
|
667
699
|
key: "exhausted",
|
|
668
|
-
header: "
|
|
669
|
-
title: "
|
|
700
|
+
header: t("providers.exhaustions"),
|
|
701
|
+
title: t("providers.exhaustions-title"),
|
|
670
702
|
align: "right",
|
|
671
703
|
sort: i => i.exhaustedEvents,
|
|
672
704
|
render: i =>
|
|
673
705
|
i.exhaustedEvents > 0 ? (
|
|
674
706
|
<Badge tone="warn" mono>
|
|
675
|
-
{formatInteger(i.exhaustedEvents)}
|
|
707
|
+
{t("providers.exhausted-n", { count: formatInteger(i.exhaustedEvents) })}
|
|
676
708
|
</Badge>
|
|
677
709
|
) : (
|
|
678
710
|
<span className="num dim">0</span>
|
|
@@ -737,7 +769,7 @@ function WindowUtilizationCard({
|
|
|
737
769
|
() =>
|
|
738
770
|
(insights ?? [])
|
|
739
771
|
.filter(i => i.provider === selected?.provider)
|
|
740
|
-
.map(i => ({ key: i.windowKey, label: i.
|
|
772
|
+
.map(i => ({ key: i.windowKey, label: i.windowKey })),
|
|
741
773
|
[insights, selected?.provider],
|
|
742
774
|
);
|
|
743
775
|
// While another provider's series are still on screen, show none of them.
|
|
@@ -791,7 +823,7 @@ function WindowUtilizationCard({
|
|
|
791
823
|
.sort(
|
|
792
824
|
(a, b) =>
|
|
793
825
|
Number(b.color !== null) - Number(a.color !== null) ||
|
|
794
|
-
a.series.
|
|
826
|
+
a.series.windowKey.localeCompare(b.series.windowKey) ||
|
|
795
827
|
(b.latest?.fraction ?? -1) - (a.latest?.fraction ?? -1),
|
|
796
828
|
);
|
|
797
829
|
}, [providerSeries, names, chart.series, selected?.windowKey]);
|
|
@@ -801,11 +833,15 @@ function WindowUtilizationCard({
|
|
|
801
833
|
return (
|
|
802
834
|
<Card
|
|
803
835
|
index={5}
|
|
804
|
-
title="
|
|
836
|
+
title={t("providers.utilization")}
|
|
805
837
|
description={
|
|
806
838
|
chart.buckets.length > 0
|
|
807
|
-
?
|
|
808
|
-
|
|
839
|
+
? t("providers.utilization-description", {
|
|
840
|
+
window: windowLabel ?? t("providers.window"),
|
|
841
|
+
from: formatDateTimeShort(chart.first),
|
|
842
|
+
to: formatDateTimeShort(chart.last),
|
|
843
|
+
})
|
|
844
|
+
: t("providers.utilization-description-plain")
|
|
809
845
|
}
|
|
810
846
|
actions={
|
|
811
847
|
selected && (
|
|
@@ -818,7 +854,7 @@ function WindowUtilizationCard({
|
|
|
818
854
|
const first = insights?.find(i => i.provider === next);
|
|
819
855
|
if (first) onSelect({ provider: next, windowKey: first.windowKey });
|
|
820
856
|
}}
|
|
821
|
-
aria-label="
|
|
857
|
+
aria-label={t("common.provider")}
|
|
822
858
|
>
|
|
823
859
|
{providers.map(p => (
|
|
824
860
|
<option key={p} value={p}>
|
|
@@ -830,7 +866,7 @@ function WindowUtilizationCard({
|
|
|
830
866
|
className="input providers-select"
|
|
831
867
|
value={selected.windowKey}
|
|
832
868
|
onChange={e => onSelect({ provider: selected.provider, windowKey: e.target.value })}
|
|
833
|
-
aria-label="
|
|
869
|
+
aria-label={t("providers.window")}
|
|
834
870
|
>
|
|
835
871
|
{windows.map(w => (
|
|
836
872
|
<option key={w.key} value={w.key}>
|
|
@@ -846,7 +882,7 @@ function WindowUtilizationCard({
|
|
|
846
882
|
{insights === null ? (
|
|
847
883
|
<ChartSkeleton height={260} />
|
|
848
884
|
) : selected === null ? (
|
|
849
|
-
<EmptyState title="
|
|
885
|
+
<EmptyState title={t("providers.no-snapshots-in-range")} hint={SNAPSHOT_HINT} />
|
|
850
886
|
) : (
|
|
851
887
|
<QueryView query={accounts} skeleton={<ChartSkeleton height={260} />}>
|
|
852
888
|
{() =>
|
|
@@ -873,12 +909,12 @@ function WindowUtilizationCard({
|
|
|
873
909
|
format={v => formatPercent(v, 0)}
|
|
874
910
|
formatTooltip={v => formatPercent(v, 1)}
|
|
875
911
|
references={[{ value: 1, label: "100%" }]}
|
|
876
|
-
emptyLabel="
|
|
912
|
+
emptyLabel={t("providers.no-utilization-readings")}
|
|
877
913
|
tooltipExtra={i => {
|
|
878
914
|
const exhausted = chart.exhausted[i];
|
|
879
915
|
return exhausted.length > 0 ? (
|
|
880
916
|
<div className="chart-tooltip-row">
|
|
881
|
-
<span className="chart-tooltip-label tone-bad">
|
|
917
|
+
<span className="chart-tooltip-label tone-bad">{t("providers.exhausted")}</span>
|
|
882
918
|
<span className="chart-tooltip-value">{exhausted.join(", ")}</span>
|
|
883
919
|
</div>
|
|
884
920
|
) : null;
|
|
@@ -979,16 +1015,16 @@ function buildUtilization(accounts: UsageWindowSeries[], names: ReadonlyMap<stri
|
|
|
979
1015
|
}
|
|
980
1016
|
|
|
981
1017
|
function statusBadge(latest: AccountRow["latest"]): ReactNode {
|
|
982
|
-
if (!latest) return <span className="dim">
|
|
983
|
-
if (latest.exhausted) return <Badge tone="bad">
|
|
984
|
-
if (latest.fraction >= 0.8) return <Badge tone="warn">
|
|
985
|
-
return <Badge tone="ok">
|
|
1018
|
+
if (!latest) return <span className="dim">{t("providers.no-reading")}</span>;
|
|
1019
|
+
if (latest.exhausted) return <Badge tone="bad">{t("providers.exhausted")}</Badge>;
|
|
1020
|
+
if (latest.fraction >= 0.8) return <Badge tone="warn">{t("providers.high")}</Badge>;
|
|
1021
|
+
return <Badge tone="ok">{t("ui.status-ok")}</Badge>;
|
|
986
1022
|
}
|
|
987
1023
|
|
|
988
1024
|
const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
|
|
989
1025
|
{
|
|
990
1026
|
key: "account",
|
|
991
|
-
header: "
|
|
1027
|
+
header: t("providers.account"),
|
|
992
1028
|
sort: r => r.name,
|
|
993
1029
|
render: r => (
|
|
994
1030
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -1001,13 +1037,13 @@ const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
|
|
|
1001
1037
|
},
|
|
1002
1038
|
{
|
|
1003
1039
|
key: "window",
|
|
1004
|
-
header: "
|
|
1005
|
-
sort: r => r.series.
|
|
1006
|
-
render: r => <span className={r.color ? undefined : "muted"}>{r.series.
|
|
1040
|
+
header: t("providers.window"),
|
|
1041
|
+
sort: r => r.series.windowKey,
|
|
1042
|
+
render: r => <span className={r.color ? undefined : "muted"}>{r.series.windowKey}</span>,
|
|
1007
1043
|
},
|
|
1008
1044
|
{
|
|
1009
1045
|
key: "latest",
|
|
1010
|
-
header: "
|
|
1046
|
+
header: t("providers.latest-used"),
|
|
1011
1047
|
align: "right",
|
|
1012
1048
|
sort: r => r.latest?.fraction ?? -1,
|
|
1013
1049
|
render: r =>
|
|
@@ -1024,27 +1060,27 @@ const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
|
|
|
1024
1060
|
},
|
|
1025
1061
|
{
|
|
1026
1062
|
key: "status",
|
|
1027
|
-
header: "
|
|
1063
|
+
header: t("providers.status"),
|
|
1028
1064
|
sort: r => (r.latest?.exhausted ? 2 : (r.latest?.fraction ?? 0) >= 0.8 ? 1 : 0),
|
|
1029
1065
|
render: r => statusBadge(r.latest),
|
|
1030
1066
|
},
|
|
1031
1067
|
{
|
|
1032
1068
|
key: "peak",
|
|
1033
|
-
header: "
|
|
1069
|
+
header: t("providers.peak-in-range"),
|
|
1034
1070
|
align: "right",
|
|
1035
1071
|
sort: r => r.peak ?? -1,
|
|
1036
1072
|
render: r => <span className="num">{r.peak === null ? "–" : formatPercent(r.peak, 0)}</span>,
|
|
1037
1073
|
},
|
|
1038
1074
|
{
|
|
1039
1075
|
key: "samples",
|
|
1040
|
-
header: "
|
|
1076
|
+
header: t("providers.snapshots"),
|
|
1041
1077
|
align: "right",
|
|
1042
1078
|
sort: r => r.samples,
|
|
1043
1079
|
render: r => <span className="num muted">{formatInteger(r.samples)}</span>,
|
|
1044
1080
|
},
|
|
1045
1081
|
{
|
|
1046
1082
|
key: "recorded",
|
|
1047
|
-
header: "
|
|
1083
|
+
header: t("providers.recorded"),
|
|
1048
1084
|
align: "right",
|
|
1049
1085
|
sort: r => r.latest?.timestamp ?? 0,
|
|
1050
1086
|
render: r => <span className="muted">{r.latest ? formatRelativeTime(r.latest.timestamp) : "–"}</span>,
|