@cassius0924/dsh-usage-dashboard 0.3.1 → 0.5.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/lib/client.js CHANGED
@@ -56,18 +56,16 @@ var ErrorBoundary = class extends import_react.Component {
56
56
  const { message } = this.state;
57
57
  if (message === null) return this.props.children;
58
58
  if (this.props.silent === true) return null;
59
+ const { t } = this.props;
59
60
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-balance", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-card", children: [
60
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-card-title", children: "\u989D\u5EA6\u9762\u677F\u51FA\u9519\u4E86" }),
61
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dq-empty", children: "\u6E32\u67D3\u65F6\u629B\u4E86\u5F02\u5E38\uFF0C\u901A\u5E38\u662F\u6D4F\u89C8\u5668\u91CC\u5B58\u7740\u65E7\u7248\u672C\u7684\u7F13\u5B58\u6570\u636E\u3002\u6E05\u6389\u672C\u63D2\u4EF6\u7684\u672C\u5730\u6570\u636E\u518D\u91CD\u65B0\u52A0\u8F7D\u5C31\u80FD\u6062\u590D\u3002" }),
61
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-card-title", children: t("boundary.title") }),
62
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dq-empty", children: t("boundary.body") }),
62
63
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "dq-crash", children: message }),
63
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dq-refresh-btn", onClick: this.reset, children: "\u6E05\u9664\u672C\u5730\u6570\u636E\u5E76\u91CD\u65B0\u52A0\u8F7D" })
64
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dq-refresh-btn", onClick: this.reset, children: t("boundary.reset") })
64
65
  ] }) });
65
66
  }
66
67
  };
67
68
 
68
- // src/client/dashboard.tsx
69
- var import_react3 = require("react");
70
-
71
69
  // src/client/cache.ts
72
70
  var LS_VERSION = 2;
73
71
  function loadPersisted(storageKey, isUsable) {
@@ -117,6 +115,9 @@ function createCache(ttlMs, storageKey, isUsable) {
117
115
  };
118
116
  }
119
117
 
118
+ // src/contract.ts
119
+ var USAGE_WINDOW_DAYS = [7, 30, 90, 365];
120
+
120
121
  // src/client/api.ts
121
122
  var BALANCE_TTL_MS = 6e4;
122
123
  var USAGE_TTL_MS = 5 * 6e4;
@@ -130,7 +131,7 @@ async function getJson(path, cache) {
130
131
  var usageIsUsable = (res) => {
131
132
  if (res.ok !== true) return false;
132
133
  const data = res.data;
133
- return data !== void 0 && Array.isArray(data.daily) && Array.isArray(data.hourly) && Array.isArray(data.heatmap) && Array.isArray(data.models) && Array.isArray(data.sessions) && typeof data.sessionCount === "number" && data.totals !== void 0 && typeof data.totals.cacheSavings === "number" && data.summary?.today !== void 0 && Array.isArray(data.pricing?.tiers) && data.peakSplit?.peak !== void 0;
134
+ return data !== void 0 && Array.isArray(data.daily) && Array.isArray(data.hourly) && Array.isArray(data.heatmap) && Array.isArray(data.models) && Array.isArray(data.sessions) && typeof data.sessionCount === "number" && data.coverage?.scope === "local-dsh-session-logs" && typeof data.coverage.scannedSessions === "number" && typeof data.coverage.skippedRecords === "number" && Array.isArray(data.windows) && data.windows.length === USAGE_WINDOW_DAYS.length && USAGE_WINDOW_DAYS.every((days) => data.windows.some((window2) => window2.days === days)) && data.windows.every((window2) => Array.isArray(window2.daily) && Array.isArray(window2.hourly) && Array.isArray(window2.models) && Array.isArray(window2.sessions)) && data.totals !== void 0 && typeof data.totals.cacheSavings === "number" && data.summary?.today !== void 0 && Array.isArray(data.pricing?.tiers) && data.peakSplit?.peak !== void 0;
134
135
  };
135
136
  var balanceIsUsable = (res) => res.ok === true && Array.isArray(res.data?.balances);
136
137
  var balanceCache = createCache(BALANCE_TTL_MS, "dsh-usage-dashboard:balance", balanceIsUsable);
@@ -145,109 +146,869 @@ var subscribeUsage = (fn) => {
145
146
  usageListeners.delete(fn);
146
147
  };
147
148
  };
149
+ var balanceInflight = null;
150
+ var usageInflight = null;
148
151
  async function fetchBalance(force = false) {
149
152
  if (!force) {
150
153
  const hit = balanceCache.getFresh();
151
154
  if (hit !== null) return hit;
155
+ if (balanceInflight !== null) return balanceInflight;
152
156
  }
153
157
  const suffix = force ? "?refresh=1" : "";
154
- const res = await getJson(`/api/dsh-usage-dashboard/balance${suffix}`, force ? "no-store" : "default");
155
- if (res.ok) balanceCache.put(res);
156
- return res;
158
+ const task = getJson(`/api/dsh-usage-dashboard/balance${suffix}`, force ? "no-store" : "default").then((res) => {
159
+ if (res.ok) balanceCache.put(res);
160
+ return res;
161
+ });
162
+ if (!force) {
163
+ balanceInflight = task.finally(() => {
164
+ balanceInflight = null;
165
+ });
166
+ return balanceInflight;
167
+ }
168
+ return task;
157
169
  }
