dsh-token-total 1.0.3 → 1.0.4
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 +332 -123
- package/lib/index.js +47 -4
- package/lib/types/client/format.d.ts +8 -4
- package/lib/types/client/locales.d.ts +1 -1
- package/package.json +1 -1
- package/src/client/TokenTotalPanel.module.css +259 -99
- package/src/client/TokenTotalPanel.tsx +197 -62
- package/src/client/format.ts +9 -7
- package/src/client/locales.ts +76 -0
- package/src/query.ts +53 -2
package/lib/client.js
CHANGED
|
@@ -29427,7 +29427,7 @@ window.__ModuleLoader__.load({
|
|
|
29427
29427
|
}
|
|
29428
29428
|
//#endregion
|
|
29429
29429
|
//#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/TokenTotalPanel.module.css?inline
|
|
29430
|
-
const css$2 = "/**\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";
|
|
29430
|
+
const css$2 = "/**\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: 9px;\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 brand accent (same blue as the section bars and the total card). */\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: color-mix(in srgb, var(--dsw-tone-1) 14%, transparent);\n color: var(--dsw-tone-1);\n font-weight: 600;\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/* Trailing toolbar edge: the \"as of\" stamp sits immediately left of the\n refresh action (it stamps the very data that button refreshes). */\n.toolbarEnd_768838850 {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n}\n\n.asOf_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n white-space: nowrap;\n}\n\n/* Keep the remaining toolbar action at the trailing edge. */\n.refresh_768838850 {\n flex: 0 0 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/* Six equal tracks: the overview row is one grid of six identical cards\n (long value or not), so no card crowds its neighbours. At the Plugins\n page's 960px content width that is six columns on one row. */\n.cards_768838850 {\n display: grid;\n grid-template-columns: repeat(6, minmax(0, 1fr));\n gap: 10px;\n}\n\n/* Below the width where six cards hold their own, the grid folds to three\n columns (two rows of three), then to two (three rows of two). */\n@media (max-width: 720px) {\n .cards_768838850 {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n}\n\n@media (max-width: 430px) {\n .cards_768838850 {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n}\n\n/* Each stat card wears one accent hue (tone 1..6 → --dsw-tone-1..6): a 3px\n accent bar on the left edge, the icon in a tinted badge, and a whisper of\n the hue washing the top-left corner — the overview row reads in colour\n instead of one gray register. --card-accent is set by the .cardToneN_768838850 map\n below; content sits above the wash via a z-index layer. */\n.card_768838850 {\n --card-accent: var(--dsw-alias-label-tertiary);\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 14px 14px 12px 18px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-1);\n box-shadow: 0 1px 2px var(--dsw-alias-bg-mask-2);\n min-width: 0;\n}\n\n.card_768838850::before {\n content: \"\";\n position: absolute;\n left: 0;\n top: 12px;\n bottom: 12px;\n width: 3px;\n border-radius: 0 3px 3px 0;\n background: var(--card-accent);\n}\n\n.card_768838850::after {\n content: \"\";\n position: absolute;\n inset: 0;\n background: linear-gradient(135deg, color-mix(in srgb, var(--card-accent) 7%, transparent), transparent 60%);\n pointer-events: none;\n}\n\n/* Content must paint above the corner wash (the ::after pseudo is the last\n child, so it wins a same-layer paint order). */\n.card_768838850 > * {\n position: relative;\n z-index: 1;\n}\n\n.cardTone1_768838850 { --card-accent: var(--dsw-tone-1); }\n.cardTone2_768838850 { --card-accent: var(--dsw-tone-2); }\n.cardTone3_768838850 { --card-accent: var(--dsw-tone-3); }\n.cardTone4_768838850 { --card-accent: var(--dsw-tone-4); }\n.cardTone5_768838850 { --card-accent: var(--dsw-tone-5); }\n.cardTone6_768838850 { --card-accent: var(--dsw-tone-6); }\n\n.cardHead_768838850 {\n display: flex;\n align-items: center;\n gap: 7px;\n}\n\n/* Icon badge: the accent hue at 12% behind its own glyph. */\n.cardIcon_768838850 {\n box-sizing: border-box;\n width: 24px;\n height: 24px;\n padding: 5px;\n border-radius: 7px;\n color: var(--card-accent);\n background: color-mix(in srgb, var(--card-accent) 12%, transparent);\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/* Values never wrap: a wrapped number defeats FitText's measurement (the\n wrapped line is exactly as wide as its box) and reads as a layout break.\n Oversized text shrinks instead — see FitText in TokenTotalPanel.tsx_768838850. */\n.cardValue_768838850 {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n line-height: 1.15;\n white-space: nowrap;\n overflow: hidden;\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). Same no-wrap rule as the value above. */\n.cardSub_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 1.2;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* ── sections / chart chrome ───────────────────────────────────────────── */\n\n.section_768838850 {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n padding: 14px 16px 16px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-1);\n box-shadow: 0 1px 2px var(--dsw-alias-bg-mask-2);\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/* Section titles carry the same brand accent bar as the stat cards' tone\n language, so every card heading is anchored the same way. */\n.sectionTitle_768838850 {\n margin: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 15px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.sectionTitle_768838850::before {\n content: \"\";\n width: 3px;\n height: 13px;\n border-radius: 2px;\n background: var(--dsw-tone-1);\n flex: 0 0 auto;\n}\n\n/* Header meta cluster right of a section title (trend cap note + curve\n legend). Kept as one wrap-friendly row on a common baseline. */\n.headMeta_768838850 {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.trendNote_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Heatmap and the token trend share ONE row — one card each, left and right.\n Both columns stretch to a common height; the heatmap centres its SVG in the\n taller partner. Below 720px the pair folds to a single column. */\n.duo_768838850 {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n gap: 12px;\n align-items: stretch;\n}\n\n@media (max-width: 720px) {\n .duo_768838850 {\n grid-template-columns: minmax(0, 1fr);\n }\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 /* In the duo row the heatmap card shares height with the trend card; the\n grid centres itself in whatever slack the partner leaves. */\n flex: 1 1 auto;\n display: flex;\n align-items: center;\n}\n\n.heatmap_768838850 {\n display: block;\n margin: 0 auto;\n /* The ~364 day cells are flat SVG rects (no per-cell filter), so there is no\n blur chain left to re-rasterize — but promoting the SVG onto its own\n compositing layer still keeps the whole grid off the scroll repaint path,\n which is what made scrolling this panel drop frames while idle was clean\n (#4). Cheap and invisible, so it stays. Note that the promotion is what\n does the work: `contain: paint` on its own measured no better than no\n mitigation at all. */\n transform: translateZ(0);\n}\n\n.heatLegend_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Axis labels drawn inside the heatmap SVG: the weekday column on the left and\n the month strip along the bottom. SVG <text> takes `fill`, not `color`. */\n.heatAxis_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n.heatCell_768838850 {\n display: inline-block;\n border-radius: 2px;\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 No filter either: the reference chart paints each tile as a flat, matte\n rounded square, so a sheen/under-shadow would read as a smudge at this\n cell size rather than depth. */\n}\n\n/* The five heat levels ride a fixed emerald ramp sampled pixel-for-pixel from\n the reference chart (a GitHub-style green scale), with a darker variant for\n dark mode below — never the theme accent, which can read near-black against\n the base surface in dark mode. level0 (no data) is a neutral empty cell.\n The fills are fully opaque: the reference tiles are solid, and mixing toward\n transparent (the previous glass treatment) lightened every step and broke\n the sampled ramp. 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.heatLevel0_768838850 {\n fill: var(--dsw-heat-0);\n background: var(--dsw-heat-0);\n}\n\n.heatLevel1_768838850 {\n fill: var(--dsw-heat-1);\n background: var(--dsw-heat-1);\n}\n\n.heatLevel2_768838850 {\n fill: var(--dsw-heat-2);\n background: var(--dsw-heat-2);\n}\n\n.heatLevel3_768838850 {\n fill: var(--dsw-heat-3);\n background: var(--dsw-heat-3);\n}\n\n.heatLevel4_768838850 {\n fill: var(--dsw-heat-4);\n background: var(--dsw-heat-4);\n}\n\n.heatLevel5_768838850 {\n fill: var(--dsw-heat-5);\n background: var(--dsw-heat-5);\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: 12px;\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/* Share bar under a rank row's name: the row's slice of the section total,\n drawn in the row's own colour (a 3px track, like the design mock). */\n.rowTrack_768838850 {\n height: 3px;\n margin-top: 2px;\n border-radius: 2px;\n background: var(--dsw-alias-border-l1);\n overflow: hidden;\n}\n\n.rowFill_768838850 {\n display: block;\n height: 3px;\n border-radius: 2px;\n}\n\n/* Top-3 ranks read as a podium: the ordinal takes the warm accent instead of\n the muted gray the tail ranks keep. */\n.rankList_768838850 > .modelRow_768838850:nth-child(-n + 3) .modelRank_768838850 {\n color: var(--dsw-rank-top);\n font-weight: 700;\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/* ── 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 a fixed emerald green\n scale (GitHub-style activity heatmap), stepping from a near-white tint to\n a deep green; the hit-rate curve rides the brand blue.\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 The palette must ALSO live on :root: tooltips are fixed portals on\n document.body_768838850 (see ChartTip), outside the panel subtree, and the inline\n var(--dsw-chart-N) swatches they carry only resolve from :root. */\n:root, .panel_768838850 {\n --dsw-heat-0: #f4f5f5;\n --dsw-heat-1: #daf6ea;\n --dsw-heat-2: #c7f0df;\n --dsw-heat-3: #82edc0;\n --dsw-heat-4: #00b96b;\n --dsw-heat-5: #00663b;\n --dsw-trend-line: #4d6bfe;\n /* Overview-card tone accents (stat cards 1..6) and the podium ordinal.\n Six hues, each picked to sit apart from both the chart set and the heat\n ramp, at full strength so 12% badge tints stay visible on white. */\n --dsw-tone-1: #4d6bfe;\n --dsw-tone-2: #00b96b;\n --dsw-tone-3: #e07b39;\n --dsw-tone-4: #975bf1;\n --dsw-tone-5: #0d8a8a;\n --dsw-tone-6: #d34591;\n --dsw-rank-top: #c46212;\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 :root, .panel_768838850 {\n --dsw-heat-0: #1b211e;\n --dsw-heat-1: #123a2a;\n --dsw-heat-2: #17553a;\n --dsw-heat-3: #1c7a4e;\n --dsw-heat-4: #2fbf7c;\n --dsw-heat-5: #7ef0b4;\n --dsw-trend-line: #7d95ff;\n /* Dark tones are lifted versions of the light six: same identity hue,\n brighter so 16% badge tints and 3px bars read on dark surfaces. */\n --dsw-tone-1: #7d95ff;\n --dsw-tone-2: #2fbf7c;\n --dsw-tone-3: #e89a5c;\n --dsw-tone-4: #b18cff;\n --dsw-tone-5: #3bc5c5;\n --dsw-tone-6: #f076ac;\n --dsw-rank-top: #e08543;\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 /* Dark surfaces swallow pale tints, so the card accents step up: badge\n 12% → 16%, corner wash 7% → 10%. */\n .cardIcon_768838850 {\n background: color-mix(in srgb, var(--card-accent) 16%, transparent);\n }\n\n .card_768838850::after {\n background: linear-gradient(135deg, color-mix(in srgb, var(--card-accent) 10%, transparent), transparent 60%);\n }\n}\n";
|
|
29431
29431
|
const tagId$2 = "dsh-token-total/TokenTotalPanel.module.css";
|
|
29432
29432
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
|
|
29433
29433
|
const tag = document.createElement("style");
|
|
@@ -29450,15 +29450,25 @@ window.__ModuleLoader__.load({
|
|
|
29450
29450
|
"customRange": "customRange_768838850",
|
|
29451
29451
|
"dateInput": "dateInput_768838850",
|
|
29452
29452
|
"customSep": "customSep_768838850",
|
|
29453
|
+
"toolbarEnd": "toolbarEnd_768838850",
|
|
29454
|
+
"asOf": "asOf_768838850",
|
|
29453
29455
|
"refresh": "refresh_768838850",
|
|
29454
29456
|
"errorBanner": "errorBanner_768838850",
|
|
29455
29457
|
"loading": "loading_768838850",
|
|
29456
29458
|
"empty": "empty_768838850",
|
|
29457
29459
|
"cards": "cards_768838850",
|
|
29460
|
+
"cardToneN": "cardToneN_768838850",
|
|
29458
29461
|
"card": "card_768838850",
|
|
29462
|
+
"cardTone1": "cardTone1_768838850",
|
|
29463
|
+
"cardTone2": "cardTone2_768838850",
|
|
29464
|
+
"cardTone3": "cardTone3_768838850",
|
|
29465
|
+
"cardTone4": "cardTone4_768838850",
|
|
29466
|
+
"cardTone5": "cardTone5_768838850",
|
|
29467
|
+
"cardTone6": "cardTone6_768838850",
|
|
29459
29468
|
"cardHead": "cardHead_768838850",
|
|
29460
29469
|
"cardIcon": "cardIcon_768838850",
|
|
29461
29470
|
"cardLabel": "cardLabel_768838850",
|
|
29471
|
+
"tsx": "tsx_768838850",
|
|
29462
29472
|
"cardValue": "cardValue_768838850",
|
|
29463
29473
|
"cardValueSm": "cardValueSm_768838850",
|
|
29464
29474
|
"cardValueWrap": "cardValueWrap_768838850",
|
|
@@ -29467,12 +29477,15 @@ window.__ModuleLoader__.load({
|
|
|
29467
29477
|
"section": "section_768838850",
|
|
29468
29478
|
"sectionHead": "sectionHead_768838850",
|
|
29469
29479
|
"sectionTitle": "sectionTitle_768838850",
|
|
29480
|
+
"headMeta": "headMeta_768838850",
|
|
29470
29481
|
"trendNote": "trendNote_768838850",
|
|
29482
|
+
"duo": "duo_768838850",
|
|
29471
29483
|
"chartWrap": "chartWrap_768838850",
|
|
29472
29484
|
"heatWrap": "heatWrap_768838850",
|
|
29473
29485
|
"heatmap": "heatmap_768838850",
|
|
29474
|
-
"heatCell": "heatCell_768838850",
|
|
29475
29486
|
"heatLegend": "heatLegend_768838850",
|
|
29487
|
+
"heatAxis": "heatAxis_768838850",
|
|
29488
|
+
"heatCell": "heatCell_768838850",
|
|
29476
29489
|
"size": "size_768838850",
|
|
29477
29490
|
"heatLevel0": "heatLevel0_768838850",
|
|
29478
29491
|
"heatLevel1": "heatLevel1_768838850",
|
|
@@ -29494,7 +29507,6 @@ window.__ModuleLoader__.load({
|
|
|
29494
29507
|
"legendSwatch": "legendSwatch_768838850",
|
|
29495
29508
|
"legendTrend": "legendTrend_768838850",
|
|
29496
29509
|
"models": "models_768838850",
|
|
29497
|
-
"tsx": "tsx_768838850",
|
|
29498
29510
|
"donutWrap": "donutWrap_768838850",
|
|
29499
29511
|
"donut": "donut_768838850",
|
|
29500
29512
|
"donutTrack": "donutTrack_768838850",
|
|
@@ -29508,6 +29520,8 @@ window.__ModuleLoader__.load({
|
|
|
29508
29520
|
"rankList": "rankList_768838850",
|
|
29509
29521
|
"modelRow": "modelRow_768838850",
|
|
29510
29522
|
"modelRank": "modelRank_768838850",
|
|
29523
|
+
"rowTrack": "rowTrack_768838850",
|
|
29524
|
+
"rowFill": "rowFill_768838850",
|
|
29511
29525
|
"modelRowExpandable": "modelRowExpandable_768838850",
|
|
29512
29526
|
"modelToggle": "modelToggle_768838850",
|
|
29513
29527
|
"modelRowSub": "modelRowSub_768838850",
|
|
@@ -29530,8 +29544,7 @@ window.__ModuleLoader__.load({
|
|
|
29530
29544
|
"tipValue": "tipValue_768838850",
|
|
29531
29545
|
"tipRow": "tipRow_768838850",
|
|
29532
29546
|
"tipRowOther": "tipRowOther_768838850",
|
|
29533
|
-
"tipBreakdown": "tipBreakdown_768838850"
|
|
29534
|
-
"foot": "foot_768838850"
|
|
29547
|
+
"tipBreakdown": "tipBreakdown_768838850"
|
|
29535
29548
|
};
|
|
29536
29549
|
//#endregion
|
|
29537
29550
|
//#region src/client/ChartTip.tsx
|
|
@@ -29678,13 +29691,15 @@ window.__ModuleLoader__.load({
|
|
|
29678
29691
|
const d = parts[2];
|
|
29679
29692
|
return `${Number(m)}/${Number(d)}`;
|
|
29680
29693
|
}
|
|
29681
|
-
/** X-axis label for a trend bucket key.
|
|
29682
|
-
*
|
|
29683
|
-
*
|
|
29684
|
-
|
|
29685
|
-
|
|
29686
|
-
|
|
29687
|
-
|
|
29694
|
+
/** X-axis label for a trend bucket key. `mode` is what the axis READS AS,
|
|
29695
|
+
* chosen by the caller from the window it covers:
|
|
29696
|
+
* - 'hour' — a single-day hourly span (今日): the date is constant, so the
|
|
29697
|
+
* axis shows the bare clock ("8:00");
|
|
29698
|
+
* - 'day' — anything spanning several days (3天 hourly, any daily range):
|
|
29699
|
+
* the axis shows the short date ("10/22"); on multi-day hourly spans one
|
|
29700
|
+
* label marks each day. The tooltip still carries the full date + hour. */
|
|
29701
|
+
function bucketAxisLabel(key, mode) {
|
|
29702
|
+
return mode === "hour" ? `${Number(key.slice(11, 13))}:00` : shortDay(key);
|
|
29688
29703
|
}
|
|
29689
29704
|
/** Tooltip title for a trend bucket key: the date in day mode, date + hour
|
|
29690
29705
|
* in hour mode. */
|
|
@@ -29773,6 +29788,7 @@ window.__ModuleLoader__.load({
|
|
|
29773
29788
|
const [stats, setStats] = (0, react.useState)(null);
|
|
29774
29789
|
const [loading, setLoading] = (0, react.useState)(true);
|
|
29775
29790
|
const [error, setError] = (0, react.useState)("");
|
|
29791
|
+
const [loadedAt, setLoadedAt] = (0, react.useState)(null);
|
|
29776
29792
|
const generationRef = (0, react.useRef)(0);
|
|
29777
29793
|
const panelRef = (0, react.useRef)(null);
|
|
29778
29794
|
const heatWindow = (0, react.useMemo)(() => {
|
|
@@ -29812,6 +29828,7 @@ window.__ModuleLoader__.load({
|
|
|
29812
29828
|
const res = await fetchRange(req);
|
|
29813
29829
|
if (generationRef.current !== generation) return;
|
|
29814
29830
|
setStats(res);
|
|
29831
|
+
setLoadedAt(Date.now());
|
|
29815
29832
|
loadHeat();
|
|
29816
29833
|
} catch (e) {
|
|
29817
29834
|
if (generationRef.current !== generation) return;
|
|
@@ -29920,15 +29937,25 @@ window.__ModuleLoader__.load({
|
|
|
29920
29937
|
})
|
|
29921
29938
|
]
|
|
29922
29939
|
}),
|
|
29923
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
29924
|
-
|
|
29925
|
-
|
|
29926
|
-
|
|
29927
|
-
|
|
29928
|
-
|
|
29929
|
-
|
|
29930
|
-
|
|
29931
|
-
|
|
29940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
29941
|
+
className: TokenTotalPanel_module_default.toolbarEnd,
|
|
29942
|
+
children: [stats && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
29943
|
+
className: TokenTotalPanel_module_default.asOf,
|
|
29944
|
+
children: [
|
|
29945
|
+
t("asOf"),
|
|
29946
|
+
" ",
|
|
29947
|
+
asOfStamp(stats.to, loadedAt)
|
|
29948
|
+
]
|
|
29949
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
29950
|
+
size: "sm",
|
|
29951
|
+
variant: "outline",
|
|
29952
|
+
className: TokenTotalPanel_module_default.refresh,
|
|
29953
|
+
onClick: () => {
|
|
29954
|
+
load();
|
|
29955
|
+
},
|
|
29956
|
+
disabled: loading,
|
|
29957
|
+
children: t("refresh")
|
|
29958
|
+
})]
|
|
29932
29959
|
})
|
|
29933
29960
|
]
|
|
29934
29961
|
}),
|
|
@@ -29945,20 +29972,22 @@ window.__ModuleLoader__.load({
|
|
|
29945
29972
|
stats,
|
|
29946
29973
|
t
|
|
29947
29974
|
}),
|
|
29948
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
29949
|
-
|
|
29950
|
-
|
|
29951
|
-
|
|
29952
|
-
|
|
29953
|
-
|
|
29954
|
-
|
|
29955
|
-
|
|
29956
|
-
|
|
29957
|
-
|
|
29958
|
-
|
|
29959
|
-
|
|
29960
|
-
|
|
29961
|
-
|
|
29975
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
29976
|
+
className: TokenTotalPanel_module_default.duo,
|
|
29977
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Heatmap, {
|
|
29978
|
+
daily: heatDaily,
|
|
29979
|
+
from: heatWindow.from,
|
|
29980
|
+
to: heatWindow.to,
|
|
29981
|
+
t,
|
|
29982
|
+
panelRef
|
|
29983
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DailyTrend, {
|
|
29984
|
+
models: trendModels,
|
|
29985
|
+
daily: trendDaily,
|
|
29986
|
+
granularity: stats?.granularity ?? "day",
|
|
29987
|
+
t,
|
|
29988
|
+
colorForModel,
|
|
29989
|
+
panelRef
|
|
29990
|
+
})]
|
|
29962
29991
|
}),
|
|
29963
29992
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBreakdown, {
|
|
29964
29993
|
models: stats.models,
|
|
@@ -29966,15 +29995,8 @@ window.__ModuleLoader__.load({
|
|
|
29966
29995
|
total: stats.tokens,
|
|
29967
29996
|
providerNames: stats.providerNames,
|
|
29968
29997
|
t,
|
|
29969
|
-
panelRef
|
|
29970
|
-
|
|
29971
|
-
stats.to && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
29972
|
-
className: TokenTotalPanel_module_default.foot,
|
|
29973
|
-
children: [
|
|
29974
|
-
t("asOf"),
|
|
29975
|
-
" ",
|
|
29976
|
-
stats.to
|
|
29977
|
-
]
|
|
29998
|
+
panelRef,
|
|
29999
|
+
colorForModel
|
|
29978
30000
|
})
|
|
29979
30001
|
] }),
|
|
29980
30002
|
!error && stats && isEmptyRange(stats) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -29984,6 +30006,14 @@ window.__ModuleLoader__.load({
|
|
|
29984
30006
|
]
|
|
29985
30007
|
});
|
|
29986
30008
|
}
|
|
30009
|
+
/** "As of" stamp for a loaded range: the fetch clock time when the range
|
|
30010
|
+
* reaches today (the data is live up to that refresh), else the day's end —
|
|
30011
|
+
* the wire carries only dates, and a past day's data is complete. */
|
|
30012
|
+
function asOfStamp(to, fetchedAt) {
|
|
30013
|
+
if (to !== localDay(0)) return `${to} 23:59`;
|
|
30014
|
+
const d = new Date(fetchedAt ?? Date.now());
|
|
30015
|
+
return `${to} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
30016
|
+
}
|
|
29987
30017
|
/** A range is empty only when NOTHING was recorded in it: a pure-cache day
|
|
29988
30018
|
* (zero input/output but real cache hits) or a failed-call day (requests
|
|
29989
30019
|
* but no tokens) both carry real usage and must not read as "no data". */
|
|
@@ -29997,44 +30027,50 @@ window.__ModuleLoader__.load({
|
|
|
29997
30027
|
label: t("tokens"),
|
|
29998
30028
|
value: formatCompact(stats.tokens),
|
|
29999
30029
|
hint: t("tokensHint"),
|
|
30000
|
-
sub: `${formatTokens(stats.tokens)} ${t("tokensUnit")}
|
|
30030
|
+
sub: `${formatTokens(stats.tokens)} ${t("tokensUnit")}`,
|
|
30031
|
+
tone: 1
|
|
30001
30032
|
},
|
|
30002
30033
|
{
|
|
30003
30034
|
icon: import_lucide_react.ArrowDownToLine,
|
|
30004
30035
|
label: t("inputTokens"),
|
|
30005
30036
|
value: formatCompact(stats.inputTokens),
|
|
30006
|
-
sub: `${formatTokens(stats.inputTokens)} ${t("tokensUnit")}
|
|
30037
|
+
sub: `${formatTokens(stats.inputTokens)} ${t("tokensUnit")}`,
|
|
30038
|
+
tone: 2
|
|
30007
30039
|
},
|
|
30008
30040
|
{
|
|
30009
30041
|
icon: import_lucide_react.ArrowUpFromLine,
|
|
30010
30042
|
label: t("outputTokens"),
|
|
30011
30043
|
value: formatCompact(stats.outputTokens),
|
|
30012
|
-
sub: `${formatTokens(stats.outputTokens)} ${t("tokensUnit")}
|
|
30044
|
+
sub: `${formatTokens(stats.outputTokens)} ${t("tokensUnit")}`,
|
|
30045
|
+
tone: 3
|
|
30013
30046
|
},
|
|
30014
30047
|
{
|
|
30015
30048
|
icon: import_lucide_react.Database,
|
|
30016
30049
|
label: t("cacheTokens"),
|
|
30017
30050
|
value: formatCompact(stats.cacheTokens),
|
|
30018
|
-
sub: `${formatTokens(stats.cacheTokens)} ${t("tokensUnit")}
|
|
30051
|
+
sub: `${formatTokens(stats.cacheTokens)} ${t("tokensUnit")}`,
|
|
30052
|
+
tone: 4
|
|
30019
30053
|
},
|
|
30020
30054
|
{
|
|
30021
30055
|
icon: import_lucide_react.Activity,
|
|
30022
30056
|
label: t("cacheRate"),
|
|
30023
30057
|
value: cacheRateText(stats.cacheHit, stats.cacheMiss),
|
|
30024
30058
|
hint: t("cacheRateHint"),
|
|
30025
|
-
sub: `${formatCompact(stats.cacheHit)} ${t("cachedTokens")}
|
|
30059
|
+
sub: `${formatCompact(stats.cacheHit)} ${t("cachedTokens")}`,
|
|
30060
|
+
tone: 5
|
|
30026
30061
|
},
|
|
30027
30062
|
{
|
|
30028
30063
|
icon: import_lucide_react.MessagesSquare,
|
|
30029
30064
|
label: t("requests"),
|
|
30030
30065
|
value: String(stats.requests),
|
|
30031
|
-
sub: stats.failures > 0 ? `${t("requestsFailed")} ${stats.failures} · ${formatPercent(stats.failures / Math.max(stats.requests, 1) * 100)}` : void 0
|
|
30066
|
+
sub: stats.failures > 0 ? `${t("requestsFailed")} ${stats.failures} · ${formatPercent(stats.failures / Math.max(stats.requests, 1) * 100)}` : void 0,
|
|
30067
|
+
tone: 6
|
|
30032
30068
|
}
|
|
30033
30069
|
];
|
|
30034
30070
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30035
30071
|
className: TokenTotalPanel_module_default.cards,
|
|
30036
30072
|
children: cards.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30037
|
-
className: TokenTotalPanel_module_default.card,
|
|
30073
|
+
className: clsx(TokenTotalPanel_module_default.card, TokenTotalPanel_module_default[`cardTone${c.tone}`]),
|
|
30038
30074
|
"data-dsh-usage-glass": "card",
|
|
30039
30075
|
title: c.hint,
|
|
30040
30076
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -30054,9 +30090,11 @@ window.__ModuleLoader__.load({
|
|
|
30054
30090
|
text: c.value,
|
|
30055
30091
|
className: TokenTotalPanel_module_default.cardValue,
|
|
30056
30092
|
maxSize: 22
|
|
30057
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
30093
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FitText, {
|
|
30094
|
+
text: c.sub,
|
|
30058
30095
|
className: TokenTotalPanel_module_default.cardSub,
|
|
30059
|
-
|
|
30096
|
+
maxSize: 11,
|
|
30097
|
+
minSize: 8
|
|
30060
30098
|
})]
|
|
30061
30099
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FitText, {
|
|
30062
30100
|
text: c.value,
|
|
@@ -30066,7 +30104,7 @@ window.__ModuleLoader__.load({
|
|
|
30066
30104
|
}, c.label))
|
|
30067
30105
|
});
|
|
30068
30106
|
}
|
|
30069
|
-
function FitText({ text, className, maxSize }) {
|
|
30107
|
+
function FitText({ text, className, maxSize, minSize = 11 }) {
|
|
30070
30108
|
const ref = (0, react.useRef)(null);
|
|
30071
30109
|
const [size, setSize] = (0, react.useState)(maxSize);
|
|
30072
30110
|
(0, react.useLayoutEffect)(() => {
|
|
@@ -30075,7 +30113,7 @@ window.__ModuleLoader__.load({
|
|
|
30075
30113
|
const fit = () => {
|
|
30076
30114
|
let s = maxSize;
|
|
30077
30115
|
el.style.fontSize = `${s}px`;
|
|
30078
|
-
while (el.scrollWidth > el.clientWidth + 1 && s >
|
|
30116
|
+
while (el.scrollWidth > el.clientWidth + 1 && s > minSize) {
|
|
30079
30117
|
s -= .5;
|
|
30080
30118
|
el.style.fontSize = `${s}px`;
|
|
30081
30119
|
}
|
|
@@ -30085,7 +30123,11 @@ window.__ModuleLoader__.load({
|
|
|
30085
30123
|
const ro = new ResizeObserver(fit);
|
|
30086
30124
|
ro.observe(el);
|
|
30087
30125
|
return () => ro.disconnect();
|
|
30088
|
-
}, [
|
|
30126
|
+
}, [
|
|
30127
|
+
text,
|
|
30128
|
+
maxSize,
|
|
30129
|
+
minSize
|
|
30130
|
+
]);
|
|
30089
30131
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30090
30132
|
ref,
|
|
30091
30133
|
className,
|
|
@@ -30093,9 +30135,35 @@ window.__ModuleLoader__.load({
|
|
|
30093
30135
|
children: text
|
|
30094
30136
|
});
|
|
30095
30137
|
}
|
|
30096
|
-
const HEAT_BASE =
|
|
30097
|
-
const HEAT_GAP =
|
|
30098
|
-
const HEAT_RX =
|
|
30138
|
+
const HEAT_BASE = 12;
|
|
30139
|
+
const HEAT_GAP = 4;
|
|
30140
|
+
const HEAT_RX = 2;
|
|
30141
|
+
const HEAT_AXIS_LEFT = 30;
|
|
30142
|
+
const HEAT_AXIS_BOTTOM = 18;
|
|
30143
|
+
/** 0 = Monday … 6 = Sunday: row order of the heatmap, and the label keys. */
|
|
30144
|
+
const DOW_KEYS = [
|
|
30145
|
+
"dow.mon",
|
|
30146
|
+
"dow.tue",
|
|
30147
|
+
"dow.wed",
|
|
30148
|
+
"dow.thu",
|
|
30149
|
+
"dow.fri",
|
|
30150
|
+
"dow.sat",
|
|
30151
|
+
"dow.sun"
|
|
30152
|
+
];
|
|
30153
|
+
const MONTH_KEYS = [
|
|
30154
|
+
"month.jan",
|
|
30155
|
+
"month.feb",
|
|
30156
|
+
"month.mar",
|
|
30157
|
+
"month.apr",
|
|
30158
|
+
"month.may",
|
|
30159
|
+
"month.jun",
|
|
30160
|
+
"month.jul",
|
|
30161
|
+
"month.aug",
|
|
30162
|
+
"month.sep",
|
|
30163
|
+
"month.oct",
|
|
30164
|
+
"month.nov",
|
|
30165
|
+
"month.dec"
|
|
30166
|
+
];
|
|
30099
30167
|
/**
|
|
30100
30168
|
* Solve the heatmap's geometry for an available width.
|
|
30101
30169
|
*
|
|
@@ -30121,7 +30189,7 @@ window.__ModuleLoader__.load({
|
|
|
30121
30189
|
*/
|
|
30122
30190
|
function heatGeometry(avail, startOffset) {
|
|
30123
30191
|
const totalWeeks = Math.ceil((364 + startOffset) / 7);
|
|
30124
|
-
const baseCols = Math.max(1, Math.floor((avail - HEAT_GAP) /
|
|
30192
|
+
const baseCols = Math.max(1, Math.floor((avail - HEAT_GAP) / 16));
|
|
30125
30193
|
if (baseCols >= totalWeeks) {
|
|
30126
30194
|
const size = (avail - HEAT_GAP) / totalWeeks - HEAT_GAP;
|
|
30127
30195
|
if (size >= HEAT_BASE) return {
|
|
@@ -30149,12 +30217,12 @@ window.__ModuleLoader__.load({
|
|
|
30149
30217
|
}, [daily]);
|
|
30150
30218
|
const [tip, setTip] = (0, react.useState)(null);
|
|
30151
30219
|
const wrapRef = (0, react.useRef)(null);
|
|
30152
|
-
const [geom, setGeom] = (0, react.useState)(() => heatGeometry(
|
|
30220
|
+
const [geom, setGeom] = (0, react.useState)(() => heatGeometry(624, 0));
|
|
30153
30221
|
(0, react.useEffect)(() => {
|
|
30154
30222
|
const el = wrapRef.current;
|
|
30155
30223
|
if (!el) return;
|
|
30156
30224
|
const update = () => {
|
|
30157
|
-
const next = heatGeometry(Math.max(1, el.clientWidth - 2), (indexOfDay(from) + 1) % 7);
|
|
30225
|
+
const next = heatGeometry(Math.max(1, el.clientWidth - 2 - HEAT_AXIS_LEFT), (indexOfDay(from) + 1) % 7);
|
|
30158
30226
|
setGeom((prev) => prev.size === next.size && prev.cols === next.cols ? prev : next);
|
|
30159
30227
|
};
|
|
30160
30228
|
update();
|
|
@@ -30169,6 +30237,24 @@ window.__ModuleLoader__.load({
|
|
|
30169
30237
|
const rows = 7;
|
|
30170
30238
|
const startOffset = days[0] ? (indexOfDay(days[0]) + 1) % 7 : 0;
|
|
30171
30239
|
const weeks = geom.totalWeeks;
|
|
30240
|
+
const monthLabels = [];
|
|
30241
|
+
let lastMonth = -1;
|
|
30242
|
+
for (let i = 0; i < days.length; i++) {
|
|
30243
|
+
const col = Math.floor((i + startOffset) / 7);
|
|
30244
|
+
if ((i + startOffset) % 7 !== 0) continue;
|
|
30245
|
+
const m = (/* @__PURE__ */ new Date(days[i] + "T00:00:00")).getMonth();
|
|
30246
|
+
if (m !== lastMonth) {
|
|
30247
|
+
monthLabels.push({
|
|
30248
|
+
col,
|
|
30249
|
+
month: m
|
|
30250
|
+
});
|
|
30251
|
+
lastMonth = m;
|
|
30252
|
+
}
|
|
30253
|
+
}
|
|
30254
|
+
while (monthLabels.length > 1 && monthLabels[0].month === monthLabels[monthLabels.length - 1].month) monthLabels.pop();
|
|
30255
|
+
const cellW = geom.size + HEAT_GAP;
|
|
30256
|
+
const svgW = HEAT_AXIS_LEFT + weeks * cellW + HEAT_GAP;
|
|
30257
|
+
const svgH = rows * cellW + HEAT_GAP + HEAT_AXIS_BOTTOM;
|
|
30172
30258
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
30173
30259
|
className: TokenTotalPanel_module_default.section,
|
|
30174
30260
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -30180,6 +30266,13 @@ window.__ModuleLoader__.load({
|
|
|
30180
30266
|
className: TokenTotalPanel_module_default.heatLegend,
|
|
30181
30267
|
children: [
|
|
30182
30268
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("heatLess") }),
|
|
30269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30270
|
+
className: clsx(TokenTotalPanel_module_default.heatCell, TokenTotalPanel_module_default.heatLevel0),
|
|
30271
|
+
style: {
|
|
30272
|
+
width: geom.size,
|
|
30273
|
+
height: geom.size
|
|
30274
|
+
}
|
|
30275
|
+
}),
|
|
30183
30276
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30184
30277
|
className: clsx(TokenTotalPanel_module_default.heatCell, TokenTotalPanel_module_default.heatLevel1),
|
|
30185
30278
|
style: {
|
|
@@ -30221,40 +30314,58 @@ window.__ModuleLoader__.load({
|
|
|
30221
30314
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30222
30315
|
className: TokenTotalPanel_module_default.heatWrap,
|
|
30223
30316
|
ref: wrapRef,
|
|
30224
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
30317
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
30225
30318
|
className: TokenTotalPanel_module_default.heatmap,
|
|
30226
|
-
width:
|
|
30227
|
-
height:
|
|
30319
|
+
width: svgW,
|
|
30320
|
+
height: svgH,
|
|
30228
30321
|
role: "img",
|
|
30229
30322
|
"aria-label": t("heatmap"),
|
|
30230
|
-
children:
|
|
30231
|
-
|
|
30232
|
-
|
|
30233
|
-
|
|
30234
|
-
|
|
30235
|
-
|
|
30236
|
-
|
|
30237
|
-
|
|
30238
|
-
|
|
30239
|
-
|
|
30240
|
-
|
|
30241
|
-
|
|
30242
|
-
|
|
30243
|
-
|
|
30244
|
-
|
|
30245
|
-
|
|
30246
|
-
|
|
30247
|
-
|
|
30248
|
-
|
|
30249
|
-
|
|
30250
|
-
|
|
30251
|
-
|
|
30252
|
-
|
|
30253
|
-
|
|
30254
|
-
|
|
30255
|
-
|
|
30256
|
-
|
|
30257
|
-
|
|
30323
|
+
children: [
|
|
30324
|
+
DOW_KEYS.map((key, row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
30325
|
+
className: TokenTotalPanel_module_default.heatAxis,
|
|
30326
|
+
x: 22,
|
|
30327
|
+
y: HEAT_GAP + row * cellW + geom.size / 2,
|
|
30328
|
+
textAnchor: "end",
|
|
30329
|
+
dominantBaseline: "central",
|
|
30330
|
+
children: t(key)
|
|
30331
|
+
}, key)),
|
|
30332
|
+
days.map((day, i) => {
|
|
30333
|
+
const col = Math.floor((i + startOffset) / 7);
|
|
30334
|
+
const row = (i + startOffset) % 7;
|
|
30335
|
+
const rec = byDay.get(day);
|
|
30336
|
+
const tokens = rec?.total ?? 0;
|
|
30337
|
+
const level = tokens === 0 ? 0 : 1 + Math.floor(tokens / max * 4);
|
|
30338
|
+
const x = 34 + col * cellW;
|
|
30339
|
+
const y = HEAT_GAP + row * cellW;
|
|
30340
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
30341
|
+
className: clsx(TokenTotalPanel_module_default.heatCell, level === 0 && TokenTotalPanel_module_default.heatLevel0, level === 1 && TokenTotalPanel_module_default.heatLevel1, level === 2 && TokenTotalPanel_module_default.heatLevel2, level === 3 && TokenTotalPanel_module_default.heatLevel3, level === 4 && TokenTotalPanel_module_default.heatLevel4, level === 5 && TokenTotalPanel_module_default.heatLevel5),
|
|
30342
|
+
x,
|
|
30343
|
+
y,
|
|
30344
|
+
width: geom.size,
|
|
30345
|
+
height: geom.size,
|
|
30346
|
+
rx: HEAT_RX,
|
|
30347
|
+
onMouseEnter: (e) => {
|
|
30348
|
+
setTip({
|
|
30349
|
+
day,
|
|
30350
|
+
tokens,
|
|
30351
|
+
requests: rec?.requests ?? 0,
|
|
30352
|
+
cacheHit: rec?.cacheHit ?? 0,
|
|
30353
|
+
cacheMiss: rec?.cacheMiss ?? 0,
|
|
30354
|
+
anchor: e.currentTarget
|
|
30355
|
+
});
|
|
30356
|
+
},
|
|
30357
|
+
onMouseLeave: () => setTip(null)
|
|
30358
|
+
}, day);
|
|
30359
|
+
}),
|
|
30360
|
+
monthLabels.map(({ col, month }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
30361
|
+
className: TokenTotalPanel_module_default.heatAxis,
|
|
30362
|
+
x: 34 + col * cellW,
|
|
30363
|
+
y: rows * cellW + HEAT_GAP + 3,
|
|
30364
|
+
textAnchor: "start",
|
|
30365
|
+
dominantBaseline: "hanging",
|
|
30366
|
+
children: t(MONTH_KEYS[month])
|
|
30367
|
+
}, `${col}-${month}`))
|
|
30368
|
+
]
|
|
30258
30369
|
}), tip && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChartTip, {
|
|
30259
30370
|
anchor: tip.anchor,
|
|
30260
30371
|
panelRef,
|
|
@@ -30325,7 +30436,21 @@ window.__ModuleLoader__.load({
|
|
|
30325
30436
|
const plotWUsed = view.avail;
|
|
30326
30437
|
const maxTotal = Math.max(1, ...visible.map((d) => d.total));
|
|
30327
30438
|
const ticks = niceTicks(maxTotal, 4);
|
|
30328
|
-
const
|
|
30439
|
+
const dayCount = new Set(visible.map((d) => d.day.slice(0, 10))).size;
|
|
30440
|
+
const multiDayHourly = granularity === "hour" && dayCount > 1;
|
|
30441
|
+
const axisMode = multiDayHourly ? "day" : granularity;
|
|
30442
|
+
const labelCandidates = [];
|
|
30443
|
+
for (let i = 0; i < n; i++) if (!multiDayHourly || i === 0 || visible[i].day.slice(0, 10) !== visible[i - 1].day.slice(0, 10)) labelCandidates.push(i);
|
|
30444
|
+
const labelMinGap = 42;
|
|
30445
|
+
const labeledIdx = /* @__PURE__ */ new Set();
|
|
30446
|
+
let lastLabelX = Number.POSITIVE_INFINITY;
|
|
30447
|
+
for (let k = labelCandidates.length - 1; k >= 0; k--) {
|
|
30448
|
+
const x = padL + barHalf + labelCandidates[k] * step;
|
|
30449
|
+
if (lastLabelX - x >= labelMinGap) {
|
|
30450
|
+
labeledIdx.add(labelCandidates[k]);
|
|
30451
|
+
lastLabelX = x;
|
|
30452
|
+
}
|
|
30453
|
+
}
|
|
30329
30454
|
const trendPoints = [];
|
|
30330
30455
|
const trendPointByDay = /* @__PURE__ */ new Map();
|
|
30331
30456
|
visible.forEach((d, i) => {
|
|
@@ -30355,9 +30480,18 @@ window.__ModuleLoader__.load({
|
|
|
30355
30480
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
30356
30481
|
className: TokenTotalPanel_module_default.sectionTitle,
|
|
30357
30482
|
children: t(granularity === "hour" ? "hourlyTrend" : "dailyTrend")
|
|
30358
|
-
}),
|
|
30359
|
-
className: TokenTotalPanel_module_default.
|
|
30360
|
-
children:
|
|
30483
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30484
|
+
className: TokenTotalPanel_module_default.headMeta,
|
|
30485
|
+
children: [trendCapped && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30486
|
+
className: TokenTotalPanel_module_default.trendNote,
|
|
30487
|
+
children: t("trendLimited").replace("{n}", String(view.trimN))
|
|
30488
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
30489
|
+
className: TokenTotalPanel_module_default.legendItem,
|
|
30490
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30491
|
+
className: TokenTotalPanel_module_default.legendTrend,
|
|
30492
|
+
"aria-hidden": "true"
|
|
30493
|
+
}), t("cacheHitRate")]
|
|
30494
|
+
})]
|
|
30361
30495
|
})]
|
|
30362
30496
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30363
30497
|
className: TokenTotalPanel_module_default.chartWrap,
|
|
@@ -30437,12 +30571,12 @@ window.__ModuleLoader__.load({
|
|
|
30437
30571
|
},
|
|
30438
30572
|
onMouseLeave: () => setTip(null)
|
|
30439
30573
|
}),
|
|
30440
|
-
(i
|
|
30574
|
+
labeledIdx.has(i) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
30441
30575
|
className: TokenTotalPanel_module_default.axis,
|
|
30442
30576
|
x: padL + barHalf + i * step,
|
|
30443
30577
|
y: 212,
|
|
30444
30578
|
textAnchor: "middle",
|
|
30445
|
-
children: bucketAxisLabel(d.day,
|
|
30579
|
+
children: bucketAxisLabel(d.day, axisMode)
|
|
30446
30580
|
})
|
|
30447
30581
|
] }, d.day);
|
|
30448
30582
|
}),
|
|
@@ -30522,17 +30656,17 @@ window.__ModuleLoader__.load({
|
|
|
30522
30656
|
function providerColor(index) {
|
|
30523
30657
|
return index < 5 ? `var(--dsw-provider-${index + 1})` : "var(--dsw-provider-other)";
|
|
30524
30658
|
}
|
|
30525
|
-
|
|
30526
|
-
function modelColor(index) {
|
|
30527
|
-
return index < TOP_MODELS ? `var(--dsw-chart-${index + 1})` : OTHER_COLOR;
|
|
30528
|
-
}
|
|
30529
|
-
function UsageBreakdown({ models, providers, total, providerNames, t, panelRef }) {
|
|
30659
|
+
function UsageBreakdown({ models, providers, total, providerNames, t, panelRef, colorForModel }) {
|
|
30530
30660
|
const [tip, setTip] = (0, react.useState)(null);
|
|
30531
30661
|
const providerLabel = (0, react.useCallback)((provider) => {
|
|
30532
30662
|
const named = providerNames?.[provider];
|
|
30533
30663
|
return typeof named === "string" && named.trim() !== "" ? named : provider;
|
|
30534
30664
|
}, [providerNames]);
|
|
30535
30665
|
const shareText = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
|
|
30666
|
+
const providerColorOf = (0, react.useCallback)((provider) => {
|
|
30667
|
+
const slot = providers.findIndex((p) => p.provider === provider);
|
|
30668
|
+
return slot < 0 ? "var(--dsw-provider-other)" : providerColor(slot);
|
|
30669
|
+
}, [providers]);
|
|
30536
30670
|
const modelRows = (0, react.useMemo)(() => {
|
|
30537
30671
|
const byName = /* @__PURE__ */ new Map();
|
|
30538
30672
|
for (const m of models) {
|
|
@@ -30551,19 +30685,19 @@ window.__ModuleLoader__.load({
|
|
|
30551
30685
|
for (const provider of m.providers ?? (m.provider === "" ? [] : [m.provider])) acc.sources.add(provider);
|
|
30552
30686
|
byName.set(name, acc);
|
|
30553
30687
|
}
|
|
30554
|
-
return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc]
|
|
30555
|
-
const split = [...acc.split.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([provider, tokens]
|
|
30688
|
+
return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc]) => {
|
|
30689
|
+
const split = [...acc.split.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([provider, tokens]) => ({
|
|
30556
30690
|
key: provider,
|
|
30557
30691
|
name: providerLabel(provider),
|
|
30558
30692
|
tokens,
|
|
30559
|
-
color:
|
|
30693
|
+
color: providerColorOf(provider)
|
|
30560
30694
|
}));
|
|
30561
30695
|
const known = new Set(split.map((child) => child.key));
|
|
30562
|
-
const named = [...acc.sources].filter((provider) => !known.has(provider)).sort().map((provider
|
|
30696
|
+
const named = [...acc.sources].filter((provider) => !known.has(provider)).sort().map((provider) => ({
|
|
30563
30697
|
key: provider,
|
|
30564
30698
|
name: providerLabel(provider),
|
|
30565
30699
|
tokens: null,
|
|
30566
|
-
color:
|
|
30700
|
+
color: providerColorOf(provider)
|
|
30567
30701
|
}));
|
|
30568
30702
|
return {
|
|
30569
30703
|
key: name,
|
|
@@ -30571,7 +30705,7 @@ window.__ModuleLoader__.load({
|
|
|
30571
30705
|
tokens: acc.tokens,
|
|
30572
30706
|
cacheHit: acc.cacheHit,
|
|
30573
30707
|
cacheMiss: acc.cacheMiss,
|
|
30574
|
-
color:
|
|
30708
|
+
color: colorForModel(name),
|
|
30575
30709
|
breakdownLabel: t("byProvider"),
|
|
30576
30710
|
breakdown: [...split, ...named]
|
|
30577
30711
|
};
|
|
@@ -30579,10 +30713,12 @@ window.__ModuleLoader__.load({
|
|
|
30579
30713
|
}, [
|
|
30580
30714
|
models,
|
|
30581
30715
|
providerLabel,
|
|
30582
|
-
t
|
|
30716
|
+
t,
|
|
30717
|
+
colorForModel,
|
|
30718
|
+
providerColorOf
|
|
30583
30719
|
]);
|
|
30584
30720
|
const providerRows = (0, react.useMemo)(() => {
|
|
30585
|
-
return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p
|
|
30721
|
+
return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p) => {
|
|
30586
30722
|
const label = providerLabel(p.provider);
|
|
30587
30723
|
return {
|
|
30588
30724
|
key: p.provider,
|
|
@@ -30591,20 +30727,22 @@ window.__ModuleLoader__.load({
|
|
|
30591
30727
|
tokens: p.tokens,
|
|
30592
30728
|
cacheHit: p.cacheHit,
|
|
30593
30729
|
cacheMiss: p.cacheMiss,
|
|
30594
|
-
color:
|
|
30730
|
+
color: providerColorOf(p.provider),
|
|
30595
30731
|
breakdownLabel: t("byModel"),
|
|
30596
|
-
breakdown: [...p.modelTokens ?? []].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)).map((m
|
|
30732
|
+
breakdown: [...p.modelTokens ?? []].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)).map((m) => ({
|
|
30597
30733
|
key: m.model,
|
|
30598
30734
|
name: m.model,
|
|
30599
30735
|
tokens: m.tokens,
|
|
30600
|
-
color:
|
|
30736
|
+
color: colorForModel(m.model)
|
|
30601
30737
|
}))
|
|
30602
30738
|
};
|
|
30603
30739
|
});
|
|
30604
30740
|
}, [
|
|
30605
30741
|
providers,
|
|
30606
30742
|
providerLabel,
|
|
30607
|
-
t
|
|
30743
|
+
t,
|
|
30744
|
+
colorForModel,
|
|
30745
|
+
providerColorOf
|
|
30608
30746
|
]);
|
|
30609
30747
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
30610
30748
|
className: TokenTotalPanel_module_default.section,
|
|
@@ -30731,13 +30869,27 @@ window.__ModuleLoader__.load({
|
|
|
30731
30869
|
}),
|
|
30732
30870
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30733
30871
|
className: TokenTotalPanel_module_default.modelId,
|
|
30734
|
-
children: [
|
|
30735
|
-
|
|
30736
|
-
|
|
30737
|
-
|
|
30738
|
-
|
|
30739
|
-
|
|
30740
|
-
|
|
30872
|
+
children: [
|
|
30873
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30874
|
+
className: TokenTotalPanel_module_default.modelName,
|
|
30875
|
+
children: r.name
|
|
30876
|
+
}),
|
|
30877
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30878
|
+
className: TokenTotalPanel_module_default.modelProvider,
|
|
30879
|
+
children: share(r.tokens)
|
|
30880
|
+
}),
|
|
30881
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30882
|
+
className: TokenTotalPanel_module_default.rowTrack,
|
|
30883
|
+
"aria-hidden": "true",
|
|
30884
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30885
|
+
className: TokenTotalPanel_module_default.rowFill,
|
|
30886
|
+
style: {
|
|
30887
|
+
width: total > 0 ? `${Math.min(100, r.tokens / total * 100)}%` : "0%",
|
|
30888
|
+
background: r.color
|
|
30889
|
+
}
|
|
30890
|
+
})
|
|
30891
|
+
})
|
|
30892
|
+
]
|
|
30741
30893
|
}),
|
|
30742
30894
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30743
30895
|
className: TokenTotalPanel_module_default.modelValues,
|
|
@@ -32169,6 +32321,25 @@ window.__ModuleLoader__.load({
|
|
|
32169
32321
|
heatmap: "Activity heatmap",
|
|
32170
32322
|
heatLess: "Less",
|
|
32171
32323
|
heatMore: "More",
|
|
32324
|
+
"dow.mon": "Mon",
|
|
32325
|
+
"dow.tue": "Tue",
|
|
32326
|
+
"dow.wed": "Wed",
|
|
32327
|
+
"dow.thu": "Thu",
|
|
32328
|
+
"dow.fri": "Fri",
|
|
32329
|
+
"dow.sat": "Sat",
|
|
32330
|
+
"dow.sun": "Sun",
|
|
32331
|
+
"month.jan": "Jan",
|
|
32332
|
+
"month.feb": "Feb",
|
|
32333
|
+
"month.mar": "Mar",
|
|
32334
|
+
"month.apr": "Apr",
|
|
32335
|
+
"month.may": "May",
|
|
32336
|
+
"month.jun": "Jun",
|
|
32337
|
+
"month.jul": "Jul",
|
|
32338
|
+
"month.aug": "Aug",
|
|
32339
|
+
"month.sep": "Sep",
|
|
32340
|
+
"month.oct": "Oct",
|
|
32341
|
+
"month.nov": "Nov",
|
|
32342
|
+
"month.dec": "Dec",
|
|
32172
32343
|
dailyTrend: "Daily token trend",
|
|
32173
32344
|
hourlyTrend: "Hourly token trend",
|
|
32174
32345
|
trendLimited: "Showing the latest {n} days",
|
|
@@ -32238,6 +32409,25 @@ window.__ModuleLoader__.load({
|
|
|
32238
32409
|
heatmap: "活跃热力图",
|
|
32239
32410
|
heatLess: "较少",
|
|
32240
32411
|
heatMore: "较多",
|
|
32412
|
+
"dow.mon": "周一",
|
|
32413
|
+
"dow.tue": "周二",
|
|
32414
|
+
"dow.wed": "周三",
|
|
32415
|
+
"dow.thu": "周四",
|
|
32416
|
+
"dow.fri": "周五",
|
|
32417
|
+
"dow.sat": "周六",
|
|
32418
|
+
"dow.sun": "周日",
|
|
32419
|
+
"month.jan": "1月",
|
|
32420
|
+
"month.feb": "2月",
|
|
32421
|
+
"month.mar": "3月",
|
|
32422
|
+
"month.apr": "4月",
|
|
32423
|
+
"month.may": "5月",
|
|
32424
|
+
"month.jun": "6月",
|
|
32425
|
+
"month.jul": "7月",
|
|
32426
|
+
"month.aug": "8月",
|
|
32427
|
+
"month.sep": "9月",
|
|
32428
|
+
"month.oct": "10月",
|
|
32429
|
+
"month.nov": "11月",
|
|
32430
|
+
"month.dec": "12月",
|
|
32241
32431
|
dailyTrend: "按天 Token 趋势",
|
|
32242
32432
|
hourlyTrend: "按小时 Token 趋势",
|
|
32243
32433
|
trendLimited: "仅显示最近 {n} 天",
|
|
@@ -32307,6 +32497,25 @@ window.__ModuleLoader__.load({
|
|
|
32307
32497
|
heatmap: "活躍熱力圖",
|
|
32308
32498
|
heatLess: "較少",
|
|
32309
32499
|
heatMore: "較多",
|
|
32500
|
+
"dow.mon": "週一",
|
|
32501
|
+
"dow.tue": "週二",
|
|
32502
|
+
"dow.wed": "週三",
|
|
32503
|
+
"dow.thu": "週四",
|
|
32504
|
+
"dow.fri": "週五",
|
|
32505
|
+
"dow.sat": "週六",
|
|
32506
|
+
"dow.sun": "週日",
|
|
32507
|
+
"month.jan": "1月",
|
|
32508
|
+
"month.feb": "2月",
|
|
32509
|
+
"month.mar": "3月",
|
|
32510
|
+
"month.apr": "4月",
|
|
32511
|
+
"month.may": "5月",
|
|
32512
|
+
"month.jun": "6月",
|
|
32513
|
+
"month.jul": "7月",
|
|
32514
|
+
"month.aug": "8月",
|
|
32515
|
+
"month.sep": "9月",
|
|
32516
|
+
"month.oct": "10月",
|
|
32517
|
+
"month.nov": "11月",
|
|
32518
|
+
"month.dec": "12月",
|
|
32310
32519
|
dailyTrend: "每日 Token 趨勢",
|
|
32311
32520
|
hourlyTrend: "每小時 Token 趨勢",
|
|
32312
32521
|
trendLimited: "僅顯示最近 {n} 天",
|