@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +12 -5
  2. package/build.ts +4 -0
  3. package/dist/client/favicon.ico +0 -0
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +1 -0
  6. package/dist/client/index.js +96 -96
  7. package/dist/types/aggregator.d.ts +4 -1
  8. package/dist/types/client/api.d.ts +5 -1
  9. package/dist/types/client/app/nav.d.ts +5 -2
  10. package/dist/types/client/data/formatters.d.ts +7 -2
  11. package/dist/types/client/data/live.d.ts +16 -11
  12. package/dist/types/client/data/query-store.d.ts +39 -0
  13. package/dist/types/client/data/query.d.ts +10 -5
  14. package/dist/types/client/data/range.d.ts +4 -3
  15. package/dist/types/client/i18n/format.d.ts +9 -0
  16. package/dist/types/client/i18n/index.d.ts +18 -0
  17. package/dist/types/client/i18n/messages/charts.d.ts +8 -0
  18. package/dist/types/client/i18n/messages/common.d.ts +153 -0
  19. package/dist/types/client/i18n/messages/costs.d.ts +92 -0
  20. package/dist/types/client/i18n/messages/en.d.ts +1255 -0
  21. package/dist/types/client/i18n/messages/errors.d.ts +78 -0
  22. package/dist/types/client/i18n/messages/frustration.d.ts +191 -0
  23. package/dist/types/client/i18n/messages/gain.d.ts +54 -0
  24. package/dist/types/client/i18n/messages/models.d.ts +94 -0
  25. package/dist/types/client/i18n/messages/nav.d.ts +46 -0
  26. package/dist/types/client/i18n/messages/overview.d.ts +80 -0
  27. package/dist/types/client/i18n/messages/projects.d.ts +84 -0
  28. package/dist/types/client/i18n/messages/providers.d.ts +142 -0
  29. package/dist/types/client/i18n/messages/requests.d.ts +72 -0
  30. package/dist/types/client/i18n/messages/tools.d.ts +119 -0
  31. package/dist/types/client/i18n/messages/traces.d.ts +144 -0
  32. package/dist/types/client/i18n/messages/ui.d.ts +44 -0
  33. package/dist/types/client/i18n/messages/zh-CN.d.ts +13 -0
  34. package/dist/types/client/i18n/types.d.ts +15 -0
  35. package/dist/types/client/traces/Minimap.d.ts +2 -0
  36. package/dist/types/client/traces/TimelineCanvas.d.ts +29 -8
  37. package/dist/types/client/traces/TranscriptList.d.ts +3 -1
  38. package/dist/types/db.d.ts +14 -21
  39. package/dist/types/embedded-client.d.ts +5 -0
  40. package/dist/types/frustration.d.ts +18 -9
  41. package/dist/types/live.d.ts +5 -3
  42. package/dist/types/locale.d.ts +16 -0
  43. package/dist/types/parser.d.ts +0 -17
  44. package/dist/types/rollup.d.ts +9 -1
  45. package/dist/types/server.d.ts +6 -0
  46. package/dist/types/shared-types.d.ts +7 -0
  47. package/dist/types/trace.d.ts +18 -6
  48. package/dist/types/types.d.ts +13 -1
  49. package/dist/types/user-metrics.d.ts +23 -1
  50. package/package.json +5 -5
  51. package/src/aggregator.ts +31 -11
  52. package/src/client/api.ts +40 -5
  53. package/src/client/app/LiveChip.tsx +14 -13
  54. package/src/client/app/Shell.tsx +19 -13
  55. package/src/client/app/ThemeToggle.tsx +4 -3
  56. package/src/client/app/nav.ts +19 -16
  57. package/src/client/data/formatters.ts +80 -13
  58. package/src/client/data/live.tsx +85 -45
  59. package/src/client/data/query-store.ts +108 -0
  60. package/src/client/data/query.ts +47 -56
  61. package/src/client/data/range.ts +34 -14
  62. package/src/client/data/series.ts +7 -1
  63. package/src/client/favicon.ico +0 -0
  64. package/src/client/i18n/format.ts +28 -0
  65. package/src/client/i18n/index.ts +53 -0
  66. package/src/client/i18n/messages/charts.ts +8 -0
  67. package/src/client/i18n/messages/common.ts +157 -0
  68. package/src/client/i18n/messages/costs.ts +104 -0
  69. package/src/client/i18n/messages/en.ts +40 -0
  70. package/src/client/i18n/messages/errors.ts +80 -0
  71. package/src/client/i18n/messages/frustration.ts +198 -0
  72. package/src/client/i18n/messages/gain.ts +54 -0
  73. package/src/client/i18n/messages/models.ts +94 -0
  74. package/src/client/i18n/messages/nav.ts +46 -0
  75. package/src/client/i18n/messages/overview.ts +90 -0
  76. package/src/client/i18n/messages/projects.ts +84 -0
  77. package/src/client/i18n/messages/providers.ts +165 -0
  78. package/src/client/i18n/messages/requests.ts +73 -0
  79. package/src/client/i18n/messages/tools.ts +120 -0
  80. package/src/client/i18n/messages/traces.ts +160 -0
  81. package/src/client/i18n/messages/ui.ts +46 -0
  82. package/src/client/i18n/messages/zh-CN.ts +43 -0
  83. package/src/client/i18n/types.ts +19 -0
  84. package/src/client/index.tsx +4 -0
  85. package/src/client/routes/CostsRoute.tsx +61 -48
  86. package/src/client/routes/ErrorsRoute.tsx +71 -47
  87. package/src/client/routes/FrustrationRoute.tsx +131 -116
  88. package/src/client/routes/GainRoute.tsx +42 -30
  89. package/src/client/routes/ModelsRoute.tsx +90 -73
  90. package/src/client/routes/OverviewRoute.tsx +109 -63
  91. package/src/client/routes/ProjectsRoute.tsx +73 -46
  92. package/src/client/routes/ProvidersRoute.tsx +151 -114
  93. package/src/client/routes/RequestsRoute.tsx +69 -46
  94. package/src/client/routes/ToolsRoute.tsx +108 -94
  95. package/src/client/routes/TracesRoute.tsx +25 -23
  96. package/src/client/styles.css +5 -2
  97. package/src/client/traces/AggregatesPanel.tsx +19 -8
  98. package/src/client/traces/Minimap.tsx +65 -27
  99. package/src/client/traces/SpanDrawer.tsx +46 -21
  100. package/src/client/traces/SummaryStrip.tsx +27 -11
  101. package/src/client/traces/TimelineCanvas.tsx +245 -116
  102. package/src/client/traces/TraceView.tsx +51 -36
  103. package/src/client/traces/TranscriptList.tsx +20 -11
  104. package/src/client/traces/traces.css +26 -0
  105. package/src/client/ui/JsonBlock.tsx +3 -2
  106. package/src/client/ui/Modal.tsx +3 -2
  107. package/src/client/ui/RequestDrawer.tsx +33 -30
  108. package/src/client/ui/SearchInput.tsx +2 -1
  109. package/src/client/ui/States.tsx +3 -2
  110. package/src/client/ui/Table.tsx +7 -3
  111. package/src/db.ts +150 -135
  112. package/src/embedded-client.ts +9 -0
  113. package/src/frustration.ts +9 -30
  114. package/src/index.ts +2 -1
  115. package/src/live.ts +36 -11
  116. package/src/locale.ts +29 -0
  117. package/src/parser.ts +37 -72
  118. package/src/port-conflict.ts +1 -1
  119. package/src/prompts/judge/frustration.json +19 -0
  120. package/src/rollup.ts +41 -20
  121. package/src/server.ts +67 -27
  122. package/src/shared-types.ts +7 -0
  123. package/src/trace.ts +54 -40
  124. package/src/types.ts +13 -1
  125. package/src/user-metrics.ts +220 -6