158
170
  async function fetchUsage(force = false) {
159
171
  if (!force) {
160
172
  const hit = usageCache.getFresh();
161
173
  if (hit !== null) return hit;
174
+ if (usageInflight !== null) return usageInflight;
162
175
  }
163
176
  const suffix = force ? "?refresh=1" : "";
164
- const res = await getJson(`/api/dsh-usage-dashboard/usage${suffix}`, force ? "no-store" : "default");
165
- if (res.ok) {
166
- usageCache.put(res);
167
- for (const listener of [...usageListeners]) listener();
177
+ const task = getJson(`/api/dsh-usage-dashboard/usage${suffix}`, force ? "no-store" : "default").then((res) => {
178
+ if (res.ok) {
179
+ usageCache.put(res);
180
+ for (const listener of [...usageListeners]) listener();
181
+ }
182
+ return res;
183
+ });
184
+ if (!force) {
185
+ usageInflight = task.finally(() => {
186
+ usageInflight = null;
187
+ });
188
+ return usageInflight;
168
189
  }
169
- return res;
190
+ return task;
191
+ }
192
+ var sessionUsageInflight = /* @__PURE__ */ new Map();
193
+ var sessionUsageCache = /* @__PURE__ */ new Map();
194
+ var sessionUsageListeners = /* @__PURE__ */ new Map();
195
+ var getCachedSessionUsage = (sessionId) => sessionUsageCache.get(sessionId) ?? null;
196
+ var subscribeSessionUsage = (sessionId, fn) => {
197
+ let listeners = sessionUsageListeners.get(sessionId);
198
+ if (listeners === void 0) {
199
+ listeners = /* @__PURE__ */ new Set();
200
+ sessionUsageListeners.set(sessionId, listeners);
201
+ }
202
+ listeners.add(fn);
203
+ return () => {
204
+ listeners?.delete(fn);
205
+ if (listeners?.size === 0) sessionUsageListeners.delete(sessionId);
206
+ };
207
+ };
208
+ var publishSessionUsage = (sessionId, response) => {
209
+ if (!response.ok) return;
210
+ sessionUsageCache.set(sessionId, response);
211
+ for (const listener of [...sessionUsageListeners.get(sessionId) ?? []]) listener();
212
+ };
213
+ async function fetchSessionUsage(sessionId, force = false) {
214
+ if (typeof sessionId !== "string" || sessionId === "") {
215
+ return { ok: false, error: "\u7F3A\u5C11\u4F1A\u8BDD id" };
216
+ }
217
+ if (!force) {
218
+ const inflight = sessionUsageInflight.get(sessionId);
219
+ if (inflight !== void 0) return inflight;
220
+ }
221
+ const task = getJson(`/api/dsh-usage-dashboard/session?id=${encodeURIComponent(sessionId)}`, "no-store").then((response) => {
222
+ publishSessionUsage(sessionId, response);
223
+ return response;
224
+ });
225
+ if (force) return task;
226
+ const tracked = task.finally(() => {
227
+ if (sessionUsageInflight.get(sessionId) === tracked) sessionUsageInflight.delete(sessionId);
228
+ });
229
+ sessionUsageInflight.set(sessionId, tracked);
230
+ return tracked;
231
+ }
232
+
233
+ // src/client/dashboard.tsx
234
+ var import_react4 = require("react");
235
+
236
+ // src/client/budget.ts
237
+ var BEIJING_OFFSET_MS = 8 * 60 * 60 * 1e3;
238
+ function budgetSnapshot(spentValue, budgetValue, nowMs = Date.now()) {
239
+ const spent = Number.isFinite(spentValue) ? Math.max(0, spentValue) : 0;
240
+ const budget = Number.isFinite(budgetValue) ? Math.max(0, budgetValue) : 0;
241
+ const beijingNow = new Date(nowMs + BEIJING_OFFSET_MS);
242
+ const year = beijingNow.getUTCFullYear();
243
+ const month = beijingNow.getUTCMonth();
244
+ const start = Date.UTC(year, month, 1) - BEIJING_OFFSET_MS;
245
+ const end = Date.UTC(year, month + 1, 1) - BEIJING_OFFSET_MS;
246
+ const elapsedRatio = Math.min(1, Math.max(1 / (end - start), (nowMs - start) / (end - start)));
247
+ const forecast = spent / elapsedRatio;
248
+ const ratio = budget > 0 ? spent / budget : 0;
249
+ const remaining = budget - spent;
250
+ const forecastOver = budget > 0 ? Math.max(0, forecast - budget) : 0;
251
+ const status = ratio >= 1 ? "over" : ratio >= 0.8 || forecastOver > 0 ? "risk" : "healthy";
252
+ return { spent, budget, ratio, remaining, forecast, forecastOver, status };
170
253
  }
171
254
 
172
255
  // src/client/charts.tsx
256
+ var import_react3 = require("react");
257
+
258
+ // src/client/chart-focus.ts
259
+ function nextChartFocus(current, count, key, horizontalStep = 1) {
260
+ if (count <= 0) return 0;
261
+ const index = Math.min(Math.max(0, current), count - 1);
262
+ if (key === "Home") return 0;
263
+ if (key === "End") return count - 1;
264
+ const step = key === "ArrowLeft" || key === "ArrowRight" ? Math.max(1, horizontalStep) : 1;
265
+ const direction = key === "ArrowLeft" || key === "ArrowUp" ? -1 : 1;
266
+ return Math.min(Math.max(0, index + direction * step), count - 1);
267
+ }
268
+ function lastPopulatedIndex(values) {
269
+ for (let index = values.length - 1; index >= 0; index -= 1) {
270
+ if (values[index] > 0) return index;
271
+ }
272
+ return Math.max(0, values.length - 1);
273
+ }
274
+ function nextPinnedIndex(current, tapped) {
275
+ return current === tapped ? null : tapped;
276
+ }
277
+ function isTapGesture(dx, dy, dt, moveThreshold = 10, timeThreshold = 500) {
278
+ return Math.hypot(dx, dy) <= moveThreshold && dt <= timeThreshold;
279
+ }
280
+ function stackedTotals(series, count) {
281
+ return Array.from({ length: count }, (_, i) => series.reduce((sum, s) => sum + (s.bars[i]?.value ?? 0), 0));
282
+ }
283
+ function maxOrOne(values) {
284
+ let max = 0;
285
+ for (const v of values) if (v > max) max = v;
286
+ return max > 0 ? max : 1;
287
+ }
288
+
289
+ // src/client/i18n.tsx
173
290
  var import_react2 = require("react");
291
+
292
+ // src/client/locales.ts
293
+ var NS = "dsh-usage-dashboard";
294
+ var zh = {
295
+ "meta.locale": "zh",
296
+ "nav.quota": "\u989D\u5EA6",
297
+ "widget.title": "DeepSeek \u989D\u5EA6",
298
+ "common.unknownModel": "\u672A\u77E5\u6A21\u578B",
299
+ "common.calls": "{count} \u6B21\u8C03\u7528",
300
+ "common.callsShort": "{count} \u6B21",
301
+ "common.sessions": "{count} \u4E2A\u4F1A\u8BDD",
302
+ "common.records": "{count} \u6761",
303
+ "common.rows": "{count} \u884C",
304
+ "common.input": "\u8F93\u5165",
305
+ "common.output": "\u8F93\u51FA",
306
+ "common.cacheHit": "\u7F13\u5B58\u547D\u4E2D",
307
+ "common.status": "\u72B6\u6001",
308
+ "common.available": "\u53EF\u7528",
309
+ "common.unavailable": "\u4E0D\u53EF\u7528",
310
+ "common.justNow": "\u521A\u521A",
311
+ "common.minutesAgo": "{count} \u5206\u949F\u524D",
312
+ "common.hoursAgo": "{count} \u5C0F\u65F6\u524D",
313
+ "common.daysAgo": "{count} \u5929\u524D",
314
+ "common.listSeparator": "\u3001",
315
+ "common.period": "\u3002",
316
+ "delta.noBaselineTitle": "{label}\u6CA1\u6709\u7528\u91CF\uFF0C\u65E0\u4ECE\u5BF9\u6BD4",
317
+ "delta.noBaseline": "\u65E0\u5BF9\u6BD4",
318
+ "delta.flat": "\u6301\u5E73",
319
+ "period.volume": "{tokens} tokens \xB7 {calls}",
320
+ "budget.unset": "\u8FD8\u6CA1\u8BBE\u7F6E\u6708\u5EA6\u9884\u7B97\uFF0C\u65E0\u6CD5\u63D0\u524D\u5224\u65AD\u662F\u5426\u4F1A\u8D85\u652F\u3002",
321
+ "budget.configure": "\u8BBE\u7F6E\u9884\u7B97",
322
+ "budget.over": "\u5DF2\u8D85\u9884\u7B97 \xA5{amount}",
323
+ "budget.forecastOver": "\u7167\u5F53\u524D\u901F\u5EA6\uFF0C\u9884\u8BA1\u8D85\u51FA \xA5{amount}",
324
+ "budget.forecast": "\u7167\u5F53\u524D\u901F\u5EA6\uFF0C\u9884\u8BA1\u6708\u5E95 \xA5{amount}",
325
+ "budget.title": "\u672C\u6708\u9884\u7B97",
326
+ "budget.progressLabel": "\u672C\u6708\u9884\u7B97\u4F7F\u7528\u8FDB\u5EA6",
327
+ "budget.used": "\u5DF2\u4F7F\u7528 {percent}%\uFF0C{forecast}",
328
+ "budget.remaining": "\u5269\u4F59 \xA5{amount}",
329
+ "budget.exceeded": "\u8D85\u51FA \xA5{amount}",
330
+ "pricing.title": "\u8BA1\u4EF7\u8BF4\u660E",
331
+ "pricing.currentPeak": "\u5F53\u524D \u9AD8\u5CF0\u65F6\u6BB5",
332
+ "pricing.currentOffPeak": "\u5F53\u524D \u95F2\u65F6",
333
+ "pricing.model": "\u6A21\u578B",
334
+ "pricing.period": "\u65F6\u6BB5",
335
+ "pricing.unitPrice": "\u5355\u4EF7",
336
+ "pricing.inputCacheHit": "\u8F93\u5165\xB7\u7F13\u5B58\u547D\u4E2D",
337
+ "pricing.inputCacheMiss": "\u8F93\u5165\xB7\u672A\u547D\u4E2D",
338
+ "pricing.output": "\u8F93\u51FA",
339
+ "pricing.peak": "\u9AD8\u5CF0",
340
+ "pricing.fixed": "\u56FA\u5B9A",
341
+ "pricing.offPeak": "\u95F2\u65F6",
342
+ "pricing.unit": "\u5355\u4F4D\uFF1A{currency} / \u767E\u4E07 tokens\u3002",
343
+ "pricing.splitNote": "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 {windows}\uFF0C\u5176\u4F59\u4E3A\u95F2\u65F6\uFF08\u4EF7\u683C\u51CF\u534A\uFF09\uFF1B{date} \u4E4B\u524D\u7684\u7528\u91CF\u4ECD\u6309\u65E7\u4EF7\u4F30\u7B97\u3002",
344
+ "pricing.switchNote": "{date} 00:00 \u8D77\u6539\u4E3A\u5CF0\u8C37\u5B9A\u4EF7\uFF08\u9AD8\u5CF0 {windows}\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\uFF09\uFF0C\u5C4A\u65F6\u672C\u9875\u4F1A\u6309\u6BCF\u6761\u8BB0\u5F55\u7684\u65F6\u95F4\u81EA\u52A8\u5206\u6BB5\u8BA1\u4EF7\u3002",
345
+ "pricing.unknown": "\u672A\u77E5\u6A21\u578B\u6309 deepseek-v4-pro \u8BA1\u4EF7\u3002",
346
+ "coverage.noRange": "\u6682\u65E0\u6709\u6548\u7528\u91CF\u65F6\u95F4",
347
+ "coverage.title": "\u7EDF\u8BA1\u8303\u56F4",
348
+ "coverage.gaps": "\u5B58\u5728\u7F3A\u53E3",
349
+ "coverage.complete": "\u672C\u673A\u8BFB\u53D6\u5B8C\u6574",
350
+ "coverage.brief": "\u626B\u63CF {scanned} / {listed} \u4E2A\u4F1A\u8BDD \xB7 {records} \u6761\u7528\u91CF\u8BB0\u5F55",
351
+ "coverage.scanned": "\u6210\u529F\u626B\u63CF",
352
+ "coverage.failed": "\u8BFB\u53D6\u5931\u8D25",
353
+ "coverage.skipped": "\u8DF3\u8FC7\u8BB0\u5F55",
354
+ "coverage.time": "\u8BB0\u5F55\u65F6\u95F4\uFF1A{range}",
355
+ "coverage.scope": "\u8FD9\u91CC\u53EA\u7EDF\u8BA1\u5F53\u524D\u8BBE\u5907\u4E0A\u7684 DSH \u4F1A\u8BDD\u65E5\u5FD7\uFF0C\u4E0D\u5305\u542B\u5176\u4ED6\u8BBE\u5907\u3001DeepSeek \u5E73\u53F0\u76F4\u63A5\u8C03\u7528\u6216\u5DF2\u5220\u9664\u7684\u672C\u5730\u65E5\u5FD7\u3002",
356
+ "coverage.warning": "\u672C\u6B21\u56DE\u653E\u6709\u65E5\u5FD7\u65E0\u6CD5\u8BFB\u53D6\u6216\u7528\u91CF\u8BB0\u5F55\u683C\u5F0F\u5F02\u5E38\uFF0C\u9875\u9762\u6C47\u603B\u53EF\u80FD\u4F4E\u4E8E\u5B9E\u9645\u6D88\u8017\uFF1B\u53EF\u5237\u65B0\u91CD\u8BD5\uFF0C\u5E76\u4EE5 DeepSeek \u5B98\u65B9\u5E73\u53F0\u8D26\u5355\u4E3A\u51C6\u3002",
357
+ "export.title": "\u5BFC\u51FA\u5168\u90E8\u672C\u673A DSH \u4F1A\u8BDD\u65E5\u5FD7\u6570\u636E\uFF08\u4E0D\u53D7\u7EDF\u8BA1\u5468\u671F\u5F71\u54CD\uFF09",
358
+ "export.done": "\u5DF2\u5BFC\u51FA",
359
+ "export.action": "\u5BFC\u51FA",
360
+ "export.menuLabel": "\u9009\u62E9\u5BFC\u51FA\u683C\u5F0F",
361
+ "export.dailyCsv": "\u9010\u5929 CSV",
362
+ "export.modelCsv": "\u9010\u6A21\u578B CSV",
363
+ "export.fullJson": "\u5B8C\u6574 JSON",
364
+ "export.jsonHint": "\u542B\u56FE\u8868\u4E0E\u6392\u884C",
365
+ "cache.empty": "\u8FD8\u6CA1\u6709 prompt token \u53EF\u7EDF\u8BA1\u7F13\u5B58\u547D\u4E2D\u3002",
366
+ "cache.rate": "\u7F13\u5B58\u547D\u4E2D\u7387",
367
+ "cache.saved": "\u5DF2\u8282\u7701\u8D39\u7528\uFF08\u4F30\u7B97\uFF09",
368
+ "cache.allMiss": "\u82E5\u5168\u90E8\u672A\u547D\u4E2D",
369
+ "cache.aria": "\u7F13\u5B58\u547D\u4E2D {hit}%\uFF0C\u672A\u547D\u4E2D {miss}%",
370
+ "cache.hitLegend": "\u7F13\u5B58\u547D\u4E2D {tokens}",
371
+ "cache.missLegend": "\u672A\u547D\u4E2D {tokens}\uFF08\u6309\u672A\u547D\u4E2D\u4EF7\u8BA1\u8D39\uFF09",
372
+ "cache.lowHint": "\u547D\u4E2D\u7387\u504F\u4F4E\uFF1A\u9891\u7E41\u6539\u52A8 system prompt / \u5DE5\u5177\u5B9A\u4E49\u4F1A\u8BA9\u524D\u7F00\u7F13\u5B58\u5931\u6548\uFF0C\u628A\u7A33\u5B9A\u5185\u5BB9\u653E\u5728\u5BF9\u8BDD\u6700\u524D\u9762\u80FD\u63D0\u9AD8\u547D\u4E2D\u7387\u3002",
373
+ "cache.goodHint": "\u524D\u7F00\u7F13\u5B58\u628A\u91CD\u590D\u7684 prompt \u524D\u7F00\u6309\u547D\u4E2D\u4EF7\u8BA1\u8D39\uFF0C\u662F\u8FD9\u4EFD\u8D26\u5355\u4E0A\u6700\u5927\u7684\u7701\u94B1\u6760\u6746\u3002",
374
+ "peak.empty": "\u8FD8\u6CA1\u6709\u53EF\u6309\u65F6\u6BB5\u5F52\u7C7B\u7684\u7528\u91CF\u3002",
375
+ "peak.aria": "\u9AD8\u5CF0\u65F6\u6BB5 {peak}%\uFF0C\u95F2\u65F6 {offPeak}%",
376
+ "peak.peakLegend": "\u9AD8\u5CF0 {share}% \xB7 {tokens} tokens \xB7 {calls}",
377
+ "peak.offPeakLegend": "\u95F2\u65F6 {share}% \xB7 {tokens} tokens \xB7 {calls}",
378
+ "peak.newPrice": "\u540C\u6837\u7528\u91CF\u5728\u65B0\u4EF7\u4E0B",
379
+ "peak.increase": "\u8F83\u73B0\u4EF7 +{percent}%",
380
+ "peak.shift": "\u82E5\u9AD8\u5CF0\u7528\u91CF\u90FD\u632A\u5230\u95F2\u65F6",
381
+ "peak.saving": "\u53EF\u7701 \xA5{amount}",
382
+ "peak.schedule": "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 {windows}\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\u3002",
383
+ "peak.activeHint": "\u628A\u6279\u91CF\u3001\u53EF\u5EF6\u540E\u7684\u4EFB\u52A1\u653E\u5230\u95F2\u65F6\u8DD1\uFF0C\u540C\u6837\u7684 token \u53EA\u8981\u4E00\u534A\u7684\u94B1\u3002",
384
+ "peak.futureHint": "\u65B0\u4EF7 {date} 00:00 \u751F\u6548\uFF1B\u4E0A\u9762\u4E24\u4E2A\u6570\u5B57\u662F\u6309\u4F60\u5DF2\u6709\u7684\u5168\u90E8\u7528\u91CF\u91CD\u7B97\u7684\u3002",
385
+ "sessions.empty": "\u8FD8\u6CA1\u6709\u4EA7\u751F\u8D39\u7528\u7684\u4F1A\u8BDD\u3002",
386
+ "sessions.detail": "{tokens} tokens \xB7 {calls} \xB7 {ago}",
387
+ "sessions.share": "\u5360 {percent}%",
388
+ "sessions.more": "\u5171 {count} \u4E2A\u4F1A\u8BDD\u6709\u7528\u91CF\uFF0C\u4E0A\u9762\u662F\u6700\u8D35\u7684 {shown} \u4E2A\u3002",
389
+ "sessions.fallbackTitle": "\u4F1A\u8BDD {id}",
390
+ "models.empty": "\u8FD8\u6CA1\u6709\u53EF\u5F52\u7C7B\u7684\u6A21\u578B\u7528\u91CF\u3002",
391
+ "models.shareAria": "{name} \u5360\u603B\u8D39\u7528 {percent}%",
392
+ "models.detail": "{tokens} tokens \xB7 {calls} \xB7 \u8F93\u5165 {input} / \u8F93\u51FA {output} / \u7F13\u5B58 {cache}",
393
+ "models.all": "\u5168\u90E8\u6A21\u578B",
394
+ "models.selected": "\u6A21\u578B \xD7{count}",
395
+ "models.filterTitle": "\u7B5B\u9009\u56FE\u8868\u4E2D\u663E\u793A\u7684\u6A21\u578B",
396
+ "models.filterLabel": "\u7B5B\u9009\u56FE\u8868\u6A21\u578B",
397
+ "window.year": "\u8FD1 1 \u5E74",
398
+ "window.days": "\u8FD1 {days} \u5929",
399
+ "window.groupLabel": "\u7EDF\u8BA1\u5468\u671F",
400
+ "window.viewTitle": "\u67E5\u770B{window}\u7684\u7528\u91CF\u4E0E\u6392\u884C",
401
+ "window.yearButton": "1 \u5E74",
402
+ "window.daysButton": "{days} \u5929",
403
+ "metric.tokens": "tokens",
404
+ "metric.usage": "\u7528\u91CF",
405
+ "metric.cost": "\u8D39\u7528",
406
+ "metric.calls": "\u8C03\u7528",
407
+ "metric.groupLabel": "\u56FE\u8868\u6307\u6807",
408
+ "metric.compareTitle": "\u6309{metric}\u6BD4\u8F83",
409
+ "chart.dailyTitle": "{window} \xB7 \u9010\u5929{metric}",
410
+ "chart.hourlyTitle": "{window} \xB7 {metric}\u6309\u5C0F\u65F6\u5206\u5E03\uFF080\u201323 \u70B9\uFF09",
411
+ "chart.peak": "\u5CF0\u503C {value}",
412
+ "chart.dimensionLabel": "\u5207\u6362\u56FE\u8868\u7EF4\u5EA6",
413
+ "chart.daily": "\u9010\u5929",
414
+ "chart.hourly": "\u9010\u5C0F\u65F6",
415
+ "chart.noWindowData": "{window}\u6CA1\u6709\u7528\u91CF\u8BB0\u5F55\uFF0C\u6362\u4E2A\u66F4\u957F\u5468\u671F\u770B\u770B\u3002",
416
+ "chart.heatmapTitle": "\u8FD1\u4E00\u5E74 \xB7 \u6BCF\u65E5\u7528\u91CF\u70ED\u529B\u56FE",
417
+ "chart.empty": "\u8FD8\u6CA1\u6709\u7528\u91CF\u8BB0\u5F55\u3002\u5728 DSH \u91CC\u8DD1\u4E00\u8F6E\u5BF9\u8BDD\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u9010\u5929 / \u9010\u5C0F\u65F6\u7EDF\u8BA1\u4E0E\u8D39\u7528\u4F30\u7B97\u3002",
418
+ "chart.hourPoint": "{hour} \u70B9",
419
+ "chart.tooltip": "{head} \xB7 {tokens} tokens \xB7 \xA5{cost} \xB7 {calls}",
420
+ "chart.less": "\u5C11",
421
+ "chart.more": "\u591A",
422
+ "chart.month1": "1\u6708",
423
+ "chart.month2": "2\u6708",
424
+ "chart.month3": "3\u6708",
425
+ "chart.month4": "4\u6708",
426
+ "chart.month5": "5\u6708",
427
+ "chart.month6": "6\u6708",
428
+ "chart.month7": "7\u6708",
429
+ "chart.month8": "8\u6708",
430
+ "chart.month9": "9\u6708",
431
+ "chart.month10": "10\u6708",
432
+ "chart.month11": "11\u6708",
433
+ "chart.month12": "12\u6708",
434
+ "number.tenThousand": "{value}\u4E07",
435
+ "number.hundredMillion": "{value}\u4EBF",
436
+ "fresh.none": "\u5C1A\u65E0\u6210\u529F\u8BB0\u5F55",
437
+ "fresh.justNow": "\u521A\u521A\u66F4\u65B0",
438
+ "fresh.minutes": "\u66F4\u65B0\u4E8E {count} \u5206\u949F\u524D",
439
+ "fresh.hours": "\u66F4\u65B0\u4E8E {count} \u5C0F\u65F6\u524D",
440
+ "fresh.days": "\u66F4\u65B0\u4E8E {count} \u5929\u524D",
441
+ "sync.first": "\u9996\u6B21\u540C\u6B65\u4E2D",
442
+ "sync.syncing": "\u540C\u6B65\u4E2D \xB7 \u5F53\u524D\u6570\u636E{age}",
443
+ "sync.fresh": "\u5DF2\u540C\u6B65 \xB7 {age}",
444
+ "sync.cached": "\u7F13\u5B58\u6570\u636E \xB7 {age}",
445
+ "sync.fallback": "\u7F13\u5B58\u56DE\u9000 \xB7 {age}",
446
+ "sync.error": "\u540C\u6B65\u5931\u8D25 \xB7 \u6682\u65E0\u53EF\u663E\u793A\u7684\u7528\u91CF",
447
+ "status.neverSynced": "\u8FD8\u6CA1\u6709\u6210\u529F\u540C\u6B65\u8FC7\u7528\u91CF\u6570\u636E",
448
+ "status.usageTime": "\u7528\u91CF\u6570\u636E\u65F6\u95F4\uFF1A{time}",
449
+ "status.forceRefresh": "\u5F3A\u5236\u5237\u65B0\uFF08\u7ED5\u8FC7\u7F13\u5B58\uFF09",
450
+ "status.refreshing": "\u5237\u65B0\u4E2D",
451
+ "status.refresh": "\u5237\u65B0",
452
+ "error.balanceLoad": "\u4F59\u989D\u52A0\u8F7D\u5931\u8D25",
453
+ "error.usageLoad": "\u7528\u91CF\u52A0\u8F7D\u5931\u8D25",
454
+ "error.refreshCached": "\u5237\u65B0\u5931\u8D25\uFF0C\u6B63\u5728\u5C55\u793A\u7F13\u5B58\u6570\u636E\uFF08{error}\uFF09",
455
+ "error.balanceRefreshCached": "\u4F59\u989D\u5237\u65B0\u5931\u8D25\uFF0C\u6B63\u5728\u5C55\u793A\u7F13\u5B58\u4F59\u989D\uFF08{error}\uFF09",
456
+ "error.usageRefreshCached": "\u7528\u91CF\u5237\u65B0\u5931\u8D25\uFF0C\u6B63\u5728\u5C55\u793A\u7F13\u5B58\u7528\u91CF\uFF08{error}\uFF09",
457
+ "error.query": "\u67E5\u8BE2\u5931\u8D25",
458
+ "error.credentialsUnavailable": "\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528",
459
+ "error.credentialRead": "\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A{detail}",
460
+ "error.noApiKey": "\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09",
461
+ "error.balanceRequest": "\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A{detail}",
462
+ "error.balanceHttp": "\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP {status}\uFF09",
463
+ "error.balanceParse": "\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25",
464
+ "error.persistenceUnavailable": "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528",
465
+ "error.sessionList": "\u8BFB\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A{detail}",
466
+ "error.sessionLogRead": "\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A{detail}",
467
+ "error.missingSessionId": "\u7F3A\u5C11\u4F1A\u8BDD id",
468
+ "alert.lowTitle": "\u4F59\u989D {balance} {currency} \u5DF2\u4F4E\u4E8E\u9884\u8B66\u7EBF {threshold}",
469
+ "alert.runway": "\uFF0C\u6309\u8FD1\u671F\u7528\u91CF\u4F30\u8BA1\u8FD8\u80FD\u6491 {days}",
470
+ "alert.topUp": "\u53BB\u5145\u503C",
471
+ "balance.title": "\u8D26\u6237\u4F59\u989D",
472
+ "balance.remaining": "\u5269\u4F59\u4F59\u989D",
473
+ "balance.breakdownTitle": "\u67E5\u770B\u5145\u503C\u4E0E\u8D60\u9001\u989D\u5EA6\u660E\u7EC6",
474
+ "balance.toppedUp": "\u5145\u503C\u989D\u5EA6 {amount}",
475
+ "balance.granted": "\u8D60\u9001\u989D\u5EA6 {amount}",
476
+ "balance.runway": "\u9884\u8BA1\u53EF\u7528",
477
+ "balance.runwayBreakdownTitle": "\u67E5\u770B\u9884\u8BA1\u53EF\u7528\u5929\u6570\u8BF4\u660E",
478
+ "balance.days": "{count} \u5929",
479
+ "balance.overYear": "> 365 \u5929",
480
+ "balance.noRunway": "\u8FD1 {days} \u5929\u6CA1\u6709\u7528\u91CF\uFF0C\u65E0\u6CD5\u4F30\u7B97",
481
+ "balance.runwayTitle": "\u6309\u8FD1 {days} \u5929\u65E5\u5747 \xA5{cost} \u4F30\u7B97\uFF08\u542B\u65E0\u7528\u91CF\u7684\u65E5\u5B50\uFF1B\u4ECA\u5929\u5C1A\u672A\u8FC7\u5B8C\uFF09",
482
+ "balance.empty": "\u8BFB\u4E0D\u5230\u4F59\u989D\u3002\u8BF7\u786E\u8BA4\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u91CC\u5DF2\u586B DEEPSEEK_API_KEY\uFF0C\u7136\u540E\u70B9\u53F3\u4E0A\u89D2\u5237\u65B0\u3002",
483
+ "sessionUsage.title": "\u5F53\u524D\u4F1A\u8BDD\u6D88\u8017\uFF08\u8D39\u7528\u4E3A\u4F30\u7B97\uFF09",
484
+ "sessionUsage.cost": "\u672C\u4F1A\u8BDD\u8D39\u7528",
485
+ "sessionUsage.tokens": "\u672C\u4F1A\u8BDD tokens",
486
+ "sessionUsage.calls": "\u672C\u4F1A\u8BDD\u8C03\u7528",
487
+ "sessionUsage.range": "\u6D3B\u8DC3\u65F6\u95F4 {range}",
488
+ "sessionUsage.empty": "\u8FD9\u4E2A\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u4EA7\u751F\u7528\u91CF\u3002\u53D1\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u8FD9\u4E2A\u4F1A\u8BDD\u4E13\u5C5E\u7684\u8D39\u7528\u4E0E\u6A21\u578B\u660E\u7EC6\u3002",
489
+ "overview.title": "\u6D88\u8017\u6982\u89C8\uFF08\u8D39\u7528\u4E3A\u4F30\u7B97\uFF09",
490
+ "overview.today": "\u4ECA\u65E5",
491
+ "overview.month": "\u672C\u6708",
492
+ "overview.total": "\u7D2F\u8BA1",
493
+ "overview.vsYesterday": "\u8F83\u6628\u65E5",
494
+ "overview.vsLastMonth": "\u8F83\u4E0A\u6708\u540C\u671F",
495
+ "overview.empty": "\u8FD8\u6CA1\u6709\u53EF\u7EDF\u8BA1\u7684\u6D88\u8017\u3002",
496
+ "usage.trendTitle": "\u7528\u91CF\u8D8B\u52BF\uFF08tokens\uFF09",
497
+ "usage.windowInput": "{window}\u8F93\u5165",
498
+ "usage.modelCalls": "\u6A21\u578B\u8C03\u7528",
499
+ "heatmap.title": "\u7528\u91CF\u70ED\u529B\u56FE",
500
+ "peak.title": "\u9AD8\u5CF0 / \u95F2\u65F6\u5206\u5E03",
501
+ "cache.title": "\u7F13\u5B58\u547D\u4E2D\u4E0E\u8282\u7701",
502
+ "models.title": "\u6A21\u578B\u6210\u672C\u6392\u884C \xB7 {window}",
503
+ "sessions.title": "\u4F1A\u8BDD\u6210\u672C\u6392\u884C \xB7 {window}",
504
+ "links.title": "\u5FEB\u6377\u5165\u53E3",
505
+ "links.usage": "\u67E5\u770B\u989D\u5EA6 / \u7528\u91CF",
506
+ "links.apiKey": "\u751F\u6210 API Key",
507
+ "links.status": "\u670D\u52A1\u72B6\u6001",
508
+ "settings.title": "\u8BBE\u7F6E",
509
+ "settings.widget": "\u5728\u5176\u4ED6\u9875\u9762\u663E\u793A\u53F3\u4E0B\u89D2\u60AC\u6D6E\u989D\u5EA6\u7A97\u53E3",
510
+ "settings.widgetHint": "\u989D\u5EA6\u9875\u4F1A\u81EA\u52A8\u9690\u85CF\u6D6E\u7A97\uFF1B\u5176\u4ED6\u9875\u9762\u662F\u5426\u5C55\u793A\u7531\u4E0B\u65B9\u9009\u62E9\u51B3\u5B9A\u3002",
511
+ "settings.widgetTabs": "\u5C55\u793A\u6D6E\u7A97\u7684\u9875\u9762",
512
+ "settings.widgetTabsHint": "\u9875\u9762\u5217\u8868\u6765\u81EA DSH \u7684\u5B9E\u65F6 Tab \u6CE8\u518C\u8868\uFF1B\u5176\u4ED6\u63D2\u4EF6\u65B0\u589E\u7684 Tab \u4E5F\u4F1A\u81EA\u52A8\u51FA\u73B0\u5728\u8FD9\u91CC\u3002",
513
+ "settings.widgetTabsEmpty": "\u5F53\u524D\u6CA1\u6709\u53EF\u914D\u7F6E\u7684\u4F1A\u8BDD\u9875\u9762\u3002",
514
+ "settings.lowBalance": "\u4F59\u989D\u9884\u8B66\u7EBF",
515
+ "settings.lowBalanceOn": "\u4F59\u989D\u4F4E\u4E8E {amount} {currency} \u65F6\uFF0C\u8FD9\u91CC\u548C\u60AC\u6D6E\u7A97\u90FD\u4F1A\u8F6C\u4E3A\u8B66\u793A\u8272\u3002\u586B 0 \u5173\u95ED\u3002",
516
+ "settings.lowBalanceOff": "\u5DF2\u5173\u95ED\u4F59\u989D\u9884\u8B66\u3002\u586B\u4E00\u4E2A\u5927\u4E8E 0 \u7684\u6570\u5F00\u542F\u3002",
517
+ "settings.monthlyBudget": "\u6708\u5EA6\u9884\u7B97",
518
+ "settings.monthlyBudgetOn": "\u6309\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u6708\u8DDF\u8E2A {amount} CNY \u9884\u7B97\uFF0C\u5E76\u5728\u6D88\u8017\u6982\u89C8\u9884\u6D4B\u6708\u5E95\u82B1\u8D39\u3002\u586B 0 \u5173\u95ED\u3002",
519
+ "settings.monthlyBudgetOff": "\u586B\u4E00\u4E2A\u5927\u4E8E 0 \u7684\u91D1\u989D\uFF0C\u6D88\u8017\u6982\u89C8\u4F1A\u663E\u793A\u8FDB\u5EA6\u4E0E\u6708\u5E95\u9884\u6D4B\u3002",
520
+ "widget.updatedNow": "\u521A\u521A\u66F4\u65B0",
521
+ "widget.stale": "{minutes} \u5206\u949F\u524D\u7684\u6570\u636E\uFF0C\u70B9 \u21BB \u66F4\u65B0",
522
+ "widget.loading": "\u67E5\u8BE2\u4E2D\u2026",
523
+ "widget.lowTitle": "\u4F59\u989D\u4F4E\u4E8E\u9884\u8B66\u7EBF {amount}",
524
+ "widget.todayCostTitle": "\u4ECA\u65E5\u6D88\u8017\uFF08\u4F30\u7B97\uFF09\xB7 {age}",
525
+ "widget.todayCost": "\u4ECA\u65E5\u6D88\u8017",
526
+ "widget.sessionCostTitle": "\u5F53\u524D\u4F1A\u8BDD\u7D2F\u8BA1\u6D88\u8017\uFF08\u4F30\u7B97\uFF09",
527
+ "widget.sessionCost": "\u5F53\u524D\u4F1A\u8BDD",
528
+ "widget.noBalance": "\u6682\u65E0\u4F59\u989D\u6570\u636E",
529
+ "widget.dragCard": "\u62D6\u52A8\u5230\u56DB\u4E2A\u89D2\u843D",
530
+ "widget.dragGrip": "\u62D6\u52A8\u4EE5\u79FB\u52A8",
531
+ "widget.refresh": "\u5237\u65B0\u4F59\u989D\u4E0E\u4ECA\u65E5\u7528\u91CF",
532
+ "widget.expand": "\u5C55\u5F00",
533
+ "widget.collapse": "\u6536\u8D77",
534
+ "widget.expandAria": "\u5C55\u5F00\u989D\u5EA6\u7A97\u53E3",
535
+ "widget.collapseAria": "\u6536\u8D77\u989D\u5EA6\u7A97\u53E3",
536
+ "messageCost.label": "\u672C\u8F6E\u8D39\u7528",
537
+ "messageCost.tooltip": "\u7531 dsh-usage-dashboard \u63D0\u4F9B \xB7 \u672C\u8F6E\u5BF9\u8BDD\u8D39\u7528\u4E3A\u4F30\u7B97\u503C",
538
+ "messageCost.aria": "{amount}\uFF0C\u672C\u8F6E\u5BF9\u8BDD\u4F30\u7B97\u8D39\u7528\uFF0C\u7531 dsh-usage-dashboard \u63D0\u4F9B",
539
+ "boundary.title": "\u989D\u5EA6\u9762\u677F\u51FA\u9519\u4E86",
540
+ "boundary.body": "\u6E32\u67D3\u65F6\u629B\u4E86\u5F02\u5E38\uFF0C\u901A\u5E38\u662F\u6D4F\u89C8\u5668\u91CC\u5B58\u7740\u65E7\u7248\u672C\u7684\u7F13\u5B58\u6570\u636E\u3002\u6E05\u6389\u672C\u63D2\u4EF6\u7684\u672C\u5730\u6570\u636E\u518D\u91CD\u65B0\u52A0\u8F7D\u5C31\u80FD\u6062\u590D\u3002",
541
+ "boundary.reset": "\u6E05\u9664\u672C\u5730\u6570\u636E\u5E76\u91CD\u65B0\u52A0\u8F7D"
542
+ };
543
+ var en = {
544
+ "meta.locale": "en",
545
+ "nav.quota": "Usage",
546
+ "widget.title": "DeepSeek Usage",
547
+ "common.unknownModel": "Unknown model",
548
+ "common.calls": "{count} calls",
549
+ "common.callsShort": "{count} calls",
550
+ "common.sessions": "{count} sessions",
551
+ "common.records": "{count} records",
552
+ "common.rows": "{count} rows",
553
+ "common.input": "Input",
554
+ "common.output": "Output",
555
+ "common.cacheHit": "Cache hit",
556
+ "common.status": "Status",
557
+ "common.available": "Available",
558
+ "common.unavailable": "Unavailable",
559
+ "common.justNow": "Just now",
560
+ "common.minutesAgo": "{count}m ago",
561
+ "common.hoursAgo": "{count}h ago",
562
+ "common.daysAgo": "{count}d ago",
563
+ "common.listSeparator": ", ",
564
+ "common.period": ".",
565
+ "delta.noBaselineTitle": "No usage in {label}, so there is no baseline",
566
+ "delta.noBaseline": "No baseline",
567
+ "delta.flat": "No change",
568
+ "period.volume": "{tokens} tokens \xB7 {calls}",
569
+ "budget.unset": "Set a monthly budget to see whether current spending is on track.",
570
+ "budget.configure": "Set budget",
571
+ "budget.over": "Over budget by \xA5{amount}",
572
+ "budget.forecastOver": "At this pace, \xA5{amount} over by month-end",
573
+ "budget.forecast": "At this pace, \xA5{amount} by month-end",
574
+ "budget.title": "Monthly budget",
575
+ "budget.progressLabel": "Monthly budget usage",
576
+ "budget.used": "{percent}% used. {forecast}",
577
+ "budget.remaining": "\xA5{amount} remaining",
578
+ "budget.exceeded": "\xA5{amount} over",
579
+ "pricing.title": "Pricing details",
580
+ "pricing.currentPeak": "Peak now",
581
+ "pricing.currentOffPeak": "Off-peak now",
582
+ "pricing.model": "Model",
583
+ "pricing.period": "Period",
584
+ "pricing.unitPrice": "Rate",
585
+ "pricing.inputCacheHit": "Input \xB7 cache hit",
586
+ "pricing.inputCacheMiss": "Input \xB7 cache miss",
587
+ "pricing.output": "Output",
588
+ "pricing.peak": "Peak",
589
+ "pricing.fixed": "Fixed",
590
+ "pricing.offPeak": "Off-peak",
591
+ "pricing.unit": "Unit: {currency} / million tokens. ",
592
+ "pricing.splitNote": "Peak periods are {windows} Beijing time; off-peak rates are half price. Usage before {date} is still estimated at the old rates. ",
593
+ "pricing.switchNote": "Time-of-use pricing starts at 00:00 on {date} (peak: {windows}; off-peak is half price). This page will price each record by its timestamp. ",
594
+ "pricing.unknown": "Unknown models use deepseek-v4-pro rates.",
595
+ "coverage.noRange": "No valid usage timestamps",
596
+ "coverage.title": "Coverage",
597
+ "coverage.gaps": "Gaps found",
598
+ "coverage.complete": "Local logs complete",
599
+ "coverage.brief": "Scanned {scanned} / {listed} sessions \xB7 {records} usage records",
600
+ "coverage.scanned": "Scanned",
601
+ "coverage.failed": "Read failures",
602
+ "coverage.skipped": "Skipped records",
603
+ "coverage.time": "Record range: {range}",
604
+ "coverage.scope": "Only DSH session logs on this device are included. Other devices, direct DeepSeek Platform calls, and deleted local logs are excluded.",
605
+ "coverage.warning": "Some logs could not be read or contained invalid usage records, so totals may be lower than actual spend. Refresh to retry and use the DeepSeek Platform bill as the source of truth.",
606
+ "export.title": "Export all local DSH session-log data (independent of the selected range)",
607
+ "export.done": "Exported",
608
+ "export.action": "Export",
609
+ "export.menuLabel": "Choose export format",
610
+ "export.dailyCsv": "Daily CSV",
611
+ "export.modelCsv": "Model CSV",
612
+ "export.fullJson": "Full JSON",
613
+ "export.jsonHint": "Charts and rankings included",
614
+ "cache.empty": "No prompt tokens are available for cache analysis yet.",
615
+ "cache.rate": "Cache hit rate",
616
+ "cache.saved": "Estimated savings",
617
+ "cache.allMiss": "Cost with no hits",
618
+ "cache.aria": "{hit}% cache hits, {miss}% misses",
619
+ "cache.hitLegend": "Cache hits {tokens}",
620
+ "cache.missLegend": "Misses {tokens} (charged at miss rate)",
621
+ "cache.lowHint": "Low hit rate: frequent changes to the system prompt or tool definitions invalidate prefix caching. Put stable content first to improve reuse.",
622
+ "cache.goodHint": "Prefix caching charges repeated prompt prefixes at the hit rate, making it the largest saving lever on this bill.",
623
+ "peak.empty": "No usage can be classified by pricing period yet.",
624
+ "peak.aria": "{peak}% peak, {offPeak}% off-peak",
625
+ "peak.peakLegend": "Peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
626
+ "peak.offPeakLegend": "Off-peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
627
+ "peak.newPrice": "Same usage at new rates",
628
+ "peak.increase": "+{percent}% vs current rates",
629
+ "peak.shift": "If all peak usage moved off-peak",
630
+ "peak.saving": "Save \xA5{amount}",
631
+ "peak.schedule": "Peak periods are {windows} Beijing time; off-peak rates are half price. ",
632
+ "peak.activeHint": "Run batch or delay-tolerant work off-peak to pay half as much for the same tokens.",
633
+ "peak.futureHint": "New rates start at 00:00 on {date}; both figures recalculate all existing usage.",
634
+ "sessions.empty": "No sessions have incurred cost yet.",
635
+ "sessions.detail": "{tokens} tokens \xB7 {calls} \xB7 {ago}",
636
+ "sessions.share": "{percent}% of total",
637
+ "sessions.more": "{count} sessions have usage; the {shown} most expensive are shown above.",
638
+ "sessions.fallbackTitle": "Session {id}",
639
+ "models.empty": "No model usage can be classified yet.",
640
+ "models.shareAria": "{name} accounts for {percent}% of total cost",
641
+ "models.detail": "{tokens} tokens \xB7 {calls} \xB7 input {input} / output {output} / cache {cache}",
642
+ "models.all": "All models",
643
+ "models.selected": "{count} models",
644
+ "models.filterTitle": "Filter models shown in the chart",
645
+ "models.filterLabel": "Filter chart models",
646
+ "window.year": "Last year",
647
+ "window.days": "Last {days} days",
648
+ "window.groupLabel": "Date range",
649
+ "window.viewTitle": "View usage and rankings for {window}",
650
+ "window.yearButton": "1 year",
651
+ "window.daysButton": "{days} days",
652
+ "metric.tokens": "tokens",
653
+ "metric.usage": "usage",
654
+ "metric.cost": "cost",
655
+ "metric.calls": "calls",
656
+ "metric.groupLabel": "Chart metric",
657
+ "metric.compareTitle": "Compare by {metric}",
658
+ "chart.dailyTitle": "{window} \xB7 daily {metric}",
659
+ "chart.hourlyTitle": "{window} \xB7 {metric} by hour (00\u201323)",
660
+ "chart.peak": "Peak {value}",
661
+ "chart.dimensionLabel": "Chart interval",
662
+ "chart.daily": "Daily",
663
+ "chart.hourly": "Hourly",
664
+ "chart.noWindowData": "No usage in {window}. Try a longer range.",
665
+ "chart.heatmapTitle": "Last year \xB7 daily usage heatmap",
666
+ "chart.empty": "No usage yet. Run a conversation in DSH to see daily and hourly totals with cost estimates.",
667
+ "chart.hourPoint": "{hour}:00",
668
+ "chart.tooltip": "{head} \xB7 {tokens} tokens \xB7 \xA5{cost} \xB7 {calls}",
669
+ "chart.less": "Less",
670
+ "chart.more": "More",
671
+ "chart.month1": "Jan",
672
+ "chart.month2": "Feb",
673
+ "chart.month3": "Mar",
674
+ "chart.month4": "Apr",
675
+ "chart.month5": "May",
676
+ "chart.month6": "Jun",
677
+ "chart.month7": "Jul",
678
+ "chart.month8": "Aug",
679
+ "chart.month9": "Sep",
680
+ "chart.month10": "Oct",
681
+ "chart.month11": "Nov",
682
+ "chart.month12": "Dec",
683
+ "number.tenThousand": "{value}0K",
684
+ "number.hundredMillion": "{value}00M",
685
+ "fresh.none": "No successful sync yet",
686
+ "fresh.justNow": "Updated just now",
687
+ "fresh.minutes": "Updated {count}m ago",
688
+ "fresh.hours": "Updated {count}h ago",
689
+ "fresh.days": "Updated {count}d ago",
690
+ "sync.first": "Syncing for the first time",
691
+ "sync.syncing": "Syncing \xB7 current data {age}",
692
+ "sync.fresh": "Synced \xB7 {age}",
693
+ "sync.cached": "Cached data \xB7 {age}",
694
+ "sync.fallback": "Using cache \xB7 {age}",
695
+ "sync.error": "Sync failed \xB7 no usage to display",
696
+ "status.neverSynced": "Usage has not synced successfully yet",
697
+ "status.usageTime": "Usage data timestamp: {time}",
698
+ "status.forceRefresh": "Force refresh (bypass cache)",
699
+ "status.refreshing": "Refreshing",
700
+ "status.refresh": "Refresh",
701
+ "error.balanceLoad": "Balance failed to load",
702
+ "error.usageLoad": "Usage failed to load",
703
+ "error.refreshCached": "Refresh failed; showing cached data ({error})",
704
+ "error.balanceRefreshCached": "Balance refresh failed; showing cached balance ({error})",
705
+ "error.usageRefreshCached": "Usage refresh failed; showing cached usage ({error})",
706
+ "error.query": "Request failed",
707
+ "error.credentialsUnavailable": "Credential service is unavailable",
708
+ "error.credentialRead": "Could not read the API key: {detail}",
709
+ "error.noApiKey": "DEEPSEEK_API_KEY is not configured (add it under Settings \u2192 Models)",
710
+ "error.balanceRequest": "Balance request failed: {detail}",
711
+ "error.balanceHttp": "Balance endpoint returned HTTP {status}",
712
+ "error.balanceParse": "Could not parse the balance response",
713
+ "error.persistenceUnavailable": "Session persistence is unavailable",
714
+ "error.sessionList": "Could not read the session list: {detail}",
715
+ "error.sessionLogRead": "Could not read the session log: {detail}",
716
+ "error.missingSessionId": "Missing session id",
717
+ "alert.lowTitle": "Balance {balance} {currency} is below the {threshold} warning threshold",
718
+ "alert.runway": "; about {days} left at recent usage",
719
+ "alert.topUp": "Top up",
720
+ "balance.title": "Account balance",
721
+ "balance.remaining": "Remaining balance",
722
+ "balance.breakdownTitle": "View topped-up and granted balance details",
723
+ "balance.toppedUp": "Topped up {amount}",
724
+ "balance.granted": "Granted {amount}",
725
+ "balance.runway": "Estimated runway",
726
+ "balance.runwayBreakdownTitle": "View estimated runway details",
727
+ "balance.days": "{count} days",
728
+ "balance.overYear": "> 365 days",
729
+ "balance.noRunway": "No usage in the last {days} days, so runway cannot be estimated",
730
+ "balance.runwayTitle": "Based on \xA5{cost} average daily spend over the last {days} days (including idle days; today is incomplete)",
731
+ "balance.empty": "Balance is unavailable. Add DEEPSEEK_API_KEY under Settings \u2192 Models, then refresh.",
732
+ "sessionUsage.title": "Current session spend (estimated)",
733
+ "sessionUsage.cost": "This session",
734
+ "sessionUsage.tokens": "Session tokens",
735
+ "sessionUsage.calls": "Session calls",
736
+ "sessionUsage.range": "Active {range}",
737
+ "sessionUsage.empty": "No usage in this session yet. Send a message and this session\u2019s own cost and model breakdown will show up here.",
738
+ "overview.title": "Spend overview (estimated)",
739
+ "overview.today": "Today",
740
+ "overview.month": "This month",
741
+ "overview.total": "All time",
742
+ "overview.vsYesterday": "vs yesterday",
743
+ "overview.vsLastMonth": "vs same period last month",
744
+ "overview.empty": "No spend to summarize yet.",
745
+ "usage.trendTitle": "Usage trend (tokens)",
746
+ "usage.windowInput": "{window} input",
747
+ "usage.modelCalls": "Model calls",
748
+ "heatmap.title": "Usage heatmap",
749
+ "peak.title": "Peak / off-peak distribution",
750
+ "cache.title": "Cache hits and savings",
751
+ "models.title": "Model cost ranking \xB7 {window}",
752
+ "sessions.title": "Session cost ranking \xB7 {window}",
753
+ "links.title": "Quick Links",
754
+ "links.usage": "View balance / usage",
755
+ "links.apiKey": "Create API key",
756
+ "links.status": "Service status",
757
+ "settings.title": "Settings",
758
+ "settings.widget": "Show the floating balance widget on other views",
759
+ "settings.widgetHint": "The widget always hides on Usage; choose its other views below.",
760
+ "settings.widgetTabs": "Show widget on",
761
+ "settings.widgetTabsHint": "This live list comes from DSH\u2019s tab registry, including tabs added by other plugins.",
762
+ "settings.widgetTabsEmpty": "There are no configurable conversation views right now.",
763
+ "settings.lowBalance": "Balance warning",
764
+ "settings.lowBalanceOn": "This page and the widget turn amber below {amount} {currency}. Enter 0 to disable.",
765
+ "settings.lowBalanceOff": "Balance warnings are off. Enter an amount above 0 to enable them.",
766
+ "settings.monthlyBudget": "Monthly budget",
767
+ "settings.monthlyBudgetOn": "Track a {amount} CNY budget by Beijing calendar month and forecast month-end spend. Enter 0 to disable.",
768
+ "settings.monthlyBudgetOff": "Enter an amount above 0 to show progress and a month-end forecast.",
769
+ "widget.updatedNow": "Updated just now",
770
+ "widget.stale": "Data from {minutes}m ago; press \u21BB to update",
771
+ "widget.loading": "Loading\u2026",
772
+ "widget.lowTitle": "Balance below warning threshold {amount}",
773
+ "widget.todayCostTitle": "Today\u2019s spend (estimated) \xB7 {age}",
774
+ "widget.todayCost": "Today\u2019s spend",
775
+ "widget.sessionCostTitle": "Estimated spend for the current session",
776
+ "widget.sessionCost": "Current session",
777
+ "widget.noBalance": "No balance data",
778
+ "widget.dragCard": "Drag to any corner",
779
+ "widget.dragGrip": "Drag to move",
780
+ "widget.refresh": "Refresh balance and today\u2019s usage",
781
+ "widget.expand": "Expand",
782
+ "widget.collapse": "Collapse",
783
+ "widget.expandAria": "Expand balance widget",
784
+ "widget.collapseAria": "Collapse balance widget",
785
+ "messageCost.label": "Turn cost",
786
+ "messageCost.tooltip": "Provided by dsh-usage-dashboard \xB7 estimated cost for this turn",
787
+ "messageCost.aria": "{amount}, estimated cost for this turn, provided by dsh-usage-dashboard",
788
+ "boundary.title": "The usage dashboard crashed",
789
+ "boundary.body": "Rendering failed, usually because this browser still has cached data from an older version. Clear this plugin\u2019s local data and reload to recover.",
790
+ "boundary.reset": "Clear local data and reload"
791
+ };
792
+
793
+ // src/client/i18n.tsx
174
794
  var import_jsx_runtime2 = require("react/jsx-runtime");
795
+ var fallbackT = (key, params) => {
796
+ const template = zh[key];
797
+ if (params === void 0) return template;
798
+ return template.replace(/\{(\w+)\}/g, (match, name2) => name2 in params ? String(params[name2]) : match);
799
+ };
800
+ var TranslationContext = (0, import_react2.createContext)(fallbackT);
801
+ function LocaleProvider(props) {
802
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TranslationContext.Provider, { value: props.t, children: props.children });
803
+ }
804
+ function useI18n() {
805
+ const t = (0, import_react2.useContext)(TranslationContext);
806
+ return { t, locale: t("meta.locale") === "en" ? "en" : "zh" };
807
+ }
808
+ function localizeApiError(message, t, fallback) {
809
+ if (message === void 0 || message === "") return t(fallback);
810
+ if (message === "\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528") return t("error.credentialsUnavailable");
811
+ if (message === "\u67E5\u8BE2\u5931\u8D25") return t("error.query");
812
+ if (message === "\u4F59\u989D\u52A0\u8F7D\u5931\u8D25") return t("error.balanceLoad");
813
+ if (message === "\u7528\u91CF\u52A0\u8F7D\u5931\u8D25") return t("error.usageLoad");
814
+ if (message === "\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09") return t("error.noApiKey");
815
+ if (message === "\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25") return t("error.balanceParse");
816
+ if (message === "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528") return t("error.persistenceUnavailable");
817
+ if (message === "\u7F3A\u5C11\u4F1A\u8BDD id") return t("error.missingSessionId");
818
+ const mappings = [
819
+ [/^读取 API Key 失败:(.*)$/, "error.credentialRead", "detail"],
820
+ [/^请求余额接口失败:(.*)$/, "error.balanceRequest", "detail"],
821
+ [/^余额接口返回错误(HTTP (\d+))$/, "error.balanceHttp", "status"],
822
+ [/^读取会话列表失败:(.*)$/, "error.sessionList", "detail"],
823
+ [/^读取会话日志失败:(.*)$/, "error.sessionLogRead", "detail"]
824
+ ];
825
+ for (const [pattern, key, field] of mappings) {
826
+ const match = message.match(pattern);
827
+ if (match !== null) return t(key, { [field]: match[1] });
828
+ }
829
+ return message;
830
+ }
831
+
832
+ // src/client/charts.tsx
833
+ var import_jsx_runtime3 = require("react/jsx-runtime");
175
834
  function useChartTip() {
176
- const wrapRef = (0, import_react2.useRef)(null);
177
- const [tip, setTip] = (0, import_react2.useState)(null);
178
- const show = (e, text) => {
835
+ const wrapRef = (0, import_react3.useRef)(null);
836
+ const [tip, setTip] = (0, import_react3.useState)(null);
837
+ const [pinned, setPinned] = (0, import_react3.useState)(null);
838
+ const pinnedRef = (0, import_react3.useRef)(null);
839
+ const touchStart = (0, import_react3.useRef)(null);
840
+ const setPin = (next) => {
841
+ pinnedRef.current = next;
842
+ setPinned(next);
843
+ };
844
+ const showAt = (target, text, clientX, clientY) => {
179
845
  const wrap = wrapRef.current;
180
846
  if (text === void 0 || text === "" || wrap === null) return;
181
847
  const rect = wrap.getBoundingClientRect();
182
- const x = Math.min(Math.max(e.clientX - rect.left, 70), Math.max(rect.width - 70, 70));
183
- setTip({ x, y: e.clientY - rect.top, lines: text.split(" \xB7 ") });
848
+ const targetRect = target.getBoundingClientRect();
849
+ const x = Math.min(Math.max((clientX ?? targetRect.left + targetRect.width / 2) - rect.left, 70), Math.max(rect.width - 70, 70));
850
+ const viewportY = clientY ?? targetRect.top;
851
+ const y = viewportY - rect.top;
852
+ setTip({ x, y, lines: text.split(" \xB7 "), above: viewportY > window.innerHeight - 160 });
853
+ };
854
+ const show = (e, text) => {
855
+ if (e.pointerType === "touch") return;
856
+ showAt(e.currentTarget, text, e.clientX, e.clientY);
857
+ };
858
+ const showFocused = (e, text) => {
859
+ showAt(e.currentTarget, text);
860
+ };
861
+ const hide = () => {
862
+ if (pinnedRef.current !== null) return;
863
+ setTip(null);
864
+ };
865
+ const onPointStart = (e) => {
866
+ if (e.pointerType === "touch") touchStart.current = { x: e.clientX, y: e.clientY, t: Date.now() };
867
+ };
868
+ const onPointEnd = (e, index, text) => {
869
+ if (e.pointerType !== "touch") return;
870
+ const start = touchStart.current;
871
+ touchStart.current = null;
872
+ if (start === null || !isTapGesture(e.clientX - start.x, e.clientY - start.y, Date.now() - start.t)) return;
873
+ const next = nextPinnedIndex(pinnedRef.current, index);
874
+ setPin(next);
875
+ if (next === null) setTip(null);
876
+ else showAt(e.currentTarget, text, e.clientX, e.clientY);
877
+ };
878
+ (0, import_react3.useEffect)(() => {
879
+ if (pinned === null) return void 0;
880
+ const onDocPointerDown = (e) => {
881
+ const wrap = wrapRef.current;
882
+ if (wrap !== null && e.target instanceof Node && wrap.contains(e.target)) return;
883
+ setPin(null);
884
+ setTip(null);
885
+ };
886
+ document.addEventListener("pointerdown", onDocPointerDown);
887
+ return () => document.removeEventListener("pointerdown", onDocPointerDown);
888
+ }, [pinned]);
889
+ return { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd };
890
+ }
891
+ function useRovingChartPoints(count, initial, horizontalStep = 1) {
892
+ const refs = (0, import_react3.useRef)([]);
893
+ const [active, setActive] = (0, import_react3.useState)(initial);
894
+ (0, import_react3.useEffect)(() => setActive(Math.min(Math.max(0, initial), Math.max(0, count - 1))), [count, initial]);
895
+ const onKeyDown = (event, index) => {
896
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
897
+ event.preventDefault();
898
+ const next = nextChartFocus(index, count, event.key, horizontalStep);
899
+ setActive(next);
900
+ const target = refs.current[next];
901
+ target?.focus({ preventScroll: true });
902
+ target?.scrollIntoView({ block: "nearest", inline: "nearest" });
184
903
  };
185
- return { wrapRef, tip, show, hide: () => setTip(null) };
904
+ return { active, refs, onKeyDown };
186
905
  }
187
906
  function ChartTip(props) {
188
907
  const { tip } = props;
189
908
  if (tip === null) return null;
190
909
  const [head, ...rows] = tip.lines;
191
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-tip", style: { left: `${tip.x}px`, top: `${tip.y}px` }, role: "tooltip", children: [
192
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-tip-head", children: head }),
193
- rows.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-tip-row", children: row }, i))
910
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `dq-tip${tip.above ? " dq-tip--above" : ""}`, style: { left: `${tip.x}px`, top: `${tip.y}px` }, role: "tooltip", children: [
911
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-tip-head", children: head }),
912
+ rows.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-tip-row", children: row }, i))
194
913
  ] });
