dsh-token-pulse 0.2.2-beta

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 ADDED
@@ -0,0 +1,2290 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-token-pulse",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __exportAll = (all, no_symbols) => {
15
+ let target = {};
16
+ for (var name in all) __defProp(target, name, {
17
+ get: all[name],
18
+ enumerable: true
19
+ });
20
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
21
+ return target;
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
25
+ key = keys[i];
26
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
27
+ get: ((k) => from[k]).bind(null, key),
28
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
29
+ });
30
+ }
31
+ return to;
32
+ };
33
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
34
+ value: mod,
35
+ enumerable: true
36
+ }) : target, mod));
37
+ //#endregion
38
+ let react = require("react");
39
+ react = __toESM(react, 1);
40
+ let react_jsx_runtime = require("react/jsx-runtime");
41
+ //#region src/client/StatsCards.tsx
42
+ function formatTokens$1(n) {
43
+ if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
44
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
45
+ if (n >= 1e3) return (n / 1e3).toFixed(2) + "k";
46
+ return n.toLocaleString();
47
+ }
48
+ function StatCard({ label, value, hint }) {
49
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50
+ style: {
51
+ border: "1px solid var(--dsw-alias-border-l2)",
52
+ background: "var(--dsw-alias-bg-layer-3)",
53
+ borderRadius: 12,
54
+ padding: "12px 14px",
55
+ display: "flex",
56
+ flexDirection: "column",
57
+ gap: 4,
58
+ minWidth: 0
59
+ },
60
+ children: [
61
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
62
+ style: {
63
+ fontSize: 12,
64
+ color: "var(--dsw-alias-label-tertiary)",
65
+ lineHeight: "18px"
66
+ },
67
+ children: label
68
+ }),
69
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
70
+ style: {
71
+ fontSize: 18,
72
+ fontWeight: 600,
73
+ color: "var(--dsw-alias-label-primary)",
74
+ lineHeight: "28px",
75
+ overflow: "hidden",
76
+ textOverflow: "ellipsis",
77
+ whiteSpace: "nowrap"
78
+ },
79
+ children: formatTokens$1(value)
80
+ }),
81
+ hint ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
82
+ style: {
83
+ fontSize: 12,
84
+ color: "var(--dsw-alias-label-tertiary)",
85
+ lineHeight: "16px"
86
+ },
87
+ children: hint
88
+ }) : null
89
+ ]
90
+ });
91
+ }
92
+ function StatsCards({ t, totals, todayCount }) {
93
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
94
+ style: {
95
+ display: "grid",
96
+ gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
97
+ gap: 12
98
+ },
99
+ children: [
100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
101
+ label: t("stats.today"),
102
+ value: totals.today,
103
+ hint: todayCount !== void 0 ? t("stats.count", { count: todayCount }) : void 0
104
+ }),
105
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
106
+ label: t("stats.week"),
107
+ value: totals.thisWeek
108
+ }),
109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
110
+ label: t("stats.month"),
111
+ value: totals.thisMonth
112
+ }),
113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
114
+ label: t("stats.all"),
115
+ value: totals.all
116
+ })
117
+ ]
118
+ });
119
+ }
120
+ //#endregion
121
+ //#region src/date-bucket.ts
122
+ var date_bucket_exports = /* @__PURE__ */ __exportAll({
123
+ addDays: () => addDays,
124
+ dayKeyToDate: () => dayKeyToDate,
125
+ listDaysInRange: () => listDaysInRange,
126
+ monthRangeFor: () => monthRangeFor,
127
+ monthStartOf: () => monthStartOf,
128
+ quarterRangeFor: () => quarterRangeFor,
129
+ toDayKey: () => toDayKey,
130
+ weekRangeFor: () => weekRangeFor,
131
+ weekStartOf: () => weekStartOf,
132
+ yearRangeFor: () => yearRangeFor,
133
+ yearStartOf: () => yearStartOf
134
+ });
135
+ const TZ = "Asia/Shanghai";
136
+ function toDayKey(ms, tz = TZ) {
137
+ return new Intl.DateTimeFormat("en-CA", {
138
+ timeZone: tz,
139
+ year: "numeric",
140
+ month: "2-digit",
141
+ day: "2-digit"
142
+ }).format(new Date(ms));
143
+ }
144
+ function dayKeyToDate(key, _tz = TZ) {
145
+ return /* @__PURE__ */ new Date(key + "T12:00:00+08:00");
146
+ }
147
+ function weekStartOf(dayKey) {
148
+ const d = dayKeyToDate(dayKey);
149
+ const dow = (d.getDay() + 6) % 7;
150
+ d.setDate(d.getDate() - dow);
151
+ return toDayKey(d.getTime());
152
+ }
153
+ function monthStartOf(dayKey) {
154
+ const d = dayKeyToDate(dayKey);
155
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-01`;
156
+ }
157
+ function yearStartOf(dayKey) {
158
+ return `${dayKeyToDate(dayKey).getFullYear()}-01-01`;
159
+ }
160
+ function addDays(dayKey, n) {
161
+ const d = dayKeyToDate(dayKey);
162
+ d.setDate(d.getDate() + n);
163
+ return toDayKey(d.getTime());
164
+ }
165
+ function listDaysInRange(startKey, endKey) {
166
+ const result = [];
167
+ let cur = startKey;
168
+ while (cur <= endKey) {
169
+ result.push(cur);
170
+ cur = addDays(cur, 1);
171
+ }
172
+ return result;
173
+ }
174
+ function weekRangeFor(dateKey) {
175
+ const s = weekStartOf(dateKey);
176
+ return [s, addDays(s, 6)];
177
+ }
178
+ function monthRangeFor(dateKey) {
179
+ const s = monthStartOf(dateKey);
180
+ const d = dayKeyToDate(s);
181
+ const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
182
+ return [s, `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`];
183
+ }
184
+ /** Last ~90 days: sliding window for the "Quarter" tab (not calendar quarter). */
185
+ function quarterRangeFor(dateKey) {
186
+ return [addDays(dateKey, -89), dateKey];
187
+ }
188
+ function yearRangeFor(dateKey) {
189
+ const s = yearStartOf(dateKey);
190
+ return [s, `${dayKeyToDate(s).getFullYear()}-12-31`];
191
+ }
192
+ //#endregion
193
+ //#region src/aggregation.ts
194
+ function emptyDay(dayKey) {
195
+ return {
196
+ dayKey,
197
+ totalTokens: 0,
198
+ uncachedInputTokens: 0,
199
+ cacheReadTokens: 0,
200
+ cacheWriteTokens: 0,
201
+ outputTokens: 0,
202
+ count: 0,
203
+ byModel: /* @__PURE__ */ new Map(),
204
+ byProvider: /* @__PURE__ */ new Map(),
205
+ hourlyTokens: new Array(24).fill(0),
206
+ winnerModel: null,
207
+ winnerProvider: null
208
+ };
209
+ }
210
+ function sumByDay(days) {
211
+ return days.reduce((s, d) => s + d.totalTokens, 0);
212
+ }
213
+ function resolveDay(byDay, key) {
214
+ return byDay.get(key) ?? emptyDay(key);
215
+ }
216
+ function resolveRange(byDay, start, end) {
217
+ return listDaysInRange(start, end).map((k) => resolveDay(byDay, k));
218
+ }
219
+ /**
220
+ * Rank-based (percentile) level. Outlier-proof: no matter how large one day is,
221
+ * lower values still spread across levels 1-4 by their position among non-zero
222
+ * values — a single huge day no longer flattens everything to level 1.
223
+ * `sortedNonZero` must be the ascending-sorted array of all non-zero values in
224
+ * the view. Uses the midpoint of the "below" and "at-or-below" ranks, so ties
225
+ * sit in the middle of their band; the maximum always lands on level 4 and the
226
+ * minimum on level 1, guaranteeing all four colored levels appear (n ≥ 2).
227
+ * 0 maps to 0.
228
+ */
229
+ function levelByRank(value, sortedNonZero) {
230
+ if (value <= 0) return 0;
231
+ const n = sortedNonZero.length;
232
+ if (n === 0) return 1;
233
+ let below = 0;
234
+ let atOrBelow = 0;
235
+ for (const v of sortedNonZero) {
236
+ if (v < value) below++;
237
+ if (v <= value) atOrBelow++;
238
+ }
239
+ const rank = (below / n + atOrBelow / n) / 2;
240
+ if (rank <= .25) return 1;
241
+ if (rank <= .5) return 2;
242
+ if (rank <= .75) return 3;
243
+ return 4;
244
+ }
245
+ /**
246
+ * Log-scaled ratio fallback for sparse data (< MIN_RANK_POINTS non-zero values).
247
+ * Compresses outliers logarithmically so a single large value doesn't flatten
248
+ * the rest, while keeping "larger = darker" intuition.
249
+ */
250
+ function logLevel(value, viewMax) {
251
+ if (value <= 0) return 0;
252
+ if (viewMax <= 0) return 1;
253
+ const r = Math.log1p(value) / Math.log1p(viewMax);
254
+ if (r <= .25) return 1;
255
+ if (r <= .5) return 2;
256
+ if (r <= .75) return 3;
257
+ return 4;
258
+ }
259
+ function hourInShanghai(ms) {
260
+ const s = new Date(ms).toLocaleString("en-GB", {
261
+ timeZone: "Asia/Shanghai",
262
+ hour: "2-digit",
263
+ hour12: false
264
+ });
265
+ const h = Number(s);
266
+ return h === 24 ? 0 : h;
267
+ }
268
+ function aggregate(events, nowMs) {
269
+ const byDay = /* @__PURE__ */ new Map();
270
+ const globalByModel = /* @__PURE__ */ new Map();
271
+ const globalByProvider = /* @__PURE__ */ new Map();
272
+ for (const ev of events) {
273
+ const dayKey = toDayKey(ev.time);
274
+ let agg = byDay.get(dayKey);
275
+ if (!agg) {
276
+ agg = emptyDay(dayKey);
277
+ byDay.set(dayKey, agg);
278
+ }
279
+ const u = ev.usage;
280
+ const total = (u.inputTokens ?? 0) + (u.cacheReadTokens ?? 0) + (u.cacheWriteTokens ?? 0) + (u.outputTokens ?? 0);
281
+ agg.totalTokens += total;
282
+ agg.uncachedInputTokens += u.inputTokens ?? 0;
283
+ agg.cacheReadTokens += u.cacheReadTokens ?? 0;
284
+ agg.cacheWriteTokens += u.cacheWriteTokens ?? 0;
285
+ agg.outputTokens += u.outputTokens ?? 0;
286
+ agg.count += 1;
287
+ const h = hourInShanghai(ev.time);
288
+ if (h >= 0 && h < 24) agg.hourlyTokens[h] += total;
289
+ const modelKey = ev.model || "unknown";
290
+ const providerKey = ev.provider || "unknown";
291
+ agg.byModel.set(modelKey, (agg.byModel.get(modelKey) ?? 0) + total);
292
+ agg.byProvider.set(providerKey, (agg.byProvider.get(providerKey) ?? 0) + total);
293
+ globalByModel.set(modelKey, (globalByModel.get(modelKey) ?? 0) + total);
294
+ globalByProvider.set(providerKey, (globalByProvider.get(providerKey) ?? 0) + total);
295
+ }
296
+ for (const agg of byDay.values()) {
297
+ let bestM = null;
298
+ let bestMVal = -1;
299
+ for (const [m, v] of agg.byModel) if (v > bestMVal) {
300
+ bestMVal = v;
301
+ bestM = m;
302
+ }
303
+ agg.winnerModel = bestM;
304
+ let bestP = null;
305
+ let bestPVal = -1;
306
+ for (const [p, v] of agg.byProvider) if (v > bestPVal) {
307
+ bestPVal = v;
308
+ bestP = p;
309
+ }
310
+ agg.winnerProvider = bestP;
311
+ }
312
+ const todayKey = toDayKey(nowMs);
313
+ const [wStart, wEnd] = weekRangeFor(todayKey);
314
+ const [mStart, mEnd] = monthRangeFor(todayKey);
315
+ const today = byDay.get(todayKey)?.totalTokens ?? 0;
316
+ const thisWeek = sumByDay(resolveRange(byDay, wStart, wEnd));
317
+ const thisMonth = sumByDay(resolveRange(byDay, mStart, mEnd));
318
+ let all = 0;
319
+ for (const v of byDay.values()) all += v.totalTokens;
320
+ const topModels = [...globalByModel.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([model, tokens]) => ({
321
+ model,
322
+ tokens
323
+ }));
324
+ const topProviders = [...globalByProvider.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([provider, tokens]) => ({
325
+ provider,
326
+ tokens
327
+ }));
328
+ function top5InWindow(days) {
329
+ const m = /* @__PURE__ */ new Map();
330
+ for (const d of days) for (const [model, tokens] of d.byModel) m.set(model, (m.get(model) ?? 0) + tokens);
331
+ return [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([model, tokens]) => ({
332
+ model,
333
+ tokens
334
+ }));
335
+ }
336
+ function topProvidersInWindow(days) {
337
+ const m = /* @__PURE__ */ new Map();
338
+ for (const d of days) for (const [provider, tokens] of d.byProvider) m.set(provider, (m.get(provider) ?? 0) + tokens);
339
+ return [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([provider, tokens]) => ({
340
+ provider,
341
+ tokens
342
+ }));
343
+ }
344
+ return {
345
+ byDay,
346
+ totals: {
347
+ today,
348
+ thisWeek,
349
+ thisMonth,
350
+ all
351
+ },
352
+ topModels,
353
+ topProviders,
354
+ top5: topModels,
355
+ weekDays: (k) => {
356
+ const [s, e] = weekRangeFor(k);
357
+ return resolveRange(byDay, s, e);
358
+ },
359
+ monthDays: (k) => {
360
+ const [s, e] = monthRangeFor(k);
361
+ return resolveRange(byDay, s, e);
362
+ },
363
+ quarterDays: (k) => {
364
+ const [s, e] = quarterRangeFor(k);
365
+ return resolveRange(byDay, s, e);
366
+ },
367
+ yearDays: (k) => {
368
+ const [s, e] = yearRangeFor(k);
369
+ return resolveRange(byDay, s, e);
370
+ },
371
+ top5InWindow,
372
+ topProvidersInWindow
373
+ };
374
+ }
375
+ //#endregion
376
+ //#region src/client/HeatmapGrid.tsx
377
+ const VIEW_ORDER = [
378
+ "week",
379
+ "month",
380
+ "quarter",
381
+ "year"
382
+ ];
383
+ const DEFAULT_THRESHOLDS = {
384
+ high: 1e8,
385
+ low: 1e7
386
+ };
387
+ /**
388
+ * Build a rank-based (percentile) level map for a day-window, keyed by dayKey.
389
+ * Outlier-proof: a single huge day no longer flattens the rest of the window
390
+ * to level 1. Falls back to log-scaled ratio when non-zero days are too few.
391
+ * Absolute thresholds override ranking: >= high -> always 4, < low -> always 1.
392
+ * (Day grids only — week-hour bars skip these, see buildHourLevels.)
393
+ */
394
+ function buildDayLevels(days, th) {
395
+ const nonZero = days.filter((d) => d.totalTokens > 0).map((d) => d.totalTokens).sort((a, b) => a - b);
396
+ const viewMax = days.reduce((m, d) => Math.max(m, d.totalTokens), 0);
397
+ const map = /* @__PURE__ */ new Map();
398
+ for (const d of days) {
399
+ let level;
400
+ if (d.totalTokens <= 0) level = 0;
401
+ else if (th.high > 0 && d.totalTokens >= th.high) level = 4;
402
+ else if (th.low > 0 && d.totalTokens < th.low) level = 1;
403
+ else if (nonZero.length < 5) level = logLevel(d.totalTokens, viewMax);
404
+ else level = levelByRank(d.totalTokens, nonZero);
405
+ map.set(d.dayKey, level);
406
+ }
407
+ return map;
408
+ }
409
+ /**
410
+ * Build rank-based level arrays for hour bars across a week (cross-day comparable).
411
+ * Maps dayKey -> number[24] of levels. NO absolute thresholds here: the 37-ish
412
+ * small hours would otherwise be pinned to level 1 and the mid band would pile
413
+ * into 3/4, leaving level 2 empty. Pure rank keeps all four levels populated.
414
+ */
415
+ function buildHourLevels(days) {
416
+ const nonZero = [];
417
+ const raw = /* @__PURE__ */ new Map();
418
+ for (const d of days) {
419
+ const h = d.hourlyTokens ?? new Array(24).fill(0);
420
+ raw.set(d.dayKey, h);
421
+ for (const v of h) if (v > 0) nonZero.push(v);
422
+ }
423
+ nonZero.sort((a, b) => a - b);
424
+ const weekMax = nonZero.length ? nonZero[nonZero.length - 1] : 1;
425
+ const out = /* @__PURE__ */ new Map();
426
+ for (const [k, h] of raw) out.set(k, h.map((v) => {
427
+ if (v <= 0) return 0;
428
+ if (nonZero.length < 5) return logLevel(v, weekMax);
429
+ return levelByRank(v, nonZero);
430
+ }));
431
+ return out;
432
+ }
433
+ function formatTokensShort(n) {
434
+ if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
435
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
436
+ if (n >= 1e3) return (n / 1e3).toFixed(2) + "k";
437
+ return n.toLocaleString();
438
+ }
439
+ function tooltipTotal(day) {
440
+ if (day.totalTokens === 0) return `${day.dayKey}: No usage`;
441
+ return `${day.dayKey}: ${formatTokensShort(day.totalTokens)} tokens`;
442
+ }
443
+ function HeatCell({ day, level, size = 14 }) {
444
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
445
+ title: tooltipTotal(day),
446
+ "aria-label": tooltipTotal(day),
447
+ style: {
448
+ width: size,
449
+ height: size,
450
+ borderRadius: 3,
451
+ background: {
452
+ 0: "var(--dsw-alias-bg-layer-2, #ebedf0)",
453
+ 1: "#c6e48b",
454
+ 2: "#7bc96f",
455
+ 3: "#239a3b",
456
+ 4: "#196127"
457
+ }[level],
458
+ border: level === 0 ? "1px solid var(--dsw-alias-border-l2, #e5e7eb)" : "1px solid transparent",
459
+ boxSizing: "border-box",
460
+ cursor: "pointer",
461
+ flexShrink: 0
462
+ }
463
+ });
464
+ }
465
+ function HourStrip({ day, hourLevels }) {
466
+ const hourly = day.hourlyTokens ?? new Array(24).fill(0);
467
+ const colors = {
468
+ 0: "var(--dsw-alias-bg-layer-2, #ebedf0)",
469
+ 1: "#c6e48b",
470
+ 2: "#7bc96f",
471
+ 3: "#239a3b",
472
+ 4: "#196127"
473
+ };
474
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
475
+ style: {
476
+ display: "flex",
477
+ gap: 1,
478
+ alignItems: "end",
479
+ flex: 1,
480
+ minWidth: 0
481
+ },
482
+ children: hourly.map((v, h) => {
483
+ const lv = hourLevels[h] ?? 0;
484
+ const hh = v === 0 ? `${String(h).padStart(2, "0")}:00 — No usage` : `${String(h).padStart(2, "0")}:00 — ${formatTokensShort(v)} tokens`;
485
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
486
+ title: hh,
487
+ "aria-label": hh,
488
+ style: {
489
+ flex: 1,
490
+ height: 6 + lv * 4,
491
+ borderRadius: 1,
492
+ background: colors[lv],
493
+ border: lv === 0 ? "1px solid var(--dsw-alias-border-l2)" : "none",
494
+ boxSizing: "border-box",
495
+ minWidth: 0
496
+ }
497
+ }, h);
498
+ })
499
+ });
500
+ }
501
+ function ViewSwitcher({ t, value, onChange }) {
502
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
503
+ role: "tablist",
504
+ "aria-label": "view switcher",
505
+ style: {
506
+ display: "inline-flex",
507
+ border: "1px solid var(--dsw-alias-border-l2)",
508
+ borderRadius: 8,
509
+ overflow: "hidden",
510
+ background: "var(--dsw-alias-bg-layer-3)"
511
+ },
512
+ children: VIEW_ORDER.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
513
+ role: "tab",
514
+ "aria-selected": value === v,
515
+ onClick: () => onChange(v),
516
+ style: {
517
+ padding: "6px 14px",
518
+ fontSize: 13,
519
+ lineHeight: "20px",
520
+ border: "none",
521
+ cursor: "pointer",
522
+ background: value === v ? "var(--dsw-alias-label-primary)" : "transparent",
523
+ color: value === v ? "var(--dsw-alias-bg-layer-3)" : "var(--dsw-alias-label-primary)",
524
+ fontWeight: value === v ? 600 : 400
525
+ },
526
+ children: t(`view.${v}`)
527
+ }, v))
528
+ });
529
+ }
530
+ function MonthHeader({ weeks, cellSize }) {
531
+ const seen = /* @__PURE__ */ new Set();
532
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
533
+ style: {
534
+ display: "flex",
535
+ gap: 3,
536
+ paddingLeft: cellSize + 20,
537
+ marginBottom: 2
538
+ },
539
+ children: weeks.map((week, i) => {
540
+ const m = week.find((d) => d.totalTokens !== -1)?.dayKey.slice(0, 7) ?? "";
541
+ let label = "";
542
+ if (m && !seen.has(m)) {
543
+ seen.add(m);
544
+ label = [
545
+ "Jan",
546
+ "Feb",
547
+ "Mar",
548
+ "Apr",
549
+ "May",
550
+ "Jun",
551
+ "Jul",
552
+ "Aug",
553
+ "Sep",
554
+ "Oct",
555
+ "Nov",
556
+ "Dec"
557
+ ][Number(m.slice(5, 7)) - 1];
558
+ }
559
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
560
+ style: {
561
+ width: cellSize,
562
+ fontSize: 10,
563
+ color: "var(--dsw-alias-label-tertiary)",
564
+ lineHeight: "12px",
565
+ whiteSpace: "nowrap",
566
+ overflow: "visible"
567
+ },
568
+ children: label
569
+ }, i);
570
+ })
571
+ });
572
+ }
573
+ function GitHubGrid({ days, t, cellSize, twoRows, isEn, dayLevels }) {
574
+ const weeks = [];
575
+ let cur = [];
576
+ for (const d of days) {
577
+ const rawDow = (/* @__PURE__ */ new Date(d.dayKey + "T12:00:00+08:00")).getDay();
578
+ const dow = isEn ? rawDow : (rawDow + 6) % 7;
579
+ if (cur.length === 0 && weeks.length === 0 && dow !== 0) for (let i = 0; i < dow; i++) cur.push({
580
+ dayKey: `pad-${i}`,
581
+ totalTokens: -1,
582
+ uncachedInputTokens: 0,
583
+ cacheReadTokens: 0,
584
+ cacheWriteTokens: 0,
585
+ outputTokens: 0,
586
+ count: 0,
587
+ byModel: /* @__PURE__ */ new Map(),
588
+ byProvider: /* @__PURE__ */ new Map(),
589
+ hourlyTokens: new Array(24).fill(0),
590
+ winnerModel: null,
591
+ winnerProvider: null
592
+ });
593
+ cur.push(d);
594
+ if (cur.length === 7) {
595
+ weeks.push(cur);
596
+ cur = [];
597
+ }
598
+ }
599
+ if (cur.length > 0) {
600
+ while (cur.length < 7) cur.push({
601
+ dayKey: `pad-end-${cur.length}`,
602
+ totalTokens: -1,
603
+ uncachedInputTokens: 0,
604
+ cacheReadTokens: 0,
605
+ cacheWriteTokens: 0,
606
+ outputTokens: 0,
607
+ count: 0,
608
+ byModel: /* @__PURE__ */ new Map(),
609
+ byProvider: /* @__PURE__ */ new Map(),
610
+ hourlyTokens: new Array(24).fill(0),
611
+ winnerModel: null,
612
+ winnerProvider: null
613
+ });
614
+ weeks.push(cur);
615
+ }
616
+ const renderWeeks = (ws) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
617
+ style: {
618
+ display: "flex",
619
+ gap: cellSize >= 22 ? 10 : 5
620
+ },
621
+ children: ws.map((week, wi) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
622
+ style: {
623
+ display: "flex",
624
+ flexDirection: "column",
625
+ gap: 3
626
+ },
627
+ children: week.map((d) => d.totalTokens === -1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
628
+ width: cellSize,
629
+ height: cellSize
630
+ } }, d.dayKey) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatCell, {
631
+ day: d,
632
+ level: dayLevels.get(d.dayKey) ?? 0,
633
+ size: cellSize
634
+ }, d.dayKey))
635
+ }, wi))
636
+ });
637
+ if (twoRows && weeks.length > 26) {
638
+ const mid = Math.ceil(weeks.length / 2);
639
+ const top = weeks.slice(0, mid);
640
+ const bot = weeks.slice(mid);
641
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
642
+ style: {
643
+ display: "flex",
644
+ flexDirection: "column",
645
+ gap: 16,
646
+ alignItems: "center"
647
+ },
648
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonthHeader, {
649
+ weeks: top,
650
+ cellSize
651
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
652
+ style: {
653
+ display: "flex",
654
+ gap: 3
655
+ },
656
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
657
+ style: {
658
+ display: "flex",
659
+ flexDirection: "column",
660
+ gap: 3
661
+ },
662
+ children: [
663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
664
+ style: {
665
+ height: cellSize,
666
+ fontSize: cellSize >= 22 ? 13 : 10,
667
+ color: "var(--dsw-alias-label-tertiary)",
668
+ lineHeight: `${cellSize}px`
669
+ },
670
+ children: isEn ? "Sun" : "Mon"
671
+ }),
672
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } }),
673
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
674
+ style: {
675
+ height: cellSize,
676
+ fontSize: cellSize >= 22 ? 13 : 10,
677
+ color: "var(--dsw-alias-label-tertiary)",
678
+ lineHeight: `${cellSize}px`
679
+ },
680
+ children: isEn ? "Tue" : "Wed"
681
+ }),
682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } }),
683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
684
+ style: {
685
+ height: cellSize,
686
+ fontSize: cellSize >= 22 ? 13 : 10,
687
+ color: "var(--dsw-alias-label-tertiary)",
688
+ lineHeight: `${cellSize}px`
689
+ },
690
+ children: isEn ? "Thu" : "Fri"
691
+ }),
692
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } }),
693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } })
694
+ ]
695
+ }), renderWeeks(top)]
696
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonthHeader, {
697
+ weeks: bot,
698
+ cellSize
699
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
700
+ style: {
701
+ display: "flex",
702
+ gap: 3
703
+ },
704
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
705
+ style: {
706
+ display: "flex",
707
+ flexDirection: "column",
708
+ gap: 3
709
+ },
710
+ children: [
711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
712
+ style: {
713
+ height: cellSize,
714
+ fontSize: cellSize >= 22 ? 13 : 10,
715
+ color: "var(--dsw-alias-label-tertiary)",
716
+ lineHeight: `${cellSize}px`
717
+ },
718
+ children: isEn ? "Sun" : "Mon"
719
+ }),
720
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } }),
721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
722
+ style: {
723
+ height: cellSize,
724
+ fontSize: cellSize >= 22 ? 13 : 10,
725
+ color: "var(--dsw-alias-label-tertiary)",
726
+ lineHeight: `${cellSize}px`
727
+ },
728
+ children: isEn ? "Tue" : "Wed"
729
+ }),
730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } }),
731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
732
+ style: {
733
+ height: cellSize,
734
+ fontSize: cellSize >= 22 ? 13 : 10,
735
+ color: "var(--dsw-alias-label-tertiary)",
736
+ lineHeight: `${cellSize}px`
737
+ },
738
+ children: isEn ? "Thu" : "Fri"
739
+ }),
740
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } }),
741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { height: cellSize } })
742
+ ]
743
+ }), renderWeeks(bot)]
744
+ })] })]
745
+ });
746
+ }
747
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
748
+ style: {
749
+ overflowX: "auto",
750
+ paddingBottom: 4,
751
+ display: "flex",
752
+ flexDirection: "column",
753
+ alignItems: "center",
754
+ gap: 6
755
+ },
756
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonthHeader, {
757
+ weeks,
758
+ cellSize
759
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
760
+ style: {
761
+ display: "flex",
762
+ gap: cellSize >= 22 ? 10 : 5,
763
+ justifyContent: "center"
764
+ },
765
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
766
+ style: {
767
+ display: "flex",
768
+ flexDirection: "column",
769
+ gap: 3,
770
+ fontSize: cellSize >= 22 ? 13 : 10,
771
+ color: "var(--dsw-alias-label-tertiary)"
772
+ },
773
+ children: (isEn ? [
774
+ "Sun",
775
+ "Mon",
776
+ "Tue",
777
+ "Wed",
778
+ "Thu",
779
+ "Fri",
780
+ "Sat"
781
+ ] : [
782
+ "Mon",
783
+ "Tue",
784
+ "Wed",
785
+ "Thu",
786
+ "Fri",
787
+ "Sat",
788
+ "Sun"
789
+ ]).map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
790
+ style: {
791
+ height: cellSize,
792
+ lineHeight: `${cellSize}px`,
793
+ textAlign: "right",
794
+ paddingRight: 4,
795
+ fontWeight: 500
796
+ },
797
+ children: w
798
+ }, w))
799
+ }), renderWeeks(weeks)]
800
+ })]
801
+ });
802
+ }
803
+ function CalendarGrid({ days, t, dayLevels, selectedKey, onSelect, isEn }) {
804
+ (t("heatmap.title") || "").includes("Token");
805
+ const header = [
806
+ "Mon",
807
+ "Tue",
808
+ "Wed",
809
+ "Thu",
810
+ "Fri",
811
+ "Sat",
812
+ "Sun"
813
+ ];
814
+ const pad = firstDowOf(days);
815
+ const cells = [];
816
+ for (let i = 0; i < pad; i++) cells.push(null);
817
+ for (const d of days) cells.push(d);
818
+ while (cells.length % 7 !== 0) cells.push(null);
819
+ function firstDowOf(ds) {
820
+ if (ds.length === 0) return 0;
821
+ const raw = (/* @__PURE__ */ new Date(ds[0].dayKey + "T12:00:00+08:00")).getDay();
822
+ return isEn ? raw : (raw + 6) % 7;
823
+ }
824
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
825
+ style: {
826
+ border: "1px solid var(--dsw-alias-border-l2)",
827
+ borderRadius: 12,
828
+ overflow: "hidden",
829
+ background: "var(--dsw-alias-bg-layer-3)"
830
+ },
831
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
832
+ style: {
833
+ display: "grid",
834
+ gridTemplateColumns: "repeat(7, 1fr)",
835
+ gap: 6,
836
+ padding: "8px 6px 6px"
837
+ },
838
+ children: (isEn ? [
839
+ "Sun",
840
+ "Mon",
841
+ "Tue",
842
+ "Wed",
843
+ "Thu",
844
+ "Fri",
845
+ "Sat"
846
+ ] : header).map((w) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
847
+ style: {
848
+ textAlign: "center",
849
+ fontSize: 11,
850
+ fontWeight: 500,
851
+ color: "var(--dsw-alias-label-tertiary)"
852
+ },
853
+ children: w
854
+ }, w))
855
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
856
+ style: {
857
+ display: "grid",
858
+ gridTemplateColumns: "repeat(7, 1fr)",
859
+ gap: 6,
860
+ padding: "0 6px"
861
+ },
862
+ children: cells.map((d, idx) => {
863
+ if (!d) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: { minHeight: 50 } }, `pad-${idx}`);
864
+ const selected = d.dayKey === selectedKey;
865
+ const level = dayLevels.get(d.dayKey) ?? 0;
866
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
867
+ onClick: () => onSelect(selected ? null : d.dayKey),
868
+ title: tooltipTotal(d),
869
+ "aria-label": tooltipTotal(d),
870
+ style: {
871
+ minHeight: 50,
872
+ padding: "6px 6px 4px",
873
+ textAlign: "left",
874
+ border: selected ? "1.5px solid var(--dsw-alias-brand-primary, #1677ff)" : "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
875
+ borderRadius: 8,
876
+ background: selected ? "var(--dsw-alias-brand-primary, #1677ff)" : {
877
+ 0: "var(--dsw-alias-bg-layer-3)",
878
+ 1: "#c6e48b",
879
+ 2: "#7bc96f",
880
+ 3: "#239a3b",
881
+ 4: "#196127"
882
+ }[level],
883
+ color: selected ? "#fff" : level === 0 ? "var(--dsw-alias-label-primary)" : level <= 2 ? "#1a1a1a" : "#fff",
884
+ cursor: "pointer",
885
+ display: "flex",
886
+ flexDirection: "column",
887
+ gap: 1,
888
+ alignItems: "flex-start"
889
+ },
890
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
891
+ style: {
892
+ fontSize: 12,
893
+ fontWeight: 600,
894
+ lineHeight: "16px"
895
+ },
896
+ children: d.dayKey.slice(8, 10).replace(/^0/, "")
897
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
898
+ style: {
899
+ fontSize: 10,
900
+ lineHeight: "12px",
901
+ opacity: .8,
902
+ whiteSpace: "nowrap",
903
+ overflow: "hidden",
904
+ textOverflow: "ellipsis",
905
+ maxWidth: "100%"
906
+ },
907
+ children: d.totalTokens > 0 ? formatTokensShort(d.totalTokens) : ""
908
+ })]
909
+ }, d.dayKey);
910
+ })
911
+ })]
912
+ });
913
+ }
914
+ function HeatmapGrid({ t, days, view, onViewChange, selectedKey, onSelect, isEn, thresholds = DEFAULT_THRESHOLDS }) {
915
+ const isWeek = view === "week";
916
+ const hasAny = days.some((d) => d.totalTokens > 0);
917
+ const dayLevels = buildDayLevels(days, thresholds);
918
+ const hourLevels = buildHourLevels(days);
919
+ const cellSize = isWeek ? 16 : view === "quarter" ? 23 : view === "year" ? 14 : 14;
920
+ const handleViewChange = (v) => {
921
+ onSelect?.(null);
922
+ onViewChange(v);
923
+ };
924
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
925
+ style: {
926
+ display: "flex",
927
+ flexDirection: "column",
928
+ gap: 6
929
+ },
930
+ children: [
931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewSwitcher, {
932
+ t,
933
+ value: view,
934
+ onChange: handleViewChange
935
+ }),
936
+ !hasAny ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
937
+ style: {
938
+ border: "1px solid var(--dsw-alias-border-l2)",
939
+ background: "var(--dsw-alias-bg-layer-3)",
940
+ borderRadius: 12,
941
+ padding: 24,
942
+ color: "var(--dsw-alias-label-tertiary)",
943
+ fontSize: 13
944
+ },
945
+ children: t("heatmap.empty")
946
+ }) : null,
947
+ isWeek ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
948
+ style: {
949
+ display: "flex",
950
+ flexDirection: "column",
951
+ gap: 5
952
+ },
953
+ children: days.map((d) => {
954
+ const wd = (/* @__PURE__ */ new Date(d.dayKey + "T12:00:00+08:00")).getDay();
955
+ const weekday = isEn ? [
956
+ "Sun",
957
+ "Mon",
958
+ "Tue",
959
+ "Wed",
960
+ "Thu",
961
+ "Fri",
962
+ "Sat"
963
+ ][wd] : [
964
+ "Mon",
965
+ "Tue",
966
+ "Wed",
967
+ "Thu",
968
+ "Fri",
969
+ "Sat",
970
+ "Sun"
971
+ ][(wd + 6) % 7];
972
+ const sel = d.dayKey === selectedKey;
973
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
974
+ onClick: () => onSelect?.(sel ? null : d.dayKey),
975
+ title: tooltipTotal(d),
976
+ style: {
977
+ display: "flex",
978
+ alignItems: "center",
979
+ gap: 10,
980
+ minWidth: 0,
981
+ width: "100%",
982
+ padding: "6px 8px",
983
+ borderRadius: 8,
984
+ border: sel ? "1px solid var(--dsw-alias-brand-primary)" : "1px solid transparent",
985
+ background: sel ? "var(--dsw-alias-bg-layer-2)" : "transparent",
986
+ cursor: "pointer",
987
+ textAlign: "left"
988
+ },
989
+ children: [
990
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
991
+ style: {
992
+ width: 52,
993
+ flexShrink: 0,
994
+ display: "flex",
995
+ flexDirection: "column",
996
+ gap: 1
997
+ },
998
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
999
+ style: {
1000
+ fontSize: 12,
1001
+ fontWeight: 600,
1002
+ color: "var(--dsw-alias-label-primary)",
1003
+ lineHeight: "16px"
1004
+ },
1005
+ children: weekday
1006
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1007
+ style: {
1008
+ fontSize: 10,
1009
+ color: "var(--dsw-alias-label-tertiary)",
1010
+ lineHeight: "12px"
1011
+ },
1012
+ children: d.dayKey.slice(5)
1013
+ })]
1014
+ }),
1015
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HourStrip, {
1016
+ day: d,
1017
+ hourLevels: hourLevels.get(d.dayKey) ?? new Array(24).fill(0)
1018
+ }),
1019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1020
+ style: {
1021
+ fontSize: 11,
1022
+ color: "var(--dsw-alias-label-tertiary)",
1023
+ whiteSpace: "nowrap",
1024
+ width: 80,
1025
+ textAlign: "right"
1026
+ },
1027
+ children: d.totalTokens === 0 ? "No usage" : `${formatTokensShort(d.totalTokens)} tokens`
1028
+ })
1029
+ ]
1030
+ }, d.dayKey);
1031
+ })
1032
+ }) : view === "month" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CalendarGrid, {
1033
+ days,
1034
+ t,
1035
+ dayLevels,
1036
+ selectedKey: selectedKey ?? null,
1037
+ onSelect: onSelect ?? (() => {}),
1038
+ isEn
1039
+ }) : view === "year" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitHubGrid, {
1040
+ days,
1041
+ t,
1042
+ cellSize,
1043
+ twoRows: true,
1044
+ isEn,
1045
+ dayLevels
1046
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitHubGrid, {
1047
+ days,
1048
+ t,
1049
+ cellSize,
1050
+ isEn,
1051
+ dayLevels
1052
+ }),
1053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1054
+ style: {
1055
+ display: "flex",
1056
+ alignItems: "center",
1057
+ gap: 6,
1058
+ justifyContent: "flex-end",
1059
+ fontSize: 12,
1060
+ color: "var(--dsw-alias-label-tertiary)"
1061
+ },
1062
+ children: [
1063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("heatmap.legend.less") }),
1064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1065
+ width: 12,
1066
+ height: 12,
1067
+ borderRadius: 2,
1068
+ background: "var(--dsw-alias-bg-layer-2, #ebedf0)",
1069
+ border: "1px solid var(--dsw-alias-border-l2)"
1070
+ } }),
1071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1072
+ width: 12,
1073
+ height: 12,
1074
+ borderRadius: 2,
1075
+ background: "#c6e48b"
1076
+ } }),
1077
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1078
+ width: 12,
1079
+ height: 12,
1080
+ borderRadius: 2,
1081
+ background: "#7bc96f"
1082
+ } }),
1083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1084
+ width: 12,
1085
+ height: 12,
1086
+ borderRadius: 2,
1087
+ background: "#239a3b"
1088
+ } }),
1089
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1090
+ width: 12,
1091
+ height: 12,
1092
+ borderRadius: 2,
1093
+ background: "#196127"
1094
+ } }),
1095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("heatmap.legend.more") })
1096
+ ]
1097
+ })
1098
+ ]
1099
+ });
1100
+ }
1101
+ //#endregion
1102
+ //#region src/client/ModelTop5.tsx
1103
+ function formatTokens(n) {
1104
+ if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
1105
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
1106
+ if (n >= 1e3) return (n / 1e3).toFixed(2) + "k";
1107
+ return n.toLocaleString();
1108
+ }
1109
+ function ModelTop5({ t, topModels, topProviders, days, mode = "model", onModeChange }) {
1110
+ const active = mode === "provider" ? topProviders : topModels;
1111
+ if (active.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1112
+ style: {
1113
+ border: "1px solid var(--dsw-alias-border-l2)",
1114
+ background: "var(--dsw-alias-bg-layer-3)",
1115
+ borderRadius: 12,
1116
+ padding: 16,
1117
+ color: "var(--dsw-alias-label-tertiary)",
1118
+ fontSize: 13
1119
+ },
1120
+ children: t("model.none")
1121
+ });
1122
+ const max = active[0]?.tokens ?? 1;
1123
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1124
+ style: {
1125
+ display: "flex",
1126
+ flexDirection: "column",
1127
+ gap: 12
1128
+ },
1129
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1130
+ style: {
1131
+ border: "1px solid var(--dsw-alias-border-l2)",
1132
+ background: "var(--dsw-alias-bg-layer-3)",
1133
+ borderRadius: 12,
1134
+ overflow: "hidden"
1135
+ },
1136
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1137
+ style: {
1138
+ padding: "12px 16px 8px",
1139
+ borderBottom: "1px solid var(--dsw-alias-border-l2)",
1140
+ display: "flex",
1141
+ alignItems: "center",
1142
+ justifyContent: "space-between",
1143
+ gap: 8
1144
+ },
1145
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1146
+ style: {
1147
+ display: "flex",
1148
+ alignItems: "center",
1149
+ gap: 8
1150
+ },
1151
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1152
+ style: {
1153
+ fontSize: 13,
1154
+ fontWeight: 600,
1155
+ color: "var(--dsw-alias-label-primary)"
1156
+ },
1157
+ children: mode === "provider" ? t("provider.top5") : t("model.top5")
1158
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1159
+ style: {
1160
+ fontSize: 11,
1161
+ color: "var(--dsw-alias-label-tertiary)"
1162
+ },
1163
+ children: t("model.top5.hint")
1164
+ })]
1165
+ }), onModeChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1166
+ style: {
1167
+ display: "inline-flex",
1168
+ border: "1px solid var(--dsw-alias-border-l2)",
1169
+ borderRadius: 6,
1170
+ overflow: "hidden"
1171
+ },
1172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1173
+ onClick: () => onModeChange("model"),
1174
+ style: {
1175
+ padding: "2px 8px",
1176
+ fontSize: 11,
1177
+ border: "none",
1178
+ cursor: "pointer",
1179
+ background: mode === "model" ? "var(--dsw-alias-label-primary)" : "transparent",
1180
+ color: mode === "model" ? "var(--dsw-alias-bg-layer-3)" : "var(--dsw-alias-label-secondary)"
1181
+ },
1182
+ children: t("view.model")
1183
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1184
+ onClick: () => onModeChange("provider"),
1185
+ style: {
1186
+ padding: "2px 8px",
1187
+ fontSize: 11,
1188
+ border: "none",
1189
+ cursor: "pointer",
1190
+ background: mode === "provider" ? "var(--dsw-alias-label-primary)" : "transparent",
1191
+ color: mode === "provider" ? "var(--dsw-alias-bg-layer-3)" : "var(--dsw-alias-label-secondary)"
1192
+ },
1193
+ children: t("view.provider")
1194
+ })]
1195
+ }) : null]
1196
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1197
+ style: {
1198
+ display: "flex",
1199
+ flexDirection: "column"
1200
+ },
1201
+ children: active.map((item, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1202
+ style: {
1203
+ display: "flex",
1204
+ alignItems: "center",
1205
+ gap: 12,
1206
+ padding: "10px 16px",
1207
+ borderTop: idx === 0 ? "none" : "1px solid var(--dsw-alias-border-l2)"
1208
+ },
1209
+ children: [
1210
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1211
+ style: {
1212
+ width: 20,
1213
+ textAlign: "center",
1214
+ fontSize: 12,
1215
+ fontWeight: 600,
1216
+ color: "var(--dsw-alias-label-tertiary)"
1217
+ },
1218
+ children: idx + 1
1219
+ }),
1220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1221
+ style: {
1222
+ flex: "0 1 40%",
1223
+ minWidth: 0,
1224
+ fontSize: 13,
1225
+ color: "var(--dsw-alias-label-primary)",
1226
+ overflow: "hidden",
1227
+ textOverflow: "ellipsis",
1228
+ whiteSpace: "nowrap"
1229
+ },
1230
+ title: item.name,
1231
+ children: item.name
1232
+ }),
1233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1234
+ style: {
1235
+ flex: 1,
1236
+ height: 6,
1237
+ borderRadius: 3,
1238
+ background: "var(--dsw-alias-bg-layer-2, #ebedf0)",
1239
+ overflow: "hidden"
1240
+ },
1241
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1242
+ width: `${Math.max(4, item.tokens / max * 100)}%`,
1243
+ height: "100%",
1244
+ background: "#40c463",
1245
+ borderRadius: 3
1246
+ } })
1247
+ }),
1248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1249
+ style: {
1250
+ fontSize: 12,
1251
+ color: "var(--dsw-alias-label-secondary)",
1252
+ whiteSpace: "nowrap"
1253
+ },
1254
+ children: formatTokens(item.tokens)
1255
+ })
1256
+ ]
1257
+ }, item.name))
1258
+ })]
1259
+ })
1260
+ });
1261
+ }
1262
+ //#endregion
1263
+ //#region src/client/hooks.ts
1264
+ const LS_INTERVAL_KEY = "dsh-token-pulse:autoRefreshMinutes";
1265
+ function getAutoRefreshMinutes() {
1266
+ try {
1267
+ const v = localStorage.getItem(LS_INTERVAL_KEY);
1268
+ if (v === null) return 10;
1269
+ const n = Number(v);
1270
+ if (!Number.isFinite(n) || n < 0) return 10;
1271
+ return Math.min(1440, Math.max(0, Math.round(n)));
1272
+ } catch {
1273
+ return 10;
1274
+ }
1275
+ }
1276
+ function setAutoRefreshMinutes(minutes) {
1277
+ try {
1278
+ localStorage.setItem(LS_INTERVAL_KEY, String(minutes));
1279
+ } catch {}
1280
+ }
1281
+ const API_TIMEOUT_MS = 5e3;
1282
+ async function fetchFromApi() {
1283
+ const ctrl = new AbortController();
1284
+ const timer = setTimeout(() => ctrl.abort(), API_TIMEOUT_MS);
1285
+ try {
1286
+ const res = await fetch("/api/dsh-token-pulse/daily.json", {
1287
+ cache: "no-store",
1288
+ signal: ctrl.signal
1289
+ });
1290
+ if (!res.ok) return null;
1291
+ const json = await res.json();
1292
+ if (!json || !Array.isArray(json.days)) return null;
1293
+ if (json.ready === false) return { __notReady: true };
1294
+ const byDay = /* @__PURE__ */ new Map();
1295
+ for (const d of json.days) byDay.set(d.dayKey, {
1296
+ dayKey: d.dayKey,
1297
+ totalTokens: d.totalTokens ?? 0,
1298
+ uncachedInputTokens: d.uncachedInputTokens ?? 0,
1299
+ cacheReadTokens: d.cacheReadTokens ?? 0,
1300
+ cacheWriteTokens: d.cacheWriteTokens ?? 0,
1301
+ outputTokens: d.outputTokens ?? 0,
1302
+ count: d.count ?? 0,
1303
+ byModel: new Map(Object.entries(d.byModel ?? {})),
1304
+ byProvider: new Map(Object.entries(d.byProvider ?? {})),
1305
+ hourlyTokens: Array.isArray(d.hourlyTokens) && d.hourlyTokens.length === 24 ? d.hourlyTokens : new Array(24).fill(0),
1306
+ winnerModel: d.winnerModel ?? null,
1307
+ winnerProvider: d.winnerProvider ?? null
1308
+ });
1309
+ const totals = json.totals ?? {
1310
+ today: 0,
1311
+ thisWeek: 0,
1312
+ thisMonth: 0,
1313
+ all: 0
1314
+ };
1315
+ const topModels = Array.isArray(json.topModels) ? json.topModels.map((x) => ({
1316
+ model: x.name ?? x.model,
1317
+ tokens: x.tokens
1318
+ })) : [];
1319
+ const topProviders = Array.isArray(json.topProviders) ? json.topProviders.map((x) => ({
1320
+ provider: x.name ?? x.provider,
1321
+ tokens: x.tokens
1322
+ })) : [];
1323
+ const fakeAgg = aggregate([], Date.now());
1324
+ fakeAgg.byDay = byDay;
1325
+ fakeAgg.totals = totals;
1326
+ fakeAgg.topModels = topModels;
1327
+ fakeAgg.topProviders = topProviders;
1328
+ fakeAgg.top5 = topModels;
1329
+ const { weekRangeFor, monthRangeFor, quarterRangeFor, yearRangeFor, listDaysInRange } = await Promise.resolve().then(() => date_bucket_exports);
1330
+ const resolveDay = (k) => byDay.get(k) ?? {
1331
+ dayKey: k,
1332
+ totalTokens: 0,
1333
+ uncachedInputTokens: 0,
1334
+ cacheReadTokens: 0,
1335
+ cacheWriteTokens: 0,
1336
+ outputTokens: 0,
1337
+ count: 0,
1338
+ byModel: /* @__PURE__ */ new Map(),
1339
+ byProvider: /* @__PURE__ */ new Map(),
1340
+ hourlyTokens: new Array(24).fill(0),
1341
+ winnerModel: null,
1342
+ winnerProvider: null
1343
+ };
1344
+ const resolveRange = (s, e) => listDaysInRange(s, e).map(resolveDay);
1345
+ fakeAgg.weekDays = (k) => resolveRange(...weekRangeFor(k));
1346
+ fakeAgg.monthDays = (k) => resolveRange(...monthRangeFor(k));
1347
+ fakeAgg.quarterDays = (k) => resolveRange(...quarterRangeFor(k));
1348
+ fakeAgg.yearDays = (k) => resolveRange(...yearRangeFor(k));
1349
+ fakeAgg.top5InWindow = (days) => {
1350
+ const m = /* @__PURE__ */ new Map();
1351
+ for (const d of days) for (const [model, tokens] of d.byModel) m.set(model, (m.get(model) ?? 0) + tokens);
1352
+ return [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([model, tokens]) => ({
1353
+ model,
1354
+ tokens
1355
+ }));
1356
+ };
1357
+ fakeAgg.topProvidersInWindow = (days) => {
1358
+ const m = /* @__PURE__ */ new Map();
1359
+ for (const d of days) for (const [provider, tokens] of d.byProvider) m.set(provider, (m.get(provider) ?? 0) + tokens);
1360
+ return [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([provider, tokens]) => ({
1361
+ provider,
1362
+ tokens
1363
+ }));
1364
+ };
1365
+ return fakeAgg;
1366
+ } catch {
1367
+ return null;
1368
+ } finally {
1369
+ clearTimeout(timer);
1370
+ }
1371
+ }
1372
+ let _cache = null;
1373
+ function useHeatmapData(ctx, refreshMs, manualTick) {
1374
+ const [data, setData] = react.useState(() => _cache?.agg ?? null);
1375
+ const [refreshing, setRefreshing] = react.useState(false);
1376
+ const [lastRefresh, setLastRefresh] = react.useState(() => _cache?.label ?? null);
1377
+ const cancelledRef = react.useRef(false);
1378
+ const retryTimerRef = react.useRef(null);
1379
+ const mountedRef = react.useRef(false);
1380
+ const intervalMs = react.useMemo(() => {
1381
+ if (refreshMs !== void 0) return refreshMs;
1382
+ const mins = getAutoRefreshMinutes();
1383
+ return mins === 0 ? 0 : mins * 60 * 1e3;
1384
+ }, [refreshMs, manualTick]);
1385
+ const fetchData = react.useCallback(async () => {
1386
+ setRefreshing(true);
1387
+ const commit = (agg) => {
1388
+ const label = (/* @__PURE__ */ new Date()).toLocaleString();
1389
+ _cache = {
1390
+ agg,
1391
+ label,
1392
+ at: Date.now()
1393
+ };
1394
+ setData(agg);
1395
+ setLastRefresh(label);
1396
+ };
1397
+ try {
1398
+ const apiData = await fetchFromApi();
1399
+ if (apiData && !apiData.__notReady) {
1400
+ commit(apiData);
1401
+ return;
1402
+ }
1403
+ if (apiData && apiData.__notReady) {
1404
+ if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
1405
+ if (!cancelledRef.current) retryTimerRef.current = setTimeout(() => {
1406
+ fetchData();
1407
+ }, 2e3);
1408
+ setRefreshing(false);
1409
+ return;
1410
+ }
1411
+ const store = ctx?.get?.("heatmapStore") ?? (typeof window !== "undefined" ? window.__dsh_heatmapStore : null);
1412
+ if (store?.refresh) await store.refresh();
1413
+ if (store?.getAggregated) {
1414
+ commit(store.getAggregated());
1415
+ return;
1416
+ }
1417
+ const snap = typeof window !== "undefined" ? window.__dsh_heatmapSnapshot : null;
1418
+ if (snap?.byDay && snap?.totals) {
1419
+ commit(snap);
1420
+ return;
1421
+ }
1422
+ } catch {} finally {
1423
+ setRefreshing(false);
1424
+ }
1425
+ if (!_cache) setData((prev) => prev ?? aggregate([], Date.now()));
1426
+ }, [ctx]);
1427
+ react.useEffect(() => {
1428
+ if (!mountedRef.current) {
1429
+ mountedRef.current = true;
1430
+ if (!_cache || intervalMs > 0 && Date.now() - _cache.at >= intervalMs) fetchData();
1431
+ return;
1432
+ }
1433
+ fetchData();
1434
+ }, [fetchData, manualTick]);
1435
+ react.useEffect(() => {
1436
+ cancelledRef.current = false;
1437
+ return () => {
1438
+ cancelledRef.current = true;
1439
+ if (retryTimerRef.current) {
1440
+ clearTimeout(retryTimerRef.current);
1441
+ retryTimerRef.current = null;
1442
+ }
1443
+ };
1444
+ }, []);
1445
+ react.useEffect(() => {
1446
+ if (intervalMs === 0) return;
1447
+ const id = setInterval(fetchData, intervalMs);
1448
+ return () => {
1449
+ clearInterval(id);
1450
+ };
1451
+ }, [fetchData, intervalMs]);
1452
+ return {
1453
+ data,
1454
+ refreshing,
1455
+ lastRefresh,
1456
+ refresh: react.useCallback(() => fetchData(), [fetchData]),
1457
+ intervalMs
1458
+ };
1459
+ }
1460
+ function useHeatmapView(aggregated, initial = "month") {
1461
+ const [view, setView] = react.useState(initial);
1462
+ const [anchor, setAnchor] = react.useState(() => toDayKey(Date.now()));
1463
+ const days = react.useMemo(() => {
1464
+ if (!aggregated) return null;
1465
+ switch (view) {
1466
+ case "week": return aggregated.weekDays(anchor);
1467
+ case "month": return aggregated.monthDays(anchor);
1468
+ case "quarter": return aggregated.quarterDays(anchor);
1469
+ case "year": return aggregated.yearDays(anchor);
1470
+ }
1471
+ }, [
1472
+ aggregated,
1473
+ view,
1474
+ anchor
1475
+ ]);
1476
+ return {
1477
+ view,
1478
+ setView,
1479
+ anchor,
1480
+ setAnchor,
1481
+ days,
1482
+ topModels: react.useMemo(() => {
1483
+ if (!aggregated || !days) return aggregated?.topModels ?? [];
1484
+ return aggregated.top5InWindow(days);
1485
+ }, [aggregated, days]),
1486
+ topProviders: react.useMemo(() => {
1487
+ if (!aggregated || !days) return aggregated?.topProviders ?? [];
1488
+ return aggregated.topProvidersInWindow(days);
1489
+ }, [aggregated, days])
1490
+ };
1491
+ }
1492
+ //#endregion
1493
+ //#region src/client/SettingsSection.tsx
1494
+ function SettingsSection({ t, ctx, days: injectedDays, totals: injectedTotals, top5: injectedTop5 }) {
1495
+ const [tick, setTick] = react.useState(0);
1496
+ const { data: aggregated, refreshing, lastRefresh, refresh } = useHeatmapData(ctx ?? null, void 0, tick);
1497
+ const { view, setView, anchor, setAnchor, days: computedDays, topModels: computedTopModels, topProviders: computedTopProviders } = useHeatmapView(aggregated);
1498
+ const [topMode, setTopMode] = react.useState("model");
1499
+ const [selectedKey, setSelectedKey] = react.useState(null);
1500
+ const [intervalMins, setIntervalMins] = react.useState(() => getAutoRefreshMinutes());
1501
+ const [lang, setLang] = react.useState(() => {
1502
+ try {
1503
+ return localStorage.getItem("dsh-token-pulse:lang") === "en" ? "en" : "zh";
1504
+ } catch {
1505
+ return "zh";
1506
+ }
1507
+ });
1508
+ const LS_TH_KEY = "dsh-token-pulse:thresholds";
1509
+ const [thresholds, setThresholds] = react.useState(() => {
1510
+ try {
1511
+ const raw = localStorage.getItem(LS_TH_KEY);
1512
+ if (!raw) return DEFAULT_THRESHOLDS;
1513
+ const parsed = JSON.parse(raw);
1514
+ const high = Number(parsed?.high) || 0;
1515
+ const low = Number(parsed?.low) || 0;
1516
+ return {
1517
+ high: Math.max(0, high),
1518
+ low: Math.max(0, low)
1519
+ };
1520
+ } catch {
1521
+ return DEFAULT_THRESHOLDS;
1522
+ }
1523
+ });
1524
+ const [settingsOpen, setSettingsOpen] = react.useState(false);
1525
+ const applyThresholdsM = (highM, lowM) => {
1526
+ const next = {
1527
+ high: Math.max(0, highM) * 1e6,
1528
+ low: Math.max(0, lowM) * 1e6
1529
+ };
1530
+ setThresholds(next);
1531
+ try {
1532
+ localStorage.setItem(LS_TH_KEY, JSON.stringify(next));
1533
+ } catch {}
1534
+ };
1535
+ const thInM = {
1536
+ high: thresholds.high / 1e6,
1537
+ low: thresholds.low / 1e6
1538
+ };
1539
+ const applyThresholds = (t) => {
1540
+ setThresholds(t);
1541
+ try {
1542
+ localStorage.setItem(LS_TH_KEY, JSON.stringify(t));
1543
+ } catch {}
1544
+ };
1545
+ const days = injectedDays ?? computedDays;
1546
+ const totals = injectedTotals ?? aggregated?.totals;
1547
+ const viewTopModelsRaw = computedTopModels.length ? computedTopModels : aggregated?.topModels ?? [];
1548
+ const viewTopProvidersRaw = computedTopProviders.length ? computedTopProviders : aggregated?.topProviders ?? [];
1549
+ let viewTopModels = viewTopModelsRaw;
1550
+ let viewTopProviders = viewTopProvidersRaw;
1551
+ if (selectedKey && aggregated) {
1552
+ const sel = aggregated.byDay.get(selectedKey);
1553
+ if (sel) {
1554
+ viewTopModels = [...sel.byModel.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([model, tokens]) => ({
1555
+ model,
1556
+ tokens
1557
+ }));
1558
+ viewTopProviders = [...sel.byProvider.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([provider, tokens]) => ({
1559
+ provider,
1560
+ tokens
1561
+ }));
1562
+ `${selectedKey}`;
1563
+ }
1564
+ }
1565
+ const viewTopModelsNamed = viewTopModels.map((x) => ({
1566
+ name: x.model,
1567
+ tokens: x.tokens
1568
+ }));
1569
+ const viewTopProvidersNamed = viewTopProviders.map((x) => ({
1570
+ name: x.provider ?? x.name,
1571
+ tokens: x.tokens
1572
+ }));
1573
+ const injectedTop = injectedTop5;
1574
+ const hasData = totals && (totals.today > 0 || totals.thisWeek > 0 || totals.thisMonth > 0 || totals.all > 0);
1575
+ const tt = (key, params) => {
1576
+ const entry = {
1577
+ "heatmap.lang.zh": {
1578
+ zh: "中文",
1579
+ en: "中文"
1580
+ },
1581
+ "heatmap.lang.en": {
1582
+ zh: "English",
1583
+ en: "English"
1584
+ },
1585
+ "heatmap.lang.label": {
1586
+ zh: "语言",
1587
+ en: "Language"
1588
+ },
1589
+ "heatmap.dayDetail": {
1590
+ zh: `${selectedKey ?? ""} · 当日 Top 5`,
1591
+ en: `${selectedKey ?? ""} · Top 5 of the day`
1592
+ },
1593
+ "heatmap.clearSelection": {
1594
+ zh: "清除选择",
1595
+ en: "Clear"
1596
+ }
1597
+ }[key];
1598
+ if (entry) {
1599
+ let s = entry[lang];
1600
+ if (params) for (const [k, v] of Object.entries(params)) s = s.replace(`{${k}}`, String(v));
1601
+ return s;
1602
+ }
1603
+ return t(key, params);
1604
+ };
1605
+ const handleRefresh = async () => {
1606
+ await refresh();
1607
+ };
1608
+ const handleIntervalChange = (e) => {
1609
+ const v = Number(e.target.value);
1610
+ setAutoRefreshMinutes(v);
1611
+ setIntervalMins(v);
1612
+ setTick((x) => x + 1);
1613
+ };
1614
+ const handleLangChange = (e) => {
1615
+ const v = e.target.value;
1616
+ setLang(v);
1617
+ try {
1618
+ localStorage.setItem("dsh-token-pulse:lang", v);
1619
+ } catch {}
1620
+ };
1621
+ const handleReset = () => {
1622
+ if (!confirm(t("heatmap.reset.confirm"))) return;
1623
+ const store = ctx?.get?.("heatmapStore") ?? (typeof window !== "undefined" ? window.__dsh_heatmapStore : null);
1624
+ if (store?.clearPersisted) {
1625
+ store.clearPersisted();
1626
+ setTick((x) => x + 1);
1627
+ }
1628
+ };
1629
+ const handleViewChange = (v) => {
1630
+ setSelectedKey(null);
1631
+ setView(v);
1632
+ };
1633
+ const handleSelect = (k) => setSelectedKey(k);
1634
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1635
+ "aria-labelledby": "heatmap-title",
1636
+ style: {
1637
+ display: "flex",
1638
+ flexDirection: "column",
1639
+ gap: 16
1640
+ },
1641
+ children: [
1642
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1643
+ style: {
1644
+ display: "flex",
1645
+ alignItems: "flex-start",
1646
+ justifyContent: "space-between",
1647
+ gap: 12
1648
+ },
1649
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
1650
+ id: "heatmap-title",
1651
+ style: {
1652
+ margin: 0,
1653
+ fontSize: 16,
1654
+ fontWeight: 600,
1655
+ color: "var(--dsw-alias-label-primary)"
1656
+ },
1657
+ children: [t("heatmap.title"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1658
+ style: {
1659
+ marginLeft: 8,
1660
+ fontSize: 11,
1661
+ fontWeight: 400,
1662
+ color: "var(--dsw-alias-label-tertiary)",
1663
+ verticalAlign: "middle"
1664
+ },
1665
+ children: ["v", "0.2.2-beta"]
1666
+ })]
1667
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1668
+ style: {
1669
+ margin: "4px 0 0",
1670
+ fontSize: 13,
1671
+ color: "var(--dsw-alias-label-tertiary)",
1672
+ lineHeight: 1.5
1673
+ },
1674
+ children: lang === "en" ? "Token usage from local session logs, bucketed by Asia/Shanghai. Synced once then persisted — deleting sessions keeps history." : t("heatmap.subtitle")
1675
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1676
+ style: {
1677
+ display: "flex",
1678
+ alignItems: "center",
1679
+ gap: 6,
1680
+ fontSize: 12,
1681
+ color: "var(--dsw-alias-label-secondary)",
1682
+ flexShrink: 0
1683
+ },
1684
+ children: [tt("heatmap.lang.label"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1685
+ value: lang,
1686
+ onChange: handleLangChange,
1687
+ style: {
1688
+ padding: "4px 8px",
1689
+ fontSize: 12,
1690
+ borderRadius: 6,
1691
+ border: "1px solid var(--dsw-alias-border-l2)",
1692
+ background: "var(--dsw-alias-bg-layer-3)",
1693
+ color: "var(--dsw-alias-label-primary)"
1694
+ },
1695
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1696
+ value: "zh",
1697
+ children: tt("heatmap.lang.zh")
1698
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1699
+ value: "en",
1700
+ children: tt("heatmap.lang.en")
1701
+ })]
1702
+ })]
1703
+ })]
1704
+ }),
1705
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1706
+ style: {
1707
+ display: "flex",
1708
+ alignItems: "center",
1709
+ gap: 8,
1710
+ flexWrap: "wrap"
1711
+ },
1712
+ children: [
1713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1714
+ onClick: handleRefresh,
1715
+ disabled: refreshing,
1716
+ style: {
1717
+ padding: "6px 14px",
1718
+ fontSize: 13,
1719
+ borderRadius: 8,
1720
+ border: "1px solid var(--dsw-alias-border-l2)",
1721
+ background: refreshing ? "var(--dsw-alias-bg-layer-2)" : "var(--dsw-alias-bg-layer-3)",
1722
+ color: "var(--dsw-alias-label-primary)",
1723
+ cursor: refreshing ? "wait" : "pointer"
1724
+ },
1725
+ children: lang === "en" ? refreshing ? "Refreshing…" : "Refresh" : refreshing ? t("heatmap.refreshing") : t("heatmap.refresh")
1726
+ }),
1727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1728
+ style: {
1729
+ fontSize: 12,
1730
+ color: "var(--dsw-alias-label-tertiary)"
1731
+ },
1732
+ children: lastRefresh ? lang === "en" ? `Last refresh: ${lastRefresh}` : t("heatmap.lastRefresh", { time: lastRefresh }) : ""
1733
+ }),
1734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
1735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1736
+ style: {
1737
+ display: "flex",
1738
+ alignItems: "center",
1739
+ gap: 6,
1740
+ fontSize: 12,
1741
+ color: "var(--dsw-alias-label-secondary)"
1742
+ },
1743
+ children: [lang === "en" ? "Auto refresh" : t("heatmap.autoRefresh"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1744
+ value: intervalMins,
1745
+ onChange: handleIntervalChange,
1746
+ title: t("heatmap.autoRefresh.hint"),
1747
+ style: {
1748
+ padding: "4px 8px",
1749
+ fontSize: 12,
1750
+ borderRadius: 6,
1751
+ border: "1px solid var(--dsw-alias-border-l2)",
1752
+ background: "var(--dsw-alias-bg-layer-3)",
1753
+ color: "var(--dsw-alias-label-primary)"
1754
+ },
1755
+ children: [
1756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1757
+ value: 0,
1758
+ children: t("heatmap.autoRefresh.off")
1759
+ }),
1760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1761
+ value: 5,
1762
+ children: "5 min"
1763
+ }),
1764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1765
+ value: 10,
1766
+ children: "10 min"
1767
+ }),
1768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1769
+ value: 30,
1770
+ children: "30 min"
1771
+ }),
1772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1773
+ value: 60,
1774
+ children: "60 min"
1775
+ })
1776
+ ]
1777
+ })]
1778
+ }),
1779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1780
+ onClick: () => setSettingsOpen(true),
1781
+ "aria-haspopup": "dialog",
1782
+ style: {
1783
+ listStyle: "none",
1784
+ cursor: "pointer",
1785
+ padding: "2px 8px",
1786
+ fontSize: 13,
1787
+ color: "var(--dsw-alias-label-tertiary)",
1788
+ opacity: .7,
1789
+ border: "none",
1790
+ background: "transparent"
1791
+ },
1792
+ title: lang === "en" ? "Settings" : "设置",
1793
+ children: "⋯"
1794
+ }),
1795
+ settingsOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1796
+ onClick: () => setSettingsOpen(false),
1797
+ style: {
1798
+ position: "fixed",
1799
+ inset: 0,
1800
+ zIndex: 50,
1801
+ background: "rgba(0,0,0,0.35)",
1802
+ display: "flex",
1803
+ alignItems: "center",
1804
+ justifyContent: "center"
1805
+ },
1806
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1807
+ role: "dialog",
1808
+ "aria-modal": "true",
1809
+ onClick: (e) => e.stopPropagation(),
1810
+ style: {
1811
+ background: "var(--dsw-alias-bg-layer-3)",
1812
+ border: "1px solid var(--dsw-alias-border-l2)",
1813
+ borderRadius: 12,
1814
+ minWidth: 340,
1815
+ maxWidth: "90vw",
1816
+ boxShadow: "0 8px 24px rgba(0,0,0,0.2)",
1817
+ padding: 16,
1818
+ display: "flex",
1819
+ flexDirection: "column",
1820
+ gap: 12
1821
+ },
1822
+ children: [
1823
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1824
+ style: {
1825
+ display: "flex",
1826
+ alignItems: "center",
1827
+ justifyContent: "space-between"
1828
+ },
1829
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1830
+ style: {
1831
+ fontSize: 14,
1832
+ fontWeight: 600,
1833
+ color: "var(--dsw-alias-label-primary)"
1834
+ },
1835
+ children: lang === "en" ? "Pulse Settings" : "Pulse 设置"
1836
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1837
+ style: {
1838
+ display: "flex",
1839
+ alignItems: "center",
1840
+ gap: 10
1841
+ },
1842
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1843
+ onClick: () => {
1844
+ if (confirm(lang === "en" ? "Reset all historical data? This cannot be undone." : "确定重置所有历史数据?此操作不可撤销。")) handleReset();
1845
+ },
1846
+ title: lang === "en" ? "Reset history (advanced)" : "重置历史(高级)",
1847
+ style: {
1848
+ border: "none",
1849
+ background: "transparent",
1850
+ fontSize: 11,
1851
+ cursor: "pointer",
1852
+ color: "var(--dsw-alias-label-tertiary)",
1853
+ opacity: .55,
1854
+ textDecoration: "underline"
1855
+ },
1856
+ children: lang === "en" ? "Reset history…" : "重置历史…"
1857
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1858
+ onClick: () => setSettingsOpen(false),
1859
+ "aria-label": "close",
1860
+ style: {
1861
+ border: "none",
1862
+ background: "transparent",
1863
+ fontSize: 16,
1864
+ cursor: "pointer",
1865
+ color: "var(--dsw-alias-label-tertiary)"
1866
+ },
1867
+ children: "✕"
1868
+ })]
1869
+ })]
1870
+ }),
1871
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1872
+ style: {
1873
+ display: "flex",
1874
+ flexDirection: "column",
1875
+ gap: 8
1876
+ },
1877
+ children: [
1878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1879
+ style: {
1880
+ fontSize: 12,
1881
+ fontWeight: 600,
1882
+ color: "var(--dsw-alias-label-secondary)"
1883
+ },
1884
+ children: lang === "en" ? "Level thresholds (M tokens)" : "分档阈值(M tokens)"
1885
+ }),
1886
+ [{
1887
+ key: "high",
1888
+ label: lang === "en" ? "Deep green ≥ (M)" : "深绿阈值 ≥(M)"
1889
+ }, {
1890
+ key: "low",
1891
+ label: lang === "en" ? "Light green < (M)" : "浅绿阈值 <(M)"
1892
+ }].map(({ key, label }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1893
+ style: {
1894
+ display: "flex",
1895
+ flexDirection: "column",
1896
+ gap: 2,
1897
+ fontSize: 11,
1898
+ color: "var(--dsw-alias-label-secondary)"
1899
+ },
1900
+ children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1901
+ type: "number",
1902
+ min: 0,
1903
+ step: 10,
1904
+ value: thInM[key],
1905
+ onChange: (e) => applyThresholdsM(key === "high" ? Math.max(0, Number(e.target.value) || 0) : thInM.high, key === "low" ? Math.max(0, Number(e.target.value) || 0) : thInM.low),
1906
+ placeholder: "0 = off",
1907
+ style: {
1908
+ padding: "6px 10px",
1909
+ fontSize: 13,
1910
+ borderRadius: 6,
1911
+ border: "1px solid var(--dsw-alias-border-l2)",
1912
+ background: "var(--dsw-alias-bg-layer-3)",
1913
+ color: "var(--dsw-alias-label-primary)"
1914
+ }
1915
+ })]
1916
+ }, key)),
1917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1918
+ style: {
1919
+ display: "flex",
1920
+ justifyContent: "space-between",
1921
+ alignItems: "center"
1922
+ },
1923
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1924
+ onClick: () => applyThresholds(DEFAULT_THRESHOLDS),
1925
+ style: {
1926
+ padding: "4px 10px",
1927
+ fontSize: 11,
1928
+ borderRadius: 5,
1929
+ border: "1px solid var(--dsw-alias-border-l2)",
1930
+ background: "var(--dsw-alias-bg-layer-3)",
1931
+ cursor: "pointer"
1932
+ },
1933
+ children: lang === "en" ? "Defaults (100M / 10M)" : "恢复默认(100M / 10M)"
1934
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1935
+ style: {
1936
+ fontSize: 10,
1937
+ color: "var(--dsw-alias-label-tertiary)"
1938
+ },
1939
+ children: "0 = off"
1940
+ })]
1941
+ })
1942
+ ]
1943
+ }),
1944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1945
+ onClick: () => setSettingsOpen(false),
1946
+ style: {
1947
+ width: "100%",
1948
+ padding: "8px 10px",
1949
+ fontSize: 13,
1950
+ borderRadius: 8,
1951
+ border: "1px solid var(--dsw-alias-border-l2)",
1952
+ background: "var(--dsw-alias-bg-layer-3)",
1953
+ color: "var(--dsw-alias-label-primary)",
1954
+ cursor: "pointer"
1955
+ },
1956
+ children: lang === "en" ? "Done" : "完成"
1957
+ })
1958
+ ]
1959
+ })
1960
+ }) : null
1961
+ ]
1962
+ }),
1963
+ view === "week" || view === "month" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1964
+ style: {
1965
+ display: "flex",
1966
+ alignItems: "center",
1967
+ gap: 8,
1968
+ justifyContent: "space-between"
1969
+ },
1970
+ children: [
1971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1972
+ onClick: () => setAnchor((a) => {
1973
+ const d = /* @__PURE__ */ new Date(a + "T12:00:00+08:00");
1974
+ if (view === "week") d.setDate(d.getDate() - 7);
1975
+ else d.setMonth(d.getMonth() - 1);
1976
+ const ms = d.getTime();
1977
+ return new Date(ms).toLocaleDateString("en-CA", { timeZone: "Asia/Shanghai" });
1978
+ }),
1979
+ style: {
1980
+ padding: "4px 10px",
1981
+ fontSize: 12,
1982
+ borderRadius: 6,
1983
+ border: "1px solid var(--dsw-alias-border-l2)",
1984
+ background: "var(--dsw-alias-bg-layer-3)",
1985
+ cursor: "pointer"
1986
+ },
1987
+ children: [lang === "en" ? "‹ Prev" : "‹ 上一", view === "week" ? lang === "en" ? " Week" : "周" : lang === "en" ? " Month" : "月"]
1988
+ }),
1989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1990
+ style: {
1991
+ fontSize: 12,
1992
+ color: "var(--dsw-alias-label-secondary)",
1993
+ fontWeight: 500
1994
+ },
1995
+ children: [view === "week" ? (() => {
1996
+ const d = /* @__PURE__ */ new Date(anchor + "T12:00:00+08:00");
1997
+ const dow = (d.getDay() + 6) % 7;
1998
+ const mon = new Date(d);
1999
+ mon.setDate(d.getDate() - dow);
2000
+ const sun = new Date(mon);
2001
+ sun.setDate(mon.getDate() + 6);
2002
+ const fmt = (x) => `${x.getMonth() + 1}/${x.getDate()}`;
2003
+ return lang === "en" ? `Week of ${fmt(mon)} - ${fmt(sun)}` : `${mon.getMonth() + 1}月${mon.getDate()}日 - ${sun.getMonth() + 1}月${sun.getDate()}日`;
2004
+ })() : `${Number(anchor.slice(0, 4))}年${Number(anchor.slice(5, 7))}月`, lang === "en" && view === "month" ? ` ${anchor.slice(0, 7)}` : ""]
2005
+ }),
2006
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2007
+ onClick: () => setAnchor((a) => {
2008
+ const d = /* @__PURE__ */ new Date(a + "T12:00:00+08:00");
2009
+ if (view === "week") d.setDate(d.getDate() + 7);
2010
+ else d.setMonth(d.getMonth() + 1);
2011
+ const ms = d.getTime();
2012
+ return new Date(ms).toLocaleDateString("en-CA", { timeZone: "Asia/Shanghai" });
2013
+ }),
2014
+ style: {
2015
+ padding: "4px 10px",
2016
+ fontSize: 12,
2017
+ borderRadius: 6,
2018
+ border: "1px solid var(--dsw-alias-border-l2)",
2019
+ background: "var(--dsw-alias-bg-layer-3)",
2020
+ cursor: "pointer"
2021
+ },
2022
+ children: [
2023
+ lang === "en" ? "Next ›" : "下一",
2024
+ view === "week" ? lang === "en" ? " Week" : "周" : lang === "en" ? " Month" : "月",
2025
+ " ›"
2026
+ ]
2027
+ })
2028
+ ]
2029
+ }) : null,
2030
+ totals ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsCards, {
2031
+ t: lang === "en" ? (k, p) => {
2032
+ const enMap = {
2033
+ "stats.today": "Today",
2034
+ "stats.week": "This week",
2035
+ "stats.month": "This month",
2036
+ "stats.all": "All time"
2037
+ };
2038
+ if (k === "stats.count") return `${p?.count ?? ""} turns`;
2039
+ return enMap[k] ?? t(k, p);
2040
+ } : t,
2041
+ totals,
2042
+ todayCount: void 0
2043
+ }) : null,
2044
+ days ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatmapGrid, {
2045
+ t: lang === "en" ? (k, p) => {
2046
+ return {
2047
+ "view.week": "Week",
2048
+ "view.month": "Month",
2049
+ "view.quarter": "Quarter",
2050
+ "view.year": "Year",
2051
+ "heatmap.legend.less": "Less",
2052
+ "heatmap.legend.more": "More",
2053
+ "heatmap.empty": "No data yet",
2054
+ "heatmap.tooltip.none": "No usage",
2055
+ "heatmap.subtitle": "Token usage from local session logs, bucketed by Asia/Shanghai. First sync is persisted — deleting sessions keeps history.",
2056
+ "heatmap.title": "Token Pulse"
2057
+ }[k] ?? t(k, p);
2058
+ } : t,
2059
+ days,
2060
+ view,
2061
+ onViewChange: handleViewChange,
2062
+ selectedKey,
2063
+ onSelect: handleSelect,
2064
+ isEn: lang === "en",
2065
+ thresholds
2066
+ }) : null,
2067
+ selectedKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2068
+ style: {
2069
+ display: "flex",
2070
+ alignItems: "center",
2071
+ gap: 8,
2072
+ fontSize: 12,
2073
+ color: "var(--dsw-alias-label-secondary)"
2074
+ },
2075
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt("heatmap.dayDetail") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2076
+ onClick: () => setSelectedKey(null),
2077
+ style: {
2078
+ padding: "2px 8px",
2079
+ fontSize: 11,
2080
+ borderRadius: 6,
2081
+ border: "1px solid var(--dsw-alias-border-l2)",
2082
+ background: "var(--dsw-alias-bg-layer-3)",
2083
+ cursor: "pointer"
2084
+ },
2085
+ children: tt("heatmap.clearSelection")
2086
+ })]
2087
+ }) : null,
2088
+ injectedTop ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelTop5, {
2089
+ t,
2090
+ topModels: injectedTop,
2091
+ topProviders: [],
2092
+ days: days ?? void 0,
2093
+ mode: topMode,
2094
+ onModeChange: setTopMode
2095
+ }) : viewTopModelsNamed.length > 0 || viewTopProvidersNamed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelTop5, {
2096
+ t: lang === "en" ? (k, p) => ({
2097
+ "model.top5": "Top 5 Models",
2098
+ "provider.top5": "Top 5 Providers",
2099
+ "model.top5.hint": "By token usage",
2100
+ "model.none": "No data",
2101
+ "view.model": "Model",
2102
+ "view.provider": "Provider"
2103
+ })[k] ?? t(k, p) : t,
2104
+ topModels: viewTopModelsNamed,
2105
+ topProviders: viewTopProvidersNamed,
2106
+ days: days ?? void 0,
2107
+ mode: topMode,
2108
+ onModeChange: setTopMode
2109
+ }) : null,
2110
+ !hasData && !days ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2111
+ style: {
2112
+ border: "1px solid var(--dsw-alias-border-l2)",
2113
+ background: "var(--dsw-alias-bg-layer-3)",
2114
+ borderRadius: 12,
2115
+ padding: 24,
2116
+ color: "var(--dsw-alias-label-tertiary)",
2117
+ fontSize: 13
2118
+ },
2119
+ children: t("heatmap.empty")
2120
+ }) : null
2121
+ ]
2122
+ });
2123
+ }
2124
+ //#endregion
2125
+ //#region src/client/locales.ts
2126
+ const zh = {
2127
+ "nav": "Token Pulse",
2128
+ "heatmap.title": "Token Pulse",
2129
+ "heatmap.subtitle": "基于本地会话日志统计 Token 用量,北京时间自然日分桶,零网络零计费。首次同步后持久化,删除会话不丢历史。",
2130
+ "heatmap.empty": "暂无用量数据,开始一次对话后这里会出现热图。",
2131
+ "heatmap.legend.less": "少",
2132
+ "heatmap.legend.more": "多",
2133
+ "heatmap.tooltip.tokens": "{count} tokens",
2134
+ "heatmap.tooltip.none": "无用量",
2135
+ "heatmap.refresh": "刷新",
2136
+ "heatmap.refreshing": "刷新中…",
2137
+ "heatmap.lastRefresh": "上次刷新:{time}",
2138
+ "heatmap.autoRefresh": "自动刷新",
2139
+ "heatmap.autoRefresh.off": "关闭",
2140
+ "heatmap.autoRefresh.hint": "设置自动刷新间隔(分钟)",
2141
+ "heatmap.reset": "重置历史",
2142
+ "heatmap.reset.confirm": "确定重置所有历史数据?此操作不可撤销。",
2143
+ "heatmap.reset.done": "已重置",
2144
+ "view.week": "周",
2145
+ "view.month": "月",
2146
+ "view.quarter": "季度",
2147
+ "view.year": "年",
2148
+ "view.model": "模型",
2149
+ "view.provider": "供应商",
2150
+ "stats.today": "今日",
2151
+ "stats.week": "本周",
2152
+ "stats.month": "本月",
2153
+ "stats.all": "累计",
2154
+ "stats.count": "{count} 次",
2155
+ "model.top5": "Top 5 模型",
2156
+ "model.top5.hint": "按 Token 用量排序",
2157
+ "model.winner": "当日最常用模型",
2158
+ "model.none": "暂无模型数据",
2159
+ "model.tokens": "{count} tokens",
2160
+ "provider.top5": "Top 5 供应商",
2161
+ "provider.winner": "当日最常用供应商",
2162
+ "common.tokens": "tokens"
2163
+ };
2164
+ const en = {
2165
+ "nav": "Token Pulse",
2166
+ "heatmap.title": "Token Pulse",
2167
+ "heatmap.subtitle": "Local token usage from session logs, bucketed by Asia/Shanghai, no network or billing. Synced once then persisted, surviving session deletion.",
2168
+ "heatmap.empty": "No usage yet. Start a conversation and the heatmap will appear here.",
2169
+ "heatmap.legend.less": "Less",
2170
+ "heatmap.legend.more": "More",
2171
+ "heatmap.tooltip.tokens": "{count} tokens",
2172
+ "heatmap.tooltip.none": "No usage",
2173
+ "heatmap.refresh": "Refresh",
2174
+ "heatmap.refreshing": "Refreshing…",
2175
+ "heatmap.lastRefresh": "Last refresh: {time}",
2176
+ "heatmap.autoRefresh": "Auto refresh",
2177
+ "heatmap.autoRefresh.off": "Off",
2178
+ "heatmap.autoRefresh.hint": "Auto refresh interval (minutes)",
2179
+ "heatmap.reset": "Reset history",
2180
+ "heatmap.reset.confirm": "Reset all historical data? This cannot be undone.",
2181
+ "heatmap.reset.done": "Reset done",
2182
+ "view.week": "Week",
2183
+ "view.month": "Month",
2184
+ "view.quarter": "Quarter",
2185
+ "view.year": "Year",
2186
+ "view.model": "Model",
2187
+ "view.provider": "Provider",
2188
+ "stats.today": "Today",
2189
+ "stats.week": "This week",
2190
+ "stats.month": "This month",
2191
+ "stats.all": "All time",
2192
+ "stats.count": "{count} turns",
2193
+ "model.top5": "Top 5 Models",
2194
+ "model.top5.hint": "By token usage",
2195
+ "model.winner": "Daily winner (model)",
2196
+ "model.none": "No model data",
2197
+ "model.tokens": "{count} tokens",
2198
+ "provider.top5": "Top 5 Providers",
2199
+ "provider.winner": "Daily winner (provider)",
2200
+ "common.tokens": "tokens"
2201
+ };
2202
+ //#endregion
2203
+ //#region src/client/index.ts
2204
+ const NS = "dsh-token-pulse";
2205
+ const inject = ["locale", "slots"];
2206
+ const NAV_CSS = `
2207
+ [data-dsh-token-pulse-nav] > svg:first-child { display: none; }
2208
+ [data-dsh-token-pulse-nav]::before {
2209
+ content: ''; flex: none; width: 16px; height: 16px; background: currentColor;
2210
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='7' height='7' rx='1'/%3E%3Crect x='14' y='3' width='7' height='7' rx='1'/%3E%3Crect x='3' y='14' width='7' height='7' rx='1'/%3E%3Crect x='14' y='14' width='7' height='7' rx='1'/%3E%3C/svg%3E") center / contain no-repeat;
2211
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='7' height='7' rx='1'/%3E%3Crect x='14' y='3' width='7' height='7' rx='1'/%3E%3Crect x='3' y='14' width='7' height='7' rx='1'/%3E%3Crect x='14' y='14' width='7' height='7' rx='1'/%3E%3C/svg%3E") center / contain no-repeat;
2212
+ }
2213
+ `;
2214
+ function injectNavCss() {
2215
+ if (typeof document === "undefined") return;
2216
+ const id = "dsh-token-pulse-nav-css";
2217
+ if (document.getElementById(id)) return;
2218
+ const el = document.createElement("style");
2219
+ el.id = id;
2220
+ el.textContent = NAV_CSS;
2221
+ document.head.appendChild(el);
2222
+ }
2223
+ function markNavRow() {
2224
+ if (typeof document === "undefined") return;
2225
+ const nav = t_global();
2226
+ const labels = [
2227
+ typeof nav === "function" ? nav("nav") : "",
2228
+ "Token Pulse",
2229
+ "Token Heatmap",
2230
+ "用量热图"
2231
+ ].filter(Boolean);
2232
+ for (const label of labels) {
2233
+ const btn = [...document.querySelectorAll("button")].find((b) => b.textContent?.trim().includes(label));
2234
+ if (btn) btn.setAttribute("data-dsh-token-pulse-nav", "");
2235
+ }
2236
+ }
2237
+ let t_global = () => (k) => k;
2238
+ function migrateLegacyLocalStorage() {
2239
+ if (typeof localStorage === "undefined") return;
2240
+ for (const [from, to] of [
2241
+ ["dsh-token-heatmap:lang", "dsh-token-pulse:lang"],
2242
+ ["dsh-token-heatmap:thresholds", "dsh-token-pulse:thresholds"],
2243
+ ["dsh-token-heatmap:autoRefreshMinutes", "dsh-token-pulse:autoRefreshMinutes"]
2244
+ ]) try {
2245
+ if (localStorage.getItem(to) === null) {
2246
+ const v = localStorage.getItem(from);
2247
+ if (v !== null) localStorage.setItem(to, v);
2248
+ }
2249
+ } catch {}
2250
+ }
2251
+ function apply(ctx) {
2252
+ migrateLegacyLocalStorage();
2253
+ injectNavCss();
2254
+ ctx.effect(() => ctx.locale.register(NS, {
2255
+ zh,
2256
+ en
2257
+ }), "dsh-token-pulse: dictionaries");
2258
+ const t = ctx.locale.bind(NS);
2259
+ t_global = () => t;
2260
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2261
+ name: "settings.section",
2262
+ id: "heatmap",
2263
+ order: 55,
2264
+ label: () => t("nav"),
2265
+ locale: NS
2266
+ }, (props) => {
2267
+ setTimeout(markNavRow, 50);
2268
+ return react.createElement(SettingsSection, {
2269
+ t,
2270
+ ctx,
2271
+ ...props
2272
+ });
2273
+ }));
2274
+ if (typeof document !== "undefined") {
2275
+ const mo = new MutationObserver(() => markNavRow());
2276
+ mo.observe(document.documentElement, {
2277
+ childList: true,
2278
+ subtree: true
2279
+ });
2280
+ setTimeout(() => mo.disconnect(), 3e4);
2281
+ }
2282
+ }
2283
+ //#endregion
2284
+ exports.apply = apply;
2285
+ exports.inject = inject;
2286
+ return module.exports;
2287
+ }
2288
+ });
2289
+
2290
+ //# sourceMappingURL=client.js.map