dsh-token-pulse 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1106,8 +1106,10 @@ window.__ModuleLoader__.load({
1106
1106
  if (n >= 1e3) return (n / 1e3).toFixed(2) + "k";
1107
1107
  return n.toLocaleString();
1108
1108
  }
1109
- function ModelTop5({ t, topModels, topProviders, days, mode = "model", onModeChange }) {
1109
+ function ModelTop5({ t, topModels, topProviders, days, mode = "model", onModeChange, othersModelTokens, othersProviderTokens }) {
1110
1110
  const active = mode === "provider" ? topProviders : topModels;
1111
+ const othersTokens = mode === "provider" ? othersProviderTokens : othersModelTokens;
1112
+ const hasOthers = (othersTokens ?? 0) > 0;
1111
1113
  if (active.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1112
1114
  style: {
1113
1115
  border: "1px solid var(--dsw-alias-border-l2)",
@@ -1193,12 +1195,12 @@ window.__ModuleLoader__.load({
1193
1195
  children: t("view.provider")
1194
1196
  })]
1195
1197
  }) : null]
1196
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1198
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1197
1199
  style: {
1198
1200
  display: "flex",
1199
1201
  flexDirection: "column"
1200
1202
  },
1201
- children: active.map((item, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1203
+ children: [active.map((item, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1202
1204
  style: {
1203
1205
  display: "flex",
1204
1206
  alignItems: "center",
@@ -1254,7 +1256,63 @@ window.__ModuleLoader__.load({
1254
1256
  children: formatTokens(item.tokens)
1255
1257
  })
1256
1258
  ]
1257
- }, item.name))
1259
+ }, item.name)), hasOthers ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1260
+ style: {
1261
+ display: "flex",
1262
+ alignItems: "center",
1263
+ gap: 12,
1264
+ padding: "10px 16px",
1265
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
1266
+ opacity: .75
1267
+ },
1268
+ children: [
1269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1270
+ style: {
1271
+ width: 20,
1272
+ textAlign: "center",
1273
+ fontSize: 12,
1274
+ color: "var(--dsw-alias-label-tertiary)"
1275
+ },
1276
+ children: "…"
1277
+ }),
1278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1279
+ style: {
1280
+ flex: "0 1 40%",
1281
+ minWidth: 0,
1282
+ fontSize: 13,
1283
+ color: "var(--dsw-alias-label-secondary)",
1284
+ overflow: "hidden",
1285
+ textOverflow: "ellipsis",
1286
+ whiteSpace: "nowrap"
1287
+ },
1288
+ title: t("model.others"),
1289
+ children: t("model.others")
1290
+ }),
1291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1292
+ style: {
1293
+ flex: 1,
1294
+ height: 6,
1295
+ borderRadius: 3,
1296
+ background: "var(--dsw-alias-bg-layer-2, #ebedf0)",
1297
+ overflow: "hidden"
1298
+ },
1299
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1300
+ width: `${Math.min(100, Math.max(4, (othersTokens ?? 0) / max * 100))}%`,
1301
+ height: "100%",
1302
+ background: "#9aa3ad",
1303
+ borderRadius: 3
1304
+ } })
1305
+ }),
1306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1307
+ style: {
1308
+ fontSize: 12,
1309
+ color: "var(--dsw-alias-label-secondary)",
1310
+ whiteSpace: "nowrap"
1311
+ },
1312
+ children: formatTokens(othersTokens ?? 0)
1313
+ })
1314
+ ]
1315
+ }) : null]
1258
1316
  })]
1259
1317
  })
1260
1318
  });