195
914
  }
196
915
  var MODEL_COLORS = ["#4176e6", "#2da44e", "#e16f24", "#8250df", "#bf8700", "#cf222e", "#1f883d", "#0969da"];
197
916
  function Bars(props) {
198
- const { data, height = 120, labelEvery = 1 } = props;
199
- const { wrapRef, tip, show, hide } = useChartTip();
917
+ const { data, height = 120, labelEvery = 1, minWidth } = props;
918
+ const { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd } = useChartTip();
919
+ const scrollRef = (0, import_react3.useRef)(null);
920
+ const initialFocus = lastPopulatedIndex(data.map((datum) => datum.value));
921
+ const roving = useRovingChartPoints(data.length, initialFocus);
922
+ (0, import_react3.useEffect)(() => {
923
+ const scroll = scrollRef.current;
924
+ if (scroll !== null && minWidth !== void 0) scroll.scrollLeft = scroll.scrollWidth;
925
+ }, [data.length, minWidth]);
200
926
  let max = 0;
201
927
  for (const datum of data) if (datum.value > max) max = datum.value;
202
928
  const scale = max > 0 ? max : 1;
203
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
204
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px` }, children: data.map((datum, i) => {
929
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
930
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars-scroll", ref: scrollRef, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px`, minWidth: minWidth === void 0 ? void 0 : `${minWidth}px` }, children: data.map((datum, i) => {
205
931
  const h = Math.max(1, Math.round(datum.value / scale * height));
206
932
  const text = datum.title ?? `${datum.label}: ${datum.value.toLocaleString()}`;
207
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
933
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
208
934
  "div",
209
935
  {
210
- className: "dq-bar-col",
936
+ className: `dq-bar-col${pinned === i ? " dq-bar-col--pinned" : ""}`,
937
+ role: "img",
211
938
  "aria-label": text,
939
+ "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Home End",
940
+ tabIndex: i === roving.active ? 0 : -1,
941
+ ref: (element) => {
942
+ roving.refs.current[i] = element;
943
+ },
212
944
  onPointerMove: (e) => show(e, text),
945
+ onPointerDown: onPointStart,
946
+ onPointerUp: (e) => onPointEnd(e, i, text),
947
+ onFocus: (e) => showFocused(e, text),
948
+ onBlur: hide,
949
+ onKeyDown: (e) => roving.onKeyDown(e, i),
213
950
  children: [
214
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-bar", style: { height: `${h}px` } }),
215
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: datum.label })
951
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar", style: { height: `${h}px` } }),
952
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: datum.label })
216
953
  ]
217
954
  },
218
955
  i
219
956
  );
220
- }) }),
221
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChartTip, { tip })
957
+ }) }) }),
958
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChartTip, { tip })
222
959
  ] });
223
960
  }
224
961
  function GroupedBars(props) {
225
- const { series, height = 120, labelEvery = 1 } = props;
226
- const { wrapRef, tip, show, hide } = useChartTip();
962
+ const { series, height = 120, labelEvery = 1, minWidth } = props;
963
+ const { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd } = useChartTip();
964
+ const scrollRef = (0, import_react3.useRef)(null);
227
965
  const count = series.length > 0 ? series[0].bars.length : 0;
228
- let max = 0;
229
- for (const s of series) for (const datum of s.bars) if (datum.value > max) max = datum.value;
230
- const scale = max > 0 ? max : 1;
231
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
232
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px` }, children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-bar-col", children: [
233
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-bar-group", children: series.map((s) => {
234
- const datum = s.bars[i];
235
- const h = Math.max(1, Math.round(datum.value / scale * height));
236
- const text = datum.title ?? `${s.key}: ${datum.value.toLocaleString()}`;
237
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
238
- "div",
239
- {
240
- className: "dq-bar",
241
- style: { height: `${h}px`, background: s.color },
242
- "aria-label": text,
243
- onPointerMove: (e) => show(e, text)
244
- },
245
- s.key
246
- );
247
- }) }),
248
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: series[0].bars[i].label })
249
- ] }, i)) }),
250
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChartTip, { tip })
966
+ const pointValues = Array.from({ length: count }, (_, i) => series.map((s) => s.bars[i]?.value ?? 0)).flat();
967
+ const roving = useRovingChartPoints(pointValues.length, lastPopulatedIndex(pointValues));
968
+ (0, import_react3.useEffect)(() => {
969
+ const scroll = scrollRef.current;
970
+ if (scroll !== null && minWidth !== void 0) scroll.scrollLeft = scroll.scrollWidth;
971
+ }, [count, minWidth]);
972
+ const scale = maxOrOne(stackedTotals(series, count));
973
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
974
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars-scroll", ref: scrollRef, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px`, minWidth: minWidth === void 0 ? void 0 : `${minWidth}px` }, children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
975
+ "div",
976
+ {
977
+ className: "dq-bar-col",
978
+ children: [
979
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-group", children: series.map((s, seriesIndex) => {
980
+ const datum = s.bars[i];
981
+ const h = Math.max(1, Math.round(datum.value / scale * height));
982
+ const text = datum.title ?? `${s.key}: ${datum.value.toLocaleString()}`;
983
+ const pointIndex = i * series.length + seriesIndex;
984
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
985
+ "div",
986
+ {
987
+ className: `dq-bar${pinned === pointIndex ? " dq-bar--pinned" : ""}`,
988
+ style: { height: `${h}px`, background: s.color },
989
+ role: "img",
990
+ "aria-label": text,
991
+ "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Home End",
992
+ tabIndex: pointIndex === roving.active ? 0 : -1,
993
+ ref: (element) => {
994
+ roving.refs.current[pointIndex] = element;
995
+ },
996
+ onPointerMove: (e) => show(e, text),
997
+ onPointerDown: onPointStart,
998
+ onPointerUp: (e) => onPointEnd(e, pointIndex, text),
999
+ onFocus: (e) => showFocused(e, text),
1000
+ onBlur: hide,
1001
+ onKeyDown: (e) => roving.onKeyDown(e, pointIndex)
1002
+ },
1003
+ s.key
1004
+ );
1005
+ }) }),
1006
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: series[0].bars[i].label })
1007
+ ]
1008
+ },
1009
+ i
1010
+ )) }) }),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChartTip, { tip })
251
1012
  ] });
252
1013
  }
253
1014
  function heatColor(level) {
@@ -257,10 +1018,11 @@ function heatColor(level) {
257
1018
  if (level === 3) return "rgba(65,118,230,0.75)";
258
1019
  return "rgba(65,118,230,1)";
259
1020
  }
260
- var DOW_LABELS = ["", "\u4E00", "", "\u4E09", "", "\u4E94", ""];
261
1021
  function Heatmap(props) {
262
1022
  const { data } = props;
263
- const { wrapRef, tip, show, hide } = useChartTip();
1023
+ const { t, locale } = useI18n();
1024
+ const { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd } = useChartTip();
1025
+ const roving = useRovingChartPoints(data.length, lastPopulatedIndex(data.map((datum) => datum.total)), 7);
264
1026
  let max = 0;
265
1027
  for (const datum of data) if (datum.total > max) max = datum.total;
266
1028
  let firstDow = 0;
@@ -281,39 +1043,53 @@ function Heatmap(props) {
281
1043
  const month = first.date.slice(0, 7);
282
1044
  if (month === lastMonth) return "";
283
1045
  lastMonth = month;
284
- return `${Number(first.date.slice(5, 7))}\u6708`;
1046
+ return t(`chart.month${Number(first.date.slice(5, 7))}`);
285
1047
  });
286
1048
  const levelOf = (total) => {
287
1049
  if (total <= 0) return 0;
288
1050
  const r = max > 0 ? total / max : 0;
289
1051
  return r < 0.25 ? 1 : r < 0.5 ? 2 : r < 0.75 ? 3 : 4;
290
1052
  };
291
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-heat dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
292
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-heat-grid", children: [
293
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-heat-dows", "aria-hidden": "true", children: DOW_LABELS.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-heat-dow", children: label }, i)) }),
294
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-heat-cols", children: [
295
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-heat-months", "aria-hidden": "true", children: monthLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-heat-month", children: label }, i)) }),
296
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-heatmap", children: weeks.map((week, w) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-heat-week", children: week.map((datum, d) => {
297
- if (datum === null) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-heat-cell dq-heat-cell--pad" }, d);
298
- const text = `${datum.date} \xB7 ${datum.total.toLocaleString()} tokens \xB7 \xA5${datum.cost.toFixed(2)} \xB7 ${datum.calls} \u6B21\u8C03\u7528`;
299
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
300
- "div",
301
- {
302
- className: "dq-heat-cell",
303
- style: { background: heatColor(levelOf(datum.total)) },
304
- "aria-label": text,
305
- onPointerMove: (e) => show(e, text)
1053
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-heat dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
1054
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-heat-scroll", children: [
1055
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-heatmap", children: weeks.map((week, w) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-heat-week", children: week.map((datum, d) => {
1056
+ if (datum === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-heat-cell dq-heat-cell--pad" }, d);
1057
+ const text = t("chart.tooltip", {
1058
+ head: datum.date,
1059
+ tokens: datum.total.toLocaleString(locale === "en" ? "en-US" : "zh-CN"),
1060
+ cost: datum.cost.toFixed(2),
1061
+ calls: t("common.calls", { count: datum.calls })
1062
+ });
1063
+ const index = w * 7 + d - firstDow;
1064
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1065
+ "div",
1066
+ {
1067
+ className: `dq-heat-cell${pinned === index ? " dq-heat-cell--pinned" : ""}`,
1068
+ style: { background: heatColor(levelOf(datum.total)) },
1069
+ role: "img",
1070
+ "aria-label": text,
1071
+ "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Home End",
1072
+ tabIndex: index === roving.active ? 0 : -1,
1073
+ ref: (element) => {
1074
+ roving.refs.current[index] = element;
306
1075
  },
307
- d
308
- );
309
- }) }, w)) })
310
- ] })
1076
+ onPointerMove: (e) => show(e, text),
1077
+ onPointerDown: onPointStart,
1078
+ onPointerUp: (e) => onPointEnd(e, index, text),
1079
+ onFocus: (e) => showFocused(e, text),
1080
+ onBlur: hide,
1081
+ onKeyDown: (e) => roving.onKeyDown(e, index)
1082
+ },
1083
+ d
1084
+ );
1085
+ }) }, w)) }),
1086
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-heat-months", "aria-hidden": "true", children: monthLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-heat-month", children: label }, i)) })
311
1087
  ] }),
312
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChartTip, { tip }),
313
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-heat-scale", children: [
314
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u5C11" }),
315
- [0, 1, 2, 3, 4].map((level) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-heat-key", style: { background: heatColor(level) } }, level)),
316
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u591A" })
1088
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChartTip, { tip }),
1089
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-heat-scale", children: [
1090
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: t("chart.less") }),
1091
+ [0, 1, 2, 3, 4].map((level) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-heat-key", style: { background: heatColor(level) } }, level)),
1092
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: t("chart.more") })
317
1093
  ] })
318
1094
  ] });
319
1095
  }
@@ -323,13 +1099,150 @@ var fmt = (v) => {
323
1099
  return n.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
324
1100
  };
325
1101
  var fmtInt = (v) => Math.round(Number(v) || 0).toLocaleString();
326
- var fmtCompact = (v) => {
1102
+ var fmtCompact = (v, locale = "zh") => {
327
1103
  const n = Number(v) || 0;
1104
+ if (locale === "en") {
1105
+ return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(n);
1106
+ }
328
1107
  if (n >= 1e8) return `${(n / 1e8).toFixed(1)}\u4EBF`;
329
1108
  if (n >= 1e4) return `${(n / 1e4).toFixed(1)}\u4E07`;
330
1109
  return Math.round(n).toLocaleString();
331
1110
  };
332
1111
 
1112
+ // src/client/dom.ts
1113
+ function getMainColumn(frame) {
1114
+ const anchor = frame.querySelector('[data-slot="conversation"]');
1115
+ const column = anchor?.parentElement ?? null;
1116
+ return column === frame ? null : column;
1117
+ }
1118
+ function slotElement(frame, slot, side) {
1119
+ const anchor = frame.querySelector(`[data-slot="${slot}"]`);
1120
+ if (anchor === null) return null;
1121
+ if (side === "child") {
1122
+ const el2 = anchor.firstElementChild;
1123
+ return el2 !== null && el2.getBoundingClientRect().height > 0 ? el2 : null;
1124
+ }
1125
+ let el = anchor.parentElement;
1126
+ for (let depth = 0; depth < 4 && el !== null && el !== frame; depth++) {
1127
+ if (el.getBoundingClientRect().height > 0) return el;
1128
+ el = el.parentElement;
1129
+ }
1130
+ return null;
1131
+ }
1132
+ function slotBox(frame, slot, side) {
1133
+ return slotElement(frame, slot, side)?.getBoundingClientRect() ?? null;
1134
+ }
1135
+ function getShellFrame(node) {
1136
+ const overlay = node !== null && typeof node.closest === "function" ? node.closest("[data-shell-overlay]") : null;
1137
+ return overlay?.parentElement ?? null;
1138
+ }
1139
+ function getComposerElement(frame) {
1140
+ if (frame === null) return null;
1141
+ return slotElement(frame, "conversation.composer.dock", "ancestor");
1142
+ }
1143
+ function getActiveConversationViewId(frame, viewIds) {
1144
+ if (viewIds.length === 0) return null;
1145
+ const header = frame === null ? null : slotElement(frame, "conversation.session.header", "child");
1146
+ const tablist = header?.querySelector('[role="tablist"]') ?? null;
1147
+ if (tablist === null) return viewIds[0] ?? null;
1148
+ const tabs = [...tablist.querySelectorAll('[role="tab"]')];
1149
+ const activeIndex = tabs.findIndex((tab) => tab.getAttribute("aria-selected") === "true");
1150
+ return activeIndex < 0 ? viewIds[0] ?? null : viewIds[activeIndex] ?? null;
1151
+ }
1152
+
1153
+ // src/client/export.ts
1154
+ var BEIJING_OFFSET_MS2 = 8 * 60 * 60 * 1e3;
1155
+ function exportDateStamp(nowMs = Date.now()) {
1156
+ const date = new Date(nowMs + BEIJING_OFFSET_MS2);
1157
+ return [
1158
+ date.getUTCFullYear(),
1159
+ String(date.getUTCMonth() + 1).padStart(2, "0"),
1160
+ String(date.getUTCDate()).padStart(2, "0")
1161
+ ].join("");
1162
+ }
1163
+ function csvCell(value) {
1164
+ let text = String(value);
1165
+ if (typeof value === "string" && /^[=+\-@]/.test(text)) text = `'${text}`;
1166
+ return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
1167
+ }
1168
+ var csv = (rows) => `\uFEFF${rows.map((row) => row.map(csvCell).join(",")).join("\r\n")}\r
1169
+ `;
1170
+ function dailyUsageCsv(usage) {
1171
+ return csv([
1172
+ ["date", "input_tokens", "output_tokens", "cache_hit_tokens", "total_tokens", "calls", "estimated_cost_cny"],
1173
+ ...usage.daily.map((day) => [day.date, day.input, day.output, day.cache, day.total, day.calls, day.cost])
1174
+ ]);
1175
+ }
1176
+ function modelUsageCsv(usage) {
1177
+ return csv([
1178
+ ["provider", "model", "input_tokens", "output_tokens", "cache_hit_tokens", "total_tokens", "calls", "estimated_cost_cny"],
1179
+ ...usage.models.map((model) => [
1180
+ model.provider,
1181
+ model.model,
1182
+ model.input,
1183
+ model.output,
1184
+ model.cache,
1185
+ model.total,
1186
+ model.calls,
1187
+ model.cost
1188
+ ])
1189
+ ]);
1190
+ }
1191
+ function fullUsageJson(usage, nowMs = Date.now()) {
1192
+ return `${JSON.stringify({
1193
+ schemaVersion: 1,
1194
+ exportedAt: new Date(nowMs).toISOString(),
1195
+ scope: "local-dsh-session-logs",
1196
+ data: usage
1197
+ }, null, 2)}
1198
+ `;
1199
+ }
1200
+ function downloadText(filename, text, mediaType) {
1201
+ const url = URL.createObjectURL(new Blob([text], { type: mediaType }));
1202
+ const anchor = document.createElement("a");
1203
+ anchor.href = url;
1204
+ anchor.download = filename;
1205
+ anchor.hidden = true;
1206
+ document.body.appendChild(anchor);
1207
+ anchor.click();
1208
+ anchor.remove();
1209
+ window.setTimeout(() => URL.revokeObjectURL(url), 0);
1210
+ }
1211
+
1212
+ // src/client/freshness.ts
1213
+ function updatedText(updatedAt, nowMs = Date.now(), t = fallbackT) {
1214
+ if (updatedAt === null || !Number.isFinite(updatedAt)) return t("fresh.none");
1215
+ const elapsed = Math.max(0, nowMs - updatedAt);
1216
+ if (elapsed < 6e4) return t("fresh.justNow");
1217
+ const minutes = Math.floor(elapsed / 6e4);
1218
+ if (minutes < 60) return t("fresh.minutes", { count: minutes });
1219
+ const hours = Math.floor(minutes / 60);
1220
+ if (hours < 24) return t("fresh.hours", { count: hours });
1221
+ return t("fresh.days", { count: Math.floor(hours / 24) });
1222
+ }
1223
+ function syncStatusText(state, updatedAt, nowMs = Date.now(), t = fallbackT) {
1224
+ const age = updatedText(updatedAt, nowMs, t);
1225
+ if (state === "syncing") return updatedAt === null ? t("sync.first") : t("sync.syncing", { age });
1226
+ if (state === "fresh") return t("sync.fresh", { age });
1227
+ if (state === "cached") return t("sync.cached", { age });
1228
+ if (state === "fallback") return t("sync.fallback", { age });
1229
+ return t("sync.error");
1230
+ }
1231
+
1232
+ // src/client/metric.ts
1233
+ var CHART_METRICS = ["tokens", "cost", "calls"];
1234
+ var isChartMetric = (value) => typeof value === "string" && CHART_METRICS.some((metric) => metric === value);
1235
+ function chartMetricValue(metric, point) {
1236
+ if (metric === "cost") return point.cost;
1237
+ if (metric === "calls") return point.calls;
1238
+ return point.total;
1239
+ }
1240
+ function chartMetricName(metric, t = fallbackT) {
1241
+ if (metric === "cost") return t("metric.cost");
1242
+ if (metric === "calls") return t("metric.calls");
1243
+ return t("metric.usage");
1244
+ }
1245
+
333
1246
  // src/client/prefs.ts
334
1247
  var PREFIX2 = "dsh-usage-dashboard:pref:";
335
1248
  var VERSION = 1;
@@ -375,363 +1288,676 @@ function createStore(key, isValid, fallback) {
375
1288
  }
376
1289
  var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
377
1290
  var widgetVisibleStore = createStore("widget.visible", isBoolean, true);
1291
+ var isWidgetTabIds = (value) => value === null || Array.isArray(value) && value.every((id) => typeof id === "string" && id !== "") && new Set(value).size === value.length;
1292
+ var widgetTabIdsStore = createStore("widget.tabIds", isWidgetTabIds, null);
378
1293
  var lowBalanceStore = createStore("alert.lowBalance", isThreshold, 10);
1294
+ var monthlyBudgetStore = createStore("budget.monthly", isThreshold, 0);
1295
+ var isUsageWindowDays = (value) => typeof value === "number" && USAGE_WINDOW_DAYS.some((days) => days === value);
1296
+ var usageWindowStore = createStore("usage.windowDays", isUsageWindowDays, 30);
1297
+ var chartMetricStore = createStore("usage.chartMetric", isChartMetric, "tokens");
1298
+ var quotaViewActive = false;
1299
+ var quotaViewListeners = /* @__PURE__ */ new Set();
1300
+ var quotaViewActiveStore = {
1301
+ get: () => quotaViewActive,
1302
+ set: (value) => {
1303
+ if (value === quotaViewActive) return;
1304
+ quotaViewActive = value;
1305
+ for (const listener of [...quotaViewListeners]) listener();
1306
+ },
1307
+ subscribe: (fn) => {
1308
+ quotaViewListeners.add(fn);
1309
+ return () => {
1310
+ quotaViewListeners.delete(fn);
1311
+ };
1312
+ }
1313
+ };
379
1314
 
380
1315
  // src/client/dashboard.tsx
381
- var import_jsx_runtime3 = require("react/jsx-runtime");
1316
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1317
+ var COMPOSER_FALLBACK_PX = 126;
382
1318
  function Skel(props) {
383
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-skel", style: { width: `${props.w}px`, height: `${props.h}px` } });
1319
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-skel", style: { width: `${props.w}px`, height: `${props.h}px` } });
384
1320
  }
385
1321
  function BalanceSkeleton() {
386
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-balance-grid", children: [
387
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
388
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 56, h: 11 }),
389
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 150, h: 22 })
1322
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance-grid", children: [
1323
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1324
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 56, h: 11 }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 150, h: 22 })
390
1326
  ] }),
391
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
392
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 28, h: 11 }),
393
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 48, h: 22 })
1327
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1328
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 28, h: 11 }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 48, h: 22 })
1330
+ ] })
1331
+ ] });
1332
+ }
1333
+ function SessionUsageSkeleton() {
1334
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance-grid", children: [
1335
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1336
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 64, h: 11 }),
1337
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 96, h: 18 })
1338
+ ] }),
1339
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1340
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 70, h: 11 }),
1341
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 80, h: 18 })
1342
+ ] }),
1343
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1344
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 64, h: 11 }),
1345
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 40, h: 18 })
394
1346
  ] })
395
1347
  ] });
396
1348
  }
397
1349
  function UsageSkeleton() {
398
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
399
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-usage-totals", children: [44, 44, 62, 62].map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
400
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w, h: 11 }),
401
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: w > 70 ? 84 : 56, h: 20 })
1350
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1351
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-usage-totals", children: [44, 44, 62, 62].map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1352
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w, h: 11 }),
1353
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: w > 70 ? 84 : 56, h: 20 })
402
1354
  ] }, i)) }),
403
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 140, h: 12 }),
404
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-skel-bars", children: Array.from({ length: 30 }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-skel dq-skel-bar", style: { height: `${18 + i * 37 % 82}px` } }, i)) })
1355
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 140, h: 12 }),
1356
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-skel-bars", children: Array.from({ length: 30 }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-skel dq-skel-bar", style: { height: `${18 + i * 37 % 82}px` } }, i)) })
1357
+ ] });
1358
+ }
1359
+ function HeatmapSkeleton() {
1360
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1361
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 140, h: 12 }),
1362
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-skel-bars", style: { height: "88px" }, children: Array.from({ length: 52 }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-skel dq-skel-bar", style: { height: `${14 + i * 29 % 60}px` } }, i)) })
405
1363
  ] });
406
1364
  }
407
1365
  function Delta(props) {
1366
+ const { t } = useI18n();
408
1367
  const { current, previous, label } = props;
409
1368
  if (previous <= 0) {
410
1369
  if (current <= 0) return null;
411
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-delta dq-delta--new", title: `${label}\u6CA1\u6709\u7528\u91CF\uFF0C\u65E0\u4ECE\u5BF9\u6BD4`, children: "\u65E0\u5BF9\u6BD4" });
1370
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta dq-delta--new", title: t("delta.noBaselineTitle", { label }), children: t("delta.noBaseline") });
412
1371
  }
413
1372
  const percent = Math.round((current - previous) / previous * 100);
414
1373
  const title = `${label} \xA5${fmt(previous)}`;
415
- if (percent === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-delta dq-delta--flat", title, children: "\u6301\u5E73" });
1374
+ if (percent === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta dq-delta--flat", title, children: t("delta.flat") });
416
1375
  const up = percent > 0;
417
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: `dq-delta ${up ? "dq-delta--up" : "dq-delta--down"}`, title, children: [
1376
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: `dq-delta ${up ? "dq-delta--up" : "dq-delta--down"}`, title, children: [
418
1377
  up ? "\u2191" : "\u2193",
419
1378
  Math.abs(percent),
420
1379
  "% ",
421
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-delta-label", children: label })
1380
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta-label", children: label })
422
1381
  ] });
423
1382
  }
424
1383
  function Period(props) {
1384
+ const { t, locale } = useI18n();
425
1385
  const { period, previous, compare } = props;
426
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-period", children: [
427
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-period-head", children: [
428
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-period-label", children: props.label }),
429
- previous !== void 0 && compare !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Delta, { current: period.cost, previous: previous.cost, label: compare })
1386
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-period", children: [
1387
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-period-head", children: [
1388
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-period-label", children: props.label }),
1389
+ previous !== void 0 && compare !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Delta, { current: period.cost, previous: previous.cost, label: compare })
430
1390
  ] }),
431
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-period-cost", children: [
1391
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-period-cost", children: [
432
1392
  "\xA5 ",
433
1393
  fmt(period.cost)
434
1394
  ] }),
435
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-period-sub", children: [
436
- fmtCompact(period.total),
437
- " tokens \xB7 ",
438
- fmtInt(period.calls),
439
- " \u6B21\u8C03\u7528"
1395
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-period-sub", children: t("period.volume", {
1396
+ tokens: fmtCompact(period.total, locale),
1397
+ calls: t("common.calls", { count: fmtInt(period.calls) })
1398
+ }) })
1399
+ ] });
1400
+ }
1401
+ function BudgetMeter(props) {
1402
+ const { t } = useI18n();
1403
+ if (props.budget <= 0) {
1404
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-budget dq-budget--unset", children: [
1405
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("budget.unset") }),
1406
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: "dq-budget-action", onClick: props.onConfigure, children: t("budget.configure") })
1407
+ ] });
1408
+ }
1409
+ const snapshot = budgetSnapshot(props.spent, props.budget);
1410
+ const usedPercent = snapshot.ratio * 100;
1411
+ const barPercent = Math.min(100, usedPercent);
1412
+ const forecastText = snapshot.status === "over" ? t("budget.over", { amount: fmt(Math.abs(snapshot.remaining)) }) : snapshot.forecastOver > 0 ? t("budget.forecastOver", { amount: fmt(snapshot.forecastOver) }) : t("budget.forecast", { amount: fmt(snapshot.forecast) });
1413
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `dq-budget dq-budget--${snapshot.status}`, children: [
1414
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-budget-head", children: [
1415
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-budget-title", children: t("budget.title") }),
1416
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-budget-amount", children: [
1417
+ "\xA5 ",
1418
+ fmt(snapshot.spent),
1419
+ " / \xA5 ",
1420
+ fmt(snapshot.budget)
1421
+ ] })
1422
+ ] }),
1423
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1424
+ "div",
1425
+ {
1426
+ className: "dq-budget-track",
1427
+ role: "progressbar",
1428
+ "aria-label": t("budget.progressLabel"),
1429
+ "aria-valuemin": 0,
1430
+ "aria-valuemax": snapshot.budget,
1431
+ "aria-valuenow": Math.min(snapshot.spent, snapshot.budget),
1432
+ "aria-valuetext": t("budget.used", { percent: usedPercent.toFixed(1), forecast: forecastText }),
1433
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-budget-fill", style: { transform: `scaleX(${barPercent / 100})` } })
1434
+ }
1435
+ ),
1436
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-budget-meta", children: [
1437
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: snapshot.remaining >= 0 ? t("budget.remaining", { amount: fmt(snapshot.remaining) }) : t("budget.exceeded", { amount: fmt(Math.abs(snapshot.remaining)) }) }),
1438
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: forecastText })
440
1439
  ] })
441
1440
  ] });
442
1441
  }
443
1442
  var rate = (value) => `\xA5${value.toLocaleString(void 0, { maximumFractionDigits: 3 })}`;
