@cassius0924/dsh-usage-dashboard 0.3.0 → 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
@@ -27,23 +27,74 @@ __export(index_exports, {
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
29
 
30
- // src/client/dashboard.tsx
31
- var import_react2 = require("react");
30
+ // src/client/boundary.tsx
31
+ var import_react = require("react");
32
+ var import_jsx_runtime = require("react/jsx-runtime");
33
+ var PREFIX = "dsh-usage-dashboard:";
34
+ function clearPluginStorage() {
35
+ if (typeof localStorage === "undefined") return;
36
+ try {
37
+ for (const key of Object.keys(localStorage)) {
38
+ if (key.startsWith(PREFIX)) localStorage.removeItem(key);
39
+ }
40
+ } catch {
41
+ }
42
+ }
43
+ var ErrorBoundary = class extends import_react.Component {
44
+ state = { message: null };
45
+ static getDerivedStateFromError(error) {
46
+ return { message: error?.message ?? String(error) };
47
+ }
48
+ componentDidCatch(error, info) {
49
+ console.error("[dsh-usage-dashboard] render failed", error, info.componentStack);
50
+ }
51
+ reset = () => {
52
+ clearPluginStorage();
53
+ location.reload();
54
+ };
55
+ render() {
56
+ const { message } = this.state;
57
+ if (message === null) return this.props.children;
58
+ if (this.props.silent === true) return null;
59
+ const { t } = this.props;
60
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-balance", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-card", children: [
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") }),
63
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "dq-crash", children: message }),
64
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dq-refresh-btn", onClick: this.reset, children: t("boundary.reset") })
65
+ ] }) });
66
+ }
67
+ };
32
68
 
33
69
  // src/client/cache.ts
34
- var LS_VERSION = 1;
35
- function loadPersisted(storageKey) {
70
+ var LS_VERSION = 2;
71
+ function loadPersisted(storageKey, isUsable) {
36
72
  if (typeof localStorage === "undefined") return null;
73
+ const key = `${storageKey}:v${LS_VERSION}`;
37
74
  try {
38
- const raw = localStorage.getItem(`${storageKey}:v${LS_VERSION}`);
75
+ const raw = localStorage.getItem(key);
39
76
  if (raw === null) return null;
40
77
  const parsed = JSON.parse(raw);
41
78
  if (parsed.data === void 0 || typeof parsed.at !== "number") return null;
79
+ if (isUsable !== void 0 && !isUsable(parsed.data)) {
80
+ localStorage.removeItem(key);
81
+ return null;
82
+ }
42
83
  return { data: parsed.data, at: parsed.at };
43
84
  } catch {
44
85
  return null;
45
86
  }
46
87
  }
88
+ function pruneOldVersions(storageKey) {
89
+ if (typeof localStorage === "undefined") return;
90
+ try {
91
+ const current = `${storageKey}:v${LS_VERSION}`;
92
+ for (const key of Object.keys(localStorage)) {
93
+ if (key.startsWith(`${storageKey}:v`) && key !== current) localStorage.removeItem(key);
94
+ }
95
+ } catch {
96
+ }
97
+ }
47
98
  function persist(storageKey, hit) {
48
99
  if (typeof localStorage === "undefined") return;
49
100
  try {
@@ -51,8 +102,9 @@ function persist(storageKey, hit) {
51
102
  } catch {
52
103
  }
53
104
  }
54
- function createCache(ttlMs, storageKey) {
55
- let hit = storageKey === void 0 ? null : loadPersisted(storageKey);
105
+ function createCache(ttlMs, storageKey, isUsable) {
106
+ if (storageKey !== void 0) pruneOldVersions(storageKey);
107
+ let hit = storageKey === void 0 ? null : loadPersisted(storageKey, isUsable);
56
108
  return {
57
109
  get: () => hit,
58
110
  getFresh: () => hit !== null && Date.now() - hit.at < ttlMs ? hit.data : null,
@@ -63,6 +115,9 @@ function createCache(ttlMs, storageKey) {
63
115
  };
64
116
  }
65
117
 
118
+ // src/contract.ts
119
+ var USAGE_WINDOW_DAYS = [7, 30, 90, 365];
120
+
66
121
  // src/client/api.ts
67
122
  var BALANCE_TTL_MS = 6e4;
68
123
  var USAGE_TTL_MS = 5 * 6e4;
@@ -73,8 +128,14 @@ async function getJson(path, cache) {
73
128
  }
74
129
  return res.json();
75
130
  }
76
- var balanceCache = createCache(BALANCE_TTL_MS, "dsh-usage-dashboard:balance");
77
- var usageCache = createCache(USAGE_TTL_MS, "dsh-usage-dashboard:usage");
131
+ var usageIsUsable = (res) => {
132
+ if (res.ok !== true) return false;
133
+ const data = res.data;
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;
135
+ };
136
+ var balanceIsUsable = (res) => res.ok === true && Array.isArray(res.data?.balances);
137
+ var balanceCache = createCache(BALANCE_TTL_MS, "dsh-usage-dashboard:balance", balanceIsUsable);
138
+ var usageCache = createCache(USAGE_TTL_MS, "dsh-usage-dashboard:usage", usageIsUsable);
78
139
  var getCachedBalance = () => balanceCache.get()?.data ?? null;
79
140
  var getCachedUsage = () => usageCache.get()?.data ?? null;
80
141
  var getCachedUsageAt = () => usageCache.get()?.at ?? null;
@@ -85,109 +146,830 @@ var subscribeUsage = (fn) => {
85
146
  usageListeners.delete(fn);
86
147
  };
87
148
  };
149
+ var balanceInflight = null;
150
+ var usageInflight = null;
88
151
  async function fetchBalance(force = false) {
89
152
  if (!force) {
90
153
  const hit = balanceCache.getFresh();
91
154
  if (hit !== null) return hit;
155
+ if (balanceInflight !== null) return balanceInflight;
92
156
  }
93
157
  const suffix = force ? "?refresh=1" : "";
94
- const res = await getJson(`/api/dsh-usage-dashboard/balance${suffix}`, force ? "no-store" : "default");
95
- if (res.ok) balanceCache.put(res);
96
- 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;
97
169
  }
98
170
  async function fetchUsage(force = false) {
99
171
  if (!force) {
100
172
  const hit = usageCache.getFresh();
101
173
  if (hit !== null) return hit;
174
+ if (usageInflight !== null) return usageInflight;
102
175
  }
103
176
  const suffix = force ? "?refresh=1" : "";
104
- const res = await getJson(`/api/dsh-usage-dashboard/usage${suffix}`, force ? "no-store" : "default");
105
- if (res.ok) {
106
- usageCache.put(res);
107
- 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;
108
189
  }
109
- return res;
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;
200
+ }
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 };
110
230
  }
111
231
 
112
232
  // src/client/charts.tsx
113
- var import_react = require("react");
114
- var import_jsx_runtime = require("react/jsx-runtime");
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
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
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");
115
795
  function useChartTip() {
116
- const wrapRef = (0, import_react.useRef)(null);
117
- const [tip, setTip] = (0, import_react.useState)(null);
118
- 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) => {
119
806
  const wrap = wrapRef.current;
120
807
  if (text === void 0 || text === "" || wrap === null) return;
121
808
  const rect = wrap.getBoundingClientRect();
122
- const x = Math.min(Math.max(e.clientX - rect.left, 70), Math.max(rect.width - 70, 70));
123
- 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() };
124
828
  };
125
- return { wrapRef, tip, show, hide: () => setTip(null) };
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);
838
+ };
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 };
126
866
  }
127
867
  function ChartTip(props) {
128
868
  const { tip } = props;
129
869
  if (tip === null) return null;
130
870
  const [head, ...rows] = tip.lines;
131
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-tip", style: { left: `${tip.x}px`, top: `${tip.y}px` }, role: "tooltip", children: [
132
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-tip-head", children: head }),
133
- rows.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime.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))
134
874
  ] });
135
875
  }
136
876
  var MODEL_COLORS = ["#4176e6", "#2da44e", "#e16f24", "#8250df", "#bf8700", "#cf222e", "#1f883d", "#0969da"];
137
877
  function Bars(props) {
138
- const { data, height = 120, labelEvery = 1 } = props;
139
- 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]);
140
887
  let max = 0;
141
888
  for (const datum of data) if (datum.value > max) max = datum.value;
142
889
  const scale = max > 0 ? max : 1;
143
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
144
- /* @__PURE__ */ (0, import_jsx_runtime.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) => {
145
892
  const h = Math.max(1, Math.round(datum.value / scale * height));
146
893
  const text = datum.title ?? `${datum.label}: ${datum.value.toLocaleString()}`;
147
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
894
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
148
895
  "div",
149
896
  {
150
- className: "dq-bar-col",
897
+ className: `dq-bar-col${pinned === i ? " dq-bar-col--pinned" : ""}`,
898
+ role: "img",
151
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
+ },
152
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),
153
911
  children: [
154
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-bar", style: { height: `${h}px` } }),
155
- /* @__PURE__ */ (0, import_jsx_runtime.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 })
156
914
  ]
157
915
  },
158
916
  i
159
917
  );
160
- }) }),
161
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChartTip, { tip })
918
+ }) }) }),
919
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChartTip, { tip })
162
920
  ] });
163
921
  }
164
922
  function GroupedBars(props) {
165
- const { series, height = 120, labelEvery = 1 } = props;
166
- 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);
167
926
  const count = series.length > 0 ? series[0].bars.length : 0;
