@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traces copy. Keys are slugs (`trace.<name>`); the English string is the
|
|
3
|
+
* source of truth and `zh-CN` overrides it.
|
|
4
|
+
*/
|
|
5
|
+
export const traces = {
|
|
6
|
+
en: {
|
|
7
|
+
// ── session list (TracesRoute) ──────────────────────────────────────
|
|
8
|
+
"trace.title": "Traces",
|
|
9
|
+
"trace.description": "Recent sessions with subagent activity folded in. Open one to inspect its timeline.",
|
|
10
|
+
"trace.sessions": "Sessions",
|
|
11
|
+
"trace.session": "Session",
|
|
12
|
+
"trace.most-recent": "{count} most recent",
|
|
13
|
+
"trace.most-recent-filtered": "{shown} of {total} most recent",
|
|
14
|
+
"trace.filter-placeholder": "Filter by title, project, model…",
|
|
15
|
+
"trace.empty-title": "No sessions found",
|
|
16
|
+
"trace.empty-hint": "Run a sync to index recent activity.",
|
|
17
|
+
"trace.no-matching-title": "No matching sessions",
|
|
18
|
+
"trace.no-matching-hint": "Try a different filter.",
|
|
19
|
+
// ── span categories ─────────────────────────────────────────────────
|
|
20
|
+
"trace.tool": "Tool",
|
|
21
|
+
"trace.tools": "Tools",
|
|
22
|
+
"trace.agent": "Agent",
|
|
23
|
+
"trace.agents": "Agents",
|
|
24
|
+
"trace.background": "Background",
|
|
25
|
+
"trace.bg": "Bg",
|
|
26
|
+
// ── trace viewer toolbar / headings (TraceView) ─────────────────────
|
|
27
|
+
"trace.refresh": "Refresh trace",
|
|
28
|
+
"trace.timeline": "Timeline",
|
|
29
|
+
"trace.timeline-hint":
|
|
30
|
+
"W/S zoom · A/D pan · drag pan · wheel zoom · 0 fit · F focus selection · dbl-click focus · Esc deselect",
|
|
31
|
+
"trace.expand-all": "Expand all",
|
|
32
|
+
"trace.collapse-all": "Collapse all",
|
|
33
|
+
"trace.axis-mode": "Axis mode",
|
|
34
|
+
"trace.mode-time-title": "Real wall-clock time",
|
|
35
|
+
"trace.mode-turns-title": "Equal width per user turn",
|
|
36
|
+
"trace.mode-calls-title": "Equal width per model/tool call boundary",
|
|
37
|
+
"trace.turns": "Turns",
|
|
38
|
+
"trace.calls": "Calls",
|
|
39
|
+
"trace.compress-idle": "Compress idle",
|
|
40
|
+
"trace.search-placeholder": "Search spans…",
|
|
41
|
+
"trace.previous-match": "Previous match",
|
|
42
|
+
"trace.next-match": "Next match",
|
|
43
|
+
"trace.transcript": "Transcript",
|
|
44
|
+
"trace.transcript-description": "Every span and marker in order; click to inspect",
|
|
45
|
+
"trace.no-matching-events": "No matching events",
|
|
46
|
+
// ── flamegraph canvas / minimap ─────────────────────────────────────
|
|
47
|
+
"trace.canvas-aria": "Trace timeline. W and S zoom, A and D pan, 0 fits all, F focuses the selection.",
|
|
48
|
+
"trace.minimap-aria": "Timeline overview brush",
|
|
49
|
+
"trace.expand-track": "Expand {track}",
|
|
50
|
+
"trace.collapse-track": "Collapse {track}",
|
|
51
|
+
"trace.unterminated": "unterminated",
|
|
52
|
+
"trace.tok": "tok",
|
|
53
|
+
"trace.ttft": "TTFT",
|
|
54
|
+
// ── summary strip ───────────────────────────────────────────────────
|
|
55
|
+
"trace.wall-time": "Wall time",
|
|
56
|
+
"trace.model-time": "Model time",
|
|
57
|
+
"trace.tool-time": "Tool time",
|
|
58
|
+
"trace.idle": "Idle",
|
|
59
|
+
"trace.tool-calls-n": "{count} tool calls",
|
|
60
|
+
"trace.of-wall": "{percent} of wall",
|
|
61
|
+
// ── tool aggregates ─────────────────────────────────────────────────
|
|
62
|
+
"trace.tool-aggregates": "Tool aggregates",
|
|
63
|
+
"trace.tool-aggregates-desc": "{count} tools, by total time",
|
|
64
|
+
"trace.errors": "Errors",
|
|
65
|
+
"trace.failed-n": "{count} failed",
|
|
66
|
+
"trace.avg": "Avg",
|
|
67
|
+
"trace.max": "Max",
|
|
68
|
+
// ── span drawer ─────────────────────────────────────────────────────
|
|
69
|
+
"trace.start": "Start",
|
|
70
|
+
"trace.track": "Track",
|
|
71
|
+
"trace.io-cached": "Input / output / cached",
|
|
72
|
+
"trace.stop-reason": "Stop reason",
|
|
73
|
+
"trace.copy-json": "Copy raw JSON",
|
|
74
|
+
"trace.error-message": "Error message",
|
|
75
|
+
"trace.task": "Task",
|
|
76
|
+
"trace.open-child-track": "Open child track",
|
|
77
|
+
"trace.args": "Args",
|
|
78
|
+
"trace.thinking": "Thinking",
|
|
79
|
+
"trace.text": "Text",
|
|
80
|
+
"trace.raw-entry": "Raw entry",
|
|
81
|
+
"trace.load-failed": "Failed to load entry: {message}",
|
|
82
|
+
},
|
|
83
|
+
"zh-CN": {
|
|
84
|
+
// ── session list (TracesRoute) ──────────────────────────────────────
|
|
85
|
+
"trace.title": "调用链",
|
|
86
|
+
"trace.description": "最近的会话,已合并子代理活动。打开其中一个即可查看其时间线。",
|
|
87
|
+
"trace.sessions": "会话",
|
|
88
|
+
"trace.session": "会话",
|
|
89
|
+
"trace.most-recent": "最近 {count} 条",
|
|
90
|
+
"trace.most-recent-filtered": "最近 {total} 条中的 {shown} 条",
|
|
91
|
+
"trace.filter-placeholder": "按标题、项目或模型筛选…",
|
|
92
|
+
"trace.empty-title": "未找到会话",
|
|
93
|
+
"trace.empty-hint": "运行一次同步以索引最近的活动。",
|
|
94
|
+
"trace.no-matching-title": "没有匹配的会话",
|
|
95
|
+
"trace.no-matching-hint": "请尝试其他筛选条件。",
|
|
96
|
+
// ── span categories ─────────────────────────────────────────────────
|
|
97
|
+
"trace.tool": "工具",
|
|
98
|
+
"trace.tools": "工具",
|
|
99
|
+
"trace.agent": "智能体",
|
|
100
|
+
"trace.agents": "智能体",
|
|
101
|
+
"trace.background": "后台",
|
|
102
|
+
"trace.bg": "背景",
|
|
103
|
+
// ── trace viewer toolbar / headings (TraceView) ─────────────────────
|
|
104
|
+
"trace.refresh": "刷新调用链",
|
|
105
|
+
"trace.timeline": "时间线",
|
|
106
|
+
"trace.timeline-hint":
|
|
107
|
+
"W/S 缩放 · A/D 平移 · 拖拽平移 · 滚轮缩放 · 0 适应全部 · F 聚焦所选 · 双击聚焦 · Esc 取消选择",
|
|
108
|
+
"trace.expand-all": "全部展开",
|
|
109
|
+
"trace.collapse-all": "全部折叠",
|
|
110
|
+
"trace.axis-mode": "坐标轴模式",
|
|
111
|
+
"trace.mode-time-title": "真实墙钟时间",
|
|
112
|
+
"trace.mode-turns-title": "每个用户轮次等宽",
|
|
113
|
+
"trace.mode-calls-title": "每个模型/工具调用边界等宽",
|
|
114
|
+
"trace.turns": "轮次",
|
|
115
|
+
"trace.calls": "调用次数",
|
|
116
|
+
"trace.compress-idle": "压缩空闲时间",
|
|
117
|
+
"trace.search-placeholder": "搜索跨度…",
|
|
118
|
+
"trace.previous-match": "上一个匹配",
|
|
119
|
+
"trace.next-match": "下一个匹配",
|
|
120
|
+
"trace.transcript": "对话记录",
|
|
121
|
+
"trace.transcript-description": "按顺序列出所有跨度和标记;点击可查看详情",
|
|
122
|
+
"trace.no-matching-events": "没有匹配的事件",
|
|
123
|
+
// ── flamegraph canvas / minimap ─────────────────────────────────────
|
|
124
|
+
"trace.canvas-aria": "调用链时间线。W 和 S 缩放,A 和 D 平移,0 适应全部,F 聚焦所选。",
|
|
125
|
+
"trace.minimap-aria": "时间线概览刷选",
|
|
126
|
+
"trace.expand-track": "展开 {track}",
|
|
127
|
+
"trace.collapse-track": "折叠 {track}",
|
|
128
|
+
"trace.unterminated": "未结束",
|
|
129
|
+
"trace.tok": "Token",
|
|
130
|
+
"trace.ttft": "首 Token 延迟",
|
|
131
|
+
// ── summary strip ───────────────────────────────────────────────────
|
|
132
|
+
"trace.wall-time": "墙钟时间",
|
|
133
|
+
"trace.model-time": "模型耗时",
|
|
134
|
+
"trace.tool-time": "工具耗时",
|
|
135
|
+
"trace.idle": "空闲",
|
|
136
|
+
"trace.tool-calls-n": "{count} 次工具调用",
|
|
137
|
+
"trace.of-wall": "占墙钟时间 {percent}",
|
|
138
|
+
// ── tool aggregates ─────────────────────────────────────────────────
|
|
139
|
+
"trace.tool-aggregates": "工具汇总",
|
|
140
|
+
"trace.tool-aggregates-desc": "{count} 个工具,按总耗时排序",
|
|
141
|
+
"trace.errors": "错误",
|
|
142
|
+
"trace.failed-n": "{count} 个失败",
|
|
143
|
+
"trace.avg": "平均",
|
|
144
|
+
"trace.max": "最大",
|
|
145
|
+
// ── span drawer ─────────────────────────────────────────────────────
|
|
146
|
+
"trace.start": "开始",
|
|
147
|
+
"trace.track": "轨道",
|
|
148
|
+
"trace.io-cached": "输入 / 输出 / 缓存读取",
|
|
149
|
+
"trace.stop-reason": "停止原因",
|
|
150
|
+
"trace.copy-json": "复制原始 JSON",
|
|
151
|
+
"trace.error-message": "错误信息",
|
|
152
|
+
"trace.task": "任务",
|
|
153
|
+
"trace.open-child-track": "打开子轨道",
|
|
154
|
+
"trace.args": "参数",
|
|
155
|
+
"trace.thinking": "思考过程",
|
|
156
|
+
"trace.text": "文本",
|
|
157
|
+
"trace.raw-entry": "原始条目",
|
|
158
|
+
"trace.load-failed": "加载条目失败:{message}",
|
|
159
|
+
},
|
|
160
|
+
} as const;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ui copy. Keys are slugs (`ui.<name>`); the English string is the
|
|
3
|
+
* source of truth and `zh-CN` overrides it.
|
|
4
|
+
*/
|
|
5
|
+
export const ui = {
|
|
6
|
+
en: {
|
|
7
|
+
// ── table / states ──────────────────────────────────────────────────
|
|
8
|
+
"ui.table-shown-of": "{shown} of {total}",
|
|
9
|
+
"ui.show-fewer": "Show fewer",
|
|
10
|
+
"ui.retry": "Retry",
|
|
11
|
+
"ui.delta-new": "new",
|
|
12
|
+
"ui.json": "JSON",
|
|
13
|
+
// ── request drawer ──────────────────────────────────────────────────
|
|
14
|
+
"ui.trace": "Trace",
|
|
15
|
+
"ui.open-trace": "Open this request's session in the trace view",
|
|
16
|
+
"ui.status-ok": "OK",
|
|
17
|
+
"ui.section-timing": "Timing",
|
|
18
|
+
"ui.time-to-first-token": "Time to first token",
|
|
19
|
+
"ui.uncached-input": "Uncached input",
|
|
20
|
+
"ui.section-cost": "API-equivalent cost",
|
|
21
|
+
"ui.section-identity": "Identity",
|
|
22
|
+
"ui.stop-reason": "Stop reason",
|
|
23
|
+
"ui.api": "API",
|
|
24
|
+
"ui.session-file": "Session file",
|
|
25
|
+
"ui.stats-row": "Stats row",
|
|
26
|
+
},
|
|
27
|
+
"zh-CN": {
|
|
28
|
+
"ui.table-shown-of": "已显示 {shown} / 共 {total}",
|
|
29
|
+
"ui.show-fewer": "收起",
|
|
30
|
+
"ui.retry": "重试",
|
|
31
|
+
"ui.delta-new": "新增",
|
|
32
|
+
"ui.json": "JSON 数据",
|
|
33
|
+
"ui.trace": "调用链",
|
|
34
|
+
"ui.open-trace": "在调用链视图中打开该请求的会话",
|
|
35
|
+
"ui.status-ok": "正常",
|
|
36
|
+
"ui.section-timing": "时间",
|
|
37
|
+
"ui.time-to-first-token": "首 Token 延迟",
|
|
38
|
+
"ui.uncached-input": "未缓存输入",
|
|
39
|
+
"ui.section-cost": "API 等价费用",
|
|
40
|
+
"ui.section-identity": "标识",
|
|
41
|
+
"ui.stop-reason": "停止原因",
|
|
42
|
+
"ui.api": "接口",
|
|
43
|
+
"ui.session-file": "会话文件",
|
|
44
|
+
"ui.stats-row": "统计行",
|
|
45
|
+
},
|
|
46
|
+
} as const;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MessageKey } from "./en";
|
|
2
|
+
import { nav } from "./nav";
|
|
3
|
+
import { common } from "./common";
|
|
4
|
+
import { overview } from "./overview";
|
|
5
|
+
import { models } from "./models";
|
|
6
|
+
import { providers } from "./providers";
|
|
7
|
+
import { costs } from "./costs";
|
|
8
|
+
import { requests } from "./requests";
|
|
9
|
+
import { errors } from "./errors";
|
|
10
|
+
import { traces } from "./traces";
|
|
11
|
+
import { tools } from "./tools";
|
|
12
|
+
import { frustration } from "./frustration";
|
|
13
|
+
import { projects } from "./projects";
|
|
14
|
+
import { gain } from "./gain";
|
|
15
|
+
import { ui } from "./ui";
|
|
16
|
+
import { charts } from "./charts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Simplified Chinese overrides. Incomplete on purpose in source: the coverage
|
|
20
|
+
* test (`packages/stats/test/i18n-coverage.test.ts`) fails on any missing key,
|
|
21
|
+
* which keeps a half-translated page out of a release without breaking builds
|
|
22
|
+
* mid-flight.
|
|
23
|
+
*/
|
|
24
|
+
export const zhCNMessages: Partial<Record<MessageKey, string>> = {
|
|
25
|
+
...nav["zh-CN"],
|
|
26
|
+
...common["zh-CN"],
|
|
27
|
+
...overview["zh-CN"],
|
|
28
|
+
...models["zh-CN"],
|
|
29
|
+
...providers["zh-CN"],
|
|
30
|
+
...costs["zh-CN"],
|
|
31
|
+
...requests["zh-CN"],
|
|
32
|
+
...errors["zh-CN"],
|
|
33
|
+
...traces["zh-CN"],
|
|
34
|
+
...tools["zh-CN"],
|
|
35
|
+
...frustration["zh-CN"],
|
|
36
|
+
...projects["zh-CN"],
|
|
37
|
+
...gain["zh-CN"],
|
|
38
|
+
...ui["zh-CN"],
|
|
39
|
+
...charts["zh-CN"],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The Simplified Chinese locale pack. */
|
|
43
|
+
export const zhCNLocale = { id: "zh-CN", messages: zhCNMessages } as const;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type { StatsLocale } from "../../locale";
|
|
2
|
+
|
|
3
|
+
import type { StatsLocale } from "../../locale";
|
|
4
|
+
|
|
5
|
+
/** Values a message may interpolate through `{name}` placeholders. */
|
|
6
|
+
export type TranslationParams = Record<string, string | number>;
|
|
7
|
+
|
|
8
|
+
/** A locale pack: the messages one locale overrides, keyed by slug. */
|
|
9
|
+
export interface LocalePack {
|
|
10
|
+
readonly id: StatsLocale;
|
|
11
|
+
readonly messages: Readonly<Record<string, string>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Messages for every locale, keyed first by locale id. `undefined` is part of
|
|
16
|
+
* the value type because a locale may override only some keys (English is the
|
|
17
|
+
* fallback for the rest).
|
|
18
|
+
*/
|
|
19
|
+
export type MessagesByLocale = Readonly<Record<StatsLocale, Readonly<Record<string, string | undefined>>>>;
|
package/src/client/index.tsx
CHANGED
|
@@ -2,6 +2,10 @@ import "./styles.css";
|
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import App from "./App";
|
|
4
4
|
|
|
5
|
+
// The locale resolves when `./i18n` is first imported (the server stamps the
|
|
6
|
+
// host CLI's language into the page before this module runs), so the app is
|
|
7
|
+
// already rendering in the right language by the time React mounts.
|
|
8
|
+
|
|
5
9
|
const root = document.getElementById("root");
|
|
6
10
|
if (!root) throw new Error("missing #root");
|
|
7
11
|
createRoot(root).render(<App />);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "./costs.css";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { getCostDashboardStats } from "../api";
|
|
4
|
+
import { t } from "../i18n";
|
|
4
5
|
import { Chart, type ChartSeries, Legend, ShareBar, useHiddenSeries } from "../charts";
|
|
5
6
|
import { buildModelColorLookup, modelKey, OTHER_COLOR, SERIES_COLORS } from "../data/colors";
|
|
6
7
|
import { formatCost, formatEstimatedCost, formatInteger, formatPercent } from "../data/formatters";
|
|
@@ -35,8 +36,8 @@ export interface CostsRouteProps {
|
|
|
35
36
|
type SplitMode = "model" | "component";
|
|
36
37
|
|
|
37
38
|
const SPLIT_OPTIONS = [
|
|
38
|
-
{ value: "model" as const, label: "
|
|
39
|
-
{ value: "component" as const, label: "
|
|
39
|
+
{ value: "model" as const, label: t("costs.by-model") },
|
|
40
|
+
{ value: "component" as const, label: t("costs.by-component") },
|
|
40
41
|
];
|
|
41
42
|
|
|
42
43
|
/** Models stacked individually in the daily chart; the rest fold into "Other". */
|
|
@@ -46,10 +47,10 @@ const MODEL_LIMIT = 6;
|
|
|
46
47
|
const DAY_MS = 86_400_000;
|
|
47
48
|
|
|
48
49
|
const COMPONENTS = [
|
|
49
|
-
{ key: "costInput", label: "
|
|
50
|
-
{ key: "costOutput", label: "
|
|
51
|
-
{ key: "costCacheRead", label: "
|
|
52
|
-
{ key: "costCacheWrite", label: "
|
|
50
|
+
{ key: "costInput", label: t("common.input"), color: "#5b8cff" },
|
|
51
|
+
{ key: "costOutput", label: t("common.output"), color: "var(--chart-secondary)" },
|
|
52
|
+
{ key: "costCacheRead", label: t("common.cache-read"), color: "var(--chart-primary)" },
|
|
53
|
+
{ key: "costCacheWrite", label: t("common.cache-write"), color: "#f5b54a" },
|
|
53
54
|
] as const satisfies readonly { key: keyof CostComponents; label: string; color: string }[];
|
|
54
55
|
|
|
55
56
|
const UTC_DAY = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", timeZone: "UTC" });
|
|
@@ -62,20 +63,19 @@ const UTC_DAY_LONG = new Intl.DateTimeFormat(undefined, {
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
65
|
-
const query = useQuery(["costs", range], () => getCostDashboardStats(range), { enabled: active });
|
|
66
|
+
const query = useQuery(["costs", range], ({ signal }) => getCostDashboardStats(range, signal), { enabled: active });
|
|
66
67
|
const meta = rangeMeta(range);
|
|
67
68
|
const [split, setSplit] = useState<SplitMode>("model");
|
|
68
69
|
const [hidden, toggleSeries] = useHiddenSeries();
|
|
69
70
|
const view = useMemo(() => (query.data ? buildCostsView(query.data, range) : null), [query.data, range]);
|
|
71
|
+
// Stable columns: a fresh array would make Table re-sort on every render.
|
|
72
|
+
const columns = useMemo(() => (view ? buildCostColumns(view) : []), [view]);
|
|
70
73
|
const isEmpty = (data: CostDashboardStats) => data.costSeries.length === 0;
|
|
71
|
-
const empty = <EmptyState title="
|
|
74
|
+
const empty = <EmptyState title={t("costs.no-usage-in-range")} hint={t("costs.empty-hint")} />;
|
|
72
75
|
|
|
73
76
|
return (
|
|
74
77
|
<div className="page">
|
|
75
|
-
<PageHeader
|
|
76
|
-
title="Costs"
|
|
77
|
-
description={`What ${meta.windowLabel} of usage would cost at public API rates. Subscription usage without a public price is counted as unpriced, not free.`}
|
|
78
|
-
/>
|
|
78
|
+
<PageHeader title={t("costs.title")} description={t("costs.description", { window: t(meta.windowKey) })} />
|
|
79
79
|
|
|
80
80
|
<QueryView
|
|
81
81
|
query={query}
|
|
@@ -89,17 +89,15 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
89
89
|
<div className="grid grid-main-side">
|
|
90
90
|
<Card
|
|
91
91
|
index={1}
|
|
92
|
-
title="
|
|
93
|
-
description={
|
|
94
|
-
split === "model" ? "Per UTC day, stacked by model" : "Per UTC day, stacked by billing component"
|
|
95
|
-
}
|
|
92
|
+
title={t("costs.daily-estimate")}
|
|
93
|
+
description={split === "model" ? t("costs.daily-by-model") : t("costs.daily-by-component")}
|
|
96
94
|
actions={
|
|
97
95
|
<Segmented
|
|
98
96
|
size="sm"
|
|
99
97
|
options={SPLIT_OPTIONS}
|
|
100
98
|
value={split}
|
|
101
99
|
onChange={setSplit}
|
|
102
|
-
aria-label="
|
|
100
|
+
aria-label={t("costs.split-by")}
|
|
103
101
|
/>
|
|
104
102
|
}
|
|
105
103
|
stale={query.stale}
|
|
@@ -119,11 +117,11 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
119
117
|
height={280}
|
|
120
118
|
format={v => formatCost(v, Number.isInteger(v) ? 0 : 2)}
|
|
121
119
|
formatTooltip={v => formatCost(v)}
|
|
122
|
-
emptyLabel="
|
|
120
|
+
emptyLabel={t("costs.no-priced-usage")}
|
|
123
121
|
tooltipExtra={i =>
|
|
124
122
|
view.unpricedPerDay[i] > 0 ? (
|
|
125
123
|
<div className="chart-tooltip-row chart-tooltip-total">
|
|
126
|
-
<span className="chart-tooltip-label">
|
|
124
|
+
<span className="chart-tooltip-label">{t("costs.unpriced-requests")}</span>
|
|
127
125
|
<span className="chart-tooltip-value">
|
|
128
126
|
{formatInteger(view.unpricedPerDay[i])}
|
|
129
127
|
</span>
|
|
@@ -147,7 +145,12 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
147
145
|
</QueryView>
|
|
148
146
|
</Card>
|
|
149
147
|
|
|
150
|
-
<Card
|
|
148
|
+
<Card
|
|
149
|
+
index={2}
|
|
150
|
+
title={t("costs.where-it-went")}
|
|
151
|
+
description={t("costs.estimate-by-component")}
|
|
152
|
+
stale={query.stale}
|
|
153
|
+
>
|
|
151
154
|
<QueryView query={query} skeleton={<ChartSkeleton height={280} />} isEmpty={isEmpty} empty={empty}>
|
|
152
155
|
{() => view && <ComponentBreakdown summary={view.summary} />}
|
|
153
156
|
</QueryView>
|
|
@@ -156,8 +159,8 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
156
159
|
|
|
157
160
|
<Card
|
|
158
161
|
index={3}
|
|
159
|
-
title="
|
|
160
|
-
description="
|
|
162
|
+
title={t("costs.by-model")}
|
|
163
|
+
description={t("costs.by-model-description")}
|
|
161
164
|
stale={query.stale}
|
|
162
165
|
flush
|
|
163
166
|
>
|
|
@@ -167,7 +170,7 @@ export function CostsRoute({ active, range }: CostsRouteProps) {
|
|
|
167
170
|
<Table
|
|
168
171
|
rows={view.summary.models}
|
|
169
172
|
rowKey={row => row.key}
|
|
170
|
-
columns={
|
|
173
|
+
columns={columns}
|
|
171
174
|
initialSort={{ key: "cost", dir: "desc" }}
|
|
172
175
|
limit={20}
|
|
173
176
|
/>
|
|
@@ -245,36 +248,45 @@ function CostStats({ view, stale }: { view: CostsView; stale: boolean }) {
|
|
|
245
248
|
<div data-stale={stale}>
|
|
246
249
|
<StatGrid min={180}>
|
|
247
250
|
<Stat
|
|
248
|
-
label="
|
|
249
|
-
title="
|
|
251
|
+
label={t("costs.api-equivalent-estimate")}
|
|
252
|
+
title={t("costs.api-estimate-title")}
|
|
250
253
|
value={formatEstimatedCost(summary.totalCost, summary.unpricedRequests)}
|
|
251
|
-
hint={
|
|
254
|
+
hint={t("costs.requests-n", { count: formatInteger(summary.requests) })}
|
|
252
255
|
spark={view.dailyTotals}
|
|
253
256
|
sparkColor="var(--chart-secondary)"
|
|
254
257
|
/>
|
|
255
258
|
<Stat
|
|
256
|
-
label="
|
|
257
|
-
title="
|
|
259
|
+
label={t("costs.average-per-day")}
|
|
260
|
+
title={t("costs.average-per-day-title")}
|
|
258
261
|
value={formatEstimatedCost(summary.avgDailyCost, summary.unpricedRequests)}
|
|
259
|
-
hint={
|
|
262
|
+
hint={t(summary.activeDays === 1 ? "costs.active-days-one" : "costs.active-days-n", {
|
|
263
|
+
count: formatInteger(summary.activeDays),
|
|
264
|
+
})}
|
|
260
265
|
/>
|
|
261
266
|
<Stat
|
|
262
|
-
label="
|
|
267
|
+
label={t("costs.top-model")}
|
|
263
268
|
title={top ? `${top.model} (${top.provider})` : undefined}
|
|
264
269
|
value={top ? top.model : "–"}
|
|
265
|
-
hint={
|
|
270
|
+
hint={
|
|
271
|
+
top
|
|
272
|
+
? t("costs.top-model-hint", {
|
|
273
|
+
cost: formatCost(top.cost),
|
|
274
|
+
share: formatPercent(top.share),
|
|
275
|
+
})
|
|
276
|
+
: t("costs.nothing-priced-yet")
|
|
277
|
+
}
|
|
266
278
|
/>
|
|
267
279
|
<Stat
|
|
268
|
-
label="
|
|
269
|
-
title="
|
|
280
|
+
label={t("costs.per-priced-request")}
|
|
281
|
+
title={t("costs.per-priced-request-title")}
|
|
270
282
|
value={pricedRequests > 0 ? formatUnitCost(summary.totalCost / pricedRequests) : "–"}
|
|
271
|
-
hint={
|
|
283
|
+
hint={t("costs.priced-n", { count: formatInteger(pricedRequests) })}
|
|
272
284
|
/>
|
|
273
285
|
<Stat
|
|
274
|
-
label="
|
|
275
|
-
title="
|
|
286
|
+
label={t("costs.unpriced-requests")}
|
|
287
|
+
title={t("costs.unpriced-title")}
|
|
276
288
|
value={formatInteger(summary.unpricedRequests)}
|
|
277
|
-
hint={summary.unpricedRequests > 0 ? "
|
|
289
|
+
hint={summary.unpricedRequests > 0 ? t("costs.unpriced-hint") : t("costs.all-priced")}
|
|
278
290
|
/>
|
|
279
291
|
</StatGrid>
|
|
280
292
|
</div>
|
|
@@ -302,15 +314,16 @@ function ComponentBreakdown({ summary }: { summary: CostSummaryView }) {
|
|
|
302
314
|
</div>
|
|
303
315
|
))}
|
|
304
316
|
<div className="costs-component-row costs-component-total">
|
|
305
|
-
<span>
|
|
317
|
+
<span>{t("common.total")}</span>
|
|
306
318
|
<span className="num">{formatEstimatedCost(summary.totalCost, summary.unpricedRequests)}</span>
|
|
307
319
|
<span className="costs-component-share" />
|
|
308
320
|
</div>
|
|
309
321
|
</div>
|
|
310
322
|
{summary.unpricedRequests > 0 && (
|
|
311
323
|
<p className="micro dim">
|
|
312
|
-
{
|
|
313
|
-
|
|
324
|
+
{t(summary.unpricedRequests === 1 ? "costs.unpriced-note-one" : "costs.unpriced-note-n", {
|
|
325
|
+
count: formatInteger(summary.unpricedRequests),
|
|
326
|
+
})}
|
|
314
327
|
</p>
|
|
315
328
|
)}
|
|
316
329
|
</div>
|
|
@@ -328,7 +341,7 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
328
341
|
return [
|
|
329
342
|
{
|
|
330
343
|
key: "model",
|
|
331
|
-
header: "
|
|
344
|
+
header: t("common.model"),
|
|
332
345
|
sort: row => row.model,
|
|
333
346
|
render: row => (
|
|
334
347
|
<LabelCell
|
|
@@ -340,15 +353,15 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
340
353
|
},
|
|
341
354
|
{
|
|
342
355
|
key: "requests",
|
|
343
|
-
header: "
|
|
356
|
+
header: t("common.requests"),
|
|
344
357
|
align: "right",
|
|
345
358
|
sort: row => row.requests,
|
|
346
359
|
render: row => <span className="num">{formatInteger(row.requests)}</span>,
|
|
347
360
|
},
|
|
348
361
|
{
|
|
349
362
|
key: "cost",
|
|
350
|
-
header: "
|
|
351
|
-
title: "
|
|
363
|
+
header: t("costs.estimate"),
|
|
364
|
+
title: t("costs.estimate-title"),
|
|
352
365
|
align: "right",
|
|
353
366
|
sort: row => row.cost,
|
|
354
367
|
render: row => (
|
|
@@ -362,14 +375,14 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
362
375
|
},
|
|
363
376
|
{
|
|
364
377
|
key: "share",
|
|
365
|
-
header: "
|
|
378
|
+
header: t("common.share"),
|
|
366
379
|
align: "right",
|
|
367
380
|
sort: row => row.share,
|
|
368
381
|
render: row => <span className="num muted">{row.cost > 0 ? formatPercent(row.share) : "–"}</span>,
|
|
369
382
|
},
|
|
370
383
|
{
|
|
371
384
|
key: "split",
|
|
372
|
-
header: "
|
|
385
|
+
header: t("costs.split"),
|
|
373
386
|
title: "Input · output · cache read · cache write",
|
|
374
387
|
width: 150,
|
|
375
388
|
render: row =>
|
|
@@ -384,7 +397,7 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
384
397
|
...componentColumns,
|
|
385
398
|
{
|
|
386
399
|
key: "perRequest",
|
|
387
|
-
header: "
|
|
400
|
+
header: t("costs.per-request"),
|
|
388
401
|
title: "Estimate ÷ priced requests",
|
|
389
402
|
align: "right",
|
|
390
403
|
sort: row => (row.requests > row.unpricedRequests ? row.cost / (row.requests - row.unpricedRequests) : -1),
|
|
@@ -398,7 +411,7 @@ function buildCostColumns(view: CostsView): Column<CostModelRow>[] {
|
|
|
398
411
|
},
|
|
399
412
|
{
|
|
400
413
|
key: "unpriced",
|
|
401
|
-
header: "
|
|
414
|
+
header: t("costs.unpriced"),
|
|
402
415
|
title: "Requests with no public-equivalent price",
|
|
403
416
|
align: "right",
|
|
404
417
|
sort: row => row.unpricedRequests,
|