@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 { 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 {
|
|
@@ -44,20 +45,26 @@ export interface ToolsRouteProps {
|
|
|
44
45
|
type CallMetric = "calls" | "errors";
|
|
45
46
|
|
|
46
47
|
const METRIC_OPTIONS = [
|
|
47
|
-
{ value: "calls" as const, label: "
|
|
48
|
-
{ value: "errors" as const, label: "
|
|
48
|
+
{ value: "calls" as const, label: t("tools.calls") },
|
|
49
|
+
{ value: "errors" as const, label: t("tools.errors") },
|
|
49
50
|
];
|
|
50
51
|
|
|
51
52
|
/** Tools stacked individually in the calls chart; the rest fold into "Other". */
|
|
52
53
|
const TOP_TOOLS = 6;
|
|
53
54
|
|
|
54
|
-
const ATTRIBUTION_NOTE =
|
|
55
|
-
"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");
|
|
56
56
|
|
|
57
|
-
|
|
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")} />;
|
|
58
65
|
|
|
59
66
|
export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
60
|
-
const tools = useQuery(["tools", range], () => getToolDashboardStats(range), { enabled: active });
|
|
67
|
+
const tools = useQuery(["tools", range], ({ signal }) => getToolDashboardStats(range, signal), { enabled: active });
|
|
61
68
|
const [metric, setMetric] = useState<CallMetric>("calls");
|
|
62
69
|
const [hidden, toggleHidden] = useHiddenSeries();
|
|
63
70
|
const [pickedTool, setToolFilter] = useState<string | null>(null);
|
|
@@ -72,75 +79,78 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
72
79
|
|
|
73
80
|
return (
|
|
74
81
|
<div className="page">
|
|
75
|
-
<PageHeader
|
|
76
|
-
title="Tools"
|
|
77
|
-
description={`Which tools zeta called in ${meta.windowLabel}, how often they failed, and what they cost.`}
|
|
78
|
-
/>
|
|
82
|
+
<PageHeader title={t("tools.title")} description={t("tools.description", { window: t(meta.windowKey) })} />
|
|
79
83
|
|
|
80
84
|
<QueryView query={tools} skeleton={<ChartSkeleton height={112} />}>
|
|
81
85
|
{() => {
|
|
82
|
-
const
|
|
86
|
+
const totals = view.totals;
|
|
83
87
|
return (
|
|
84
88
|
<div data-stale={tools.stale} className="stack" style={{ gap: 16 }}>
|
|
85
89
|
<StatGrid min={190}>
|
|
86
90
|
<Stat
|
|
87
|
-
label="
|
|
88
|
-
value={formatInteger(
|
|
89
|
-
hint={
|
|
91
|
+
label={t("tools.tool-calls")}
|
|
92
|
+
value={formatInteger(totals.calls)}
|
|
93
|
+
hint={t("tools.failed-n", { count: formatInteger(totals.errors) })}
|
|
90
94
|
spark={view.totalCalls}
|
|
91
95
|
/>
|
|
92
96
|
<Stat
|
|
93
|
-
label="
|
|
94
|
-
value={formatInteger(
|
|
95
|
-
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}
|
|
96
100
|
/>
|
|
97
101
|
<Stat
|
|
98
|
-
label="
|
|
99
|
-
title="
|
|
100
|
-
value={formatErrorRate(
|
|
101
|
-
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) })}
|
|
102
106
|
spark={view.totalErrors}
|
|
103
107
|
sparkColor="var(--bad)"
|
|
104
108
|
/>
|
|
105
109
|
<Stat
|
|
106
|
-
label="
|
|
110
|
+
label={t("tools.attributed-tokens")}
|
|
107
111
|
title={ATTRIBUTION_NOTE}
|
|
108
|
-
value={formatCompact(Math.round(
|
|
109
|
-
hint={
|
|
112
|
+
value={formatCompact(Math.round(totals.tokens))}
|
|
113
|
+
hint={t("tools.tokens-output", { tokens: formatCompact(Math.round(totals.output)) })}
|
|
110
114
|
/>
|
|
111
115
|
<Stat
|
|
112
|
-
label="
|
|
113
|
-
title={
|
|
114
|
-
value={formatEstimatedCost(
|
|
116
|
+
label={t("tools.attributed-cost")}
|
|
117
|
+
title={t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE })}
|
|
118
|
+
value={formatEstimatedCost(totals.cost, totals.unpriced)}
|
|
115
119
|
hint={
|
|
116
|
-
|
|
117
|
-
?
|
|
118
|
-
: "
|
|
120
|
+
totals.unpriced > 0
|
|
121
|
+
? t("tools.unpriced-requests-n", { count: formatInteger(Math.round(totals.unpriced)) })
|
|
122
|
+
: t("tools.api-equivalent")
|
|
119
123
|
}
|
|
120
124
|
/>
|
|
121
125
|
</StatGrid>
|
|
122
126
|
<StatGrid min={150}>
|
|
123
127
|
<Stat
|
|
124
128
|
size="sm"
|
|
125
|
-
label="
|
|
126
|
-
title="
|
|
127
|
-
value={
|
|
129
|
+
label={t("tools.result-text")}
|
|
130
|
+
title={t("tools.result-chars-title")}
|
|
131
|
+
value={t("tools.chars", { count: formatCompact(totals.resultChars) })}
|
|
128
132
|
/>
|
|
129
133
|
<Stat
|
|
130
134
|
size="sm"
|
|
131
|
-
label="
|
|
132
|
-
title="
|
|
133
|
-
value={
|
|
135
|
+
label={t("tools.call-arguments")}
|
|
136
|
+
title={t("tools.args-chars-title")}
|
|
137
|
+
value={t("tools.chars", { count: formatCompact(totals.argsChars) })}
|
|
134
138
|
/>
|
|
135
139
|
<Stat
|
|
136
140
|
size="sm"
|
|
137
|
-
label="
|
|
138
|
-
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
|
+
})}
|
|
139
147
|
/>
|
|
140
148
|
<Stat
|
|
141
149
|
size="sm"
|
|
142
|
-
label="
|
|
143
|
-
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
|
+
})}
|
|
144
154
|
/>
|
|
145
155
|
</StatGrid>
|
|
146
156
|
</div>
|
|
@@ -150,8 +160,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
150
160
|
|
|
151
161
|
<Card
|
|
152
162
|
index={1}
|
|
153
|
-
title={metric === "calls" ? "
|
|
154
|
-
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 })}
|
|
155
165
|
actions={<Segmented size="sm" options={METRIC_OPTIONS} value={metric} onChange={setMetric} />}
|
|
156
166
|
stale={tools.stale}
|
|
157
167
|
>
|
|
@@ -165,7 +175,7 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
165
175
|
hidden={hidden}
|
|
166
176
|
height={260}
|
|
167
177
|
formatTooltip={formatInteger}
|
|
168
|
-
emptyLabel={metric === "errors" ? "
|
|
178
|
+
emptyLabel={metric === "errors" ? t("tools.no-tool-errors") : undefined}
|
|
169
179
|
/>
|
|
170
180
|
<Legend
|
|
171
181
|
items={chartSeries.map(s => ({
|
|
@@ -184,8 +194,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
184
194
|
|
|
185
195
|
<Card
|
|
186
196
|
index={2}
|
|
187
|
-
title="
|
|
188
|
-
description={
|
|
197
|
+
title={t("tools.by-tool")}
|
|
198
|
+
description={t("tools.by-tool-description", { note: ATTRIBUTION_NOTE })}
|
|
189
199
|
flush
|
|
190
200
|
stale={tools.stale}
|
|
191
201
|
>
|
|
@@ -207,8 +217,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
207
217
|
|
|
208
218
|
<Card
|
|
209
219
|
index={3}
|
|
210
|
-
title="
|
|
211
|
-
description="
|
|
220
|
+
title={t("tools.by-tool-and-model")}
|
|
221
|
+
description={t("tools.by-tool-and-model-description")}
|
|
212
222
|
flush
|
|
213
223
|
stale={tools.stale}
|
|
214
224
|
actions={
|
|
@@ -217,9 +227,9 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
217
227
|
className="input tools-filter"
|
|
218
228
|
value={toolFilter ?? ""}
|
|
219
229
|
onChange={e => setToolFilter(e.target.value || null)}
|
|
220
|
-
aria-label="
|
|
230
|
+
aria-label={t("tools.filter-by-tool")}
|
|
221
231
|
>
|
|
222
|
-
<option value="">
|
|
232
|
+
<option value="">{t("tools.all-tools")}</option>
|
|
223
233
|
{view.toolNames.map(name => (
|
|
224
234
|
<option key={name} value={name}>
|
|
225
235
|
{name}
|
|
@@ -233,8 +243,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
233
243
|
data-size="sm"
|
|
234
244
|
data-variant="ghost"
|
|
235
245
|
data-icon="true"
|
|
236
|
-
title="
|
|
237
|
-
aria-label="
|
|
246
|
+
title={t("tools.clear-tool-filter")}
|
|
247
|
+
aria-label={t("tools.clear-tool-filter")}
|
|
238
248
|
onClick={() => setToolFilter(null)}
|
|
239
249
|
>
|
|
240
250
|
<X size={13} />
|
|
@@ -271,7 +281,7 @@ function buildToolsView(data: ToolDashboardStats | null, range: TimeRange) {
|
|
|
271
281
|
const byTool = data?.byTool ?? [];
|
|
272
282
|
const points = data?.series ?? [];
|
|
273
283
|
const rows = buildToolRows(byTool).sort((a, b) => b.calls - a.calls);
|
|
274
|
-
const colors = buildColorLookup(byTool.map(
|
|
284
|
+
const colors = buildColorLookup(byTool.map(item => ({ key: item.tool, weight: item.calls })));
|
|
275
285
|
const buckets = bucketAxis(
|
|
276
286
|
range,
|
|
277
287
|
points.map(p => p.timestamp),
|
|
@@ -288,15 +298,15 @@ function buildToolsView(data: ToolDashboardStats | null, range: TimeRange) {
|
|
|
288
298
|
resultChars: 0,
|
|
289
299
|
argsChars: 0,
|
|
290
300
|
};
|
|
291
|
-
for (const
|
|
292
|
-
totals.calls +=
|
|
293
|
-
totals.errors +=
|
|
294
|
-
totals.tokens +=
|
|
295
|
-
totals.output +=
|
|
296
|
-
totals.cost +=
|
|
297
|
-
totals.unpriced +=
|
|
298
|
-
totals.resultChars +=
|
|
299
|
-
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;
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
// Every tool keeps its table swatch color in both metrics; zero slots become
|
|
@@ -338,7 +348,7 @@ function buildToolColumns(
|
|
|
338
348
|
return [
|
|
339
349
|
{
|
|
340
350
|
key: "tool",
|
|
341
|
-
header: "
|
|
351
|
+
header: t("tools.tool"),
|
|
342
352
|
sort: row => row.tool,
|
|
343
353
|
render: row => (
|
|
344
354
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -351,8 +361,8 @@ function buildToolColumns(
|
|
|
351
361
|
},
|
|
352
362
|
{
|
|
353
363
|
key: "trend",
|
|
354
|
-
header: "
|
|
355
|
-
title: "
|
|
364
|
+
header: t("tools.trend"),
|
|
365
|
+
title: t("tools.trend-title"),
|
|
356
366
|
render: row => {
|
|
357
367
|
const values = trends.get(row.tool);
|
|
358
368
|
return values && values.length > 1 ? (
|
|
@@ -364,7 +374,7 @@ function buildToolColumns(
|
|
|
364
374
|
},
|
|
365
375
|
{
|
|
366
376
|
key: "calls",
|
|
367
|
-
header: "
|
|
377
|
+
header: t("tools.calls"),
|
|
368
378
|
align: "right",
|
|
369
379
|
sort: row => row.calls,
|
|
370
380
|
render: row => (
|
|
@@ -372,7 +382,7 @@ function buildToolColumns(
|
|
|
372
382
|
value={row.calls}
|
|
373
383
|
max={maxCalls}
|
|
374
384
|
display={
|
|
375
|
-
<span title={
|
|
385
|
+
<span title={t("tools.share-of-calls", { percent: formatPercent(row.callFraction) })}>
|
|
376
386
|
{formatInteger(row.calls)}
|
|
377
387
|
<span className="dim tools-share">{formatPercent(row.callFraction, 0)}</span>
|
|
378
388
|
</span>
|
|
@@ -382,8 +392,8 @@ function buildToolColumns(
|
|
|
382
392
|
},
|
|
383
393
|
{
|
|
384
394
|
key: "errorRate",
|
|
385
|
-
header: "
|
|
386
|
-
title: "
|
|
395
|
+
header: t("tools.errors"),
|
|
396
|
+
title: t("tools.errors-title"),
|
|
387
397
|
align: "right",
|
|
388
398
|
sort: row => row.errorRate,
|
|
389
399
|
render: row => (
|
|
@@ -397,36 +407,36 @@ function buildToolColumns(
|
|
|
397
407
|
},
|
|
398
408
|
{
|
|
399
409
|
key: "args",
|
|
400
|
-
header: "
|
|
401
|
-
title: "
|
|
410
|
+
header: t("tools.args"),
|
|
411
|
+
title: t("tools.args-chars-title"),
|
|
402
412
|
align: "right",
|
|
403
413
|
sort: row => row.argsChars,
|
|
404
414
|
render: row => <span className="num">{formatCompact(row.argsChars)}</span>,
|
|
405
415
|
},
|
|
406
416
|
{
|
|
407
417
|
key: "result",
|
|
408
|
-
header: "
|
|
409
|
-
title: "
|
|
418
|
+
header: t("tools.result"),
|
|
419
|
+
title: t("tools.result-chars-title"),
|
|
410
420
|
align: "right",
|
|
411
421
|
sort: row => row.resultChars,
|
|
412
422
|
render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
|
|
413
423
|
},
|
|
414
424
|
{
|
|
415
425
|
key: "avgResult",
|
|
416
|
-
header: "
|
|
417
|
-
title: "
|
|
426
|
+
header: t("tools.result-per-call"),
|
|
427
|
+
title: t("tools.result-per-call-title"),
|
|
418
428
|
align: "right",
|
|
419
429
|
sort: row => row.avgResultChars,
|
|
420
430
|
render: row => <span className="num muted">{formatCompact(Math.round(row.avgResultChars))}</span>,
|
|
421
431
|
},
|
|
422
432
|
{
|
|
423
433
|
key: "tokens",
|
|
424
|
-
header: "
|
|
434
|
+
header: t("tools.attr-tokens"),
|
|
425
435
|
title: ATTRIBUTION_NOTE,
|
|
426
436
|
align: "right",
|
|
427
437
|
sort: row => row.totalTokensShare,
|
|
428
438
|
render: row => (
|
|
429
|
-
<span className="num" title={
|
|
439
|
+
<span className="num" title={t("tools.share-of-tokens", { percent: formatPercent(row.tokenFraction) })}>
|
|
430
440
|
{formatCompact(Math.round(row.totalTokensShare))}
|
|
431
441
|
<span className="dim tools-share">{formatPercent(row.tokenFraction, 0)}</span>
|
|
432
442
|
</span>
|
|
@@ -434,12 +444,12 @@ function buildToolColumns(
|
|
|
434
444
|
},
|
|
435
445
|
{
|
|
436
446
|
key: "cost",
|
|
437
|
-
header: "
|
|
438
|
-
title:
|
|
447
|
+
header: t("tools.attr-cost"),
|
|
448
|
+
title: t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE }),
|
|
439
449
|
align: "right",
|
|
440
450
|
sort: row => row.costShare,
|
|
441
451
|
render: row => (
|
|
442
|
-
<span className="num" title={
|
|
452
|
+
<span className="num" title={t("tools.share-of-cost", { percent: formatPercent(row.costFraction) })}>
|
|
443
453
|
{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}
|
|
444
454
|
<span className="dim tools-share">{formatPercent(row.costFraction, 0)}</span>
|
|
445
455
|
</span>
|
|
@@ -447,7 +457,7 @@ function buildToolColumns(
|
|
|
447
457
|
},
|
|
448
458
|
{
|
|
449
459
|
key: "lastUsed",
|
|
450
|
-
header: "
|
|
460
|
+
header: t("tools.last-used"),
|
|
451
461
|
align: "right",
|
|
452
462
|
sort: row => row.lastUsed,
|
|
453
463
|
render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
|
|
@@ -485,7 +495,7 @@ function ToolModelTable({
|
|
|
485
495
|
return [
|
|
486
496
|
{
|
|
487
497
|
key: "tool",
|
|
488
|
-
header: "
|
|
498
|
+
header: t("tools.tool"),
|
|
489
499
|
sort: row => row.tool,
|
|
490
500
|
render: row => (
|
|
491
501
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -498,22 +508,25 @@ function ToolModelTable({
|
|
|
498
508
|
},
|
|
499
509
|
{
|
|
500
510
|
key: "model",
|
|
501
|
-
header: "
|
|
511
|
+
header: t("common.model"),
|
|
502
512
|
sort: row => row.model,
|
|
503
513
|
render: row => (
|
|
504
|
-
<LabelCell
|
|
514
|
+
<LabelCell
|
|
515
|
+
primary={<span className="mono">{row.model || t("common.unknown")}</span>}
|
|
516
|
+
secondary={row.provider}
|
|
517
|
+
/>
|
|
505
518
|
),
|
|
506
519
|
},
|
|
507
520
|
{
|
|
508
521
|
key: "calls",
|
|
509
|
-
header: "
|
|
522
|
+
header: t("tools.calls"),
|
|
510
523
|
align: "right",
|
|
511
524
|
sort: row => row.calls,
|
|
512
525
|
render: row => <MeterCell value={row.calls} max={maxCalls} display={formatInteger(row.calls)} />,
|
|
513
526
|
},
|
|
514
527
|
{
|
|
515
528
|
key: "errorRate",
|
|
516
|
-
header: "
|
|
529
|
+
header: t("tools.errors"),
|
|
517
530
|
align: "right",
|
|
518
531
|
sort: row => row.errorRate,
|
|
519
532
|
render: row => (
|
|
@@ -527,15 +540,15 @@ function ToolModelTable({
|
|
|
527
540
|
},
|
|
528
541
|
{
|
|
529
542
|
key: "result",
|
|
530
|
-
header: "
|
|
531
|
-
title: "
|
|
543
|
+
header: t("tools.result"),
|
|
544
|
+
title: t("tools.result-chars-title"),
|
|
532
545
|
align: "right",
|
|
533
546
|
sort: row => row.resultChars,
|
|
534
547
|
render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
|
|
535
548
|
},
|
|
536
549
|
{
|
|
537
550
|
key: "tokens",
|
|
538
|
-
header: "
|
|
551
|
+
header: t("tools.attr-tokens"),
|
|
539
552
|
title: ATTRIBUTION_NOTE,
|
|
540
553
|
align: "right",
|
|
541
554
|
sort: row => row.totalTokensShare,
|
|
@@ -543,15 +556,15 @@ function ToolModelTable({
|
|
|
543
556
|
},
|
|
544
557
|
{
|
|
545
558
|
key: "cost",
|
|
546
|
-
header: "
|
|
547
|
-
title:
|
|
559
|
+
header: t("tools.attr-cost"),
|
|
560
|
+
title: t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE }),
|
|
548
561
|
align: "right",
|
|
549
562
|
sort: row => row.costShare,
|
|
550
563
|
render: row => <span className="num">{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}</span>,
|
|
551
564
|
},
|
|
552
565
|
{
|
|
553
566
|
key: "lastUsed",
|
|
554
|
-
header: "
|
|
567
|
+
header: t("tools.last-used"),
|
|
555
568
|
align: "right",
|
|
556
569
|
sort: row => row.lastUsed,
|
|
557
570
|
render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
|
|
@@ -567,7 +580,7 @@ function ToolModelTable({
|
|
|
567
580
|
initialSort={{ key: "calls", dir: "desc" }}
|
|
568
581
|
limit={25}
|
|
569
582
|
dense
|
|
570
|
-
empty={<EmptyState title={tool ?
|
|
583
|
+
empty={<EmptyState title={tool ? t("tools.no-calls-to-tool", { tool }) : t("tools.no-tool-calls")} />}
|
|
571
584
|
/>
|
|
572
585
|
);
|
|
573
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