168
- let max = 0;
169
- for (const s of series) for (const datum of s.bars) if (datum.value > max) max = datum.value;
170
- const scale = max > 0 ? max : 1;
171
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
172
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-bars", style: { height: `${height + 18}px` }, children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-bar-col", children: [
173
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-bar-group", children: series.map((s) => {
174
- const datum = s.bars[i];
175
- const h = Math.max(1, Math.round(datum.value / scale * height));
176
- const text = datum.title ?? `${s.key}: ${datum.value.toLocaleString()}`;
177
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
178
- "div",
179
- {
180
- className: "dq-bar",
181
- style: { height: `${h}px`, background: s.color },
182
- "aria-label": text,
183
- onPointerMove: (e) => show(e, text)
184
- },
185
- s.key
186
- );
187
- }) }),
188
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-bar-label", style: { visibility: i % labelEvery === 0 ? "visible" : "hidden" }, children: series[0].bars[i].label })
189
- ] }, i)) }),
190
- /* @__PURE__ */ (0, import_jsx_runtime.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 })
191
973
  ] });
192
974
  }
193
975
  function heatColor(level) {
@@ -197,10 +979,11 @@ function heatColor(level) {
197
979
  if (level === 3) return "rgba(65,118,230,0.75)";
198
980
  return "rgba(65,118,230,1)";
199
981
  }
200
- var DOW_LABELS = ["", "\u4E00", "", "\u4E09", "", "\u4E94", ""];
201
982
  function Heatmap(props) {
202
983
  const { data } = props;
203
- 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);
204
987
  let max = 0;
205
988
  for (const datum of data) if (datum.total > max) max = datum.total;
206
989
  let firstDow = 0;
@@ -221,39 +1004,53 @@ function Heatmap(props) {
221
1004
  const month = first.date.slice(0, 7);
222
1005
  if (month === lastMonth) return "";
223
1006
  lastMonth = month;
224
- return `${Number(first.date.slice(5, 7))}\u6708`;
1007
+ return t(`chart.month${Number(first.date.slice(5, 7))}`);
225
1008
  });
226
1009
  const levelOf = (total) => {
227
1010
  if (total <= 0) return 0;
228
1011
  const r = max > 0 ? total / max : 0;
229
1012
  return r < 0.25 ? 1 : r < 0.5 ? 2 : r < 0.75 ? 3 : 4;
230
1013
  };
231
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-heat dq-chart-wrap", ref: wrapRef, onPointerLeave: hide, children: [
232
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-heat-grid", children: [
233
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-heat-dows", "aria-hidden": "true", children: DOW_LABELS.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-heat-dow", children: label }, i)) }),
234
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-heat-cols", children: [
235
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-heat-months", "aria-hidden": "true", children: monthLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-heat-month", children: label }, i)) }),
236
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-heatmap", children: weeks.map((week, w) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-heat-week", children: week.map((datum, d) => {
237
- if (datum === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dq-heat-cell dq-heat-cell--pad" }, d);
238
- const text = `${datum.date} \xB7 ${datum.total.toLocaleString()} tokens \xB7 \xA5${datum.cost.toFixed(2)} \xB7 ${datum.calls} \u6B21\u8C03\u7528`;
239
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
240
- "div",
241
- {
242
- className: "dq-heat-cell",
243
- style: { background: heatColor(levelOf(datum.total)) },
244
- "aria-label": text,
245
- 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;
246
1036
  },
247
- d
248
- );
249
- }) }, w)) })
250
- ] })
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)) })
251
1048
  ] }),
252
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChartTip, { tip }),
253
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dq-heat-scale", children: [
254
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u5C11" }),
255
- [0, 1, 2, 3, 4].map((level) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dq-heat-key", style: { background: heatColor(level) } }, level)),
256
- /* @__PURE__ */ (0, import_jsx_runtime.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") })
257
1054
  ] })
258
1055
  ] });
259
1056
  }
@@ -263,17 +1060,145 @@ var fmt = (v) => {
263
1060
  return n.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
264
1061
  };
265
1062
  var fmtInt = (v) => Math.round(Number(v) || 0).toLocaleString();
266
- var fmtCompact = (v) => {
1063
+ var fmtCompact = (v, locale = "zh") => {
267
1064
  const n = Number(v) || 0;
1065
+ if (locale === "en") {
1066
+ return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(n);
1067
+ }
268
1068
  if (n >= 1e8) return `${(n / 1e8).toFixed(1)}\u4EBF`;
269
1069
  if (n >= 1e4) return `${(n / 1e4).toFixed(1)}\u4E07`;
270
1070
  return Math.round(n).toLocaleString();
271
1071
  };
272
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
+
273
1198
  // src/client/prefs.ts
274
- var PREFIX = "dsh-usage-dashboard:pref:";
1199
+ var PREFIX2 = "dsh-usage-dashboard:pref:";
275
1200
  var VERSION = 1;
276
- var keyOf = (key) => `${PREFIX}${key}:v${VERSION}`;
1201
+ var keyOf = (key) => `${PREFIX2}${key}:v${VERSION}`;
277
1202
  function loadPref(key, isValid, fallback) {
278
1203
  if (typeof localStorage === "undefined") return fallback;
279
1204
  try {
@@ -316,362 +1241,673 @@ function createStore(key, isValid, fallback) {
316
1241
  var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
317
1242
  var widgetVisibleStore = createStore("widget.visible", isBoolean, true);
318
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
+ };
319
1264
 
320
1265
  // src/client/dashboard.tsx
321
- var import_jsx_runtime2 = require("react/jsx-runtime");
1266
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1267
+ var COMPOSER_FALLBACK_PX = 126;
322
1268
  function Skel(props) {
323
- return /* @__PURE__ */ (0, import_jsx_runtime2.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` } });
324
1270
  }
325
1271
  function BalanceSkeleton() {
326
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-balance-grid", children: [
327
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
328
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 56, h: 11 }),
329
- /* @__PURE__ */ (0, import_jsx_runtime2.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 })
1276
+ ] }),
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 })
330
1292
  ] }),
331
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
332
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 28, h: 11 }),
333
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 48, h: 22 })
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 })
334
1296
  ] })
335
1297
  ] });
336
1298
  }
337
1299
  function UsageSkeleton() {
338
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
339
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-usage-totals", children: [44, 44, 62, 62].map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
340
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w, h: 11 }),
341
- /* @__PURE__ */ (0, import_jsx_runtime2.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 })
342
1304
  ] }, i)) }),
343
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 140, h: 12 }),
344
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-skel-bars", children: Array.from({ length: 30 }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime2.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)) })
345
1313
  ] });
346
1314
  }
347
1315
  function Delta(props) {
1316
+ const { t } = useI18n();
348
1317
  const { current, previous, label } = props;
349
1318
  if (previous <= 0) {
350
1319
  if (current <= 0) return null;
351
- return /* @__PURE__ */ (0, import_jsx_runtime2.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") });
352
1321
  }
353
1322
  const percent = Math.round((current - previous) / previous * 100);
354
1323
  const title = `${label} \xA5${fmt(previous)}`;
355
- if (percent === 0) return /* @__PURE__ */ (0, import_jsx_runtime2.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") });
356
1325
  const up = percent > 0;
357
- return /* @__PURE__ */ (0, import_jsx_runtime2.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: [
358
1327
  up ? "\u2191" : "\u2193",
359
1328
  Math.abs(percent),
360
1329
  "% ",
361
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-delta-label", children: label })
1330
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta-label", children: label })
362
1331
  ] });
363
1332
  }
364
1333
  function Period(props) {
1334
+ const { t, locale } = useI18n();
365
1335
  const { period, previous, compare } = props;
366
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-period", children: [
367
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-period-head", children: [
368
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-period-label", children: props.label }),
369
- previous !== void 0 && compare !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.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 })
370
1340
  ] }),
371
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-period-cost", children: [
1341
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-period-cost", children: [
372
1342
  "\xA5 ",
373
1343
  fmt(period.cost)
374
1344
  ] }),
375
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-period-sub", children: [
376
- fmtCompact(period.total),
377
- " tokens \xB7 ",
378
- fmtInt(period.calls),
379
- " \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 })
380
1389
  ] })
381
1390
  ] });
382
1391
  }
383
1392
  var rate = (value) => `\xA5${value.toLocaleString(void 0, { maximumFractionDigits: 3 })}`;
384
1393
  function PricingNote(props) {
1394
+ const { t } = useI18n();
385
1395
  const { pricing } = props;
386
1396
  const split = pricing.splitActive;
387
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("details", { className: "dq-pricing", children: [
388
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("summary", { className: "dq-pricing-summary", children: [
389
- "\u8BA1\u4EF7\u8BF4\u660E",
390
- split && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: `dq-pricing-now${pricing.inPeakNow ? " dq-pricing-now--peak" : ""}`, children: [
391
- "\u5F53\u524D ",
392
- pricing.inPeakNow ? "\u9AD8\u5CF0\u65F6\u6BB5" : "\u95F2\u65F6"
393
- ] })
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") })
394
1401
  ] }),
395
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-pricing-body", children: [
396
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { className: "dq-pricing-table", children: [
397
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
398
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { children: "\u6A21\u578B" }),
399
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { children: split ? "\u65F6\u6BB5" : "\u5355\u4EF7" }),
400
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { children: "\u8F93\u5165\xB7\u7F13\u5B58\u547D\u4E2D" }),
401
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { children: "\u8F93\u5165\xB7\u672A\u547D\u4E2D" }),
402
- /* @__PURE__ */ (0, import_jsx_runtime2.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") })
403
1410
  ] }) }),
404
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react2.Fragment, { children: [
405
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
406
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { rowSpan: tier.offPeak !== null ? 2 : 1, children: tier.model }),
407
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { children: split ? "\u9AD8\u5CF0" : "\u56FA\u5B9A" }),
408
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { children: rate(tier.peak.cacheHit) }),
409
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { children: rate(tier.peak.input) }),
410
- /* @__PURE__ */ (0, import_jsx_runtime2.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) })
411
1418
  ] }),
412
- tier.offPeak !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
413
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { children: "\u95F2\u65F6" }),
414
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { children: rate(tier.offPeak.cacheHit) }),
415
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { children: rate(tier.offPeak.input) }),
416
- /* @__PURE__ */ (0, import_jsx_runtime2.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) })
417
1424
  ] })
418
1425
  ] }, tier.model)) })
419
1426
  ] }),
420
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "dq-pricing-foot", children: [
421
- "\u5355\u4F4D\uFF1A",
422
- pricing.currency,
423
- " / \u767E\u4E07 tokens\u3002",
424
- 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`,
425
- "\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")
426
1431
  ] })
427
1432
  ] })
