dsh-token-pulse 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/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +673 -84
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +41 -24
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -430,7 +430,7 @@ window.__ModuleLoader__.load({
|
|
|
430
430
|
}));
|
|
431
431
|
return out;
|
|
432
432
|
}
|
|
433
|
-
function formatTokensShort(n) {
|
|
433
|
+
function formatTokensShort$1(n) {
|
|
434
434
|
if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
|
|
435
435
|
if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
|
|
436
436
|
if (n >= 1e3) return (n / 1e3).toFixed(2) + "k";
|
|
@@ -438,7 +438,7 @@ window.__ModuleLoader__.load({
|
|
|
438
438
|
}
|
|
439
439
|
function tooltipTotal(day) {
|
|
440
440
|
if (day.totalTokens === 0) return `${day.dayKey}: No usage`;
|
|
441
|
-
return `${day.dayKey}: ${formatTokensShort(day.totalTokens)} tokens`;
|
|
441
|
+
return `${day.dayKey}: ${formatTokensShort$1(day.totalTokens)} tokens`;
|
|
442
442
|
}
|
|
443
443
|
function HeatCell({ day, level, size = 14 }) {
|
|
444
444
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -481,7 +481,7 @@ window.__ModuleLoader__.load({
|
|
|
481
481
|
},
|
|
482
482
|
children: hourly.map((v, h) => {
|
|
483
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`;
|
|
484
|
+
const hh = v === 0 ? `${String(h).padStart(2, "0")}:00 — No usage` : `${String(h).padStart(2, "0")}:00 — ${formatTokensShort$1(v)} tokens`;
|
|
485
485
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
486
486
|
title: hh,
|
|
487
487
|
"aria-label": hh,
|
|
@@ -904,7 +904,7 @@ window.__ModuleLoader__.load({
|
|
|
904
904
|
textOverflow: "ellipsis",
|
|
905
905
|
maxWidth: "100%"
|
|
906
906
|
},
|
|
907
|
-
children: d.totalTokens > 0 ? formatTokensShort(d.totalTokens) : ""
|
|
907
|
+
children: d.totalTokens > 0 ? formatTokensShort$1(d.totalTokens) : ""
|
|
908
908
|
})]
|
|
909
909
|
}, d.dayKey);
|
|
910
910
|
})
|
|
@@ -1024,7 +1024,7 @@ window.__ModuleLoader__.load({
|
|
|
1024
1024
|
width: 80,
|
|
1025
1025
|
textAlign: "right"
|
|
1026
1026
|
},
|
|
1027
|
-
children: d.totalTokens === 0 ? "No usage" : `${formatTokensShort(d.totalTokens)} tokens`
|
|
1027
|
+
children: d.totalTokens === 0 ? "No usage" : `${formatTokensShort$1(d.totalTokens)} tokens`
|
|
1028
1028
|
})
|
|
1029
1029
|
]
|
|
1030
1030
|
}, d.dayKey);
|
|
@@ -1260,6 +1260,664 @@ window.__ModuleLoader__.load({
|
|
|
1260
1260
|
});
|
|
1261
1261
|
}
|
|
1262
1262
|
//#endregion
|
|
1263
|
+
//#region src/client/trend.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* Pure logic for the bottom-of-page trend line chart: window slicing,
|
|
1266
|
+
* top-model selection, per-model series building and monotone cubic smoothing.
|
|
1267
|
+
* Kept free of React/DOM so it is unit-testable and reusable.
|
|
1268
|
+
*/
|
|
1269
|
+
/** N consecutive day keys ending at `endKey` (inclusive). */
|
|
1270
|
+
function recentDayKeys(endKey, n) {
|
|
1271
|
+
const out = [];
|
|
1272
|
+
for (let i = n - 1; i >= 0; i--) out.push(addDays(endKey, -i));
|
|
1273
|
+
return out;
|
|
1274
|
+
}
|
|
1275
|
+
/** Sum each model's tokens over `dayKeys`. */
|
|
1276
|
+
function modelTotals(days, dayKeys) {
|
|
1277
|
+
const inWindow = new Set(dayKeys);
|
|
1278
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1279
|
+
for (const d of days) {
|
|
1280
|
+
if (!inWindow.has(d.dayKey)) continue;
|
|
1281
|
+
for (const [model, tokens] of d.byModel) totals.set(model, (totals.get(model) ?? 0) + tokens);
|
|
1282
|
+
}
|
|
1283
|
+
return totals;
|
|
1284
|
+
}
|
|
1285
|
+
const byTotalDesc = (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1);
|
|
1286
|
+
/** Sum each model's tokens over `dayKeys`, return up to `k` names by total desc. */
|
|
1287
|
+
function pickTopModels(days, dayKeys, k = 5) {
|
|
1288
|
+
return [...modelTotals(days, dayKeys).entries()].sort(byTotalDesc).slice(0, k).map(([model]) => model);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Legend selection for the trend chart: the range's top-k models PLUS the
|
|
1292
|
+
* week's top-`weekTopN` models guaranteed (a model that took over this week
|
|
1293
|
+
* must stay visible in a 30/90-day view). If the union exceeds `k`, the
|
|
1294
|
+
* weakest range-ranked models give up their slots. Result is ordered by range
|
|
1295
|
+
* usage desc, deduplicated, at most `k` names.
|
|
1296
|
+
*/
|
|
1297
|
+
function pickTrendModels(days, dayKeys, weekKeys, k = 10, weekTopN = 3) {
|
|
1298
|
+
const rangeTotals = modelTotals(days, dayKeys);
|
|
1299
|
+
const rangeRanked = [...rangeTotals.entries()].sort(byTotalDesc).slice(0, k).map(([m]) => m);
|
|
1300
|
+
const weekTop = pickTopModels(days, weekKeys, weekTopN);
|
|
1301
|
+
const selected = new Set(weekTop);
|
|
1302
|
+
for (const m of rangeRanked) {
|
|
1303
|
+
if (selected.size >= k) break;
|
|
1304
|
+
selected.add(m);
|
|
1305
|
+
}
|
|
1306
|
+
return [...selected].sort((a, b) => (rangeTotals.get(b) ?? 0) - (rangeTotals.get(a) ?? 0) || (a < b ? -1 : 1));
|
|
1307
|
+
}
|
|
1308
|
+
/** One series per model: its daily tokens aligned to `dayKeys`, 0 where absent. */
|
|
1309
|
+
function buildSeries(days, dayKeys, models) {
|
|
1310
|
+
const index = new Map(days.map((d) => [d.dayKey, d]));
|
|
1311
|
+
return models.map((model) => ({
|
|
1312
|
+
model,
|
|
1313
|
+
points: dayKeys.map((k) => index.get(k)?.byModel.get(model) ?? 0)
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Monotone cubic (Fritsch–Carlson) Hermite interpolation of 2-D points as
|
|
1318
|
+
* cubic bezier segments. Used instead of plain Catmull-Rom because a smooth
|
|
1319
|
+
* trend line must NOT overshoot: a zero day between two peaks stays at zero
|
|
1320
|
+
* instead of dipping below the axis and inventing a fake trough.
|
|
1321
|
+
*/
|
|
1322
|
+
function monotoneSegments(pts) {
|
|
1323
|
+
const n = pts.length;
|
|
1324
|
+
if (n < 2) return [];
|
|
1325
|
+
const h = [];
|
|
1326
|
+
const delta = [];
|
|
1327
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1328
|
+
const dx = pts[i + 1][0] - pts[i][0];
|
|
1329
|
+
h.push(dx);
|
|
1330
|
+
delta.push(dx === 0 ? 0 : (pts[i + 1][1] - pts[i][1]) / dx);
|
|
1331
|
+
}
|
|
1332
|
+
const m = new Array(n);
|
|
1333
|
+
m[0] = delta[0];
|
|
1334
|
+
m[n - 1] = delta[n - 2];
|
|
1335
|
+
for (let i = 1; i < n - 1; i++) m[i] = delta[i - 1] * delta[i] <= 0 ? 0 : (delta[i - 1] + delta[i]) / 2;
|
|
1336
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1337
|
+
if (delta[i] === 0) {
|
|
1338
|
+
m[i] = 0;
|
|
1339
|
+
m[i + 1] = 0;
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
const a = m[i] / delta[i];
|
|
1343
|
+
const b = m[i + 1] / delta[i];
|
|
1344
|
+
const s = a * a + b * b;
|
|
1345
|
+
if (s > 9) {
|
|
1346
|
+
const t = 3 / Math.sqrt(s);
|
|
1347
|
+
m[i] = t * a * delta[i];
|
|
1348
|
+
m[i + 1] = t * b * delta[i];
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const segs = [];
|
|
1352
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1353
|
+
const [x0, y0] = pts[i];
|
|
1354
|
+
const [x1, y1] = pts[i + 1];
|
|
1355
|
+
const dx = (x1 - x0) / 3;
|
|
1356
|
+
segs.push({
|
|
1357
|
+
p0: [x0, y0],
|
|
1358
|
+
c1: [x0 + dx, y0 + m[i] * (x1 - x0) / 3],
|
|
1359
|
+
c2: [x1 - dx, y1 - m[i + 1] * (x1 - x0) / 3],
|
|
1360
|
+
p1: [x1, y1]
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
return segs;
|
|
1364
|
+
}
|
|
1365
|
+
const fmtNum = (v) => Math.round(v * 100) / 100;
|
|
1366
|
+
/** SVG path `d` for a smoothed polyline through `points` ([x,y] pairs). */
|
|
1367
|
+
function seriesPath(points) {
|
|
1368
|
+
if (points.length === 0) return "";
|
|
1369
|
+
const head = `M ${fmtNum(points[0][0])} ${fmtNum(points[0][1])}`;
|
|
1370
|
+
if (points.length === 1) return head;
|
|
1371
|
+
return head + monotoneSegments(points).map((s) => ` C ${fmtNum(s.c1[0])} ${fmtNum(s.c1[1])}, ${fmtNum(s.c2[0])} ${fmtNum(s.c2[1])}, ${fmtNum(s.p1[0])} ${fmtNum(s.p1[1])}`).join("");
|
|
1372
|
+
}
|
|
1373
|
+
/** Compact token count for axis labels (same semantics as the heatmap). */
|
|
1374
|
+
function formatTokensShort(n) {
|
|
1375
|
+
if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
|
|
1376
|
+
if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
|
|
1377
|
+
if (n >= 1e3) return (n / 1e3).toFixed(2) + "k";
|
|
1378
|
+
return n.toLocaleString();
|
|
1379
|
+
}
|
|
1380
|
+
//#endregion
|
|
1381
|
+
//#region src/client/TrendChart.tsx
|
|
1382
|
+
/** Distinct, colorblind-tolerant series colors (first matches the heatmap green). */
|
|
1383
|
+
const COLORS = [
|
|
1384
|
+
"#40c463",
|
|
1385
|
+
"#4c8dff",
|
|
1386
|
+
"#f6a609",
|
|
1387
|
+
"#c956e0",
|
|
1388
|
+
"#ff7a59",
|
|
1389
|
+
"#26c6da",
|
|
1390
|
+
"#ff5c8a",
|
|
1391
|
+
"#9ccc65",
|
|
1392
|
+
"#7e57c2",
|
|
1393
|
+
"#ffd54f"
|
|
1394
|
+
];
|
|
1395
|
+
/** Round a max up to a friendly axis ceiling (steps of half a decade). */
|
|
1396
|
+
function niceCeil(v) {
|
|
1397
|
+
if (v <= 0) return 1;
|
|
1398
|
+
const unit = Math.pow(10, Math.floor(Math.log10(v))) / 2;
|
|
1399
|
+
return Math.ceil(v / unit) * unit;
|
|
1400
|
+
}
|
|
1401
|
+
const W = 720;
|
|
1402
|
+
const H = 280;
|
|
1403
|
+
const PAD = {
|
|
1404
|
+
top: 10,
|
|
1405
|
+
right: 28,
|
|
1406
|
+
bottom: 34,
|
|
1407
|
+
left: 76
|
|
1408
|
+
};
|
|
1409
|
+
/** chart container horizontal padding (must match the div below) */
|
|
1410
|
+
const CX_PAD = 8;
|
|
1411
|
+
/**
|
|
1412
|
+
* Trend line chart at the bottom of the settings section: one smoothed line
|
|
1413
|
+
* per model (top 10 of the selected range), legend chips toggle visibility,
|
|
1414
|
+
* range switch between the last 7 / 30 / 90 days. Hovering a date column shows a
|
|
1415
|
+
* compact translucent tooltip BESIDE the cursor (never covering the hovered
|
|
1416
|
+
* point): date, day total, per-model totals with color dots.
|
|
1417
|
+
* Pure SVG + absolutely-positioned HTML tooltip, no dependencies.
|
|
1418
|
+
*/
|
|
1419
|
+
function TrendChart({ t, days, isEn = false }) {
|
|
1420
|
+
const [rangeDays, setRangeDays] = react.useState(7);
|
|
1421
|
+
const [hidden, setHidden] = react.useState(/* @__PURE__ */ new Set());
|
|
1422
|
+
/** hovered column index + pointer position in wrapper px (for side placement) */
|
|
1423
|
+
const [hover, setHover] = react.useState(null);
|
|
1424
|
+
const endKey = toDayKey(Date.now());
|
|
1425
|
+
const dayKeys = react.useMemo(() => recentDayKeys(endKey, rangeDays), [endKey, rangeDays]);
|
|
1426
|
+
const weekKeys = react.useMemo(() => recentDayKeys(endKey, 7), [endKey]);
|
|
1427
|
+
const topModels = react.useMemo(() => pickTrendModels(days, dayKeys, weekKeys, 10, 3), [
|
|
1428
|
+
days,
|
|
1429
|
+
dayKeys,
|
|
1430
|
+
weekKeys
|
|
1431
|
+
]);
|
|
1432
|
+
const series = react.useMemo(() => buildSeries(days, dayKeys, topModels), [
|
|
1433
|
+
days,
|
|
1434
|
+
dayKeys,
|
|
1435
|
+
topModels
|
|
1436
|
+
]);
|
|
1437
|
+
const totalsByModel = react.useMemo(() => {
|
|
1438
|
+
const m = /* @__PURE__ */ new Map();
|
|
1439
|
+
for (const s of series) m.set(s.model, s.points.reduce((a, b) => a + b, 0));
|
|
1440
|
+
return m;
|
|
1441
|
+
}, [series]);
|
|
1442
|
+
const wrapRef = react.useRef(null);
|
|
1443
|
+
const [wrap, setWrap] = react.useState({
|
|
1444
|
+
w: 0,
|
|
1445
|
+
h: 0
|
|
1446
|
+
});
|
|
1447
|
+
react.useEffect(() => {
|
|
1448
|
+
const el = wrapRef.current;
|
|
1449
|
+
if (!el) return;
|
|
1450
|
+
const measure = () => setWrap({
|
|
1451
|
+
w: el.clientWidth,
|
|
1452
|
+
h: el.clientHeight
|
|
1453
|
+
});
|
|
1454
|
+
measure();
|
|
1455
|
+
const ro = new ResizeObserver(measure);
|
|
1456
|
+
ro.observe(el);
|
|
1457
|
+
return () => ro.disconnect();
|
|
1458
|
+
}, []);
|
|
1459
|
+
const toggle = (model) => {
|
|
1460
|
+
setHidden((prev) => {
|
|
1461
|
+
const next = new Set(prev);
|
|
1462
|
+
if (next.has(model)) next.delete(model);
|
|
1463
|
+
else next.add(model);
|
|
1464
|
+
return next;
|
|
1465
|
+
});
|
|
1466
|
+
};
|
|
1467
|
+
const colorOf = (model) => COLORS[Math.max(0, topModels.indexOf(model)) % COLORS.length];
|
|
1468
|
+
const visible = series.filter((s) => !hidden.has(s.model));
|
|
1469
|
+
const plotW = W - PAD.left - PAD.right;
|
|
1470
|
+
const plotH = H - PAD.top - PAD.bottom;
|
|
1471
|
+
const yMax = niceCeil(Math.max(1, ...visible.map((s) => Math.max(0, ...s.points))));
|
|
1472
|
+
const step = dayKeys.length > 1 ? plotW / (dayKeys.length - 1) : plotW;
|
|
1473
|
+
const xOf = (i) => PAD.left + (dayKeys.length > 1 ? i * step : plotW / 2);
|
|
1474
|
+
const yOf = (v) => PAD.top + (1 - v / yMax) * plotH;
|
|
1475
|
+
const fmtDay = (k) => `${Number(k.slice(5, 7))}/${Number(k.slice(8, 10))}`;
|
|
1476
|
+
const fmtDayLong = (k) => isEn ? `${Number(k.slice(5, 7))}/${Number(k.slice(8, 10))}` : `${Number(k.slice(5, 7))}月${Number(k.slice(8, 10))}日`;
|
|
1477
|
+
const tickEvery = rangeDays === 7 ? 1 : rangeDays === 30 ? 5 : 10;
|
|
1478
|
+
const tickIdx = dayKeys.map((_, i) => i).filter((i) => i % tickEvery === 0 || i === dayKeys.length - 1);
|
|
1479
|
+
const onMove = (e) => {
|
|
1480
|
+
const wrapEl = wrapRef.current;
|
|
1481
|
+
if (!wrapEl) return;
|
|
1482
|
+
const wrapRect = wrapEl.getBoundingClientRect();
|
|
1483
|
+
const svgRect = e.currentTarget.getBoundingClientRect();
|
|
1484
|
+
const scale = svgRect.width / W || 1;
|
|
1485
|
+
const xvb = (e.clientX - svgRect.left) / scale;
|
|
1486
|
+
const i = Math.round((xvb - PAD.left) / step);
|
|
1487
|
+
setHover({
|
|
1488
|
+
idx: Math.max(0, Math.min(dayKeys.length - 1, i)),
|
|
1489
|
+
px: e.clientX - wrapRect.left,
|
|
1490
|
+
py: e.clientY - wrapRect.top
|
|
1491
|
+
});
|
|
1492
|
+
};
|
|
1493
|
+
const hoverRows = hover == null ? [] : visible.map((s) => ({
|
|
1494
|
+
model: s.model,
|
|
1495
|
+
color: colorOf(s.model),
|
|
1496
|
+
value: s.points[hover.idx]
|
|
1497
|
+
})).filter((r) => r.value > 0).sort((a, b) => b.value - a.value);
|
|
1498
|
+
const hoverTotal = hoverRows.reduce((a, r) => a + r.value, 0);
|
|
1499
|
+
const tipW = 200;
|
|
1500
|
+
const tipH = 56 + hoverRows.length * 19;
|
|
1501
|
+
const gap = 14;
|
|
1502
|
+
const svgW = Math.max(0, wrap.w - 16);
|
|
1503
|
+
const colPx = hover == null ? 0 : CX_PAD + xOf(hover.idx) / W * svgW;
|
|
1504
|
+
let tipLeft = hover == null ? 0 : colPx + gap;
|
|
1505
|
+
if (hover != null && tipLeft + tipW > wrap.w - 4) tipLeft = colPx - gap - tipW;
|
|
1506
|
+
tipLeft = Math.max(4, tipLeft);
|
|
1507
|
+
const tipTop = hover == null ? 0 : Math.max(4, Math.min(hover.py - tipH / 2, Math.max(4, wrap.h - tipH - 4)));
|
|
1508
|
+
const segStyle = {
|
|
1509
|
+
padding: "2px 8px",
|
|
1510
|
+
fontSize: 12,
|
|
1511
|
+
border: "none",
|
|
1512
|
+
cursor: "pointer",
|
|
1513
|
+
borderRadius: 5,
|
|
1514
|
+
background: "var(--dsw-alias-bg-layer-3)",
|
|
1515
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
1516
|
+
};
|
|
1517
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1518
|
+
style: {
|
|
1519
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1520
|
+
background: "var(--dsw-alias-bg-layer-3)",
|
|
1521
|
+
borderRadius: 12,
|
|
1522
|
+
overflow: "hidden"
|
|
1523
|
+
},
|
|
1524
|
+
children: [
|
|
1525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1526
|
+
style: {
|
|
1527
|
+
padding: "12px 16px 8px",
|
|
1528
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2)",
|
|
1529
|
+
display: "flex",
|
|
1530
|
+
alignItems: "center",
|
|
1531
|
+
justifyContent: "space-between",
|
|
1532
|
+
gap: 8
|
|
1533
|
+
},
|
|
1534
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1535
|
+
style: {
|
|
1536
|
+
display: "flex",
|
|
1537
|
+
alignItems: "center",
|
|
1538
|
+
gap: 8
|
|
1539
|
+
},
|
|
1540
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1541
|
+
style: {
|
|
1542
|
+
fontSize: 13,
|
|
1543
|
+
fontWeight: 600,
|
|
1544
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1545
|
+
},
|
|
1546
|
+
children: t("trend.title")
|
|
1547
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1548
|
+
style: {
|
|
1549
|
+
fontSize: 12,
|
|
1550
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1551
|
+
},
|
|
1552
|
+
children: t("trend.hint")
|
|
1553
|
+
})]
|
|
1554
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1555
|
+
style: {
|
|
1556
|
+
display: "inline-flex",
|
|
1557
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1558
|
+
borderRadius: 6,
|
|
1559
|
+
overflow: "hidden"
|
|
1560
|
+
},
|
|
1561
|
+
children: [
|
|
1562
|
+
7,
|
|
1563
|
+
30,
|
|
1564
|
+
90
|
|
1565
|
+
].map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1566
|
+
onClick: () => {
|
|
1567
|
+
setRangeDays(n);
|
|
1568
|
+
setHover(null);
|
|
1569
|
+
},
|
|
1570
|
+
style: {
|
|
1571
|
+
...segStyle,
|
|
1572
|
+
background: rangeDays === n ? "var(--dsw-alias-label-primary)" : "transparent",
|
|
1573
|
+
color: rangeDays === n ? "var(--dsw-alias-bg-layer-3)" : "var(--dsw-alias-label-secondary)"
|
|
1574
|
+
},
|
|
1575
|
+
children: n === 7 ? t("trend.range7") : n === 30 ? t("trend.range30") : t("trend.range90")
|
|
1576
|
+
}, n))
|
|
1577
|
+
})]
|
|
1578
|
+
}),
|
|
1579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1580
|
+
style: { padding: "10px 16px 4px" },
|
|
1581
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1582
|
+
style: {
|
|
1583
|
+
display: "grid",
|
|
1584
|
+
gridTemplateColumns: "repeat(5, minmax(0, 1fr))",
|
|
1585
|
+
gap: 6
|
|
1586
|
+
},
|
|
1587
|
+
children: series.map((s) => {
|
|
1588
|
+
const off = hidden.has(s.model);
|
|
1589
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1590
|
+
onClick: () => toggle(s.model),
|
|
1591
|
+
title: s.model,
|
|
1592
|
+
style: {
|
|
1593
|
+
display: "inline-flex",
|
|
1594
|
+
alignItems: "center",
|
|
1595
|
+
gap: 4,
|
|
1596
|
+
padding: "1px 6px",
|
|
1597
|
+
borderRadius: 999,
|
|
1598
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1599
|
+
background: "transparent",
|
|
1600
|
+
cursor: "pointer",
|
|
1601
|
+
opacity: off ? .35 : 1,
|
|
1602
|
+
fontSize: 11,
|
|
1603
|
+
color: "var(--dsw-alias-label-primary)",
|
|
1604
|
+
minWidth: 0
|
|
1605
|
+
},
|
|
1606
|
+
children: [
|
|
1607
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
|
|
1608
|
+
width: 6,
|
|
1609
|
+
height: 6,
|
|
1610
|
+
borderRadius: 3,
|
|
1611
|
+
background: colorOf(s.model),
|
|
1612
|
+
flexShrink: 0
|
|
1613
|
+
} }),
|
|
1614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1615
|
+
style: {
|
|
1616
|
+
flex: 1,
|
|
1617
|
+
minWidth: 0,
|
|
1618
|
+
overflow: "hidden",
|
|
1619
|
+
textOverflow: "ellipsis",
|
|
1620
|
+
whiteSpace: "nowrap",
|
|
1621
|
+
textAlign: "left",
|
|
1622
|
+
textDecoration: off ? "line-through" : "none"
|
|
1623
|
+
},
|
|
1624
|
+
children: s.model
|
|
1625
|
+
}),
|
|
1626
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1627
|
+
style: {
|
|
1628
|
+
fontSize: 10,
|
|
1629
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
1630
|
+
flexShrink: 0
|
|
1631
|
+
},
|
|
1632
|
+
children: formatTokensShort(totalsByModel.get(s.model) ?? 0)
|
|
1633
|
+
})
|
|
1634
|
+
]
|
|
1635
|
+
}, s.model);
|
|
1636
|
+
})
|
|
1637
|
+
}), series.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1638
|
+
style: {
|
|
1639
|
+
fontSize: 10,
|
|
1640
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
1641
|
+
marginTop: 4
|
|
1642
|
+
},
|
|
1643
|
+
children: t("trend.legend.hint")
|
|
1644
|
+
})]
|
|
1645
|
+
}),
|
|
1646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1647
|
+
ref: wrapRef,
|
|
1648
|
+
style: {
|
|
1649
|
+
position: "relative",
|
|
1650
|
+
padding: `4px ${CX_PAD}px 12px`
|
|
1651
|
+
},
|
|
1652
|
+
children: [topModels.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1653
|
+
style: {
|
|
1654
|
+
padding: 24,
|
|
1655
|
+
textAlign: "center",
|
|
1656
|
+
fontSize: 14,
|
|
1657
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1658
|
+
},
|
|
1659
|
+
children: t("trend.empty")
|
|
1660
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1661
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
1662
|
+
width: "100%",
|
|
1663
|
+
style: { display: "block" },
|
|
1664
|
+
role: "img",
|
|
1665
|
+
"aria-label": t("trend.title"),
|
|
1666
|
+
onMouseMove: onMove,
|
|
1667
|
+
onMouseLeave: () => setHover(null),
|
|
1668
|
+
children: [
|
|
1669
|
+
[
|
|
1670
|
+
0,
|
|
1671
|
+
.25,
|
|
1672
|
+
.5,
|
|
1673
|
+
.75,
|
|
1674
|
+
1
|
|
1675
|
+
].map((f) => {
|
|
1676
|
+
const y = PAD.top + (1 - f) * plotH;
|
|
1677
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
1678
|
+
x1: PAD.left,
|
|
1679
|
+
y1: y,
|
|
1680
|
+
x2: W - PAD.right,
|
|
1681
|
+
y2: y,
|
|
1682
|
+
stroke: "var(--dsw-alias-border-l2)",
|
|
1683
|
+
strokeWidth: 1,
|
|
1684
|
+
strokeDasharray: f === 0 ? void 0 : "3 3"
|
|
1685
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1686
|
+
x: PAD.left - 8,
|
|
1687
|
+
y: y + 5,
|
|
1688
|
+
textAnchor: "end",
|
|
1689
|
+
fontSize: 14,
|
|
1690
|
+
fill: "var(--dsw-alias-label-tertiary)",
|
|
1691
|
+
children: formatTokensShort(yMax * f)
|
|
1692
|
+
})] }, f);
|
|
1693
|
+
}),
|
|
1694
|
+
tickIdx.map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1695
|
+
x: xOf(i),
|
|
1696
|
+
y: 270,
|
|
1697
|
+
textAnchor: "middle",
|
|
1698
|
+
fontSize: 14,
|
|
1699
|
+
fill: "var(--dsw-alias-label-tertiary)",
|
|
1700
|
+
children: fmtDay(dayKeys[i])
|
|
1701
|
+
}, i)),
|
|
1702
|
+
hover != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
1703
|
+
x1: xOf(hover.idx),
|
|
1704
|
+
y1: PAD.top,
|
|
1705
|
+
x2: xOf(hover.idx),
|
|
1706
|
+
y2: PAD.top + plotH,
|
|
1707
|
+
stroke: "var(--dsw-alias-label-tertiary)",
|
|
1708
|
+
strokeWidth: 1,
|
|
1709
|
+
strokeDasharray: "4 3",
|
|
1710
|
+
opacity: .6
|
|
1711
|
+
}) : null,
|
|
1712
|
+
visible.map((s) => {
|
|
1713
|
+
const color = colorOf(s.model);
|
|
1714
|
+
const pts = s.points.map((v, i) => [xOf(i), yOf(v)]);
|
|
1715
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
|
|
1716
|
+
opacity: .8,
|
|
1717
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1718
|
+
d: seriesPath(pts),
|
|
1719
|
+
fill: "none",
|
|
1720
|
+
stroke: color,
|
|
1721
|
+
strokeWidth: 2,
|
|
1722
|
+
strokeLinecap: "round",
|
|
1723
|
+
strokeLinejoin: "round"
|
|
1724
|
+
}), pts.map(([x, y], i) => s.points[i] > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1725
|
+
cx: x,
|
|
1726
|
+
cy: y,
|
|
1727
|
+
r: hover?.idx === i ? 4 : 2,
|
|
1728
|
+
fill: color
|
|
1729
|
+
}, i) : null)]
|
|
1730
|
+
}, s.model);
|
|
1731
|
+
}),
|
|
1732
|
+
visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1733
|
+
x: W / 2,
|
|
1734
|
+
y: PAD.top + plotH / 2,
|
|
1735
|
+
textAnchor: "middle",
|
|
1736
|
+
fontSize: 13,
|
|
1737
|
+
fill: "var(--dsw-alias-label-tertiary)",
|
|
1738
|
+
children: t("trend.legend.hint")
|
|
1739
|
+
}) : null
|
|
1740
|
+
]
|
|
1741
|
+
}), hover != null && topModels.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1742
|
+
style: {
|
|
1743
|
+
position: "absolute",
|
|
1744
|
+
left: tipLeft,
|
|
1745
|
+
top: tipTop,
|
|
1746
|
+
width: tipW,
|
|
1747
|
+
opacity: .7,
|
|
1748
|
+
pointerEvents: "none",
|
|
1749
|
+
background: "var(--dsw-alias-bg-layer-3)",
|
|
1750
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1751
|
+
borderRadius: 8,
|
|
1752
|
+
boxShadow: "0 6px 18px rgba(0,0,0,0.25)",
|
|
1753
|
+
padding: "6px 10px",
|
|
1754
|
+
fontSize: 11,
|
|
1755
|
+
lineHeight: 1.5,
|
|
1756
|
+
color: "var(--dsw-alias-label-primary)",
|
|
1757
|
+
zIndex: 5
|
|
1758
|
+
},
|
|
1759
|
+
children: [
|
|
1760
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1761
|
+
style: {
|
|
1762
|
+
fontWeight: 600,
|
|
1763
|
+
fontSize: 12,
|
|
1764
|
+
marginBottom: 4
|
|
1765
|
+
},
|
|
1766
|
+
children: fmtDayLong(dayKeys[hover.idx])
|
|
1767
|
+
}),
|
|
1768
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1769
|
+
style: {
|
|
1770
|
+
display: "flex",
|
|
1771
|
+
justifyContent: "space-between",
|
|
1772
|
+
gap: 10,
|
|
1773
|
+
fontWeight: 600
|
|
1774
|
+
},
|
|
1775
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: isEn ? "Total" : "消耗总量" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [formatTokensShort(hoverTotal), " tokens"] })]
|
|
1776
|
+
}),
|
|
1777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
1778
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)",
|
|
1779
|
+
margin: "4px 0"
|
|
1780
|
+
} }),
|
|
1781
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1782
|
+
style: {
|
|
1783
|
+
display: "flex",
|
|
1784
|
+
flexDirection: "column",
|
|
1785
|
+
gap: 3
|
|
1786
|
+
},
|
|
1787
|
+
children: hoverRows.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1788
|
+
style: {
|
|
1789
|
+
display: "flex",
|
|
1790
|
+
alignItems: "center",
|
|
1791
|
+
gap: 6
|
|
1792
|
+
},
|
|
1793
|
+
children: [
|
|
1794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
|
|
1795
|
+
width: 7,
|
|
1796
|
+
height: 7,
|
|
1797
|
+
borderRadius: 4,
|
|
1798
|
+
background: r.color,
|
|
1799
|
+
flexShrink: 0
|
|
1800
|
+
} }),
|
|
1801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1802
|
+
style: {
|
|
1803
|
+
flex: 1,
|
|
1804
|
+
minWidth: 0,
|
|
1805
|
+
overflow: "hidden",
|
|
1806
|
+
textOverflow: "ellipsis",
|
|
1807
|
+
whiteSpace: "nowrap"
|
|
1808
|
+
},
|
|
1809
|
+
title: r.model,
|
|
1810
|
+
children: r.model
|
|
1811
|
+
}),
|
|
1812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1813
|
+
style: {
|
|
1814
|
+
fontWeight: 500,
|
|
1815
|
+
whiteSpace: "nowrap"
|
|
1816
|
+
},
|
|
1817
|
+
children: [formatTokensShort(r.value), " tokens"]
|
|
1818
|
+
})
|
|
1819
|
+
]
|
|
1820
|
+
}, r.model))
|
|
1821
|
+
})
|
|
1822
|
+
]
|
|
1823
|
+
}) : null]
|
|
1824
|
+
})
|
|
1825
|
+
]
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
//#endregion
|
|
1829
|
+
//#region src/client/locales.ts
|
|
1830
|
+
const zh = {
|
|
1831
|
+
"nav": "Token Pulse",
|
|
1832
|
+
"heatmap.title": "Token Pulse",
|
|
1833
|
+
"heatmap.subtitle": "基于本地会话日志统计 Token 用量,北京时间自然日分桶,零网络零计费。首次同步后持久化,删除会话不丢历史。",
|
|
1834
|
+
"heatmap.empty": "暂无用量数据,开始一次对话后这里会出现热图。",
|
|
1835
|
+
"heatmap.legend.less": "少",
|
|
1836
|
+
"heatmap.legend.more": "多",
|
|
1837
|
+
"heatmap.tooltip.tokens": "{count} tokens",
|
|
1838
|
+
"heatmap.tooltip.none": "无用量",
|
|
1839
|
+
"heatmap.refresh": "刷新",
|
|
1840
|
+
"heatmap.refreshing": "刷新中…",
|
|
1841
|
+
"heatmap.lastRefresh": "上次刷新:{time}",
|
|
1842
|
+
"heatmap.autoRefresh": "自动刷新",
|
|
1843
|
+
"heatmap.autoRefresh.off": "关闭",
|
|
1844
|
+
"heatmap.autoRefresh.hint": "设置自动刷新间隔(分钟)",
|
|
1845
|
+
"heatmap.reset": "重置历史",
|
|
1846
|
+
"heatmap.reset.confirm": "确定重置所有历史数据?此操作不可撤销。",
|
|
1847
|
+
"heatmap.reset.done": "已重置",
|
|
1848
|
+
"view.week": "周",
|
|
1849
|
+
"view.month": "月",
|
|
1850
|
+
"view.quarter": "季度",
|
|
1851
|
+
"view.year": "年",
|
|
1852
|
+
"view.model": "模型",
|
|
1853
|
+
"view.provider": "供应商",
|
|
1854
|
+
"stats.today": "今日",
|
|
1855
|
+
"stats.week": "本周",
|
|
1856
|
+
"stats.month": "本月",
|
|
1857
|
+
"stats.all": "累计",
|
|
1858
|
+
"stats.count": "{count} 次",
|
|
1859
|
+
"model.top5": "Top 5 模型",
|
|
1860
|
+
"model.top5.hint": "按 Token 用量排序",
|
|
1861
|
+
"model.winner": "当日最常用模型",
|
|
1862
|
+
"model.none": "暂无模型数据",
|
|
1863
|
+
"model.tokens": "{count} tokens",
|
|
1864
|
+
"provider.top5": "Top 5 供应商",
|
|
1865
|
+
"provider.winner": "当日最常用供应商",
|
|
1866
|
+
"trend.title": "用量趋势",
|
|
1867
|
+
"trend.hint": "按模型的每日 Token 用量",
|
|
1868
|
+
"trend.range7": "近一周",
|
|
1869
|
+
"trend.range30": "近一月",
|
|
1870
|
+
"trend.range90": "近90天",
|
|
1871
|
+
"trend.empty": "所选范围内暂无数据",
|
|
1872
|
+
"trend.legend.hint": "点击图例可显示/隐藏",
|
|
1873
|
+
"common.tokens": "tokens"
|
|
1874
|
+
};
|
|
1875
|
+
const en = {
|
|
1876
|
+
"nav": "Token Pulse",
|
|
1877
|
+
"heatmap.title": "Token Pulse",
|
|
1878
|
+
"heatmap.subtitle": "Local token usage from session logs, bucketed by Asia/Shanghai, no network or billing. Synced once then persisted, surviving session deletion.",
|
|
1879
|
+
"heatmap.empty": "No usage yet. Start a conversation and the heatmap will appear here.",
|
|
1880
|
+
"heatmap.legend.less": "Less",
|
|
1881
|
+
"heatmap.legend.more": "More",
|
|
1882
|
+
"heatmap.tooltip.tokens": "{count} tokens",
|
|
1883
|
+
"heatmap.tooltip.none": "No usage",
|
|
1884
|
+
"heatmap.refresh": "Refresh",
|
|
1885
|
+
"heatmap.refreshing": "Refreshing…",
|
|
1886
|
+
"heatmap.lastRefresh": "Last refresh: {time}",
|
|
1887
|
+
"heatmap.autoRefresh": "Auto refresh",
|
|
1888
|
+
"heatmap.autoRefresh.off": "Off",
|
|
1889
|
+
"heatmap.autoRefresh.hint": "Auto refresh interval (minutes)",
|
|
1890
|
+
"heatmap.reset": "Reset history",
|
|
1891
|
+
"heatmap.reset.confirm": "Reset all historical data? This cannot be undone.",
|
|
1892
|
+
"heatmap.reset.done": "Reset done",
|
|
1893
|
+
"view.week": "Week",
|
|
1894
|
+
"view.month": "Month",
|
|
1895
|
+
"view.quarter": "Quarter",
|
|
1896
|
+
"view.year": "Year",
|
|
1897
|
+
"view.model": "Model",
|
|
1898
|
+
"view.provider": "Provider",
|
|
1899
|
+
"stats.today": "Today",
|
|
1900
|
+
"stats.week": "This week",
|
|
1901
|
+
"stats.month": "This month",
|
|
1902
|
+
"stats.all": "All time",
|
|
1903
|
+
"stats.count": "{count} turns",
|
|
1904
|
+
"model.top5": "Top 5 Models",
|
|
1905
|
+
"model.top5.hint": "By token usage",
|
|
1906
|
+
"model.winner": "Daily winner (model)",
|
|
1907
|
+
"model.none": "No model data",
|
|
1908
|
+
"model.tokens": "{count} tokens",
|
|
1909
|
+
"provider.top5": "Top 5 Providers",
|
|
1910
|
+
"provider.winner": "Daily winner (provider)",
|
|
1911
|
+
"trend.title": "Usage Trend",
|
|
1912
|
+
"trend.hint": "Daily token usage per model",
|
|
1913
|
+
"trend.range7": "7 days",
|
|
1914
|
+
"trend.range30": "30 days",
|
|
1915
|
+
"trend.range90": "90 days",
|
|
1916
|
+
"trend.empty": "No data in this range",
|
|
1917
|
+
"trend.legend.hint": "Click legend to toggle",
|
|
1918
|
+
"common.tokens": "tokens"
|
|
1919
|
+
};
|
|
1920
|
+
//#endregion
|
|
1263
1921
|
//#region src/client/hooks.ts
|
|
1264
1922
|
const LS_INTERVAL_KEY = "dsh-token-pulse:autoRefreshMinutes";
|
|
1265
1923
|
function getAutoRefreshMinutes() {
|
|
@@ -1543,6 +2201,10 @@ window.__ModuleLoader__.load({
|
|
|
1543
2201
|
} catch {}
|
|
1544
2202
|
};
|
|
1545
2203
|
const days = injectedDays ?? computedDays;
|
|
2204
|
+
const allDays = react.useMemo(() => {
|
|
2205
|
+
if (aggregated?.byDay) return [...aggregated.byDay.values()];
|
|
2206
|
+
return days ?? [];
|
|
2207
|
+
}, [aggregated, days]);
|
|
1546
2208
|
const totals = injectedTotals ?? aggregated?.totals;
|
|
1547
2209
|
const viewTopModelsRaw = computedTopModels.length ? computedTopModels : aggregated?.topModels ?? [];
|
|
1548
2210
|
const viewTopProvidersRaw = computedTopProviders.length ? computedTopProviders : aggregated?.topProviders ?? [];
|
|
@@ -1662,7 +2324,7 @@ window.__ModuleLoader__.load({
|
|
|
1662
2324
|
color: "var(--dsw-alias-label-tertiary)",
|
|
1663
2325
|
verticalAlign: "middle"
|
|
1664
2326
|
},
|
|
1665
|
-
children: ["v", "0.
|
|
2327
|
+
children: ["v", "0.4.0"]
|
|
1666
2328
|
})]
|
|
1667
2329
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1668
2330
|
style: {
|
|
@@ -2117,89 +2779,16 @@ window.__ModuleLoader__.load({
|
|
|
2117
2779
|
fontSize: 13
|
|
2118
2780
|
},
|
|
2119
2781
|
children: t("heatmap.empty")
|
|
2782
|
+
}) : null,
|
|
2783
|
+
allDays.some((d) => d.totalTokens > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendChart, {
|
|
2784
|
+
t: lang === "en" ? (k, p) => en[k] ?? t(k, p) : t,
|
|
2785
|
+
days: allDays,
|
|
2786
|
+
isEn: lang === "en"
|
|
2120
2787
|
}) : null
|
|
2121
2788
|
]
|
|
2122
2789
|
});
|
|
2123
2790
|
}
|
|
2124
2791
|
//#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
2792
|
//#region src/client/index.ts
|
|
2204
2793
|
const NS = "dsh-token-pulse";
|
|
2205
2794
|
const inject = ["locale", "slots"];
|