@@ -1283,6 +1341,22 @@ window.__ModuleLoader__.load({
1283
1341
  return totals;
1284
1342
  }
1285
1343
  const byTotalDesc = (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1);
1344
+ /** Synthetic series key for "all other models combined". */
1345
+ const OTHER_KEY = "__other__";
1346
+ /** Sum of every model NOT in `exclude`, per day, aligned to `dayKeys`. */
1347
+ function buildOtherSeries(days, dayKeys, exclude) {
1348
+ const index = new Map(dayKeys.map((k, i) => [k, i]));
1349
+ const points = new Array(dayKeys.length).fill(0);
1350
+ for (const d of days) {
1351
+ const i = index.get(d.dayKey);
1352
+ if (i === void 0) continue;
1353
+ for (const [m, v] of d.byModel) if (!exclude.has(m)) points[i] += v;
1354
+ }
1355
+ return {
1356
+ model: OTHER_KEY,
1357
+ points
1358
+ };
1359
+ }
1286
1360
  /** Sum each model's tokens over `dayKeys`, return up to `k` names by total desc. */
1287
1361
  function pickTopModels(days, dayKeys, k = 5) {
1288
1362
  return [...modelTotals(days, dayKeys).entries()].sort(byTotalDesc).slice(0, k).map(([model]) => model);
@@ -1363,12 +1437,33 @@ window.__ModuleLoader__.load({
1363
1437
  return segs;
1364
1438
  }
1365
1439
  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) {
1440
+ /**
1441
+ * Same curve as seriesPath, but resampled at `samples` evenly spaced x
1442
+ * positions into a FIXED-structure polyline (M + samples L). Every path then
1443
+ * has an identical command count no matter how many days it represents, so
1444
+ * CSS `transition: d` can morph lines across range switches too — where raw
1445
+ * bezier paths (M + n-1 C) have mismatched structures and would jump.
1446
+ * Visual output is indistinguishable at this density.
1447
+ */
1448
+ function seriesPathFixed(points, samples = 89) {
1368
1449
  if (points.length === 0) return "";
1369
- const head = `M ${fmtNum(points[0][0])} ${fmtNum(points[0][1])}`;
1450
+ const x0 = points[0][0];
1451
+ const x1 = points[points.length - 1][0];
1452
+ const head = `M ${fmtNum(x0)} ${fmtNum(points[0][1])}`;
1370
1453
  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("");
1454
+ const segs = monotoneSegments(points);
1455
+ let out = "";
1456
+ let seg = 0;
1457
+ for (let s = 1; s <= samples; s++) {
1458
+ const x = x0 + (x1 - x0) * s / samples;
1459
+ while (seg < segs.length - 1 && x > segs[seg].p1[0]) seg++;
1460
+ const g = segs[seg];
1461
+ const t = g.p1[0] === g.p0[0] ? 0 : Math.min(1, Math.max(0, (x - g.p0[0]) / (g.p1[0] - g.p0[0])));
1462
+ const mt = 1 - t;
1463
+ const y = mt * mt * mt * g.p0[1] + 3 * mt * mt * t * g.c1[1] + 3 * mt * t * t * g.c2[1] + t * t * t * g.p1[1];
1464
+ out += ` L ${fmtNum(x)} ${fmtNum(y)}`;
1465
+ }
1466
+ return head + out;
1372
1467
  }
1373
1468
  /** Compact token count for axis labels (same semantics as the heatmap). */
1374
1469
  function formatTokensShort(n) {
@@ -1398,6 +1493,11 @@ window.__ModuleLoader__.load({
1398
1493
  const unit = Math.pow(10, Math.floor(Math.log10(v))) / 2;
1399
1494
  return Math.ceil(v / unit) * unit;
1400
1495
  }
1496
+ /** "z-ai/glm-5.3" -> "glm-5.3": display only the part after the last "/". */
1497
+ function shortName(model) {
1498
+ const i = model.lastIndexOf("/");
1499
+ return i >= 0 ? model.slice(i + 1) : model;
1500
+ }
1401
1501
  const W = 720;
1402
1502
  const H = 280;
1403
1503
  const PAD = {
@@ -1408,37 +1508,96 @@ window.__ModuleLoader__.load({
1408
1508
  };
1409
1509
  /** chart container horizontal padding (must match the div below) */
1410
1510
  const CX_PAD = 8;
1511
+ const LS_TYPE_KEY = "dsh-token-pulse:trendChartType";
1512
+ /** transition/animation duration for the value/axis morphs (one tempo everywhere) */
1513
+ const DUR = ".55s";
1514
+ const T_POS = `x ${DUR} ease, y ${DUR} ease, width ${DUR} ease, height ${DUR} ease`;
1515
+ const T_D = `d ${DUR} ease`;
1516
+ const T_POINT = `cx ${DUR} ease, cy ${DUR} ease, r .2s ease`;
1517
+ const T_FADE = `opacity ${DUR} ease`;
1518
+ const T_SHIFT = `transform ${DUR} ease`;
1519
+ const GHOST_MS = 650;
1520
+ /**
1521
+ * Stable color assignment per MODEL (not per rank): the same model keeps its
1522
+ * color across range switches and legend toggles, so the animation never
1523
+ * repaints a series. First-seen order locks the palette slot for good.
1524
+ * Module-level so the assignment survives closing/reopening the panel.
1525
+ */
1526
+ const modelColorSlot = /* @__PURE__ */ new Map();
1527
+ function colorFor(model) {
1528
+ let slot = modelColorSlot.get(model);
1529
+ if (slot === void 0) {
1530
+ slot = modelColorSlot.size % COLORS.length;
1531
+ modelColorSlot.set(model, slot);
1532
+ }
1533
+ return COLORS[slot];
1534
+ }
1535
+ /**
1536
+ * Animation styles. Elements that ENTER on a range/type switch (CSS transitions
1537
+ * only animate elements that already existed) play a keyframe in the same 0.55s
1538
+ * tempo as the gliding elements, and elements that LEAVE stay mounted for one
1539
+ * beat as a "ghost" layer that collapses/fades — so the whole chart moves
1540
+ * together instead of mixing instant pops with in-flight marks.
1541
+ * The enter classes self-remove on animationend so live styles (hover opacity,
1542
+ * legend toggles) keep working afterwards.
1543
+ */
1544
+ const ANIM_CSS = `
1545
+ @keyframes dtp-grow { from { transform: scaleY(.02); opacity: .3; } }
1546
+ @keyframes dtp-fadein { from { opacity: 0; } }
1547
+ @keyframes dtp-shrink { to { transform: scaleY(.02); opacity: 0; } }
1548
+ @keyframes dtp-fadeout { to { opacity: 0; } }
1549
+ .dtp-growin { transform-box: fill-box; transform-origin: bottom; animation: dtp-grow ${DUR} ease; }
1550
+ .dtp-fadein { animation: dtp-fadein ${DUR} ease; }
1551
+ .dtp-ghost-bars { transform-box: fill-box; transform-origin: bottom; animation: dtp-shrink ${DUR} ease forwards; }
1552
+ .dtp-ghost-lines { animation: dtp-fadeout ${DUR} ease forwards; }
1553
+ `;
1411
1554
  /**
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.
1555
+ * Usage chart at the bottom of the settings section, two switchable forms:
1556
+ * - "line": one smoothed (monotone cubic) line per model over the days
1557
+ * - "bar": one stacked bar per day, visible models layered bottom-up
1558
+ * Both share the legend (top-10 + week's top-3, chips toggle models), the
1559
+ * 7 / 30 / 90-day ranges and a compact translucent hover tooltip BESIDE the
1560
+ * cursor (date, day total, per-model totals sorted desc, zero rows hidden).
1561
+ *
1562
+ * Range/type switches animate as ONE coordinated beat: retained marks glide
1563
+ * by date identity (the last 7 days of a 30-day view sit in the rightmost
1564
+ * slots, so 7→30 slides the week's bars right+down), entering marks grow from
1565
+ * the baseline / fade in, and leaving marks collapse in a ghost overlay.
1566
+ * Model colors are locked to model identity, never to rank position.
1567
+ *
1417
1568
  * Pure SVG + absolutely-positioned HTML tooltip, no dependencies.
1418
1569
  */
1419
1570
  function TrendChart({ t, days, isEn = false }) {
1420
1571
  const [rangeDays, setRangeDays] = react.useState(7);
1572
+ const [chartType, setChartType] = react.useState(() => {
1573
+ try {
1574
+ return localStorage.getItem(LS_TYPE_KEY) === "line" ? "line" : "bar";
1575
+ } catch {
1576
+ return "bar";
1577
+ }
1578
+ });
1421
1579
  const [hidden, setHidden] = react.useState(/* @__PURE__ */ new Set());
1422
1580
  /** hovered column index + pointer position in wrapper px (for side placement) */
1423
1581
  const [hover, setHover] = react.useState(null);
1582
+ /** one-beat overlay of the PREVIOUS layout's marks, collapsing out on switches */
1583
+ const [ghost, setGhost] = react.useState(null);
1424
1584
  const endKey = toDayKey(Date.now());
1425
1585
  const dayKeys = react.useMemo(() => recentDayKeys(endKey, rangeDays), [endKey, rangeDays]);
1426
1586
  const weekKeys = react.useMemo(() => recentDayKeys(endKey, 7), [endKey]);
1427
- const topModels = react.useMemo(() => pickTrendModels(days, dayKeys, weekKeys, 10, 3), [
1587
+ const topModels = react.useMemo(() => pickTrendModels(days, dayKeys, weekKeys, 9, 3), [
1428
1588
  days,
1429
1589
  dayKeys,
1430
1590
  weekKeys
1431
1591
  ]);
1432
- const series = react.useMemo(() => buildSeries(days, dayKeys, topModels), [
1592
+ const series = react.useMemo(() => {
1593
+ const base = buildSeries(days, dayKeys, topModels);
1594
+ const other = buildOtherSeries(days, dayKeys, new Set(topModels));
1595
+ return other.points.some((v) => v > 0) ? [...base, other] : base;
1596
+ }, [
1433
1597
  days,
1434
1598
  dayKeys,
1435
1599
  topModels
1436
1600
  ]);
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
1601
  const wrapRef = react.useRef(null);
1443
1602
  const [wrap, setWrap] = react.useState({
1444
1603
  w: 0,
@@ -1456,6 +1615,12 @@ window.__ModuleLoader__.load({
1456
1615
  ro.observe(el);
1457
1616
  return () => ro.disconnect();
1458
1617
  }, []);
1618
+ const prevDaysRef = react.useRef(null);
1619
+ const prevModelsRef = react.useRef(null);
1620
+ const prevTickRef = react.useRef(null);
1621
+ const prevTypeRef = react.useRef(null);
1622
+ const marksRef = react.useRef(null);
1623
+ const labelsRef = react.useRef(null);
1459
1624
  const toggle = (model) => {
1460
1625
  setHidden((prev) => {
1461
1626
  const next = new Set(prev);
@@ -1464,18 +1629,62 @@ window.__ModuleLoader__.load({
1464
1629
  return next;
1465
1630
  });
1466
1631
  };
1467
- const colorOf = (model) => COLORS[Math.max(0, topModels.indexOf(model)) % COLORS.length];
1468
- const visible = series.filter((s) => !hidden.has(s.model));
1632
+ const colorOf = (model) => model === "__other__" ? "#8b949e" : colorFor(model);
1633
+ /** 其它 series 用固定中性灰;显示名:其它系列显示「其它/Other」,模型只显示 "/" 后半段 */
1634
+ const dispName = (model) => model === "__other__" ? t("model.others") : shortName(model);
1635
+ const isHidden = (model) => hidden.has(model);
1636
+ const visible = series.filter((s) => !isHidden(s.model));
1469
1637
  const plotW = W - PAD.left - PAD.right;
1470
1638
  const plotH = H - PAD.top - PAD.bottom;
1471
- const yMax = niceCeil(Math.max(1, ...visible.map((s) => Math.max(0, ...s.points))));
1472
1639
  const step = dayKeys.length > 1 ? plotW / (dayKeys.length - 1) : plotW;
1473
1640
  const xOf = (i) => PAD.left + (dayKeys.length > 1 ? i * step : plotW / 2);
1641
+ const slot = plotW / dayKeys.length;
1642
+ const barW = Math.max(2, slot * .6);
1643
+ const cxOf = (i) => PAD.left + slot * (i + .5);
1644
+ const anchorX = (i) => chartType === "bar" ? cxOf(i) : xOf(i);
1645
+ const dayTotals = dayKeys.map((_, i) => visible.reduce((a, s) => a + s.points[i], 0));
1646
+ const yMax = chartType === "bar" ? niceCeil(Math.max(1, ...dayTotals)) : niceCeil(Math.max(1, ...visible.map((s) => Math.max(0, ...s.points))));
1474
1647
  const yOf = (v) => PAD.top + (1 - v / yMax) * plotH;
1475
1648
  const fmtDay = (k) => `${Number(k.slice(5, 7))}/${Number(k.slice(8, 10))}`;
1476
1649
  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
1650
  const tickEvery = rangeDays === 7 ? 1 : rangeDays === 30 ? 5 : 10;
1478
1651
  const tickIdx = dayKeys.map((_, i) => i).filter((i) => i % tickEvery === 0 || i === dayKeys.length - 1);
1652
+ const typeChanged = prevTypeRef.current !== null && prevTypeRef.current !== chartType;
1653
+ const isNewDay = (k) => prevDaysRef.current !== null && (typeChanged || !prevDaysRef.current.includes(k));
1654
+ const isNewModel = (m) => prevModelsRef.current !== null && (typeChanged || !prevModelsRef.current.includes(m));
1655
+ const isNewTick = (k) => prevTickRef.current !== null && (typeChanged || !prevTickRef.current.includes(k));
1656
+ const enterCls = (isNew, grow) => isNew ? grow ? "dtp-growin" : "dtp-fadein" : void 0;
1657
+ const onAnimEnd = (e) => {
1658
+ e.currentTarget.classList.remove("dtp-growin", "dtp-fadein");
1659
+ };
1660
+ react.useEffect(() => {
1661
+ marksRef.current = marksNode;
1662
+ labelsRef.current = labelsNode;
1663
+ prevDaysRef.current = dayKeys;
1664
+ prevModelsRef.current = series.map((s) => s.model);
1665
+ prevTickRef.current = tickIdx.map((i) => dayKeys[i]);
1666
+ prevTypeRef.current = chartType;
1667
+ });
1668
+ react.useEffect(() => {
1669
+ if (!ghost) return;
1670
+ const timer = setTimeout(() => setGhost(null), GHOST_MS);
1671
+ return () => clearTimeout(timer);
1672
+ }, [ghost]);
1673
+ const switchTo = (patch) => {
1674
+ setGhost({
1675
+ marks: marksRef.current,
1676
+ labels: labelsRef.current,
1677
+ type: chartType
1678
+ });
1679
+ if (patch.range !== void 0) setRangeDays(patch.range);
1680
+ if (patch.type !== void 0) {
1681
+ setChartType(patch.type);
1682
+ try {
1683
+ localStorage.setItem(LS_TYPE_KEY, patch.type);
1684
+ } catch {}
1685
+ }
1686
+ setHover(null);
1687
+ };
1479
1688
  const onMove = (e) => {
1480
1689
  const wrapEl = wrapRef.current;
1481
1690
  if (!wrapEl) return;
@@ -1483,7 +1692,7 @@ window.__ModuleLoader__.load({
1483
1692
  const svgRect = e.currentTarget.getBoundingClientRect();
1484
1693
  const scale = svgRect.width / W || 1;
1485
1694
  const xvb = (e.clientX - svgRect.left) / scale;
1486
- const i = Math.round((xvb - PAD.left) / step);
1695
+ const i = chartType === "bar" ? Math.floor((xvb - PAD.left) / slot) : Math.round((xvb - PAD.left) / step);
1487
1696
  setHover({
1488
1697
  idx: Math.max(0, Math.min(dayKeys.length - 1, i)),
1489
1698
  px: e.clientX - wrapRect.left,
@@ -1500,7 +1709,7 @@ window.__ModuleLoader__.load({
1500
1709
  const tipH = 56 + hoverRows.length * 19;
1501
1710
  const gap = 14;
1502
1711
  const svgW = Math.max(0, wrap.w - 16);
1503
- const colPx = hover == null ? 0 : CX_PAD + xOf(hover.idx) / W * svgW;
1712
+ const colPx = hover == null ? 0 : CX_PAD + anchorX(hover.idx) / W * svgW;
1504
1713
  let tipLeft = hover == null ? 0 : colPx + gap;
1505
1714
  if (hover != null && tipLeft + tipW > wrap.w - 4) tipLeft = colPx - gap - tipW;
1506
1715
  tipLeft = Math.max(4, tipLeft);
@@ -1514,6 +1723,81 @@ window.__ModuleLoader__.load({
1514
1723
  background: "var(--dsw-alias-bg-layer-3)",
1515
1724
  color: "var(--dsw-alias-label-secondary)"
1516
1725
  };
1726
+ const segBtn = (active) => ({
1727
+ ...segStyle,
1728
+ background: active ? "var(--dsw-alias-label-primary)" : "transparent",
1729
+ color: active ? "var(--dsw-alias-bg-layer-3)" : "var(--dsw-alias-label-secondary)",
1730
+ transition: "background .25s ease, color .25s ease"
1731
+ });
1732
+ const labelsNode = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: tickIdx.map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1733
+ x: 0,
1734
+ y: 0,
1735
+ textAnchor: "middle",
1736
+ fontSize: 14,
1737
+ fill: "var(--dsw-alias-label-tertiary)",
1738
+ className: enterCls(isNewTick(dayKeys[i]), false),
1739
+ onAnimationEnd: onAnimEnd,
1740
+ style: {
1741
+ transform: `translate(${anchorX(i)}px, 270px)`,
1742
+ transition: T_SHIFT
1743
+ },
1744
+ children: fmtDay(dayKeys[i])
1745
+ }, dayKeys[i])) });
1746
+ const marksNode = chartType === "bar" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: dayKeys.map((k, i) => {
1747
+ const dayNew = isNewDay(k);
1748
+ let cum = 0;
1749
+ const segs = series.map((s) => {
1750
+ const v = isHidden(s.model) ? 0 : s.points[i];
1751
+ const yTop = yOf(cum + v);
1752
+ const h = v > 0 ? Math.max(.5, yOf(cum) - yTop) : 0;
1753
+ cum += v;
1754
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1755
+ x: cxOf(i) - barW / 2,
1756
+ y: yTop,
1757
+ width: barW,
1758
+ height: h,
1759
+ fill: colorOf(s.model),
1760
+ className: enterCls(!dayNew && isNewModel(s.model), true),
1761
+ onAnimationEnd: onAnimEnd,
1762
+ style: { transition: T_POS }
1763
+ }, s.model);
1764
+ });
1765
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
1766
+ opacity: hover?.idx === i ? 1 : .8,
1767
+ className: enterCls(dayNew, true),
1768
+ onAnimationEnd: onAnimEnd,
1769
+ style: { transition: T_FADE },
1770
+ children: segs
1771
+ }, k);
1772
+ }) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: series.map((s) => {
1773
+ const color = colorOf(s.model);
1774
+ const off = isHidden(s.model);
1775
+ const modelNew = isNewModel(s.model);
1776
+ const pts = s.points.map((v, i) => [xOf(i), yOf(v)]);
1777
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1778
+ opacity: off ? 0 : .8,
1779
+ className: enterCls(modelNew, false),
1780
+ onAnimationEnd: onAnimEnd,
1781
+ style: { transition: T_FADE },
1782
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1783
+ d: seriesPathFixed(pts),
1784
+ fill: "none",
1785
+ stroke: color,
1786
+ strokeWidth: 2,
1787
+ strokeLinecap: "round",
1788
+ strokeLinejoin: "round",
1789
+ style: { transition: T_D }
1790
+ }), pts.map(([x, y], i) => s.points[i] > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1791
+ cx: x,
1792
+ cy: y,
1793
+ r: hover?.idx === i ? 4 : 2,
1794
+ fill: color,
1795
+ className: enterCls(!modelNew && isNewDay(dayKeys[i]), false),
1796
+ onAnimationEnd: onAnimEnd,
1797
+ style: { transition: T_POINT }
1798
+ }, dayKeys[i]) : null)]
1799
+ }, s.model);
1800
+ }) });
1517
1801
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1518
1802
  style: {
1519
1803
  border: "1px solid var(--dsw-alias-border-l2)",
@@ -1522,6 +1806,7 @@ window.__ModuleLoader__.load({
1522
1806
  overflow: "hidden"
1523
1807
  },
1524
1808
  children: [
1809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ANIM_CSS }),
1525
1810
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1526
1811
  style: {
1527
1812
  padding: "12px 16px 8px",
@@ -1551,29 +1836,40 @@ window.__ModuleLoader__.load({
1551
1836
  },
1552
1837
  children: t("trend.hint")
1553
1838
  })]
1554
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1839
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1555
1840
  style: {
1556
- display: "inline-flex",
1557
- border: "1px solid var(--dsw-alias-border-l2)",
1558
- borderRadius: 6,
1559
- overflow: "hidden"
1841
+ display: "flex",
1842
+ gap: 8
1560
1843
  },
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);
1844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1845
+ style: {
1846
+ display: "inline-flex",
1847
+ border: "1px solid var(--dsw-alias-border-l2)",
1848
+ borderRadius: 6,
1849
+ overflow: "hidden"
1569
1850
  },
1851
+ children: ["line", "bar"].map((ty) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1852
+ onClick: () => switchTo({ type: ty }),
1853
+ style: segBtn(chartType === ty),
1854
+ children: ty === "line" ? t("trend.type.line") : t("trend.type.bar")
1855
+ }, ty))
1856
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1570
1857
  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)"
1858
+ display: "inline-flex",
1859
+ border: "1px solid var(--dsw-alias-border-l2)",
1860
+ borderRadius: 6,
1861
+ overflow: "hidden"
1574
1862
  },
1575
- children: n === 7 ? t("trend.range7") : n === 30 ? t("trend.range30") : t("trend.range90")
1576
- }, n))
1863
+ children: [
1864
+ 7,
1865
+ 30,
1866
+ 90
1867
+ ].map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1868
+ onClick: () => switchTo({ range: n }),
1869
+ style: segBtn(rangeDays === n),
1870
+ children: n === 7 ? t("trend.range7") : n === 30 ? t("trend.range30") : t("trend.range90")
1871
+ }, n))
1872
+ })]
1577
1873
  })]