428
1433
  ] });
429
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
+ }
430
1480
  function Stat(props) {
431
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
432
- /* @__PURE__ */ (0, import_jsx_runtime2.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 }),
433
1483
  props.children
434
1484
  ] });
435
1485
  }
436
1486
  function Link(props) {
437
- return /* @__PURE__ */ (0, import_jsx_runtime2.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
+ ] });
438
1568
  }
439
1569
  var modelKeyOf = (m) => m.provider !== "" ? `${m.provider}/${m.model}` : m.model;
440
- 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
+ }
441
1635
  function CacheCard(props) {
1636
+ const { t, locale } = useI18n();
442
1637
  const { totals } = props;
443
1638
  const prompt = totals.input + totals.cache;
444
- if (prompt === 0) return /* @__PURE__ */ (0, import_jsx_runtime2.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") });
445
1640
  const rate2 = totals.cache / prompt;
446
1641
  const wouldHaveCost = totals.cost + totals.cacheSavings;
447
1642
  const low = rate2 < 0.6;
448
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-cache", children: [
449
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-cache-figures", children: [
450
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
451
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-label", children: "\u7F13\u5B58\u547D\u4E2D\u7387" }),
452
- /* @__PURE__ */ (0, import_jsx_runtime2.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: [
453
1648
  (rate2 * 100).toFixed(1),
454
1649
  "%"
455
1650
  ] })
456
1651
  ] }),
457
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
458
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-label", children: "\u5DF2\u8282\u7701\u8D39\u7528\uFF08\u4F30\u7B97\uFF09" }),
459
- /* @__PURE__ */ (0, import_jsx_runtime2.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: [
460
1655
  "\xA5 ",
461
1656
  fmt(totals.cacheSavings)
462
1657
  ] })
463
1658
  ] }),
464
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
465
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-label", children: "\u82E5\u5168\u90E8\u672A\u547D\u4E2D" }),
466
- /* @__PURE__ */ (0, import_jsx_runtime2.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: [
467
1662
  "\xA5 ",
468
1663
  fmt(wouldHaveCost)
469
1664
  ] })
470
1665
  ] })
471
1666
  ] }),
472
- /* @__PURE__ */ (0, import_jsx_runtime2.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_runtime2.jsx)("div", { className: "dq-cache-fill", style: { width: `${rate2 * 100}%` } }) }),
473
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-cache-legend", children: [
474
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
475
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--hit" }),
476
- "\u7F13\u5B58\u547D\u4E2D ",
477
- 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) })
478
1672
  ] }),
479
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
480
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--miss" }),
481
- "\u672A\u547D\u4E2D ",
482
- fmtCompact(totals.input),
483
- "\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) })
484
1676
  ] })
485
1677
  ] }),
486
- /* @__PURE__ */ (0, import_jsx_runtime2.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") })
487
1679
  ] });
488
1680
  }