444
1443
  function PricingNote(props) {
1444
+ const { t } = useI18n();
445
1445
  const { pricing } = props;
446
1446
  const split = pricing.splitActive;
447
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("details", { className: "dq-pricing", children: [
448
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("summary", { className: "dq-pricing-summary", children: [
449
- "\u8BA1\u4EF7\u8BF4\u660E",
450
- split && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: `dq-pricing-now${pricing.inPeakNow ? " dq-pricing-now--peak" : ""}`, children: [
451
- "\u5F53\u524D ",
452
- pricing.inPeakNow ? "\u9AD8\u5CF0\u65F6\u6BB5" : "\u95F2\u65F6"
453
- ] })
1447
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("details", { className: "dq-pricing", children: [
1448
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("summary", { className: "dq-pricing-summary", children: [
1449
+ t("pricing.title"),
1450
+ split && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-pricing-now${pricing.inPeakNow ? " dq-pricing-now--peak" : ""}`, children: pricing.inPeakNow ? t("pricing.currentPeak") : t("pricing.currentOffPeak") })
454
1451
  ] }),
455
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-pricing-body", children: [
456
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("table", { className: "dq-pricing-table", children: [
457
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
458
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { children: "\u6A21\u578B" }),
459
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { children: split ? "\u65F6\u6BB5" : "\u5355\u4EF7" }),
460
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { children: "\u8F93\u5165\xB7\u7F13\u5B58\u547D\u4E2D" }),
461
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { children: "\u8F93\u5165\xB7\u672A\u547D\u4E2D" }),
462
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { children: "\u8F93\u51FA" })
1452
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-pricing-body", children: [
1453
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("table", { className: "dq-pricing-table", children: [
1454
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1455
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.model") }),
1456
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: split ? t("pricing.period") : t("pricing.unitPrice") }),
1457
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheHit") }),
1458
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheMiss") }),
1459
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.output") })
463
1460
  ] }) }),
464
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react3.Fragment, { children: [
465
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
466
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { rowSpan: tier.offPeak !== null ? 2 : 1, children: tier.model }),
467
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: split ? "\u9AD8\u5CF0" : "\u56FA\u5B9A" }),
468
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: rate(tier.peak.cacheHit) }),
469
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: rate(tier.peak.input) }),
470
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: rate(tier.peak.output) })
1461
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react4.Fragment, { children: [
1462
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1463
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { rowSpan: tier.offPeak !== null ? 2 : 1, children: tier.model }),
1464
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: split ? t("pricing.peak") : t("pricing.fixed") }),
1465
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.cacheHit) }),
1466
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.input) }),
1467
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.output) })
471
1468
  ] }),
472
- tier.offPeak !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
473
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: "\u95F2\u65F6" }),
474
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: rate(tier.offPeak.cacheHit) }),
475
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: rate(tier.offPeak.input) }),
476
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: rate(tier.offPeak.output) })
1469
+ tier.offPeak !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1470
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: t("pricing.offPeak") }),
1471
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.cacheHit) }),
1472
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.input) }),
1473
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.output) })
477
1474
  ] })
478
1475
  ] }, tier.model)) })
479
1476
  ] }),
480
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "dq-pricing-foot", children: [
481
- "\u5355\u4F4D\uFF1A",
482
- pricing.currency,
483
- " / \u767E\u4E07 tokens\u3002",
484
- split ? `\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 ${pricing.peakWindows.join("\u3001")}\uFF0C\u5176\u4F59\u4E3A\u95F2\u65F6\uFF08\u4EF7\u683C\u51CF\u534A\uFF09\uFF1B${pricing.switchDate} \u4E4B\u524D\u7684\u7528\u91CF\u4ECD\u6309\u65E7\u4EF7\u4F30\u7B97\u3002` : `${pricing.switchDate} 00:00 \u8D77\u6539\u4E3A\u5CF0\u8C37\u5B9A\u4EF7\uFF08\u9AD8\u5CF0 ${pricing.peakWindows.join("\u3001")}\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\uFF09\uFF0C\u5C4A\u65F6\u672C\u9875\u4F1A\u6309\u6BCF\u6761\u8BB0\u5F55\u7684\u65F6\u95F4\u81EA\u52A8\u5206\u6BB5\u8BA1\u4EF7\u3002`,
485
- "\u672A\u77E5\u6A21\u578B\u6309 deepseek-v4-pro \u8BA1\u4EF7\u3002"
1477
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-pricing-foot", children: [
1478
+ t("pricing.unit", { currency: pricing.currency }),
1479
+ split ? t("pricing.splitNote", { windows: pricing.peakWindows.join(t("common.listSeparator")), date: pricing.switchDate }) : t("pricing.switchNote", { windows: pricing.peakWindows.join(t("common.listSeparator")), date: pricing.switchDate }),
1480
+ t("pricing.unknown")
486
1481
  ] })
487
1482
  ] })
488
1483
  ] });
489
1484
  }
1485
+ function CoverageDiagnostics(props) {
1486
+ const { t, locale } = useI18n();
1487
+ const { coverage } = props;
1488
+ const hasGaps = coverage.failedSessions > 0 || coverage.skippedRecords > 0 || coverage.scannedSessions < coverage.listedSessions;
1489
+ const coverageTime = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
1490
+ timeZone: "Asia/Shanghai",
1491
+ year: "numeric",
1492
+ month: "numeric",
1493
+ day: "numeric",
1494
+ hour: "2-digit",
1495
+ minute: "2-digit",
1496
+ hour12: false
1497
+ });
1498
+ const range = coverage.earliestAt === null || coverage.latestAt === null ? t("coverage.noRange") : `${coverageTime.format(coverage.earliestAt)} \u2013 ${coverageTime.format(coverage.latestAt)}`;
1499
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("details", { className: `dq-coverage${hasGaps ? " dq-coverage--warn" : ""}`, children: [
1500
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("summary", { className: "dq-coverage-summary", children: [
1501
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-coverage-title", children: t("coverage.title") }),
1502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-coverage-status", children: hasGaps ? t("coverage.gaps") : t("coverage.complete") }),
1503
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-coverage-brief", children: t("coverage.brief", {
1504
+ scanned: fmtInt(coverage.scannedSessions),
1505
+ listed: fmtInt(coverage.listedSessions),
1506
+ records: fmtInt(coverage.usageRecords)
1507
+ }) })
1508
+ ] }),
1509
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-coverage-body", children: [
1510
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("dl", { className: "dq-coverage-metrics", children: [
1511
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1512
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dt", { children: t("coverage.scanned") }),
1513
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dd", { children: t("common.sessions", { count: fmtInt(coverage.scannedSessions) }) })
1514
+ ] }),
1515
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1516
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dt", { children: t("coverage.failed") }),
1517
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dd", { children: t("common.sessions", { count: fmtInt(coverage.failedSessions) }) })
1518
+ ] }),
1519
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1520
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dt", { children: t("coverage.skipped") }),
1521
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dd", { children: t("common.records", { count: fmtInt(coverage.skippedRecords) }) })
1522
+ ] })
1523
+ ] }),
1524
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-coverage-range", children: t("coverage.time", { range }) }),
1525
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-coverage-note", children: t("coverage.scope") }),
1526
+ hasGaps && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-coverage-warning", children: t("coverage.warning") })
1527
+ ] })
1528
+ ] });
1529
+ }
490
1530
  function Stat(props) {
491
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
492
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: props.label }),
1531
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1532
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: props.label }),
493
1533
  props.children
494
1534
  ] });
495
1535
  }
496
1536
  function Link(props) {
497
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { className: "dq-link", href: props.href, target: "_blank", rel: "noreferrer", children: props.children });
1537
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { className: "dq-link", href: props.href, target: "_blank", rel: "noreferrer", children: props.children });
1538
+ }
1539
+ function ExportMenu(props) {
1540
+ const { t } = useI18n();
1541
+ const [open, setOpen] = (0, import_react4.useState)(false);
1542
+ const [done, setDone] = (0, import_react4.useState)(false);
1543
+ const rootRef = (0, import_react4.useRef)(null);
1544
+ const triggerRef = (0, import_react4.useRef)(null);
1545
+ const menuRef = (0, import_react4.useRef)(null);
1546
+ const doneTimerRef = (0, import_react4.useRef)(null);
1547
+ (0, import_react4.useEffect)(() => {
1548
+ if (!open) return;
1549
+ const onDown = (event) => {
1550
+ if (rootRef.current !== null && !rootRef.current.contains(event.target)) setOpen(false);
1551
+ };
1552
+ const onKey = (event) => {
1553
+ if (event.key !== "Escape") return;
1554
+ setOpen(false);
1555
+ triggerRef.current?.focus();
1556
+ };
1557
+ document.addEventListener("mousedown", onDown);
1558
+ document.addEventListener("keydown", onKey);
1559
+ window.requestAnimationFrame(() => menuRef.current?.querySelector("button")?.focus());
1560
+ return () => {
1561
+ document.removeEventListener("mousedown", onDown);
1562
+ document.removeEventListener("keydown", onKey);
1563
+ };
1564
+ }, [open]);
1565
+ (0, import_react4.useEffect)(() => () => {
1566
+ if (doneTimerRef.current !== null) window.clearTimeout(doneTimerRef.current);
1567
+ }, []);
1568
+ const finish = () => {
1569
+ setOpen(false);
1570
+ setDone(true);
1571
+ if (doneTimerRef.current !== null) window.clearTimeout(doneTimerRef.current);
1572
+ doneTimerRef.current = window.setTimeout(() => setDone(false), 1400);
1573
+ };
1574
+ const exportDaily = () => {
1575
+ const stamp = exportDateStamp();
1576
+ downloadText(`dsh-usage-${stamp}.csv`, dailyUsageCsv(props.usage), "text/csv;charset=utf-8");
1577
+ finish();
1578
+ };
1579
+ const exportModels = () => {
1580
+ const stamp = exportDateStamp();
1581
+ downloadText(`dsh-usage-models-${stamp}.csv`, modelUsageCsv(props.usage), "text/csv;charset=utf-8");
1582
+ finish();
1583
+ };
1584
+ const exportJson = () => {
1585
+ const stamp = exportDateStamp();
1586
+ downloadText(`dsh-usage-${stamp}.json`, fullUsageJson(props.usage), "application/json;charset=utf-8");
1587
+ finish();
1588
+ };
1589
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-export", ref: rootRef, children: [
1590
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1591
+ "button",
1592
+ {
1593
+ ref: triggerRef,
1594
+ type: "button",
1595
+ className: `dq-export-btn${done ? " dq-export-btn--done" : ""}`,
1596
+ "aria-haspopup": "true",
1597
+ "aria-expanded": open,
1598
+ title: t("export.title"),
1599
+ onClick: () => setOpen((value) => !value),
1600
+ children: done ? t("export.done") : t("export.action")
1601
+ }
1602
+ ),
1603
+ open && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { ref: menuRef, className: "dq-export-menu", "aria-label": t("export.menuLabel"), children: [
1604
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("button", { type: "button", onClick: exportDaily, children: [
1605
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("export.dailyCsv") }),
1606
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("small", { children: t("common.rows", { count: props.usage.daily.length }) })
1607
+ ] }),
1608
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("button", { type: "button", onClick: exportModels, children: [
1609
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("export.modelCsv") }),
1610
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("small", { children: t("common.rows", { count: props.usage.models.length }) })
1611
+ ] }),
1612
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("button", { type: "button", onClick: exportJson, children: [
1613
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("export.fullJson") }),
1614
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("small", { children: t("export.jsonHint") })
1615
+ ] })
1616
+ ] })
1617
+ ] });
498
1618
  }
499
1619
  var modelKeyOf = (m) => m.provider !== "" ? `${m.provider}/${m.model}` : m.model;
500
- var modelNameOf = (m) => m.model !== "" ? m.model : "\u672A\u77E5\u6A21\u578B";
1620
+ var modelNameOf = (m, t = fallbackT) => m.model !== "" ? m.model : t("common.unknownModel");
1621
+ function SessionUsageCard(props) {
1622
+ const { t, locale } = useI18n();
1623
+ const { data } = props;
1624
+ if (data.calls === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("sessionUsage.empty") });
1625
+ const rangeTime = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
1626
+ timeZone: "Asia/Shanghai",
1627
+ year: "numeric",
1628
+ month: "numeric",
1629
+ day: "numeric",
1630
+ hour: "2-digit",
1631
+ minute: "2-digit",
1632
+ hour12: false
1633
+ });
1634
+ const range = data.firstActive !== null && data.lastActive !== null ? data.firstActive === data.lastActive ? rangeTime.format(data.firstActive) : `${rangeTime.format(data.firstActive)} \u2013 ${rangeTime.format(data.lastActive)}` : null;
1635
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1636
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance-grid", children: [
1637
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("sessionUsage.cost"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1638
+ "\xA5 ",
1639
+ fmt(data.cost)
1640
+ ] }) }),
1641
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("sessionUsage.tokens"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value", children: fmtCompact(data.total, locale) }) }),
1642
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("sessionUsage.calls"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value", children: fmtInt(data.calls) }) })
1643
+ ] }),
1644
+ range !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-session-sub", children: t("sessionUsage.range", { range }) }),
1645
+ data.models.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank", children: data.models.map((m, index) => {
1646
+ const key = m.provider !== "" ? `${m.provider}/${m.model}` : m.model !== "" ? m.model : `model-${index}`;
1647
+ const name2 = m.model !== "" ? m.model : t("common.unknownModel");
1648
+ const color = MODEL_COLORS[index % MODEL_COLORS.length];
1649
+ const share = data.cost > 0 ? m.cost / data.cost : 0;
1650
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-rank-row", children: [
1651
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-rank-head", children: [
1652
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-name", title: key, children: [
1653
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch", style: { background: color } }),
1654
+ name2
1655
+ ] }),
1656
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-cost", children: [
1657
+ "\xA5 ",
1658
+ fmt(m.cost),
1659
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-share", children: [
1660
+ (share * 100).toFixed(1),
1661
+ "%"
1662
+ ] })
1663
+ ] })
1664
+ ] }),
1665
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1666
+ "div",
1667
+ {
1668
+ className: "dq-rank-track",
1669
+ role: "img",
1670
+ "aria-label": t("models.shareAria", { name: name2, percent: (share * 100).toFixed(1) }),
1671
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank-fill", style: { width: `${Math.max(share * 100, 1.5)}%`, background: color } })
1672
+ }
1673
+ ),
1674
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank-sub", children: t("models.detail", {
1675
+ tokens: fmtCompact(m.total, locale),
1676
+ calls: t("common.calls", { count: fmtInt(m.calls) }),
1677
+ input: fmtCompact(m.input, locale),
1678
+ output: fmtCompact(m.output, locale),
1679
+ cache: fmtCompact(m.cache, locale)
1680
+ }) })
1681
+ ] }, key);
1682
+ }) })
1683
+ ] });
1684
+ }
501
1685
  function CacheCard(props) {
1686
+ const { t, locale } = useI18n();
502
1687
  const { totals } = props;
503
1688
  const prompt = totals.input + totals.cache;
504
- if (prompt === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709 prompt token \u53EF\u7EDF\u8BA1\u7F13\u5B58\u547D\u4E2D\u3002" });
1689
+ if (prompt === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("cache.empty") });
505
1690
  const rate2 = totals.cache / prompt;
506
1691
  const wouldHaveCost = totals.cost + totals.cacheSavings;
507
1692
  const low = rate2 < 0.6;
508
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-cache", children: [
509
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-cache-figures", children: [
510
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
511
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: "\u7F13\u5B58\u547D\u4E2D\u7387" }),
512
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `dq-stat-value${low ? " dq-stat-value--warn" : " dq-stat-value--ok"}`, children: [
1693
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-cache", children: [
1694
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-cache-figures", children: [
1695
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1696
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("cache.rate") }),
1697
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `dq-stat-value${low ? " dq-stat-value--warn" : " dq-stat-value--ok"}`, children: [
513
1698
  (rate2 * 100).toFixed(1),
514
1699
  "%"
515
1700
  ] })
516
1701
  ] }),
517
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
518
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: "\u5DF2\u8282\u7701\u8D39\u7528\uFF08\u4F30\u7B97\uFF09" }),
519
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat-value", children: [
1702
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1703
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("cache.saved") }),
1704
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
520
1705
  "\xA5 ",
521
1706
  fmt(totals.cacheSavings)
522
1707
  ] })
523
1708
  ] }),
524
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
525
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: "\u82E5\u5168\u90E8\u672A\u547D\u4E2D" }),
526
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat-value dq-muted-value", children: [
1709
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1710
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("cache.allMiss") }),
1711
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value dq-muted-value", children: [
527
1712
  "\xA5 ",
528
1713
  fmt(wouldHaveCost)
529
1714
  ] })
530
1715
  ] })
531
1716
  ] }),
532
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-cache-track", role: "img", "aria-label": `\u7F13\u5B58\u547D\u4E2D ${(rate2 * 100).toFixed(1)}%\uFF0C\u672A\u547D\u4E2D ${((1 - rate2) * 100).toFixed(1)}%`, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-cache-fill", style: { width: `${rate2 * 100}%` } }) }),
533
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-cache-legend", children: [
534
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
535
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--hit" }),
536
- "\u7F13\u5B58\u547D\u4E2D ",
537
- fmtCompact(totals.cache)
1717
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-cache-track", role: "img", "aria-label": t("cache.aria", { hit: (rate2 * 100).toFixed(1), miss: ((1 - rate2) * 100).toFixed(1) }), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-cache-fill", style: { width: `${rate2 * 100}%` } }) }),
1718
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-cache-legend", children: [
1719
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
1720
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--hit" }),
1721
+ t("cache.hitLegend", { tokens: fmtCompact(totals.cache, locale) })
538
1722
  ] }),
539
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
540
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--miss" }),
541
- "\u672A\u547D\u4E2D ",
542
- fmtCompact(totals.input),
543
- "\uFF08\u6309\u672A\u547D\u4E2D\u4EF7\u8BA1\u8D39\uFF09"
1723
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
1724
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--miss" }),
1725
+ t("cache.missLegend", { tokens: fmtCompact(totals.input, locale) })
544
1726
  ] })
545
1727
  ] }),
546
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "dq-cache-foot", children: low ? "\u547D\u4E2D\u7387\u504F\u4F4E\uFF1A\u9891\u7E41\u6539\u52A8 system prompt / \u5DE5\u5177\u5B9A\u4E49\u4F1A\u8BA9\u524D\u7F00\u7F13\u5B58\u5931\u6548\uFF0C\u628A\u7A33\u5B9A\u5185\u5BB9\u653E\u5728\u5BF9\u8BDD\u6700\u524D\u9762\u80FD\u63D0\u9AD8\u547D\u4E2D\u7387\u3002" : "\u524D\u7F00\u7F13\u5B58\u628A\u91CD\u590D\u7684 prompt \u524D\u7F00\u6309\u547D\u4E2D\u4EF7\u8BA1\u8D39\uFF0C\u662F\u8FD9\u4EFD\u8D26\u5355\u4E0A\u6700\u5927\u7684\u7701\u94B1\u6760\u6746\u3002" })
1728
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-cache-foot", children: low ? t("cache.lowHint") : t("cache.goodHint") })
547
1729
  ] });
548
1730
  }
549
1731
  function PeakCard(props) {
1732
+ const { t, locale } = useI18n();
550
1733
  const { split, pricing, currentCost } = props;
551
1734
  const total = split.peak.total + split.offPeak.total;
552
- if (total === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u6309\u65F6\u6BB5\u5F52\u7C7B\u7684\u7528\u91CF\u3002" });
1735
+ if (total === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("peak.empty") });
553
1736
  const peakShare = split.peak.total / total;
554
1737
  const shiftSaving = split.peakEraCost - split.offPeakEraCost;
555
1738
  const increase = split.peakEraCost - currentCost;
556
1739
  const increasePercent = currentCost > 0 ? increase / currentCost * 100 : 0;
557
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-peak", children: [
558
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1740
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak", children: [
1741
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
559
1742
  "div",
560
1743
  {
561
1744
  className: "dq-peak-track",
562
1745
  role: "img",
563
- "aria-label": `\u9AD8\u5CF0\u65F6\u6BB5 ${(peakShare * 100).toFixed(1)}%\uFF0C\u95F2\u65F6 ${((1 - peakShare) * 100).toFixed(1)}%`,
564
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-peak-fill", style: { width: `${peakShare * 100}%` } })
1746
+ "aria-label": t("peak.aria", { peak: (peakShare * 100).toFixed(1), offPeak: ((1 - peakShare) * 100).toFixed(1) }),
1747
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-fill", style: { width: `${peakShare * 100}%` } })
565
1748
  }
566
1749
  ),
567
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-peak-legend", children: [
568
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
569
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--peak" }),
570
- "\u9AD8\u5CF0 ",
571
- (peakShare * 100).toFixed(1),
572
- "% \xB7 ",
573
- fmtCompact(split.peak.total),
574
- " tokens \xB7 ",
575
- fmtInt(split.peak.calls),
576
- " \u6B21"
1750
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-legend", children: [
1751
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
1752
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--peak" }),
1753
+ t("peak.peakLegend", {
1754
+ share: (peakShare * 100).toFixed(1),
1755
+ tokens: fmtCompact(split.peak.total, locale),
1756
+ calls: t("common.callsShort", { count: fmtInt(split.peak.calls) })
1757
+ })
577
1758
  ] }),
578
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
579
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--offpeak" }),
580
- "\u95F2\u65F6 ",
581
- ((1 - peakShare) * 100).toFixed(1),
582
- "% \xB7 ",
583
- fmtCompact(split.offPeak.total),
584
- " tokens \xB7 ",
585
- fmtInt(split.offPeak.calls),
586
- " \u6B21"
1759
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
1760
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--offpeak" }),
1761
+ t("peak.offPeakLegend", {
1762
+ share: ((1 - peakShare) * 100).toFixed(1),
1763
+ tokens: fmtCompact(split.offPeak.total, locale),
1764
+ calls: t("common.callsShort", { count: fmtInt(split.offPeak.calls) })
1765
+ })
587
1766
  ] })
588
1767
  ] }),
589
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-peak-figures", children: [
590
- !pricing.splitActive && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
591
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: "\u540C\u6837\u7528\u91CF\u5728\u65B0\u4EF7\u4E0B" }),
592
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat-value", children: [
1768
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-figures", children: [
1769
+ !pricing.splitActive && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1770
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.newPrice") }),
1771
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
593
1772
  "\xA5 ",
594
1773
  fmt(split.peakEraCost),
595
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-peak-delta", children: [
596
- "\u8F83\u73B0\u4EF7 +",
597
- increasePercent.toFixed(0),
598
- "%"
599
- ] })
1774
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta", children: t("peak.increase", { percent: increasePercent.toFixed(0) }) })
600
1775
  ] })
601
1776
  ] }),
602
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
603
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: "\u82E5\u9AD8\u5CF0\u7528\u91CF\u90FD\u632A\u5230\u95F2\u65F6" }),
604
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat-value", children: [
1777
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1778
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.shift") }),
1779
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
605
1780
  "\xA5 ",
606
1781
  fmt(split.offPeakEraCost),
607
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-peak-delta dq-peak-delta--save", children: [
608
- "\u53EF\u7701 \xA5",
609
- fmt(shiftSaving)
610
- ] })
1782
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta dq-peak-delta--save", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
611
1783
  ] })
612
1784
  ] })
613
1785
  ] }),
614
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "dq-peak-foot", children: [
615
- "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 ",
616
- pricing.peakWindows.join("\u3001"),
617
- "\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\u3002",
618
- pricing.splitActive ? "\u628A\u6279\u91CF\u3001\u53EF\u5EF6\u540E\u7684\u4EFB\u52A1\u653E\u5230\u95F2\u65F6\u8DD1\uFF0C\u540C\u6837\u7684 token \u53EA\u8981\u4E00\u534A\u7684\u94B1\u3002" : `\u65B0\u4EF7 ${pricing.switchDate} 00:00 \u751F\u6548\uFF1B\u4E0A\u9762\u4E24\u4E2A\u6570\u5B57\u662F\u6309\u4F60\u5DF2\u6709\u7684\u5168\u90E8\u7528\u91CF\u91CD\u7B97\u7684\u3002`
1786
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-peak-foot", children: [
1787
+ t("peak.schedule", { windows: pricing.peakWindows.join(t("common.listSeparator")) }),
1788
+ pricing.splitActive ? t("peak.activeHint") : t("peak.futureHint", { date: pricing.switchDate })
619
1789
  ] })
620
1790
  ] });
621
1791
  }
622
- function agoText(ms) {
1792
+ function agoText(ms, t) {
623
1793
  const minutes = Math.floor((Date.now() - ms) / 6e4);
624
- if (minutes < 1) return "\u521A\u521A";
625
- if (minutes < 60) return `${minutes} \u5206\u949F\u524D`;
1794
+ if (minutes < 1) return t("common.justNow");
1795
+ if (minutes < 60) return t("common.minutesAgo", { count: minutes });
626
1796
  const hours = Math.floor(minutes / 60);
627
- if (hours < 24) return `${hours} \u5C0F\u65F6\u524D`;
628
- return `${Math.floor(hours / 24)} \u5929\u524D`;
1797
+ if (hours < 24) return t("common.hoursAgo", { count: hours });
1798
+ return t("common.daysAgo", { count: Math.floor(hours / 24) });
1799
+ }
1800
+ function SessionDetail(props) {
1801
+ const { t } = useI18n();
1802
+ const { state } = props;
1803
+ if (state === void 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionUsageSkeleton, {});
1804
+ if (state.data !== null) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionUsageCard, { data: state.data });
1805
+ if (state.error !== null) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty dq-empty--error", children: localizeApiError(state.error, t, "error.query") });
1806
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionUsageSkeleton, {});
629
1807
  }
630
1808
  function SessionRanking(props) {
1809
+ const { t, locale } = useI18n();
631
1810
  const { sessions, count, totalCost } = props;
632
- if (sessions.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u4EA7\u751F\u8D39\u7528\u7684\u4F1A\u8BDD\u3002" });
1811
+ const [details, setDetails] = (0, import_react4.useState)({});
1812
+ const startedRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
1813
+ const loadDetail = (id) => {
1814
+ if (startedRef.current.has(id)) return;
1815
+ startedRef.current.add(id);
1816
+ fetchSessionUsage(id).then((res) => {
1817
+ setDetails((prev) => ({
1818
+ ...prev,
1819
+ [id]: res.ok && res.data !== void 0 ? { error: null, data: res.data } : { error: res.error ?? null, data: null }
1820
+ }));
1821
+ }, (err) => {
1822
+ const message = err?.message ?? String(err);
1823
+ setDetails((prev) => ({ ...prev, [id]: { error: message, data: null } }));
1824
+ });
1825
+ };
1826
+ if (sessions.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("sessions.empty") });
633
1827
  const top = sessions[0].cost;
634
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
635
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ol", { className: "dq-sessions", children: sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "dq-session", children: [
636
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-session-head", children: [
637
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-session-title", title: `${s.title}
638
- ${s.id}`, children: s.title }),
639
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-session-cost", children: [
640
- "\xA5 ",
641
- fmt(s.cost)
642
- ] })
643
- ] }),
644
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-session-track", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-session-fill", style: { width: `${Math.max(s.cost / (top || 1) * 100, 1.5)}%` } }) }),
645
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-session-sub", children: [
646
- fmtCompact(s.total),
647
- " tokens \xB7 ",
648
- fmtInt(s.calls),
649
- " \u6B21\u8C03\u7528 \xB7 ",
650
- agoText(s.lastActive),
651
- totalCost > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
652
- " \xB7 \u5360 ",
653
- (s.cost / totalCost * 100).toFixed(1),
654
- "%"
655
- ] })
656
- ] })
657
- ] }, s.id)) }),
658
- count > sessions.length && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "dq-session-foot", children: [
659
- "\u5171 ",
660
- fmtInt(count),
661
- " \u4E2A\u4F1A\u8BDD\u6709\u7528\u91CF\uFF0C\u4E0A\u9762\u662F\u6700\u8D35\u7684 ",
662
- sessions.length,
663
- " \u4E2A\u3002"
664
- ] })
1828
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1829
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ol", { className: "dq-sessions", children: sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { className: "dq-session", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1830
+ "details",
1831
+ {
1832
+ className: "dq-session-details",
1833
+ onToggle: (e) => {
1834
+ if (e.currentTarget.open) loadDetail(s.id);
1835
+ },
1836
+ children: [
1837
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("summary", { className: "dq-session-summary", children: [
1838
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-session-chevron", "aria-hidden": "true", children: "\u25B8" }),
1839
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-session-summary-body", children: [
1840
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-session-head", children: [
1841
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-session-title", title: `${s.title}
1842
+ ${s.id}`, children: s.title || t("sessions.fallbackTitle", { id: s.id }) }),
1843
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-session-cost", children: [
1844
+ "\xA5 ",
1845
+ fmt(s.cost)
1846
+ ] })
1847
+ ] }),
1848
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-session-track", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-session-fill", style: { width: `${Math.max(s.cost / (top || 1) * 100, 1.5)}%` } }) }),
1849
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-session-sub", children: [
1850
+ t("sessions.detail", {
1851
+ tokens: fmtCompact(s.total, locale),
1852
+ calls: t("common.calls", { count: fmtInt(s.calls) }),
1853
+ ago: agoText(s.lastActive, t)
1854
+ }),
1855
+ totalCost > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1856
+ " \xB7 ",
1857
+ t("sessions.share", { percent: (s.cost / totalCost * 100).toFixed(1) })
1858
+ ] })
1859
+ ] })
1860
+ ] })
1861
+ ] }),
1862
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-session-expand", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionDetail, { state: details[s.id] }) })
1863
+ ]
1864
+ }
1865
+ ) }, s.id)) }),
1866
+ count > sessions.length && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-session-foot", children: t("sessions.more", { count: fmtInt(count), shown: sessions.length }) })
665
1867
  ] });
666
1868
  }
667
1869
  function ModelRanking(props) {
1870
+ const { t, locale } = useI18n();
668
1871
  const { models, totalCost } = props;
669
- if (models.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u5F52\u7C7B\u7684\u6A21\u578B\u7528\u91CF\u3002" });
670
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-rank", children: models.map((m) => {
1872
+ if (models.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("models.empty") });
1873
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank", children: models.map((m) => {
671
1874
  const key = modelKeyOf(m);
672
1875
  const share = totalCost > 0 ? m.cost / totalCost : 0;
673
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-rank-row", children: [
674
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-rank-head", children: [
675
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-rank-name", title: key, children: [
676
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-legend-swatch", style: { background: props.colorOf(key) } }),
677
- modelNameOf(m)
1876
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-rank-row", children: [
1877
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-rank-head", children: [
1878
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-name", title: key, children: [
1879
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch", style: { background: props.colorOf(key) } }),
1880
+ modelNameOf(m, t)
678
1881
  ] }),
679
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-rank-cost", children: [
1882
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-cost", children: [
680
1883
  "\xA5 ",
681
1884
  fmt(m.cost),
682
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-rank-share", children: [
1885
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-share", children: [
683
1886
  (share * 100).toFixed(1),
684
1887
  "%"
685
1888
  ] })
686
1889
  ] })
687
1890
  ] }),
688
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1891
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
689
1892
  "div",
690
1893
  {
691
1894
  className: "dq-rank-track",
692
1895
  role: "img",
693
- "aria-label": `${modelNameOf(m)} \u5360\u603B\u8D39\u7528 ${(share * 100).toFixed(1)}%`,
694
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-rank-fill", style: { width: `${Math.max(share * 100, 1.5)}%`, background: props.colorOf(key) } })
1896
+ "aria-label": t("models.shareAria", { name: modelNameOf(m, t), percent: (share * 100).toFixed(1) }),
1897
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank-fill", style: { width: `${Math.max(share * 100, 1.5)}%`, background: props.colorOf(key) } })
695
1898
  }
696
1899
  ),
697
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-rank-sub", children: [
698
- fmtCompact(m.total),
699
- " tokens \xB7 ",
700
- fmtInt(m.calls),
701
- " \u6B21\u8C03\u7528 \xB7 \u8F93\u5165 ",
702
- fmtCompact(m.input),
703
- " / \u8F93\u51FA ",
704
- fmtCompact(m.output),
705
- " / \u7F13\u5B58 ",
706
- fmtCompact(m.cache)
707
- ] })
1900
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank-sub", children: t("models.detail", {
1901
+ tokens: fmtCompact(m.total, locale),
1902
+ calls: t("common.calls", { count: fmtInt(m.calls) }),
1903
+ input: fmtCompact(m.input, locale),
1904
+ output: fmtCompact(m.output, locale),
1905
+ cache: fmtCompact(m.cache, locale)
1906
+ }) })
708
1907
  ] }, key);
709
1908
  }) });
710
1909
  }
711
1910
  function ModelPicker(props) {
1911
+ const { t, locale } = useI18n();
712
1912
  const { models } = props;
713
- if (models.length === 0) return null;
714
- const [open, setOpen] = (0, import_react3.useState)(false);
715
- const rootRef = (0, import_react3.useRef)(null);
716
- (0, import_react3.useEffect)(() => {
1913
+ const [open, setOpen] = (0, import_react4.useState)(false);
1914
+ const rootRef = (0, import_react4.useRef)(null);
1915
+ const triggerRef = (0, import_react4.useRef)(null);
1916
+ const menuRef = (0, import_react4.useRef)(null);
1917
+ (0, import_react4.useEffect)(() => {
717
1918
  if (!open) return;
718
1919
  const onDown = (e) => {
719
1920
  if (rootRef.current !== null && !rootRef.current.contains(e.target)) setOpen(false);
720
1921
  };
1922
+ const onKey = (e) => {
1923
+ if (e.key !== "Escape") return;
1924
+ e.preventDefault();
1925
+ setOpen(false);
1926
+ triggerRef.current?.focus();
1927
+ };
721
1928
  document.addEventListener("mousedown", onDown);
722
- return () => document.removeEventListener("mousedown", onDown);
1929
+ document.addEventListener("keydown", onKey);
1930
+ window.requestAnimationFrame(() => menuRef.current?.querySelector("input")?.focus());
1931
+ return () => {
1932
+ document.removeEventListener("mousedown", onDown);
1933
+ document.removeEventListener("keydown", onKey);
1934
+ };
723
1935
  }, [open]);
1936
+ if (models.length === 0) return null;
724
1937
  const toggle = (key) => {
725
1938
  props.onChange(
726
1939
  props.selected.includes(key) ? props.selected.filter((k) => k !== key) : [...props.selected, key]
727
1940
  );
728
1941
  };
729
- const label = props.selected.length === 0 ? "\u5168\u90E8\u6A21\u578B" : `\u6A21\u578B \xD7${props.selected.length}`;
730
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-model-picker", ref: rootRef, children: [
731
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "dq-model-btn", onClick: () => setOpen(!open), children: label }),
732
- open && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-model-menu", children: [
733
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "dq-model-item dq-model-item--all", children: [
734
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1942
+ const label = props.selected.length === 0 ? t("models.all") : t("models.selected", { count: props.selected.length });
1943
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-model-picker", ref: rootRef, children: [
1944
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1945
+ "button",
1946
+ {
1947
+ ref: triggerRef,
1948
+ type: "button",
1949
+ className: "dq-model-btn",
1950
+ "aria-haspopup": "true",
1951
+ "aria-expanded": open,
1952
+ "aria-controls": "dq-model-menu",
1953
+ title: t("models.filterTitle"),
1954
+ onClick: () => setOpen((value) => !value),
1955
+ children: label
1956
+ }
1957
+ ),
1958
+ open && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { id: "dq-model-menu", ref: menuRef, className: "dq-model-menu", role: "group", "aria-label": t("models.filterLabel"), children: [
1959
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-model-item dq-model-item--all", children: [
1960
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
735
1961
  "input",
736
1962
  {
737
1963
  type: "checkbox",
@@ -739,40 +1965,125 @@ function ModelPicker(props) {
739
1965
  onChange: () => props.onChange([])
740
1966
  }
741
1967
  ),
742
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "\u5168\u90E8\u6A21\u578B" })
1968
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("models.all") })
743
1969
  ] }),
744
1970
  models.map((m) => {
745
1971
  const key = modelKeyOf(m);
746
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "dq-model-item", children: [
747
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "checkbox", checked: props.selected.includes(key), onChange: () => toggle(key) }),
748
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-legend-swatch", style: { background: props.colorOf(key) } }),
749
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: modelNameOf(m) }),
750
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-model-tag", children: fmtCompact(m.total) })
1972
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-model-item", children: [
1973
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", checked: props.selected.includes(key), onChange: () => toggle(key) }),
1974
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch", style: { background: props.colorOf(key) } }),
1975
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: modelNameOf(m, t) }),
1976
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-model-tag", children: fmtCompact(m.total, locale) })
751
1977
  ] }, key);
752
1978
  })
753
1979
  ] })
754
1980
  ] });
755
1981
  }
756
- function BalanceDashboard() {
1982
+ var windowName = (days, t = fallbackT) => days === 365 ? t("window.year") : t("window.days", { days });
1983
+ function UsageWindowPicker(props) {
1984
+ const { t } = useI18n();
1985
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-window-switch", role: "group", "aria-label": t("window.groupLabel"), children: USAGE_WINDOW_DAYS.map((days) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1986
+ "button",
1987
+ {
1988
+ type: "button",
1989
+ className: `dq-window-btn${props.value === days ? " dq-window-btn--on" : ""}`,
1990
+ "aria-pressed": props.value === days,
1991
+ title: t("window.viewTitle", { window: windowName(days, t) }),
1992
+ onClick: () => props.onChange(days),
1993
+ children: days === 365 ? t("window.yearButton") : t("window.daysButton", { days })
1994
+ },
1995
+ days
1996
+ )) });
1997
+ }
1998
+ var chartMetricLabel = (metric, t = fallbackT) => metric === "tokens" ? t("metric.tokens") : chartMetricName(metric, t);
1999
+ function ChartMetricPicker(props) {
2000
+ const { t } = useI18n();
2001
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-chart-switch", role: "group", "aria-label": t("metric.groupLabel"), children: CHART_METRICS.map((metric) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2002
+ "button",
2003
+ {
2004
+ type: "button",
2005
+ className: `dq-chart-switch-btn${props.value === metric ? " dq-chart-switch-btn--on" : ""}`,
2006
+ "aria-pressed": props.value === metric,
2007
+ title: t("metric.compareTitle", { metric: chartMetricLabel(metric, t) }),
2008
+ onClick: () => props.onChange(metric),
2009
+ children: chartMetricLabel(metric, t)
2010
+ },
2011
+ metric
2012
+ )) });
2013
+ }
2014
+ function BalanceDashboard(props) {
2015
+ const { t, locale } = useI18n();
757
2016
  const cachedBalance = getCachedBalance();
758
2017
  const cachedUsage = getCachedUsage();
759
- const [balance, setBalance] = (0, import_react3.useState)(cachedBalance?.data ?? null);
760
- const [usage, setUsage] = (0, import_react3.useState)(cachedUsage?.data ?? null);
761
- const [error, setError] = (0, import_react3.useState)(null);
762
- const [notice, setNotice] = (0, import_react3.useState)(null);
763
- const [loadingBalance, setLoadingBalance] = (0, import_react3.useState)(cachedBalance === null);
764
- const [loadingUsage, setLoadingUsage] = (0, import_react3.useState)(cachedUsage === null);
765
- const [refreshing, setRefreshing] = (0, import_react3.useState)(false);
766
- const hadDataRef = (0, import_react3.useRef)(cachedBalance !== null || cachedUsage !== null);
767
- const [widgetOn, setWidgetOn] = (0, import_react3.useState)(widgetVisibleStore.get());
768
- const [lowBalance, setLowBalance] = (0, import_react3.useState)(lowBalanceStore.get());
769
- const [barMode, setBarMode] = (0, import_react3.useState)("daily");
770
- const [selectedModels, setSelectedModels] = (0, import_react3.useState)([]);
2018
+ const cachedUsageAt = getCachedUsageAt();
2019
+ const [balance, setBalance] = (0, import_react4.useState)(cachedBalance?.data ?? null);
2020
+ const [usage, setUsage] = (0, import_react4.useState)(cachedUsage?.data ?? null);
2021
+ const [error, setError] = (0, import_react4.useState)(null);
2022
+ const [notice, setNotice] = (0, import_react4.useState)(null);
2023
+ const [loadingBalance, setLoadingBalance] = (0, import_react4.useState)(cachedBalance === null);
2024
+ const [loadingUsage, setLoadingUsage] = (0, import_react4.useState)(cachedUsage === null);
2025
+ const [refreshing, setRefreshing] = (0, import_react4.useState)(false);
2026
+ const [usageUpdatedAt, setUsageUpdatedAt] = (0, import_react4.useState)(cachedUsageAt);
2027
+ const [syncState, setSyncState] = (0, import_react4.useState)(cachedUsageAt === null ? "syncing" : "cached");
2028
+ const [freshnessNow, setFreshnessNow] = (0, import_react4.useState)(Date.now());
2029
+ const hadDataRef = (0, import_react4.useRef)(cachedBalance !== null || cachedUsage !== null);
2030
+ const [widgetOn, setWidgetOn] = (0, import_react4.useState)(widgetVisibleStore.get());
2031
+ const [widgetTabIds, setWidgetTabIds] = (0, import_react4.useState)(widgetTabIdsStore.get());
2032
+ const [viewTabs, setViewTabs] = (0, import_react4.useState)(() => props.views.list());
2033
+ (0, import_react4.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
2034
+ const [lowBalance, setLowBalance] = (0, import_react4.useState)(lowBalanceStore.get());
2035
+ const [monthlyBudget, setMonthlyBudget] = (0, import_react4.useState)(monthlyBudgetStore.get());
2036
+ const [barMode, setBarMode] = (0, import_react4.useState)("daily");
2037
+ const [selectedModels, setSelectedModels] = (0, import_react4.useState)([]);
2038
+ const [windowDays, setWindowDays] = (0, import_react4.useState)(usageWindowStore.get());
2039
+ const [chartMetric, setChartMetric] = (0, import_react4.useState)(chartMetricStore.get());
2040
+ const [runwayHovering, setRunwayHovering] = (0, import_react4.useState)(false);
2041
+ const [runwayManuallyOpened, setRunwayManuallyOpened] = (0, import_react4.useState)(false);
2042
+ const [remainingHovering, setRemainingHovering] = (0, import_react4.useState)(false);
2043
+ const [remainingManuallyOpened, setRemainingManuallyOpened] = (0, import_react4.useState)(false);
2044
+ const budgetInputRef = (0, import_react4.useRef)(null);
2045
+ const rootRef = (0, import_react4.useRef)(null);
2046
+ const [composerHeight, setComposerHeight] = (0, import_react4.useState)(null);
2047
+ const [sessionUsage, setSessionUsage] = (0, import_react4.useState)(null);
2048
+ const [loadingSessionUsage, setLoadingSessionUsage] = (0, import_react4.useState)(props.sessionId !== void 0 && props.sessionId !== "");
2049
+ const [sessionUsageError, setSessionUsageError] = (0, import_react4.useState)(null);
2050
+ const sessionUsageIdRef = (0, import_react4.useRef)(null);
2051
+ (0, import_react4.useLayoutEffect)(() => {
2052
+ const node = rootRef.current;
2053
+ if (node === null) return;
2054
+ const measure = () => {
2055
+ const frame2 = getShellFrame(node);
2056
+ const composer2 = getComposerElement(frame2);
2057
+ const rect = composer2?.getBoundingClientRect() ?? null;
2058
+ const next = rect !== null && rect.height > 0 ? Math.round(rect.height) : null;
2059
+ setComposerHeight((prev) => prev === next ? prev : next);
2060
+ };
2061
+ measure();
2062
+ const frame = getShellFrame(node);
2063
+ const composer = getComposerElement(frame);
2064
+ let observer = null;
2065
+ if (typeof ResizeObserver === "function") {
2066
+ observer = new ResizeObserver(measure);
2067
+ if (composer !== null) observer.observe(composer);
2068
+ if (frame !== null) observer.observe(frame);
2069
+ }
2070
+ window.addEventListener("resize", measure);
2071
+ return () => {
2072
+ observer?.disconnect();
2073
+ window.removeEventListener("resize", measure);
2074
+ };
2075
+ }, []);
2076
+ (0, import_react4.useLayoutEffect)(() => {
2077
+ quotaViewActiveStore.set(true);
2078
+ return () => quotaViewActiveStore.set(false);
2079
+ }, []);
771
2080
  const errText = (err) => err?.message ?? String(err);
772
2081
  const load = async (force) => {
2082
+ const usageAtBefore = getCachedUsageAt();
773
2083
  setError(null);
774
2084
  setNotice(null);
775
2085
  setRefreshing(true);
2086
+ setSyncState("syncing");
776
2087
  const balanceTask = fetchBalance(force).then((res) => {
777
2088
  if (res.ok && res.data !== void 0) {
778
2089
  setBalance(res.data);
@@ -787,6 +2098,10 @@ function BalanceDashboard() {
787
2098
  const usageTask = fetchUsage(force).then((res) => {
788
2099
  if (res.ok && res.data !== void 0) {
789
2100
  setUsage(res.data);
2101
+ const updatedAt = getCachedUsageAt();
2102
+ setUsageUpdatedAt(updatedAt);
2103
+ setFreshnessNow(Date.now());
2104
+ setSyncState(updatedAt !== null && updatedAt !== usageAtBefore ? "fresh" : "cached");
790
2105
  hadDataRef.current = true;
791
2106
  return null;
792
2107
  }
@@ -797,57 +2112,134 @@ function BalanceDashboard() {
797
2112
  });
798
2113
  const [balanceError, usageError] = await Promise.all([balanceTask, usageTask]);
799
2114
  setRefreshing(false);
2115
+ if (usageError !== null) {
2116
+ const fallbackAt = getCachedUsageAt();
2117
+ setUsageUpdatedAt(fallbackAt);
2118
+ setFreshnessNow(Date.now());
2119
+ setSyncState(fallbackAt === null ? "error" : "fallback");
2120
+ }
800
2121
  if (balanceError !== null && usageError !== null) {
801
- if (hadDataRef.current) setNotice(`\u5237\u65B0\u5931\u8D25\uFF0C\u6B63\u5728\u5C55\u793A\u7F13\u5B58\u6570\u636E\uFF08${balanceError}\uFF09`);
2122
+ if (hadDataRef.current) setNotice({ kind: "all", error: balanceError });
802
2123
  else setError(balanceError);
803
2124
  } else if (balanceError !== null) {
804
- setNotice(`\u4F59\u989D\u5237\u65B0\u5931\u8D25\uFF0C\u6B63\u5728\u5C55\u793A\u7F13\u5B58\u4F59\u989D\uFF08${balanceError}\uFF09`);
2125
+ setNotice({ kind: "balance", error: balanceError });
805
2126
  } else if (usageError !== null) {
806
- setNotice(`\u7528\u91CF\u5237\u65B0\u5931\u8D25\uFF0C\u6B63\u5728\u5C55\u793A\u7F13\u5B58\u7528\u91CF\uFF08${usageError}\uFF09`);
2127
+ setNotice({ kind: "usage", error: usageError });
2128
+ }
2129
+ if (force && props.sessionId !== void 0 && props.sessionId !== "") {
2130
+ void loadSessionUsage(props.sessionId, true);
807
2131
  }
808
2132
  };
809
- (0, import_react3.useEffect)(() => {
2133
+ const loadSessionUsage = async (id, force) => {
2134
+ if (!force) setLoadingSessionUsage(true);
2135
+ try {
2136
+ const res = await fetchSessionUsage(id, force);
2137
+ if (sessionUsageIdRef.current !== id) return;
2138
+ if (res.ok && res.data !== void 0) {
2139
+ setSessionUsage(res.data);
2140
+ setSessionUsageError(null);
2141
+ } else {
2142
+ setSessionUsageError(res.error ?? null);
2143
+ }
2144
+ } catch (err) {
2145
+ if (sessionUsageIdRef.current === id) setSessionUsageError(errText(err));
2146
+ } finally {
2147
+ if (sessionUsageIdRef.current === id) setLoadingSessionUsage(false);
2148
+ }
2149
+ };
2150
+ (0, import_react4.useEffect)(() => {
810
2151
  void load(false);
811
2152
  }, []);
2153
+ (0, import_react4.useEffect)(() => {
2154
+ const id = props.sessionId;
2155
+ sessionUsageIdRef.current = id ?? null;
2156
+ if (id === void 0 || id === "") {
2157
+ setSessionUsage(null);
2158
+ setSessionUsageError(null);
2159
+ setLoadingSessionUsage(false);
2160
+ return;
2161
+ }
2162
+ setSessionUsage(null);
2163
+ setSessionUsageError(null);
2164
+ setLoadingSessionUsage(true);
2165
+ void loadSessionUsage(id, false);
2166
+ }, [props.sessionId]);
2167
+ (0, import_react4.useEffect)(() => {
2168
+ const timer = window.setInterval(() => setFreshnessNow(Date.now()), 3e4);
2169
+ return () => window.clearInterval(timer);
2170
+ }, []);
812
2171
  const toggle = () => {
813
2172
  const next = !widgetOn;
814
2173
  setWidgetOn(next);
815
2174
  widgetVisibleStore.set(next);
816
2175
  };
2176
+ const widgetTabs = viewTabs.filter((tab) => tab.id !== "balance");
2177
+ const toggleWidgetTab = (id) => {
2178
+ const selected = widgetTabIds ?? widgetTabs.map((tab) => tab.id);
2179
+ const next = selected.includes(id) ? selected.filter((tabId) => tabId !== id) : [...selected, id];
2180
+ setWidgetTabIds(next);
2181
+ widgetTabIdsStore.set(next);
2182
+ };
817
2183
  const changeLowBalance = (value) => {
818
2184
  setLowBalance(value);
819
2185
  lowBalanceStore.set(value);
820
2186
  };
2187
+ const changeMonthlyBudget = (value) => {
2188
+ setMonthlyBudget(value);
2189
+ monthlyBudgetStore.set(value);
2190
+ };
2191
+ const changeWindowDays = (value) => {
2192
+ setWindowDays(value);
2193
+ usageWindowStore.set(value);
2194
+ };
2195
+ const changeChartMetric = (value) => {
2196
+ setChartMetric(value);
2197
+ chartMetricStore.set(value);
2198
+ };
821
2199
  const primary = balance !== null && balance.balances.length > 0 ? balance.balances[0] : null;
822
2200
  const recentDays = (usage?.daily ?? []).slice(-7);
823
2201
  const avgDailyCost = recentDays.length > 0 ? recentDays.reduce((sum, d) => sum + d.cost, 0) / recentDays.length : 0;
824
2202
  const balanceValue = primary !== null ? Number(primary.total) : Number.NaN;
825
2203
  const balanceLow = lowBalance > 0 && Number.isFinite(balanceValue) && balanceValue < lowBalance;
826
2204
  const daysLeft = avgDailyCost > 0 && Number.isFinite(balanceValue) ? balanceValue / avgDailyCost : null;
827
- const daysLeftText = daysLeft === null ? "\u2014" : daysLeft >= 365 ? "> 365 \u5929" : `${daysLeft < 10 ? daysLeft.toFixed(1) : Math.round(daysLeft)} \u5929`;
828
- const runwayTitle = daysLeft === null ? `\u8FD1 ${recentDays.length || 7} \u5929\u6CA1\u6709\u7528\u91CF\uFF0C\u65E0\u6CD5\u4F30\u7B97` : `\u6309\u8FD1 ${recentDays.length} \u5929\u65E5\u5747 \xA5${fmt(avgDailyCost)} \u4F30\u7B97\uFF08\u542B\u65E0\u7528\u91CF\u7684\u65E5\u5B50\uFF1B\u4ECA\u5929\u5C1A\u672A\u8FC7\u5B8C\uFF09`;
829
- const dailyBars = (usage?.daily ?? []).map((d) => ({
830
- label: d.date.slice(8, 10),
831
- value: d.total,
832
- title: `${d.date} \xB7 ${fmtInt(d.total)} tokens \xB7 \xA5${fmt(d.cost)} \xB7 ${d.calls} \u6B21\u8C03\u7528`
2205
+ const daysLeftText = daysLeft === null ? "\u2014" : daysLeft >= 365 ? t("balance.overYear") : t("balance.days", { count: daysLeft < 10 ? daysLeft.toFixed(1) : Math.round(daysLeft) });
2206
+ const runwayTitle = daysLeft === null ? t("balance.noRunway", { days: recentDays.length || 7 }) : t("balance.runwayTitle", { days: recentDays.length, cost: fmt(avgDailyCost) });
2207
+ const activeWindow = usage?.windows.find((window2) => window2.days === windowDays) ?? null;
2208
+ const activeWindowName = windowName(windowDays, t);
2209
+ const dailyBars = (activeWindow?.daily ?? []).map((d) => ({
2210
+ label: windowDays === 365 ? d.date.slice(5).replace("-", "/") : d.date.slice(8, 10),
2211
+ value: chartMetricValue(chartMetric, d),
2212
+ title: t("chart.tooltip", {
2213
+ head: d.date,
2214
+ tokens: fmtInt(d.total),
2215
+ cost: fmt(d.cost),
2216
+ calls: t("common.calls", { count: d.calls })
2217
+ })
833
2218
  }));
834
- const hourlyBars = (usage?.hourly ?? []).map((h) => ({
2219
+ const hourlyBars = (activeWindow?.hourly ?? []).map((h) => ({
835
2220
  label: String(h.hour),
836
- value: h.total,
837
- title: `${h.hour} \u70B9 \xB7 ${fmtInt(h.total)} tokens \xB7 ${h.calls} \u6B21\u8C03\u7528`
2221
+ value: chartMetricValue(chartMetric, h),
2222
+ title: t("chart.tooltip", {
2223
+ head: t("chart.hourPoint", { hour: h.hour }),
2224
+ tokens: fmtInt(h.total),
2225
+ cost: fmt(h.cost),
2226
+ calls: t("common.calls", { count: h.calls })
2227
+ })
838
2228
  }));
839
2229
  const chartPeak = (barMode === "daily" ? dailyBars : hourlyBars).reduce((peak, bar) => bar.value > peak ? bar.value : peak, 0);
840
- const modelList = [...usage?.models ?? []].sort((a, b) => b.cost - a.cost);
2230
+ const chartPeakText = chartMetric === "cost" ? `\xA5${fmt(chartPeak)}` : chartMetric === "calls" ? t("common.callsShort", { count: fmtInt(chartPeak) }) : `${fmtCompact(chartPeak, locale)} tokens`;
2231
+ const modelList = [...activeWindow?.models ?? []].sort((a, b) => b.cost - a.cost);
2232
+ const canonicalModels = [...usage?.models ?? []].sort((a, b) => b.cost - a.cost);
841
2233
  const availableModelKeys = new Set(modelList.map(modelKeyOf));
842
2234
  const effectiveSelection = selectedModels.filter((k) => availableModelKeys.has(k));
843
2235
  const colorOf = (key) => {
844
- const index = modelList.findIndex((m) => modelKeyOf(m) === key);
2236
+ const index = canonicalModels.findIndex((m) => modelKeyOf(m) === key);
845
2237
  return MODEL_COLORS[(index < 0 ? 0 : index) % MODEL_COLORS.length];
846
2238
  };
847
2239
  const modelCostTotal = modelList.reduce((sum, m) => sum + m.cost, 0);
848
2240
  const grouped = effectiveSelection.map((key) => {
849
2241
  const m = modelList.find((x) => modelKeyOf(x) === key);
850
- const name2 = m === void 0 ? key : modelNameOf(m);
2242
+ const name2 = m === void 0 ? key : modelNameOf(m, t);
851
2243
  const color = colorOf(key);
852
2244
  if (barMode === "daily") {
853
2245
  return {
@@ -855,9 +2247,14 @@ function BalanceDashboard() {
855
2247
  name: name2,
856
2248
  color,
857
2249
  bars: (m?.daily ?? []).map((p, i) => ({
858
- label: (usage?.daily ?? [])[i]?.date.slice(8, 10) ?? "",
859
- value: p.total,
860
- title: `${name2} \xB7 ${(usage?.daily ?? [])[i]?.date ?? ""} \xB7 ${fmtInt(p.total)} tokens \xB7 \xA5${fmt(p.cost)} \xB7 ${p.calls} \u6B21\u8C03\u7528`
2250
+ label: windowDays === 365 ? (activeWindow?.daily ?? [])[i]?.date.slice(5).replace("-", "/") ?? "" : (activeWindow?.daily ?? [])[i]?.date.slice(8, 10) ?? "",
2251
+ value: chartMetricValue(chartMetric, p),
2252
+ title: t("chart.tooltip", {
2253
+ head: `${name2} \xB7 ${(activeWindow?.daily ?? [])[i]?.date ?? ""}`,
2254
+ tokens: fmtInt(p.total),
2255
+ cost: fmt(p.cost),
2256
+ calls: t("common.calls", { count: p.calls })
2257
+ })
861
2258
  }))
862
2259
  };
863
2260
  }
@@ -867,134 +2264,193 @@ function BalanceDashboard() {
867
2264
  color,
868
2265
  bars: (m?.hourly ?? []).map((p, i) => ({
869
2266
  label: String(i),
870
- value: p.total,
871
- title: `${name2} \xB7 ${i} \u70B9 \xB7 ${fmtInt(p.total)} tokens \xB7 \xA5${fmt(p.cost)} \xB7 ${p.calls} \u6B21\u8C03\u7528`
2267
+ value: chartMetricValue(chartMetric, p),
2268
+ title: t("chart.tooltip", {
2269
+ head: `${name2} \xB7 ${t("chart.hourPoint", { hour: i })}`,
2270
+ tokens: fmtInt(p.total),
2271
+ cost: fmt(p.cost),
2272
+ calls: t("common.calls", { count: p.calls })
2273
+ })
872
2274
  }))
873
2275
  };
874
2276
  });
875
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-balance", children: [
876
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-status-row", children: [
877
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
878
- (loadingBalance || loadingUsage) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-muted", children: "\u52A0\u8F7D\u4E2D\u2026" }),
879
- !loadingBalance && !loadingUsage && refreshing && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-muted", children: "\u66F4\u65B0\u4E2D\u2026" }),
880
- notice !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-warn", children: notice }),
881
- error !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-error", children: error })
2277
+ const dailyLabelEvery = windowDays === 7 ? 1 : windowDays === 30 ? 5 : windowDays === 90 ? 15 : 30;
2278
+ const dailyMinWidth = windowDays === 365 ? 1825 : void 0;
2279
+ const noticeText = notice === null ? null : t(
2280
+ notice.kind === "all" ? "error.refreshCached" : notice.kind === "balance" ? "error.balanceRefreshCached" : "error.usageRefreshCached",
2281
+ { error: localizeApiError(notice.error, t, "error.query") }
2282
+ );
2283
+ const errorText = error === null ? null : localizeApiError(error, t, "error.query");
2284
+ const composerSafeAreaStyle = {
2285
+ "--dq-composer-h": `${composerHeight ?? COMPOSER_FALLBACK_PX}px`
2286
+ };
2287
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance", ref: rootRef, style: composerSafeAreaStyle, children: [
2288
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-row", children: [
2289
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-messages", children: [
2290
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2291
+ "span",
2292
+ {
2293
+ className: `dq-sync dq-sync--${syncState}`,
2294
+ role: "status",
2295
+ "aria-live": "polite",
2296
+ title: usageUpdatedAt === null ? t("status.neverSynced") : t("status.usageTime", { time: new Date(usageUpdatedAt).toLocaleString(locale === "en" ? "en-US" : "zh-CN", { timeZone: "Asia/Shanghai" }) }),
2297
+ children: [
2298
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-sync-dot", "aria-hidden": "true" }),
2299
+ syncStatusText(syncState, usageUpdatedAt, freshnessNow, t)
2300
+ ]
2301
+ }
2302
+ ),
2303
+ noticeText !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-warn", children: noticeText }),
2304
+ errorText !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-error", children: errorText })
882
2305
  ] }),
883
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2306
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
884
2307
  "button",
885
2308
  {
886
2309
  type: "button",
887
2310
  className: "dq-refresh-btn",
888
- title: "\u5F3A\u5236\u5237\u65B0\uFF08\u7ED5\u8FC7\u7F13\u5B58\uFF09",
2311
+ title: t("status.forceRefresh"),
889
2312
  disabled: refreshing,
890
2313
  "aria-busy": refreshing,
891
2314
  onClick: () => {
892
2315
  void load(true);
893
2316
  },
894
2317
  children: [
895
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `dq-refresh-icon${refreshing ? " dq-refresh-icon--spin" : ""}`, children: "\u21BB" }),
896
- refreshing ? "\u5237\u65B0\u4E2D" : "\u5237\u65B0"
2318
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-refresh-icon${refreshing ? " dq-refresh-icon--spin" : ""}`, children: "\u21BB" }),
2319
+ refreshing ? t("status.refreshing") : t("status.refresh")
897
2320
  ]
898
2321
  }
899
2322
  )
900
2323
  ] }),
901
- balanceLow && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-alert", role: "status", children: [
902
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-alert-icon", "aria-hidden": "true", children: "!" }),
903
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
904
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("strong", { children: [
905
- "\u4F59\u989D ",
906
- fmt(balanceValue),
907
- " ",
908
- primary?.currency,
909
- " \u5DF2\u4F4E\u4E8E\u9884\u8B66\u7EBF ",
910
- fmt(lowBalance)
911
- ] }),
912
- daysLeft !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
913
- "\uFF0C\u6309\u8FD1\u671F\u7528\u91CF\u4F30\u8BA1\u8FD8\u80FD\u6491 ",
914
- daysLeftText
915
- ] }),
916
- "\u3002",
917
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { className: "dq-alert-link", href: "https://platform.deepseek.com/top_up", target: "_blank", rel: "noreferrer", children: "\u53BB\u5145\u503C" })
2324
+ balanceLow && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-alert", role: "status", children: [
2325
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-alert-icon", "aria-hidden": "true", children: "!" }),
2326
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
2327
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: t("alert.lowTitle", { balance: fmt(balanceValue), currency: primary?.currency, threshold: fmt(lowBalance) }) }),
2328
+ daysLeft !== null && t("alert.runway", { days: daysLeftText }),
2329
+ t("common.period"),
2330
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { className: "dq-alert-link", href: "https://platform.deepseek.com/top_up", target: "_blank", rel: "noreferrer", children: t("alert.topUp") })
918
2331
  ] })