1578
1874
  }),
1579
1875
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1585,58 +1881,48 @@ window.__ModuleLoader__.load({
1585
1881
  gap: 6
1586
1882
  },
1587
1883
  children: series.map((s) => {
1588
- const off = hidden.has(s.model);
1884
+ const off = isHidden(s.model);
1589
1885
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1590
1886
  onClick: () => toggle(s.model),
1591
- title: s.model,
1887
+ title: s.model === "__other__" ? dispName(s.model) : s.model,
1592
1888
  style: {
1593
1889
  display: "inline-flex",
1594
1890
  alignItems: "center",
1595
- gap: 4,
1596
- padding: "1px 6px",
1891
+ gap: 6,
1892
+ padding: "2px 8px",
1597
1893
  borderRadius: 999,
1598
1894
  border: "1px solid var(--dsw-alias-border-l2)",
1599
1895
  background: "transparent",
1600
1896
  cursor: "pointer",
1601
1897
  opacity: off ? .35 : 1,
1602
- fontSize: 11,
1898
+ transition: T_FADE,
1899
+ fontSize: 12,
1603
1900
  color: "var(--dsw-alias-label-primary)",
1604
1901
  minWidth: 0
1605
1902
  },
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
- ]
1903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1904
+ width: 8,
1905
+ height: 8,
1906
+ borderRadius: 4,
1907
+ background: colorOf(s.model),
1908
+ flexShrink: 0
1909
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1910
+ style: {
1911
+ flex: 1,
1912
+ minWidth: 0,
1913
+ overflow: "hidden",
1914
+ textOverflow: "ellipsis",
1915
+ whiteSpace: "nowrap",
1916
+ textAlign: "left",
1917
+ textDecoration: off ? "line-through" : "none"
1918
+ },
1919
+ children: dispName(s.model)
1920
+ })]
1635
1921
  }, s.model);