489
1681
  function PeakCard(props) {
1682
+ const { t, locale } = useI18n();
490
1683
  const { split, pricing, currentCost } = props;
491
1684
  const total = split.peak.total + split.offPeak.total;
492
- if (total === 0) return /* @__PURE__ */ (0, import_jsx_runtime2.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") });
493
1686
  const peakShare = split.peak.total / total;
494
1687
  const shiftSaving = split.peakEraCost - split.offPeakEraCost;
495
1688
  const increase = split.peakEraCost - currentCost;
496
1689
  const increasePercent = currentCost > 0 ? increase / currentCost * 100 : 0;
497
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-peak", children: [
498
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1690
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak", children: [
1691
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
499
1692
  "div",
500
1693
  {
501
1694
  className: "dq-peak-track",
502
1695
  role: "img",
503
- "aria-label": `\u9AD8\u5CF0\u65F6\u6BB5 ${(peakShare * 100).toFixed(1)}%\uFF0C\u95F2\u65F6 ${((1 - peakShare) * 100).toFixed(1)}%`,
504
- children: /* @__PURE__ */ (0, import_jsx_runtime2.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}%` } })
505
1698
  }
506
1699
  ),
507
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-peak-legend", children: [
508
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
509
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--peak" }),
510
- "\u9AD8\u5CF0 ",
511
- (peakShare * 100).toFixed(1),
512
- "% \xB7 ",
513
- fmtCompact(split.peak.total),
514
- " tokens \xB7 ",
515
- fmtInt(split.peak.calls),
516
- " \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
+ })
517
1708
  ] }),
518
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
519
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-legend-swatch dq-legend-swatch--offpeak" }),
520
- "\u95F2\u65F6 ",
521
- ((1 - peakShare) * 100).toFixed(1),
522
- "% \xB7 ",
523
- fmtCompact(split.offPeak.total),
524
- " tokens \xB7 ",
525
- fmtInt(split.offPeak.calls),
526
- " \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
+ })
527
1716
  ] })
528
1717
  ] }),
529
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-peak-figures", children: [
530
- !pricing.splitActive && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
531
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-label", children: "\u540C\u6837\u7528\u91CF\u5728\u65B0\u4EF7\u4E0B" }),
532
- /* @__PURE__ */ (0, import_jsx_runtime2.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: [
533
1722
  "\xA5 ",
534
1723
  fmt(split.peakEraCost),
535
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-peak-delta", children: [
536
- "\u8F83\u73B0\u4EF7 +",
537
- increasePercent.toFixed(0),
538
- "%"
539
- ] })
1724
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta", children: t("peak.increase", { percent: increasePercent.toFixed(0) }) })
540
1725
  ] })
541
1726
  ] }),
542
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
543
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-label", children: "\u82E5\u9AD8\u5CF0\u7528\u91CF\u90FD\u632A\u5230\u95F2\u65F6" }),
544
- /* @__PURE__ */ (0, import_jsx_runtime2.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: [
545
1730
  "\xA5 ",
546
1731
  fmt(split.offPeakEraCost),
547
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-peak-delta dq-peak-delta--save", children: [
548
- "\u53EF\u7701 \xA5",
549
- fmt(shiftSaving)
550
- ] })
1732
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta dq-peak-delta--save", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
551
1733
  ] })
552
1734
  ] })
553
1735
  ] }),
554
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "dq-peak-foot", children: [
555
- "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 ",
556
- pricing.peakWindows.join("\u3001"),
557
- "\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\u3002",
558
- 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 })
559
1739
  ] })
560
1740
  ] });
561
1741
  }
562
- function agoText(ms) {
1742
+ function agoText(ms, t) {
563
1743
  const minutes = Math.floor((Date.now() - ms) / 6e4);
564
- if (minutes < 1) return "\u521A\u521A";
565
- 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 });
566
1746
  const hours = Math.floor(minutes / 60);
567
- if (hours < 24) return `${hours} \u5C0F\u65F6\u524D`;
568
- 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, {});
569
1757
  }
570
1758
  function SessionRanking(props) {
1759
+ const { t, locale } = useI18n();
571
1760
  const { sessions, count, totalCost } = props;
572
- if (sessions.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime2.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") });
573
1777
  const top = sessions[0].cost;
574
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
575
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ol", { className: "dq-sessions", children: sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: "dq-session", children: [
576
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-session-head", children: [
577
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-session-title", title: `${s.title}
578
- ${s.id}`, children: s.title }),
579
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-session-cost", children: [
580
- "\xA5 ",
581
- fmt(s.cost)
582
- ] })
583
- ] }),
584
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-session-track", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-session-fill", style: { width: `${Math.max(s.cost / (top || 1) * 100, 1.5)}%` } }) }),
585
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-session-sub", children: [
586
- fmtCompact(s.total),
587
- " tokens \xB7 ",
588
- fmtInt(s.calls),
589
- " \u6B21\u8C03\u7528 \xB7 ",
590
- agoText(s.lastActive),
591
- totalCost > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
592
- " \xB7 \u5360 ",
593
- (s.cost / totalCost * 100).toFixed(1),
594
- "%"
595
- ] })
596
- ] })
597
- ] }, s.id)) }),
598
- count > sessions.length && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "dq-session-foot", children: [
599
- "\u5171 ",
600
- fmtInt(count),
601
- " \u4E2A\u4F1A\u8BDD\u6709\u7528\u91CF\uFF0C\u4E0A\u9762\u662F\u6700\u8D35\u7684 ",
602
- sessions.length,
603
- " \u4E2A\u3002"
604
- ] })
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 }) })
605
1817
  ] });
606
1818
  }
607
1819
  function ModelRanking(props) {
1820
+ const { t, locale } = useI18n();
608
1821
  const { models, totalCost } = props;
609
- if (models.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u5F52\u7C7B\u7684\u6A21\u578B\u7528\u91CF\u3002" });
610
- return /* @__PURE__ */ (0, import_jsx_runtime2.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) => {
611
1824
  const key = modelKeyOf(m);
612
1825
  const share = totalCost > 0 ? m.cost / totalCost : 0;
613
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-rank-row", children: [
614
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-rank-head", children: [
615
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-rank-name", title: key, children: [
616
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-legend-swatch", style: { background: props.colorOf(key) } }),
617
- 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)
618
1831
  ] }),
619
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-rank-cost", children: [
1832
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-cost", children: [
620
1833
  "\xA5 ",
621
1834
  fmt(m.cost),
622
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-rank-share", children: [
1835
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-rank-share", children: [
623
1836
  (share * 100).toFixed(1),
624
1837
  "%"
625
1838
  ] })
626
1839
  ] })
627
1840
  ] }),
628
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1841
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
629
1842
  "div",
630
1843
  {
631
1844
  className: "dq-rank-track",
632
1845
  role: "img",
633
- "aria-label": `${modelNameOf(m)} \u5360\u603B\u8D39\u7528 ${(share * 100).toFixed(1)}%`,
634
- children: /* @__PURE__ */ (0, import_jsx_runtime2.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) } })
635
1848
  }
636
1849
  ),
637
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-rank-sub", children: [
638
- fmtCompact(m.total),
639
- " tokens \xB7 ",
640
- fmtInt(m.calls),
641
- " \u6B21\u8C03\u7528 \xB7 \u8F93\u5165 ",
642
- fmtCompact(m.input),
643
- " / \u8F93\u51FA ",
644
- fmtCompact(m.output),
645
- " / \u7F13\u5B58 ",
646
- fmtCompact(m.cache)
647
- ] })
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
+ }) })
648
1857
  ] }, key);
649
1858
  }) });
650
1859
  }
651
1860
  function ModelPicker(props) {
1861
+ const { t, locale } = useI18n();
652
1862
  const { models } = props;
653
- if (models.length === 0) return null;
654
- const [open, setOpen] = (0, import_react2.useState)(false);
655
- const rootRef = (0, import_react2.useRef)(null);
656
- (0, import_react2.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)(() => {
657
1868
  if (!open) return;
658
1869
  const onDown = (e) => {
659
1870
  if (rootRef.current !== null && !rootRef.current.contains(e.target)) setOpen(false);
660
1871
  };
1872
+ const onKey = (e) => {
1873
+ if (e.key !== "Escape") return;
1874
+ e.preventDefault();
1875
+ setOpen(false);
1876
+ triggerRef.current?.focus();
1877
+ };
661
1878
  document.addEventListener("mousedown", onDown);
662
- 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
+ };
663
1885
  }, [open]);
1886
+ if (models.length === 0) return null;
664
1887
  const toggle = (key) => {
665
1888
  props.onChange(
666
1889
  props.selected.includes(key) ? props.selected.filter((k) => k !== key) : [...props.selected, key]
667
1890
  );
668
1891
  };
669
- const label = props.selected.length === 0 ? "\u5168\u90E8\u6A21\u578B" : `\u6A21\u578B \xD7${props.selected.length}`;
670
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-model-picker", ref: rootRef, children: [
671
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "dq-model-btn", onClick: () => setOpen(!open), children: label }),
672
- open && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-model-menu", children: [
673
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dq-model-item dq-model-item--all", children: [
674
- /* @__PURE__ */ (0, import_jsx_runtime2.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)(
675
1911
  "input",
676
1912
  {
677
1913
  type: "checkbox",
@@ -679,40 +1915,120 @@ function ModelPicker(props) {
679
1915
  onChange: () => props.onChange([])
680
1916
  }
681
1917
  ),
682
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u5168\u90E8\u6A21\u578B" })
1918
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("models.all") })
683
1919
  ] }),
684
1920
  models.map((m) => {
685
1921
  const key = modelKeyOf(m);
686
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dq-model-item", children: [
687
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "checkbox", checked: props.selected.includes(key), onChange: () => toggle(key) }),
688
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-legend-swatch", style: { background: props.colorOf(key) } }),
689
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: modelNameOf(m) }),
690
- /* @__PURE__ */ (0, import_jsx_runtime2.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) })
691
1927
  ] }, key);
692
1928
  })
693
1929
  ] })
694
1930
  ] });
695
1931
  }
696
- 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();
697
1966
  const cachedBalance = getCachedBalance();
698
1967
  const cachedUsage = getCachedUsage();
699
- const [balance, setBalance] = (0, import_react2.useState)(cachedBalance?.data ?? null);
700
- const [usage, setUsage] = (0, import_react2.useState)(cachedUsage?.data ?? null);
701
- const [error, setError] = (0, import_react2.useState)(null);
702
- const [notice, setNotice] = (0, import_react2.useState)(null);
703
- const [loadingBalance, setLoadingBalance] = (0, import_react2.useState)(cachedBalance === null);
704
- const [loadingUsage, setLoadingUsage] = (0, import_react2.useState)(cachedUsage === null);
705
- const [refreshing, setRefreshing] = (0, import_react2.useState)(false);
706
- const hadDataRef = (0, import_react2.useRef)(cachedBalance !== null || cachedUsage !== null);
707
- const [widgetOn, setWidgetOn] = (0, import_react2.useState)(widgetVisibleStore.get());
708
- const [lowBalance, setLowBalance] = (0, import_react2.useState)(lowBalanceStore.get());
709
- const [barMode, setBarMode] = (0, import_react2.useState)("daily");
710
- const [selectedModels, setSelectedModels] = (0, import_react2.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
+ }, []);
711
2025
  const errText = (err) => err?.message ?? String(err);
712
2026
  const load = async (force) => {
2027
+ const usageAtBefore = getCachedUsageAt();
713
2028
  setError(null);
714
2029
  setNotice(null);
715
2030
  setRefreshing(true);
2031
+ setSyncState("syncing");
716
2032
  const balanceTask = fetchBalance(force).then((res) => {
717
2033
  if (res.ok && res.data !== void 0) {
718
2034
  setBalance(res.data);
@@ -727,6 +2043,10 @@ function BalanceDashboard() {
727
2043
  const usageTask = fetchUsage(force).then((res) => {
728
2044
  if (res.ok && res.data !== void 0) {
729
2045
  setUsage(res.data);
2046
+ const updatedAt = getCachedUsageAt();
2047
+ setUsageUpdatedAt(updatedAt);
2048
+ setFreshnessNow(Date.now());
2049
+ setSyncState(updatedAt !== null && updatedAt !== usageAtBefore ? "fresh" : "cached");
730
2050
  hadDataRef.current = true;
731
2051
  return null;
732
2052
  }
@@ -737,18 +2057,62 @@ function BalanceDashboard() {
737
2057
  });
738
2058
  const [balanceError, usageError] = await Promise.all([balanceTask, usageTask]);
739
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
+ }
740
2066
  if (balanceError !== null && usageError !== null) {
741
- 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 });
742
2068
  else setError(balanceError);
743
2069
  } else if (balanceError !== null) {
744
- 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 });
745
2071
  } else if (usageError !== null) {
746
- 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);
747
2076
  }
748
2077
  };
749
- (0, import_react2.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)(() => {
750
2096
  void load(false);
751
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
+ }, []);
752
2116
  const toggle = () => {
753
2117
  const next = !widgetOn;
754
2118
  setWidgetOn(next);
@@ -758,36 +2122,62 @@ function BalanceDashboard() {
758
2122
  setLowBalance(value);
759
2123
  lowBalanceStore.set(value);
760
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
+ };
761
2137
  const primary = balance !== null && balance.balances.length > 0 ? balance.balances[0] : null;
762
2138
  const recentDays = (usage?.daily ?? []).slice(-7);
763
2139
  const avgDailyCost = recentDays.length > 0 ? recentDays.reduce((sum, d) => sum + d.cost, 0) / recentDays.length : 0;
764
2140
  const balanceValue = primary !== null ? Number(primary.total) : Number.NaN;
765
2141
  const balanceLow = lowBalance > 0 && Number.isFinite(balanceValue) && balanceValue < lowBalance;
766
2142
  const daysLeft = avgDailyCost > 0 && Number.isFinite(balanceValue) ? balanceValue / avgDailyCost : null;
767
- const daysLeftText = daysLeft === null ? "\u2014" : daysLeft >= 365 ? "> 365 \u5929" : `${daysLeft < 10 ? daysLeft.toFixed(1) : Math.round(daysLeft)} \u5929`;
768
- 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`;
769
- const dailyBars = (usage?.daily ?? []).map((d) => ({
770
- label: d.date.slice(8, 10),
771
- value: d.total,
772
- 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
+ })
773
2156
  }));
774
- const hourlyBars = (usage?.hourly ?? []).map((h) => ({
2157
+ const hourlyBars = (activeWindow?.hourly ?? []).map((h) => ({
775
2158
  label: String(h.hour),
776
- value: h.total,
777
- 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
+ })
778
2166
  }));
779
2167
  const chartPeak = (barMode === "daily" ? dailyBars : hourlyBars).reduce((peak, bar) => bar.value > peak ? bar.value : peak, 0);
780
- 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);
781
2171
  const availableModelKeys = new Set(modelList.map(modelKeyOf));
782
2172
  const effectiveSelection = selectedModels.filter((k) => availableModelKeys.has(k));
783
2173
  const colorOf = (key) => {
784
- const index = modelList.findIndex((m) => modelKeyOf(m) === key);
2174
+ const index = canonicalModels.findIndex((m) => modelKeyOf(m) === key);
785
2175
  return MODEL_COLORS[(index < 0 ? 0 : index) % MODEL_COLORS.length];
786
2176
  };
787
2177
  const modelCostTotal = modelList.reduce((sum, m) => sum + m.cost, 0);
788
2178
  const grouped = effectiveSelection.map((key) => {
789
2179
  const m = modelList.find((x) => modelKeyOf(x) === key);
790
- const name2 = m === void 0 ? key : modelNameOf(m);
2180
+ const name2 = m === void 0 ? key : modelNameOf(m, t);
791
2181
  const color = colorOf(key);
792
2182
  if (barMode === "daily") {
793
2183
  return {
@@ -795,9 +2185,14 @@ function BalanceDashboard() {
795
2185
  name: name2,
796
2186
  color,
797
2187
  bars: (m?.daily ?? []).map((p, i) => ({
798
- label: (usage?.daily ?? [])[i]?.date.slice(8, 10) ?? "",
799
- value: p.total,
800
- 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
+ })
801
2196
  }))
802
2197
  };
803
2198
  }
@@ -807,134 +2202,174 @@ function BalanceDashboard() {
807
2202
  color,
808
2203
  bars: (m?.hourly ?? []).map((p, i) => ({
809
2204
  label: String(i),
810
- value: p.total,
811
- 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
+ })
812
2212
  }))
813
2213
  };