919
2332
  ] }),
920
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
921
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u8D26\u6237\u4F59\u989D" }),
922
- primary !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-balance-grid", children: [
923
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
924
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: "\u5269\u4F59\u4F59\u989D" }),
925
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat-value dq-remaining", children: [
926
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
927
- fmt(primary.total),
928
- " ",
929
- primary.currency
930
- ] }),
931
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-remaining-breakdown", children: [
932
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
933
- "\u5145\u503C\u989D\u5EA6 ",
934
- fmt(primary.toppedUp)
935
- ] }),
936
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-remaining-granted", children: [
937
- "\u8D60\u9001\u989D\u5EA6 ",
938
- fmt(primary.granted)
939
- ] })
940
- ] })
941
- ] })
2333
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2334
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("balance.title") }),
2335
+ primary !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance-grid", children: [
2336
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2337
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("balance.remaining") }),
2338
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2339
+ "details",
2340
+ {
2341
+ className: "dq-stat-value dq-remaining",
2342
+ open: remainingHovering || remainingManuallyOpened,
2343
+ onPointerEnter: () => setRemainingHovering(true),
2344
+ onPointerLeave: () => setRemainingHovering(false),
2345
+ children: [
2346
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2347
+ "summary",
2348
+ {
2349
+ title: t("balance.breakdownTitle"),
2350
+ onClick: (e) => {
2351
+ e.preventDefault();
2352
+ setRemainingManuallyOpened((v) => !v);
2353
+ },
2354
+ children: [
2355
+ fmt(primary.total),
2356
+ " ",
2357
+ primary.currency
2358
+ ]
2359
+ }
2360
+ ),
2361
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-remaining-breakdown", children: [
2362
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("balance.toppedUp", { amount: fmt(primary.toppedUp) }) }),
2363
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-remaining-granted", children: t("balance.granted", { amount: fmt(primary.granted) }) })
2364
+ ] })
2365
+ ]
2366
+ }
2367
+ )
942
2368
  ] }),
