@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 { X } from "lucide-react";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { getToolDashboardStats } from "../api";
|
|
4
|
+
import { t } from "../i18n";
|
|
4
5
|
import { Legend, Sparkline, TimeChart, useHiddenSeries } from "../charts";
|
|
5
6
|
import { buildColorLookup, OTHER_COLOR } from "../data/colors";
|
|
6
7
|
import {
|
|
7
8
|
formatCompact,
|
|
9
|
+
formatErrorRate,
|
|
8
10
|
formatEstimatedCost,
|
|
9
11
|
formatInteger,
|
|
10
12
|
formatPercent,
|
|
@@ -43,20 +45,26 @@ export interface ToolsRouteProps {
|
|
|
43
45
|
type CallMetric = "calls" | "errors";
|
|
44
46
|
|
|
45
47
|
const METRIC_OPTIONS = [
|
|
46
|
-
{ value: "calls" as const, label: "
|
|
47
|
-
{ value: "errors" as const, label: "
|
|
48
|
+
{ value: "calls" as const, label: t("tools.calls") },
|
|
49
|
+
{ value: "errors" as const, label: t("tools.errors") },
|
|
48
50
|
];
|
|
49
51
|
|
|
50
52
|
/** Tools stacked individually in the calls chart; the rest fold into "Other". */
|
|
51
53
|
const TOP_TOOLS = 6;
|
|
52
54
|
|
|
53
|
-
const ATTRIBUTION_NOTE =
|
|
54
|
-
"Tokens and API-equivalent cost of each invoking turn, split evenly across that turn's tool calls";
|
|
55
|
+
const ATTRIBUTION_NOTE = t("tools.attribution-note");
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
/** Bucket granularity label for the calls chart description. */
|
|
58
|
+
function bucketUnit(bucketMs: number): string {
|
|
59
|
+
if (bucketMs < 3_600_000) return t("tools.bucket-5m");
|
|
60
|
+
if (bucketMs < 86_400_000) return t("tools.bucket-hour");
|
|
61
|
+
return t("tools.bucket-day");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const NO_CALLS = <EmptyState title={t("tools.no-tool-calls")} />;
|
|
57
65
|
|
|
58
66
|
export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
59
|
-
const tools = useQuery(["tools", range], () => getToolDashboardStats(range), { enabled: active });
|
|
67
|
+
const tools = useQuery(["tools", range], ({ signal }) => getToolDashboardStats(range, signal), { enabled: active });
|
|
60
68
|
const [metric, setMetric] = useState<CallMetric>("calls");
|
|
61
69
|
const [hidden, toggleHidden] = useHiddenSeries();
|
|
62
70
|
const [pickedTool, setToolFilter] = useState<string | null>(null);
|
|
@@ -71,75 +79,78 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
71
79
|
|
|
72
80
|
return (
|
|
73
81
|
<div className="page">
|
|
74
|
-
<PageHeader
|
|
75
|
-
title="Tools"
|
|
76
|
-
description={`Which tools zeta called in ${meta.windowLabel}, how often they failed, and what they cost.`}
|
|
77
|
-
/>
|
|
82
|
+
<PageHeader title={t("tools.title")} description={t("tools.description", { window: t(meta.windowKey) })} />
|
|
78
83
|
|
|
79
84
|
<QueryView query={tools} skeleton={<ChartSkeleton height={112} />}>
|
|
80
85
|
{() => {
|
|
81
|
-
const
|
|
86
|
+
const totals = view.totals;
|
|
82
87
|
return (
|
|
83
88
|
<div data-stale={tools.stale} className="stack" style={{ gap: 16 }}>
|
|
84
89
|
<StatGrid min={190}>
|
|
85
90
|
<Stat
|
|
86
|
-
label="
|
|
87
|
-
value={formatInteger(
|
|
88
|
-
hint={
|
|
91
|
+
label={t("tools.tool-calls")}
|
|
92
|
+
value={formatInteger(totals.calls)}
|
|
93
|
+
hint={t("tools.failed-n", { count: formatInteger(totals.errors) })}
|
|
89
94
|
spark={view.totalCalls}
|
|
90
95
|
/>
|
|
91
96
|
<Stat
|
|
92
|
-
label="
|
|
93
|
-
value={formatInteger(
|
|
94
|
-
hint={view.rows[0] ?
|
|
97
|
+
label={t("tools.distinct-tools")}
|
|
98
|
+
value={formatInteger(totals.tools)}
|
|
99
|
+
hint={view.rows[0] ? t("tools.most-used", { tool: view.rows[0].tool }) : undefined}
|
|
95
100
|
/>
|
|
96
101
|
<Stat
|
|
97
|
-
label="
|
|
98
|
-
title="
|
|
99
|
-
value={
|
|
100
|
-
hint={
|
|
102
|
+
label={t("common.error-rate")}
|
|
103
|
+
title={t("tools.error-rate-title")}
|
|
104
|
+
value={formatErrorRate(totals.calls > 0 ? totals.errors / totals.calls : 0)}
|
|
105
|
+
hint={t("tools.succeeded-n", { count: formatInteger(totals.calls - totals.errors) })}
|
|
101
106
|
spark={view.totalErrors}
|
|
102
107
|
sparkColor="var(--bad)"
|
|
103
108
|
/>
|
|
104
109
|
<Stat
|
|
105
|
-
label="
|
|
110
|
+
label={t("tools.attributed-tokens")}
|
|
106
111
|
title={ATTRIBUTION_NOTE}
|
|
107
|
-
value={formatCompact(Math.round(
|
|
108
|
-
hint={
|
|
112
|
+
value={formatCompact(Math.round(totals.tokens))}
|
|
113
|
+
hint={t("tools.tokens-output", { tokens: formatCompact(Math.round(totals.output)) })}
|
|
109
114
|
/>
|
|
110
115
|
<Stat
|
|
111
|
-
label="
|
|
112
|
-
title={
|
|
113
|
-
value={formatEstimatedCost(
|
|
116
|
+
label={t("tools.attributed-cost")}
|
|
117
|
+
title={t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE })}
|
|
118
|
+
value={formatEstimatedCost(totals.cost, totals.unpriced)}
|
|
114
119
|
hint={
|
|
115
|
-
|
|
116
|
-
?
|
|
117
|
-
: "
|
|
120
|
+
totals.unpriced > 0
|
|
121
|
+
? t("tools.unpriced-requests-n", { count: formatInteger(Math.round(totals.unpriced)) })
|
|
122
|
+
: t("tools.api-equivalent")
|
|
118
123
|
}
|
|
119
124
|
/>
|
|
120
125
|
</StatGrid>
|
|
121
126
|
<StatGrid min={150}>
|
|
122
127
|
<Stat
|
|
123
128
|
size="sm"
|
|
124
|
-
label="
|
|
125
|
-
title="
|
|
126
|
-
value={
|
|
129
|
+
label={t("tools.result-text")}
|
|
130
|
+
title={t("tools.result-chars-title")}
|
|
131
|
+
value={t("tools.chars", { count: formatCompact(totals.resultChars) })}
|
|
127
132
|
/>
|
|
128
133
|
<Stat
|
|
129
134
|
size="sm"
|
|
130
|
-
label="
|
|
131
|
-
title="
|
|
132
|
-
value={
|
|
135
|
+
label={t("tools.call-arguments")}
|
|
136
|
+
title={t("tools.args-chars-title")}
|
|
137
|
+
value={t("tools.chars", { count: formatCompact(totals.argsChars) })}
|
|
133
138
|
/>
|
|
134
139
|
<Stat
|
|
135
140
|
size="sm"
|
|
136
|
-
label="
|
|
137
|
-
value={
|
|
141
|
+
label={t("tools.avg-result-per-call")}
|
|
142
|
+
value={t("tools.chars", {
|
|
143
|
+
count: formatCompact(
|
|
144
|
+
totals.calls > 0 ? Math.round(totals.resultChars / totals.calls) : 0,
|
|
145
|
+
),
|
|
146
|
+
})}
|
|
138
147
|
/>
|
|
139
148
|
<Stat
|
|
140
149
|
size="sm"
|
|
141
|
-
label="
|
|
142
|
-
value={
|
|
150
|
+
label={t("tools.avg-args-per-call")}
|
|
151
|
+
value={t("tools.chars", {
|
|
152
|
+
count: formatCompact(totals.calls > 0 ? Math.round(totals.argsChars / totals.calls) : 0),
|
|
153
|
+
})}
|
|
143
154
|
/>
|
|
144
155
|
</StatGrid>
|
|
145
156
|
</div>
|
|
@@ -149,8 +160,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
149
160
|
|
|
150
161
|
<Card
|
|
151
162
|
index={1}
|
|
152
|
-
title={metric === "calls" ? "
|
|
153
|
-
description={
|
|
163
|
+
title={metric === "calls" ? t("tools.calls-over-time") : t("tools.errors-over-time")}
|
|
164
|
+
description={t("tools.chart-description", { bucket: bucketUnit(meta.bucketMs), count: TOP_TOOLS })}
|
|
154
165
|
actions={<Segmented size="sm" options={METRIC_OPTIONS} value={metric} onChange={setMetric} />}
|
|
155
166
|
stale={tools.stale}
|
|
156
167
|
>
|
|
@@ -164,7 +175,7 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
164
175
|
hidden={hidden}
|
|
165
176
|
height={260}
|
|
166
177
|
formatTooltip={formatInteger}
|
|
167
|
-
emptyLabel={metric === "errors" ? "
|
|
178
|
+
emptyLabel={metric === "errors" ? t("tools.no-tool-errors") : undefined}
|
|
168
179
|
/>
|
|
169
180
|
<Legend
|
|
170
181
|
items={chartSeries.map(s => ({
|
|
@@ -183,8 +194,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
183
194
|
|
|
184
195
|
<Card
|
|
185
196
|
index={2}
|
|
186
|
-
title="
|
|
187
|
-
description={
|
|
197
|
+
title={t("tools.by-tool")}
|
|
198
|
+
description={t("tools.by-tool-description", { note: ATTRIBUTION_NOTE })}
|
|
188
199
|
flush
|
|
189
200
|
stale={tools.stale}
|
|
190
201
|
>
|
|
@@ -206,8 +217,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
206
217
|
|
|
207
218
|
<Card
|
|
208
219
|
index={3}
|
|
209
|
-
title="
|
|
210
|
-
description="
|
|
220
|
+
title={t("tools.by-tool-and-model")}
|
|
221
|
+
description={t("tools.by-tool-and-model-description")}
|
|
211
222
|
flush
|
|
212
223
|
stale={tools.stale}
|
|
213
224
|
actions={
|
|
@@ -216,9 +227,9 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
216
227
|
className="input tools-filter"
|
|
217
228
|
value={toolFilter ?? ""}
|
|
218
229
|
onChange={e => setToolFilter(e.target.value || null)}
|
|
219
|
-
aria-label="
|
|
230
|
+
aria-label={t("tools.filter-by-tool")}
|
|
220
231
|
>
|
|
221
|
-
<option value="">
|
|
232
|
+
<option value="">{t("tools.all-tools")}</option>
|
|
222
233
|
{view.toolNames.map(name => (
|
|
223
234
|
<option key={name} value={name}>
|
|
224
235
|
{name}
|
|
@@ -232,8 +243,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
232
243
|
data-size="sm"
|
|
233
244
|
data-variant="ghost"
|
|
234
245
|
data-icon="true"
|
|
235
|
-
title="
|
|
236
|
-
aria-label="
|
|
246
|
+
title={t("tools.clear-tool-filter")}
|
|
247
|
+
aria-label={t("tools.clear-tool-filter")}
|
|
237
248
|
onClick={() => setToolFilter(null)}
|
|
238
249
|
>
|
|
239
250
|
<X size={13} />
|
|
@@ -270,7 +281,7 @@ function buildToolsView(data: ToolDashboardStats | null, range: TimeRange) {
|
|
|
270
281
|
const byTool = data?.byTool ?? [];
|
|
271
282
|
const points = data?.series ?? [];
|
|
272
283
|
const rows = buildToolRows(byTool).sort((a, b) => b.calls - a.calls);
|
|
273
|
-
const colors = buildColorLookup(byTool.map(
|
|
284
|
+
const colors = buildColorLookup(byTool.map(item => ({ key: item.tool, weight: item.calls })));
|
|
274
285
|
const buckets = bucketAxis(
|
|
275
286
|
range,
|
|
276
287
|
points.map(p => p.timestamp),
|
|
@@ -287,15 +298,15 @@ function buildToolsView(data: ToolDashboardStats | null, range: TimeRange) {
|
|
|
287
298
|
resultChars: 0,
|
|
288
299
|
argsChars: 0,
|
|
289
300
|
};
|
|
290
|
-
for (const
|
|
291
|
-
totals.calls +=
|
|
292
|
-
totals.errors +=
|
|
293
|
-
totals.tokens +=
|
|
294
|
-
totals.output +=
|
|
295
|
-
totals.cost +=
|
|
296
|
-
totals.unpriced +=
|
|
297
|
-
totals.resultChars +=
|
|
298
|
-
totals.argsChars +=
|
|
301
|
+
for (const item of byTool) {
|
|
302
|
+
totals.calls += item.calls;
|
|
303
|
+
totals.errors += item.errors;
|
|
304
|
+
totals.tokens += item.totalTokensShare;
|
|
305
|
+
totals.output += item.outputTokensShare;
|
|
306
|
+
totals.cost += item.costShare;
|
|
307
|
+
totals.unpriced += item.unpricedRequestsShare;
|
|
308
|
+
totals.resultChars += item.resultChars;
|
|
309
|
+
totals.argsChars += item.argsChars;
|
|
299
310
|
}
|
|
300
311
|
|
|
301
312
|
// Every tool keeps its table swatch color in both metrics; zero slots become
|
|
@@ -337,7 +348,7 @@ function buildToolColumns(
|
|
|
337
348
|
return [
|
|
338
349
|
{
|
|
339
350
|
key: "tool",
|
|
340
|
-
header: "
|
|
351
|
+
header: t("tools.tool"),
|
|
341
352
|
sort: row => row.tool,
|
|
342
353
|
render: row => (
|
|
343
354
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -350,8 +361,8 @@ function buildToolColumns(
|
|
|
350
361
|
},
|
|
351
362
|
{
|
|
352
363
|
key: "trend",
|
|
353
|
-
header: "
|
|
354
|
-
title: "
|
|
364
|
+
header: t("tools.trend"),
|
|
365
|
+
title: t("tools.trend-title"),
|
|
355
366
|
render: row => {
|
|
356
367
|
const values = trends.get(row.tool);
|
|
357
368
|
return values && values.length > 1 ? (
|
|
@@ -363,7 +374,7 @@ function buildToolColumns(
|
|
|
363
374
|
},
|
|
364
375
|
{
|
|
365
376
|
key: "calls",
|
|
366
|
-
header: "
|
|
377
|
+
header: t("tools.calls"),
|
|
367
378
|
align: "right",
|
|
368
379
|
sort: row => row.calls,
|
|
369
380
|
render: row => (
|
|
@@ -371,7 +382,7 @@ function buildToolColumns(
|
|
|
371
382
|
value={row.calls}
|
|
372
383
|
max={maxCalls}
|
|
373
384
|
display={
|
|
374
|
-
<span title={
|
|
385
|
+
<span title={t("tools.share-of-calls", { percent: formatPercent(row.callFraction) })}>
|
|
375
386
|
{formatInteger(row.calls)}
|
|
376
387
|
<span className="dim tools-share">{formatPercent(row.callFraction, 0)}</span>
|
|
377
388
|
</span>
|
|
@@ -381,51 +392,51 @@ function buildToolColumns(
|
|
|
381
392
|
},
|
|
382
393
|
{
|
|
383
394
|
key: "errorRate",
|
|
384
|
-
header: "
|
|
385
|
-
title: "
|
|
395
|
+
header: t("tools.errors"),
|
|
396
|
+
title: t("tools.errors-title"),
|
|
386
397
|
align: "right",
|
|
387
398
|
sort: row => row.errorRate,
|
|
388
399
|
render: row => (
|
|
389
400
|
<span className="row" style={{ gap: 8, justifyContent: "flex-end" }}>
|
|
390
401
|
<span className="num dim">{formatInteger(row.errors)}</span>
|
|
391
402
|
<Badge tone={row.errors > 0 ? errorRateTone(row.errorRate) : "neutral"} mono>
|
|
392
|
-
{
|
|
403
|
+
{formatErrorRate(row.errorRate)}
|
|
393
404
|
</Badge>
|
|
394
405
|
</span>
|
|
395
406
|
),
|
|
396
407
|
},
|
|
397
408
|
{
|
|
398
409
|
key: "args",
|
|
399
|
-
header: "
|
|
400
|
-
title: "
|
|
410
|
+
header: t("tools.args"),
|
|
411
|
+
title: t("tools.args-chars-title"),
|
|
401
412
|
align: "right",
|
|
402
413
|
sort: row => row.argsChars,
|
|
403
414
|
render: row => <span className="num">{formatCompact(row.argsChars)}</span>,
|
|
404
415
|
},
|
|
405
416
|
{
|
|
406
417
|
key: "result",
|
|
407
|
-
header: "
|
|
408
|
-
title: "
|
|
418
|
+
header: t("tools.result"),
|
|
419
|
+
title: t("tools.result-chars-title"),
|
|
409
420
|
align: "right",
|
|
410
421
|
sort: row => row.resultChars,
|
|
411
422
|
render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
|
|
412
423
|
},
|
|
413
424
|
{
|
|
414
425
|
key: "avgResult",
|
|
415
|
-
header: "
|
|
416
|
-
title: "
|
|
426
|
+
header: t("tools.result-per-call"),
|
|
427
|
+
title: t("tools.result-per-call-title"),
|
|
417
428
|
align: "right",
|
|
418
429
|
sort: row => row.avgResultChars,
|
|
419
430
|
render: row => <span className="num muted">{formatCompact(Math.round(row.avgResultChars))}</span>,
|
|
420
431
|
},
|
|
421
432
|
{
|
|
422
433
|
key: "tokens",
|
|
423
|
-
header: "
|
|
434
|
+
header: t("tools.attr-tokens"),
|
|
424
435
|
title: ATTRIBUTION_NOTE,
|
|
425
436
|
align: "right",
|
|
426
437
|
sort: row => row.totalTokensShare,
|
|
427
438
|
render: row => (
|
|
428
|
-
<span className="num" title={
|
|
439
|
+
<span className="num" title={t("tools.share-of-tokens", { percent: formatPercent(row.tokenFraction) })}>
|
|
429
440
|
{formatCompact(Math.round(row.totalTokensShare))}
|
|
430
441
|
<span className="dim tools-share">{formatPercent(row.tokenFraction, 0)}</span>
|
|
431
442
|
</span>
|
|
@@ -433,12 +444,12 @@ function buildToolColumns(
|
|
|
433
444
|
},
|
|
434
445
|
{
|
|
435
446
|
key: "cost",
|
|
436
|
-
header: "
|
|
437
|
-
title:
|
|
447
|
+
header: t("tools.attr-cost"),
|
|
448
|
+
title: t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE }),
|
|
438
449
|
align: "right",
|
|
439
450
|
sort: row => row.costShare,
|
|
440
451
|
render: row => (
|
|
441
|
-
<span className="num" title={
|
|
452
|
+
<span className="num" title={t("tools.share-of-cost", { percent: formatPercent(row.costFraction) })}>
|
|
442
453
|
{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}
|
|
443
454
|
<span className="dim tools-share">{formatPercent(row.costFraction, 0)}</span>
|
|
444
455
|
</span>
|
|
@@ -446,7 +457,7 @@ function buildToolColumns(
|
|
|
446
457
|
},
|
|
447
458
|
{
|
|
448
459
|
key: "lastUsed",
|
|
449
|
-
header: "
|
|
460
|
+
header: t("tools.last-used"),
|
|
450
461
|
align: "right",
|
|
451
462
|
sort: row => row.lastUsed,
|
|
452
463
|
render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
|
|
@@ -484,7 +495,7 @@ function ToolModelTable({
|
|
|
484
495
|
return [
|
|
485
496
|
{
|
|
486
497
|
key: "tool",
|
|
487
|
-
header: "
|
|
498
|
+
header: t("tools.tool"),
|
|
488
499
|
sort: row => row.tool,
|
|
489
500
|
render: row => (
|
|
490
501
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -497,44 +508,47 @@ function ToolModelTable({
|
|
|
497
508
|
},
|
|
498
509
|
{
|
|
499
510
|
key: "model",
|
|
500
|
-
header: "
|
|
511
|
+
header: t("common.model"),
|
|
501
512
|
sort: row => row.model,
|
|
502
513
|
render: row => (
|
|
503
|
-
<LabelCell
|
|
514
|
+
<LabelCell
|
|
515
|
+
primary={<span className="mono">{row.model || t("common.unknown")}</span>}
|
|
516
|
+
secondary={row.provider}
|
|
517
|
+
/>
|
|
504
518
|
),
|
|
505
519
|
},
|
|
506
520
|
{
|
|
507
521
|
key: "calls",
|
|
508
|
-
header: "
|
|
522
|
+
header: t("tools.calls"),
|
|
509
523
|
align: "right",
|
|
510
524
|
sort: row => row.calls,
|
|
511
525
|
render: row => <MeterCell value={row.calls} max={maxCalls} display={formatInteger(row.calls)} />,
|
|
512
526
|
},
|
|
513
527
|
{
|
|
514
528
|
key: "errorRate",
|
|
515
|
-
header: "
|
|
529
|
+
header: t("tools.errors"),
|
|
516
530
|
align: "right",
|
|
517
531
|
sort: row => row.errorRate,
|
|
518
532
|
render: row => (
|
|
519
533
|
<span className="row" style={{ gap: 8, justifyContent: "flex-end" }}>
|
|
520
534
|
<span className="num dim">{formatInteger(row.errors)}</span>
|
|
521
535
|
<Badge tone={row.errors > 0 ? errorRateTone(row.errorRate) : "neutral"} mono>
|
|
522
|
-
{
|
|
536
|
+
{formatErrorRate(row.errorRate)}
|
|
523
537
|
</Badge>
|
|
524
538
|
</span>
|
|
525
539
|
),
|
|
526
540
|
},
|
|
527
541
|
{
|
|
528
542
|
key: "result",
|
|
529
|
-
header: "
|
|
530
|
-
title: "
|
|
543
|
+
header: t("tools.result"),
|
|
544
|
+
title: t("tools.result-chars-title"),
|
|
531
545
|
align: "right",
|
|
532
546
|
sort: row => row.resultChars,
|
|
533
547
|
render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
|
|
534
548
|
},
|
|
535
549
|
{
|
|
536
550
|
key: "tokens",
|
|
537
|
-
header: "
|
|
551
|
+
header: t("tools.attr-tokens"),
|
|
538
552
|
title: ATTRIBUTION_NOTE,
|
|
539
553
|
align: "right",
|
|
540
554
|
sort: row => row.totalTokensShare,
|
|
@@ -542,15 +556,15 @@ function ToolModelTable({
|
|
|
542
556
|
},
|
|
543
557
|
{
|
|
544
558
|
key: "cost",
|
|
545
|
-
header: "
|
|
546
|
-
title:
|
|
559
|
+
header: t("tools.attr-cost"),
|
|
560
|
+
title: t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE }),
|
|
547
561
|
align: "right",
|
|
548
562
|
sort: row => row.costShare,
|
|
549
563
|
render: row => <span className="num">{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}</span>,
|
|
550
564
|
},
|
|
551
565
|
{
|
|
552
566
|
key: "lastUsed",
|
|
553
|
-
header: "
|
|
567
|
+
header: t("tools.last-used"),
|
|
554
568
|
align: "right",
|
|
555
569
|
sort: row => row.lastUsed,
|
|
556
570
|
render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
|
|
@@ -566,7 +580,7 @@ function ToolModelTable({
|
|
|
566
580
|
initialSort={{ key: "calls", dir: "desc" }}
|
|
567
581
|
limit={25}
|
|
568
582
|
dense
|
|
569
|
-
empty={<EmptyState title={tool ?
|
|
583
|
+
empty={<EmptyState title={tool ? t("tools.no-calls-to-tool", { tool }) : t("tools.no-tool-calls")} />}
|
|
570
584
|
/>
|
|
571
585
|
);
|
|
572
586
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatRelativeTime,
|
|
14
14
|
} from "../data/formatters";
|
|
15
15
|
import { useQuery } from "../data/query";
|
|
16
|
+
import { t } from "../i18n";
|
|
16
17
|
import { TraceView } from "../traces/TraceView";
|
|
17
18
|
import type { SessionSummary } from "../types";
|
|
18
19
|
import {
|
|
@@ -49,10 +50,11 @@ function ModelList({ models }: { models: string[] }) {
|
|
|
49
50
|
|
|
50
51
|
export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps) {
|
|
51
52
|
const [filter, setFilter] = useState("");
|
|
52
|
-
const sessions = useQuery(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const sessions = useQuery(
|
|
54
|
+
["sessions", SESSION_LIMIT],
|
|
55
|
+
({ signal }) => getSessions(SESSION_LIMIT, undefined, signal),
|
|
56
|
+
{ pollMs: 30000, enabled: active && session === null },
|
|
57
|
+
);
|
|
56
58
|
|
|
57
59
|
const filtered = useMemo(() => {
|
|
58
60
|
const rows = sessions.data ?? [];
|
|
@@ -72,7 +74,7 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
|
|
|
72
74
|
() => [
|
|
73
75
|
{
|
|
74
76
|
key: "title",
|
|
75
|
-
header: "
|
|
77
|
+
header: t("trace.session"),
|
|
76
78
|
width: "32%",
|
|
77
79
|
render: row => (
|
|
78
80
|
<LabelCell
|
|
@@ -82,10 +84,10 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
|
|
|
82
84
|
),
|
|
83
85
|
sort: row => (row.title ?? row.file).toLowerCase(),
|
|
84
86
|
},
|
|
85
|
-
{ key: "models", header: "
|
|
87
|
+
{ key: "models", header: t("common.models"), render: row => <ModelList models={row.models} /> },
|
|
86
88
|
{
|
|
87
89
|
key: "started",
|
|
88
|
-
header: "
|
|
90
|
+
header: t("common.started"),
|
|
89
91
|
align: "right",
|
|
90
92
|
render: row => (
|
|
91
93
|
<span className="muted" title={new Date(row.startedAt).toLocaleString()}>
|
|
@@ -96,28 +98,28 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
|
|
|
96
98
|
},
|
|
97
99
|
{
|
|
98
100
|
key: "duration",
|
|
99
|
-
header: "
|
|
101
|
+
header: t("common.duration"),
|
|
100
102
|
align: "right",
|
|
101
103
|
render: row => <span className="num">{formatElapsed(row.endedAt - row.startedAt)}</span>,
|
|
102
104
|
sort: row => row.endedAt - row.startedAt,
|
|
103
105
|
},
|
|
104
106
|
{
|
|
105
107
|
key: "requests",
|
|
106
|
-
header: "
|
|
108
|
+
header: t("common.requests"),
|
|
107
109
|
align: "right",
|
|
108
110
|
render: row => <span className="num">{formatInteger(row.requests)}</span>,
|
|
109
111
|
sort: row => row.requests,
|
|
110
112
|
},
|
|
111
113
|
{
|
|
112
114
|
key: "toolCalls",
|
|
113
|
-
header: "
|
|
115
|
+
header: t("trace.tools"),
|
|
114
116
|
align: "right",
|
|
115
117
|
render: row => <span className="num">{formatInteger(row.toolCalls)}</span>,
|
|
116
118
|
sort: row => row.toolCalls,
|
|
117
119
|
},
|
|
118
120
|
{
|
|
119
121
|
key: "subagents",
|
|
120
|
-
header: "
|
|
122
|
+
header: t("trace.agents"),
|
|
121
123
|
align: "right",
|
|
122
124
|
render: row =>
|
|
123
125
|
row.subagents > 0 ? (
|
|
@@ -129,14 +131,14 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
|
|
|
129
131
|
},
|
|
130
132
|
{
|
|
131
133
|
key: "tokens",
|
|
132
|
-
header: "
|
|
134
|
+
header: t("common.tokens"),
|
|
133
135
|
align: "right",
|
|
134
136
|
render: row => <span className="num">{formatCompact(row.totalTokens)}</span>,
|
|
135
137
|
sort: row => row.totalTokens,
|
|
136
138
|
},
|
|
137
139
|
{
|
|
138
140
|
key: "cost",
|
|
139
|
-
header: "
|
|
141
|
+
header: t("common.cost"),
|
|
140
142
|
align: "right",
|
|
141
143
|
width: 150,
|
|
142
144
|
render: row => (
|
|
@@ -159,24 +161,24 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
|
|
|
159
161
|
const total = sessions.data?.length ?? 0;
|
|
160
162
|
return (
|
|
161
163
|
<div className="stack traces-page">
|
|
162
|
-
<PageHeader
|
|
163
|
-
title="Traces"
|
|
164
|
-
description="Recent sessions with subagent activity folded in. Open one to inspect its timeline."
|
|
165
|
-
/>
|
|
164
|
+
<PageHeader title={t("trace.title")} description={t("trace.description")} />
|
|
166
165
|
<Card
|
|
167
|
-
title="
|
|
166
|
+
title={t("trace.sessions")}
|
|
168
167
|
description={
|
|
169
168
|
sessions.data
|
|
170
169
|
? filter.trim()
|
|
171
|
-
?
|
|
172
|
-
|
|
170
|
+
? t("trace.most-recent-filtered", {
|
|
171
|
+
shown: formatInteger(filtered.length),
|
|
172
|
+
total: formatInteger(total),
|
|
173
|
+
})
|
|
174
|
+
: t("trace.most-recent", { count: formatInteger(total) })
|
|
173
175
|
: undefined
|
|
174
176
|
}
|
|
175
177
|
actions={
|
|
176
178
|
<SearchInput
|
|
177
179
|
value={filter}
|
|
178
180
|
onChange={setFilter}
|
|
179
|
-
placeholder="
|
|
181
|
+
placeholder={t("trace.filter-placeholder")}
|
|
180
182
|
width={260}
|
|
181
183
|
/>
|
|
182
184
|
}
|
|
@@ -188,7 +190,7 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
|
|
|
188
190
|
query={sessions}
|
|
189
191
|
skeleton={<TableSkeleton rows={10} />}
|
|
190
192
|
isEmpty={rows => rows.length === 0}
|
|
191
|
-
empty={<EmptyState title="
|
|
193
|
+
empty={<EmptyState title={t("trace.empty-title")} hint={t("trace.empty-hint")} />}
|
|
192
194
|
>
|
|
193
195
|
{() => (
|
|
194
196
|
<Table
|
|
@@ -198,7 +200,7 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
|
|
|
198
200
|
onRowClick={row => onOpenSession(row.file)}
|
|
199
201
|
initialSort={{ key: "started", dir: "desc" }}
|
|
200
202
|
limit={50}
|
|
201
|
-
empty={<EmptyState title="
|
|
203
|
+
empty={<EmptyState title={t("trace.no-matching-title")} hint={t("trace.no-matching-hint")} />}
|
|
202
204
|
/>
|
|
203
205
|
)}
|
|
204
206
|
</QueryView>
|
package/src/client/styles.css
CHANGED
|
@@ -460,8 +460,10 @@ kbd {
|
|
|
460
460
|
gap: 8px;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
@media (min-width: 901px) {
|
|
464
|
+
.btn.topbar-menu {
|
|
465
|
+
display: none;
|
|
466
|
+
}
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
/* Sync progress hairline along the bottom edge of the topbar. */
|
|
@@ -949,6 +951,7 @@ kbd {
|
|
|
949
951
|
|
|
950
952
|
.btn[data-variant="accent"] {
|
|
951
953
|
background: var(--accent-gradient);
|
|
954
|
+
background-origin: border-box;
|
|
952
955
|
color: #fff;
|
|
953
956
|
border-color: transparent;
|
|
954
957
|
}
|