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