943
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
944
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-label", children: "\u9884\u8BA1\u53EF\u7528" }),
945
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
946
- "div",
2369
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2370
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("balance.runway") }),
2371
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2372
+ "details",
947
2373
  {
948
2374
  className: `dq-stat-value dq-runway${daysLeft !== null && daysLeft < 3 ? " dq-stat-value--bad" : ""}`,
949
- title: runwayTitle,
950
- children: daysLeftText
2375
+ open: runwayHovering || runwayManuallyOpened,
2376
+ onPointerEnter: () => setRunwayHovering(true),
2377
+ onPointerLeave: () => setRunwayHovering(false),
2378
+ children: [
2379
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2380
+ "summary",
2381
+ {
2382
+ title: t("balance.runwayBreakdownTitle"),
2383
+ onClick: (e) => {
2384
+ e.preventDefault();
2385
+ setRunwayManuallyOpened((v) => !v);
2386
+ },
2387
+ children: daysLeftText
2388
+ }
2389
+ ),
2390
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-runway-detail", children: runwayTitle })
2391
+ ]
951
2392
  }
952
2393
  )
953
2394
  ] }),
954
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Stat, { label: "\u72B6\u6001", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `dq-stat-value${balance?.isAvailable === false ? " dq-stat-value--bad" : " dq-stat-value--ok"}`, children: balance?.isAvailable === false ? "\u4E0D\u53EF\u7528" : "\u53EF\u7528" }) })
955
- ] }) : loadingBalance ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BalanceSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8BFB\u4E0D\u5230\u4F59\u989D\u3002\u8BF7\u786E\u8BA4\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u91CC\u5DF2\u586B DEEPSEEK_API_KEY\uFF0C\u7136\u540E\u70B9\u53F3\u4E0A\u89D2\u5237\u65B0\u3002" })
2395
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("common.status"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `dq-stat-value${balance?.isAvailable === false ? " dq-stat-value--bad" : " dq-stat-value--ok"}`, children: balance?.isAvailable === false ? t("common.unavailable") : t("common.available") }) })
2396
+ ] }) : loadingBalance ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BalanceSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("balance.empty") })
2397
+ ] }),
2398
+ props.sessionId !== void 0 && props.sessionId !== "" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2399
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("sessionUsage.title") }),
2400
+ sessionUsage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionUsageCard, { data: sessionUsage }) : loadingSessionUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionUsageSkeleton, {}) : sessionUsageError !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty dq-empty--error", children: localizeApiError(sessionUsageError, t, "error.query") }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("sessionUsage.empty") })
956
2401
  ] }),
957
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
958
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u6D88\u8017\u6982\u89C8\uFF08\u8D39\u7528\u4E3A\u4F30\u7B97\uFF09" }),
959
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-period-grid", children: [
960
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Period, { label: "\u4ECA\u65E5", period: usage.summary.today, previous: usage.summary.yesterday, compare: "\u8F83\u6628\u65E5" }),
961
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Period, { label: "\u672C\u6708", period: usage.summary.month, previous: usage.summary.lastMonthToDate, compare: "\u8F83\u4E0A\u6708\u540C\u671F" }),
962
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2402
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2403
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("overview.title") }),
2404
+ usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-period-grid", children: [
2405
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Period, { label: t("overview.today"), period: usage.summary.today, previous: usage.summary.yesterday, compare: t("overview.vsYesterday") }),
2406
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Period, { label: t("overview.month"), period: usage.summary.month, previous: usage.summary.lastMonthToDate, compare: t("overview.vsLastMonth") }),
2407
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
963
2408
  Period,
964
2409
  {
965
- label: "\u7D2F\u8BA1",
2410
+ label: t("overview.total"),
966
2411
  period: { total: usage.totals.total, cost: usage.totals.cost, calls: usage.totals.calls }
967
2412
  }
968
2413
  )
969
- ] }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-period-grid", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-period", children: [
970
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 40, h: 11 }),
971
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 104, h: 24 }),
972
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 132, h: 11 })
973
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u7EDF\u8BA1\u7684\u6D88\u8017\u3002" }),
974
- usage !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PricingNote, { pricing: usage.pricing })
2414
+ ] }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-period-grid", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-period", children: [
2415
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 40, h: 11 }),
2416
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 104, h: 24 }),
2417
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 132, h: 11 })
2418
+ ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("overview.empty") }),
2419
+ usage !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2420
+ BudgetMeter,
2421
+ {
2422
+ spent: usage.summary.month.cost,
2423
+ budget: monthlyBudget,
2424
+ onConfigure: () => budgetInputRef.current?.focus()
2425
+ }
2426
+ ),
2427
+ usage !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PricingNote, { pricing: usage.pricing })
975
2428
  ] }),
976
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
977
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "DSH \u7528\u91CF\uFF08tokens\uFF09" }),
978
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
979
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-usage-totals", children: [
980
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Stat, { label: "\u8F93\u5165", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-value", children: fmtCompact(usage.totals.input) }) }),
981
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Stat, { label: "\u8F93\u51FA", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-value", children: fmtCompact(usage.totals.output) }) }),
982
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Stat, { label: "\u7F13\u5B58\u547D\u4E2D", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-value", children: fmtCompact(usage.totals.cache) }) }),
983
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Stat, { label: "\u6A21\u578B\u8C03\u7528", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-stat-value", children: fmtInt(usage.totals.calls) }) })
2429
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2430
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card-head", children: [
2431
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("usage.trendTitle") }),
2432
+ usage !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card-actions", children: [
2433
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UsageWindowPicker, { value: windowDays, onChange: changeWindowDays }),
2434
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ExportMenu, { usage })
2435
+ ] })
2436
+ ] }),
2437
+ usage !== null && activeWindow !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2438
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-usage-totals", children: [
2439
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("usage.windowInput", { window: activeWindowName }), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value", children: fmtCompact(activeWindow.totals.input, locale) }) }),
2440
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("common.output"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value", children: fmtCompact(activeWindow.totals.output, locale) }) }),
2441
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("common.cacheHit"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value", children: fmtCompact(activeWindow.totals.cache, locale) }) }),
2442
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("usage.modelCalls"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value", children: fmtInt(activeWindow.totals.calls) }) })
984
2443
  ] }),
985
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-block-head", children: [
986
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-title", children: [
987
- barMode === "daily" ? "\u8FD1 30 \u5929 \xB7 \u9010\u5929\u7528\u91CF" : "\u6309\u5C0F\u65F6\u5206\u5E03\uFF080\u201323 \u70B9\uFF09",
988
- chartPeak > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-chart-peak", children: [
989
- "\u5CF0\u503C ",
990
- fmtCompact(chartPeak),
991
- " tokens"
992
- ] })
2444
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-chart-block-head", children: [
2445
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-chart-title", children: [
2446
+ barMode === "daily" ? t("chart.dailyTitle", { window: activeWindowName, metric: chartMetricName(chartMetric, t) }) : t("chart.hourlyTitle", { window: activeWindowName, metric: chartMetricName(chartMetric, t) }),
2447
+ chartPeak > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-chart-peak", children: t("chart.peak", { value: chartPeakText }) })
993
2448
  ] }),
994
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-controls", children: [
995
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ModelPicker, { models: modelList, selected: effectiveSelection, colorOf, onChange: setSelectedModels }),
996
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-chart-switch", role: "tablist", "aria-label": "\u5207\u6362\u56FE\u8868\u7EF4\u5EA6", children: [
997
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2449
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-chart-controls", children: [
2450
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ModelPicker, { models: modelList, selected: effectiveSelection, colorOf, onChange: setSelectedModels }),
2451
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChartMetricPicker, { value: chartMetric, onChange: changeChartMetric }),
2452
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-chart-switch", role: "tablist", "aria-label": t("chart.dimensionLabel"), children: [
2453
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
998
2454
  "button",
999
2455
  {
1000
2456
  type: "button",
@@ -1002,10 +2458,10 @@ function BalanceDashboard() {
1002
2458
  "aria-selected": barMode === "daily",
1003
2459
  className: `dq-chart-switch-btn${barMode === "daily" ? " dq-chart-switch-btn--on" : ""}`,
1004
2460
  onClick: () => setBarMode("daily"),
1005
- children: "\u9010\u5929"
2461
+ children: t("chart.daily")
1006
2462
  }
1007
2463
  ),
1008
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2464
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1009
2465
  "button",
1010
2466
  {
1011
2467
  type: "button",
@@ -1013,81 +2469,104 @@ function BalanceDashboard() {
1013
2469
  "aria-selected": barMode === "hourly",
1014
2470
  className: `dq-chart-switch-btn${barMode === "hourly" ? " dq-chart-switch-btn--on" : ""}`,
1015
2471
  onClick: () => setBarMode("hourly"),
1016
- children: "\u9010\u5C0F\u65F6"
2472
+ children: t("chart.hourly")
1017
2473
  }
1018
2474
  )
1019
2475
  ] })
1020
2476
  ] })
1021
2477
  ] }),
1022
- grouped.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1023
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2478
+ activeWindow.totals.calls === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("chart.noWindowData", { window: activeWindowName }) }) : grouped.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2479
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1024
2480
  GroupedBars,
1025
2481
  {
1026
2482
  series: grouped,
1027
2483
  height: barMode === "daily" ? 120 : 100,
1028
- labelEvery: barMode === "daily" ? 5 : 3
2484
+ labelEvery: barMode === "daily" ? dailyLabelEvery : 3,
2485
+ minWidth: barMode === "daily" ? dailyMinWidth : void 0
1029
2486
  }
1030
2487
  ),
1031
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-legend", children: grouped.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dq-legend-item", children: [
1032
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-legend-swatch", style: { background: s.color } }),
2488
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-legend", children: grouped.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-legend-item", children: [
2489
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-legend-swatch", style: { background: s.color } }),
1033
2490
  s.name
1034
2491
  ] }, s.key)) })
1035
- ] }) : barMode === "daily" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Bars, { data: dailyBars, height: 120, labelEvery: 5 }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Bars, { data: hourlyBars, height: 100, labelEvery: 3 }),
1036
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-chart-title", children: "\u8FD1 12 \u5468 \xB7 \u6BCF\u65E5\u7528\u91CF\u70ED\u529B\u56FE" }),
1037
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Heatmap, { data: usage.heatmap })
1038
- ] }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(UsageSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u7528\u91CF\u8BB0\u5F55\u3002\u5728 DSH \u91CC\u8DD1\u4E00\u8F6E\u5BF9\u8BDD\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u9010\u5929 / \u9010\u5C0F\u65F6\u7EDF\u8BA1\u4E0E\u8D39\u7528\u4F30\u7B97\u3002" })
2492
+ ] }) : barMode === "daily" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Bars, { data: dailyBars, height: 120, labelEvery: dailyLabelEvery, minWidth: dailyMinWidth }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Bars, { data: hourlyBars, height: 100, labelEvery: 3 }),
2493
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CoverageDiagnostics, { coverage: usage.coverage })
2494
+ ] }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UsageSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("chart.empty") })
1039
2495
  ] }),
1040
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
1041
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u9AD8\u5CF0 / \u95F2\u65F6\u5206\u5E03" }),
1042
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeakCard, { split: usage.peakSplit, pricing: usage.pricing, currentCost: usage.totals.cost }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1043
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-peak-track" }),
1044
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-peak-figures", children: [
1045
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 120, h: 11 }),
1046
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 96, h: 20 })
1047
- ] })
1048
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u6309\u65F6\u6BB5\u5F52\u7C7B\u7684\u7528\u91CF\u3002" })
2496
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2497
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("heatmap.title") }),
2498
+ usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2499
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-chart-title", children: t("chart.heatmapTitle") }),
2500
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Heatmap, { data: usage.heatmap })
2501
+ ] }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(HeatmapSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("chart.empty") })
1049
2502
  ] }),
1050
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
1051
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u7F13\u5B58\u547D\u4E2D\u4E0E\u8282\u7701" }),
1052
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CacheCard, { totals: usage.totals }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-cache-figures", children: [60, 96, 72].map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-stat", children: [
1053
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w, h: 11 }),
1054
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 72, h: 20 })
1055
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709 prompt token \u53EF\u7EDF\u8BA1\u7F13\u5B58\u547D\u4E2D\u3002" })
2503
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2504
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("peak.title") }),
2505
+ usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PeakCard, { split: usage.peakSplit, pricing: usage.pricing, currentCost: usage.totals.cost }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2506
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-track" }),
2507
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-figures", children: [
2508
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 120, h: 11 }),
2509
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 96, h: 20 })
2510
+ ] })
2511
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("peak.empty") })
1056
2512
  ] }),
1057
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
1058
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u6A21\u578B\u6210\u672C\u6392\u884C" }),
1059
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ModelRanking, { models: modelList, totalCost: modelCostTotal, colorOf }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-rank", children: [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-rank-row", children: [
1060
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 148, h: 13 }),
1061
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-rank-track" }),
1062
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 230, h: 11 })
1063
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u5F52\u7C7B\u7684\u6A21\u578B\u7528\u91CF\u3002" })
2513
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2514
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("cache.title") }),
2515
+ usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CacheCard, { totals: usage.totals }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-cache-figures", children: [60, 96, 72].map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2516
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w, h: 11 }),
2517
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 72, h: 20 })
2518
+ ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("cache.empty") })
1064
2519
  ] }),
1065
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
1066
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u4F1A\u8BDD\u6210\u672C\u6392\u884C" }),
1067
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SessionRanking, { sessions: usage.sessions, count: usage.sessionCount, totalCost: usage.totals.cost }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-sessions", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-session", children: [
1068
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 200, h: 13 }),
1069
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-session-track" }),
1070
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Skel, { w: 250, h: 11 })
1071
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u4EA7\u751F\u8D39\u7528\u7684\u4F1A\u8BDD\u3002" })
2520
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2521
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("models.title", { window: activeWindowName }) }),
2522
+ activeWindow !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ModelRanking, { models: modelList, totalCost: modelCostTotal, colorOf }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank", children: [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-rank-row", children: [
2523
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 148, h: 13 }),
2524
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-rank-track" }),
2525
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 230, h: 11 })
2526
+ ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("models.empty") })
1072
2527
  ] }),
1073
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
1074
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u5B98\u65B9\u5E73\u53F0" }),
1075
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-links", children: [
1076
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Link, { href: "https://platform.deepseek.com/usage", children: "\u67E5\u770B\u989D\u5EA6 / \u7528\u91CF" }),
1077
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Link, { href: "https://platform.deepseek.com/api_keys", children: "\u751F\u6210 API Key" }),
1078
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Link, { href: "https://status.deepseek.com", children: "\u670D\u52A1\u72B6\u6001" })
1079
- ] })
2528
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2529
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("sessions.title", { window: activeWindowName }) }),
2530
+ activeWindow !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SessionRanking, { sessions: activeWindow.sessions, count: activeWindow.sessionCount, totalCost: activeWindow.totals.cost }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-sessions", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-session", children: [
2531
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 200, h: 13 }),
2532
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-session-track" }),
2533
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 250, h: 11 })
2534
+ ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("sessions.empty") })
1080
2535
  ] }),
1081
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-card", children: [
1082
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-card-title", children: "\u8BBE\u7F6E" }),
1083
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "dq-toggle", children: [
1084
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "checkbox", checked: widgetOn, onChange: toggle }),
1085
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "\u663E\u793A\u53F3\u4E0B\u89D2\u60AC\u6D6E\u989D\u5EA6\u7A97\u53E3" })
2536
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2537
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("settings.title") }),
2538
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-links-title", children: t("links.title") }),
2539
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-links", children: [
2540
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://platform.deepseek.com/usage", children: t("links.usage") }),
2541
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://platform.deepseek.com/api_keys", children: t("links.apiKey") }),
2542
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Link, { href: "https://status.deepseek.com", children: t("links.status") })
2543
+ ] }),
2544
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-toggle", children: [
2545
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", checked: widgetOn, onChange: toggle }),
2546
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("settings.widget") })
2547
+ ] }),
2548
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-toggle-hint", children: t("settings.widgetHint") }),
2549
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting dq-setting--tabs", children: [
2550
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-label", id: "dq-widget-tabs-label", children: t("settings.widgetTabs") }),
2551
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control dq-widget-tabs", role: "group", "aria-labelledby": "dq-widget-tabs-label", children: [
2552
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-widget-tab-list", children: widgetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-widget-tab", children: [
2553
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2554
+ "input",
2555
+ {
2556
+ type: "checkbox",
2557
+ checked: widgetTabIds === null || widgetTabIds.includes(tab.id),
2558
+ onChange: () => toggleWidgetTab(tab.id)
2559
+ }
2560
+ ),
2561
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: tab.label })
2562
+ ] }, tab.id)) }),
2563
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: widgetTabs.length === 0 ? t("settings.widgetTabsEmpty") : t("settings.widgetTabsHint") })
2564
+ ] })
1086
2565
  ] }),
1087
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-setting", children: [
1088
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-low-balance", children: "\u4F59\u989D\u9884\u8B66\u7EBF" }),
1089
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dq-setting-control", children: [
1090
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2566
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2567
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-low-balance", children: t("settings.lowBalance") }),
2568
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2569
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1091
2570
  "input",
1092
2571
  {
1093
2572
  id: "dq-low-balance",
@@ -1099,16 +2578,77 @@ function BalanceDashboard() {
1099
2578
  onChange: (e) => changeLowBalance(Math.max(0, Number(e.target.value) || 0))
1100
2579
  }
1101
2580
  ),
1102
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dq-setting-hint", children: lowBalance > 0 ? `\u4F59\u989D\u4F4E\u4E8E ${fmt(lowBalance)} ${primary?.currency ?? "CNY"} \u65F6\uFF0C\u8FD9\u91CC\u548C\u60AC\u6D6E\u7A97\u90FD\u4F1A\u8F6C\u4E3A\u8B66\u793A\u8272\u3002\u586B 0 \u5173\u95ED\u3002` : "\u5DF2\u5173\u95ED\u4F59\u989D\u9884\u8B66\u3002\u586B\u4E00\u4E2A\u5927\u4E8E 0 \u7684\u6570\u5F00\u542F\u3002" })
2581
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: lowBalance > 0 ? t("settings.lowBalanceOn", { amount: fmt(lowBalance), currency: primary?.currency ?? "CNY" }) : t("settings.lowBalanceOff") })
2582
+ ] })
2583
+ ] }),
2584
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2585
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-monthly-budget", children: t("settings.monthlyBudget") }),
2586
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
2587
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2588
+ "input",
2589
+ {
2590
+ ref: budgetInputRef,
2591
+ id: "dq-monthly-budget",
2592
+ className: "dq-number",
2593
+ type: "number",
2594
+ min: 0,
2595
+ step: 10,
2596
+ value: monthlyBudget,
2597
+ "aria-describedby": "dq-monthly-budget-hint",
2598
+ onChange: (e) => changeMonthlyBudget(Math.max(0, Number(e.target.value) || 0))
2599
+ }
2600
+ ),
2601
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { id: "dq-monthly-budget-hint", className: "dq-setting-hint", children: monthlyBudget > 0 ? t("settings.monthlyBudgetOn", { amount: fmt(monthlyBudget) }) : t("settings.monthlyBudgetOff") })
1103
2602
  ] })
1104
2603
  ] })
1105
2604
  ] })
1106
2605
  ] });
1107
2606
  }
1108
2607
 
2608
+ // src/client/message-cost.tsx
2609
+ var import_react5 = require("react");
2610
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2611
+ var fmtTurnCost = (cost) => cost.toLocaleString(void 0, {
2612
+ minimumFractionDigits: cost > 0 && cost < 0.01 ? 4 : 2,
2613
+ maximumFractionDigits: cost > 0 && cost < 0.01 ? 6 : 4
2614
+ });
2615
+ var cachedData = (sessionId) => {
2616
+ const response = getCachedSessionUsage(sessionId);
2617
+ return response?.ok === true ? response.data ?? null : null;
2618
+ };
2619
+ function MessageCost({ sessionId, messageId }) {
2620
+ const { t } = useI18n();
2621
+ const [data, setData] = (0, import_react5.useState)(() => cachedData(sessionId));
2622
+ (0, import_react5.useEffect)(() => {
2623
+ setData(cachedData(sessionId));
2624
+ const unsubscribe = subscribeSessionUsage(sessionId, () => setData(cachedData(sessionId)));
2625
+ const hit = cachedData(sessionId)?.turns.some((turn2) => turn2.messageId === messageId) ?? false;
2626
+ if (!hit) void fetchSessionUsage(sessionId);
2627
+ return unsubscribe;
2628
+ }, [messageId, sessionId]);
2629
+ const turn = data?.turns.find((candidate) => candidate.messageId === messageId);
2630
+ if (turn === void 0) return null;
2631
+ const amount = `\xA5 ${fmtTurnCost(turn.cost)}`;
2632
+ const tooltip = t("messageCost.tooltip");
2633
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2634
+ "span",
2635
+ {
2636
+ className: "dq-message-cost",
2637
+ tabIndex: 0,
2638
+ "data-dq-tooltip": tooltip,
2639
+ "aria-label": t("messageCost.aria", { amount }),
2640
+ children: [
2641
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-message-cost-separator", "aria-hidden": true, children: "\xB7" }),
2642
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-message-cost-amount", children: amount })
2643
+ ]
2644
+ }
2645
+ );
2646
+ }
2647
+
1109
2648
  // src/client/styles.ts