814
2214
  });
815
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-balance", children: [
816
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-status-row", children: [
817
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
818
- (loadingBalance || loadingUsage) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-muted", children: "\u52A0\u8F7D\u4E2D\u2026" }),
819
- !loadingBalance && !loadingUsage && refreshing && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-muted", children: "\u66F4\u65B0\u4E2D\u2026" }),
820
- notice !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-warn", children: notice }),
821
- error !== null && /* @__PURE__ */ (0, import_jsx_runtime2.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 })
822
2243
  ] }),
823
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2244
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
824
2245
  "button",
825
2246
  {
826
2247
  type: "button",
827
2248
  className: "dq-refresh-btn",
828
- title: "\u5F3A\u5236\u5237\u65B0\uFF08\u7ED5\u8FC7\u7F13\u5B58\uFF09",
2249
+ title: t("status.forceRefresh"),
829
2250
  disabled: refreshing,
830
2251
  "aria-busy": refreshing,
831
2252
  onClick: () => {
832
2253
  void load(true);
833
2254
  },
834
2255
  children: [
835
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `dq-refresh-icon${refreshing ? " dq-refresh-icon--spin" : ""}`, children: "\u21BB" }),
836
- 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")
837
2258
  ]
838
2259
  }
839
2260
  )
840
2261
  ] }),
841
- balanceLow && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-alert", role: "status", children: [
842
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dq-alert-icon", "aria-hidden": "true", children: "!" }),
843
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
844
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
845
- "\u4F59\u989D ",
846
- fmt(balanceValue),
847
- " ",
848
- primary?.currency,
849
- " \u5DF2\u4F4E\u4E8E\u9884\u8B66\u7EBF ",
850
- fmt(lowBalance)
851
- ] }),
852
- daysLeft !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
853
- "\uFF0C\u6309\u8FD1\u671F\u7528\u91CF\u4F30\u8BA1\u8FD8\u80FD\u6491 ",
854
- daysLeftText
855
- ] }),
856
- "\u3002",
857
- /* @__PURE__ */ (0, import_jsx_runtime2.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") })
858
2269
  ] })
859
2270
  ] }),
860
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
861
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u8D26\u6237\u4F59\u989D" }),
862
- primary !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-balance-grid", children: [
863
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
864
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-label", children: "\u5269\u4F59\u4F59\u989D" }),
865
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat-value dq-remaining", children: [
866
- /* @__PURE__ */ (0, import_jsx_runtime2.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: [
867
2278
  fmt(primary.total),
868
2279
  " ",
869
2280
  primary.currency
870
2281
  ] }),
871
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-remaining-breakdown", children: [
872
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
873
- "\u5145\u503C\u989D\u5EA6 ",
874
- fmt(primary.toppedUp)
875
- ] }),
876
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-remaining-granted", children: [
877
- "\u8D60\u9001\u989D\u5EA6 ",
878
- fmt(primary.granted)
879
- ] })
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) }) })
880
2285
  ] })
881
2286
  ] })
882
2287
  ] }),
883
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
884
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-label", children: "\u9884\u8BA1\u53EF\u7528" }),
885
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
886
- "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",
887
2292
  {
888
2293
  className: `dq-stat-value dq-runway${daysLeft !== null && daysLeft < 3 ? " dq-stat-value--bad" : ""}`,
889
- title: runwayTitle,
890
- 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
+ ]
891
2311
  }
892
2312
  )
893
2313
  ] }),
894
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Stat, { label: "\u72B6\u6001", children: /* @__PURE__ */ (0, import_jsx_runtime2.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" }) })
895
- ] }) : loadingBalance ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BalanceSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.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") })
896
2320
  ] }),
897
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
898
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u6D88\u8017\u6982\u89C8\uFF08\u8D39\u7528\u4E3A\u4F30\u7B97\uFF09" }),
899
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-period-grid", children: [
900
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Period, { label: "\u4ECA\u65E5", period: usage.summary.today, previous: usage.summary.yesterday, compare: "\u8F83\u6628\u65E5" }),
901
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Period, { label: "\u672C\u6708", period: usage.summary.month, previous: usage.summary.lastMonthToDate, compare: "\u8F83\u4E0A\u6708\u540C\u671F" }),
902
- /* @__PURE__ */ (0, import_jsx_runtime2.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)(
903
2327
  Period,
904
2328
  {
905
- label: "\u7D2F\u8BA1",
2329
+ label: t("overview.total"),
906
2330
  period: { total: usage.totals.total, cost: usage.totals.cost, calls: usage.totals.calls }
907
2331
  }
908
2332
  )
909
- ] }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-period-grid", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-period", children: [
910
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 40, h: 11 }),
911
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 104, h: 24 }),
912
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 132, h: 11 })
913
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u7EDF\u8BA1\u7684\u6D88\u8017\u3002" }),
914
- usage !== null && /* @__PURE__ */ (0, import_jsx_runtime2.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 })
915
2347
  ] }),
916
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
917
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "DSH \u7528\u91CF\uFF08tokens\uFF09" }),
918
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
919
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-usage-totals", children: [
920
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Stat, { label: "\u8F93\u5165", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-value", children: fmtCompact(usage.totals.input) }) }),
921
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Stat, { label: "\u8F93\u51FA", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-value", children: fmtCompact(usage.totals.output) }) }),
922
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Stat, { label: "\u7F13\u5B58\u547D\u4E2D", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-stat-value", children: fmtCompact(usage.totals.cache) }) }),
923
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Stat, { label: "\u6A21\u578B\u8C03\u7528", children: /* @__PURE__ */ (0, import_jsx_runtime2.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) }) })
924
2362
  ] }),
925
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-chart-block-head", children: [
926
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-chart-title", children: [
927
- barMode === "daily" ? "\u8FD1 30 \u5929 \xB7 \u9010\u5929\u7528\u91CF" : "\u6309\u5C0F\u65F6\u5206\u5E03\uFF080\u201323 \u70B9\uFF09",
928
- chartPeak > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-chart-peak", children: [
929
- "\u5CF0\u503C ",
930
- fmtCompact(chartPeak),
931
- " tokens"
932
- ] })
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 }) })
933
2367
  ] }),
934
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-chart-controls", children: [
935
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ModelPicker, { models: modelList, selected: effectiveSelection, colorOf, onChange: setSelectedModels }),
936
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-chart-switch", role: "tablist", "aria-label": "\u5207\u6362\u56FE\u8868\u7EF4\u5EA6", children: [
937
- /* @__PURE__ */ (0, import_jsx_runtime2.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)(
938
2373
  "button",
939
2374
  {
940
2375
  type: "button",
@@ -942,10 +2377,10 @@ function BalanceDashboard() {
942
2377
  "aria-selected": barMode === "daily",
943
2378
  className: `dq-chart-switch-btn${barMode === "daily" ? " dq-chart-switch-btn--on" : ""}`,
944
2379
  onClick: () => setBarMode("daily"),
945
- children: "\u9010\u5929"
2380
+ children: t("chart.daily")
946
2381
  }
947
2382
  ),
948
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2383
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
949
2384
  "button",
950
2385
  {
951
2386
  type: "button",
@@ -953,81 +2388,87 @@ function BalanceDashboard() {
953
2388
  "aria-selected": barMode === "hourly",
954
2389
  className: `dq-chart-switch-btn${barMode === "hourly" ? " dq-chart-switch-btn--on" : ""}`,
955
2390
  onClick: () => setBarMode("hourly"),
956
- children: "\u9010\u5C0F\u65F6"
2391
+ children: t("chart.hourly")
957
2392
  }
958
2393
  )
959
2394
  ] })
960
2395
  ] })
961
2396
  ] }),
962
- grouped.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
963
- /* @__PURE__ */ (0, import_jsx_runtime2.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)(
964
2399
  GroupedBars,
965
2400
  {
966
2401
  series: grouped,
967
2402
  height: barMode === "daily" ? 120 : 100,
968
- labelEvery: barMode === "daily" ? 5 : 3
2403
+ labelEvery: barMode === "daily" ? dailyLabelEvery : 3,
2404
+ minWidth: barMode === "daily" ? dailyMinWidth : void 0
969
2405
  }
970
2406
  ),
971
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-legend", children: grouped.map((s) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dq-legend-item", children: [
972
- /* @__PURE__ */ (0, import_jsx_runtime2.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 } }),
973
2409
  s.name
974
2410
  ] }, s.key)) })