1636
1922
  })
1637
1923
  }), series.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1638
1924
  style: {
1639
- fontSize: 10,
1925
+ fontSize: 11,
1640
1926
  color: "var(--dsw-alias-label-tertiary)",
1641
1927
  marginTop: 4
1642
1928
  },
@@ -1674,61 +1960,44 @@ window.__ModuleLoader__.load({
1674
1960
  1
1675
1961
  ].map((f) => {
1676
1962
  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,
1963
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1964
+ d: `M ${PAD.left} ${y} L ${W - PAD.right} ${y}`,
1965
+ fill: "none",
1682
1966
  stroke: "var(--dsw-alias-border-l2)",
1683
1967
  strokeWidth: 1,
1684
- strokeDasharray: f === 0 ? void 0 : "3 3"
1968
+ strokeDasharray: f === 0 ? void 0 : "3 3",
1969
+ style: { transition: T_D }
1685
1970
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1686
- x: PAD.left - 8,
1687
- y: y + 5,
1971
+ x: 0,
1972
+ y: 0,
1688
1973
  textAnchor: "end",
1689
1974
  fontSize: 14,
1690
1975
  fill: "var(--dsw-alias-label-tertiary)",
1976
+ style: {
1977
+ transform: `translate(${PAD.left - 8}px, ${y + 5}px)`,
1978
+ transition: T_SHIFT
1979
+ },
1691
1980
  children: formatTokensShort(yMax * f)
1692
1981
  })] }, f);