1110
2649
  var css = `
1111
- .dsh-quota-root{position:absolute;right:16px;bottom:16px;z-index:2147483000;pointer-events:auto;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary,#1f2328);transition:left .28s cubic-bezier(.22,1,.36,1),top .28s cubic-bezier(.22,1,.36,1)}
2650
+ .dsh-quota-root{position:absolute;right:16px;bottom:16px;z-index:2147483000;pointer-events:auto;opacity:1;visibility:visible;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary,#1f2328);transition:left .28s cubic-bezier(.22,1,.36,1),top .28s cubic-bezier(.22,1,.36,1),opacity .16s ease-out,visibility 0s linear}
2651
+ .dsh-quota-root.dsh-quota-root--hidden{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .14s ease-out,visibility 0s linear .14s}
1112
2652
  .dsh-quota-root.dsh-quota-dragging{transition:none}
1113
2653
  .dsh-quota-card,.dsh-quota-card *{box-sizing:border-box}
1114
2654
  .dsh-quota-card{background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;min-width:208px;max-width:270px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none}
@@ -1122,7 +2662,8 @@ var css = `
1122
2662
  .dsh-quota-dot--warn{background:var(--dsw-alias-state-warning-primary,#bf8700)}
1123
2663
  .dsh-quota-total--low{color:var(--dsw-alias-state-warning-primary,#9a6700)!important}
1124
2664
  .dsh-quota-dot--idle{background:var(--dsw-alias-label-secondary,#59636e);opacity:.6}
1125
- .dsh-quota-collapsed-total{font-weight:700;font-size:13px;color:var(--dsw-alias-brand-primary,#0969da);flex:none;font-variant-numeric:tabular-nums}
2665
+ .dsh-quota-collapsed-total-row{margin-top:6px}
2666
+ .dsh-quota-collapsed-total{font-weight:700;font-size:15px;color:var(--dsw-alias-state-business-primary,#0969da);font-variant-numeric:tabular-nums}
1126
2667
  .dsh-quota-actions{display:flex;gap:2px;flex:none}
1127
2668
  .dsh-quota-btn{border:none;background:transparent;cursor:pointer;color:var(--dsw-alias-label-secondary,#59636e);padding:3px 7px;border-radius:6px;font-size:13px;line-height:1}
1128
2669
  .dsh-quota-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.06));color:var(--dsw-alias-label-primary,#1f2328)}
@@ -1137,27 +2678,128 @@ var css = `
1137
2678
  .dsh-quota-label{color:var(--dsw-alias-label-secondary,#59636e)}
1138
2679
  .dsh-quota-value{color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
1139
2680
  .dsh-quota-error{color:var(--dsw-alias-state-error-primary,#cf222e)}
1140
- .dq-balance{display:flex;flex-direction:column;gap:16px;padding:20px 24px 48px;max-width:860px;margin:0 auto;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328)}
2681
+ .dq-message-cost{position:relative;display:inline-flex;align-items:center;order:10;gap:10px;height:28px;color:var(--dsw-alias-label-tertiary,#59636e);font-size:12px;line-height:20px;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:help}
2682
+ .dq-message-cost-separator{cursor:default}
2683
+ .dq-message-cost-amount{text-decoration-line:underline;text-decoration-style:dotted;text-decoration-color:currentColor;text-underline-offset:4px}
2684
+ .dq-message-cost:hover{color:var(--dsw-alias-label-secondary,#3f4852)}
2685
+ .dq-message-cost:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px;border-radius:4px}
2686
+ .dq-message-cost::after{content:attr(data-dq-tooltip);position:absolute;left:0;top:calc(100% + 6px);z-index:2147483001;width:max-content;max-width:min(240px,calc(100vw - 24px));padding:6px 9px;border-radius:8px;background:var(--dsw-alias-bg-inverse,#1f2328);color:var(--dsw-alias-label-inverse,#fff);box-shadow:0 6px 18px rgba(0,0,0,.22);font-size:12px;font-weight:400;line-height:1.45;text-align:left;white-space:normal;pointer-events:none;opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s ease-out,transform .12s cubic-bezier(.22,1,.36,1),visibility 0s linear .12s}
2687
+ .dq-message-cost:hover::after,.dq-message-cost:focus-visible::after{opacity:1;visibility:visible;transform:translateY(0);transition:opacity .12s ease-out,transform .12s cubic-bezier(.22,1,.36,1),visibility 0s linear}
2688
+ @media (hover:hover){
2689
+ [data-time-hover-root] .dq-message-cost{opacity:0;transition:opacity 80ms ease}
2690
+ [data-time-hover-root]:hover .dq-message-cost,[data-time-hover-root]:focus-within .dq-message-cost{opacity:1}
2691
+ }
2692
+ /* \u5361\u7247\u95F4\u8DDD\uFF08\u6B64\u5904 gap\uFF09\u523B\u610F\u5927\u4E8E\u5361\u5185\u5C0F\u8282\u95F4\u8DDD\uFF08margin-top:12/14/16px \u90A3\u4E00\u6863\uFF09\uFF0C\u5236\u9020"\u5361\u7247\u5916\u758F\u3001\u5361\u7247\u5185\u5BC6"\u7684
2693
+ \u758F\u5BC6\u5BF9\u6BD4\uFF1A24px \u76F8\u5BF9\u5361\u5185\u6700\u5927\u6863 16px \u4ECD\u6709 1.5 \u500D\u7EA7\u5DEE\uFF0C\u76F8\u5BF9\u5E38\u89C1\u6863 12px \u6709 2 \u500D\u7EA7\u5DEE\uFF0C\u4E0D\u4E0E .dq-card \u7684
2694
+ padding\uFF0816px/20px\uFF0C\u5361\u7247\u5185\u90E8\u547C\u5438\u611F\uFF0C\u8F6E\u6B21 36 \u5DF2\u5B9A\uFF09\u6DF7\u540C\u3002 */
2695
+ .dq-balance{display:flex;flex-direction:column;gap:24px;padding:20px 24px 48px;max-width:860px;margin:0 auto;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328)}
2696
+ @media (max-width:620px){.dq-balance{box-sizing:border-box;max-width:calc(100vw - 56px);padding:16px 16px calc(var(--dq-composer-h,126px) + 16px)}}
1141
2697
  .dq-card{background:var(--dsw-alias-bg-layer-1,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));border-radius:12px;padding:16px}
1142
2698
  .dq-card-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:0 0 12px;letter-spacing:.02em}
2699
+ .dq-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
2700
+ .dq-card-head .dq-card-title{margin:0}
2701
+ .dq-card-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;min-width:0}
2702
+ /* \u4E3B\u5361\u7247\uFF08\u4F59\u989D/\u5F53\u524D\u4F1A\u8BDD\u6D88\u8017/\u6D88\u8017\u6982\u89C8/\u7528\u91CF\u8D8B\u52BF\uFF0C\u8F6E\u6B21 45 \u65B0\u589E\u300C\u5F53\u524D\u4F1A\u8BDD\u6D88\u8017\u300D\uFF09\u89C6\u89C9\u6743\u91CD\u9AD8\u4E8E\u6B21\u7EA7\u5206\u6790
2703
+ \u5361\u7247\u4E0E\u5BFC\u822A\u7C7B\u5361\u7247\uFF1A\u53EA\u8C03\u6392\u7248\u5C5E\u6027\u2014\u2014\u5185\u8FB9\u8DDD\u3001\u6807\u9898\u5B57\u53F7\u5B57\u91CD\u4E0E\u53D6\u8272\u3001\u8FB9\u6846\u6DF1\u6D45\u2014\u2014\u4E0D\u5F15\u5165\u65B0\u914D\u8272\u4F53\u7CFB\u3002\u4E24\u6761\u89C4\u5219
2704
+ \u4E0E\u4E0A\u9762\u7684\u57FA\u7840\u89C4\u5219\u540C\u4E3A\u5355\u7C7B\u9009\u62E9\u5668\u4E14\u6E90\u7801\u987A\u5E8F\u66F4\u9760\u540E\uFF0C\u5929\u7136\u8986\u76D6\uFF0C\u4E0D\u9700\u8981 !important\u3002\u6B21\u7EA7\u5206\u6790\u5361\u7247\uFF08\u7528\u91CF
2705
+ \u70ED\u529B\u56FE/\u9AD8\u5CF0\u95F2\u65F6/\u7F13\u5B58\u547D\u4E2D/\u6A21\u578B\u6392\u884C/\u4F1A\u8BDD\u6392\u884C\uFF09\u4E0E\u5BFC\u822A\u7C7B\u5361\u7247\uFF08\u8BBE\u7F6E\uFF0C\u542B\u8F6E\u6B21 39 \u5E76\u5165\u7684\u5B98\u65B9\u5E73\u53F0\u94FE\u63A5\uFF09\u90FD\u4E0D\u52A0
2706
+ \u8FD9\u4E2A\u4FEE\u9970\u7C7B\uFF0C\u7EF4\u6301\u539F\u6709 .dq-card \u57FA\u7EBF\uFF0C\u5F62\u6210\u4E24\u6863\u6743\u91CD\u3002\u300C\u5F53\u524D\u4F1A\u8BDD\u6D88\u8017\u300D\u5347\u7EA7\u4E3A\u4E3B\u5361\u7247\u7684\u7406\u7531\uFF1A\u5B83\u548C\u4F59\u989D\u4E00\u6837\u662F
2707
+ \u6253\u5F00\u8FD9\u4E2A\u300C\u989D\u5EA6\u300Dtab \u65F6\u4E0A\u4E0B\u6587\u6700\u76F4\u63A5\u76F8\u5173\u7684\u6570\u5B57\uFF08\u8FD9\u4E2A tab \u6302\u5728\u5177\u4F53\u4F1A\u8BDD\u5185\u90E8\uFF0C\u89C1 DESIGN_NOTES\u300C\u4FE1\u606F\u5C42\u7EA7\u300D\uFF09\uFF0C
2708
+ \u4E0D\u5C5E\u4E8E\u539F\u6709\u8D26\u6237\u5168\u5C40\u5C42\u7EA7 1-3 \u7684\u4EFB\u4F55\u4E00\u7EA7\uFF0C\u800C\u662F\u4E0E\u4E4B\u6B63\u4EA4\u7684\u53E6\u4E00\u4E2A\u7EF4\u5EA6\uFF0C\u4F46\u540C\u6837\u662F\u9996\u5C4F\u5FC5\u8BFB\uFF0C\u56E0\u6B64\u540C\u6837\u7ED9
2709
+ primary \u6743\u91CD\uFF0C\u4E0D\u65B0\u5F00\u7B2C\u4E09\u6863\u3002 */
2710
+ .dq-card--primary{padding:20px;border-color:var(--dsw-alias-border-l2,rgba(0,0,0,.12))}
2711
+ .dq-card--primary .dq-card-title{font-size:13px;font-weight:700;color:var(--dsw-alias-label-primary,#1f2328)}
2712
+ .dq-window-switch{display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.08))}
2713
+ .dq-window-btn{border:0;border-radius:6px;padding:4px 8px;background:transparent;color:var(--dsw-alias-label-secondary,#59636e);font-size:12px;white-space:nowrap;cursor:pointer;transition:background .15s ease,color .15s ease,box-shadow .15s ease}
2714
+ .dq-window-btn:hover{color:var(--dsw-alias-label-primary,#1f2328)}
2715
+ .dq-window-btn--on{background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1f2328);box-shadow:0 1px 3px rgba(0,0,0,.12)}
2716
+ .dq-window-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
2717
+ @media (max-width:620px){.dq-card-head{align-items:flex-start;flex-wrap:wrap}.dq-card-actions{width:100%;justify-content:space-between}}
2718
+ .dq-export{position:relative;flex:none}
2719
+ .dq-export-btn{min-width:58px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#59636e);border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer;transition:border-color .15s ease,color .15s ease,background .15s ease}
2720
+ .dq-export-btn:hover{border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}
2721
+ .dq-export-btn--done{border-color:rgba(45,164,78,.35);color:var(--dsw-alias-state-success-primary,#1a7f37);background:rgba(45,164,78,.08)}
2722
+ .dq-export-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:30;min-width:180px;padding:6px;background:var(--dsw-alias-bg-overlay,#fff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);display:flex;flex-direction:column;gap:2px}
2723
+ .dq-export-menu button{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;border:0;border-radius:6px;padding:7px 8px;background:transparent;color:var(--dsw-alias-label-primary,#1f2328);font-size:12px;text-align:left;cursor:pointer}
2724
+ .dq-export-menu button:hover,.dq-export-menu button:focus-visible{background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.06))}
2725
+ .dq-export-menu button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:-1px}
2726
+ .dq-export-menu small{font-size:10.5px;font-weight:400;color:var(--dsw-alias-label-tertiary,#59636e);white-space:nowrap}
2727
+ /* Small-text tier system (\u8F6E\u6B21 41 \u6536\u655B): two whole-pixel tiers replace what used to be four values
2728
+ (11/11.5/12/12.5px) spaced only 0.5px apart \u2014 a gap browsers subpixel-round away, so it read as one
2729
+ size with extra maintenance cost, not an actual visual hierarchy. 11px = "micro" chrome the user
2730
+ scans rather than reads (dt-style stat/period labels, delta/status/coverage pills, share %, tags,
2731
+ the widget grip) \u2014 kept as-is. 12px = "secondary text" (section titles, hint/footnote/legend/sub
2732
+ prose, table cells, tooltip copy, alert/empty-state sentences, and filter/segmented-control button
2733
+ labels like .dq-window-btn/.dq-chart-switch-btn, which are the same control family and should share
2734
+ one size) \u2014 absorbed the old 11.5px and 12.5px selectors, all by rounding toward the nearer tier
2735
+ (11.5\u219212 grows slightly for legibility, safe because none of the affected containers are width-
2736
+ capped; 12.5\u219212 shrinks by 0.5px on two full-sentence blocks, imperceptible). Weight/color still do
2737
+ the job of separating titles from body text within the 12px tier, same as before this merge. Do not
2738
+ reintroduce a fractional-px value in this range; if a selector needs to stand apart from these two,
2739
+ it belongs in an already-established different tier (10px icon glyphs, 13px+ hero-adjacent text). */
1143
2740
  .dq-balance-grid{display:flex;flex-wrap:wrap;gap:12px 24px}
1144
2741
  .dq-stat{min-width:110px}
1145
2742
  .dq-stat-label{font-size:11px;color:var(--dsw-alias-label-tertiary,#59636e);margin-bottom:2px}
1146
2743
  .dq-stat-value{font-size:18px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
1147
2744
  .dq-stat-value--ok{color:var(--dsw-alias-state-success-primary,#2da44e)}
1148
2745
  .dq-stat-value--bad{color:var(--dsw-alias-state-error-primary,#cf222e)}
1149
- .dq-runway{cursor:help;text-decoration:underline dotted var(--dsw-alias-border-l2,rgba(0,0,0,.25));text-underline-offset:4px}
1150
- .dq-remaining{position:relative;cursor:help}
2746
+ /* \u8F6E\u6B21 43: .dq-runway is a controlled <details> (see dashboard.tsx) \u2014 hover/toggle state
2747
+ drives the real open attribute now, so .dq-runway-detail only needs to react to [open];
2748
+ an unopened <details>'s non-summary children don't actually render in current Chromium no
2749
+ matter what display value author CSS gives them (:hover{display:block} alone is a dead end). */
2750
+ .dq-runway{position:relative}
2751
+ .dq-runway>summary{display:inline-block;list-style:none;border-radius:4px;cursor:pointer;text-decoration:underline dotted var(--dsw-alias-border-l2,rgba(0,0,0,.25));text-underline-offset:4px}
2752
+ .dq-runway>summary::-webkit-details-marker{display:none}
2753
+ .dq-runway>summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:3px}
2754
+ .dq-runway-detail{display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:20;max-width:240px;padding:8px 10px;background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.15);white-space:normal;font-size:12px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328)}
2755
+ .dq-runway[open] .dq-runway-detail{display:block}
2756
+ /* hero number for the balance, matching .dq-period-cost; relies on cascade order (declared after
2757
+ .dq-stat-value, same specificity) to win over the .dq-stat-value it's paired with in JSX \u2014 keep it below */
2758
+ /* \u8F6E\u6B21 47: .dq-remaining is a controlled <details> too now (see dashboard.tsx), same reason as
2759
+ .dq-runway above \u2014 an unopened <details>'s non-summary children don't actually render in current
2760
+ Chromium regardless of author display value, so the old :hover{display:flex} branch was dead code. */
2761
+ .dq-remaining{position:relative;font-size:22px;font-weight:680;letter-spacing:-.01em;line-height:1.25}
2762
+ .dq-remaining>summary{display:inline-block;list-style:none;border-radius:4px;cursor:pointer;text-decoration:underline dotted var(--dsw-alias-border-l2,rgba(0,0,0,.25));text-underline-offset:4px}
2763
+ .dq-remaining>summary::-webkit-details-marker{display:none}
2764
+ .dq-remaining>summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:3px}
1151
2765
  .dq-remaining-breakdown{display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:20;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.15);white-space:nowrap;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary,#1f2328)}
1152
- .dq-remaining:hover .dq-remaining-breakdown{display:flex}
2766
+ .dq-remaining[open] .dq-remaining-breakdown{display:flex}
1153
2767
  .dq-remaining-granted{color:var(--dsw-alias-label-tertiary,#59636e);font-weight:400;font-size:11px}
1154
- .dq-period-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
1155
- @media (max-width:620px){.dq-period-grid{grid-template-columns:1fr}}
1156
- .dq-period{background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.06));border-radius:10px;padding:12px 14px;min-width:0}
2768
+ /* narrow screens: .dq-balance-grid wraps its 3 stats onto separate lines, so the absolutely-positioned
2769
+ .dq-remaining-breakdown/.dq-runway-detail popovers can float down over whichever stat now sits in the
2770
+ next row (e.g. "\u4F59\u989D\u660E\u7EC6" open covering "\u9884\u8BA1\u53EF\u7528"'s clickable summary) \u2014 force each stat onto its own
2771
+ full-width row and switch both popovers to normal in-flow layout so an open one pushes its own row
2772
+ taller instead of overlaying a sibling; desktop keeps the original hover-overlay behavior unchanged */
2773
+ @media (max-width:620px){
2774
+ .dq-balance-grid .dq-stat{flex:1 1 100%}
2775
+ .dq-remaining-breakdown,.dq-runway-detail{position:static;margin-top:6px;box-shadow:none;max-width:none;white-space:normal}
2776
+ }
2777
+ .dq-period-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0}
2778
+ .dq-period{min-width:0;padding:4px 16px}
2779
+ .dq-period:first-child{padding-left:0}
2780
+ .dq-period:last-child{padding-right:0}
2781
+ .dq-period+.dq-period{border-left:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
2782
+ @media (max-width:620px){.dq-period-grid{grid-template-columns:1fr}.dq-period,.dq-period:first-child,.dq-period:last-child{padding:12px 0}.dq-period:first-child{padding-top:2px}.dq-period+.dq-period{border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));border-left:0}}
1157
2783
  .dq-period-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}
1158
2784
  .dq-period-label{font-size:11px;color:var(--dsw-alias-label-tertiary,#59636e)}
1159
2785
  .dq-period-cost{font-size:22px;font-weight:680;letter-spacing:-.01em;color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums;line-height:1.25}
1160
- .dq-period-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);margin-top:2px;font-variant-numeric:tabular-nums;line-height:1.45}
2786
+ .dq-period-sub{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);margin-top:2px;font-variant-numeric:tabular-nums;line-height:1.45}
2787
+ .dq-budget{margin-top:12px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
2788
+ .dq-budget--unset{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e)}
2789
+ .dq-budget-action{border:0;background:transparent;padding:3px 4px;border-radius:5px;color:var(--dsw-alias-state-business-primary,#0969da);font:inherit;font-weight:600;white-space:nowrap;cursor:pointer}
2790
+ .dq-budget-action:hover{text-decoration:underline;text-underline-offset:3px}
2791
+ .dq-budget-action:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px}
2792
+ .dq-budget-head,.dq-budget-meta{display:flex;align-items:center;justify-content:space-between;gap:12px}
2793
+ .dq-budget-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328)}
2794
+ .dq-budget-amount{font-size:12px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
2795
+ .dq-budget-track{height:8px;margin-top:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.12));overflow:hidden}
2796
+ .dq-budget-fill{width:100%;height:100%;border-radius:999px;background:var(--dsw-alias-state-success-primary,#2da44e);transform-origin:left center;transition:transform .35s cubic-bezier(.22,1,.36,1)}
2797
+ .dq-budget--risk .dq-budget-fill{background:var(--dsw-alias-state-warning-primary,#bf8700)}
2798
+ .dq-budget--over .dq-budget-fill{background:var(--dsw-alias-state-error-primary,#cf222e)}
2799
+ .dq-budget-meta{margin-top:6px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums}
2800
+ .dq-budget--risk .dq-budget-meta span:last-child{color:var(--dsw-alias-state-warning-primary,#9a6700);font-weight:600}
2801
+ .dq-budget--over .dq-budget-meta{color:var(--dsw-alias-state-error-primary,#cf222e);font-weight:600}
2802
+ @media (max-width:620px){.dq-budget-head,.dq-budget-meta,.dq-budget--unset{align-items:flex-start;flex-direction:column;gap:4px}}
1161
2803
  .dq-delta{display:inline-flex;align-items:center;gap:3px;font-size:11px;font-weight:600;padding:1px 6px;border-radius:999px;white-space:nowrap;cursor:help;font-variant-numeric:tabular-nums}
1162
2804
  .dq-delta--up{color:var(--dsw-alias-state-warning-primary,#9a6700);background:rgba(191,135,0,.14)}
1163
2805
  .dq-delta--down{color:var(--dsw-alias-state-success-primary,#1a7f37);background:rgba(45,164,78,.14)}
@@ -1173,31 +2815,51 @@ var css = `
1173
2815
  .dq-pricing-now{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;color:var(--dsw-alias-label-tertiary,#59636e);background:rgba(120,130,150,.14)}
1174
2816
  .dq-pricing-now--peak{color:var(--dsw-alias-state-warning-primary,#9a6700);background:rgba(191,135,0,.14)}
1175
2817
  .dq-pricing-body{margin-top:10px;overflow-x:auto}
1176
- .dq-pricing-table{border-collapse:collapse;font-size:11.5px;font-variant-numeric:tabular-nums;min-width:100%}
2818
+ .dq-pricing-table{border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums;min-width:100%}
1177
2819
  .dq-pricing-table th{text-align:left;font-weight:500;color:var(--dsw-alias-label-tertiary,#59636e);padding:4px 12px 4px 0;white-space:nowrap;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
1178
2820
  .dq-pricing-table td{padding:4px 12px 4px 0;color:var(--dsw-alias-label-primary,#1f2328);white-space:nowrap;vertical-align:top}
1179
2821
  .dq-pricing-table tbody tr+tr td{border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.05))}
1180
- .dq-pricing-foot{margin:8px 0 0;font-size:11.5px;line-height:1.6;color:var(--dsw-alias-label-secondary,#59636e)}
2822
+ .dq-pricing-foot{margin:8px 0 0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,#59636e)}
2823
+ .dq-coverage{margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
2824
+ .dq-coverage-summary{display:flex;align-items:center;flex-wrap:wrap;gap:6px 9px;cursor:pointer;list-style:none;width:fit-content;max-width:100%;margin-left:-4px;padding:3px 4px;border-radius:6px;color:var(--dsw-alias-label-secondary,#59636e)}
2825
+ .dq-coverage-summary::-webkit-details-marker{display:none}
2826
+ .dq-coverage-summary::before{content:"\u25B8";display:inline-block;font-size:10px;opacity:.7;transition:transform .15s ease}
2827
+ .dq-coverage[open] .dq-coverage-summary::before{transform:rotate(90deg)}
2828
+ .dq-coverage-summary:hover{color:var(--dsw-alias-label-primary,#1f2328)}
2829
+ .dq-coverage-summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
2830
+ .dq-coverage-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328)}
2831
+ .dq-coverage-status{padding:1px 7px;border-radius:999px;background:rgba(45,164,78,.14);color:var(--dsw-alias-state-success-primary,#1a7f37);font-size:11px;font-weight:600}
2832
+ .dq-coverage--warn .dq-coverage-status{background:rgba(191,135,0,.14);color:var(--dsw-alias-state-warning-primary,#9a6700)}
2833
+ .dq-coverage-brief{font-size:12px;font-variant-numeric:tabular-nums}
2834
+ .dq-coverage-body{margin-top:10px;padding:11px 12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.06))}
2835
+ .dq-coverage-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px 20px;margin:0}
2836
+ .dq-coverage-metrics div{min-width:0}
2837
+ .dq-coverage-metrics dt{font-size:11px;color:var(--dsw-alias-label-tertiary,#59636e)}
2838
+ .dq-coverage-metrics dd{margin:3px 0 0;font-size:13px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
2839
+ .dq-coverage-range,.dq-coverage-note,.dq-coverage-warning{margin:9px 0 0;font-size:12px;line-height:1.55;color:var(--dsw-alias-label-secondary,#59636e)}
2840
+ .dq-coverage-range{font-variant-numeric:tabular-nums}
2841
+ .dq-coverage-warning{padding:8px 10px;border-radius:8px;background:rgba(191,135,0,.10);color:var(--dsw-alias-state-warning-primary,#7a5200)}
2842
+ @media (max-width:620px){.dq-coverage-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.dq-coverage-brief{flex-basis:100%;margin-left:19px}}
1181
2843
  .dq-peak-track{height:8px;border-radius:999px;background:var(--dsw-alias-state-success-primary,#2da44e);opacity:.85;overflow:hidden}
1182
2844
  .dq-peak-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-warning-primary,#bf8700);transition:width .35s cubic-bezier(.22,1,.36,1)}
1183
- .dq-peak-legend{display:flex;flex-wrap:wrap;gap:6px 20px;margin-top:8px;font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums}
2845
+ .dq-peak-legend{display:flex;flex-wrap:wrap;gap:6px 20px;margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums}
1184
2846
  .dq-peak-legend span{display:inline-flex;align-items:center;gap:6px}
1185
2847
  .dq-legend-swatch--peak{background:var(--dsw-alias-state-warning-primary,#bf8700)}
1186
2848
  .dq-legend-swatch--offpeak{background:var(--dsw-alias-state-success-primary,#2da44e);opacity:.85}
1187
2849
  .dq-peak-figures{display:flex;flex-wrap:wrap;gap:12px 32px;margin-top:14px}
1188
2850
  .dq-peak-delta{font-size:11px;font-weight:500;margin-left:8px;color:var(--dsw-alias-state-warning-primary,#9a6700)}
1189
2851
  .dq-peak-delta--save{color:var(--dsw-alias-state-success-primary,#1a7f37)}
1190
- .dq-peak-foot{margin:10px 0 0;font-size:11.5px;line-height:1.6;color:var(--dsw-alias-label-secondary,#59636e)}
2852
+ .dq-peak-foot{margin:10px 0 0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,#59636e)}
1191
2853
  .dq-cache-figures{display:flex;flex-wrap:wrap;gap:12px 32px;margin-bottom:12px}
1192
2854
  .dq-muted-value{color:var(--dsw-alias-label-secondary,#59636e)!important;font-weight:550}
1193
2855
  .dq-stat-value--warn{color:var(--dsw-alias-state-warning-primary,#9a6700)}
1194
2856
  .dq-cache-track{height:8px;border-radius:999px;background:var(--dsw-alias-state-warning-primary,#bf8700);overflow:hidden}
1195
2857
  .dq-cache-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-success-primary,#2da44e);transition:width .35s cubic-bezier(.22,1,.36,1)}
1196
- .dq-cache-legend{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:8px;font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums}
2858
+ .dq-cache-legend{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums}
1197
2859
  .dq-cache-legend span{display:inline-flex;align-items:center;gap:6px}
1198
2860
  .dq-legend-swatch--hit{background:var(--dsw-alias-state-success-primary,#2da44e)}
1199
2861
  .dq-legend-swatch--miss{background:var(--dsw-alias-state-warning-primary,#bf8700)}
1200
- .dq-cache-foot{margin:10px 0 0;font-size:11.5px;line-height:1.6;color:var(--dsw-alias-label-secondary,#59636e)}
2862
+ .dq-cache-foot{margin:10px 0 0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,#59636e)}
1201
2863
  .dq-sessions{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}
1202
2864
  .dq-session{display:flex;flex-direction:column;gap:5px;min-width:0}
1203
2865
  .dq-session-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
@@ -1205,8 +2867,32 @@ var css = `
1205
2867
  .dq-session-cost{font-size:14px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums;flex:none}
1206
2868
  .dq-session-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.12));overflow:hidden}
1207
2869
  .dq-session-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#4176e6);transition:width .35s cubic-bezier(.22,1,.36,1)}
1208
- .dq-session-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums;line-height:1.5}
1209
- .dq-session-foot{margin:12px 0 0;font-size:11.5px;color:var(--dsw-alias-label-tertiary,#59636e)}
2870
+ .dq-session-sub{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums;line-height:1.5}
2871
+ /* The \u300C\u5F53\u524D\u4F1A\u8BDD\u6D88\u8017\u300Dcard uses this class on a bare <p> (no flex-gap parent
2872
+ to lean on, unlike the .dq-session usage above), so it needs its own
2873
+ explicit spacing rather than falling back to the browser's implicit <p>
2874
+ margin \u2014 this project always declares spacing explicitly. */
2875
+ p.dq-session-sub{margin:8px 0}
2876
+ .dq-session-foot{margin:12px 0 0;font-size:12px;color:var(--dsw-alias-label-tertiary,#59636e)}
2877
+ /* \u8F6E\u6B21 46: each ranking row is now a <details> whose <summary> wraps the existing
2878
+ head/track/sub trio unchanged (in a flex-column body div) plus a leading chevron \u2014
2879
+ same disclosure grammar as .dq-pricing/.dq-coverage above (hidden native marker,
2880
+ rotating glyph, :focus-visible ring), just applied to a multi-line summary instead
2881
+ of a single line. .dq-session-details needs its own min-width:0 because it is now
2882
+ the flex item inside .dq-session that the ellipsis chain must shrink through. */
2883
+ .dq-session-details{min-width:0}
2884
+ .dq-session-details>summary{list-style:none;cursor:pointer}
2885
+ .dq-session-details>summary::-webkit-details-marker{display:none}
2886
+ .dq-session-summary{display:flex;align-items:flex-start;gap:8px;border-radius:8px;padding:4px;margin:-4px}
2887
+ .dq-session-summary:hover{background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.05))}
2888
+ .dq-session-summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
2889
+ .dq-session-chevron{flex:none;width:12px;margin-top:4px;font-size:10px;line-height:1;color:var(--dsw-alias-label-tertiary,#59636e);transition:transform .15s ease}
2890
+ .dq-session-details[open] .dq-session-chevron{transform:rotate(90deg)}
2891
+ .dq-session-summary-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px}
2892
+ /* Expanded body reuses the same shaded-inset language as .dq-coverage-body (10px
2893
+ gap from the summary, 11px/12px padding, 10px radius, bg-layer-2) rather than
2894
+ inventing a new "nested panel" look. SessionUsageCard renders inside unchanged. */
2895
+ .dq-session-expand{margin-top:10px;padding:11px 12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.06))}
1210
2896
  .dq-rank{display:flex;flex-direction:column;gap:14px}
1211
2897
  .dq-rank-row{display:flex;flex-direction:column;gap:5px;min-width:0}
1212
2898
  .dq-rank-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
@@ -1215,31 +2901,49 @@ var css = `
1215
2901
  .dq-rank-share{font-size:11px;font-weight:500;color:var(--dsw-alias-label-tertiary,#59636e);min-width:30px;text-align:right}
1216
2902
  .dq-rank-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.12));overflow:hidden}
1217
2903
  .dq-rank-fill{height:100%;border-radius:999px;transition:width .35s cubic-bezier(.22,1,.36,1)}
1218
- .dq-rank-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums;line-height:1.5}
1219
- .dq-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-state-warning-primary,#7a5200);background:rgba(191,135,0,.12);border:1px solid rgba(191,135,0,.35)}
2904
+ .dq-rank-sub{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums;line-height:1.5}
2905
+ .dq-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;font-size:12px;line-height:1.6;color:var(--dsw-alias-state-warning-primary,#7a5200);background:rgba(191,135,0,.12);border:1px solid rgba(191,135,0,.35)}
1220
2906
  .dq-alert-icon{flex:none;width:18px;height:18px;border-radius:50%;background:var(--dsw-alias-state-warning-primary,#bf8700);color:#fff;font-weight:700;font-size:12px;line-height:18px;text-align:center}
1221
2907
  .dq-alert strong{font-weight:650}
1222
2908
  .dq-alert-link{margin-left:8px;color:inherit;text-decoration:underline;text-underline-offset:2px;white-space:nowrap}
1223
2909
  .dq-setting{display:flex;align-items:flex-start;gap:12px;margin-top:14px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
1224
2910
  .dq-setting-label{font-size:13px;color:var(--dsw-alias-label-primary,#1f2328);padding-top:6px;flex:none}
1225
2911
  .dq-setting-control{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
1226
- .dq-setting-hint{font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);line-height:1.5}
2912
+ .dq-setting-hint{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);line-height:1.5}
1227
2913
  .dq-number{width:82px;padding:5px 8px;font-size:13px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
1228
2914
  .dq-number:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;border-color:transparent}
2915
+ .dq-links-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 8px}
1229
2916
  .dq-links{display:flex;flex-wrap:wrap;gap:8px}
1230
2917
  .dq-link{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-state-business-primary,#0969da);text-decoration:none;font-size:13px;font-weight:500;cursor:pointer}
1231
2918
  .dq-link:hover{border-color:var(--dsw-alias-state-business-primary,#0969da);background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}
1232
- .dq-toggle{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13px}
2919
+ /* .dq-toggle is the first settings control after the links row (\u8F6E\u6B21 39 merge) \u2014 its own
2920
+ border-top separates the "\u5FEB\u6377\u5165\u53E3" links (originally the standalone \u5B98\u65B9\u5E73\u53F0 card, renamed
2921
+ in \u8F6E\u6B21 39 fix-round-1 to read as part of \u8BBE\u7F6E's own navigation area) from \u8BBE\u7F6E controls
2922
+ inside the merged card, same 14px/14px/border-l1 recipe as .dq-setting so the two
2923
+ sub-sections read at equal weight. */
2924
+ .dq-toggle{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13px;margin-top:14px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
1233
2925
  .dq-toggle input{width:16px;height:16px;accent-color:var(--dsw-alias-state-business-primary,#0969da);cursor:pointer}
1234
- .dq-toggle input:focus-visible,.dq-model-item input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
1235
- .dq-link:focus-visible,.dq-model-btn:focus-visible,.dq-chart-switch-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
2926
+ .dq-toggle-hint{margin:5px 0 0 26px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary,#59636e)}
2927
+ .dq-widget-tabs{align-items:flex-start;flex-direction:column;gap:7px}
2928
+ .dq-widget-tab-list{display:flex;align-items:center;flex-wrap:wrap;gap:6px 14px}
2929
+ .dq-widget-tab{display:inline-flex;align-items:center;gap:7px;min-height:28px;color:var(--dsw-alias-label-primary,#1f2328);font-size:12px;cursor:pointer}
2930
+ .dq-widget-tab input{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#0969da);cursor:pointer;flex:none}
2931
+ .dq-toggle input:focus-visible,.dq-model-item input:focus-visible,.dq-widget-tab input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
2932
+ .dq-link:focus-visible,.dq-model-btn:focus-visible,.dq-chart-switch-btn:focus-visible,.dq-export-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
1236
2933
  .dq-usage-totals{display:flex;flex-wrap:wrap;gap:12px 24px;margin-bottom:16px}
2934
+ /* value itself (number+unit, e.g. "9999.9\u4E07") must never break across lines inside its own column \u2014
2935
+ nowrap+ellipsis is a defensive floor for all widths; the actual fix for the realistic 320px 2-col
2936
+ case is the font-size drop below, which keeps every measured stress value comfortably under the
2937
+ 83px column width without ever hitting the ellipsis fallback (see round 38 PROGRESS entry) */
2938
+ .dq-usage-totals .dq-stat-value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2939
+ @media (max-width:620px){.dq-usage-totals{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 24px}.dq-usage-totals .dq-stat{min-width:0}.dq-usage-totals .dq-stat-value{font-size:13px}}
1237
2940
  .dq-chart-peak{margin-left:10px;font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary,#59636e);font-variant-numeric:tabular-nums}
1238
- .dq-chart-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:18px 0 8px}
2941
+ .dq-chart-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:16px 0 8px}
1239
2942
  .dq-chart-title:first-of-type{margin-top:0}
1240
- .dq-chart-block-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:18px 0 8px}
2943
+ .dq-chart-block-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:16px 0 8px}
1241
2944
  .dq-chart-block-head .dq-chart-title{margin:0}
1242
- .dq-chart-controls{display:flex;align-items:center;gap:8px;flex:none}
2945
+ .dq-chart-controls{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex:none;flex-wrap:wrap}
2946
+ @media (max-width:620px){.dq-chart-block-head{align-items:flex-start;flex-direction:column}.dq-chart-controls{width:100%}}
1243
2947
  .dq-model-picker{position:relative}
1244
2948
  .dq-model-btn{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#59636e);border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px}
1245
2949
  .dq-model-btn:hover{border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}
@@ -1251,8 +2955,11 @@ var css = `
1251
2955
  .dq-model-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1252
2956
  .dq-model-item--all{border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));padding-bottom:8px;margin-bottom:2px;border-radius:6px}
1253
2957
  .dq-model-tag{color:var(--dsw-alias-label-tertiary,#59636e);font-size:11px;flex:none}
1254
- .dq-bar-group{display:flex;align-items:flex-end;gap:2px;height:100%;width:100%}
1255
- .dq-bar-group .dq-bar{flex:1 1 0;min-width:0}
2958
+ .dq-bar-group{display:flex;flex-direction:column-reverse;width:100%}
2959
+ .dq-bar-group .dq-bar{flex:0 0 auto;width:100%;border-radius:0}
2960
+ .dq-bar-group .dq-bar:first-child{border-radius:0 0 2px 2px}
2961
+ .dq-bar-group .dq-bar:last-child{border-radius:2px 2px 0 0}
2962
+ .dq-bar-group .dq-bar:not(:last-child){box-shadow:inset 0 1px 0 rgba(0,0,0,.18)}
1256
2963
  .dq-legend{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e)}
1257
2964
  .dq-legend-item{display:inline-flex;align-items:center;gap:6px}
1258
2965
  .dq-legend-swatch{width:10px;height:10px;border-radius:3px;flex:none}
@@ -1261,27 +2968,34 @@ var css = `
1261
2968
  .dq-chart-switch-btn:hover{color:var(--dsw-alias-label-primary,#1f2328)}
1262
2969
  .dq-chart-switch-btn--on{background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1f2328);box-shadow:0 1px 3px rgba(0,0,0,.14)}
1263
2970
  .dq-chart-wrap{position:relative}
1264
- .dq-tip{position:absolute;transform:translate(-50%,14px);z-index:40;pointer-events:none;background:var(--dsw-alias-bg-inverse,#1f2328);color:var(--dsw-alias-label-inverse,#fff);border-radius:8px;padding:7px 10px;font-size:11.5px;line-height:1.55;white-space:nowrap;box-shadow:0 6px 20px rgba(0,0,0,.28);font-variant-numeric:tabular-nums}
2971
+ .dq-bars-scroll{overflow-x:auto;overflow-y:hidden;padding-bottom:2px}
2972
+ .dq-tip{position:absolute;transform:translate(-50%,14px);z-index:40;pointer-events:none;background:var(--dsw-alias-bg-inverse,#1f2328);color:var(--dsw-alias-label-inverse,#fff);border-radius:8px;padding:7px 10px;font-size:12px;line-height:1.55;white-space:nowrap;box-shadow:0 6px 20px rgba(0,0,0,.28);font-variant-numeric:tabular-nums}
2973
+ .dq-tip--above{transform:translate(-50%,calc(-100% - 8px))}
1265
2974
  .dq-tip-head{font-weight:650;margin-bottom:2px}
1266
2975
  .dq-tip-row{opacity:.85}
1267
2976
  .dq-bars{display:flex;align-items:flex-end;gap:2px;width:100%}
1268
2977
  .dq-bar-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
1269
2978
  .dq-bar{background:var(--dsw-alias-state-business-primary,#4176e6);border-radius:2px 2px 0 0;min-height:1px;transition:filter .12s ease}
1270
2979
  .dq-bar-col{cursor:default}
2980
+ .dq-bar-col,.dq-bar,.dq-heat-cell{scroll-margin-block-start:24px;scroll-margin-block-end:160px}
1271
2981
  .dq-bar-col:hover .dq-bar{filter:brightness(1.18) saturate(1.1)}
2982
+ .dq-bar-col:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:-2px;border-radius:3px}
2983
+ .dq-bar:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;filter:brightness(1.18) saturate(1.1);position:relative;z-index:2}
2984
+ .dq-bar-col--pinned{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:-2px;border-radius:3px}
2985
+ .dq-bar-col--pinned .dq-bar{filter:brightness(1.18) saturate(1.1)}
2986
+ .dq-bar--pinned{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;filter:brightness(1.18) saturate(1.1);position:relative;z-index:2}
1272
2987
  .dq-heat-cell:not(.dq-heat-cell--pad):hover{outline:1.5px solid var(--dsw-alias-label-primary,#1f2328);outline-offset:1px}
1273
- .dq-bar-label{font-size:9px;color:var(--dsw-alias-label-tertiary,#59636e);text-align:center;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2988
+ .dq-heat-cell:not(.dq-heat-cell--pad):focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;position:relative;z-index:2}
2989
+ .dq-heat-cell--pinned{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;position:relative;z-index:2}
2990
+ .dq-bar-label{font-size:9px;color:var(--dsw-alias-label-tertiary,#59636e);text-align:center;margin-top:3px;overflow:visible;white-space:nowrap}
1274
2991
  .dq-heat{display:flex;flex-direction:column;gap:8px;max-width:100%}
1275
- .dq-heat-grid{display:flex;gap:6px;overflow-x:auto;padding-bottom:2px}
1276
- .dq-heat-dows{display:flex;flex-direction:column;gap:3px;padding-top:17px;flex:none}
1277
- .dq-heat-dow{height:12px;line-height:12px;font-size:9px;color:var(--dsw-alias-label-tertiary,#59636e);text-align:right;width:12px}
1278
- .dq-heat-cols{display:flex;flex-direction:column;gap:2px;min-width:0}
1279
- .dq-heat-months{display:flex;gap:3px;height:12px}
1280
- .dq-heat-month{width:12px;flex:none;font-size:9px;line-height:12px;color:var(--dsw-alias-label-tertiary,#59636e);white-space:nowrap;overflow:visible}
1281
- .dq-heatmap{display:flex;gap:3px;width:max-content}
1282
- .dq-heat-week{display:flex;flex-direction:column;gap:3px}
1283
- .dq-heat-cell{width:12px;height:12px;border-radius:2px;flex:none}
2992
+ .dq-heat-scroll{overflow-x:auto;padding-bottom:2px}
2993
+ .dq-heatmap{display:flex;gap:3px}
2994
+ .dq-heat-week{display:flex;flex:1 1 0;min-width:6px;flex-direction:column;gap:3px}
2995
+ .dq-heat-cell{width:100%;aspect-ratio:1/1;border-radius:3px;flex:none}
1284
2996
  .dq-heat-cell--pad{background:transparent}
2997
+ .dq-heat-months{display:flex;gap:3px;margin-top:4px}
2998
+ .dq-heat-month{flex:1 1 0;min-width:6px;font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary,#59636e);white-space:nowrap;overflow:visible}
1285
2999
  .dq-heat-key{width:12px;height:12px;border-radius:2px;flex:none}
1286
3000
  .dq-heat-scale{display:flex;align-items:center;gap:3px;font-size:10px;color:var(--dsw-alias-label-tertiary,#59636e);align-self:flex-end}
1287
3001
  .dq-heat-scale>span:first-child{margin-right:2px}
@@ -1289,16 +3003,31 @@ var css = `
1289
3003
  .dq-muted{color:var(--dsw-alias-label-secondary,#59636e)}
1290
3004
  .dq-error{color:var(--dsw-alias-state-error-primary,#cf222e)}
1291
3005
  .dq-warn{color:var(--dsw-alias-state-warning-primary,#9a6700)}
1292
- .dq-crash{margin:10px 0 14px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.10));border-radius:8px;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#cf222e);white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
1293
- .dq-empty{color:var(--dsw-alias-label-secondary,#59636e);font-size:12.5px;line-height:1.6;padding:6px 0}
3006
+ .dq-crash{margin:10px 0 14px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.10));border-radius:8px;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#cf222e);white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
3007
+ .dq-empty{color:var(--dsw-alias-label-secondary,#59636e);font-size:12px;line-height:1.6;padding:6px 0}
3008
+ /* Error-state modifier for a card body that has nothing else to show (mirrors the .dq-stat-value--ok/
3009
+ --bad/--warn pattern: same layout as .dq-empty, declared after it so this color wins at equal
3010
+ specificity). Used by the current-session card when its own fetch failed and there is no prior
3011
+ data to fall back to \u2014 the status-row .dq-error/.dq-warn spans are for the account-wide balance/
3012
+ usage fetch, this is the per-card equivalent. */
3013
+ .dq-empty--error{color:var(--dsw-alias-state-error-primary,#cf222e)}
1294
3014
  .dq-skel{background:linear-gradient(90deg,rgba(125,135,155,.10) 25%,rgba(125,135,155,.20) 37%,rgba(125,135,155,.10) 63%);background-size:400% 100%;border-radius:5px;animation:dq-shimmer 1.5s ease-in-out infinite}
1295
3015
  .dq-stat .dq-skel+.dq-skel{margin-top:6px}
1296
3016
  .dq-skel-bars{display:flex;align-items:flex-end;gap:2px;width:100%;height:120px;margin-top:10px}
1297
3017
  .dq-skel-bar{flex:1 1 0;min-width:0;border-radius:2px 2px 0 0}
1298
3018
  @keyframes dq-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}
1299
- @media (prefers-reduced-motion:reduce){.dq-skel{animation:none}.dq-refresh-icon--spin{animation:none}}
1300
3019
  .dq-status-row{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:20px}
1301
- .dq-status-row .dq-muted,.dq-status-row .dq-error{font-size:12px}
3020
+ .dq-status-messages{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0}
3021
+ .dq-status-row .dq-warn,.dq-status-row .dq-error{font-size:12px;line-height:1.45}
3022
+ .dq-sync{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums}
3023
+ .dq-sync-dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-label-tertiary,#8c959f)}
3024
+ .dq-sync--fresh .dq-sync-dot{background:var(--dsw-alias-state-success-primary,#2da44e)}
3025
+ .dq-sync--syncing .dq-sync-dot{background:var(--dsw-alias-state-business-primary,#4176e6);animation:dq-sync-pulse 1.2s ease-in-out infinite}
3026
+ .dq-sync--fallback{color:var(--dsw-alias-state-warning-primary,#9a6700)}
3027
+ .dq-sync--fallback .dq-sync-dot{background:var(--dsw-alias-state-warning-primary,#bf8700)}
3028
+ .dq-sync--error{color:var(--dsw-alias-state-error-primary,#cf222e)}
3029
+ .dq-sync--error .dq-sync-dot{background:var(--dsw-alias-state-error-primary,#cf222e)}
3030
+ @keyframes dq-sync-pulse{50%{opacity:.35;transform:scale(.8)}}
1302
3031
  .dq-refresh-btn{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#59636e);border-radius:8px;padding:5px 10px;font-size:12px;cursor:pointer;transition:border-color .15s ease,color .15s ease}
1303
3032
  .dq-refresh-btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}
1304
3033
  .dq-refresh-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
@@ -1306,41 +3035,27 @@ var css = `
1306
3035
  .dq-refresh-icon{display:inline-block;line-height:1}
1307
3036
  .dq-refresh-icon--spin{animation:dq-spin .9s linear infinite}
1308
3037
  @keyframes dq-spin{to{transform:rotate(360deg)}}
3038
+ @media (prefers-reduced-motion:reduce){
3039
+ .dsh-quota-root,.dsh-quota-root *,
3040
+ .dsh-quota-root *::before,.dsh-quota-root *::after,
3041
+ .dq-message-cost,.dq-message-cost::after,
3042
+ .dq-balance,.dq-balance *,
3043
+ .dq-balance *::before,.dq-balance *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
3044
+ }
1309
3045
  `;
1310
3046
 
1311
3047
  // src/client/widget.tsx
1312
- var import_react4 = require("react");
1313
- var import_jsx_runtime4 = require("react/jsx-runtime");
3048
+ var import_react6 = require("react");
3049
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1314
3050
  var MARGIN = 16;
1315
3051
  var CORNER_KEY = "widget.corner";
1316
3052
  var COLLAPSED_KEY = "widget.collapsed";
1317
3053
  var isCorner = (value) => value === "tl" || value === "tr" || value === "bl" || value === "br";
1318
- function getMainColumn(frame) {
1319
- const anchor = frame.querySelector('[data-slot="conversation"]');
1320
- const column = anchor?.parentElement ?? null;
1321
- return column === frame ? null : column;
1322
- }
1323
- function slotBox(frame, slot, side) {
1324
- const anchor = frame.querySelector(`[data-slot="${slot}"]`);
1325
- if (anchor === null) return null;
1326
- if (side === "child") {
1327
- const rect = anchor.firstElementChild?.getBoundingClientRect() ?? null;
1328
- return rect !== null && rect.height > 0 ? rect : null;
1329
- }
1330
- let el = anchor.parentElement;
1331
- for (let depth = 0; depth < 4 && el !== null && el !== frame; depth++) {
1332
- const rect = el.getBoundingClientRect();
1333
- if (rect.height > 0) return rect;
1334
- el = el.parentElement;
1335
- }
1336
- return null;
1337
- }
1338
3054
  var MIN_REGION_HEIGHT = 140;
1339
3055
  function getBounds(node) {
1340
3056
  const fallback = { left: 0, top: 0, right: 1e5, bottom: 1e5 };
1341
3057
  if (node === null) return fallback;
1342
- const overlay = typeof node.closest === "function" ? node.closest("[data-shell-overlay]") : null;
1343
- const frame = overlay?.parentElement ?? null;
3058
+ const frame = getShellFrame(node);
1344
3059
  if (frame === null) return fallback;
1345
3060
  const frameRect = frame.getBoundingClientRect();
1346
3061
  let left = frameRect.left;
@@ -1379,27 +3094,68 @@ function cornerPos(corner, node, bounds) {
1379
3094
  if (corner === "bl") return { x: xLeft, y: yBottom };
1380
3095
  return { x: xRight, y: yBottom };
1381
3096
  }
1382
- function QuotaWidget() {
1383
- const [visible, setVisible] = (0, import_react4.useState)(widgetVisibleStore.get());
1384
- (0, import_react4.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
1385
- const [lowBalance, setLowBalance] = (0, import_react4.useState)(lowBalanceStore.get());
1386
- (0, import_react4.useEffect)(() => lowBalanceStore.subscribe(() => setLowBalance(lowBalanceStore.get())), []);
3097
+ function QuotaWidget(props) {
3098
+ const { t } = useI18n();
3099
+ const sessionId = props.useSessions((state) => state.current);
3100
+ const [visible, setVisible] = (0, import_react6.useState)(widgetVisibleStore.get());
3101
+ (0, import_react6.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
3102
+ const [quotaViewActive2, setQuotaViewActive] = (0, import_react6.useState)(quotaViewActiveStore.get());
3103
+ (0, import_react6.useEffect)(() => quotaViewActiveStore.subscribe(() => setQuotaViewActive(quotaViewActiveStore.get())), []);
3104
+ const [lowBalance, setLowBalance] = (0, import_react6.useState)(lowBalanceStore.get());
3105
+ (0, import_react6.useEffect)(() => lowBalanceStore.subscribe(() => setLowBalance(lowBalanceStore.get())), []);
3106
+ const [selectedTabIds, setSelectedTabIds] = (0, import_react6.useState)(widgetTabIdsStore.get());
3107
+ (0, import_react6.useEffect)(() => widgetTabIdsStore.subscribe(() => setSelectedTabIds(widgetTabIdsStore.get())), []);
3108
+ const [viewTabs, setViewTabs] = (0, import_react6.useState)(() => props.views.list());
3109
+ (0, import_react6.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
3110
+ const [activeViewId, setActiveViewId] = (0, import_react6.useState)(null);
1387
3111
  const cachedBalance = getCachedBalance();
1388
- const [data, setData] = (0, import_react4.useState)(cachedBalance?.data ?? null);
1389
- const [error, setError] = (0, import_react4.useState)(null);
1390
- const [loading, setLoading] = (0, import_react4.useState)(cachedBalance === null);
1391
- const [collapsed, setCollapsed] = (0, import_react4.useState)(() => loadPref(COLLAPSED_KEY, isBoolean, false));
1392
- const [pos, setPos] = (0, import_react4.useState)(null);
1393
- const [corner, setCorner] = (0, import_react4.useState)(() => loadPref(CORNER_KEY, isCorner, "br"));
1394
- const [dragging, setDragging] = (0, import_react4.useState)(false);
1395
- const rootRef = (0, import_react4.useRef)(null);
1396
- const dragRef = (0, import_react4.useRef)(null);
3112
+ const [data, setData] = (0, import_react6.useState)(cachedBalance?.data ?? null);
3113
+ const [error, setError] = (0, import_react6.useState)(null);
3114
+ const [loading, setLoading] = (0, import_react6.useState)(cachedBalance === null);
3115
+ const [collapsed, setCollapsed] = (0, import_react6.useState)(() => loadPref(COLLAPSED_KEY, isBoolean, false));
3116
+ const [pos, setPos] = (0, import_react6.useState)(null);
3117
+ const [corner, setCorner] = (0, import_react6.useState)(() => loadPref(CORNER_KEY, isCorner, "br"));
3118
+ const [dragging, setDragging] = (0, import_react6.useState)(false);
3119
+ const rootRef = (0, import_react6.useRef)(null);
3120
+ const dragRef = (0, import_react6.useRef)(null);
1397
3121
  const readUsage = () => {
1398
3122
  const cached = getCachedUsage();
1399
3123
  return { data: cached?.ok === true ? cached.data ?? null : null, at: getCachedUsageAt() };
1400
3124
  };
1401
- const [usage, setUsage] = (0, import_react4.useState)(readUsage);
1402
- (0, import_react4.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
3125
+ const [usage, setUsage] = (0, import_react6.useState)(readUsage);
3126
+ (0, import_react6.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
3127
+ const readSessionUsage = (id) => {
3128
+ if (id === void 0) return null;
3129
+ const response = getCachedSessionUsage(id);
3130
+ return response?.ok === true ? response.data ?? null : null;
3131
+ };
3132
+ const [sessionUsage, setSessionUsage] = (0, import_react6.useState)(() => readSessionUsage(sessionId));
3133
+ (0, import_react6.useEffect)(() => {
3134
+ setSessionUsage(readSessionUsage(sessionId));
3135
+ if (sessionId === void 0) return;
3136
+ const unsubscribe = subscribeSessionUsage(sessionId, () => setSessionUsage(readSessionUsage(sessionId)));
3137
+ void fetchSessionUsage(sessionId);
3138
+ return unsubscribe;
3139
+ }, [sessionId]);
3140
+ (0, import_react6.useLayoutEffect)(() => {
3141
+ const viewIds = viewTabs.map((tab) => tab.id);
3142
+ const read = () => {
3143
+ const next = getActiveConversationViewId(getShellFrame(rootRef.current), viewIds);
3144
+ setActiveViewId((previous) => previous === next ? previous : next);
3145
+ };
3146
+ read();
3147
+ const frame = getShellFrame(rootRef.current);
3148
+ let observer = null;
3149
+ if (frame !== null && typeof MutationObserver === "function") {
3150
+ observer = new MutationObserver(read);
3151
+ observer.observe(frame, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-selected"] });
3152
+ }
3153
+ const poll = setInterval(read, 1e3);
3154
+ return () => {
3155
+ observer?.disconnect();
3156
+ clearInterval(poll);
3157
+ };
3158
+ }, [sessionId, viewTabs]);
1403
3159
  const load = async (showLoading, force = false) => {
1404
3160
  if (showLoading) setLoading(true);
1405
3161
  try {
@@ -1409,7 +3165,7 @@ function QuotaWidget() {
1409
3165
  setError(null);
1410
3166
  } else {
1411
3167
  setData(null);
1412
- setError(res.error ?? "\u67E5\u8BE2\u5931\u8D25");
3168
+ setError(res.error ?? "");
1413
3169
  }
1414
3170
  } catch (err) {
1415
3171
  setData(null);
@@ -1418,15 +3174,15 @@ function QuotaWidget() {
1418
3174
  setLoading(false);
1419
3175
  }
1420
3176
  };
1421
- (0, import_react4.useEffect)(() => {
3177
+ (0, import_react6.useEffect)(() => {
1422
3178
  void load(true);
1423
3179
  const id = setInterval(() => {
1424
3180
  void load(false);
1425
3181
  }, 6e4);
1426
3182
  return () => clearInterval(id);
1427
3183
  }, []);
1428
- (0, import_react4.useLayoutEffect)(() => {
1429
- if (!visible) return;
3184
+ (0, import_react6.useLayoutEffect)(() => {
3185
+ if (!visible || quotaViewActive2) return;
1430
3186
  const node = rootRef.current;
1431
3187
  if (node === null) return;
1432
3188
  const place = () => {
@@ -1435,7 +3191,7 @@ function QuotaWidget() {
1435
3191
  setPos((prev) => prev !== null && prev.x === next.x && prev.y === next.y ? prev : next);
1436
3192
  };
1437
3193
  place();
1438
- const frame = node.closest("[data-shell-overlay]")?.parentElement;
3194
+ const frame = getShellFrame(node);
1439
3195
  let observer = null;
1440
3196
  if (frame !== null && typeof ResizeObserver === "function") {
1441
3197
  observer = new ResizeObserver(place);
@@ -1450,7 +3206,7 @@ function QuotaWidget() {
1450
3206
  window.removeEventListener("resize", place);
1451
3207
  clearInterval(poll);
1452
3208
  };
1453
- }, [visible, corner, collapsed]);
3209
+ }, [visible, quotaViewActive2, corner, collapsed]);
1454
3210
  const snapToNearest = (session) => {
1455
3211
  const node = rootRef.current;
1456
3212
  if (node === null) return;
@@ -1545,123 +3301,192 @@ function QuotaWidget() {
1545
3301
  endDrag(true);
1546
3302
  };
1547
3303
  if (!visible) return null;
3304
+ const allowedOnActiveView = activeViewId === null ? sessionId === void 0 : activeViewId !== "balance" && (selectedTabIds === null || selectedTabIds.includes(activeViewId));
3305
+ const hidden = quotaViewActive2 || !allowedOnActiveView;
1548
3306
  const primary = data !== null && data.balances.length > 0 ? data.balances[0] : null;
1549
3307
  const balanceValue = primary !== null ? Number(primary.total) : Number.NaN;
1550
3308
  const low = lowBalance > 0 && Number.isFinite(balanceValue) && balanceValue < lowBalance;
1551
3309
  const dotClass = primary === null ? "dsh-quota-dot dsh-quota-dot--idle" : data?.isAvailable === false ? "dsh-quota-dot dsh-quota-dot--error" : low ? "dsh-quota-dot dsh-quota-dot--warn" : "dsh-quota-dot";
1552
3310
  const ageMinutes = usage.at === null ? null : Math.floor((Date.now() - usage.at) / 6e4);
1553
- const usageAgeText = ageMinutes === null || ageMinutes < 1 ? "\u521A\u521A\u66F4\u65B0" : `${ageMinutes} \u5206\u949F\u524D\u7684\u6570\u636E\uFF0C\u70B9 \u21BB \u66F4\u65B0`;
3311
+ const usageAgeText = ageMinutes === null || ageMinutes < 1 ? t("widget.updatedNow") : t("widget.stale", { minutes: ageMinutes });
3312
+ const sessionCostRow = sessionUsage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-row", title: t("widget.sessionCostTitle"), children: [
3313
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-label", children: t("widget.sessionCost") }),
3314
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "dsh-quota-value", children: [
3315
+ "\xA5 ",
3316
+ fmtTurnCost(sessionUsage.cost)
3317
+ ] })
3318
+ ] });
1554
3319
  let body = null;
1555
3320
  if (!collapsed) {
1556
3321
  if (loading && data === null && error === null) {
1557
- body = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dsh-quota-body", children: "\u67E5\u8BE2\u4E2D\u2026" });
3322
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3323
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { children: t("widget.loading") }),
3324
+ sessionCostRow
3325
+ ] });
1558
3326
  } else if (error !== null && data === null) {
1559
- body = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dsh-quota-body dsh-quota-error", children: error });
3327
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3328
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-error", children: localizeApiError(error, t, "error.query") }),
3329
+ sessionCostRow
3330
+ ] });
1560
3331
  } else if (primary !== null) {
1561
- body = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dsh-quota-body", children: [
1562
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dsh-quota-remaining-label", children: "\u5269\u4F59\u4F59\u989D" }),
1563
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { title: low ? `\u4F59\u989D\u4F4E\u4E8E\u9884\u8B66\u7EBF ${fmt(lowBalance)}` : void 0, children: [
1564
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dsh-quota-total${low ? " dsh-quota-total--low" : ""}`, children: fmt(primary.total) }),
1565
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dsh-quota-currency", children: primary.currency })
3332
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3333
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-remaining-label", children: t("balance.remaining") }),
3334
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { title: low ? t("widget.lowTitle", { amount: fmt(lowBalance) }) : void 0, children: [
3335
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `dsh-quota-total${low ? " dsh-quota-total--low" : ""}`, children: fmt(primary.total) }),
3336
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-currency", children: primary.currency })
1566
3337
  ] }),
1567
- usage.data !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dsh-quota-row", title: `\u4ECA\u65E5\u6D88\u8017\uFF08\u4F30\u7B97\uFF09\xB7 ${usageAgeText}`, children: [
1568
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dsh-quota-label", children: "\u4ECA\u65E5\u6D88\u8017" }),
1569
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dsh-quota-value", children: [
3338
+ usage.data !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-row", title: t("widget.todayCostTitle", { age: usageAgeText }), children: [
3339
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-label", children: t("widget.todayCost") }),
3340
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "dsh-quota-value", children: [
1570
3341
  "\xA5 ",
1571
3342
  fmt(usage.data.summary.today.cost)
1572
3343
  ] })
1573
3344
  ] }),
1574
- data?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dsh-quota-row", children: [
1575
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dsh-quota-label", children: "\u72B6\u6001" }),
1576
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dsh-quota-value dsh-quota-error", children: "\u4E0D\u53EF\u7528" })
3345
+ sessionCostRow,
3346
+ data?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-row", children: [
3347
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-label", children: t("common.status") }),
3348
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-value dsh-quota-error", children: t("common.unavailable") })
1577
3349
  ] })
1578
3350
  ] });
1579
3351
  } else {
1580
- body = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dsh-quota-body dsh-quota-error", children: "\u6682\u65E0\u4F59\u989D\u6570\u636E" });
3352
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3353
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-error", children: t("widget.noBalance") }),
3354
+ sessionCostRow
3355
+ ] });
1581
3356
  }
1582
3357
  }
1583
3358
  const style = pos !== null ? { left: `${pos.x}px`, top: `${pos.y}px`, right: "auto", bottom: "auto" } : void 0;
1584
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `dsh-quota-root${dragging ? " dsh-quota-dragging" : ""}`, style, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
3359
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1585
3360
  "div",
1586
3361
  {
1587
- ref: rootRef,
1588
- className: "dsh-quota-card",
1589
- title: "\u62D6\u52A8\u5230\u56DB\u4E2A\u89D2\u843D",
1590
- onPointerDown,
1591
- onPointerMove,
1592
- onPointerUp,
1593
- onPointerCancel: () => endDrag(false),
1594
- onLostPointerCapture: () => endDrag(false),
1595
- children: [
1596
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dsh-quota-header", children: [
1597
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dsh-quota-title", children: [
1598
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dsh-quota-grip", title: "\u62D6\u52A8\u4EE5\u79FB\u52A8", children: "\u2807" }),
1599
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: dotClass }),
1600
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dsh-quota-name", children: "DeepSeek \u989D\u5EA6" }),
1601
- collapsed && primary !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: `dsh-quota-collapsed-total${low ? " dsh-quota-total--low" : ""}`, children: [
3362
+ className: `dsh-quota-root${dragging ? " dsh-quota-dragging" : ""}${hidden ? " dsh-quota-root--hidden" : ""}`,
3363
+ style,
3364
+ "aria-hidden": hidden,
3365
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3366
+ "div",
3367
+ {
3368
+ ref: rootRef,
3369
+ className: "dsh-quota-card",
3370
+ title: t("widget.dragCard"),
3371
+ onPointerDown,
3372
+ onPointerMove,
3373
+ onPointerUp,
3374
+ onPointerCancel: () => endDrag(false),
3375
+ onLostPointerCapture: () => endDrag(false),
3376
+ children: [
3377
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-header", children: [
3378
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-title", children: [
3379
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-grip", title: t("widget.dragGrip"), children: "\u2807" }),
3380
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: dotClass }),
3381
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-name", children: t("widget.title") })
3382
+ ] }),
3383
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-actions", children: [
3384
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3385
+ "button",
3386
+ {
3387
+ className: "dsh-quota-btn",
3388
+ type: "button",
3389
+ title: t("widget.refresh"),
3390
+ "aria-label": t("widget.refresh"),
3391
+ disabled: loading,
3392
+ onClick: () => {
3393
+ void load(true, true);
3394
+ void fetchUsage(true);
3395
+ if (sessionId !== void 0) void fetchSessionUsage(sessionId, true);
3396
+ },
3397
+ children: "\u21BB"
3398
+ }
3399
+ ),
3400
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3401
+ "button",
3402
+ {
3403
+ className: "dsh-quota-btn",
3404
+ type: "button",
3405
+ title: collapsed ? t("widget.expand") : t("widget.collapse"),
3406
+ "aria-label": collapsed ? t("widget.expandAria") : t("widget.collapseAria"),
3407
+ "aria-expanded": !collapsed,
3408
+ onClick: () => {
3409
+ const next = !collapsed;
3410
+ setCollapsed(next);
3411
+ savePref(COLLAPSED_KEY, next);
3412
+ },
3413
+ children: collapsed ? "+" : "\u2212"
3414
+ }
3415
+ )
3416
+ ] })
3417
+ ] }),
3418
+ collapsed && primary !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-collapsed-total-row", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: `dsh-quota-collapsed-total${low ? " dsh-quota-total--low" : ""}`, children: [
1602
3419
  fmt(primary.total),
1603
3420
  " ",
1604
3421
  primary.currency
1605
- ] })
1606
- ] }),
1607
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dsh-quota-actions", children: [
1608
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1609
- "button",
1610
- {
1611
- className: "dsh-quota-btn",
1612
- type: "button",
1613
- title: "\u5237\u65B0\u4F59\u989D\u4E0E\u4ECA\u65E5\u7528\u91CF",
1614
- "aria-label": "\u5237\u65B0\u4F59\u989D\u4E0E\u4ECA\u65E5\u7528\u91CF",
1615
- disabled: loading,
1616
- onClick: () => {
1617
- void load(true, true);
1618
- void fetchUsage(true);
1619
- },
1620
- children: "\u21BB"
1621
- }
1622
- ),
1623
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1624
- "button",
1625
- {
1626
- className: "dsh-quota-btn",
1627
- type: "button",
1628
- title: collapsed ? "\u5C55\u5F00" : "\u6536\u8D77",
1629
- "aria-label": collapsed ? "\u5C55\u5F00\u989D\u5EA6\u7A97\u53E3" : "\u6536\u8D77\u989D\u5EA6\u7A97\u53E3",
1630
- "aria-expanded": !collapsed,
1631
- onClick: () => {
1632
- const next = !collapsed;
1633
- setCollapsed(next);
1634
- savePref(COLLAPSED_KEY, next);
1635
- },
1636
- children: collapsed ? "+" : "\u2212"
1637
- }
1638
- )
1639
- ] })
1640
- ] }),
1641
- body
1642
- ]
3422
+ ] }) }),
3423
+ body
3424
+ ]
3425
+ }
3426
+ )
1643
3427
  }
1644
- ) });
3428
+ );
3429
+ }
3430
+
3431
+ // src/client/views.ts
3432
+ var labelOf = (entry) => {
3433
+ const { id = "", label } = entry.options;
3434
+ try {
3435
+ const resolved = typeof label === "function" ? label() : label;
3436
+ return typeof resolved === "string" && resolved !== "" ? resolved : id;
3437
+ } catch {
3438
+ return id;
3439
+ }
3440
+ };
3441
+ function conversationViewsSource(slots) {
3442
+ return {
3443
+ list: () => slots.entries("conversation.view").flatMap((entry) => entry.options.id === void 0 ? [] : [{ id: entry.options.id, label: labelOf(entry) }]),
3444
+ subscribe: (fn) => slots.subscribe("conversation.view", fn)
3445
+ };
1645
3446
  }
1646
3447
 
1647
3448
  // src/client/index.tsx
1648
- var import_jsx_runtime5 = require("react/jsx-runtime");
3449
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1649
3450
  var name = "dsh-usage-dashboard";
1650
- var inject = ["slots"];
3451
+ var inject = ["slots", "locale"];
1651
3452
  function apply(ctx) {
3453
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-usage-dashboard: browser dictionaries");
3454
+ const t = ctx.locale.bind(NS);
3455
+ const views = conversationViewsSource(ctx.slots);
1652
3456
  if (typeof document !== "undefined") {
1653
3457
  const tag = document.createElement("style");
1654
3458
  tag.dataset.plugin = "dsh-usage-dashboard";
1655
3459
  tag.textContent = css;
1656
3460
  document.head.appendChild(tag);
1657
3461
  }
3462
+ void fetchBalance().catch(() => {
3463
+ });
3464
+ void fetchUsage().catch(() => {
3465
+ });
1658
3466
  ctx.slots.inject("shell.overlay", () => ctx.slots.register(
1659
- { name: "shell.overlay", id: "deepseek-quota", order: 1e3, label: "DeepSeek \u989D\u5EA6" },
1660
- () => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ErrorBoundary, { silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(QuotaWidget, {}) })
3467
+ { name: "shell.overlay", id: "deepseek-quota", order: 1e3, locale: NS, label: () => t("widget.title") },
3468
+ ({ t: slotT, useSessions }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(QuotaWidget, { useSessions, views }) }) })
3469
+ ));
3470
+ ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register(
3471
+ {
3472
+ name: "conversation.chat.assistant-actions",
3473
+ id: "dsh-usage-turn-cost",
3474
+ order: 5,
3475
+ locale: NS,
3476
+ label: () => t("messageCost.label")
3477
+ },
3478
+ ({ t: slotT, sessionId, messageId }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MessageCost, { sessionId, messageId }) }) })
1661
3479
  ));
1662
3480
  ctx.slots.inject("conversation.view", () => ctx.slots.register(
1663
- { name: "conversation.view", id: "balance", order: 20, label: "\u989D\u5EA6" },
1664
- () => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ErrorBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BalanceDashboard, {}) })
3481
+ { name: "conversation.view", id: "balance", order: 20, locale: NS, label: () => t("nav.quota") },
3482
+ // `conversation.view` is a session-scoped slot, so the host injects its
3483
+ // standard session props (including `sessionId`) alongside `t` — this
3484
+ // plugin only destructures the two fields it needs, per the existing
3485
+ // "hand-write the minimal structural type" convention (see context.ts).
3486
+ // `sessionId` is typed optional defensively even though the host is
3487
+ // expected to always supply it: BalanceDashboard renders the current-
3488
+ // session card only when it actually has one, never throws either way.
3489
+ ({ t: slotT, sessionId }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBoundary, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BalanceDashboard, { sessionId, views }) }) })
1665
3490
  ));
1666
3491
  }
1667
3492
  return module.exports; } });