@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,10 +1,12 @@
|
|
|
1
1
|
import { ArrowRight } from "lucide-react";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { getOverviewStats, getRecentRequests } from "../api";
|
|
4
|
+
import { t } from "../i18n";
|
|
4
5
|
import { Legend, ShareBar, TimeChart } from "../charts";
|
|
5
6
|
import {
|
|
6
7
|
formatCompact,
|
|
7
8
|
formatDurationMs,
|
|
9
|
+
formatErrorRate,
|
|
8
10
|
formatEstimatedCost,
|
|
9
11
|
formatInteger,
|
|
10
12
|
formatMessageCost,
|
|
@@ -41,12 +43,16 @@ export interface OverviewRouteProps {
|
|
|
41
43
|
type ActivityMetric = "requests" | "tokens" | "cost";
|
|
42
44
|
|
|
43
45
|
const ACTIVITY_OPTIONS = [
|
|
44
|
-
{ value: "requests" as const, label: "
|
|
45
|
-
{ value: "tokens" as const, label: "
|
|
46
|
-
{ value: "cost" as const, label: "
|
|
46
|
+
{ value: "requests" as const, label: t("common.requests") },
|
|
47
|
+
{ value: "tokens" as const, label: t("common.tokens") },
|
|
48
|
+
{ value: "cost" as const, label: t("common.cost") },
|
|
47
49
|
];
|
|
48
50
|
|
|
49
|
-
const AGENT_LABEL: Record<AgentType, string> = {
|
|
51
|
+
const AGENT_LABEL: Record<AgentType, string> = {
|
|
52
|
+
main: t("overview.main-agent"),
|
|
53
|
+
subagent: t("overview.subagents"),
|
|
54
|
+
advisor: t("overview.advisor"),
|
|
55
|
+
};
|
|
50
56
|
const AGENT_COLOR: Record<AgentType, string> = {
|
|
51
57
|
main: "var(--chart-primary)",
|
|
52
58
|
subagent: "var(--chart-secondary)",
|
|
@@ -54,15 +60,15 @@ const AGENT_COLOR: Record<AgentType, string> = {
|
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
const TOKEN_MIX = [
|
|
57
|
-
{ key: "input", label: "
|
|
58
|
-
{ key: "cacheRead", label: "
|
|
59
|
-
{ key: "cacheWrite", label: "
|
|
60
|
-
{ key: "output", label: "
|
|
63
|
+
{ key: "input", label: t("overview.uncached-input"), color: "#5b8cff" },
|
|
64
|
+
{ key: "cacheRead", label: t("common.cache-read"), color: "var(--chart-primary)" },
|
|
65
|
+
{ key: "cacheWrite", label: t("common.cache-write"), color: "#f5b54a" },
|
|
66
|
+
{ key: "output", label: t("common.output"), color: "var(--chart-secondary)" },
|
|
61
67
|
] as const;
|
|
62
68
|
|
|
63
69
|
export function OverviewRoute({ active, range, onRequestClick }: OverviewRouteProps) {
|
|
64
|
-
const overview = useQuery(["overview", range], () => getOverviewStats(range), { enabled: active });
|
|
65
|
-
const recent = useQuery(["recent-requests"], () => getRecentRequests(12), { enabled: active });
|
|
70
|
+
const overview = useQuery(["overview", range], ({ signal }) => getOverviewStats(range, signal), { enabled: active });
|
|
71
|
+
const recent = useQuery(["recent-requests"], ({ signal }) => getRecentRequests(12, signal), { enabled: active });
|
|
66
72
|
const [metric, setMetric] = useState<ActivityMetric>("requests");
|
|
67
73
|
const meta = rangeMeta(range);
|
|
68
74
|
|
|
@@ -85,18 +91,25 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
85
91
|
const chartSeries =
|
|
86
92
|
metric === "requests"
|
|
87
93
|
? [
|
|
88
|
-
{ key: "ok", label: "
|
|
89
|
-
{ key: "err", label: "
|
|
94
|
+
{ key: "ok", label: t("overview.succeeded"), color: "var(--chart-primary)", values: series.requests },
|
|
95
|
+
{ key: "err", label: t("common.failed"), color: "var(--bad)", values: series.errors },
|
|
90
96
|
]
|
|
91
97
|
: metric === "tokens"
|
|
92
|
-
? [{ key: "tokens", label: "
|
|
93
|
-
: [
|
|
98
|
+
? [{ key: "tokens", label: t("common.tokens"), color: "var(--chart-primary)", values: series.tokens }]
|
|
99
|
+
: [
|
|
100
|
+
{
|
|
101
|
+
key: "cost",
|
|
102
|
+
label: t("overview.api-equivalent"),
|
|
103
|
+
color: "var(--chart-secondary)",
|
|
104
|
+
values: series.cost,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
94
107
|
|
|
95
108
|
return (
|
|
96
109
|
<div className="page">
|
|
97
110
|
<PageHeader
|
|
98
|
-
title="
|
|
99
|
-
description={
|
|
111
|
+
title={t("overview.title")}
|
|
112
|
+
description={t("overview.description", { window: t(meta.windowKey) })}
|
|
100
113
|
/>
|
|
101
114
|
|
|
102
115
|
<QueryView query={overview} skeleton={<ChartSkeleton height={112} />}>
|
|
@@ -104,57 +117,73 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
104
117
|
<div data-stale={overview.stale} className="stack" style={{ gap: 16 }}>
|
|
105
118
|
<StatGrid min={190}>
|
|
106
119
|
<Stat
|
|
107
|
-
label="
|
|
108
|
-
title="
|
|
120
|
+
label={t("overview.api-equivalent-cost")}
|
|
121
|
+
title={t("overview.api-cost-title")}
|
|
109
122
|
value={formatEstimatedCost(overall.totalCost, overall.unpricedRequests)}
|
|
110
123
|
hint={
|
|
111
124
|
overall.unpricedRequests > 0
|
|
112
|
-
?
|
|
125
|
+
? t("overview.unpriced-n", { count: formatInteger(overall.unpricedRequests) })
|
|
113
126
|
: undefined
|
|
114
127
|
}
|
|
115
128
|
spark={series.cost}
|
|
116
129
|
sparkColor="var(--chart-secondary)"
|
|
117
130
|
/>
|
|
118
131
|
<Stat
|
|
119
|
-
label="
|
|
132
|
+
label={t("common.requests")}
|
|
120
133
|
value={formatInteger(overall.totalRequests)}
|
|
121
|
-
hint={
|
|
134
|
+
hint={t("overview.failed-n", { count: formatInteger(overall.failedRequests) })}
|
|
122
135
|
spark={series.all}
|
|
123
136
|
/>
|
|
124
137
|
<Stat
|
|
125
|
-
label="
|
|
126
|
-
title="
|
|
138
|
+
label={t("overview.conversation-tokens")}
|
|
139
|
+
title={t("overview.tokens-title")}
|
|
127
140
|
value={formatCompact(sumConversationTokens(overall))}
|
|
128
|
-
hint={
|
|
141
|
+
hint={t("overview.output-n", { count: formatCompact(overall.totalOutputTokens) })}
|
|
129
142
|
spark={series.tokens}
|
|
130
143
|
/>
|
|
131
144
|
<Stat
|
|
132
|
-
label="
|
|
133
|
-
title="
|
|
145
|
+
label={t("overview.cache-rate")}
|
|
146
|
+
title={t("overview.cache-rate-title")}
|
|
134
147
|
value={formatPercent(overall.cacheRate)}
|
|
135
|
-
hint={
|
|
148
|
+
hint={t("overview.cache-saved", { value: formatPercent(overall.cacheSavings) })}
|
|
136
149
|
/>
|
|
137
150
|
<Stat
|
|
138
|
-
label="
|
|
139
|
-
value={
|
|
140
|
-
hint={
|
|
151
|
+
label={t("common.error-rate")}
|
|
152
|
+
value={formatErrorRate(overall.errorRate)}
|
|
153
|
+
hint={t("overview.succeeded-n", { count: formatInteger(overall.successfulRequests) })}
|
|
141
154
|
spark={series.errors}
|
|
142
155
|
sparkColor="var(--bad)"
|
|
143
156
|
/>
|
|
144
157
|
</StatGrid>
|
|
145
158
|
<StatGrid min={140}>
|
|
146
|
-
<Stat size="sm" label="Uncached input" value={formatCompact(overall.totalInputTokens)} />
|
|
147
|
-
<Stat size="sm" label="Cache read" value={formatCompact(overall.totalCacheReadTokens)} />
|
|
148
|
-
<Stat size="sm" label="Cache write" value={formatCompact(overall.totalCacheWriteTokens)} />
|
|
149
|
-
<Stat size="sm" label="Output" value={formatCompact(overall.totalOutputTokens)} />
|
|
150
159
|
<Stat
|
|
151
160
|
size="sm"
|
|
152
|
-
label="
|
|
161
|
+
label={t("overview.uncached-input")}
|
|
162
|
+
value={formatCompact(overall.totalInputTokens)}
|
|
163
|
+
/>
|
|
164
|
+
<Stat
|
|
165
|
+
size="sm"
|
|
166
|
+
label={t("common.cache-read")}
|
|
167
|
+
value={formatCompact(overall.totalCacheReadTokens)}
|
|
168
|
+
/>
|
|
169
|
+
<Stat
|
|
170
|
+
size="sm"
|
|
171
|
+
label={t("common.cache-write")}
|
|
172
|
+
value={formatCompact(overall.totalCacheWriteTokens)}
|
|
173
|
+
/>
|
|
174
|
+
<Stat size="sm" label={t("common.output")} value={formatCompact(overall.totalOutputTokens)} />
|
|
175
|
+
<Stat
|
|
176
|
+
size="sm"
|
|
177
|
+
label={t("common.premium-requests")}
|
|
153
178
|
value={formatInteger(Math.round(overall.totalPremiumRequests * 100) / 100)}
|
|
154
179
|
/>
|
|
155
|
-
<Stat
|
|
156
|
-
|
|
157
|
-
|
|
180
|
+
<Stat
|
|
181
|
+
size="sm"
|
|
182
|
+
label={t("common.tokens-per-second")}
|
|
183
|
+
value={formatTokensPerSecond(overall.avgTokensPerSecond)}
|
|
184
|
+
/>
|
|
185
|
+
<Stat size="sm" label={t("overview.avg-latency")} value={formatDurationMs(overall.avgDuration)} />
|
|
186
|
+
<Stat size="sm" label={t("overview.avg-ttft")} value={formatDurationMs(overall.avgTtft)} />
|
|
158
187
|
</StatGrid>
|
|
159
188
|
</div>
|
|
160
189
|
)}
|
|
@@ -163,8 +192,14 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
163
192
|
<div className="grid grid-main-side">
|
|
164
193
|
<Card
|
|
165
194
|
index={1}
|
|
166
|
-
title="
|
|
167
|
-
description={
|
|
195
|
+
title={t("overview.activity")}
|
|
196
|
+
description={
|
|
197
|
+
meta.bucketMs < 3_600_000
|
|
198
|
+
? t("overview.per-5m")
|
|
199
|
+
: meta.bucketMs < 86_400_000
|
|
200
|
+
? t("overview.per-hour")
|
|
201
|
+
: t("overview.per-day")
|
|
202
|
+
}
|
|
168
203
|
actions={<Segmented size="sm" options={ACTIVITY_OPTIONS} value={metric} onChange={setMetric} />}
|
|
169
204
|
stale={overview.stale}
|
|
170
205
|
>
|
|
@@ -181,7 +216,12 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
181
216
|
</QueryView>
|
|
182
217
|
</Card>
|
|
183
218
|
|
|
184
|
-
<Card
|
|
219
|
+
<Card
|
|
220
|
+
index={2}
|
|
221
|
+
title={t("overview.token-mix")}
|
|
222
|
+
description={t("overview.token-mix-description")}
|
|
223
|
+
stale={overview.stale}
|
|
224
|
+
>
|
|
185
225
|
<QueryView query={overview} skeleton={<ChartSkeleton height={260} />}>
|
|
186
226
|
{({ overall, byAgentType }) => {
|
|
187
227
|
const mix = {
|
|
@@ -196,25 +236,25 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
196
236
|
<div className="stack" style={{ gap: 18 }}>
|
|
197
237
|
<div className="stack" style={{ gap: 10 }}>
|
|
198
238
|
<ShareBar
|
|
199
|
-
segments={TOKEN_MIX.map(
|
|
200
|
-
key:
|
|
201
|
-
label:
|
|
202
|
-
value: mix[
|
|
203
|
-
color:
|
|
239
|
+
segments={TOKEN_MIX.map(slice => ({
|
|
240
|
+
key: slice.key,
|
|
241
|
+
label: slice.label,
|
|
242
|
+
value: mix[slice.key],
|
|
243
|
+
color: slice.color,
|
|
204
244
|
}))}
|
|
205
245
|
/>
|
|
206
246
|
<Legend
|
|
207
|
-
items={TOKEN_MIX.map(
|
|
208
|
-
key:
|
|
209
|
-
label:
|
|
210
|
-
color:
|
|
211
|
-
value: total > 0 ? formatPercent(mix[
|
|
247
|
+
items={TOKEN_MIX.map(slice => ({
|
|
248
|
+
key: slice.key,
|
|
249
|
+
label: slice.label,
|
|
250
|
+
color: slice.color,
|
|
251
|
+
value: total > 0 ? formatPercent(mix[slice.key] / total, 0) : "–",
|
|
212
252
|
}))}
|
|
213
253
|
/>
|
|
214
254
|
</div>
|
|
215
255
|
<div className="stack" style={{ gap: 10 }}>
|
|
216
256
|
<div className="section-label" style={{ marginBottom: 0 }}>
|
|
217
|
-
|
|
257
|
+
{t("overview.by-agent")}
|
|
218
258
|
</div>
|
|
219
259
|
<ShareBar
|
|
220
260
|
segments={agents.segments.map(s => ({
|
|
@@ -229,7 +269,9 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
229
269
|
<span className="row">
|
|
230
270
|
<span className="swatch" style={{ background: AGENT_COLOR[s.agentType] }} />
|
|
231
271
|
{AGENT_LABEL[s.agentType]}
|
|
232
|
-
<span className="dim num">
|
|
272
|
+
<span className="dim num">
|
|
273
|
+
{t("overview.requests-short", { count: formatInteger(s.requests) })}
|
|
274
|
+
</span>
|
|
233
275
|
</span>
|
|
234
276
|
<span className="row">
|
|
235
277
|
<span className="dim num">{formatCompact(s.tokens)}</span>
|
|
@@ -251,13 +293,13 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
251
293
|
index={3}
|
|
252
294
|
title={
|
|
253
295
|
<>
|
|
254
|
-
<Dot tone="live" pulse />
|
|
296
|
+
<Dot tone="live" pulse /> {t("overview.latest-requests")}
|
|
255
297
|
</>
|
|
256
298
|
}
|
|
257
|
-
description="
|
|
299
|
+
description={t("overview.latest-requests-description")}
|
|
258
300
|
actions={
|
|
259
301
|
<a className="btn" data-size="sm" data-variant="ghost" href={`#/requests?range=${range}`}>
|
|
260
|
-
|
|
302
|
+
{t("overview.all-requests")} <ArrowRight size={13} />
|
|
261
303
|
</a>
|
|
262
304
|
}
|
|
263
305
|
flush
|
|
@@ -281,37 +323,41 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
281
323
|
const REQUEST_COLUMNS = [
|
|
282
324
|
{
|
|
283
325
|
key: "model",
|
|
284
|
-
header: "
|
|
326
|
+
header: t("common.model"),
|
|
285
327
|
render: (row: MessageStats) => <LabelCell primary={row.model} secondary={row.provider} />,
|
|
286
328
|
},
|
|
287
329
|
{
|
|
288
330
|
key: "time",
|
|
289
|
-
header: "
|
|
331
|
+
header: t("overview.when"),
|
|
290
332
|
render: (row: MessageStats) => <span className="muted">{formatRelativeTime(row.timestamp)}</span>,
|
|
291
333
|
},
|
|
292
334
|
{
|
|
293
335
|
key: "tokens",
|
|
294
|
-
header: "
|
|
336
|
+
header: t("common.tokens"),
|
|
295
337
|
align: "right" as const,
|
|
296
338
|
render: (row: MessageStats) => <span className="num">{formatInteger(row.usage.totalTokens)}</span>,
|
|
297
339
|
},
|
|
298
340
|
{
|
|
299
341
|
key: "cost",
|
|
300
|
-
header: "
|
|
342
|
+
header: t("common.cost"),
|
|
301
343
|
align: "right" as const,
|
|
302
344
|
render: (row: MessageStats) => <span className="num">{formatMessageCost(row, 4)}</span>,
|
|
303
345
|
},
|
|
304
346
|
{
|
|
305
347
|
key: "duration",
|
|
306
|
-
header: "
|
|
348
|
+
header: t("common.duration"),
|
|
307
349
|
align: "right" as const,
|
|
308
350
|
render: (row: MessageStats) => <span className="num">{formatDurationMs(row.duration)}</span>,
|
|
309
351
|
},
|
|
310
352
|
{
|
|
311
353
|
key: "status",
|
|
312
|
-
header: "
|
|
354
|
+
header: t("overview.status"),
|
|
313
355
|
align: "right" as const,
|
|
314
356
|
render: (row: MessageStats) =>
|
|
315
|
-
row.errorMessage ?
|
|
357
|
+
row.errorMessage ? (
|
|
358
|
+
<Badge tone="bad">{t("common.failed")}</Badge>
|
|
359
|
+
) : (
|
|
360
|
+
<Badge tone="ok">{t("overview.ok")}</Badge>
|
|
361
|
+
),
|
|
316
362
|
},
|
|
317
363
|
];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useDeferredValue, useMemo, useState } from "react";
|
|
2
2
|
import { getFolderStats } from "../api";
|
|
3
|
+
import { t } from "../i18n";
|
|
3
4
|
import { BarList } from "../charts";
|
|
4
5
|
import {
|
|
5
6
|
formatCompact,
|
|
6
7
|
formatDurationMs,
|
|
8
|
+
formatErrorRate,
|
|
7
9
|
formatEstimatedCost,
|
|
8
10
|
formatFolder,
|
|
9
11
|
formatInteger,
|
|
@@ -42,7 +44,7 @@ const TABLE_LIMIT = 100;
|
|
|
42
44
|
const TOP_LIMIT = 8;
|
|
43
45
|
|
|
44
46
|
export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
|
|
45
|
-
const folders = useQuery(["projects", range], () => getFolderStats(range), { enabled: active });
|
|
47
|
+
const folders = useQuery(["projects", range], ({ signal }) => getFolderStats(range, signal), { enabled: active });
|
|
46
48
|
const [search, setSearch] = useState("");
|
|
47
49
|
// Benchmarks and scratch sessions can leave tens of thousands of one-off temp folders; hide them by default.
|
|
48
50
|
const [hideTemporary, setHideTemporary] = useState(true);
|
|
@@ -66,47 +68,54 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
|
|
|
66
68
|
[scoped],
|
|
67
69
|
);
|
|
68
70
|
const topEmpty = (
|
|
69
|
-
<EmptyState
|
|
70
|
-
title={view.rows.length === 0 ? "No project folders in this range" : "Only temporary folders in this range"}
|
|
71
|
-
/>
|
|
71
|
+
<EmptyState title={t(view.rows.length === 0 ? "projects.empty-no-folders" : "projects.empty-only-temporary")} />
|
|
72
72
|
);
|
|
73
73
|
// Meter scales follow the whole range so a bar's length does not change while filtering.
|
|
74
74
|
const columns = useMemo(() => folderColumns(view.maxRequests, view.maxCost), [view]);
|
|
75
75
|
|
|
76
76
|
return (
|
|
77
77
|
<div className="page">
|
|
78
|
-
<PageHeader
|
|
78
|
+
<PageHeader
|
|
79
|
+
title={t("projects.title")}
|
|
80
|
+
description={t("projects.description", { window: t(meta.windowKey) })}
|
|
81
|
+
/>
|
|
79
82
|
|
|
80
83
|
<QueryView query={folders} skeleton={<ChartSkeleton height={96} />}>
|
|
81
84
|
{() => (
|
|
82
85
|
<div data-stale={folders.stale}>
|
|
83
86
|
<StatGrid min={180}>
|
|
84
87
|
<Stat
|
|
85
|
-
label="
|
|
88
|
+
label={t("projects.folders")}
|
|
86
89
|
value={formatInteger(view.rows.length)}
|
|
87
|
-
hint={
|
|
90
|
+
hint={
|
|
91
|
+
view.temporaryCount > 0
|
|
92
|
+
? t("projects.temporary-n", { count: formatInteger(view.temporaryCount) })
|
|
93
|
+
: undefined
|
|
94
|
+
}
|
|
88
95
|
/>
|
|
89
96
|
<Stat
|
|
90
|
-
label="
|
|
97
|
+
label={t("common.requests")}
|
|
91
98
|
value={formatInteger(view.totalRequests)}
|
|
92
|
-
hint={
|
|
99
|
+
hint={t("projects.failed-n", { count: formatInteger(view.failedRequests) })}
|
|
93
100
|
/>
|
|
94
101
|
<Stat
|
|
95
|
-
label="
|
|
96
|
-
title="
|
|
102
|
+
label={t("projects.api-equivalent-cost")}
|
|
103
|
+
title={t("projects.api-equivalent-cost-hint")}
|
|
97
104
|
value={formatEstimatedCost(view.totalCost, view.unpricedRequests)}
|
|
98
105
|
hint={
|
|
99
|
-
view.unpricedRequests > 0
|
|
106
|
+
view.unpricedRequests > 0
|
|
107
|
+
? t("projects.unpriced-n", { count: formatInteger(view.unpricedRequests) })
|
|
108
|
+
: undefined
|
|
100
109
|
}
|
|
101
110
|
/>
|
|
102
111
|
<Stat
|
|
103
|
-
label="
|
|
104
|
-
title="
|
|
112
|
+
label={t("projects.conversation-tokens")}
|
|
113
|
+
title={t("projects.conversation-tokens-hint")}
|
|
105
114
|
value={formatCompact(view.conversationTokens)}
|
|
106
115
|
/>
|
|
107
116
|
<Stat
|
|
108
|
-
label="
|
|
109
|
-
title="
|
|
117
|
+
label={t("projects.cache-rate")}
|
|
118
|
+
title={t("projects.cache-rate-hint-all")}
|
|
110
119
|
value={formatPercent(view.cacheRate)}
|
|
111
120
|
/>
|
|
112
121
|
</StatGrid>
|
|
@@ -115,7 +124,12 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
|
|
|
115
124
|
</QueryView>
|
|
116
125
|
|
|
117
126
|
<div className="grid grid-2">
|
|
118
|
-
<Card
|
|
127
|
+
<Card
|
|
128
|
+
index={1}
|
|
129
|
+
title={t("projects.top-by-cost")}
|
|
130
|
+
description={t("projects.top-by-cost-description")}
|
|
131
|
+
stale={folders.stale}
|
|
132
|
+
>
|
|
119
133
|
<QueryView
|
|
120
134
|
query={folders}
|
|
121
135
|
skeleton={<ChartSkeleton height={236} />}
|
|
@@ -136,7 +150,12 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
|
|
|
136
150
|
)}
|
|
137
151
|
</QueryView>
|
|
138
152
|
</Card>
|
|
139
|
-
<Card
|
|
153
|
+
<Card
|
|
154
|
+
index={2}
|
|
155
|
+
title={t("projects.top-by-requests")}
|
|
156
|
+
description={t("projects.top-by-requests-description")}
|
|
157
|
+
stale={folders.stale}
|
|
158
|
+
>
|
|
140
159
|
<QueryView
|
|
141
160
|
query={folders}
|
|
142
161
|
skeleton={<ChartSkeleton height={236} />}
|
|
@@ -160,27 +179,35 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
|
|
|
160
179
|
|
|
161
180
|
<Card
|
|
162
181
|
index={3}
|
|
163
|
-
title="
|
|
182
|
+
title={t("projects.folders")}
|
|
164
183
|
description={
|
|
165
184
|
folders.data
|
|
166
185
|
? matching.length === view.rows.length
|
|
167
|
-
?
|
|
168
|
-
:
|
|
186
|
+
? t("projects.folders-n", { count: formatInteger(view.rows.length) })
|
|
187
|
+
: t("projects.folders-of-n", {
|
|
188
|
+
count: formatInteger(matching.length),
|
|
189
|
+
total: formatInteger(view.rows.length),
|
|
190
|
+
})
|
|
169
191
|
: undefined
|
|
170
192
|
}
|
|
171
193
|
actions={
|
|
172
194
|
<>
|
|
173
195
|
{view.temporaryCount > 0 && (
|
|
174
|
-
<label className="check" title="
|
|
196
|
+
<label className="check" title={t("projects.hide-temporary-hint")}>
|
|
175
197
|
<input
|
|
176
198
|
type="checkbox"
|
|
177
199
|
checked={hideTemporary}
|
|
178
200
|
onChange={e => setHideTemporary(e.target.checked)}
|
|
179
201
|
/>
|
|
180
|
-
|
|
202
|
+
{t("projects.hide-temporary", { count: formatInteger(view.temporaryCount) })}
|
|
181
203
|
</label>
|
|
182
204
|
)}
|
|
183
|
-
<SearchInput
|
|
205
|
+
<SearchInput
|
|
206
|
+
value={search}
|
|
207
|
+
onChange={setSearch}
|
|
208
|
+
placeholder={t("projects.filter-placeholder")}
|
|
209
|
+
width={240}
|
|
210
|
+
/>
|
|
184
211
|
</>
|
|
185
212
|
}
|
|
186
213
|
flush
|
|
@@ -197,12 +224,12 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
|
|
|
197
224
|
dense
|
|
198
225
|
empty={
|
|
199
226
|
<EmptyState
|
|
200
|
-
title={view.rows.length === 0 ? "
|
|
227
|
+
title={t(view.rows.length === 0 ? "projects.empty-no-folders" : "projects.empty-no-match")}
|
|
201
228
|
hint={
|
|
202
229
|
view.rows.length === 0
|
|
203
|
-
? "
|
|
230
|
+
? t("projects.hint-longer-range")
|
|
204
231
|
: hideTemporary && view.temporaryCount > 0
|
|
205
|
-
? "
|
|
232
|
+
? t("projects.hint-temporary-hidden")
|
|
206
233
|
: undefined
|
|
207
234
|
}
|
|
208
235
|
/>
|
|
@@ -219,34 +246,34 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
|
|
|
219
246
|
return [
|
|
220
247
|
{
|
|
221
248
|
key: "folder",
|
|
222
|
-
header: "
|
|
249
|
+
header: t("projects.folder"),
|
|
223
250
|
sort: row => row.folder,
|
|
224
251
|
render: row => (
|
|
225
|
-
<span className="row projects-folder" title={row.folder || "
|
|
252
|
+
<span className="row projects-folder" title={row.folder || t("common.root")}>
|
|
226
253
|
<span className="mono truncate">{formatFolder(row.folder)}</span>
|
|
227
|
-
{row.temporary && <Badge>temp</Badge>}
|
|
254
|
+
{row.temporary && <Badge>{t("projects.temp")}</Badge>}
|
|
228
255
|
</span>
|
|
229
256
|
),
|
|
230
257
|
},
|
|
231
258
|
{
|
|
232
259
|
key: "requests",
|
|
233
|
-
header: "
|
|
260
|
+
header: t("common.requests"),
|
|
234
261
|
align: "right",
|
|
235
262
|
sort: row => row.totalRequests,
|
|
236
263
|
render: row => (
|
|
237
|
-
<span title={
|
|
264
|
+
<span title={t("projects.share-of-requests", { share: formatPercent(row.requestShare) })}>
|
|
238
265
|
<MeterCell value={row.totalRequests} max={maxRequests} display={formatInteger(row.totalRequests)} />
|
|
239
266
|
</span>
|
|
240
267
|
),
|
|
241
268
|
},
|
|
242
269
|
{
|
|
243
270
|
key: "cost",
|
|
244
|
-
header: "
|
|
245
|
-
title: "
|
|
271
|
+
header: t("common.cost"),
|
|
272
|
+
title: t("projects.api-equivalent-cost-rates"),
|
|
246
273
|
align: "right",
|
|
247
274
|
sort: row => row.totalCost,
|
|
248
275
|
render: row => (
|
|
249
|
-
<span title={
|
|
276
|
+
<span title={t("projects.share-of-cost", { share: formatPercent(row.costShare) })}>
|
|
250
277
|
<MeterCell
|
|
251
278
|
value={row.totalCost}
|
|
252
279
|
max={maxCost}
|
|
@@ -258,8 +285,8 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
|
|
|
258
285
|
},
|
|
259
286
|
{
|
|
260
287
|
key: "tokens",
|
|
261
|
-
header: "
|
|
262
|
-
title: "
|
|
288
|
+
header: t("common.tokens"),
|
|
289
|
+
title: t("projects.conversation-tokens-hint"),
|
|
263
290
|
align: "right",
|
|
264
291
|
sort: row => row.conversationTokens,
|
|
265
292
|
render: row => (
|
|
@@ -270,16 +297,16 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
|
|
|
270
297
|
},
|
|
271
298
|
{
|
|
272
299
|
key: "cacheRate",
|
|
273
|
-
header: "
|
|
274
|
-
title: "
|
|
300
|
+
header: t("projects.cache-rate"),
|
|
301
|
+
title: t("projects.cache-rate-hint"),
|
|
275
302
|
align: "right",
|
|
276
303
|
sort: row => row.cacheRate,
|
|
277
304
|
render: row => <span className="num">{formatPercent(row.cacheRate)}</span>,
|
|
278
305
|
},
|
|
279
306
|
{
|
|
280
307
|
key: "cacheSavings",
|
|
281
|
-
header: "
|
|
282
|
-
title: "
|
|
308
|
+
header: t("projects.cache-savings"),
|
|
309
|
+
title: t("projects.cache-savings-hint"),
|
|
283
310
|
align: "right",
|
|
284
311
|
sort: row => row.cacheSavings,
|
|
285
312
|
render: row => (
|
|
@@ -290,27 +317,27 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
|
|
|
290
317
|
},
|
|
291
318
|
{
|
|
292
319
|
key: "errorRate",
|
|
293
|
-
header: "
|
|
320
|
+
header: t("projects.errors"),
|
|
294
321
|
align: "right",
|
|
295
322
|
sort: row => row.errorRate,
|
|
296
323
|
render: row => (
|
|
297
|
-
<span title={
|
|
324
|
+
<span title={t("projects.failed-n", { count: formatInteger(row.failedRequests) })}>
|
|
298
325
|
<Badge tone={row.failedRequests > 0 ? errorRateTone(row.errorRate) : "neutral"} mono>
|
|
299
|
-
{
|
|
326
|
+
{formatErrorRate(row.errorRate)}
|
|
300
327
|
</Badge>
|
|
301
328
|
</span>
|
|
302
329
|
),
|
|
303
330
|
},
|
|
304
331
|
{
|
|
305
332
|
key: "duration",
|
|
306
|
-
header: "
|
|
333
|
+
header: t("projects.avg-duration"),
|
|
307
334
|
align: "right",
|
|
308
335
|
sort: row => row.avgDuration ?? -1,
|
|
309
336
|
render: row => <span className="num">{formatDurationMs(row.avgDuration)}</span>,
|
|
310
337
|
},
|
|
311
338
|
{
|
|
312
339
|
key: "last",
|
|
313
|
-
header: "
|
|
340
|
+
header: t("projects.last-active"),
|
|
314
341
|
align: "right",
|
|
315
342
|
sort: row => row.lastTimestamp,
|
|
316
343
|
render: row => <span className="dim">{formatRelativeTime(row.lastTimestamp)}</span>,
|