1693
1982
  }),
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,
1983
+ ghost ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
1984
+ className: ghost.type === "bar" ? "dtp-ghost-bars" : "dtp-ghost-lines",
1985
+ children: ghost.marks
1986
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
1987
+ className: "dtp-ghost-lines",
1988
+ children: ghost.labels
1989
+ })] }) : null,
1990
+ labelsNode,
1991
+ marksNode,
1992
+ hover != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1993
+ d: `M ${anchorX(hover.idx)} ${PAD.top} L ${anchorX(hover.idx)} ${PAD.top + plotH}`,
1994
+ fill: "none",
1707
1995
  stroke: "var(--dsw-alias-label-tertiary)",
1708
1996
  strokeWidth: 1,
1709
1997
  strokeDasharray: "4 3",
1710
- opacity: .6
1998
+ opacity: .6,
1999
+ style: { transition: T_D }
1711
2000
  }) : 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
2001
  visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1733
2002
  x: W / 2,
1734
2003
  y: PAD.top + plotH / 2,
@@ -1807,7 +2076,7 @@ window.__ModuleLoader__.load({
1807
2076
  whiteSpace: "nowrap"
1808
2077
  },
1809
2078
  title: r.model,
1810
- children: r.model
2079
+ children: dispName(r.model)
1811
2080
  }),
