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