975
- ] }) : barMode === "daily" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Bars, { data: dailyBars, height: 120, labelEvery: 5 }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Bars, { data: hourlyBars, height: 100, labelEvery: 3 }),
976
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-chart-title", children: "\u8FD1 12 \u5468 \xB7 \u6BCF\u65E5\u7528\u91CF\u70ED\u529B\u56FE" }),
977
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Heatmap, { data: usage.heatmap })
978
- ] }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(UsageSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.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") })
979
2414
  ] }),
980
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
981
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u9AD8\u5CF0 / \u95F2\u65F6\u5206\u5E03" }),
982
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PeakCard, { split: usage.peakSplit, pricing: usage.pricing, currentCost: usage.totals.cost }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
983
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-peak-track" }),
984
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-peak-figures", children: [
985
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 120, h: 11 }),
986
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 96, h: 20 })
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") })
2421
+ ] }),
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 })
987
2429
  ] })
988
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u6309\u65F6\u6BB5\u5F52\u7C7B\u7684\u7528\u91CF\u3002" })
2430
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("peak.empty") })
989
2431
  ] }),
990
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
991
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u7F13\u5B58\u547D\u4E2D\u4E0E\u8282\u7701" }),
992
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CacheCard, { totals: usage.totals }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-cache-figures", children: [60, 96, 72].map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-stat", children: [
993
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w, h: 11 }),
994
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 72, h: 20 })
995
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709 prompt token \u53EF\u7EDF\u8BA1\u7F13\u5B58\u547D\u4E2D\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") })
996
2438
  ] }),
997
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
998
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u6A21\u578B\u6210\u672C\u6392\u884C" }),
999
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ModelRanking, { models: modelList, totalCost: modelCostTotal, colorOf }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-rank", children: [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-rank-row", children: [
1000
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 148, h: 13 }),
1001
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-rank-track" }),
1002
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 230, h: 11 })
1003
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u53EF\u5F52\u7C7B\u7684\u6A21\u578B\u7528\u91CF\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") })
1004
2446
  ] }),
1005
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
1006
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u4F1A\u8BDD\u6210\u672C\u6392\u884C" }),
1007
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SessionRanking, { sessions: usage.sessions, count: usage.sessionCount, totalCost: usage.totals.cost }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-sessions", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-session", children: [
1008
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 200, h: 13 }),
1009
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-session-track" }),
1010
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Skel, { w: 250, h: 11 })
1011
- ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-empty", children: "\u8FD8\u6CA1\u6709\u4EA7\u751F\u8D39\u7528\u7684\u4F1A\u8BDD\u3002" })
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") })
1012
2454
  ] }),
1013
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
1014
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u5B98\u65B9\u5E73\u53F0" }),
1015
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-links", children: [
1016
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Link, { href: "https://platform.deepseek.com/usage", children: "\u67E5\u770B\u989D\u5EA6 / \u7528\u91CF" }),
1017
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Link, { href: "https://platform.deepseek.com/api_keys", children: "\u751F\u6210 API Key" }),
1018
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Link, { href: "https://status.deepseek.com", children: "\u670D\u52A1\u72B6\u6001" })
1019
- ] })
1020
- ] }),
1021
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-card", children: [
1022
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dq-card-title", children: "\u8BBE\u7F6E" }),
1023
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dq-toggle", children: [
1024
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "checkbox", checked: widgetOn, onChange: toggle }),
1025
- /* @__PURE__ */ (0, import_jsx_runtime2.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") })
2462
+ ] }),
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") })
1026
2466
  ] }),
1027
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-setting", children: [
1028
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-low-balance", children: "\u4F59\u989D\u9884\u8B66\u7EBF" }),
1029
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dq-setting-control", children: [
1030
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
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)(
1031
2472
  "input",
1032
2473
  {
1033
2474
  id: "dq-low-balance",
@@ -1039,7 +2480,27 @@ function BalanceDashboard() {
1039
2480
  onChange: (e) => changeLowBalance(Math.max(0, Number(e.target.value) || 0))
1040
2481
  }
1041
2482
  ),
1042
- /* @__PURE__ */ (0, import_jsx_runtime2.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") })
1043
2504
  ] })
1044
2505
  ] })
1045
2506
  ] })
@@ -1048,7 +2509,8 @@ function BalanceDashboard() {
1048
2509
 
1049
2510
  // src/client/styles.ts
1050
2511
  var css = `
1051
- .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}
1052
2514
  .dsh-quota-root.dsh-quota-dragging{transition:none}
1053
2515
  .dsh-quota-card,.dsh-quota-card *{box-sizing:border-box}
1054
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}
@@ -1062,7 +2524,8 @@ var css = `
1062
2524
  .dsh-quota-dot--warn{background:var(--dsw-alias-state-warning-primary,#bf8700)}
1063
2525
  .dsh-quota-total--low{color:var(--dsw-alias-state-warning-primary,#9a6700)!important}
1064
2526
  .dsh-quota-dot--idle{background:var(--dsw-alias-label-secondary,#59636e);opacity:.6}
1065
- .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}
1066
2529
  .dsh-quota-actions{display:flex;gap:2px;flex:none}
1067
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}
1068
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)}
@@ -1077,27 +2540,114 @@ var css = `
1077
2540
  .dsh-quota-label{color:var(--dsw-alias-label-secondary,#59636e)}
1078
2541
  .dsh-quota-value{color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
1079
2542
  .dsh-quota-error{color:var(--dsw-alias-state-error-primary,#cf222e)}
1080
- .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)}}
1081
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}
1082
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). */
1083
2591
  .dq-balance-grid{display:flex;flex-wrap:wrap;gap:12px 24px}
1084
2592
  .dq-stat{min-width:110px}
1085
2593
  .dq-stat-label{font-size:11px;color:var(--dsw-alias-label-tertiary,#59636e);margin-bottom:2px}
1086
2594
  .dq-stat-value{font-size:18px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
1087
2595
  .dq-stat-value--ok{color:var(--dsw-alias-state-success-primary,#2da44e)}
1088
2596
  .dq-stat-value--bad{color:var(--dsw-alias-state-error-primary,#cf222e)}
1089
- .dq-runway{cursor:help;text-decoration:underline dotted var(--dsw-alias-border-l2,rgba(0,0,0,.25));text-underline-offset:4px}
1090
- .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}
1091
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)}
1092
- .dq-remaining:hover .dq-remaining-breakdown{display:flex}
2614
+ .dq-remaining:hover .dq-remaining-breakdown,.dq-remaining[open] .dq-remaining-breakdown{display:flex}
1093
2615
  .dq-remaining-granted{color:var(--dsw-alias-label-tertiary,#59636e);font-weight:400;font-size:11px}
1094
- .dq-period-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
1095
- @media (max-width:620px){.dq-period-grid{grid-template-columns:1fr}}
1096
- .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}}
1097
2631
  .dq-period-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}
1098
2632
  .dq-period-label{font-size:11px;color:var(--dsw-alias-label-tertiary,#59636e)}
1099
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}
1100
- .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}}
1101
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}
1102
2652
  .dq-delta--up{color:var(--dsw-alias-state-warning-primary,#9a6700);background:rgba(191,135,0,.14)}
1103
2653
  .dq-delta--down{color:var(--dsw-alias-state-success-primary,#1a7f37);background:rgba(45,164,78,.14)}
@@ -1113,31 +2663,51 @@ var css = `
1113
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)}
1114
2664
  .dq-pricing-now--peak{color:var(--dsw-alias-state-warning-primary,#9a6700);background:rgba(191,135,0,.14)}
1115
2665
  .dq-pricing-body{margin-top:10px;overflow-x:auto}
1116
- .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%}
1117
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))}
1118
2668
  .dq-pricing-table td{padding:4px 12px 4px 0;color:var(--dsw-alias-label-primary,#1f2328);white-space:nowrap;vertical-align:top}
1119
2669
  .dq-pricing-table tbody tr+tr td{border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.05))}
1120
- .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}}
1121
2691
  .dq-peak-track{height:8px;border-radius:999px;background:var(--dsw-alias-state-success-primary,#2da44e);opacity:.85;overflow:hidden}
1122
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)}
1123
- .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}
1124
2694
  .dq-peak-legend span{display:inline-flex;align-items:center;gap:6px}