1812
2081
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1813
2082
  style: {
@@ -1858,6 +2127,7 @@ window.__ModuleLoader__.load({
1858
2127
  "stats.count": "{count} 次",
1859
2128
  "model.top5": "Top 5 模型",
1860
2129
  "model.top5.hint": "按 Token 用量排序",
2130
+ "model.others": "其它",
1861
2131
  "model.winner": "当日最常用模型",
1862
2132
  "model.none": "暂无模型数据",
1863
2133
  "model.tokens": "{count} tokens",
@@ -1868,6 +2138,8 @@ window.__ModuleLoader__.load({
1868
2138
  "trend.range7": "近一周",
1869
2139
  "trend.range30": "近一月",
1870
2140
  "trend.range90": "近90天",
2141
+ "trend.type.line": "折线",
2142
+ "trend.type.bar": "柱状",
1871
2143
  "trend.empty": "所选范围内暂无数据",
1872
2144
  "trend.legend.hint": "点击图例可显示/隐藏",
1873
2145
  "common.tokens": "tokens"
@@ -1903,6 +2175,7 @@ window.__ModuleLoader__.load({
1903
2175
  "stats.count": "{count} turns",
1904
2176
  "model.top5": "Top 5 Models",
1905
2177
  "model.top5.hint": "By token usage",
2178
+ "model.others": "Others",
1906
2179
  "model.winner": "Daily winner (model)",
1907
2180
  "model.none": "No model data",
1908
2181
  "model.tokens": "{count} tokens",
@@ -1913,6 +2186,8 @@ window.__ModuleLoader__.load({
1913
2186
  "trend.range7": "7 days",
1914
2187
  "trend.range30": "30 days",
1915
2188
  "trend.range90": "90 days",
2189
+ "trend.type.line": "Lines",
2190
+ "trend.type.bar": "Bars",
1916
2191
  "trend.empty": "No data in this range",
1917
2192
  "trend.legend.hint": "Click legend to toggle",
1918
2193
  "common.tokens": "tokens"
@@ -2232,6 +2507,14 @@ window.__ModuleLoader__.load({
2232
2507
  name: x.provider ?? x.name,
2233
2508
  tokens: x.tokens
2234
2509
  }));
2510
+ const othersScope = selectedKey && aggregated?.byDay.has(selectedKey) ? [aggregated.byDay.get(selectedKey)] : days ?? [];
2511
+ const sumField = (field) => {
2512
+ let total = 0;
2513
+ for (const d of othersScope) for (const [, v] of d[field]) total += v;
2514
+ return total;
2515
+ };
2516
+ const modelOthersTokens = sumField("byModel") - viewTopModels.reduce((a, x) => a + x.tokens, 0);
2517
+ const providerOthersTokens = sumField("byProvider") - viewTopProviders.reduce((a, x) => a + x.tokens, 0);
2235
2518
  const injectedTop = injectedTop5;
2236
2519
  const hasData = totals && (totals.today > 0 || totals.thisWeek > 0 || totals.thisMonth > 0 || totals.all > 0);
2237
2520
  const tt = (key, params) => {
@@ -2324,7 +2607,7 @@ window.__ModuleLoader__.load({
2324
2607
  color: "var(--dsw-alias-label-tertiary)",
2325
2608
  verticalAlign: "middle"
2326
2609
  },
2327
- children: ["v", "0.4.1"]
2610
+ children: ["v", "0.4.2"]
2328
2611
  })]
2329
2612
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2330
2613
  style: {
@@ -2753,7 +3036,9 @@ window.__ModuleLoader__.load({
2753
3036
  topProviders: [],
2754
3037
  days: days ?? void 0,
2755
3038
  mode: topMode,
2756
- onModeChange: setTopMode
3039
+ onModeChange: setTopMode,
3040
+ othersModelTokens: modelOthersTokens,
3041
+ othersProviderTokens: providerOthersTokens
2757
3042
  }) : viewTopModelsNamed.length > 0 || viewTopProvidersNamed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelTop5, {
2758
3043
  t: lang === "en" ? (k, p) => ({
2759
3044
  "model.top5": "Top 5 Models",
@@ -2761,13 +3046,16 @@ window.__ModuleLoader__.load({
2761
3046
  "model.top5.hint": "By token usage",
2762
3047
  "model.none": "No data",
2763
3048
  "view.model": "Model",
2764
- "view.provider": "Provider"
3049
+ "view.provider": "Provider",
3050
+ "model.others": "Others"
2765
3051
  })[k] ?? t(k, p) : t,
2766
3052
  topModels: viewTopModelsNamed,
2767
3053
  topProviders: viewTopProvidersNamed,
2768
3054
  days: days ?? void 0,
2769
3055
  mode: topMode,
2770
- onModeChange: setTopMode
3056
+ onModeChange: setTopMode,
3057
+ othersModelTokens: modelOthersTokens,
3058
+ othersProviderTokens: providerOthersTokens
2771
3059
  }) : null,
2772
3060
  !hasData && !days ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2773
3061
  style: {