@linxiraos/pi-stats 1.1.27 → 1.2.1

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