dsh-token-total 1.0.0 → 1.0.1
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.md +3 -3
- package/lib/client.js +273 -147
- package/lib/index.js +159 -43
- package/lib/types/client/session-usage-core.d.ts +10 -0
- package/lib/types/client/use-session-usage.d.ts +2 -18
- package/lib/types/collector.d.ts +8 -9
- package/lib/types/context-types.d.ts +23 -2
- package/lib/types/model-names.d.ts +6 -0
- package/lib/types/session-usage.d.ts +8 -9
- package/lib/types/wire.d.ts +23 -1
- package/package.json +1 -1
- package/src/client/StatsLineEnhanced.tsx +15 -33
- package/src/client/TokenTotalPanel.module.css +72 -5
- package/src/client/TokenTotalPanel.tsx +199 -53
- package/src/client/session-usage-core.ts +58 -0
- package/src/client/use-session-usage.ts +45 -98
- package/src/collector.ts +57 -51
- package/src/context-types.ts +17 -2
- package/src/model-names.ts +34 -0
- package/src/query.ts +61 -5
- package/src/routes.ts +14 -2
- package/src/session-usage.ts +50 -38
- package/src/wire.ts +17 -1
package/lib/client.js
CHANGED
|
@@ -29406,7 +29406,7 @@ window.__ModuleLoader__.load({
|
|
|
29406
29406
|
}
|
|
29407
29407
|
//#endregion
|
|
29408
29408
|
//#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/TokenTotalPanel.module.css?inline
|
|
29409
|
-
const css$1 = "/**\n * Usage statistics panel styles — a DSH-native CSS Module. Every visual\n * value rides the DSH semantic tokens (--dsw-alias-* colors, --dsw-font-*\n * typography, --ds-* motion); no static palette values, no theme selectors.\n * The heatmap's 5 levels come from the brand accent via color-mix; model\n * colours are the fixed --dsw-chart-1..5 series (rank order by token\n * volume) plus the gray --dsw-chart-other step — the reasonix token-total\n * palette, with a lifted dark variant defined below.\n */\n\n/* The panel as a standalone main panel. It gets the same 960px content column\n the Plugins page gives it — the declarations below mirror the Plugins page's\n own `.page_768838850` so both entries render identically. Without the cap the charts\n would stretch to whatever the centre column happens to be.\n\n One deliberate difference from that page: its 28px top inset is NOT part of\n this container. A main panel starts at the window's top-left corner, so an\n inset painted by this scrolling container is a strip above the content that\n no window-drag region covers; `.backRow_768838850` below owns the inset instead, which\n is the shape the shipped page plugins use for the same reason\n (`ui-plugin-manager`'s `.pageHead_768838850`/`.detailTop_768838850` and `ui-schedule`'s page\n head). The rendered position is unchanged. */\n.page_768838850 {\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n height: 100%;\n overflow: auto;\n padding: 0 clamp(24px, 4vw, 48px) 48px;\n color: var(--dsw-alias-label-primary);\n}\n\n.page_768838850 > * {\n width: 100%;\n max-width: 960px;\n}\n\n/* The standalone panel's back control. Only the main panel renders this seat —\n the Plugins page entry already sits inside the page's own chrome, which\n draws its own crumb back to the bundle list. The spacing matches the gap the\n panel keeps between its own sections.\n\n The row also carries the panel's former 28px top inset: it starts at the\n window's top edge, so the whole strip above the back control drags the\n window (Electron app-regions are geometry, not stacking, so a strip owned by\n the scrolling container would subtract itself from the drag row beneath\n it). */\n.backRow_768838850 {\n display: flex;\n box-sizing: border-box;\n padding-top: 28px;\n margin-bottom: 18px;\n}\n\n.back_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.panel_768838850 {\n display: flex;\n flex-direction: column;\n gap: 18px;\n min-width: 0;\n}\n\n/* ── toolbar ───────────────────────────────────────────────────────────── */\n\n.toolbar_768838850 {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\n/* Each picker (time range) lives in a framed group. */\n.group_768838850 {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 2px;\n padding: 3px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n/* A range-preset chip: ghost Button inside the group, active state tinted\n with the interactive-active fill. */\n.segItem_768838850 {\n border-radius: 6px;\n}\n\n.segItem_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.segActive_768838850,\n.segActive_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active);\n color: var(--dsw-alias-label-primary);\n}\n\n.customRange_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.dateInput_768838850 {\n width: 150px;\n flex: 0 0 auto;\n}\n\n.customSep_768838850 {\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Keep the remaining toolbar action at the trailing edge. */\n.refresh_768838850 {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n/* ── states ────────────────────────────────────────────────────────────── */\n\n.errorBanner_768838850 {\n padding: 10px 14px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n color: var(--dsw-alias-state-warn-label);\n font-size: 13px;\n}\n\n.loading_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n}\n\n.empty_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 8px;\n}\n\n/* ── numeric cards ─────────────────────────────────────────────────────── */\n\n/* The two long-valued cards bookend the row — total tokens leads it, the top\n model closes it — each on a wider track than the four short numerics\n between them. At the Plugins page's 960px content width that is six columns\n on one row. */\n.cards_768838850 {\n display: grid;\n grid-template-columns: 1.3fr repeat(4, 1fr) 1.3fr;\n gap: 10px;\n}\n\n/* Below the width where six cards hold their own, the grid folds to four\n tracks and the two long cards take two each: they keep their extra width\n instead of degrading to the numerics' size, and each row still fills. */\n@media (max-width: 720px) {\n .cards_768838850 {\n grid-template-columns: repeat(4, 1fr);\n }\n\n .cards_768838850 > :first-child,\n .cards_768838850 > :last-child {\n grid-column: span 2;\n }\n}\n\n@media (max-width: 430px) {\n .cards_768838850 {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n.card_768838850 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n}\n\n.cardHead_768838850 {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.cardIcon_768838850 {\n color: var(--dsw-alias-label-tertiary);\n flex: 0 0 auto;\n}\n\n.cardLabel_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.cardValue_768838850 {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n line-height: 1.15;\n}\n\n.cardValueSm_768838850 {\n font-size: 14px;\n}\n\n.cardValueWrap_768838850 {\n word-break: break-word;\n}\n\n/* The \"most used model\" card value: the model name over the provider, the\n same two-line anatomy as the per-model list rows (modelName/modelProvider\n classes), instead of the raw \"provider/model\" ref. */\n.cardModelLines_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n/* A muted second line under a card value (the absolute cached-token volume\n under the hit-rate percentage). */\n.cardSub_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 1.2;\n}\n\n/* ── sections / chart chrome ───────────────────────────────────────────── */\n\n.section_768838850 {\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n}\n\n.sectionHead_768838850 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.sectionTitle_768838850 {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.trendNote_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.chartWrap_768838850 {\n position: relative;\n min-width: 0;\n}\n\n/* ── heatmap ───────────────────────────────────────────────────────────── */\n\n.heatWrap_768838850 {\n position: relative;\n /* hidden, not auto: the SVG now fills the container exactly, and an\n auto scrollbar would flash when a fixed tooltip passes over the wrap's\n right edge. */\n overflow: hidden;\n min-width: 0;\n}\n\n.heatmap_768838850 {\n display: block;\n /* The 364 cells each keep their own two-layer drop-shadow chain (see\n `.heatCell_768838850`) — 728 blurs that the compositor would otherwise re-run on\n every scroll repaint of the tile, which is what made scrolling this panel\n drop frames while idle was clean (#4). Promoting the SVG onto its own\n compositing layer takes that rasterization off the scroll path and leaves\n the painted result untouched, so the cell depth stays as designed. Note\n that the promotion is what does the work: `contain: paint` on its own\n measured no better than no mitigation at all. */\n transform: translateZ(0);\n}\n\n.heatLegend_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.heatCell_768838850 {\n display: inline-block;\n border-radius: 3px;\n /* No width/height here: the SVG cells carry their size as geometry\n attributes (x/y/width/height), and a CSS width/height would override\n those on SVG2 rects — pinning every cell to a fixed px and widening the\n visual gap. The legend swatches carry the same size inline, so both\n always match the measured geom.size_768838850. */\n /* Depth at this size, WITHOUT a per-cell backdrop-filter: the cells are SVG\n geometry, where backdrop-filter is accepted by the CSSOM but changes\n nothing on paint (measured on 364 live cells: the screenshot came out\n byte-identical to no blur at all). What reads as a glass tile instead is\n the translucent fill below — it lets the panel's own glass bed through —\n carried by one hairline highlight over one soft under-shadow. */\n filter:\n drop-shadow(0 -0.5px 0 var(--dsw-heat-sheen))\n drop-shadow(0 0.5px 0.8px var(--dsw-heat-shade));\n}\n\n/* The five heat levels use DeepSeek's brand blue (#4D6BFE) as a fixed ramp,\n light and dark variants below — never the theme accent, which can read\n near-black against the base surface in dark mode. level0 (no data) is a\n neutral empty cell. The cells are SVG <rect>s, so the color rides `fill`\n (background is a no-op on SVG geometry) — but the legend swatches are HTML,\n so both declarations stay in step.\n Each level is mixed a little toward transparent so the bed shows through;\n the amount is deliberately modest, because the ramp is a DATA encoding and\n washing it out would cost readability. */\n.heatLevel0_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n}\n\n.heatLevel1_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n}\n\n.heatLevel2_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n}\n\n.heatLevel3_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n}\n\n.heatLevel4_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n}\n\n.heatLevel5_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n}\n\n/* ── daily trend ───────────────────────────────────────────────────────── */\n\n.chart_768838850 {\n display: block;\n}\n\n.grid_768838850 {\n stroke: var(--dsw-alias-border-l1);\n stroke-width: 1;\n}\n\n.axis_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 10px;\n}\n\n.axisRate_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n}\n\n.bar_768838850 {\n transition: opacity 0.12s ease;\n /* The hover scaleX must pivot on the bar's own centre: SVG geometry\n transforms around the SVG origin unless transform-box anchors it to the\n element's fill box. Without this the scaled bar slides sideways and\n looks like it vanishes. */\n transform-box: fill-box;\n transform-origin: center;\n}\n\n.barDim_768838850 {\n opacity: 0.35;\n}\n\n.barHit_768838850 {\n fill: transparent;\n pointer-events: all;\n}\n\n/* cache hit-rate trend curve: DeepSeek's brand blue (#4D6BFE, the same hue\n family as the heatmap ramp) — a fixed brand ramp instead of the theme\n accent, which can read near-black in some themes. */\n.trend_768838850 {\n stroke: var(--dsw-trend-line);\n stroke-width: 2;\n opacity: 0.9;\n pointer-events: none;\n}\n\n.trendDot_768838850 {\n fill: var(--dsw-trend-line);\n stroke: var(--dsw-alias-bg-layer-1);\n stroke-width: 2;\n pointer-events: none;\n}\n\n.legend_768838850 {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.legendItem_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n cursor: default;\n}\n\n.legendSwatch_768838850 {\n display: inline-block;\n width: 10px;\n height: 10px;\n border-radius: 2px;\n}\n\n.legendTrend_768838850 {\n background: var(--dsw-trend-line);\n width: 14px;\n height: 3px;\n border-radius: 2px;\n}\n\n/* ── per-model / per-provider chart + list ─────────────────────────────── */\n\n/* The ring is centred against the list beside it. The list's height is never\n tied to the chart, so expanding a row cannot stretch anything. */\n.models_768838850 {\n display: flex;\n gap: 16px;\n align-items: center;\n flex-wrap: wrap;\n min-width: 0;\n}\n\n/* The ring's own wrapper. Its diameter comes from the measured row width (see\n Donut.tsx_768838850), so it must never flex. */\n.donutWrap_768838850 {\n position: relative;\n flex: 0 0 auto;\n}\n\n.donut_768838850 {\n display: block;\n}\n\n.donutTrack_768838850 {\n stroke: var(--dsw-alias-bg-mask-1);\n}\n\n.donutSeg_768838850 {\n transition: stroke-width 0.12s ease, opacity 0.12s ease;\n}\n\n.donutDim_768838850 {\n opacity: 0.35;\n}\n\n.donutCenter_768838850 {\n fill: var(--dsw-alias-label-primary);\n font-size: 18px;\n font-weight: 600;\n}\n\n.donutLabel_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n.modelList_768838850 {\n list-style: none;\n margin: 0;\n padding: 0;\n flex: 1 1 260px;\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* Position column: the row's rank inside its section. The Other bucket holds\n no single rank, so it renders an empty (but equally wide) cell. */\n.modelRank_768838850 {\n flex: 0 0 auto;\n min-width: 14px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.modelRow_768838850 {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 13px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.modelRow_768838850:last-child {\n border-bottom: none;\n}\n\n/* \"Other\" aggregate row: the entire row is clickable to expand/collapse the\n detail list. The chevron button inside it stops propagation so\n double-firing is avoided. */\n.modelRowExpandable_768838850 {\n cursor: pointer;\n}\n\n/* \"Other\" row toggle (chevron) that expands the aggregated tail models. */\n.modelToggle_768838850 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-right: 4px;\n border: none;\n background: none;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n vertical-align: middle;\n}\n\n/* Expanded detail rows: indented, slightly recessed. */\n.modelRowSub_768838850 {\n padding-left: 28px;\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent);\n font-size: 13px;\n}\n\n/* Second-level indent: a model row inside a provider that is itself folded\n into the Other bucket. Wider than .modelRowSub_768838850 so the nesting stays\n readable. Declared after it — same specificity, so this one wins. */\n.modelRowDeep_768838850 {\n padding-left: 48px;\n}\n\n/* \"Other\" details expand with an accordion: the wrapper is a grid whose\n single track animates 0fr → 1fr. */\n.modelOtherWrap_768838850 {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 0.25s ease;\n}\n\n.modelOtherOpen_768838850 {\n grid-template-rows: 1fr;\n}\n\n.modelOtherList_768838850 {\n overflow: hidden;\n min-height: 0;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/* Two-line row anatomy: [swatch] [name / provider] ……… [tokens / share].\n The name block takes the free width; the values block hugs the right. */\n.modelId_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.modelValues_768838850 {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 1px;\n flex: 0 0 auto;\n}\n\n.modelName_768838850 {\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.modelProvider_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.modelTokens_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.modelPct_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* ── tooltips ──────────────────────────────────────────────────────────── */\n\n/* Tooltips render as fixed-position portals on document.body_768838850 (see ChartTip),\n so they escape every ancestor's clipping and never overflow the settings\n panel: ChartTip clamps each tip inside the panel bounds and flips it above\n the hovered element when there is no room below. The plate follows the\n active theme: the DSH overlay token is a light gray in light mode and a\n dark gray under body[data-ds-dark-theme], so the tip reads correctly in\n both instead of staying dark in light mode. */\n.tip_768838850 {\n position: fixed;\n /* The tip is portaled to document.body_768838850, so it competes with the settings\n modal itself (z-index 1000) for the top layer: 100 would sit under the\n modal and the tip would be invisible. 1100 matches the official\n toast/portaled-menu layer, which is exactly the \"over a modal\" tier. */\n z-index: 1100;\n pointer-events: none;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-overlay);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n box-shadow: 0 2px 12px var(--dsw-alias-bg-mask-2);\n white-space: nowrap;\n}\n\n.tipTitle_768838850 {\n font-weight: 600;\n margin-bottom: 4px;\n}\n\n.tipRow_768838850 {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n\n/* Hover tooltip breakdown rows for the \"Other\" aggregate: indented, muted,\n same gray swatch as the Other step. */\n.tipRowOther_768838850 {\n padding-left: 8px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.tipBreakdown_768838850 {\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 4px;\n padding-top: 4px;\n}\n\n.foot_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── chart palette ──────────────────────────────────────────────────────── */\n\n/* Light variant (the theme's default): model colours are the reasonix\n token-total palette (rank order by token volume + gray Other) with a\n white veil mixed in (color-mix 30% white) — the raw hues read dark and\n heavy on the light card surface, and the veil lifts lightness and dulls\n saturation the way a translucent white overlay would, keeping the hues\n colourful instead of muddy. The heatmap ramp is DeepSeek's brand blue\n (#4D6BFE) stepping from a pale tint to the full blue; the hit-rate curve\n rides the same brand hue.\n Ranks 6-10 extend the reasonix five to ten (this plugin's addition): each\n is picked to keep the ten hues apart on both themes. */\n.panel_768838850 {\n --dsw-heat-0: #ebedf0;\n --dsw-heat-1: #dbe3ff;\n --dsw-heat-2: #b7c5ff;\n --dsw-heat-3: #8ea4ff;\n --dsw-heat-4: #6884ff;\n --dsw-heat-5: #4d6bfe;\n /* Heatmap cell depth: a highlight above, a shade below. Light trays need\n the stronger pair; the dark tray below lifts the shade instead. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.55);\n --dsw-heat-shade: rgb(0 0 0 / 0.20);\n --dsw-trend-line: #4d6bfe;\n --dsw-chart-1: color-mix(in srgb, #0576ff 70%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 70%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 70%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 70%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 70%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 70%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 70%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 70%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 70%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 70%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 70%, white);\n /* Provider series: a separate five-hue set for the provider section, so a\n provider never wears a model's colour. */\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 70%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 70%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 70%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 70%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 70%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 70%, white);\n}\n\n/* Dark variant: the same reasonix hues under a thicker white veil\n (35% — dark surfaces need the extra lift), so segments read soft and\n bright instead of sinking into the background. */\n@media (prefers-color-scheme: dark) {\n .panel_768838850 {\n --dsw-heat-0: #21262d;\n --dsw-heat-1: #2f4bd0;\n --dsw-heat-2: #4d6bfe;\n --dsw-heat-3: #6e8bff;\n --dsw-heat-4: #93aaff;\n --dsw-heat-5: #c4d0ff;\n /* Same depth pair, recalibrated: a dark tray swallows a light shade. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.30);\n --dsw-heat-shade: rgb(0 0 0 / 0.50);\n --dsw-trend-line: #7d95ff;\n --dsw-chart-1: color-mix(in srgb, #0576ff 65%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 65%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 65%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 65%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 65%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 65%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 65%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 65%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 65%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 65%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 65%, white);\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 65%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 65%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 65%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 65%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 65%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 65%, white);\n }\n}\n";
|
|
29409
|
+
const css$1 = "/**\n * Usage statistics panel styles — a DSH-native CSS Module. Every visual\n * value rides the DSH semantic tokens (--dsw-alias-* colors, --dsw-font-*\n * typography, --ds-* motion); no static palette values, no theme selectors.\n * The heatmap's 5 levels come from the brand accent via color-mix; model\n * colours are the fixed --dsw-chart-1..5 series (rank order by token\n * volume) plus the gray --dsw-chart-other step — the reasonix token-total\n * palette, with a lifted dark variant defined below.\n */\n\n/* The panel as a standalone main panel. It gets the same 960px content column\n the Plugins page gives it — the declarations below mirror the Plugins page's\n own `.page_768838850` so both entries render identically. Without the cap the charts\n would stretch to whatever the centre column happens to be.\n\n One deliberate difference from that page: its 28px top inset is NOT part of\n this container. A main panel starts at the window's top-left corner, so an\n inset painted by this scrolling container is a strip above the content that\n no window-drag region covers; `.backRow_768838850` below owns the inset instead, which\n is the shape the shipped page plugins use for the same reason\n (`ui-plugin-manager`'s `.pageHead_768838850`/`.detailTop_768838850` and `ui-schedule`'s page\n head). The rendered position is unchanged. */\n.page_768838850 {\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n height: 100%;\n overflow: auto;\n padding: 0 clamp(24px, 4vw, 48px) 48px;\n color: var(--dsw-alias-label-primary);\n}\n\n.page_768838850 > * {\n width: 100%;\n max-width: 960px;\n}\n\n/* The standalone panel's back control. Only the main panel renders this seat —\n the Plugins page entry already sits inside the page's own chrome, which\n draws its own crumb back to the bundle list. The spacing matches the gap the\n panel keeps between its own sections.\n\n The row also carries the panel's former 28px top inset: it starts at the\n window's top edge, so the whole strip above the back control drags the\n window (Electron app-regions are geometry, not stacking, so a strip owned by\n the scrolling container would subtract itself from the drag row beneath\n it). */\n.backRow_768838850 {\n display: flex;\n box-sizing: border-box;\n padding-top: 28px;\n margin-bottom: 18px;\n}\n\n.back_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.panel_768838850 {\n display: flex;\n flex-direction: column;\n gap: 18px;\n min-width: 0;\n}\n\n/* ── toolbar ───────────────────────────────────────────────────────────── */\n\n.toolbar_768838850 {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\n/* Each picker (time range) lives in a framed group. */\n.group_768838850 {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 2px;\n padding: 3px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n/* A range-preset chip: ghost Button inside the group, active state tinted\n with the interactive-active fill. */\n.segItem_768838850 {\n border-radius: 6px;\n}\n\n.segItem_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.segActive_768838850,\n.segActive_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active);\n color: var(--dsw-alias-label-primary);\n}\n\n.customRange_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.dateInput_768838850 {\n width: 150px;\n flex: 0 0 auto;\n}\n\n.customSep_768838850 {\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Keep the remaining toolbar action at the trailing edge. */\n.refresh_768838850 {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n/* ── states ────────────────────────────────────────────────────────────── */\n\n.errorBanner_768838850 {\n padding: 10px 14px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n color: var(--dsw-alias-state-warn-label);\n font-size: 13px;\n}\n\n.loading_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n}\n\n.empty_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 8px;\n}\n\n/* ── numeric cards ─────────────────────────────────────────────────────── */\n\n/* The two long-valued cards bookend the row — total tokens leads it, the top\n model closes it — each on a wider track than the four short numerics\n between them. At the Plugins page's 960px content width that is six columns\n on one row. */\n.cards_768838850 {\n display: grid;\n grid-template-columns: 1.3fr repeat(4, 1fr) 1.3fr;\n gap: 10px;\n}\n\n/* Below the width where six cards hold their own, the grid folds to four\n tracks and the two long cards take two each: they keep their extra width\n instead of degrading to the numerics' size, and each row still fills. */\n@media (max-width: 720px) {\n .cards_768838850 {\n grid-template-columns: repeat(4, 1fr);\n }\n\n .cards_768838850 > :first-child,\n .cards_768838850 > :last-child {\n grid-column: span 2;\n }\n}\n\n@media (max-width: 430px) {\n .cards_768838850 {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n.card_768838850 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n}\n\n.cardHead_768838850 {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.cardIcon_768838850 {\n color: var(--dsw-alias-label-tertiary);\n flex: 0 0 auto;\n}\n\n.cardLabel_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.cardValue_768838850 {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n line-height: 1.15;\n}\n\n.cardValueSm_768838850 {\n font-size: 14px;\n}\n\n.cardValueWrap_768838850 {\n word-break: break-word;\n}\n\n/* The \"most used model\" card value: the model name over the provider, the\n same two-line anatomy as the per-model list rows (modelName/modelProvider\n classes), instead of the raw \"provider/model\" ref. */\n.cardModelLines_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n/* A muted second line under a card value (the absolute cached-token volume\n under the hit-rate percentage). */\n.cardSub_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 1.2;\n}\n\n/* ── sections / chart chrome ───────────────────────────────────────────── */\n\n.section_768838850 {\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n}\n\n.sectionHead_768838850 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.sectionTitle_768838850 {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.trendNote_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.chartWrap_768838850 {\n position: relative;\n min-width: 0;\n}\n\n/* ── heatmap ───────────────────────────────────────────────────────────── */\n\n.heatWrap_768838850 {\n position: relative;\n /* hidden, not auto: the SVG now fills the container exactly, and an\n auto scrollbar would flash when a fixed tooltip passes over the wrap's\n right edge. */\n overflow: hidden;\n min-width: 0;\n}\n\n.heatmap_768838850 {\n display: block;\n /* The 364 cells each keep their own two-layer drop-shadow chain (see\n `.heatCell_768838850`) — 728 blurs that the compositor would otherwise re-run on\n every scroll repaint of the tile, which is what made scrolling this panel\n drop frames while idle was clean (#4). Promoting the SVG onto its own\n compositing layer takes that rasterization off the scroll path and leaves\n the painted result untouched, so the cell depth stays as designed. Note\n that the promotion is what does the work: `contain: paint` on its own\n measured no better than no mitigation at all. */\n transform: translateZ(0);\n}\n\n.heatLegend_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.heatCell_768838850 {\n display: inline-block;\n border-radius: 3px;\n /* No width/height here: the SVG cells carry their size as geometry\n attributes (x/y/width/height), and a CSS width/height would override\n those on SVG2 rects — pinning every cell to a fixed px and widening the\n visual gap. The legend swatches carry the same size inline, so both\n always match the measured geom.size_768838850. */\n /* Depth at this size, WITHOUT a per-cell backdrop-filter: the cells are SVG\n geometry, where backdrop-filter is accepted by the CSSOM but changes\n nothing on paint (measured on 364 live cells: the screenshot came out\n byte-identical to no blur at all). What reads as a glass tile instead is\n the translucent fill below — it lets the panel's own glass bed through —\n carried by one hairline highlight over one soft under-shadow. */\n filter:\n drop-shadow(0 -0.5px 0 var(--dsw-heat-sheen))\n drop-shadow(0 0.5px 0.8px var(--dsw-heat-shade));\n}\n\n/* The five heat levels use DeepSeek's brand blue (#4D6BFE) as a fixed ramp,\n light and dark variants below — never the theme accent, which can read\n near-black against the base surface in dark mode. level0 (no data) is a\n neutral empty cell. The cells are SVG <rect>s, so the color rides `fill`\n (background is a no-op on SVG geometry) — but the legend swatches are HTML,\n so both declarations stay in step.\n Each level is mixed a little toward transparent so the bed shows through;\n the amount is deliberately modest, because the ramp is a DATA encoding and\n washing it out would cost readability. */\n.heatLevel0_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n}\n\n.heatLevel1_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n}\n\n.heatLevel2_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n}\n\n.heatLevel3_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n}\n\n.heatLevel4_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n}\n\n.heatLevel5_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n}\n\n/* ── daily trend ───────────────────────────────────────────────────────── */\n\n.chart_768838850 {\n display: block;\n}\n\n.grid_768838850 {\n stroke: var(--dsw-alias-border-l1);\n stroke-width: 1;\n}\n\n.axis_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 10px;\n}\n\n.axisRate_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n}\n\n.bar_768838850 {\n transition: opacity 0.12s ease;\n /* The hover scaleX must pivot on the bar's own centre: SVG geometry\n transforms around the SVG origin unless transform-box anchors it to the\n element's fill box. Without this the scaled bar slides sideways and\n looks like it vanishes. */\n transform-box: fill-box;\n transform-origin: center;\n}\n\n.barDim_768838850 {\n opacity: 0.35;\n}\n\n.barHit_768838850 {\n fill: transparent;\n pointer-events: all;\n}\n\n/* cache hit-rate trend curve: DeepSeek's brand blue (#4D6BFE, the same hue\n family as the heatmap ramp) — a fixed brand ramp instead of the theme\n accent, which can read near-black in some themes. */\n.trend_768838850 {\n stroke: var(--dsw-trend-line);\n stroke-width: 2;\n opacity: 0.9;\n pointer-events: none;\n}\n\n.trendDot_768838850 {\n fill: var(--dsw-trend-line);\n stroke: var(--dsw-alias-bg-layer-1);\n stroke-width: 2;\n pointer-events: none;\n}\n\n.legend_768838850 {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.legendItem_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n cursor: default;\n}\n\n.legendSwatch_768838850 {\n display: inline-block;\n width: 10px;\n height: 10px;\n border-radius: 2px;\n}\n\n.legendTrend_768838850 {\n background: var(--dsw-trend-line);\n width: 14px;\n height: 3px;\n border-radius: 2px;\n}\n\n/* ── per-model / per-provider chart + list ─────────────────────────────── */\n\n/* The ring is centred against the list beside it. The list's height is never\n tied to the chart, so expanding a row cannot stretch anything. */\n.models_768838850 {\n display: flex;\n gap: 16px;\n align-items: center;\n flex-wrap: wrap;\n min-width: 0;\n}\n\n/* The ring's own wrapper. Its diameter comes from the measured row width (see\n Donut.tsx_768838850), so it must never flex. */\n.donutWrap_768838850 {\n position: relative;\n flex: 0 0 auto;\n}\n\n.donut_768838850 {\n display: block;\n}\n\n.donutTrack_768838850 {\n stroke: var(--dsw-alias-bg-mask-1);\n}\n\n.donutSeg_768838850 {\n transition: stroke-width 0.12s ease, opacity 0.12s ease;\n}\n\n.donutDim_768838850 {\n opacity: 0.35;\n}\n\n.donutCenter_768838850 {\n fill: var(--dsw-alias-label-primary);\n font-size: 18px;\n font-weight: 600;\n}\n\n.donutLabel_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n/* Two ranking cards, side by side: MODELS left, PROVIDERS right. Both are\n always on screen (no lens/tab switch), so they share one row grid and\n stretch to a common height; a narrow pane stacks them instead. */\n.rankCards_768838850 {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n}\n\n@media (max-width: 720px) {\n .rankCards_768838850 {\n grid-template-columns: minmax(0, 1fr);\n }\n}\n\n/* Each card is the bordered surface its rows live in; the title is a recessed\n header strip, so the two rankings read as two equal panels. */\n.rankCard_768838850 {\n display: flex;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n.rankCardTitle_768838850 {\n padding: 8px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.rankList_768838850 {\n list-style: none;\n margin: 0;\n padding: 0;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Every row opens a hover tip (the cross-dimension detail), so it carries the\n same hover affordance as the chart marks. */\n.modelRow_768838850:hover {\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 60%, transparent);\n}\n\n/* Position column: the row's rank inside its section. The Other bucket holds\n no single rank, so it renders an empty (but equally wide) cell. */\n.modelRank_768838850 {\n flex: 0 0 auto;\n min-width: 14px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.modelRow_768838850 {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 13px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.modelRow_768838850:last-child {\n border-bottom: none;\n}\n\n/* \"Other\" aggregate row: the entire row is clickable to expand/collapse the\n detail list. The chevron button inside it stops propagation so\n double-firing is avoided. */\n.modelRowExpandable_768838850 {\n cursor: pointer;\n}\n\n/* \"Other\" row toggle (chevron) that expands the aggregated tail models. */\n.modelToggle_768838850 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-right: 4px;\n border: none;\n background: none;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n vertical-align: middle;\n}\n\n/* Expanded detail rows: indented, slightly recessed. */\n.modelRowSub_768838850 {\n padding-left: 28px;\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent);\n font-size: 13px;\n}\n\n/* Second-level indent: a model row inside a provider that is itself folded\n into the Other bucket. Wider than .modelRowSub_768838850 so the nesting stays\n readable. Declared after it — same specificity, so this one wins. */\n.modelRowDeep_768838850 {\n padding-left: 48px;\n}\n\n/* \"Other\" details expand with an accordion: the wrapper is a grid whose\n single track animates 0fr → 1fr. */\n.modelOtherWrap_768838850 {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 0.25s ease;\n}\n\n.modelOtherOpen_768838850 {\n grid-template-rows: 1fr;\n}\n\n.modelOtherList_768838850 {\n overflow: hidden;\n min-height: 0;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/* Two-line row anatomy: [swatch] [name / provider] ……… [tokens / share].\n The name block takes the free width; the values block hugs the right. */\n.modelId_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.modelValues_768838850 {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 1px;\n flex: 0 0 auto;\n}\n\n.modelName_768838850 {\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.modelProvider_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.modelTokens_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.modelPct_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* ── tooltips ──────────────────────────────────────────────────────────── */\n\n/* Tooltips render as fixed-position portals on document.body_768838850 (see ChartTip),\n so they escape every ancestor's clipping and never overflow the settings\n panel: ChartTip clamps each tip inside the panel bounds and flips it above\n the hovered element when there is no room below. The plate follows the\n active theme: the DSH overlay token is a light gray in light mode and a\n dark gray under body[data-ds-dark-theme], so the tip reads correctly in\n both instead of staying dark in light mode. */\n.tip_768838850 {\n position: fixed;\n /* The tip is portaled to document.body_768838850, so it competes with the settings\n modal itself (z-index 1000) for the top layer: 100 would sit under the\n modal and the tip would be invisible. 1100 matches the official\n toast/portaled-menu layer, which is exactly the \"over a modal\" tier. */\n z-index: 1100;\n pointer-events: none;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-overlay);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n box-shadow: 0 2px 12px var(--dsw-alias-bg-mask-2);\n white-space: nowrap;\n}\n\n.tipTitle_768838850 {\n font-weight: 600;\n margin-bottom: 4px;\n}\n\n/* A provider row's raw route id, kept out of the row label but reachable on\n hover (the row shows the display name). */\n.tipSubtitle_768838850 {\n margin: -2px 0 4px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n/* Breakdown heading inside a row tip: names the cross-dimension list under it\n (\"By provider\" under a model, \"By model\" under a provider). */\n.tipLabel_768838850 {\n margin-bottom: 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n/* The breakdown line pushes its volume to the right edge of the plate. */\n.tipName_768838850 {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.tipValue_768838850 {\n margin-left: auto;\n padding-left: 12px;\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.tipRow_768838850 {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n\n/* Hover tooltip breakdown rows for the \"Other\" aggregate: indented, muted,\n same gray swatch as the Other step. */\n.tipRowOther_768838850 {\n padding-left: 8px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.tipBreakdown_768838850 {\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 4px;\n padding-top: 4px;\n}\n\n.foot_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── chart palette ──────────────────────────────────────────────────────── */\n\n/* Light variant (the theme's default): model colours are the reasonix\n token-total palette (rank order by token volume + gray Other) with a\n white veil mixed in (color-mix 30% white) — the raw hues read dark and\n heavy on the light card surface, and the veil lifts lightness and dulls\n saturation the way a translucent white overlay would, keeping the hues\n colourful instead of muddy. The heatmap ramp is DeepSeek's brand blue\n (#4D6BFE) stepping from a pale tint to the full blue; the hit-rate curve\n rides the same brand hue.\n Ranks 6-10 extend the reasonix five to ten (this plugin's addition): each\n is picked to keep the ten hues apart on both themes. */\n.panel_768838850 {\n --dsw-heat-0: #ebedf0;\n --dsw-heat-1: #dbe3ff;\n --dsw-heat-2: #b7c5ff;\n --dsw-heat-3: #8ea4ff;\n --dsw-heat-4: #6884ff;\n --dsw-heat-5: #4d6bfe;\n /* Heatmap cell depth: a highlight above, a shade below. Light trays need\n the stronger pair; the dark tray below lifts the shade instead. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.55);\n --dsw-heat-shade: rgb(0 0 0 / 0.20);\n --dsw-trend-line: #4d6bfe;\n --dsw-chart-1: color-mix(in srgb, #0576ff 70%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 70%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 70%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 70%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 70%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 70%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 70%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 70%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 70%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 70%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 70%, white);\n /* Provider series: a separate five-hue set for the provider section, so a\n provider never wears a model's colour. */\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 70%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 70%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 70%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 70%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 70%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 70%, white);\n}\n\n/* Dark variant: the same reasonix hues under a thicker white veil\n (35% — dark surfaces need the extra lift), so segments read soft and\n bright instead of sinking into the background. */\n@media (prefers-color-scheme: dark) {\n .panel_768838850 {\n --dsw-heat-0: #21262d;\n --dsw-heat-1: #2f4bd0;\n --dsw-heat-2: #4d6bfe;\n --dsw-heat-3: #6e8bff;\n --dsw-heat-4: #93aaff;\n --dsw-heat-5: #c4d0ff;\n /* Same depth pair, recalibrated: a dark tray swallows a light shade. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.30);\n --dsw-heat-shade: rgb(0 0 0 / 0.50);\n --dsw-trend-line: #7d95ff;\n --dsw-chart-1: color-mix(in srgb, #0576ff 65%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 65%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 65%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 65%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 65%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 65%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 65%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 65%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 65%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 65%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 65%, white);\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 65%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 65%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 65%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 65%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 65%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 65%, white);\n }\n}\n";
|
|
29410
29410
|
const tagId$1 = "dsh-token-total/TokenTotalPanel.module.css";
|
|
29411
29411
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$1 + "\"]") === null) {
|
|
29412
29412
|
const tag = document.createElement("style");
|
|
@@ -29481,9 +29481,12 @@ window.__ModuleLoader__.load({
|
|
|
29481
29481
|
"donutDim": "donutDim_768838850",
|
|
29482
29482
|
"donutCenter": "donutCenter_768838850",
|
|
29483
29483
|
"donutLabel": "donutLabel_768838850",
|
|
29484
|
-
"
|
|
29485
|
-
"
|
|
29484
|
+
"rankCards": "rankCards_768838850",
|
|
29485
|
+
"rankCard": "rankCard_768838850",
|
|
29486
|
+
"rankCardTitle": "rankCardTitle_768838850",
|
|
29487
|
+
"rankList": "rankList_768838850",
|
|
29486
29488
|
"modelRow": "modelRow_768838850",
|
|
29489
|
+
"modelRank": "modelRank_768838850",
|
|
29487
29490
|
"modelRowExpandable": "modelRowExpandable_768838850",
|
|
29488
29491
|
"modelToggle": "modelToggle_768838850",
|
|
29489
29492
|
"modelRowSub": "modelRowSub_768838850",
|
|
@@ -29500,6 +29503,10 @@ window.__ModuleLoader__.load({
|
|
|
29500
29503
|
"body": "body_768838850",
|
|
29501
29504
|
"tip": "tip_768838850",
|
|
29502
29505
|
"tipTitle": "tipTitle_768838850",
|
|
29506
|
+
"tipSubtitle": "tipSubtitle_768838850",
|
|
29507
|
+
"tipLabel": "tipLabel_768838850",
|
|
29508
|
+
"tipName": "tipName_768838850",
|
|
29509
|
+
"tipValue": "tipValue_768838850",
|
|
29503
29510
|
"tipRow": "tipRow_768838850",
|
|
29504
29511
|
"tipRowOther": "tipRowOther_768838850",
|
|
29505
29512
|
"tipBreakdown": "tipBreakdown_768838850",
|
|
@@ -29736,8 +29743,8 @@ window.__ModuleLoader__.load({
|
|
|
29736
29743
|
const TOP_MODELS = 10;
|
|
29737
29744
|
const OTHER_MODEL = "\0other";
|
|
29738
29745
|
const OTHER_COLOR = "var(--dsw-chart-other)";
|
|
29739
|
-
/** How many rows each
|
|
29740
|
-
const TOP_BREAKDOWN =
|
|
29746
|
+
/** How many rows each ranking card lists (扩展到 8 个排行). */
|
|
29747
|
+
const TOP_BREAKDOWN = 8;
|
|
29741
29748
|
function TokenTotalPanel({ t }) {
|
|
29742
29749
|
const [range, setRange] = (0, react.useState)("today");
|
|
29743
29750
|
const [customFrom, setCustomFrom] = (0, react.useState)("");
|
|
@@ -29936,7 +29943,9 @@ window.__ModuleLoader__.load({
|
|
|
29936
29943
|
models: stats.models,
|
|
29937
29944
|
providers: stats.providers,
|
|
29938
29945
|
total: stats.tokens,
|
|
29939
|
-
|
|
29946
|
+
providerNames: stats.providerNames,
|
|
29947
|
+
t,
|
|
29948
|
+
panelRef
|
|
29940
29949
|
}),
|
|
29941
29950
|
stats.to && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
29942
29951
|
className: TokenTotalPanel_module_default.foot,
|
|
@@ -30486,8 +30495,22 @@ window.__ModuleLoader__.load({
|
|
|
30486
30495
|
})]
|
|
30487
30496
|
});
|
|
30488
30497
|
}
|
|
30489
|
-
|
|
30490
|
-
|
|
30498
|
+
/** Provider hues run 1..5 then the gray tail (a provider beyond the palette is
|
|
30499
|
+
* rare: the card lists 8 rows, the palette names 5). */
|
|
30500
|
+
function providerColor(index) {
|
|
30501
|
+
return index < 5 ? `var(--dsw-provider-${index + 1})` : "var(--dsw-provider-other)";
|
|
30502
|
+
}
|
|
30503
|
+
/** Model hues run 1..10 then the gray Other step (mirrors colorForModel). */
|
|
30504
|
+
function modelColor(index) {
|
|
30505
|
+
return index < TOP_MODELS ? `var(--dsw-chart-${index + 1})` : OTHER_COLOR;
|
|
30506
|
+
}
|
|
30507
|
+
function UsageBreakdown({ models, providers, total, providerNames, t, panelRef }) {
|
|
30508
|
+
const [tip, setTip] = (0, react.useState)(null);
|
|
30509
|
+
const providerLabel = (0, react.useCallback)((provider) => {
|
|
30510
|
+
const named = providerNames?.[provider];
|
|
30511
|
+
return typeof named === "string" && named.trim() !== "" ? named : provider;
|
|
30512
|
+
}, [providerNames]);
|
|
30513
|
+
const shareText = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
|
|
30491
30514
|
const modelRows = (0, react.useMemo)(() => {
|
|
30492
30515
|
const byName = /* @__PURE__ */ new Map();
|
|
30493
30516
|
for (const m of models) {
|
|
@@ -30496,73 +30519,184 @@ window.__ModuleLoader__.load({
|
|
|
30496
30519
|
tokens: 0,
|
|
30497
30520
|
cacheHit: 0,
|
|
30498
30521
|
cacheMiss: 0,
|
|
30499
|
-
|
|
30522
|
+
split: /* @__PURE__ */ new Map(),
|
|
30523
|
+
sources: /* @__PURE__ */ new Set()
|
|
30500
30524
|
};
|
|
30501
30525
|
acc.tokens += m.tokens;
|
|
30502
30526
|
acc.cacheHit += m.cacheHit;
|
|
30503
30527
|
acc.cacheMiss += m.cacheMiss;
|
|
30504
|
-
for (const
|
|
30528
|
+
for (const item of m.providerTokens ?? []) acc.split.set(item.provider, (acc.split.get(item.provider) ?? 0) + item.tokens);
|
|
30529
|
+
for (const provider of m.providers ?? (m.provider === "" ? [] : [m.provider])) acc.sources.add(provider);
|
|
30505
30530
|
byName.set(name, acc);
|
|
30506
30531
|
}
|
|
30507
|
-
return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc], i) =>
|
|
30508
|
-
|
|
30509
|
-
|
|
30510
|
-
|
|
30511
|
-
|
|
30512
|
-
|
|
30513
|
-
|
|
30514
|
-
|
|
30515
|
-
|
|
30516
|
-
|
|
30532
|
+
return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc], i) => {
|
|
30533
|
+
const split = [...acc.split.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([provider, tokens], j) => ({
|
|
30534
|
+
key: provider,
|
|
30535
|
+
name: providerLabel(provider),
|
|
30536
|
+
tokens,
|
|
30537
|
+
color: providerColor(j)
|
|
30538
|
+
}));
|
|
30539
|
+
const known = new Set(split.map((child) => child.key));
|
|
30540
|
+
const named = [...acc.sources].filter((provider) => !known.has(provider)).sort().map((provider, j) => ({
|
|
30541
|
+
key: provider,
|
|
30542
|
+
name: providerLabel(provider),
|
|
30543
|
+
tokens: null,
|
|
30544
|
+
color: providerColor(split.length + j)
|
|
30545
|
+
}));
|
|
30546
|
+
return {
|
|
30547
|
+
key: name,
|
|
30548
|
+
name,
|
|
30549
|
+
tokens: acc.tokens,
|
|
30550
|
+
cacheHit: acc.cacheHit,
|
|
30551
|
+
cacheMiss: acc.cacheMiss,
|
|
30552
|
+
color: modelColor(i),
|
|
30553
|
+
breakdownLabel: t("byProvider"),
|
|
30554
|
+
breakdown: [...split, ...named]
|
|
30555
|
+
};
|
|
30556
|
+
});
|
|
30557
|
+
}, [
|
|
30558
|
+
models,
|
|
30559
|
+
providerLabel,
|
|
30560
|
+
t
|
|
30561
|
+
]);
|
|
30517
30562
|
const providerRows = (0, react.useMemo)(() => {
|
|
30518
|
-
return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p, i) =>
|
|
30519
|
-
|
|
30520
|
-
|
|
30521
|
-
|
|
30522
|
-
|
|
30523
|
-
|
|
30524
|
-
|
|
30525
|
-
|
|
30526
|
-
|
|
30527
|
-
|
|
30528
|
-
|
|
30563
|
+
return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p, i) => {
|
|
30564
|
+
const label = providerLabel(p.provider);
|
|
30565
|
+
return {
|
|
30566
|
+
key: p.provider,
|
|
30567
|
+
name: label,
|
|
30568
|
+
subtitle: label === p.provider ? void 0 : p.provider,
|
|
30569
|
+
tokens: p.tokens,
|
|
30570
|
+
cacheHit: p.cacheHit,
|
|
30571
|
+
cacheMiss: p.cacheMiss,
|
|
30572
|
+
color: providerColor(i),
|
|
30573
|
+
breakdownLabel: t("byModel"),
|
|
30574
|
+
breakdown: [...p.modelTokens ?? []].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)).map((m, j) => ({
|
|
30575
|
+
key: m.model,
|
|
30576
|
+
name: m.model,
|
|
30577
|
+
tokens: m.tokens,
|
|
30578
|
+
color: modelColor(j)
|
|
30579
|
+
}))
|
|
30580
|
+
};
|
|
30581
|
+
});
|
|
30582
|
+
}, [
|
|
30583
|
+
providers,
|
|
30584
|
+
providerLabel,
|
|
30585
|
+
t
|
|
30586
|
+
]);
|
|
30529
30587
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
30530
30588
|
className: TokenTotalPanel_module_default.section,
|
|
30531
|
-
children: [
|
|
30532
|
-
|
|
30533
|
-
|
|
30534
|
-
|
|
30535
|
-
|
|
30536
|
-
|
|
30537
|
-
|
|
30538
|
-
|
|
30539
|
-
|
|
30540
|
-
|
|
30541
|
-
|
|
30542
|
-
|
|
30543
|
-
|
|
30544
|
-
|
|
30545
|
-
|
|
30546
|
-
|
|
30547
|
-
|
|
30548
|
-
|
|
30549
|
-
|
|
30550
|
-
|
|
30551
|
-
|
|
30552
|
-
|
|
30553
|
-
className: clsx(TokenTotalPanel_module_default.segItem, tab === "provider" && TokenTotalPanel_module_default.segActive),
|
|
30554
|
-
onClick: () => setTab("provider"),
|
|
30555
|
-
children: t("byProvider")
|
|
30589
|
+
children: [
|
|
30590
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30591
|
+
className: TokenTotalPanel_module_default.sectionHead,
|
|
30592
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
30593
|
+
className: TokenTotalPanel_module_default.sectionTitle,
|
|
30594
|
+
children: t("usageRanking")
|
|
30595
|
+
})
|
|
30596
|
+
}),
|
|
30597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30598
|
+
className: TokenTotalPanel_module_default.rankCards,
|
|
30599
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankCard, {
|
|
30600
|
+
title: t("byModel"),
|
|
30601
|
+
rows: modelRows,
|
|
30602
|
+
total,
|
|
30603
|
+
t,
|
|
30604
|
+
onTip: setTip
|
|
30605
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankCard, {
|
|
30606
|
+
title: t("byProvider"),
|
|
30607
|
+
rows: providerRows,
|
|
30608
|
+
total,
|
|
30609
|
+
t,
|
|
30610
|
+
onTip: setTip
|
|
30556
30611
|
})]
|
|
30557
|
-
})
|
|
30612
|
+
}),
|
|
30613
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTip, {
|
|
30614
|
+
anchor: tip?.anchor ?? null,
|
|
30615
|
+
panelRef,
|
|
30616
|
+
children: tip && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
30617
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30618
|
+
className: TokenTotalPanel_module_default.tipTitle,
|
|
30619
|
+
children: tip.title
|
|
30620
|
+
}),
|
|
30621
|
+
tip.subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30622
|
+
className: TokenTotalPanel_module_default.tipSubtitle,
|
|
30623
|
+
children: tip.subtitle
|
|
30624
|
+
}),
|
|
30625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30626
|
+
className: TokenTotalPanel_module_default.tipRow,
|
|
30627
|
+
children: [
|
|
30628
|
+
t("total"),
|
|
30629
|
+
" ",
|
|
30630
|
+
formatTokens(tip.tokens),
|
|
30631
|
+
" · ",
|
|
30632
|
+
shareText(tip.tokens)
|
|
30633
|
+
]
|
|
30634
|
+
}),
|
|
30635
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30636
|
+
className: TokenTotalPanel_module_default.tipRow,
|
|
30637
|
+
children: [
|
|
30638
|
+
t("cacheRateColumn"),
|
|
30639
|
+
" ",
|
|
30640
|
+
cacheRateText(tip.cacheHit, tip.cacheMiss)
|
|
30641
|
+
]
|
|
30642
|
+
}),
|
|
30643
|
+
tip.breakdown.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30644
|
+
className: TokenTotalPanel_module_default.tipBreakdown,
|
|
30645
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30646
|
+
className: TokenTotalPanel_module_default.tipLabel,
|
|
30647
|
+
children: tip.breakdownLabel
|
|
30648
|
+
}), tip.breakdown.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30649
|
+
className: TokenTotalPanel_module_default.tipRow,
|
|
30650
|
+
children: [
|
|
30651
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30652
|
+
className: TokenTotalPanel_module_default.legendSwatch,
|
|
30653
|
+
style: { background: child.color }
|
|
30654
|
+
}),
|
|
30655
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30656
|
+
className: TokenTotalPanel_module_default.tipName,
|
|
30657
|
+
children: child.name
|
|
30658
|
+
}),
|
|
30659
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30660
|
+
className: TokenTotalPanel_module_default.tipValue,
|
|
30661
|
+
children: child.tokens === null ? "" : `${formatCompact(child.tokens)} · ${shareText(child.tokens)}`
|
|
30662
|
+
})
|
|
30663
|
+
]
|
|
30664
|
+
}, child.key))]
|
|
30665
|
+
})
|
|
30666
|
+
] })
|
|
30667
|
+
})
|
|
30668
|
+
]
|
|
30669
|
+
});
|
|
30670
|
+
}
|
|
30671
|
+
/** One ranking card: a titled list whose rows explain themselves on hover.
|
|
30672
|
+
* The tip is owned by UsageBreakdown (one open tip at a time), so a row only
|
|
30673
|
+
* reports which row it is, never where to draw. */
|
|
30674
|
+
function RankCard({ title, rows, total, t, onTip }) {
|
|
30675
|
+
const share = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
|
|
30676
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30677
|
+
className: TokenTotalPanel_module_default.rankCard,
|
|
30678
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30679
|
+
className: TokenTotalPanel_module_default.rankCardTitle,
|
|
30680
|
+
children: title
|
|
30558
30681
|
}), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30559
30682
|
className: TokenTotalPanel_module_default.empty,
|
|
30560
30683
|
children: t("empty")
|
|
30561
30684
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
30562
|
-
className: TokenTotalPanel_module_default.
|
|
30685
|
+
className: TokenTotalPanel_module_default.rankList,
|
|
30563
30686
|
"data-dsh-usage-glass": "list",
|
|
30564
30687
|
children: rows.map((r, rank) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
30565
30688
|
className: TokenTotalPanel_module_default.modelRow,
|
|
30689
|
+
onMouseEnter: (e) => onTip({
|
|
30690
|
+
anchor: e.currentTarget,
|
|
30691
|
+
title: r.name,
|
|
30692
|
+
subtitle: r.subtitle,
|
|
30693
|
+
tokens: r.tokens,
|
|
30694
|
+
cacheHit: r.cacheHit,
|
|
30695
|
+
cacheMiss: r.cacheMiss,
|
|
30696
|
+
breakdownLabel: r.breakdownLabel,
|
|
30697
|
+
breakdown: r.breakdown
|
|
30698
|
+
}),
|
|
30699
|
+
onMouseLeave: () => onTip(null),
|
|
30566
30700
|
children: [
|
|
30567
30701
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30568
30702
|
className: TokenTotalPanel_module_default.modelRank,
|
|
@@ -30580,7 +30714,7 @@ window.__ModuleLoader__.load({
|
|
|
30580
30714
|
children: r.name
|
|
30581
30715
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30582
30716
|
className: TokenTotalPanel_module_default.modelProvider,
|
|
30583
|
-
children:
|
|
30717
|
+
children: share(r.tokens)
|
|
30584
30718
|
})]
|
|
30585
30719
|
}),
|
|
30586
30720
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -31064,94 +31198,92 @@ window.__ModuleLoader__.load({
|
|
|
31064
31198
|
return Math.min(MAX_MEASURED_SCALE, Math.max(MIN_MEASURED_SCALE, actual / prior));
|
|
31065
31199
|
}
|
|
31066
31200
|
//#endregion
|
|
31067
|
-
//#region src/client/
|
|
31068
|
-
|
|
31069
|
-
|
|
31070
|
-
|
|
31071
|
-
|
|
31072
|
-
|
|
31073
|
-
|
|
31074
|
-
|
|
31075
|
-
*
|
|
31076
|
-
|
|
31077
|
-
*
|
|
31078
|
-
* Refresh policy: the request is cheap on the host (its tree cache is
|
|
31079
|
-
* invalidated by live events), and the native usage figure moves exactly when
|
|
31080
|
-
* a call settles — so a change in the native signature is the trigger to
|
|
31081
|
-
* re-ask. There is no timer: an idle session costs nothing.
|
|
31082
|
-
*
|
|
31083
|
-
* Failure policy: the pill must never break the composer, so a failed request
|
|
31084
|
-
* keeps the last good answer and simply does not retry until the next trigger.
|
|
31085
|
-
*/
|
|
31086
|
-
function add(into, from) {
|
|
31201
|
+
//#region src/client/session-usage-core.ts
|
|
31202
|
+
const ZERO = {
|
|
31203
|
+
uncachedInputTokens: 0,
|
|
31204
|
+
outputTokens: 0,
|
|
31205
|
+
cacheReadTokens: 0,
|
|
31206
|
+
cacheWriteTokens: 0
|
|
31207
|
+
};
|
|
31208
|
+
/** Use the live main-agent projection when available. Never add tree.total to
|
|
31209
|
+
* it: that would count the main agent twice. The log is only the fallback. */
|
|
31210
|
+
function combinedTotals(tree, self = tree.self) {
|
|
31087
31211
|
return {
|
|
31088
|
-
uncachedInputTokens:
|
|
31089
|
-
outputTokens:
|
|
31090
|
-
cacheReadTokens:
|
|
31091
|
-
cacheWriteTokens:
|
|
31212
|
+
uncachedInputTokens: self.uncachedInputTokens + tree.subagents.uncachedInputTokens,
|
|
31213
|
+
outputTokens: self.outputTokens + tree.subagents.outputTokens,
|
|
31214
|
+
cacheReadTokens: self.cacheReadTokens + tree.subagents.cacheReadTokens,
|
|
31215
|
+
cacheWriteTokens: self.cacheWriteTokens + tree.subagents.cacheWriteTokens
|
|
31092
31216
|
};
|
|
31093
31217
|
}
|
|
31094
|
-
/**
|
|
31095
|
-
function
|
|
31096
|
-
|
|
31218
|
+
/** One consistent split for the pill, cache hit rate and every dialog row. */
|
|
31219
|
+
function sessionUsageForDisplay(native, tree) {
|
|
31220
|
+
const extra = tree?.selfCompaction ?? ZERO;
|
|
31221
|
+
const self = native === void 0 ? tree?.self ?? { ...ZERO } : {
|
|
31222
|
+
uncachedInputTokens: native.uncachedInputTokens + extra.uncachedInputTokens,
|
|
31223
|
+
outputTokens: native.outputTokens + extra.outputTokens,
|
|
31224
|
+
cacheReadTokens: native.cacheReadTokens + extra.cacheReadTokens,
|
|
31225
|
+
cacheWriteTokens: native.cacheWriteTokens + extra.cacheWriteTokens
|
|
31226
|
+
};
|
|
31227
|
+
return {
|
|
31228
|
+
self,
|
|
31229
|
+
subagents: tree?.subagents ?? { ...ZERO },
|
|
31230
|
+
total: tree === null ? self : combinedTotals(tree, self),
|
|
31231
|
+
subagentSessions: tree?.subagentSessions ?? 0
|
|
31232
|
+
};
|
|
31097
31233
|
}
|
|
31098
|
-
/** Whether a totals record carries any billable token at all. */
|
|
31099
31234
|
function hasAnyTokens(totals) {
|
|
31100
|
-
|
|
31101
|
-
return totals.uncachedInputTokens + totals.outputTokens + totals.cacheReadTokens + totals.cacheWriteTokens > 0;
|
|
31235
|
+
return totals !== void 0 && billedTotal(totals) > 0;
|
|
31102
31236
|
}
|
|
31103
|
-
/** Total billed tokens of a flat totals record (prompt side + completion). */
|
|
31104
31237
|
function billedTotal(totals) {
|
|
31105
31238
|
return totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens + totals.outputTokens;
|
|
31106
31239
|
}
|
|
31107
|
-
|
|
31108
|
-
|
|
31109
|
-
|
|
31110
|
-
*
|
|
31111
|
-
|
|
31112
|
-
|
|
31113
|
-
|
|
31114
|
-
*
|
|
31115
|
-
* `signature` is any string that changes when the native usage figure moves;
|
|
31116
|
-
* passing it in keeps this hook out of the projection seat and lets the caller
|
|
31117
|
-
* decide what "a new call settled" means for its own pill. A session that turns
|
|
31118
|
-
* out to have subagents additionally re-asks on a slow beat, because subagent
|
|
31119
|
-
* spend lands in a different session log and no native figure moves for it.
|
|
31120
|
-
*/
|
|
31240
|
+
//#endregion
|
|
31241
|
+
//#region src/client/use-session-usage.ts
|
|
31242
|
+
/** The composer row's window onto the whole session tree. The native usage
|
|
31243
|
+
* projection only moves for main-agent calls, so poll the displayed session
|
|
31244
|
+
* even before its first child appears. Requests are serialized and late answers
|
|
31245
|
+
* from a previous session are discarded. Failures keep the last good reading. */
|
|
31246
|
+
const SESSION_POLL_MS = 4e3;
|
|
31121
31247
|
function useSessionUsage(sessionId, signature) {
|
|
31122
|
-
const [
|
|
31123
|
-
const
|
|
31124
|
-
const [beat, setBeat] = (0, react.useState)(0);
|
|
31125
|
-
const hasSubagents = (tree?.subagentSessions ?? 0) > 0;
|
|
31248
|
+
const [reading, setReading] = (0, react.useState)(null);
|
|
31249
|
+
const refresh = (0, react.useRef)(null);
|
|
31126
31250
|
(0, react.useEffect)(() => {
|
|
31127
|
-
if (
|
|
31128
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
31251
|
+
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
31252
|
+
let active = true;
|
|
31253
|
+
let pending = false;
|
|
31254
|
+
let again = false;
|
|
31255
|
+
const request = () => {
|
|
31256
|
+
if (!active) return;
|
|
31257
|
+
if (pending) {
|
|
31258
|
+
again = true;
|
|
31259
|
+
return;
|
|
31260
|
+
}
|
|
31261
|
+
pending = true;
|
|
31262
|
+
fetchSessionUsage(sessionId).then((tree) => {
|
|
31263
|
+
if (active) setReading({
|
|
31264
|
+
sessionId,
|
|
31265
|
+
tree
|
|
31266
|
+
});
|
|
31267
|
+
}).catch(() => {}).finally(() => {
|
|
31268
|
+
pending = false;
|
|
31269
|
+
if (active && again) {
|
|
31270
|
+
again = false;
|
|
31271
|
+
request();
|
|
31272
|
+
}
|
|
31273
|
+
});
|
|
31274
|
+
};
|
|
31275
|
+
refresh.current = request;
|
|
31276
|
+
const handle = setInterval(request, SESSION_POLL_MS);
|
|
31131
31277
|
return () => {
|
|
31278
|
+
active = false;
|
|
31279
|
+
refresh.current = null;
|
|
31132
31280
|
clearInterval(handle);
|
|
31133
31281
|
};
|
|
31134
|
-
}, [
|
|
31282
|
+
}, [sessionId]);
|
|
31135
31283
|
(0, react.useEffect)(() => {
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
|
|
31139
|
-
latest.current = key;
|
|
31140
|
-
let cancelled = false;
|
|
31141
|
-
fetchSessionUsage(sessionId).then((value) => {
|
|
31142
|
-
if (cancelled) return;
|
|
31143
|
-
setTree(value);
|
|
31144
|
-
}).catch(() => {
|
|
31145
|
-
latest.current = "";
|
|
31146
|
-
});
|
|
31147
|
-
return () => {
|
|
31148
|
-
cancelled = true;
|
|
31149
|
-
};
|
|
31150
|
-
}, [
|
|
31151
|
-
sessionId,
|
|
31152
|
-
signature,
|
|
31153
|
-
beat
|
|
31154
|
-
]);
|
|
31284
|
+
refresh.current?.();
|
|
31285
|
+
}, [sessionId, signature]);
|
|
31286
|
+
const tree = reading !== null && reading.sessionId === sessionId ? reading.tree : null;
|
|
31155
31287
|
return {
|
|
31156
31288
|
tree,
|
|
31157
31289
|
totals: tree === null ? null : combinedTotals(tree)
|
|
@@ -31446,16 +31578,10 @@ window.__ModuleLoader__.load({
|
|
|
31446
31578
|
}
|
|
31447
31579
|
function UsagePill({ usage, tree, t, dialog, dock }) {
|
|
31448
31580
|
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
|
|
31449
|
-
const
|
|
31450
|
-
|
|
31451
|
-
|
|
31452
|
-
|
|
31453
|
-
cacheWriteTokens: usage.cacheWriteTokens
|
|
31454
|
-
};
|
|
31455
|
-
const treeTotals = tree?.totals ?? null;
|
|
31456
|
-
const subagentTotals = tree?.tree?.subagents;
|
|
31457
|
-
const showSubagents = subagentTotals !== void 0 && hasAnyTokens(subagentTotals);
|
|
31458
|
-
const billed = showSubagents && treeTotals !== null ? treeTotals : nativeTotals;
|
|
31581
|
+
const display = sessionUsageForDisplay(usage, tree?.tree ?? null);
|
|
31582
|
+
const subagentTotals = display.subagents;
|
|
31583
|
+
const showSubagents = display.subagentSessions > 0;
|
|
31584
|
+
const billed = display.total;
|
|
31459
31585
|
const total = billedTotal(billed);
|
|
31460
31586
|
const totalText = t("message.turnUsage.count", { count: formatTokensCompact(total) });
|
|
31461
31587
|
const cacheHit = cacheHitPercentPrecise(billed);
|
|
@@ -31531,9 +31657,9 @@ window.__ModuleLoader__.load({
|
|
|
31531
31657
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(tokenBreakdown(billed).cacheMiss) }),
|
|
31532
31658
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.output") }),
|
|
31533
31659
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.outputTokens) }),
|
|
31534
|
-
showSubagents &&
|
|
31660
|
+
showSubagents && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31535
31661
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.main") }),
|
|
31536
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(
|
|
31662
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(display.self)) }),
|
|
31537
31663
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.subagents") }),
|
|
31538
31664
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
|
|
31539
31665
|
exactCount(billedTotal(subagentTotals)),
|
|
@@ -31541,7 +31667,7 @@ window.__ModuleLoader__.load({
|
|
|
31541
31667
|
t("stats.dialog.subagentCount", { count: tree?.tree?.subagentSessions ?? 0 })
|
|
31542
31668
|
] }),
|
|
31543
31669
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.treeTotal") }),
|
|
31544
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(
|
|
31670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(billed)) })
|
|
31545
31671
|
] })
|
|
31546
31672
|
]
|
|
31547
31673
|
})
|
|
@@ -31563,7 +31689,7 @@ window.__ModuleLoader__.load({
|
|
|
31563
31689
|
const stats = (0, react.useMemo)(() => projected ?? deriveStats(settledNodes ?? []), [projected, settledNodes]);
|
|
31564
31690
|
const tokenScale = (0, react.useMemo)(() => measuredTokenScale(settledNodes ?? []) ?? 1, [settledNodes]);
|
|
31565
31691
|
const tree = useSessionUsage(sessionId, usageSignature(usage, stats.steps));
|
|
31566
|
-
const hasTokens =
|
|
31692
|
+
const hasTokens = hasAnyTokens(sessionUsageForDisplay(usage, tree.tree).total);
|
|
31567
31693
|
if (stats.steps === 0 && !hasTokens) return null;
|
|
31568
31694
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31569
31695
|
ref: markDockContainer,
|
|
@@ -31629,7 +31755,7 @@ window.__ModuleLoader__.load({
|
|
|
31629
31755
|
const StatsUsageEntry = (0, react.memo)(function StatsUsageEntry({ useProjection, t, sessionId }) {
|
|
31630
31756
|
const usage = useProjection("tokenUsage");
|
|
31631
31757
|
const tree = useSessionUsage(sessionId, usageSignature(usage, usage === void 0 ? 0 : 1));
|
|
31632
|
-
if (!(
|
|
31758
|
+
if (!hasAnyTokens(sessionUsageForDisplay(usage, tree.tree).total)) return null;
|
|
31633
31759
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePill, {
|
|
31634
31760
|
usage,
|
|
31635
31761
|
tree,
|