1125
2695
  .dq-legend-swatch--peak{background:var(--dsw-alias-state-warning-primary,#bf8700)}
1126
2696
  .dq-legend-swatch--offpeak{background:var(--dsw-alias-state-success-primary,#2da44e);opacity:.85}
1127
2697
  .dq-peak-figures{display:flex;flex-wrap:wrap;gap:12px 32px;margin-top:14px}
1128
2698
  .dq-peak-delta{font-size:11px;font-weight:500;margin-left:8px;color:var(--dsw-alias-state-warning-primary,#9a6700)}
1129
2699
  .dq-peak-delta--save{color:var(--dsw-alias-state-success-primary,#1a7f37)}
1130
- .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)}
1131
2701
  .dq-cache-figures{display:flex;flex-wrap:wrap;gap:12px 32px;margin-bottom:12px}
1132
2702
  .dq-muted-value{color:var(--dsw-alias-label-secondary,#59636e)!important;font-weight:550}
1133
2703
  .dq-stat-value--warn{color:var(--dsw-alias-state-warning-primary,#9a6700)}
1134
2704
  .dq-cache-track{height:8px;border-radius:999px;background:var(--dsw-alias-state-warning-primary,#bf8700);overflow:hidden}
1135
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)}
1136
- .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}
1137
2707
  .dq-cache-legend span{display:inline-flex;align-items:center;gap:6px}
1138
2708
  .dq-legend-swatch--hit{background:var(--dsw-alias-state-success-primary,#2da44e)}
1139
2709
  .dq-legend-swatch--miss{background:var(--dsw-alias-state-warning-primary,#bf8700)}
1140
- .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)}
1141
2711
  .dq-sessions{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}
1142
2712
  .dq-session{display:flex;flex-direction:column;gap:5px;min-width:0}
1143
2713
  .dq-session-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
@@ -1145,8 +2715,32 @@ var css = `
1145
2715
  .dq-session-cost{font-size:14px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums;flex:none}
1146
2716
  .dq-session-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.12));overflow:hidden}
1147
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)}
1148
- .dq-session-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums;line-height:1.5}
1149
- .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))}
1150
2744
  .dq-rank{display:flex;flex-direction:column;gap:14px}
1151
2745
  .dq-rank-row{display:flex;flex-direction:column;gap:5px;min-width:0}
1152
2746
  .dq-rank-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
@@ -1155,31 +2749,45 @@ var css = `
1155
2749
  .dq-rank-share{font-size:11px;font-weight:500;color:var(--dsw-alias-label-tertiary,#59636e);min-width:30px;text-align:right}
1156
2750
  .dq-rank-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,rgba(120,130,150,.12));overflow:hidden}
1157
2751
  .dq-rank-fill{height:100%;border-radius:999px;transition:width .35s cubic-bezier(.22,1,.36,1)}
1158
- .dq-rank-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary,#59636e);font-variant-numeric:tabular-nums;line-height:1.5}
1159
- .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)}
1160
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}
1161
2755
  .dq-alert strong{font-weight:650}
1162
2756
  .dq-alert-link{margin-left:8px;color:inherit;text-decoration:underline;text-underline-offset:2px;white-space:nowrap}
1163
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))}
1164
2758
  .dq-setting-label{font-size:13px;color:var(--dsw-alias-label-primary,#1f2328);padding-top:6px;flex:none}
1165
2759
  .dq-setting-control{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
1166
- .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}
1167
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}
1168
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}
1169
2764
  .dq-links{display:flex;flex-wrap:wrap;gap:8px}
1170
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}
1171
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))}
1172
- .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))}
1173
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)}
1174
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}
1175
- .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}
1176
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}}
1177
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}
1178
- .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}
1179
2786
  .dq-chart-title:first-of-type{margin-top:0}
1180
- .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}
1181
2788
  .dq-chart-block-head .dq-chart-title{margin:0}
1182
- .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%}}
1183
2791
  .dq-model-picker{position:relative}
1184
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}
1185
2793
  .dq-model-btn:hover{border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}
@@ -1191,8 +2799,11 @@ var css = `
1191
2799
  .dq-model-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1192
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}
1193
2801
  .dq-model-tag{color:var(--dsw-alias-label-tertiary,#59636e);font-size:11px;flex:none}
1194
- .dq-bar-group{display:flex;align-items:flex-end;gap:2px;height:100%;width:100%}
1195
- .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)}
1196
2807
  .dq-legend{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary,#59636e)}
1197
2808
  .dq-legend-item{display:inline-flex;align-items:center;gap:6px}
1198
2809
  .dq-legend-swatch{width:10px;height:10px;border-radius:3px;flex:none}
@@ -1201,27 +2812,34 @@ var css = `
1201
2812
  .dq-chart-switch-btn:hover{color:var(--dsw-alias-label-primary,#1f2328)}
1202
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)}
1203
2814
  .dq-chart-wrap{position:relative}
1204
- .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))}
1205
2818
  .dq-tip-head{font-weight:650;margin-bottom:2px}
1206
2819
  .dq-tip-row{opacity:.85}
1207
2820
  .dq-bars{display:flex;align-items:flex-end;gap:2px;width:100%}
1208
2821
  .dq-bar-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
1209
2822
  .dq-bar{background:var(--dsw-alias-state-business-primary,#4176e6);border-radius:2px 2px 0 0;min-height:1px;transition:filter .12s ease}
1210
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}
1211
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}
1212
2831
  .dq-heat-cell:not(.dq-heat-cell--pad):hover{outline:1.5px solid var(--dsw-alias-label-primary,#1f2328);outline-offset:1px}
1213
- .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}
1214
2835
  .dq-heat{display:flex;flex-direction:column;gap:8px;max-width:100%}
1215
- .dq-heat-grid{display:flex;gap:6px;overflow-x:auto;padding-bottom:2px}
1216
- .dq-heat-dows{display:flex;flex-direction:column;gap:3px;padding-top:17px;flex:none}
1217
- .dq-heat-dow{height:12px;line-height:12px;font-size:9px;color:var(--dsw-alias-label-tertiary,#59636e);text-align:right;width:12px}
1218
- .dq-heat-cols{display:flex;flex-direction:column;gap:2px;min-width:0}
1219
- .dq-heat-months{display:flex;gap:3px;height:12px}
1220
- .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}
1221
- .dq-heatmap{display:flex;gap:3px;width:max-content}
1222
- .dq-heat-week{display:flex;flex-direction:column;gap:3px}
1223
- .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}
1224
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}
1225
2843
  .dq-heat-key{width:12px;height:12px;border-radius:2px;flex:none}
1226
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}
1227
2845
  .dq-heat-scale>span:first-child{margin-right:2px}
@@ -1229,15 +2847,31 @@ var css = `
1229
2847
  .dq-muted{color:var(--dsw-alias-label-secondary,#59636e)}
1230
2848
  .dq-error{color:var(--dsw-alias-state-error-primary,#cf222e)}
1231
2849
  .dq-warn{color:var(--dsw-alias-state-warning-primary,#9a6700)}
1232
- .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)}
1233
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}
1234
2859
  .dq-stat .dq-skel+.dq-skel{margin-top:6px}
1235
2860
  .dq-skel-bars{display:flex;align-items:flex-end;gap:2px;width:100%;height:120px;margin-top:10px}
1236
2861
  .dq-skel-bar{flex:1 1 0;min-width:0;border-radius:2px 2px 0 0}
1237
2862
  @keyframes dq-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}
1238
- @media (prefers-reduced-motion:reduce){.dq-skel{animation:none}.dq-refresh-icon--spin{animation:none}}
1239
2863
  .dq-status-row{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:20px}
1240
- .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)}}
1241
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}
1242
2876
  .dq-refresh-btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}
1243
2877
  .dq-refresh-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
@@ -1245,41 +2879,26 @@ var css = `
1245
2879
  .dq-refresh-icon{display:inline-block;line-height:1}
1246
2880
  .dq-refresh-icon--spin{animation:dq-spin .9s linear infinite}
