@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
|
@@ -8,7 +8,8 @@ import { ArrowLeft, ChevronLeft, ChevronRight, RefreshCw } from "lucide-react";
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { getSessionTrace } from "../api";
|
|
10
10
|
import { useQuery } from "../data/query";
|
|
11
|
-
import
|
|
11
|
+
import { t } from "../i18n";
|
|
12
|
+
import type { SessionTrace, TraceSpan, TraceSpanKind, TraceTrack } from "../types";
|
|
12
13
|
import { Card, ChartSkeleton, PageHeader, QueryView, SearchInput, Segmented } from "../ui";
|
|
13
14
|
import { AggregatesPanel } from "./AggregatesPanel";
|
|
14
15
|
import { Minimap } from "./Minimap";
|
|
@@ -26,21 +27,25 @@ export interface TraceViewProps {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const MODE_OPTIONS: Array<{ value: AxisMode; label: string; title?: string }> = [
|
|
29
|
-
{ value: "time", label: "
|
|
30
|
-
{ value: "turns", label: "
|
|
31
|
-
{ value: "calls", label: "
|
|
30
|
+
{ value: "time", label: t("common.duration"), title: t("trace.mode-time-title") },
|
|
31
|
+
{ value: "turns", label: t("trace.turns"), title: t("trace.mode-turns-title") },
|
|
32
|
+
{ value: "calls", label: t("trace.calls"), title: t("trace.mode-calls-title") },
|
|
32
33
|
];
|
|
33
34
|
|
|
34
35
|
const LEGEND: Array<{ kind: TraceSpanKind; label: string }> = [
|
|
35
|
-
{ kind: "turn", label: "
|
|
36
|
-
{ kind: "model", label: "
|
|
37
|
-
{ kind: "tool", label: "
|
|
38
|
-
{ kind: "subagent", label: "
|
|
39
|
-
{ kind: "background", label: "
|
|
36
|
+
{ kind: "turn", label: t("common.input") },
|
|
37
|
+
{ kind: "model", label: t("common.model") },
|
|
38
|
+
{ kind: "tool", label: t("trace.tool") },
|
|
39
|
+
{ kind: "subagent", label: t("trace.agent") },
|
|
40
|
+
{ kind: "background", label: t("trace.background") },
|
|
40
41
|
];
|
|
41
42
|
|
|
42
43
|
export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
43
|
-
const query = useQuery(
|
|
44
|
+
const query = useQuery<SessionTrace>(
|
|
45
|
+
["trace", file],
|
|
46
|
+
({ signal }, previous) => getSessionTrace(file, signal, previous),
|
|
47
|
+
{ pollMs: 15000, enabled: active },
|
|
48
|
+
);
|
|
44
49
|
// A previous session's trace must never render under this file's header.
|
|
45
50
|
const trace = query.stale ? null : query.data;
|
|
46
51
|
|
|
@@ -89,7 +94,10 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
89
94
|
});
|
|
90
95
|
}, [scale]);
|
|
91
96
|
|
|
92
|
-
const effectiveViewport
|
|
97
|
+
const effectiveViewport = useMemo<TimelineViewport>(
|
|
98
|
+
() => viewport ?? { u0: scale.domain[0], u1: scale.domain[1] },
|
|
99
|
+
[viewport, scale],
|
|
100
|
+
);
|
|
93
101
|
|
|
94
102
|
const allSpans = useMemo(() => {
|
|
95
103
|
const spans: Array<{ span: TraceSpan; track: TraceTrack }> = [];
|
|
@@ -105,6 +113,11 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
105
113
|
if (!needle) return [];
|
|
106
114
|
return allSpans.filter(({ span }) => `${span.label} ${span.detail ?? ""}`.toLowerCase().includes(needle));
|
|
107
115
|
}, [allSpans, search]);
|
|
116
|
+
// Shared with the canvas and transcript so the match test runs once per search.
|
|
117
|
+
const matchIds = useMemo(
|
|
118
|
+
() => (search.trim() ? new Set(matches.map(({ span }) => span.id)) : null),
|
|
119
|
+
[matches, search],
|
|
120
|
+
);
|
|
108
121
|
|
|
109
122
|
const centerOnSpan = useCallback(
|
|
110
123
|
(span: TraceSpan) => {
|
|
@@ -198,6 +211,15 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
198
211
|
[tracks],
|
|
199
212
|
);
|
|
200
213
|
|
|
214
|
+
const openFromTranscript = useCallback(
|
|
215
|
+
(spanId: string) => {
|
|
216
|
+
selectAndReveal(spanId);
|
|
217
|
+
setDrawerOpen(true);
|
|
218
|
+
},
|
|
219
|
+
[selectAndReveal],
|
|
220
|
+
);
|
|
221
|
+
const closeDrawer = useCallback(() => setDrawerOpen(false), []);
|
|
222
|
+
|
|
201
223
|
const traceStart = trace?.startedAt ?? 0;
|
|
202
224
|
|
|
203
225
|
const traceQuery = { ...query, data: trace };
|
|
@@ -207,7 +229,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
207
229
|
<div>
|
|
208
230
|
<button type="button" className="btn" data-variant="ghost" data-size="sm" onClick={onBack}>
|
|
209
231
|
<ArrowLeft size={14} aria-hidden="true" />
|
|
210
|
-
|
|
232
|
+
{t("trace.sessions")}
|
|
211
233
|
</button>
|
|
212
234
|
</div>
|
|
213
235
|
<PageHeader
|
|
@@ -220,8 +242,8 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
220
242
|
data-variant="ghost"
|
|
221
243
|
data-icon="true"
|
|
222
244
|
onClick={query.refetch}
|
|
223
|
-
aria-label="
|
|
224
|
-
title="
|
|
245
|
+
aria-label={t("trace.refresh")}
|
|
246
|
+
title={t("trace.refresh")}
|
|
225
247
|
>
|
|
226
248
|
<RefreshCw size={14} className={query.refreshing ? "traces-spin" : undefined} />
|
|
227
249
|
</button>
|
|
@@ -235,8 +257,8 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
235
257
|
|
|
236
258
|
<Card
|
|
237
259
|
index={1}
|
|
238
|
-
title="
|
|
239
|
-
description="
|
|
260
|
+
title={t("trace.timeline")}
|
|
261
|
+
description={t("trace.timeline-hint")}
|
|
240
262
|
actions={
|
|
241
263
|
<>
|
|
242
264
|
<button
|
|
@@ -246,7 +268,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
246
268
|
data-size="sm"
|
|
247
269
|
onClick={() => collapseAll(false)}
|
|
248
270
|
>
|
|
249
|
-
|
|
271
|
+
{t("trace.expand-all")}
|
|
250
272
|
</button>
|
|
251
273
|
<button
|
|
252
274
|
type="button"
|
|
@@ -255,7 +277,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
255
277
|
data-size="sm"
|
|
256
278
|
onClick={() => collapseAll(true)}
|
|
257
279
|
>
|
|
258
|
-
|
|
280
|
+
{t("trace.collapse-all")}
|
|
259
281
|
</button>
|
|
260
282
|
</>
|
|
261
283
|
}
|
|
@@ -267,7 +289,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
267
289
|
value={mode}
|
|
268
290
|
onChange={setMode}
|
|
269
291
|
size="sm"
|
|
270
|
-
aria-label="
|
|
292
|
+
aria-label={t("trace.axis-mode")}
|
|
271
293
|
/>
|
|
272
294
|
{mode === "time" && (
|
|
273
295
|
<label className="traces-check">
|
|
@@ -276,7 +298,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
276
298
|
checked={compressIdle}
|
|
277
299
|
onChange={event => setCompressIdle(event.target.checked)}
|
|
278
300
|
/>
|
|
279
|
-
|
|
301
|
+
{t("trace.compress-idle")}
|
|
280
302
|
</label>
|
|
281
303
|
)}
|
|
282
304
|
<div className="traces-search">
|
|
@@ -286,7 +308,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
286
308
|
setSearch(value);
|
|
287
309
|
setMatchIndex(0);
|
|
288
310
|
}}
|
|
289
|
-
placeholder="
|
|
311
|
+
placeholder={t("trace.search-placeholder")}
|
|
290
312
|
width={200}
|
|
291
313
|
/>
|
|
292
314
|
{search.trim() && (
|
|
@@ -303,7 +325,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
303
325
|
data-size="sm"
|
|
304
326
|
data-icon="true"
|
|
305
327
|
onClick={() => cycleMatch(-1)}
|
|
306
|
-
aria-label="
|
|
328
|
+
aria-label={t("trace.previous-match")}
|
|
307
329
|
disabled={matches.length === 0}
|
|
308
330
|
>
|
|
309
331
|
<ChevronLeft size={14} />
|
|
@@ -315,7 +337,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
315
337
|
data-size="sm"
|
|
316
338
|
data-icon="true"
|
|
317
339
|
onClick={() => cycleMatch(1)}
|
|
318
|
-
aria-label="
|
|
340
|
+
aria-label={t("trace.next-match")}
|
|
319
341
|
disabled={matches.length === 0}
|
|
320
342
|
>
|
|
321
343
|
<ChevronRight size={14} />
|
|
@@ -346,9 +368,9 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
346
368
|
scale={scale}
|
|
347
369
|
viewport={effectiveViewport}
|
|
348
370
|
onViewportChange={setViewport}
|
|
349
|
-
|
|
371
|
+
selected={selected}
|
|
350
372
|
onSelect={selectAndOpen}
|
|
351
|
-
|
|
373
|
+
matchIds={matchIds}
|
|
352
374
|
collapsed={collapsed}
|
|
353
375
|
onToggleCollapse={toggleCollapse}
|
|
354
376
|
traceStart={traceStart}
|
|
@@ -356,20 +378,13 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
356
378
|
</div>
|
|
357
379
|
</Card>
|
|
358
380
|
|
|
359
|
-
<Card
|
|
360
|
-
index={2}
|
|
361
|
-
title="Transcript"
|
|
362
|
-
description="Every span and marker in order; click to inspect"
|
|
363
|
-
flush
|
|
364
|
-
>
|
|
381
|
+
<Card index={2} title={t("trace.transcript")} description={t("trace.transcript-description")} flush>
|
|
365
382
|
<TranscriptList
|
|
366
383
|
tracks={tracks}
|
|
367
384
|
selection={selection}
|
|
368
|
-
onSelect={
|
|
369
|
-
selectAndReveal(spanId);
|
|
370
|
-
setDrawerOpen(true);
|
|
371
|
-
}}
|
|
385
|
+
onSelect={openFromTranscript}
|
|
372
386
|
search={search}
|
|
387
|
+
matchIds={matchIds}
|
|
373
388
|
traceStart={traceStart}
|
|
374
389
|
/>
|
|
375
390
|
</Card>
|
|
@@ -381,7 +396,7 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
381
396
|
<SpanDrawer
|
|
382
397
|
span={drawerOpen ? (selected?.span ?? null) : null}
|
|
383
398
|
track={selected?.track ?? null}
|
|
384
|
-
onClose={
|
|
399
|
+
onClose={closeDrawer}
|
|
385
400
|
onOpenChildTrack={openChildTrack}
|
|
386
401
|
/>
|
|
387
402
|
</div>
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* selection. Hand-rolled fixed-row virtualization (32px rows, spacer divs).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { formatDurationMs } from "../data/formatters";
|
|
9
|
+
import { t } from "../i18n";
|
|
9
10
|
import { EmptyState } from "../ui";
|
|
10
11
|
import type { TraceMarker, TraceSpan, TraceTrack } from "../types";
|
|
11
12
|
import { formatOffset } from "./time-scale";
|
|
@@ -25,6 +26,8 @@ export interface TranscriptListProps {
|
|
|
25
26
|
selection: string | null;
|
|
26
27
|
onSelect: (spanId: string) => void;
|
|
27
28
|
search: string;
|
|
29
|
+
/** Spans matching `search` (from the trace view); `null` when not searching. */
|
|
30
|
+
matchIds: ReadonlySet<string> | null;
|
|
28
31
|
traceStart: number;
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -49,20 +52,26 @@ export function buildTranscriptRows(tracks: TraceTrack[]): TranscriptRow[] {
|
|
|
49
52
|
return rows;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
export
|
|
55
|
+
export const TranscriptList = memo(function TranscriptList({
|
|
56
|
+
tracks,
|
|
57
|
+
selection,
|
|
58
|
+
onSelect,
|
|
59
|
+
search,
|
|
60
|
+
matchIds,
|
|
61
|
+
traceStart,
|
|
62
|
+
}: TranscriptListProps) {
|
|
53
63
|
const colors = useTraceTheme();
|
|
54
64
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
55
65
|
const [scrollTop, setScrollTop] = useState(0);
|
|
56
66
|
|
|
57
67
|
const allRows = useMemo(() => buildTranscriptRows(tracks), [tracks]);
|
|
58
68
|
const rows = useMemo(() => {
|
|
69
|
+
if (!matchIds) return allRows;
|
|
59
70
|
const needle = search.trim().toLowerCase();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
}, [allRows, search]);
|
|
71
|
+
return allRows.filter(row =>
|
|
72
|
+
row.span ? matchIds.has(row.span.id) : (row.marker?.label ?? "").toLowerCase().includes(needle),
|
|
73
|
+
);
|
|
74
|
+
}, [allRows, search, matchIds]);
|
|
66
75
|
|
|
67
76
|
// Scroll the selected row into view when selection changes externally.
|
|
68
77
|
useEffect(() => {
|
|
@@ -106,7 +115,7 @@ export function TranscriptList({ tracks, selection, onSelect, search, traceStart
|
|
|
106
115
|
<span className="traces-row-label truncate">{label}</span>
|
|
107
116
|
{detail && <span className="traces-row-detail truncate">{detail}</span>}
|
|
108
117
|
<span className="traces-row-meta num">
|
|
109
|
-
{row.span?.isError && <span className="tone-bad">error</span>}
|
|
118
|
+
{row.span?.isError && <span className="tone-bad">{t("common.error")}</span>}
|
|
110
119
|
<span>{duration}</span>
|
|
111
120
|
<span>{formatOffset(row.time - traceStart)}</span>
|
|
112
121
|
</span>
|
|
@@ -132,7 +141,7 @@ export function TranscriptList({ tracks, selection, onSelect, search, traceStart
|
|
|
132
141
|
})}
|
|
133
142
|
</div>
|
|
134
143
|
</div>
|
|
135
|
-
{rows.length === 0 && <EmptyState title="
|
|
144
|
+
{rows.length === 0 && <EmptyState title={t("trace.no-matching-events")} />}
|
|
136
145
|
</div>
|
|
137
146
|
);
|
|
138
|
-
}
|
|
147
|
+
});
|
|
@@ -111,6 +111,32 @@
|
|
|
111
111
|
cursor: grabbing;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/* Scene canvas with the hover/selection boxes stacked on top. */
|
|
115
|
+
.traces-canvas-stack {
|
|
116
|
+
position: relative;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.traces-canvas-stack > canvas {
|
|
122
|
+
display: block;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.traces-canvas-hover,
|
|
126
|
+
.traces-canvas-selection {
|
|
127
|
+
position: absolute;
|
|
128
|
+
pointer-events: none;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.traces-canvas-hover {
|
|
133
|
+
background: rgba(255, 255, 255, 0.16);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.traces-canvas-selection {
|
|
137
|
+
border: 1.5px solid;
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
.traces-chevron {
|
|
115
141
|
display: inline-flex;
|
|
116
142
|
background: none;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Check, ChevronDown, ChevronRight, Copy } from "lucide-react";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { t } from "../i18n";
|
|
3
4
|
|
|
4
5
|
export interface JsonBlockProps {
|
|
5
6
|
data: unknown;
|
|
@@ -8,7 +9,7 @@ export interface JsonBlockProps {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
/** Collapsible, copyable pretty-printed JSON. */
|
|
11
|
-
export function JsonBlock({ data, title = "
|
|
12
|
+
export function JsonBlock({ data, title = t("ui.json"), initialCollapsed = false }: JsonBlockProps) {
|
|
12
13
|
const [collapsed, setCollapsed] = useState(initialCollapsed);
|
|
13
14
|
const [copied, setCopied] = useState(false);
|
|
14
15
|
const copyResetRef = useRef<number>(0);
|
|
@@ -44,7 +45,7 @@ export function JsonBlock({ data, title = "JSON", initialCollapsed = false }: Js
|
|
|
44
45
|
</button>
|
|
45
46
|
<button type="button" className="btn" data-variant="ghost" data-size="sm" onClick={copy}>
|
|
46
47
|
{copied ? <Check size={13} /> : <Copy size={13} />}
|
|
47
|
-
{copied ? "
|
|
48
|
+
{copied ? t("common.copied") : t("common.copy")}
|
|
48
49
|
</button>
|
|
49
50
|
</div>
|
|
50
51
|
{!collapsed && (
|
package/src/client/ui/Modal.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { X } from "lucide-react";
|
|
|
2
2
|
import type React from "react";
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
+
import { t } from "../i18n";
|
|
5
6
|
import "./modal.css";
|
|
6
7
|
|
|
7
8
|
export interface ModalAction {
|
|
@@ -26,7 +27,7 @@ export interface ModalProps {
|
|
|
26
27
|
* containers cannot clip it. Focus lands on the primary action (or the cancel
|
|
27
28
|
* button when there is none) and returns to the opener on close.
|
|
28
29
|
*/
|
|
29
|
-
export function Modal({ open, title, onClose, primaryAction, cancelLabel = "
|
|
30
|
+
export function Modal({ open, title, onClose, primaryAction, cancelLabel = t("common.cancel"), children }: ModalProps) {
|
|
30
31
|
const primaryRef = useRef<HTMLButtonElement>(null);
|
|
31
32
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
|
32
33
|
const onCloseRef = useRef(onClose);
|
|
@@ -76,7 +77,7 @@ export function Modal({ open, title, onClose, primaryAction, cancelLabel = "Canc
|
|
|
76
77
|
data-icon="true"
|
|
77
78
|
data-size="sm"
|
|
78
79
|
onClick={onClose}
|
|
79
|
-
aria-label="
|
|
80
|
+
aria-label={t("common.close")}
|
|
80
81
|
>
|
|
81
82
|
<X size={15} />
|
|
82
83
|
</button>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GitBranch } from "lucide-react";
|
|
2
2
|
import { getRequestDetails } from "../api";
|
|
3
|
+
import { t } from "../i18n";
|
|
3
4
|
import {
|
|
4
5
|
formatCost,
|
|
5
6
|
formatDurationMs,
|
|
@@ -22,9 +23,9 @@ import "./request-drawer.css";
|
|
|
22
23
|
|
|
23
24
|
/** Label and tone for each request outcome; shared by the request tables. */
|
|
24
25
|
export const REQUEST_STATUS: Record<RequestStatus, { label: string; tone: Tone }> = {
|
|
25
|
-
ok: { label: "
|
|
26
|
-
aborted: { label: "
|
|
27
|
-
failed: { label: "
|
|
26
|
+
ok: { label: t("ui.status-ok"), tone: "ok" },
|
|
27
|
+
aborted: { label: t("common.aborted"), tone: "warn" },
|
|
28
|
+
failed: { label: t("common.failed"), tone: "bad" },
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export interface RequestDrawerProps {
|
|
@@ -34,7 +35,9 @@ export interface RequestDrawerProps {
|
|
|
34
35
|
|
|
35
36
|
/** Full detail sheet for one model request: timing, tokens, cost, ids, error, and the raw session entry. */
|
|
36
37
|
export function RequestDrawer({ id, onClose }: RequestDrawerProps) {
|
|
37
|
-
const query = useQuery(["request", id], () => getRequestDetails(id ?? 0), {
|
|
38
|
+
const query = useQuery(["request", id], ({ signal }) => getRequestDetails(id ?? 0, signal), {
|
|
39
|
+
enabled: id !== null,
|
|
40
|
+
});
|
|
38
41
|
// Never show the previous request's payload under a new id.
|
|
39
42
|
const details = query.stale ? null : query.data;
|
|
40
43
|
const status = details ? REQUEST_STATUS[requestStatus(details)] : null;
|
|
@@ -44,7 +47,7 @@ export function RequestDrawer({ id, onClose }: RequestDrawerProps) {
|
|
|
44
47
|
open={id !== null}
|
|
45
48
|
onClose={onClose}
|
|
46
49
|
width={620}
|
|
47
|
-
title={details ? <span className="mono">{details.model}</span> : "
|
|
50
|
+
title={details ? <span className="mono">{details.model}</span> : t("common.request")}
|
|
48
51
|
subtitle={
|
|
49
52
|
details ? (
|
|
50
53
|
<>
|
|
@@ -68,7 +71,7 @@ export function RequestDrawer({ id, onClose }: RequestDrawerProps) {
|
|
|
68
71
|
session: details.sessionFile,
|
|
69
72
|
})}
|
|
70
73
|
onClick={onClose}
|
|
71
|
-
title="
|
|
74
|
+
title={t("ui.open-trace")}
|
|
72
75
|
>
|
|
73
76
|
<GitBranch size={13} /> Trace
|
|
74
77
|
</a>
|
|
@@ -106,35 +109,35 @@ function RequestDetailsBody({ details, aborted }: { details: RequestDetails; abo
|
|
|
106
109
|
<>
|
|
107
110
|
{details.errorMessage && (
|
|
108
111
|
<section className="request-drawer-error" data-tone={aborted ? "warn" : "bad"} role="note">
|
|
109
|
-
<div className="request-drawer-error-label">{aborted ? "
|
|
112
|
+
<div className="request-drawer-error-label">{aborted ? t("common.aborted") : t("common.error")}</div>
|
|
110
113
|
<pre className="request-drawer-error-text">{details.errorMessage}</pre>
|
|
111
114
|
</section>
|
|
112
115
|
)}
|
|
113
116
|
|
|
114
117
|
<section>
|
|
115
|
-
<div className="section-label">
|
|
118
|
+
<div className="section-label">{t("ui.section-timing")}</div>
|
|
116
119
|
<KeyValues
|
|
117
120
|
items={[
|
|
118
|
-
{ key: "at", label: "
|
|
119
|
-
{ key: "duration", label: "
|
|
120
|
-
{ key: "ttft", label: "
|
|
121
|
-
{ key: "tps", label: "
|
|
121
|
+
{ key: "at", label: t("common.started"), value: formatTimestamp(details.timestamp) },
|
|
122
|
+
{ key: "duration", label: t("common.duration"), value: formatDurationMs(details.duration) },
|
|
123
|
+
{ key: "ttft", label: t("ui.time-to-first-token"), value: formatDurationMs(details.ttft) },
|
|
124
|
+
{ key: "tps", label: t("common.output-tokens-per-second"), value: formatTokensPerSecond(throughput) },
|
|
122
125
|
]}
|
|
123
126
|
/>
|
|
124
127
|
</section>
|
|
125
128
|
|
|
126
129
|
<section>
|
|
127
|
-
<div className="section-label">
|
|
130
|
+
<div className="section-label">{t("common.tokens")}</div>
|
|
128
131
|
<KeyValues
|
|
129
132
|
items={[
|
|
130
|
-
{ key: "input", label: "
|
|
131
|
-
{ key: "cacheRead", label: "
|
|
132
|
-
{ key: "cacheWrite", label: "
|
|
133
|
-
{ key: "output", label: "
|
|
134
|
-
{ key: "total", label: "
|
|
133
|
+
{ key: "input", label: t("ui.uncached-input"), value: formatInteger(usage.input) },
|
|
134
|
+
{ key: "cacheRead", label: t("common.cache-read"), value: formatInteger(usage.cacheRead) },
|
|
135
|
+
{ key: "cacheWrite", label: t("common.cache-write"), value: formatInteger(usage.cacheWrite) },
|
|
136
|
+
{ key: "output", label: t("common.output"), value: formatInteger(usage.output) },
|
|
137
|
+
{ key: "total", label: t("common.total"), value: formatInteger(usage.totalTokens) },
|
|
135
138
|
{
|
|
136
139
|
key: "premium",
|
|
137
|
-
label: "
|
|
140
|
+
label: t("common.premium-requests"),
|
|
138
141
|
value: formatInteger(Math.round((usage.premiumRequests ?? 0) * 100) / 100),
|
|
139
142
|
},
|
|
140
143
|
]}
|
|
@@ -142,11 +145,11 @@ function RequestDetailsBody({ details, aborted }: { details: RequestDetails; abo
|
|
|
142
145
|
</section>
|
|
143
146
|
|
|
144
147
|
<section>
|
|
145
|
-
<div className="section-label">
|
|
148
|
+
<div className="section-label">{t("ui.section-cost")}</div>
|
|
146
149
|
<KeyValues
|
|
147
150
|
items={[
|
|
148
151
|
{ key: "total", label: "Total", value: formatMessageCost(details, 4) },
|
|
149
|
-
{ key: "input", label: "
|
|
152
|
+
{ key: "input", label: t("common.input"), value: formatCost(usage.cost.input, 4) },
|
|
150
153
|
{ key: "cacheRead", label: "Cache read", value: formatCost(usage.cost.cacheRead, 4) },
|
|
151
154
|
{ key: "cacheWrite", label: "Cache write", value: formatCost(usage.cost.cacheWrite, 4) },
|
|
152
155
|
{ key: "output", label: "Output", value: formatCost(usage.cost.output, 4) },
|
|
@@ -155,29 +158,29 @@ function RequestDetailsBody({ details, aborted }: { details: RequestDetails; abo
|
|
|
155
158
|
</section>
|
|
156
159
|
|
|
157
160
|
<section>
|
|
158
|
-
<div className="section-label">
|
|
161
|
+
<div className="section-label">{t("ui.section-identity")}</div>
|
|
159
162
|
<KeyValues
|
|
160
163
|
items={[
|
|
161
|
-
{ key: "id", label: "
|
|
162
|
-
{ key: "entry", label: "
|
|
163
|
-
{ key: "stop", label: "
|
|
164
|
+
{ key: "id", label: t("common.request-id"), value: details.id ?? "–" },
|
|
165
|
+
{ key: "entry", label: t("common.entry-id"), value: details.entryId },
|
|
166
|
+
{ key: "stop", label: t("ui.stop-reason"), value: details.stopReason },
|
|
164
167
|
{ key: "api", label: "API", value: details.api },
|
|
165
168
|
{
|
|
166
169
|
key: "project",
|
|
167
|
-
label: "
|
|
170
|
+
label: t("common.project"),
|
|
168
171
|
value: <span title={details.folder}>{formatFolder(details.folder)}</span>,
|
|
169
172
|
},
|
|
170
173
|
]}
|
|
171
174
|
/>
|
|
172
175
|
<div className="request-drawer-file">
|
|
173
|
-
<span className="kv-key">
|
|
176
|
+
<span className="kv-key">{t("ui.session-file")}</span>
|
|
174
177
|
<span className="kv-value">{details.sessionFile}</span>
|
|
175
178
|
</div>
|
|
176
179
|
</section>
|
|
177
180
|
|
|
178
|
-
<JsonBlock data={output} title="
|
|
179
|
-
<JsonBlock data={messages} title="
|
|
180
|
-
<JsonBlock data={row} title="
|
|
181
|
+
{output != null && <JsonBlock data={output} title={t("common.output-message")} />}
|
|
182
|
+
<JsonBlock data={messages} title={t("common.session-entry")} initialCollapsed />
|
|
183
|
+
<JsonBlock data={row} title={t("ui.stats-row")} initialCollapsed />
|
|
181
184
|
</>
|
|
182
185
|
);
|
|
183
186
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Search } from "lucide-react";
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
|
|
3
4
|
export interface SearchInputProps {
|
|
4
5
|
value: string;
|
|
@@ -8,7 +9,7 @@ export interface SearchInputProps {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
/** Text filter with a leading search glyph. */
|
|
11
|
-
export function SearchInput({ value, onChange, placeholder = "
|
|
12
|
+
export function SearchInput({ value, onChange, placeholder = t("common.filter"), width }: SearchInputProps) {
|
|
12
13
|
return (
|
|
13
14
|
<label className="search">
|
|
14
15
|
<Search size={14} />
|
package/src/client/ui/States.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Inbox, RotateCw, TriangleAlert } from "lucide-react";
|
|
2
2
|
import type { CSSProperties, ReactNode } from "react";
|
|
3
|
+
import { t } from "../i18n";
|
|
3
4
|
|
|
4
|
-
export function EmptyState({ title = "
|
|
5
|
+
export function EmptyState({ title = t("common.nothing-here-yet"), hint }: { title?: ReactNode; hint?: ReactNode }) {
|
|
5
6
|
return (
|
|
6
7
|
<div className="empty">
|
|
7
8
|
<Inbox size={20} />
|
|
@@ -18,7 +19,7 @@ export function ErrorState({ error, onRetry }: { error: Error | string; onRetry?
|
|
|
18
19
|
<span className="error-state-message">{typeof error === "string" ? error : error.message}</span>
|
|
19
20
|
{onRetry && (
|
|
20
21
|
<button type="button" className="btn" data-size="sm" data-variant="ghost" onClick={onRetry}>
|
|
21
|
-
<RotateCw size={13} />
|
|
22
|
+
<RotateCw size={13} /> {t("ui.retry")}
|
|
22
23
|
</button>
|
|
23
24
|
)}
|
|
24
25
|
</div>
|
package/src/client/ui/Table.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
2
2
|
import { Fragment, type ReactNode, useMemo, useState } from "react";
|
|
3
3
|
import { formatInteger } from "../data/formatters";
|
|
4
|
+
import { t } from "../i18n";
|
|
4
5
|
import { EmptyState } from "./States";
|
|
5
6
|
|
|
6
7
|
export interface Column<T> {
|
|
@@ -155,7 +156,10 @@ export function Table<T>({
|
|
|
155
156
|
{limit !== undefined && sorted.length > limit && (
|
|
156
157
|
<div className="table-more">
|
|
157
158
|
<span className="micro">
|
|
158
|
-
{
|
|
159
|
+
{t("ui.table-shown-of", {
|
|
160
|
+
shown: formatInteger(visible.length),
|
|
161
|
+
total: formatInteger(sorted.length),
|
|
162
|
+
})}
|
|
159
163
|
</span>
|
|
160
164
|
{sorted.length > shown && (
|
|
161
165
|
<button
|
|
@@ -165,7 +169,7 @@ export function Table<T>({
|
|
|
165
169
|
data-variant="ghost"
|
|
166
170
|
onClick={() => setShown(n => n + limit * 4)}
|
|
167
171
|
>
|
|
168
|
-
|
|
172
|
+
{t("common.show-more")}
|
|
169
173
|
</button>
|
|
170
174
|
)}
|
|
171
175
|
{shown > limit && (
|
|
@@ -176,7 +180,7 @@ export function Table<T>({
|
|
|
176
180
|
data-variant="ghost"
|
|
177
181
|
onClick={() => setShown(limit)}
|
|
178
182
|
>
|
|
179
|
-
|
|
183
|
+
{t("ui.show-fewer")}
|
|
180
184
|
</button>
|
|
181
185
|
)}
|
|
182
186
|
</div>
|