@@ -1,10 +1,12 @@
1
1
  import { ArrowRight } from "lucide-react";
2
2
  import { useMemo, useState } from "react";
3
3
  import { getOverviewStats, getRecentRequests } from "../api";
4
+ import { t } from "../i18n";
4
5
  import { Legend, ShareBar, TimeChart } from "../charts";
5
6
  import {
6
7
  formatCompact,
7
8
  formatDurationMs,
9
+ formatErrorRate,
8
10
  formatEstimatedCost,
9
11
  formatInteger,
10
12
  formatMessageCost,
@@ -41,12 +43,16 @@ export interface OverviewRouteProps {
41
43
  type ActivityMetric = "requests" | "tokens" | "cost";
42
44
 
43
45
  const ACTIVITY_OPTIONS = [
44
- { value: "requests" as const, label: "Requests" },
45
- { value: "tokens" as const, label: "Tokens" },
46
- { value: "cost" as const, label: "Cost" },
46
+ { value: "requests" as const, label: t("common.requests") },
47
+ { value: "tokens" as const, label: t("common.tokens") },
48
+ { value: "cost" as const, label: t("common.cost") },
47
49
  ];
48
50
 
49
- const AGENT_LABEL: Record<AgentType, string> = { main: "Main agent", subagent: "Subagents", advisor: "Advisor" };
51
+ const AGENT_LABEL: Record<AgentType, string> = {
52
+ main: t("overview.main-agent"),
53
+ subagent: t("overview.subagents"),
54
+ advisor: t("overview.advisor"),
55
+ };
50
56
  const AGENT_COLOR: Record<AgentType, string> = {
51
57
  main: "var(--chart-primary)",
52
58
  subagent: "var(--chart-secondary)",
@@ -54,15 +60,15 @@ const AGENT_COLOR: Record<AgentType, string> = {
54
60
  };
55
61
 
56
62
  const TOKEN_MIX = [
57
- { key: "input", label: "Uncached input", color: "#5b8cff" },
58
- { key: "cacheRead", label: "Cache read", color: "var(--chart-primary)" },
59
- { key: "cacheWrite", label: "Cache write", color: "#f5b54a" },
60
- { key: "output", label: "Output", color: "var(--chart-secondary)" },
63
+ { key: "input", label: t("overview.uncached-input"), color: "#5b8cff" },
64
+ { key: "cacheRead", label: t("common.cache-read"), color: "var(--chart-primary)" },
65
+ { key: "cacheWrite", label: t("common.cache-write"), color: "#f5b54a" },
66
+ { key: "output", label: t("common.output"), color: "var(--chart-secondary)" },
61
67
  ] as const;
62
68
 
63
69
  export function OverviewRoute({ active, range, onRequestClick }: OverviewRouteProps) {
64
- const overview = useQuery(["overview", range], () => getOverviewStats(range), { enabled: active });
65
- const recent = useQuery(["recent-requests"], () => getRecentRequests(12), { enabled: active });
70
+ const overview = useQuery(["overview", range], ({ signal }) => getOverviewStats(range, signal), { enabled: active });
71
+ const recent = useQuery(["recent-requests"], ({ signal }) => getRecentRequests(12, signal), { enabled: active });
66
72
  const [metric, setMetric] = useState<ActivityMetric>("requests");
67
73
  const meta = rangeMeta(range);
68
74
 
@@ -85,18 +91,25 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
85
91
  const chartSeries =
86
92
  metric === "requests"
87
93
  ? [
88
- { key: "ok", label: "Succeeded", color: "var(--chart-primary)", values: series.requests },
89
- { key: "err", label: "Failed", color: "var(--bad)", values: series.errors },
94
+ { key: "ok", label: t("overview.succeeded"), color: "var(--chart-primary)", values: series.requests },
95
+ { key: "err", label: t("common.failed"), color: "var(--bad)", values: series.errors },
90
96
  ]
91
97
  : metric === "tokens"
92
- ? [{ key: "tokens", label: "Tokens", color: "var(--chart-primary)", values: series.tokens }]
93
- : [{ key: "cost", label: "API-equivalent", color: "var(--chart-secondary)", values: series.cost }];
98
+ ? [{ key: "tokens", label: t("common.tokens"), color: "var(--chart-primary)", values: series.tokens }]
99
+ : [
100
+ {
101
+ key: "cost",
102
+ label: t("overview.api-equivalent"),
103
+ color: "var(--chart-secondary)",
104
+ values: series.cost,
105
+ },
106
+ ];
94
107
 
95
108
  return (
96
109
  <div className="page">
97
110
  <PageHeader
98
- title="Overview"
99
- description={`Everything zeta did across your sessions in ${meta.windowLabel}.`}
111
+ title={t("overview.title")}
112
+ description={t("overview.description", { window: t(meta.windowKey) })}
100
113
  />
101
114
 
102
115
  <QueryView query={overview} skeleton={<ChartSkeleton height={112} />}>
@@ -104,57 +117,73 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
104
117
  <div data-stale={overview.stale} className="stack" style={{ gap: 16 }}>
105
118
  <StatGrid min={190}>
106
119
  <Stat
107
- label="API-equivalent cost"
108
- title="What this usage would cost at public API rates"
120
+ label={t("overview.api-equivalent-cost")}
121
+ title={t("overview.api-cost-title")}
109
122
  value={formatEstimatedCost(overall.totalCost, overall.unpricedRequests)}
110
123
  hint={
111
124
  overall.unpricedRequests > 0
112
- ? `${formatInteger(overall.unpricedRequests)} unpriced`
125
+ ? t("overview.unpriced-n", { count: formatInteger(overall.unpricedRequests) })
113
126
  : undefined
114
127
  }
115
128
  spark={series.cost}
116
129
  sparkColor="var(--chart-secondary)"
117
130
  />
118
131
  <Stat
119
- label="Requests"
132
+ label={t("common.requests")}
120
133
  value={formatInteger(overall.totalRequests)}
121
- hint={`${formatInteger(overall.failedRequests)} failed`}
134
+ hint={t("overview.failed-n", { count: formatInteger(overall.failedRequests) })}
122
135
  spark={series.all}
123
136
  />
124
137
  <Stat
125
- label="Conversation tokens"
126
- title="Uncached input + cache reads + cache writes + output"
138
+ label={t("overview.conversation-tokens")}
139
+ title={t("overview.tokens-title")}
127
140
  value={formatCompact(sumConversationTokens(overall))}
128
- hint={`${formatCompact(overall.totalOutputTokens)} output`}
141
+ hint={t("overview.output-n", { count: formatCompact(overall.totalOutputTokens) })}
129
142
  spark={series.tokens}
130
143
  />
131
144
  <Stat
132
- label="Cache rate"
133
- title="Cache reads ÷ (uncached input + cache reads)"
145
+ label={t("overview.cache-rate")}
146
+ title={t("overview.cache-rate-title")}
134
147
  value={formatPercent(overall.cacheRate)}
135
- hint={`${formatPercent(overall.cacheSavings)} saved`}
148
+ hint={t("overview.cache-saved", { value: formatPercent(overall.cacheSavings) })}
136
149
  />
137
150
  <Stat
138
- label="Error rate"
139
- value={formatPercent(overall.errorRate)}
140
- hint={`${formatInteger(overall.successfulRequests)} succeeded`}
151
+ label={t("common.error-rate")}
152
+ value={formatErrorRate(overall.errorRate)}
153
+ hint={t("overview.succeeded-n", { count: formatInteger(overall.successfulRequests) })}
141
154
  spark={series.errors}
142
155
  sparkColor="var(--bad)"
143
156
  />
144
157
  </StatGrid>
145
158
  <StatGrid min={140}>
146
- <Stat size="sm" label="Uncached input" value={formatCompact(overall.totalInputTokens)} />
147
- <Stat size="sm" label="Cache read" value={formatCompact(overall.totalCacheReadTokens)} />
148
- <Stat size="sm" label="Cache write" value={formatCompact(overall.totalCacheWriteTokens)} />
149
- <Stat size="sm" label="Output" value={formatCompact(overall.totalOutputTokens)} />
150
159
  <Stat
151
160
  size="sm"
152
- label="Premium requests"
161
+ label={t("overview.uncached-input")}
162
+ value={formatCompact(overall.totalInputTokens)}
163
+ />
164
+ <Stat
165
+ size="sm"
166
+ label={t("common.cache-read")}
167
+ value={formatCompact(overall.totalCacheReadTokens)}
168
+ />
169
+ <Stat
170
+ size="sm"
171
+ label={t("common.cache-write")}
172
+ value={formatCompact(overall.totalCacheWriteTokens)}
173
+ />
174
+ <Stat size="sm" label={t("common.output")} value={formatCompact(overall.totalOutputTokens)} />
175
+ <Stat
176
+ size="sm"
177
+ label={t("common.premium-requests")}
153
178
  value={formatInteger(Math.round(overall.totalPremiumRequests * 100) / 100)}
154
179
  />
155
- <Stat size="sm" label="Tokens/s" value={formatTokensPerSecond(overall.avgTokensPerSecond)} />
156
- <Stat size="sm" label="Avg latency" value={formatDurationMs(overall.avgDuration)} />
157
- <Stat size="sm" label="Avg TTFT" value={formatDurationMs(overall.avgTtft)} />
180
+ <Stat
181
+ size="sm"
182
+ label={t("common.tokens-per-second")}
183
+ value={formatTokensPerSecond(overall.avgTokensPerSecond)}
184
+ />
185
+ <Stat size="sm" label={t("overview.avg-latency")} value={formatDurationMs(overall.avgDuration)} />
186
+ <Stat size="sm" label={t("overview.avg-ttft")} value={formatDurationMs(overall.avgTtft)} />
158
187
  </StatGrid>
159
188
  </div>
160
189
  )}
@@ -163,8 +192,14 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
163
192
  <div className="grid grid-main-side">
164
193
  <Card
165
194
  index={1}
166
- title="Activity"
167
- description={`Per ${meta.bucketMs < 3_600_000 ? "5 minutes" : meta.bucketMs < 86_400_000 ? "hour" : "day"}`}
195
+ title={t("overview.activity")}
196
+ description={
197
+ meta.bucketMs < 3_600_000
198
+ ? t("overview.per-5m")
199
+ : meta.bucketMs < 86_400_000
200
+ ? t("overview.per-hour")
201
+ : t("overview.per-day")
202
+ }
168
203
  actions={<Segmented size="sm" options={ACTIVITY_OPTIONS} value={metric} onChange={setMetric} />}
169
204
  stale={overview.stale}
170
205
  >
@@ -181,7 +216,12 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
181
216
  </QueryView>
182
217
  </Card>
183
218
 
184
- <Card index={2} title="Token mix" description="Where conversation tokens went" stale={overview.stale}>
219
+ <Card
220
+ index={2}
221
+ title={t("overview.token-mix")}
222
+ description={t("overview.token-mix-description")}
223
+ stale={overview.stale}
224
+ >
185
225
  <QueryView query={overview} skeleton={<ChartSkeleton height={260} />}>
186
226
  {({ overall, byAgentType }) => {
187
227
  const mix = {
@@ -196,25 +236,25 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
196
236
  <div className="stack" style={{ gap: 18 }}>
197
237
  <div className="stack" style={{ gap: 10 }}>
198
238
  <ShareBar
199
- segments={TOKEN_MIX.map(t => ({
200
- key: t.key,
201
- label: t.label,
202
- value: mix[t.key],
203
- color: t.color,
239
+ segments={TOKEN_MIX.map(slice => ({
240
+ key: slice.key,
241
+ label: slice.label,
242
+ value: mix[slice.key],
243
+ color: slice.color,
204
244
  }))}
205
245
  />
206
246
  <Legend
207
- items={TOKEN_MIX.map(t => ({
208
- key: t.key,
209
- label: t.label,
210
- color: t.color,
211
- value: total > 0 ? formatPercent(mix[t.key] / total, 0) : "–",
247
+ items={TOKEN_MIX.map(slice => ({
248
+ key: slice.key,
249
+ label: slice.label,
250
+ color: slice.color,
251
+ value: total > 0 ? formatPercent(mix[slice.key] / total, 0) : "–",
212
252
  }))}
213
253
  />
214
254
  </div>
215
255
  <div className="stack" style={{ gap: 10 }}>
216
256
  <div className="section-label" style={{ marginBottom: 0 }}>
217
- By agent
257
+ {t("overview.by-agent")}
218
258
  </div>
219
259
  <ShareBar
220
260
  segments={agents.segments.map(s => ({
@@ -229,7 +269,9 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
229
269
  <span className="row">
230
270
  <span className="swatch" style={{ background: AGENT_COLOR[s.agentType] }} />
231
271
  {AGENT_LABEL[s.agentType]}
232
- <span className="dim num">{formatInteger(s.requests)} req</span>
272
+ <span className="dim num">
273
+ {t("overview.requests-short", { count: formatInteger(s.requests) })}
274
+ </span>
233
275
  </span>
234
276
  <span className="row">
235
277
  <span className="dim num">{formatCompact(s.tokens)}</span>
@@ -251,13 +293,13 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
251
293
  index={3}
252
294
  title={
253
295
  <>
254
- <Dot tone="live" pulse /> Latest requests
296
+ <Dot tone="live" pulse /> {t("overview.latest-requests")}
255
297
  </>
256
298
  }
257
- description="Most recent model calls across every session"
299
+ description={t("overview.latest-requests-description")}
258
300
  actions={
259
301
  <a className="btn" data-size="sm" data-variant="ghost" href={`#/requests?range=${range}`}>
260
- All requests <ArrowRight size={13} />
302
+ {t("overview.all-requests")} <ArrowRight size={13} />
261
303
  </a>
262
304
  }
263
305
  flush
@@ -281,37 +323,41 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
281
323
  const REQUEST_COLUMNS = [
282
324
  {
283
325
  key: "model",
284
- header: "Model",
326
+ header: t("common.model"),
285
327
  render: (row: MessageStats) => <LabelCell primary={row.model} secondary={row.provider} />,
286
328
  },
287
329
  {
288
330
  key: "time",
289
- header: "When",
331
+ header: t("overview.when"),
290
332
  render: (row: MessageStats) => <span className="muted">{formatRelativeTime(row.timestamp)}</span>,
291
333
  },
292
334
  {
293
335
  key: "tokens",
294
- header: "Tokens",
336
+ header: t("common.tokens"),
295
337
  align: "right" as const,
296
338
  render: (row: MessageStats) => <span className="num">{formatInteger(row.usage.totalTokens)}</span>,
297
339
  },
298
340
  {
299
341
  key: "cost",
300
- header: "Cost",
342
+ header: t("common.cost"),
301
343
  align: "right" as const,
302
344
  render: (row: MessageStats) => <span className="num">{formatMessageCost(row, 4)}</span>,
303
345
  },
304
346
  {
305
347
  key: "duration",
306
- header: "Duration",
348
+ header: t("common.duration"),
307
349
  align: "right" as const,
308
350
  render: (row: MessageStats) => <span className="num">{formatDurationMs(row.duration)}</span>,
309
351
  },
310
352
  {
311
353
  key: "status",
312
- header: "Status",
354
+ header: t("overview.status"),
313
355
  align: "right" as const,
314
356
  render: (row: MessageStats) =>
315
- row.errorMessage ? <Badge tone="bad">Failed</Badge> : <Badge tone="ok">OK</Badge>,
357
+ row.errorMessage ? (
358
+ <Badge tone="bad">{t("common.failed")}</Badge>
359
+ ) : (
360
+ <Badge tone="ok">{t("overview.ok")}</Badge>
361
+ ),
316
362
  },
317
363
  ];
@@ -1,9 +1,11 @@
1
1
  import { useDeferredValue, useMemo, useState } from "react";
2
2
  import { getFolderStats } from "../api";
3
+ import { t } from "../i18n";
3
4
  import { BarList } from "../charts";
4
5
  import {
5
6
  formatCompact,
6
7
  formatDurationMs,
8
+ formatErrorRate,
7
9
  formatEstimatedCost,
8
10
  formatFolder,
9
11
  formatInteger,
@@ -42,7 +44,7 @@ const TABLE_LIMIT = 100;
42
44
  const TOP_LIMIT = 8;
43
45
 
44
46
  export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
45
- const folders = useQuery(["projects", range], () => getFolderStats(range), { enabled: active });
47
+ const folders = useQuery(["projects", range], ({ signal }) => getFolderStats(range, signal), { enabled: active });
46
48
  const [search, setSearch] = useState("");
47
49
  // Benchmarks and scratch sessions can leave tens of thousands of one-off temp folders; hide them by default.
48
50
  const [hideTemporary, setHideTemporary] = useState(true);
@@ -66,47 +68,54 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
66
68
  [scoped],
67
69
  );
68
70
  const topEmpty = (
69
- <EmptyState
70
- title={view.rows.length === 0 ? "No project folders in this range" : "Only temporary folders in this range"}
71
- />
71
+ <EmptyState title={t(view.rows.length === 0 ? "projects.empty-no-folders" : "projects.empty-only-temporary")} />
72
72
  );
73
73
  // Meter scales follow the whole range so a bar's length does not change while filtering.
74
74
  const columns = useMemo(() => folderColumns(view.maxRequests, view.maxCost), [view]);
75
75
 
76
76
  return (
77
77
  <div className="page">
78
- <PageHeader title="Projects" description={`Usage by session folder in ${meta.windowLabel}.`} />
78
+ <PageHeader
79
+ title={t("projects.title")}
80
+ description={t("projects.description", { window: t(meta.windowKey) })}
81
+ />
79
82
 
80
83
  <QueryView query={folders} skeleton={<ChartSkeleton height={96} />}>
81
84
  {() => (
82
85
  <div data-stale={folders.stale}>
83
86
  <StatGrid min={180}>
84
87
  <Stat
85
- label="Folders"
88
+ label={t("projects.folders")}
86
89
  value={formatInteger(view.rows.length)}
87
- hint={view.temporaryCount > 0 ? `${formatInteger(view.temporaryCount)} temporary` : undefined}
90
+ hint={
91
+ view.temporaryCount > 0
92
+ ? t("projects.temporary-n", { count: formatInteger(view.temporaryCount) })
93
+ : undefined
94
+ }
88
95
  />
89
96
  <Stat
90
- label="Requests"
97
+ label={t("common.requests")}
91
98
  value={formatInteger(view.totalRequests)}
92
- hint={`${formatInteger(view.failedRequests)} failed`}
99
+ hint={t("projects.failed-n", { count: formatInteger(view.failedRequests) })}
93
100
  />
94
101
  <Stat
95
- label="API-equivalent cost"
96
- title="What this usage would cost at public API rates"
102
+ label={t("projects.api-equivalent-cost")}
103
+ title={t("projects.api-equivalent-cost-hint")}
97
104
  value={formatEstimatedCost(view.totalCost, view.unpricedRequests)}
98
105
  hint={
99
- view.unpricedRequests > 0 ? `${formatInteger(view.unpricedRequests)} unpriced` : undefined
106
+ view.unpricedRequests > 0
107
+ ? t("projects.unpriced-n", { count: formatInteger(view.unpricedRequests) })
108
+ : undefined
100
109
  }
101
110
  />
102
111
  <Stat
103
- label="Conversation tokens"
104
- title="Uncached input + cache reads + cache writes + output"
112
+ label={t("projects.conversation-tokens")}
113
+ title={t("projects.conversation-tokens-hint")}
105
114
  value={formatCompact(view.conversationTokens)}
106
115
  />
107
116
  <Stat
108
- label="Cache rate"
109
- title="Cache reads ÷ (uncached input + cache reads), across every folder"
117
+ label={t("projects.cache-rate")}
118
+ title={t("projects.cache-rate-hint-all")}
110
119
  value={formatPercent(view.cacheRate)}
111
120
  />
112
121
  </StatGrid>
@@ -115,7 +124,12 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
115
124
  </QueryView>
116
125
 
117
126
  <div className="grid grid-2">
118
- <Card index={1} title="Top by cost" description="Share of API-equivalent cost" stale={folders.stale}>
127
+ <Card
128
+ index={1}
129
+ title={t("projects.top-by-cost")}
130
+ description={t("projects.top-by-cost-description")}
131
+ stale={folders.stale}
132
+ >
119
133
  <QueryView
120
134
  query={folders}
121
135
  skeleton={<ChartSkeleton height={236} />}
@@ -136,7 +150,12 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
136
150
  )}
137
151
  </QueryView>
138
152
  </Card>
139
- <Card index={2} title="Top by requests" description="Share of all requests" stale={folders.stale}>
153
+ <Card
154
+ index={2}
155
+ title={t("projects.top-by-requests")}
156
+ description={t("projects.top-by-requests-description")}
157
+ stale={folders.stale}
158
+ >
140
159
  <QueryView
141
160
  query={folders}
142
161
  skeleton={<ChartSkeleton height={236} />}
@@ -160,27 +179,35 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
160
179
 
161
180
  <Card
162
181
  index={3}
163
- title="Folders"
182
+ title={t("projects.folders")}
164
183
  description={
165
184
  folders.data
166
185
  ? matching.length === view.rows.length
167
- ? `${formatInteger(view.rows.length)} folders`
168
- : `${formatInteger(matching.length)} of ${formatInteger(view.rows.length)} folders`
186
+ ? t("projects.folders-n", { count: formatInteger(view.rows.length) })
187
+ : t("projects.folders-of-n", {
188
+ count: formatInteger(matching.length),
189
+ total: formatInteger(view.rows.length),
190
+ })
169
191
  : undefined
170
192
  }
171
193
  actions={
172
194
  <>
173
195
  {view.temporaryCount > 0 && (
174
- <label className="check" title="Session folders under /tmp or /var/folders">
196
+ <label className="check" title={t("projects.hide-temporary-hint")}>
175
197
  <input
176
198
  type="checkbox"
177
199
  checked={hideTemporary}
178
200
  onChange={e => setHideTemporary(e.target.checked)}
179
201
  />
180
- Hide temporary ({formatInteger(view.temporaryCount)})
202
+ {t("projects.hide-temporary", { count: formatInteger(view.temporaryCount) })}
181
203
  </label>
182
204
  )}
183
- <SearchInput value={search} onChange={setSearch} placeholder="Filter folders…" width={240} />
205
+ <SearchInput
206
+ value={search}
207
+ onChange={setSearch}
208
+ placeholder={t("projects.filter-placeholder")}
209
+ width={240}
210
+ />
184
211
  </>
185
212
  }
186
213
  flush
@@ -197,12 +224,12 @@ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
197
224
  dense
198
225
  empty={
199
226
  <EmptyState
200
- title={view.rows.length === 0 ? "No project folders in this range" : "No folders match"}
227
+ title={t(view.rows.length === 0 ? "projects.empty-no-folders" : "projects.empty-no-match")}
201
228
  hint={
202
229
  view.rows.length === 0
203
- ? "Try a longer range."
230
+ ? t("projects.hint-longer-range")
204
231
  : hideTemporary && view.temporaryCount > 0
205
- ? "Temporary folders are hidden."
232
+ ? t("projects.hint-temporary-hidden")
206
233
  : undefined
207
234
  }
208
235
  />
@@ -219,34 +246,34 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
219
246
  return [
220
247
  {
221
248
  key: "folder",
222
- header: "Folder",
249
+ header: t("projects.folder"),
223
250
  sort: row => row.folder,
224
251
  render: row => (
225
- <span className="row projects-folder" title={row.folder || "(root)"}>
252
+ <span className="row projects-folder" title={row.folder || t("common.root")}>
226
253
  <span className="mono truncate">{formatFolder(row.folder)}</span>
227
- {row.temporary && <Badge>temp</Badge>}
254
+ {row.temporary && <Badge>{t("projects.temp")}</Badge>}
228
255
  </span>
229
256
  ),
230
257
  },
231
258
  {
232
259
  key: "requests",
233
- header: "Requests",
260
+ header: t("common.requests"),
234
261
  align: "right",
235
262
  sort: row => row.totalRequests,
236
263
  render: row => (
237
- <span title={`${formatPercent(row.requestShare)} of all requests`}>
264
+ <span title={t("projects.share-of-requests", { share: formatPercent(row.requestShare) })}>
238
265
  <MeterCell value={row.totalRequests} max={maxRequests} display={formatInteger(row.totalRequests)} />
239
266
  </span>
240
267
  ),
241
268
  },
242
269
  {
243
270
  key: "cost",
244
- header: "Cost",
245
- title: "API-equivalent cost at public API rates",
271
+ header: t("common.cost"),
272
+ title: t("projects.api-equivalent-cost-rates"),
246
273
  align: "right",
247
274
  sort: row => row.totalCost,
248
275
  render: row => (
249
- <span title={`${formatPercent(row.costShare)} of all cost`}>
276
+ <span title={t("projects.share-of-cost", { share: formatPercent(row.costShare) })}>
250
277
  <MeterCell
251
278
  value={row.totalCost}
252
279
  max={maxCost}
@@ -258,8 +285,8 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
258
285
  },
259
286
  {
260
287
  key: "tokens",
261
- header: "Tokens",
262
- title: "Uncached input + cache reads + cache writes + output",
288
+ header: t("common.tokens"),
289
+ title: t("projects.conversation-tokens-hint"),
263
290
  align: "right",
264
291
  sort: row => row.conversationTokens,
265
292
  render: row => (
@@ -270,16 +297,16 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
270
297
  },
271
298
  {
272
299
  key: "cacheRate",
273
- header: "Cache rate",
274
- title: "Cache reads ÷ (uncached input + cache reads)",
300
+ header: t("projects.cache-rate"),
301
+ title: t("projects.cache-rate-hint"),
275
302
  align: "right",
276
303
  sort: row => row.cacheRate,
277
304
  render: row => <span className="num">{formatPercent(row.cacheRate)}</span>,
278
305
  },
279
306
  {
280
307
  key: "cacheSavings",
281
- header: "Cache savings",
282
- title: "Prompt-input cost saved versus billing the same tokens uncached",
308
+ header: t("projects.cache-savings"),
309
+ title: t("projects.cache-savings-hint"),
283
310
  align: "right",
284
311
  sort: row => row.cacheSavings,
285
312
  render: row => (
@@ -290,27 +317,27 @@ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowVi
290
317
  },
291
318
  {
292
319
  key: "errorRate",
293
- header: "Errors",
320
+ header: t("projects.errors"),
294
321
  align: "right",
295
322
  sort: row => row.errorRate,
296
323
  render: row => (
297
- <span title={`${formatInteger(row.failedRequests)} failed`}>
324
+ <span title={t("projects.failed-n", { count: formatInteger(row.failedRequests) })}>
298
325
  <Badge tone={row.failedRequests > 0 ? errorRateTone(row.errorRate) : "neutral"} mono>
299
- {formatPercent(row.errorRate)}
326
+ {formatErrorRate(row.errorRate)}
300
327
  </Badge>
301
328
  </span>
302
329
  ),
303
330
  },
304
331
  {
305
332
  key: "duration",
306
- header: "Avg duration",
333
+ header: t("projects.avg-duration"),
307
334
  align: "right",
308
335
  sort: row => row.avgDuration ?? -1,
309
336
  render: row => <span className="num">{formatDurationMs(row.avgDuration)}</span>,
310
337
  },
311
338
  {
312
339
  key: "last",
313
- header: "Last active",
340
+ header: t("projects.last-active"),
314
341
  align: "right",
315
342
  sort: row => row.lastTimestamp,
316
343
  render: row => <span className="dim">{formatRelativeTime(row.lastTimestamp)}</span>,