1247
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
+ }
1248
2888
  `;
1249
2889
 
1250
2890
  // src/client/widget.tsx
1251
- var import_react3 = require("react");
1252
- var import_jsx_runtime3 = require("react/jsx-runtime");
2891
+ var import_react5 = require("react");
2892
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1253
2893
  var MARGIN = 16;
1254
2894
  var CORNER_KEY = "widget.corner";
1255
2895
  var COLLAPSED_KEY = "widget.collapsed";
1256
2896
  var isCorner = (value) => value === "tl" || value === "tr" || value === "bl" || value === "br";
1257
- function getMainColumn(frame) {
1258
- const anchor = frame.querySelector('[data-slot="conversation"]');
1259
- const column = anchor?.parentElement ?? null;
1260
- return column === frame ? null : column;
1261
- }
1262
- function slotBox(frame, slot, side) {
1263
- const anchor = frame.querySelector(`[data-slot="${slot}"]`);
1264
- if (anchor === null) return null;
1265
- if (side === "child") {
1266
- const rect = anchor.firstElementChild?.getBoundingClientRect() ?? null;
1267
- return rect !== null && rect.height > 0 ? rect : null;
1268
- }
1269
- let el = anchor.parentElement;
1270
- for (let depth = 0; depth < 4 && el !== null && el !== frame; depth++) {
1271
- const rect = el.getBoundingClientRect();
1272
- if (rect.height > 0) return rect;
1273
- el = el.parentElement;
1274
- }
1275
- return null;
1276
- }
1277
2897
  var MIN_REGION_HEIGHT = 140;
1278
2898
  function getBounds(node) {
1279
2899
  const fallback = { left: 0, top: 0, right: 1e5, bottom: 1e5 };
1280
2900
  if (node === null) return fallback;
1281
- const overlay = typeof node.closest === "function" ? node.closest("[data-shell-overlay]") : null;
1282
- const frame = overlay?.parentElement ?? null;
2901
+ const frame = getShellFrame(node);
1283
2902
  if (frame === null) return fallback;
1284
2903
  const frameRect = frame.getBoundingClientRect();
1285
2904
  let left = frameRect.left;
@@ -1319,26 +2938,29 @@ function cornerPos(corner, node, bounds) {
1319
2938
  return { x: xRight, y: yBottom };
1320
2939
  }
1321
2940
  function QuotaWidget() {
1322
- const [visible, setVisible] = (0, import_react3.useState)(widgetVisibleStore.get());
1323
- (0, import_react3.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
1324
- const [lowBalance, setLowBalance] = (0, import_react3.useState)(lowBalanceStore.get());
1325
- (0, import_react3.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())), []);
1326
2948
  const cachedBalance = getCachedBalance();
1327
- const [data, setData] = (0, import_react3.useState)(cachedBalance?.data ?? null);
1328
- const [error, setError] = (0, import_react3.useState)(null);
1329
- const [loading, setLoading] = (0, import_react3.useState)(cachedBalance === null);
1330
- const [collapsed, setCollapsed] = (0, import_react3.useState)(() => loadPref(COLLAPSED_KEY, isBoolean, false));
1331
- const [pos, setPos] = (0, import_react3.useState)(null);
1332
- const [corner, setCorner] = (0, import_react3.useState)(() => loadPref(CORNER_KEY, isCorner, "br"));
1333
- const [dragging, setDragging] = (0, import_react3.useState)(false);
1334
- const rootRef = (0, import_react3.useRef)(null);
1335
- const dragRef = (0, import_react3.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);
1336
2958
  const readUsage = () => {
1337
2959
  const cached = getCachedUsage();
1338
2960
  return { data: cached?.ok === true ? cached.data ?? null : null, at: getCachedUsageAt() };
1339
2961
  };
1340
- const [usage, setUsage] = (0, import_react3.useState)(readUsage);
1341
- (0, import_react3.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
2962
+ const [usage, setUsage] = (0, import_react5.useState)(readUsage);
2963
+ (0, import_react5.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
1342
2964
  const load = async (showLoading, force = false) => {
1343
2965
  if (showLoading) setLoading(true);
1344
2966
  try {
@@ -1348,7 +2970,7 @@ function QuotaWidget() {
1348
2970
  setError(null);
1349
2971
  } else {
1350
2972
  setData(null);
1351
- setError(res.error ?? "\u67E5\u8BE2\u5931\u8D25");
2973
+ setError(res.error ?? "");
1352
2974
  }
1353
2975
  } catch (err) {
1354
2976
  setData(null);
@@ -1357,15 +2979,15 @@ function QuotaWidget() {
1357
2979
  setLoading(false);
1358
2980
  }
1359
2981
  };
1360
- (0, import_react3.useEffect)(() => {
2982
+ (0, import_react5.useEffect)(() => {
1361
2983
  void load(true);
1362
2984
  const id = setInterval(() => {
1363
2985
  void load(false);
1364
2986
  }, 6e4);
1365
2987
  return () => clearInterval(id);
1366
2988
  }, []);
1367
- (0, import_react3.useLayoutEffect)(() => {
1368
- if (!visible) return;
2989
+ (0, import_react5.useLayoutEffect)(() => {
2990
+ if (!visible || quotaViewActive2) return;
1369
2991
  const node = rootRef.current;
1370
2992
  if (node === null) return;
1371
2993
  const place = () => {
@@ -1374,7 +2996,7 @@ function QuotaWidget() {
1374
2996
  setPos((prev) => prev !== null && prev.x === next.x && prev.y === next.y ? prev : next);
1375
2997
  };
1376
2998
  place();
1377
- const frame = node.closest("[data-shell-overlay]")?.parentElement;
2999
+ const frame = getShellFrame(node);
1378
3000
  let observer = null;
1379
3001
  if (frame !== null && typeof ResizeObserver === "function") {
1380
3002
  observer = new ResizeObserver(place);
@@ -1389,7 +3011,7 @@ function QuotaWidget() {
1389
3011
  window.removeEventListener("resize", place);
1390
3012
  clearInterval(poll);
1391
3013
  };
1392
- }, [visible, corner, collapsed]);
3014
+ }, [visible, quotaViewActive2, corner, collapsed]);
1393
3015
  const snapToNearest = (session) => {
1394
3016
  const node = rootRef.current;
1395
3017
  if (node === null) return;
@@ -1489,118 +3111,139 @@ function QuotaWidget() {
1489
3111
  const low = lowBalance > 0 && Number.isFinite(balanceValue) && balanceValue < lowBalance;
1490
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";
1491
3113
  const ageMinutes = usage.at === null ? null : Math.floor((Date.now() - usage.at) / 6e4);
1492
- 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 });
1493
3115
  let body = null;
1494
3116
  if (!collapsed) {
1495
3117
  if (loading && data === null && error === null) {
1496
- body = /* @__PURE__ */ (0, import_jsx_runtime3.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") });
1497
3119
  } else if (error !== null && data === null) {
1498
- body = /* @__PURE__ */ (0, import_jsx_runtime3.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") });
1499
3121
  } else if (primary !== null) {
1500
- body = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-quota-body", children: [
1501
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dsh-quota-remaining-label", children: "\u5269\u4F59\u4F59\u989D" }),
1502
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { title: low ? `\u4F59\u989D\u4F4E\u4E8E\u9884\u8B66\u7EBF ${fmt(lowBalance)}` : void 0, children: [
1503
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `dsh-quota-total${low ? " dsh-quota-total--low" : ""}`, children: fmt(primary.total) }),
1504
- /* @__PURE__ */ (0, import_jsx_runtime3.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 })
1505
3127
  ] }),
1506
- usage.data !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-quota-row", title: `\u4ECA\u65E5\u6D88\u8017\uFF08\u4F30\u7B97\uFF09\xB7 ${usageAgeText}`, children: [
1507
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-quota-label", children: "\u4ECA\u65E5\u6D88\u8017" }),
1508
- /* @__PURE__ */ (0, import_jsx_runtime3.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: [
1509
3131
  "\xA5 ",
1510
3132
  fmt(usage.data.summary.today.cost)
1511
3133
  ] })
1512
3134
  ] }),
1513
- data?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-quota-row", children: [
1514
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-quota-label", children: "\u72B6\u6001" }),
1515
- /* @__PURE__ */ (0, import_jsx_runtime3.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") })
1516
3138
  ] })
1517
3139
  ] });
1518
3140
  } else {
1519
- body = /* @__PURE__ */ (0, import_jsx_runtime3.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") });
1520
3142
  }
1521
3143
  }
1522
3144
  const style = pos !== null ? { left: `${pos.x}px`, top: `${pos.y}px`, right: "auto", bottom: "auto" } : void 0;
1523
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `dsh-quota-root${dragging ? " dsh-quota-dragging" : ""}`, style, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3145
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1524
3146
  "div",
1525
3147
  {
1526
- ref: rootRef,
1527
- className: "dsh-quota-card",
1528
- title: "\u62D6\u52A8\u5230\u56DB\u4E2A\u89D2\u843D",
1529
- onPointerDown,
1530
- onPointerMove,
1531
- onPointerUp,
1532
- onPointerCancel: () => endDrag(false),
1533
- onLostPointerCapture: () => endDrag(false),
1534
- children: [
1535
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-quota-header", children: [
1536
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-quota-title", children: [
1537
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-quota-grip", title: "\u62D6\u52A8\u4EE5\u79FB\u52A8", children: "\u2807" }),
1538
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: dotClass }),
1539
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-quota-name", children: "DeepSeek \u989D\u5EA6" }),
1540
- collapsed && primary !== null && /* @__PURE__ */ (0, import_jsx_runtime3.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: [
1541
3204
  fmt(primary.total),
1542
3205
  " ",
1543
3206
  primary.currency
1544
- ] })
1545
- ] }),
1546
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-quota-actions", children: [
1547
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1548
- "button",
1549
- {
1550
- className: "dsh-quota-btn",
1551
- type: "button",
1552
- title: "\u5237\u65B0\u4F59\u989D\u4E0E\u4ECA\u65E5\u7528\u91CF",
1553
- "aria-label": "\u5237\u65B0\u4F59\u989D\u4E0E\u4ECA\u65E5\u7528\u91CF",
1554
- disabled: loading,
1555
- onClick: () => {
1556
- void load(true, true);
1557
- void fetchUsage(true);
1558
- },
1559
- children: "\u21BB"
1560
- }
1561
- ),
1562
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1563
- "button",
1564
- {
1565
- className: "dsh-quota-btn",
1566
- type: "button",
1567
- title: collapsed ? "\u5C55\u5F00" : "\u6536\u8D77",
1568
- "aria-label": collapsed ? "\u5C55\u5F00\u989D\u5EA6\u7A97\u53E3" : "\u6536\u8D77\u989D\u5EA6\u7A97\u53E3",
1569
- "aria-expanded": !collapsed,
1570
- onClick: () => {
1571
- const next = !collapsed;
1572
- setCollapsed(next);
1573
- savePref(COLLAPSED_KEY, next);
1574
- },
1575
- children: collapsed ? "+" : "\u2212"
1576
- }
1577
- )
1578
- ] })
1579
- ] }),
1580
- body
1581
- ]
3207
+ ] }) }),
3208
+ body
3209
+ ]
3210
+ }
3211
+ )
1582
3212
  }
1583
- ) });
3213
+ );
1584
3214
  }
1585
3215
 
1586
3216
  // src/client/index.tsx
1587
- var import_jsx_runtime4 = require("react/jsx-runtime");
3217
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1588
3218
  var name = "dsh-usage-dashboard";
1589
- var inject = ["slots"];
3219
+ var inject = ["slots", "locale"];
1590
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);
1591
3223
  if (typeof document !== "undefined") {
1592
3224
  const tag = document.createElement("style");
1593
3225
  tag.dataset.plugin = "dsh-usage-dashboard";
1594
3226
  tag.textContent = css;
1595
3227
  document.head.appendChild(tag);
1596
3228
  }
3229
+ void fetchBalance().catch(() => {
3230
+ });
3231
+ void fetchUsage().catch(() => {
3232
+ });
1597
3233
  ctx.slots.inject("shell.overlay", () => ctx.slots.register(
1598
- { name: "shell.overlay", id: "deepseek-quota", order: 1e3, label: "DeepSeek \u989D\u5EA6" },
1599
- () => /* @__PURE__ */ (0, import_jsx_runtime4.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, {}) }) })
1600
3236
  ));
1601
3237
  ctx.slots.inject("conversation.view", () => ctx.slots.register(
1602
- { name: "conversation.view", id: "balance", order: 20, label: "\u989D\u5EA6" },
1603
- () => /* @__PURE__ */ (0, import_jsx_runtime4.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 }) }) })
1604
3247
  ));
1605
3248
  }
1606
3249
  return module.exports; } });