dsh-token-total 1.0.3 → 1.0.5
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 +16 -2
- package/lib/client.js +1318 -176
- package/lib/index.js +434 -4
- package/lib/types/client/SessionUsageView.d.ts +14 -0
- package/lib/types/client/api.d.ts +3 -1
- package/lib/types/client/format.d.ts +9 -5
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/session-usage-locales.d.ts +62 -0
- package/lib/types/client/use-session-detail.d.ts +8 -0
- package/lib/types/routes.d.ts +2 -2
- package/lib/types/session-detail.d.ts +3 -0
- package/lib/types/wire.d.ts +55 -0
- package/package.json +1 -1
- package/src/client/SessionUsageView.module.css +117 -0
- package/src/client/SessionUsageView.tsx +217 -0
- package/src/client/TokenTotalPanel.module.css +259 -99
- package/src/client/TokenTotalPanel.tsx +210 -63
- package/src/client/api.ts +8 -2
- package/src/client/format.ts +13 -9
- package/src/client/index.tsx +20 -1
- package/src/client/locales.ts +76 -0
- package/src/client/session-usage-locales.ts +50 -0
- package/src/client/use-session-detail.ts +50 -0
- package/src/query.ts +53 -2
- package/src/routes.ts +11 -2
- package/src/session-detail.ts +182 -0
- package/src/wire.ts +56 -0
package/lib/client.js
CHANGED
|
@@ -29390,13 +29390,14 @@ window.__ModuleLoader__.load({
|
|
|
29390
29390
|
this.code = code;
|
|
29391
29391
|
}
|
|
29392
29392
|
};
|
|
29393
|
-
async function post(method, body) {
|
|
29393
|
+
async function post(method, body, signal) {
|
|
29394
29394
|
let res;
|
|
29395
29395
|
try {
|
|
29396
29396
|
res = await fetch(`token-total/api/${method}`, {
|
|
29397
29397
|
method: "POST",
|
|
29398
29398
|
headers: { "content-type": "application/json" },
|
|
29399
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
29399
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
29400
|
+
signal
|
|
29400
29401
|
});
|
|
29401
29402
|
} catch (err) {
|
|
29402
29403
|
throw new UsageApiError("network", err instanceof Error ? err.message : String(err));
|
|
@@ -29425,15 +29426,19 @@ window.__ModuleLoader__.load({
|
|
|
29425
29426
|
async function fetchSessionUsage(sessionId) {
|
|
29426
29427
|
return post("session", { sessionId });
|
|
29427
29428
|
}
|
|
29429
|
+
/** Log-owned requests and live route states for the per-session Usage view. */
|
|
29430
|
+
async function fetchSessionDetail(sessionId, signal) {
|
|
29431
|
+
return post("session-detail", { sessionId }, signal);
|
|
29432
|
+
}
|
|
29428
29433
|
//#endregion
|
|
29429
29434
|
//#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";
|
|
29431
|
-
const tagId$
|
|
29432
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$
|
|
29435
|
+
const css$3 = "/**\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";
|
|
29436
|
+
const tagId$3 = "dsh-token-total/TokenTotalPanel.module.css";
|
|
29437
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$3 + "\"]") === null) {
|
|
29433
29438
|
const tag = document.createElement("style");
|
|
29434
29439
|
tag.dataset.plugin = "dsh-token-total";
|
|
29435
|
-
tag.dataset.pluginCss = tagId$
|
|
29436
|
-
tag.textContent = css$
|
|
29440
|
+
tag.dataset.pluginCss = tagId$3;
|
|
29441
|
+
tag.textContent = css$3;
|
|
29437
29442
|
document.head.appendChild(tag);
|
|
29438
29443
|
}
|
|
29439
29444
|
var TokenTotalPanel_module_default = {
|
|
@@ -29450,15 +29455,25 @@ window.__ModuleLoader__.load({
|
|
|
29450
29455
|
"customRange": "customRange_768838850",
|
|
29451
29456
|
"dateInput": "dateInput_768838850",
|
|
29452
29457
|
"customSep": "customSep_768838850",
|
|
29458
|
+
"toolbarEnd": "toolbarEnd_768838850",
|
|
29459
|
+
"asOf": "asOf_768838850",
|
|
29453
29460
|
"refresh": "refresh_768838850",
|
|
29454
29461
|
"errorBanner": "errorBanner_768838850",
|
|
29455
29462
|
"loading": "loading_768838850",
|
|
29456
29463
|
"empty": "empty_768838850",
|
|
29457
29464
|
"cards": "cards_768838850",
|
|
29465
|
+
"cardToneN": "cardToneN_768838850",
|
|
29458
29466
|
"card": "card_768838850",
|
|
29467
|
+
"cardTone1": "cardTone1_768838850",
|
|
29468
|
+
"cardTone2": "cardTone2_768838850",
|
|
29469
|
+
"cardTone3": "cardTone3_768838850",
|
|
29470
|
+
"cardTone4": "cardTone4_768838850",
|
|
29471
|
+
"cardTone5": "cardTone5_768838850",
|
|
29472
|
+
"cardTone6": "cardTone6_768838850",
|
|
29459
29473
|
"cardHead": "cardHead_768838850",
|
|
29460
29474
|
"cardIcon": "cardIcon_768838850",
|
|
29461
29475
|
"cardLabel": "cardLabel_768838850",
|
|
29476
|
+
"tsx": "tsx_768838850",
|
|
29462
29477
|
"cardValue": "cardValue_768838850",
|
|
29463
29478
|
"cardValueSm": "cardValueSm_768838850",
|
|
29464
29479
|
"cardValueWrap": "cardValueWrap_768838850",
|
|
@@ -29467,12 +29482,15 @@ window.__ModuleLoader__.load({
|
|
|
29467
29482
|
"section": "section_768838850",
|
|
29468
29483
|
"sectionHead": "sectionHead_768838850",
|
|
29469
29484
|
"sectionTitle": "sectionTitle_768838850",
|
|
29485
|
+
"headMeta": "headMeta_768838850",
|
|
29470
29486
|
"trendNote": "trendNote_768838850",
|
|
29487
|
+
"duo": "duo_768838850",
|
|
29471
29488
|
"chartWrap": "chartWrap_768838850",
|
|
29472
29489
|
"heatWrap": "heatWrap_768838850",
|
|
29473
29490
|
"heatmap": "heatmap_768838850",
|
|
29474
|
-
"heatCell": "heatCell_768838850",
|
|
29475
29491
|
"heatLegend": "heatLegend_768838850",
|
|
29492
|
+
"heatAxis": "heatAxis_768838850",
|
|
29493
|
+
"heatCell": "heatCell_768838850",
|
|
29476
29494
|
"size": "size_768838850",
|
|
29477
29495
|
"heatLevel0": "heatLevel0_768838850",
|
|
29478
29496
|
"heatLevel1": "heatLevel1_768838850",
|
|
@@ -29494,7 +29512,6 @@ window.__ModuleLoader__.load({
|
|
|
29494
29512
|
"legendSwatch": "legendSwatch_768838850",
|
|
29495
29513
|
"legendTrend": "legendTrend_768838850",
|
|
29496
29514
|
"models": "models_768838850",
|
|
29497
|
-
"tsx": "tsx_768838850",
|
|
29498
29515
|
"donutWrap": "donutWrap_768838850",
|
|
29499
29516
|
"donut": "donut_768838850",
|
|
29500
29517
|
"donutTrack": "donutTrack_768838850",
|
|
@@ -29508,6 +29525,8 @@ window.__ModuleLoader__.load({
|
|
|
29508
29525
|
"rankList": "rankList_768838850",
|
|
29509
29526
|
"modelRow": "modelRow_768838850",
|
|
29510
29527
|
"modelRank": "modelRank_768838850",
|
|
29528
|
+
"rowTrack": "rowTrack_768838850",
|
|
29529
|
+
"rowFill": "rowFill_768838850",
|
|
29511
29530
|
"modelRowExpandable": "modelRowExpandable_768838850",
|
|
29512
29531
|
"modelToggle": "modelToggle_768838850",
|
|
29513
29532
|
"modelRowSub": "modelRowSub_768838850",
|
|
@@ -29530,8 +29549,7 @@ window.__ModuleLoader__.load({
|
|
|
29530
29549
|
"tipValue": "tipValue_768838850",
|
|
29531
29550
|
"tipRow": "tipRow_768838850",
|
|
29532
29551
|
"tipRowOther": "tipRowOther_768838850",
|
|
29533
|
-
"tipBreakdown": "tipBreakdown_768838850"
|
|
29534
|
-
"foot": "foot_768838850"
|
|
29552
|
+
"tipBreakdown": "tipBreakdown_768838850"
|
|
29535
29553
|
};
|
|
29536
29554
|
//#endregion
|
|
29537
29555
|
//#region src/client/ChartTip.tsx
|
|
@@ -29632,6 +29650,13 @@ window.__ModuleLoader__.load({
|
|
|
29632
29650
|
}
|
|
29633
29651
|
return out;
|
|
29634
29652
|
}
|
|
29653
|
+
/** Canonicalize a DISPLAY NAME, never a route id. Case and word separators
|
|
29654
|
+
* do not distinguish models, but versions, punctuation and suffixes do:
|
|
29655
|
+
* gpt-6.1-sol / GPT 6.1 sol -> gpt-6.1-sol, not gpt-6.1 or gpt-61-sol.
|
|
29656
|
+
* Do not strip a slash here: it may be part of the actual display name. */
|
|
29657
|
+
function normalizeModelName(displayName) {
|
|
29658
|
+
return displayName.normalize("NFKC").trim().toLowerCase().replace(/[\s_\p{Dash_Punctuation}]+/gu, "-").replace(/^-+|-+$/g, "") || "(unknown)";
|
|
29659
|
+
}
|
|
29635
29660
|
//#endregion
|
|
29636
29661
|
//#region src/client/format.ts
|
|
29637
29662
|
/** Exact token counts with thousands separators — no 万/亿/k/M units, the
|
|
@@ -29673,18 +29698,20 @@ window.__ModuleLoader__.load({
|
|
|
29673
29698
|
return ((/* @__PURE__ */ new Date(day + "T00:00:00")).getDay() + 6) % 7;
|
|
29674
29699
|
}
|
|
29675
29700
|
function shortDay(day) {
|
|
29676
|
-
const parts = day.split("-");
|
|
29701
|
+
const parts = day.slice(0, 10).split("-");
|
|
29677
29702
|
const m = parts[1];
|
|
29678
29703
|
const d = parts[2];
|
|
29679
29704
|
return `${Number(m)}/${Number(d)}`;
|
|
29680
29705
|
}
|
|
29681
|
-
/** X-axis label for a trend bucket key.
|
|
29682
|
-
*
|
|
29683
|
-
*
|
|
29684
|
-
|
|
29685
|
-
|
|
29686
|
-
|
|
29687
|
-
|
|
29706
|
+
/** X-axis label for a trend bucket key. `mode` is what the axis READS AS,
|
|
29707
|
+
* chosen by the caller from the window it covers:
|
|
29708
|
+
* - 'hour' — a single-day hourly span (今日): the date is constant, so the
|
|
29709
|
+
* axis shows the bare clock ("8:00");
|
|
29710
|
+
* - 'day' — anything spanning several days (3天 hourly, any daily range):
|
|
29711
|
+
* the axis shows the short date ("10/22"); on multi-day hourly spans one
|
|
29712
|
+
* label marks each day. The tooltip still carries the full date + hour. */
|
|
29713
|
+
function bucketAxisLabel(key, mode) {
|
|
29714
|
+
return mode === "hour" ? `${Number(key.slice(11, 13))}:00` : shortDay(key);
|
|
29688
29715
|
}
|
|
29689
29716
|
/** Tooltip title for a trend bucket key: the date in day mode, date + hour
|
|
29690
29717
|
* in hour mode. */
|
|
@@ -29773,6 +29800,7 @@ window.__ModuleLoader__.load({
|
|
|
29773
29800
|
const [stats, setStats] = (0, react.useState)(null);
|
|
29774
29801
|
const [loading, setLoading] = (0, react.useState)(true);
|
|
29775
29802
|
const [error, setError] = (0, react.useState)("");
|
|
29803
|
+
const [loadedAt, setLoadedAt] = (0, react.useState)(null);
|
|
29776
29804
|
const generationRef = (0, react.useRef)(0);
|
|
29777
29805
|
const panelRef = (0, react.useRef)(null);
|
|
29778
29806
|
const heatWindow = (0, react.useMemo)(() => {
|
|
@@ -29812,6 +29840,7 @@ window.__ModuleLoader__.load({
|
|
|
29812
29840
|
const res = await fetchRange(req);
|
|
29813
29841
|
if (generationRef.current !== generation) return;
|
|
29814
29842
|
setStats(res);
|
|
29843
|
+
setLoadedAt(Date.now());
|
|
29815
29844
|
loadHeat();
|
|
29816
29845
|
} catch (e) {
|
|
29817
29846
|
if (generationRef.current !== generation) return;
|
|
@@ -29920,15 +29949,25 @@ window.__ModuleLoader__.load({
|
|
|
29920
29949
|
})
|
|
29921
29950
|
]
|
|
29922
29951
|
}),
|
|
29923
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
29924
|
-
|
|
29925
|
-
|
|
29926
|
-
|
|
29927
|
-
|
|
29928
|
-
|
|
29929
|
-
|
|
29930
|
-
|
|
29931
|
-
|
|
29952
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
29953
|
+
className: TokenTotalPanel_module_default.toolbarEnd,
|
|
29954
|
+
children: [stats && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
29955
|
+
className: TokenTotalPanel_module_default.asOf,
|
|
29956
|
+
children: [
|
|
29957
|
+
t("asOf"),
|
|
29958
|
+
" ",
|
|
29959
|
+
asOfStamp(stats.to, loadedAt)
|
|
29960
|
+
]
|
|
29961
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
29962
|
+
size: "sm",
|
|
29963
|
+
variant: "outline",
|
|
29964
|
+
className: TokenTotalPanel_module_default.refresh,
|
|
29965
|
+
onClick: () => {
|
|
29966
|
+
load();
|
|
29967
|
+
},
|
|
29968
|
+
disabled: loading,
|
|
29969
|
+
children: t("refresh")
|
|
29970
|
+
})]
|
|
29932
29971
|
})
|
|
29933
29972
|
]
|
|
29934
29973
|
}),
|
|
@@ -29945,20 +29984,22 @@ window.__ModuleLoader__.load({
|
|
|
29945
29984
|
stats,
|
|
29946
29985
|
t
|
|
29947
29986
|
}),
|
|
29948
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
29949
|
-
|
|
29950
|
-
|
|
29951
|
-
|
|
29952
|
-
|
|
29953
|
-
|
|
29954
|
-
|
|
29955
|
-
|
|
29956
|
-
|
|
29957
|
-
|
|
29958
|
-
|
|
29959
|
-
|
|
29960
|
-
|
|
29961
|
-
|
|
29987
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
29988
|
+
className: TokenTotalPanel_module_default.duo,
|
|
29989
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Heatmap, {
|
|
29990
|
+
daily: heatDaily,
|
|
29991
|
+
from: heatWindow.from,
|
|
29992
|
+
to: heatWindow.to,
|
|
29993
|
+
t,
|
|
29994
|
+
panelRef
|
|
29995
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DailyTrend, {
|
|
29996
|
+
models: trendModels,
|
|
29997
|
+
daily: trendDaily,
|
|
29998
|
+
granularity: stats?.granularity ?? "day",
|
|
29999
|
+
t,
|
|
30000
|
+
colorForModel,
|
|
30001
|
+
panelRef
|
|
30002
|
+
})]
|
|
29962
30003
|
}),
|
|
29963
30004
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBreakdown, {
|
|
29964
30005
|
models: stats.models,
|
|
@@ -29966,15 +30007,8 @@ window.__ModuleLoader__.load({
|
|
|
29966
30007
|
total: stats.tokens,
|
|
29967
30008
|
providerNames: stats.providerNames,
|
|
29968
30009
|
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
|
-
]
|
|
30010
|
+
panelRef,
|
|
30011
|
+
colorForModel
|
|
29978
30012
|
})
|
|
29979
30013
|
] }),
|
|
29980
30014
|
!error && stats && isEmptyRange(stats) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -29984,6 +30018,14 @@ window.__ModuleLoader__.load({
|
|
|
29984
30018
|
]
|
|
29985
30019
|
});
|
|
29986
30020
|
}
|
|
30021
|
+
/** "As of" stamp for a loaded range: the fetch clock time when the range
|
|
30022
|
+
* reaches today (the data is live up to that refresh), else the day's end —
|
|
30023
|
+
* the wire carries only dates, and a past day's data is complete. */
|
|
30024
|
+
function asOfStamp(to, fetchedAt) {
|
|
30025
|
+
if (to !== localDay(0)) return `${to} 23:59`;
|
|
30026
|
+
const d = new Date(fetchedAt ?? Date.now());
|
|
30027
|
+
return `${to} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
30028
|
+
}
|
|
29987
30029
|
/** A range is empty only when NOTHING was recorded in it: a pure-cache day
|
|
29988
30030
|
* (zero input/output but real cache hits) or a failed-call day (requests
|
|
29989
30031
|
* but no tokens) both carry real usage and must not read as "no data". */
|
|
@@ -29997,44 +30039,50 @@ window.__ModuleLoader__.load({
|
|
|
29997
30039
|
label: t("tokens"),
|
|
29998
30040
|
value: formatCompact(stats.tokens),
|
|
29999
30041
|
hint: t("tokensHint"),
|
|
30000
|
-
sub: `${formatTokens(stats.tokens)} ${t("tokensUnit")}
|
|
30042
|
+
sub: `${formatTokens(stats.tokens)} ${t("tokensUnit")}`,
|
|
30043
|
+
tone: 1
|
|
30001
30044
|
},
|
|
30002
30045
|
{
|
|
30003
30046
|
icon: import_lucide_react.ArrowDownToLine,
|
|
30004
30047
|
label: t("inputTokens"),
|
|
30005
30048
|
value: formatCompact(stats.inputTokens),
|
|
30006
|
-
sub: `${formatTokens(stats.inputTokens)} ${t("tokensUnit")}
|
|
30049
|
+
sub: `${formatTokens(stats.inputTokens)} ${t("tokensUnit")}`,
|
|
30050
|
+
tone: 2
|
|
30007
30051
|
},
|
|
30008
30052
|
{
|
|
30009
30053
|
icon: import_lucide_react.ArrowUpFromLine,
|
|
30010
30054
|
label: t("outputTokens"),
|
|
30011
30055
|
value: formatCompact(stats.outputTokens),
|
|
30012
|
-
sub: `${formatTokens(stats.outputTokens)} ${t("tokensUnit")}
|
|
30056
|
+
sub: `${formatTokens(stats.outputTokens)} ${t("tokensUnit")}`,
|
|
30057
|
+
tone: 3
|
|
30013
30058
|
},
|
|
30014
30059
|
{
|
|
30015
30060
|
icon: import_lucide_react.Database,
|
|
30016
30061
|
label: t("cacheTokens"),
|
|
30017
30062
|
value: formatCompact(stats.cacheTokens),
|
|
30018
|
-
sub: `${formatTokens(stats.cacheTokens)} ${t("tokensUnit")}
|
|
30063
|
+
sub: `${formatTokens(stats.cacheTokens)} ${t("tokensUnit")}`,
|
|
30064
|
+
tone: 4
|
|
30019
30065
|
},
|
|
30020
30066
|
{
|
|
30021
30067
|
icon: import_lucide_react.Activity,
|
|
30022
30068
|
label: t("cacheRate"),
|
|
30023
30069
|
value: cacheRateText(stats.cacheHit, stats.cacheMiss),
|
|
30024
30070
|
hint: t("cacheRateHint"),
|
|
30025
|
-
sub: `${formatCompact(stats.cacheHit)} ${t("cachedTokens")}
|
|
30071
|
+
sub: `${formatCompact(stats.cacheHit)} ${t("cachedTokens")}`,
|
|
30072
|
+
tone: 5
|
|
30026
30073
|
},
|
|
30027
30074
|
{
|
|
30028
30075
|
icon: import_lucide_react.MessagesSquare,
|
|
30029
30076
|
label: t("requests"),
|
|
30030
30077
|
value: String(stats.requests),
|
|
30031
|
-
sub: stats.failures > 0 ? `${t("requestsFailed")} ${stats.failures} · ${formatPercent(stats.failures / Math.max(stats.requests, 1) * 100)}` : void 0
|
|
30078
|
+
sub: stats.failures > 0 ? `${t("requestsFailed")} ${stats.failures} · ${formatPercent(stats.failures / Math.max(stats.requests, 1) * 100)}` : void 0,
|
|
30079
|
+
tone: 6
|
|
30032
30080
|
}
|
|
30033
30081
|
];
|
|
30034
30082
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30035
30083
|
className: TokenTotalPanel_module_default.cards,
|
|
30036
30084
|
children: cards.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30037
|
-
className: TokenTotalPanel_module_default.card,
|
|
30085
|
+
className: clsx(TokenTotalPanel_module_default.card, TokenTotalPanel_module_default[`cardTone${c.tone}`]),
|
|
30038
30086
|
"data-dsh-usage-glass": "card",
|
|
30039
30087
|
title: c.hint,
|
|
30040
30088
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -30054,9 +30102,11 @@ window.__ModuleLoader__.load({
|
|
|
30054
30102
|
text: c.value,
|
|
30055
30103
|
className: TokenTotalPanel_module_default.cardValue,
|
|
30056
30104
|
maxSize: 22
|
|
30057
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
30105
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FitText, {
|
|
30106
|
+
text: c.sub,
|
|
30058
30107
|
className: TokenTotalPanel_module_default.cardSub,
|
|
30059
|
-
|
|
30108
|
+
maxSize: 11,
|
|
30109
|
+
minSize: 8
|
|
30060
30110
|
})]
|
|
30061
30111
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FitText, {
|
|
30062
30112
|
text: c.value,
|
|
@@ -30066,7 +30116,7 @@ window.__ModuleLoader__.load({
|
|
|
30066
30116
|
}, c.label))
|
|
30067
30117
|
});
|
|
30068
30118
|
}
|
|
30069
|
-
function FitText({ text, className, maxSize }) {
|
|
30119
|
+
function FitText({ text, className, maxSize, minSize = 11 }) {
|
|
30070
30120
|
const ref = (0, react.useRef)(null);
|
|
30071
30121
|
const [size, setSize] = (0, react.useState)(maxSize);
|
|
30072
30122
|
(0, react.useLayoutEffect)(() => {
|
|
@@ -30075,7 +30125,7 @@ window.__ModuleLoader__.load({
|
|
|
30075
30125
|
const fit = () => {
|
|
30076
30126
|
let s = maxSize;
|
|
30077
30127
|
el.style.fontSize = `${s}px`;
|
|
30078
|
-
while (el.scrollWidth > el.clientWidth + 1 && s >
|
|
30128
|
+
while (el.scrollWidth > el.clientWidth + 1 && s > minSize) {
|
|
30079
30129
|
s -= .5;
|
|
30080
30130
|
el.style.fontSize = `${s}px`;
|
|
30081
30131
|
}
|
|
@@ -30085,7 +30135,11 @@ window.__ModuleLoader__.load({
|
|
|
30085
30135
|
const ro = new ResizeObserver(fit);
|
|
30086
30136
|
ro.observe(el);
|
|
30087
30137
|
return () => ro.disconnect();
|
|
30088
|
-
}, [
|
|
30138
|
+
}, [
|
|
30139
|
+
text,
|
|
30140
|
+
maxSize,
|
|
30141
|
+
minSize
|
|
30142
|
+
]);
|
|
30089
30143
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30090
30144
|
ref,
|
|
30091
30145
|
className,
|
|
@@ -30093,9 +30147,35 @@ window.__ModuleLoader__.load({
|
|
|
30093
30147
|
children: text
|
|
30094
30148
|
});
|
|
30095
30149
|
}
|
|
30096
|
-
const HEAT_BASE =
|
|
30097
|
-
const HEAT_GAP =
|
|
30098
|
-
const HEAT_RX =
|
|
30150
|
+
const HEAT_BASE = 12;
|
|
30151
|
+
const HEAT_GAP = 4;
|
|
30152
|
+
const HEAT_RX = 2;
|
|
30153
|
+
const HEAT_AXIS_LEFT = 30;
|
|
30154
|
+
const HEAT_AXIS_BOTTOM = 18;
|
|
30155
|
+
/** 0 = Monday … 6 = Sunday: row order of the heatmap, and the label keys. */
|
|
30156
|
+
const DOW_KEYS = [
|
|
30157
|
+
"dow.mon",
|
|
30158
|
+
"dow.tue",
|
|
30159
|
+
"dow.wed",
|
|
30160
|
+
"dow.thu",
|
|
30161
|
+
"dow.fri",
|
|
30162
|
+
"dow.sat",
|
|
30163
|
+
"dow.sun"
|
|
30164
|
+
];
|
|
30165
|
+
const MONTH_KEYS = [
|
|
30166
|
+
"month.jan",
|
|
30167
|
+
"month.feb",
|
|
30168
|
+
"month.mar",
|
|
30169
|
+
"month.apr",
|
|
30170
|
+
"month.may",
|
|
30171
|
+
"month.jun",
|
|
30172
|
+
"month.jul",
|
|
30173
|
+
"month.aug",
|
|
30174
|
+
"month.sep",
|
|
30175
|
+
"month.oct",
|
|
30176
|
+
"month.nov",
|
|
30177
|
+
"month.dec"
|
|
30178
|
+
];
|
|
30099
30179
|
/**
|
|
30100
30180
|
* Solve the heatmap's geometry for an available width.
|
|
30101
30181
|
*
|
|
@@ -30121,7 +30201,7 @@ window.__ModuleLoader__.load({
|
|
|
30121
30201
|
*/
|
|
30122
30202
|
function heatGeometry(avail, startOffset) {
|
|
30123
30203
|
const totalWeeks = Math.ceil((364 + startOffset) / 7);
|
|
30124
|
-
const baseCols = Math.max(1, Math.floor((avail - HEAT_GAP) /
|
|
30204
|
+
const baseCols = Math.max(1, Math.floor((avail - HEAT_GAP) / 16));
|
|
30125
30205
|
if (baseCols >= totalWeeks) {
|
|
30126
30206
|
const size = (avail - HEAT_GAP) / totalWeeks - HEAT_GAP;
|
|
30127
30207
|
if (size >= HEAT_BASE) return {
|
|
@@ -30149,12 +30229,12 @@ window.__ModuleLoader__.load({
|
|
|
30149
30229
|
}, [daily]);
|
|
30150
30230
|
const [tip, setTip] = (0, react.useState)(null);
|
|
30151
30231
|
const wrapRef = (0, react.useRef)(null);
|
|
30152
|
-
const [geom, setGeom] = (0, react.useState)(() => heatGeometry(
|
|
30232
|
+
const [geom, setGeom] = (0, react.useState)(() => heatGeometry(624, 0));
|
|
30153
30233
|
(0, react.useEffect)(() => {
|
|
30154
30234
|
const el = wrapRef.current;
|
|
30155
30235
|
if (!el) return;
|
|
30156
30236
|
const update = () => {
|
|
30157
|
-
const next = heatGeometry(Math.max(1, el.clientWidth - 2), (indexOfDay(from) + 1) % 7);
|
|
30237
|
+
const next = heatGeometry(Math.max(1, el.clientWidth - 2 - HEAT_AXIS_LEFT), (indexOfDay(from) + 1) % 7);
|
|
30158
30238
|
setGeom((prev) => prev.size === next.size && prev.cols === next.cols ? prev : next);
|
|
30159
30239
|
};
|
|
30160
30240
|
update();
|
|
@@ -30169,6 +30249,24 @@ window.__ModuleLoader__.load({
|
|
|
30169
30249
|
const rows = 7;
|
|
30170
30250
|
const startOffset = days[0] ? (indexOfDay(days[0]) + 1) % 7 : 0;
|
|
30171
30251
|
const weeks = geom.totalWeeks;
|
|
30252
|
+
const monthLabels = [];
|
|
30253
|
+
let lastMonth = -1;
|
|
30254
|
+
for (let i = 0; i < days.length; i++) {
|
|
30255
|
+
const col = Math.floor((i + startOffset) / 7);
|
|
30256
|
+
if ((i + startOffset) % 7 !== 0) continue;
|
|
30257
|
+
const m = (/* @__PURE__ */ new Date(days[i] + "T00:00:00")).getMonth();
|
|
30258
|
+
if (m !== lastMonth) {
|
|
30259
|
+
monthLabels.push({
|
|
30260
|
+
col,
|
|
30261
|
+
month: m
|
|
30262
|
+
});
|
|
30263
|
+
lastMonth = m;
|
|
30264
|
+
}
|
|
30265
|
+
}
|
|
30266
|
+
while (monthLabels.length > 1 && monthLabels[0].month === monthLabels[monthLabels.length - 1].month) monthLabels.pop();
|
|
30267
|
+
const cellW = geom.size + HEAT_GAP;
|
|
30268
|
+
const svgW = HEAT_AXIS_LEFT + weeks * cellW + HEAT_GAP;
|
|
30269
|
+
const svgH = rows * cellW + HEAT_GAP + HEAT_AXIS_BOTTOM;
|
|
30172
30270
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
30173
30271
|
className: TokenTotalPanel_module_default.section,
|
|
30174
30272
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -30180,6 +30278,13 @@ window.__ModuleLoader__.load({
|
|
|
30180
30278
|
className: TokenTotalPanel_module_default.heatLegend,
|
|
30181
30279
|
children: [
|
|
30182
30280
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("heatLess") }),
|
|
30281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30282
|
+
className: clsx(TokenTotalPanel_module_default.heatCell, TokenTotalPanel_module_default.heatLevel0),
|
|
30283
|
+
style: {
|
|
30284
|
+
width: geom.size,
|
|
30285
|
+
height: geom.size
|
|
30286
|
+
}
|
|
30287
|
+
}),
|
|
30183
30288
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30184
30289
|
className: clsx(TokenTotalPanel_module_default.heatCell, TokenTotalPanel_module_default.heatLevel1),
|
|
30185
30290
|
style: {
|
|
@@ -30221,40 +30326,58 @@ window.__ModuleLoader__.load({
|
|
|
30221
30326
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30222
30327
|
className: TokenTotalPanel_module_default.heatWrap,
|
|
30223
30328
|
ref: wrapRef,
|
|
30224
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
30329
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
30225
30330
|
className: TokenTotalPanel_module_default.heatmap,
|
|
30226
|
-
width:
|
|
30227
|
-
height:
|
|
30331
|
+
width: svgW,
|
|
30332
|
+
height: svgH,
|
|
30228
30333
|
role: "img",
|
|
30229
30334
|
"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
|
-
|
|
30335
|
+
children: [
|
|
30336
|
+
DOW_KEYS.map((key, row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
30337
|
+
className: TokenTotalPanel_module_default.heatAxis,
|
|
30338
|
+
x: 22,
|
|
30339
|
+
y: HEAT_GAP + row * cellW + geom.size / 2,
|
|
30340
|
+
textAnchor: "end",
|
|
30341
|
+
dominantBaseline: "central",
|
|
30342
|
+
children: t(key)
|
|
30343
|
+
}, key)),
|
|
30344
|
+
days.map((day, i) => {
|
|
30345
|
+
const col = Math.floor((i + startOffset) / 7);
|
|
30346
|
+
const row = (i + startOffset) % 7;
|
|
30347
|
+
const rec = byDay.get(day);
|
|
30348
|
+
const tokens = rec?.total ?? 0;
|
|
30349
|
+
const level = tokens === 0 ? 0 : 1 + Math.floor(tokens / max * 4);
|
|
30350
|
+
const x = 34 + col * cellW;
|
|
30351
|
+
const y = HEAT_GAP + row * cellW;
|
|
30352
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
30353
|
+
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),
|
|
30354
|
+
x,
|
|
30355
|
+
y,
|
|
30356
|
+
width: geom.size,
|
|
30357
|
+
height: geom.size,
|
|
30358
|
+
rx: HEAT_RX,
|
|
30359
|
+
onMouseEnter: (e) => {
|
|
30360
|
+
setTip({
|
|
30361
|
+
day,
|
|
30362
|
+
tokens,
|
|
30363
|
+
requests: rec?.requests ?? 0,
|
|
30364
|
+
cacheHit: rec?.cacheHit ?? 0,
|
|
30365
|
+
cacheMiss: rec?.cacheMiss ?? 0,
|
|
30366
|
+
anchor: e.currentTarget
|
|
30367
|
+
});
|
|
30368
|
+
},
|
|
30369
|
+
onMouseLeave: () => setTip(null)
|
|
30370
|
+
}, day);
|
|
30371
|
+
}),
|
|
30372
|
+
monthLabels.map(({ col, month }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
30373
|
+
className: TokenTotalPanel_module_default.heatAxis,
|
|
30374
|
+
x: 34 + col * cellW,
|
|
30375
|
+
y: rows * cellW + HEAT_GAP + 3,
|
|
30376
|
+
textAnchor: "start",
|
|
30377
|
+
dominantBaseline: "hanging",
|
|
30378
|
+
children: t(MONTH_KEYS[month])
|
|
30379
|
+
}, `${col}-${month}`))
|
|
30380
|
+
]
|
|
30258
30381
|
}), tip && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChartTip, {
|
|
30259
30382
|
anchor: tip.anchor,
|
|
30260
30383
|
panelRef,
|
|
@@ -30325,7 +30448,29 @@ window.__ModuleLoader__.load({
|
|
|
30325
30448
|
const plotWUsed = view.avail;
|
|
30326
30449
|
const maxTotal = Math.max(1, ...visible.map((d) => d.total));
|
|
30327
30450
|
const ticks = niceTicks(maxTotal, 4);
|
|
30328
|
-
const
|
|
30451
|
+
const dayCount = new Set(visible.map((d) => d.day.slice(0, 10))).size;
|
|
30452
|
+
const multiDayHourly = granularity === "hour" && dayCount > 1;
|
|
30453
|
+
const axisMode = multiDayHourly ? "day" : granularity;
|
|
30454
|
+
const labelGroups = [];
|
|
30455
|
+
for (let i = 0; i < n; i++) {
|
|
30456
|
+
const last = labelGroups[labelGroups.length - 1];
|
|
30457
|
+
if (multiDayHourly && last && visible[last.end].day.slice(0, 10) === visible[i].day.slice(0, 10)) last.end = i;
|
|
30458
|
+
else labelGroups.push({
|
|
30459
|
+
start: i,
|
|
30460
|
+
end: i
|
|
30461
|
+
});
|
|
30462
|
+
}
|
|
30463
|
+
const labelMinGap = 42;
|
|
30464
|
+
const labeledGroups = /* @__PURE__ */ new Set();
|
|
30465
|
+
let lastLabelX = Number.POSITIVE_INFINITY;
|
|
30466
|
+
for (let k = labelGroups.length - 1; k >= 0; k--) {
|
|
30467
|
+
const g = labelGroups[k];
|
|
30468
|
+
const x = padL + barHalf + (g.start + g.end) / 2 * step;
|
|
30469
|
+
if (lastLabelX - x >= labelMinGap) {
|
|
30470
|
+
labeledGroups.add(k);
|
|
30471
|
+
lastLabelX = x;
|
|
30472
|
+
}
|
|
30473
|
+
}
|
|
30329
30474
|
const trendPoints = [];
|
|
30330
30475
|
const trendPointByDay = /* @__PURE__ */ new Map();
|
|
30331
30476
|
visible.forEach((d, i) => {
|
|
@@ -30355,9 +30500,18 @@ window.__ModuleLoader__.load({
|
|
|
30355
30500
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
30356
30501
|
className: TokenTotalPanel_module_default.sectionTitle,
|
|
30357
30502
|
children: t(granularity === "hour" ? "hourlyTrend" : "dailyTrend")
|
|
30358
|
-
}),
|
|
30359
|
-
className: TokenTotalPanel_module_default.
|
|
30360
|
-
children:
|
|
30503
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30504
|
+
className: TokenTotalPanel_module_default.headMeta,
|
|
30505
|
+
children: [trendCapped && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30506
|
+
className: TokenTotalPanel_module_default.trendNote,
|
|
30507
|
+
children: t("trendLimited").replace("{n}", String(view.trimN))
|
|
30508
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
30509
|
+
className: TokenTotalPanel_module_default.legendItem,
|
|
30510
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30511
|
+
className: TokenTotalPanel_module_default.legendTrend,
|
|
30512
|
+
"aria-hidden": "true"
|
|
30513
|
+
}), t("cacheHitRate")]
|
|
30514
|
+
})]
|
|
30361
30515
|
})]
|
|
30362
30516
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30363
30517
|
className: TokenTotalPanel_module_default.chartWrap,
|
|
@@ -30408,44 +30562,41 @@ window.__ModuleLoader__.load({
|
|
|
30408
30562
|
};
|
|
30409
30563
|
});
|
|
30410
30564
|
const hovered = tip?.day === d.day;
|
|
30411
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
30412
|
-
|
|
30413
|
-
|
|
30414
|
-
|
|
30415
|
-
|
|
30416
|
-
|
|
30417
|
-
|
|
30418
|
-
|
|
30419
|
-
|
|
30420
|
-
|
|
30421
|
-
|
|
30422
|
-
|
|
30423
|
-
|
|
30424
|
-
|
|
30425
|
-
|
|
30426
|
-
|
|
30427
|
-
|
|
30428
|
-
|
|
30429
|
-
|
|
30430
|
-
|
|
30431
|
-
|
|
30432
|
-
|
|
30433
|
-
|
|
30434
|
-
|
|
30435
|
-
|
|
30436
|
-
|
|
30437
|
-
|
|
30438
|
-
onMouseLeave: () => setTip(null)
|
|
30439
|
-
}),
|
|
30440
|
-
(i % labelEvery === 0 || i === n - 1) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
30441
|
-
className: TokenTotalPanel_module_default.axis,
|
|
30442
|
-
x: padL + barHalf + i * step,
|
|
30443
|
-
y: 212,
|
|
30444
|
-
textAnchor: "middle",
|
|
30445
|
-
children: bucketAxisLabel(d.day, granularity)
|
|
30446
|
-
})
|
|
30447
|
-
] }, d.day);
|
|
30565
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [bars.map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
30566
|
+
className: clsx(TokenTotalPanel_module_default.bar, b.dimmed && TokenTotalPanel_module_default.barDim),
|
|
30567
|
+
x: b.x,
|
|
30568
|
+
y: b.y,
|
|
30569
|
+
width: barW,
|
|
30570
|
+
height: b.h,
|
|
30571
|
+
fill: b.color,
|
|
30572
|
+
style: hovered ? { transform: `scaleX(${(barW + 3) / barW})` } : void 0
|
|
30573
|
+
}, `${d.day}-${b.model}`)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
30574
|
+
className: TokenTotalPanel_module_default.barHit,
|
|
30575
|
+
x,
|
|
30576
|
+
y: padT,
|
|
30577
|
+
width: barW,
|
|
30578
|
+
height: plotH,
|
|
30579
|
+
onMouseEnter: (e) => {
|
|
30580
|
+
setTip({
|
|
30581
|
+
day: d.day,
|
|
30582
|
+
total: d.total,
|
|
30583
|
+
byModel: d.byModel,
|
|
30584
|
+
otherByModel: d.otherByModel,
|
|
30585
|
+
cacheHit: d.cacheHit,
|
|
30586
|
+
cacheMiss: d.cacheMiss,
|
|
30587
|
+
anchor: e.currentTarget
|
|
30588
|
+
});
|
|
30589
|
+
},
|
|
30590
|
+
onMouseLeave: () => setTip(null)
|
|
30591
|
+
})] }, d.day);
|
|
30448
30592
|
}),
|
|
30593
|
+
labelGroups.map((g, k) => labeledGroups.has(k) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
30594
|
+
className: TokenTotalPanel_module_default.axis,
|
|
30595
|
+
x: padL + barHalf + (g.start + g.end) / 2 * step,
|
|
30596
|
+
y: 212,
|
|
30597
|
+
textAnchor: "middle",
|
|
30598
|
+
children: bucketAxisLabel(visible[g.start].day, axisMode)
|
|
30599
|
+
}, `axis-${k}`)),
|
|
30449
30600
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
30450
30601
|
className: TokenTotalPanel_module_default.trend,
|
|
30451
30602
|
d: trendPath,
|
|
@@ -30522,17 +30673,17 @@ window.__ModuleLoader__.load({
|
|
|
30522
30673
|
function providerColor(index) {
|
|
30523
30674
|
return index < 5 ? `var(--dsw-provider-${index + 1})` : "var(--dsw-provider-other)";
|
|
30524
30675
|
}
|
|
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 }) {
|
|
30676
|
+
function UsageBreakdown({ models, providers, total, providerNames, t, panelRef, colorForModel }) {
|
|
30530
30677
|
const [tip, setTip] = (0, react.useState)(null);
|
|
30531
30678
|
const providerLabel = (0, react.useCallback)((provider) => {
|
|
30532
30679
|
const named = providerNames?.[provider];
|
|
30533
30680
|
return typeof named === "string" && named.trim() !== "" ? named : provider;
|
|
30534
30681
|
}, [providerNames]);
|
|
30535
30682
|
const shareText = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
|
|
30683
|
+
const providerColorOf = (0, react.useCallback)((provider) => {
|
|
30684
|
+
const slot = providers.findIndex((p) => p.provider === provider);
|
|
30685
|
+
return slot < 0 ? "var(--dsw-provider-other)" : providerColor(slot);
|
|
30686
|
+
}, [providers]);
|
|
30536
30687
|
const modelRows = (0, react.useMemo)(() => {
|
|
30537
30688
|
const byName = /* @__PURE__ */ new Map();
|
|
30538
30689
|
for (const m of models) {
|
|
@@ -30551,19 +30702,19 @@ window.__ModuleLoader__.load({
|
|
|
30551
30702
|
for (const provider of m.providers ?? (m.provider === "" ? [] : [m.provider])) acc.sources.add(provider);
|
|
30552
30703
|
byName.set(name, acc);
|
|
30553
30704
|
}
|
|
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]
|
|
30705
|
+
return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc]) => {
|
|
30706
|
+
const split = [...acc.split.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([provider, tokens]) => ({
|
|
30556
30707
|
key: provider,
|
|
30557
30708
|
name: providerLabel(provider),
|
|
30558
30709
|
tokens,
|
|
30559
|
-
color:
|
|
30710
|
+
color: providerColorOf(provider)
|
|
30560
30711
|
}));
|
|
30561
30712
|
const known = new Set(split.map((child) => child.key));
|
|
30562
|
-
const named = [...acc.sources].filter((provider) => !known.has(provider)).sort().map((provider
|
|
30713
|
+
const named = [...acc.sources].filter((provider) => !known.has(provider)).sort().map((provider) => ({
|
|
30563
30714
|
key: provider,
|
|
30564
30715
|
name: providerLabel(provider),
|
|
30565
30716
|
tokens: null,
|
|
30566
|
-
color:
|
|
30717
|
+
color: providerColorOf(provider)
|
|
30567
30718
|
}));
|
|
30568
30719
|
return {
|
|
30569
30720
|
key: name,
|
|
@@ -30571,7 +30722,7 @@ window.__ModuleLoader__.load({
|
|
|
30571
30722
|
tokens: acc.tokens,
|
|
30572
30723
|
cacheHit: acc.cacheHit,
|
|
30573
30724
|
cacheMiss: acc.cacheMiss,
|
|
30574
|
-
color:
|
|
30725
|
+
color: colorForModel(name),
|
|
30575
30726
|
breakdownLabel: t("byProvider"),
|
|
30576
30727
|
breakdown: [...split, ...named]
|
|
30577
30728
|
};
|
|
@@ -30579,10 +30730,12 @@ window.__ModuleLoader__.load({
|
|
|
30579
30730
|
}, [
|
|
30580
30731
|
models,
|
|
30581
30732
|
providerLabel,
|
|
30582
|
-
t
|
|
30733
|
+
t,
|
|
30734
|
+
colorForModel,
|
|
30735
|
+
providerColorOf
|
|
30583
30736
|
]);
|
|
30584
30737
|
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
|
|
30738
|
+
return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p) => {
|
|
30586
30739
|
const label = providerLabel(p.provider);
|
|
30587
30740
|
return {
|
|
30588
30741
|
key: p.provider,
|
|
@@ -30591,20 +30744,22 @@ window.__ModuleLoader__.load({
|
|
|
30591
30744
|
tokens: p.tokens,
|
|
30592
30745
|
cacheHit: p.cacheHit,
|
|
30593
30746
|
cacheMiss: p.cacheMiss,
|
|
30594
|
-
color:
|
|
30747
|
+
color: providerColorOf(p.provider),
|
|
30595
30748
|
breakdownLabel: t("byModel"),
|
|
30596
|
-
breakdown: [...p.modelTokens ?? []].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)).map((m
|
|
30749
|
+
breakdown: [...p.modelTokens ?? []].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)).map((m) => ({
|
|
30597
30750
|
key: m.model,
|
|
30598
30751
|
name: m.model,
|
|
30599
30752
|
tokens: m.tokens,
|
|
30600
|
-
color:
|
|
30753
|
+
color: colorForModel(m.model)
|
|
30601
30754
|
}))
|
|
30602
30755
|
};
|
|
30603
30756
|
});
|
|
30604
30757
|
}, [
|
|
30605
30758
|
providers,
|
|
30606
30759
|
providerLabel,
|
|
30607
|
-
t
|
|
30760
|
+
t,
|
|
30761
|
+
colorForModel,
|
|
30762
|
+
providerColorOf
|
|
30608
30763
|
]);
|
|
30609
30764
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
30610
30765
|
className: TokenTotalPanel_module_default.section,
|
|
@@ -30731,13 +30886,27 @@ window.__ModuleLoader__.load({
|
|
|
30731
30886
|
}),
|
|
30732
30887
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30733
30888
|
className: TokenTotalPanel_module_default.modelId,
|
|
30734
|
-
children: [
|
|
30735
|
-
|
|
30736
|
-
|
|
30737
|
-
|
|
30738
|
-
|
|
30739
|
-
|
|
30740
|
-
|
|
30889
|
+
children: [
|
|
30890
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30891
|
+
className: TokenTotalPanel_module_default.modelName,
|
|
30892
|
+
children: r.name
|
|
30893
|
+
}),
|
|
30894
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30895
|
+
className: TokenTotalPanel_module_default.modelProvider,
|
|
30896
|
+
children: share(r.tokens)
|
|
30897
|
+
}),
|
|
30898
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30899
|
+
className: TokenTotalPanel_module_default.rowTrack,
|
|
30900
|
+
"aria-hidden": "true",
|
|
30901
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30902
|
+
className: TokenTotalPanel_module_default.rowFill,
|
|
30903
|
+
style: {
|
|
30904
|
+
width: total > 0 ? `${Math.min(100, r.tokens / total * 100)}%` : "0%",
|
|
30905
|
+
background: r.color
|
|
30906
|
+
}
|
|
30907
|
+
})
|
|
30908
|
+
})
|
|
30909
|
+
]
|
|
30741
30910
|
}),
|
|
30742
30911
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30743
30912
|
className: TokenTotalPanel_module_default.modelValues,
|
|
@@ -31348,6 +31517,11 @@ window.__ModuleLoader__.load({
|
|
|
31348
31517
|
function billedTotal(totals) {
|
|
31349
31518
|
return totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens + totals.outputTokens;
|
|
31350
31519
|
}
|
|
31520
|
+
function cacheHitPercentOf(totals) {
|
|
31521
|
+
const hit = totals.cacheReadTokens;
|
|
31522
|
+
const miss = totals.uncachedInputTokens + totals.cacheWriteTokens;
|
|
31523
|
+
return hit + miss === 0 ? null : hit / (hit + miss) * 100;
|
|
31524
|
+
}
|
|
31351
31525
|
//#endregion
|
|
31352
31526
|
//#region src/client/use-session-usage.ts
|
|
31353
31527
|
/** The composer row's window onto the whole session tree. The native usage
|
|
@@ -31402,13 +31576,13 @@ window.__ModuleLoader__.load({
|
|
|
31402
31576
|
}
|
|
31403
31577
|
//#endregion
|
|
31404
31578
|
//#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/StatsLineEnhanced.module.css?inline
|
|
31405
|
-
const css$
|
|
31406
|
-
const tagId$
|
|
31407
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$
|
|
31579
|
+
const css$2 = "/* Session stats pills under the composer (shadow of the official StatsPills):\n a gauge pill (counts + output speed, click opens the time dialog) and a\n database pill (total tokens + cache hit, click opens the usage dialog).\n Replicated from the official StatsPills.module_348665330.css_348665330 + stat-dialog.module_348665330.css_348665330\n (13/20 tertiary tier, hover-pill affordance, shared dialog skin).\n\n Host contract: the row serves two containers at once, and picks between them\n at attach time — the component marks which one it landed in (see\n StatsLineEnhanced's `markDockContainer`):\n - through 0.1.6-alpha.1 the dock slot renders straight into InputBar's own\n column, and this row owns the content-width clamp, its side gutters and\n its 4px top pad — the official StatsPills.module_348665330.css_348665330 of that line;\n - 0.1.6-alpha.2 renders it inside InputBar's flex `.dock_348665330` beside the resident\n ContextMeter, which took all three jobs over, so the row only has to\n shrink inside it (the official stylesheet's current form).\n Both forms are kept side by side because one published build has to render\n correctly on either host line. */\n\n.root_348665330 {\n display: flex;\n justify-content: center;\n gap: 12px;\n box-sizing: border-box;\n /* <= 0.1.6-alpha.1: this row is the composer column's content-width box. */\n width: 100%;\n max-width: var(--dsh-chat-content-width);\n margin: 0 auto;\n padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n}\n\n/* 0.1.6-alpha.2+: InputBar's `.dock_348665330` owns the centring, the 12px gap, the top\n pad and the side clearance. */\n.root_348665330[data-dock-row] {\n width: auto;\n min-width: 0;\n max-width: 100%;\n margin: 0;\n padding: 0;\n}\n\n/* Dialog anchor: shrink-wraps its pill so the panel clamp measures the pill. */\n.anchor_348665330 {\n display: inline-flex;\n min-width: 0;\n}\n\n/* Split-mode anchor (DSH >= 0.2.1-alpha.1). The host split the single `stats`\n dock entry into one entry per pill and let its own `.dock_348665330` flex row own the\n layout, so the anchor IS the entry root and carries the text tier `.root_348665330`\n used to set — the official StatsPills.module_348665330.css_348665330 moved exactly this pair of\n declarations from `.root_348665330` onto `.anchor_348665330`. The tier stays off the plain\n `.anchor_348665330` because the pre-split row must keep its own `.root_348665330` tier. */\n.dockAnchor_348665330 {\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n}\n\n.pill_348665330 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n box-sizing: border-box;\n max-width: 100%;\n padding: 1px 8px;\n border: none;\n border-radius: var(--dsw-radius-sm);\n background: none;\n color: var(--dsw-alias-label-tertiary);\n font: inherit;\n font-variant-numeric: tabular-nums;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.pill_348665330 svg {\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n/* Only the button form (a pill with dialog rows to open) invites interaction;\n the static span form keeps the resting tertiary reading. */\nbutton.pill_348665330 {\n cursor: pointer;\n}\n\nbutton.pill_348665330:hover,\nbutton.pill_348665330[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.label_348665330 {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sep_348665330 {\n color: var(--dsw-alias-separator-primary);\n margin: 0 6px;\n}\n\n/* The live throughput reading sits inline exactly like the official figure:\n no reserved width, no box of its own. The earlier `min-width: 10ch` slot kept\n the centred row from shifting while the number grew, but it also left a\n permanent gap before the separator — the official pill has no such slot, and\n native parity wins over that jitter fix. */\n.speed_348665330 {\n display: inline;\n}\n\n/* Portal dialog surface: fixed in the viewport, left/top supplied inline from\n the anchored-position clamp (12px viewport margin, layers above overlays).\n Material = the official stat-dialog PAIRING, which dsh 0.1.7's menu\n redesign made mandatory: `--dsw-specific-menu` is translucent there\n (rgba 58%/45%), so the blur token must ride along — the 0.1.5-era recipe\n this file copied was a single opaque line, and a fill-only copy reads as\n bare transparency over the page (issue #6). */\n.dialogPanel_348665330 {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 12px;\n border: 0;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n\n/* Dialog heading row: the section name left, the headline value right. */\n.dialogTitle_348665330 {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n/* Rule under the heading, above its rows. */\n.dialogTitleRule_348665330 {\n margin-bottom: 8px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n\n.dialogTitleValue_348665330 {\n font-variant-numeric: tabular-nums;\n}\n\n/* Heading glyph seated left of the section name. */\n.dialogTitleLabel_348665330 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.dialogTitleLabel_348665330 svg {\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n.dialogDetails_348665330 {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 4px 12px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dialogDetails_348665330 dt,\n.dialogDetails_348665330 dd {\n min-width: 0;\n margin: 0;\n}\n\n.dialogDetails_348665330 dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n";
|
|
31580
|
+
const tagId$2 = "dsh-token-total/StatsLineEnhanced.module.css";
|
|
31581
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
|
|
31408
31582
|
const tag = document.createElement("style");
|
|
31409
31583
|
tag.dataset.plugin = "dsh-token-total";
|
|
31410
|
-
tag.dataset.pluginCss = tagId$
|
|
31411
|
-
tag.textContent = css$
|
|
31584
|
+
tag.dataset.pluginCss = tagId$2;
|
|
31585
|
+
tag.textContent = css$2;
|
|
31412
31586
|
document.head.appendChild(tag);
|
|
31413
31587
|
}
|
|
31414
31588
|
var StatsLineEnhanced_module_default = {
|
|
@@ -31888,13 +32062,13 @@ window.__ModuleLoader__.load({
|
|
|
31888
32062
|
}
|
|
31889
32063
|
//#endregion
|
|
31890
32064
|
//#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/ContextUsageEntry.module.css?inline
|
|
31891
|
-
const css = "/* Mirror the host ContextMeter, including its theme and font-size tokens. */\n.root_1877794043 {\n flex: none;\n display: inline-flex;\n}\n\n.trigger_1877794043 {\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-tertiary);\n font-family: inherit;\n font-size: var(--dsh-content-font-size-secondary, 13px);\n font-variant-numeric: tabular-nums;\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n white-space: nowrap;\n cursor: pointer;\n background: transparent;\n border: none;\n flex: none;\n align-items: center;\n gap: 6px;\n padding: 1px 8px;\n display: inline-flex;\n}\n\n.trigger_1877794043:hover,\n.trigger_1877794043[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.track_1877794043 {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2px;\n}\n\n.fill_1877794043 {\n fill: none;\n stroke: var(--dsw-alias-label-tertiary);\n stroke-width: 2px;\n stroke-linecap: round;\n}\n\n.panel_1877794043 {\n z-index: 1100;\n box-sizing: border-box;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu);\n width: min(264px, calc(100vw - 24px));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n border: 0;\n padding: 12px;\n font-size: 12px;\n line-height: 20px;\n position: fixed;\n}\n\n.header_1877794043 {\n align-items: center;\n gap: 6px;\n display: flex;\n}\n\n.figures_1877794043 {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n margin-left: auto;\n font-weight: 500;\n}\n\n.percent_1877794043 {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.headline_1877794043 { color: var(--dsw-alias-label-tertiary); }\n.headline_1877794043:empty { display: none; }\n\n.bar_1877794043 {\n corner-shape: round;\n background: var(--dsw-alias-interactive-bg-hover);\n border-radius: 999px;\n gap: 1px;\n height: 4px;\n margin: 10px 0 12px;\n display: flex;\n overflow: hidden;\n}\n\n.segment_1877794043 {\n background: var(--meter-tint, var(--dsw-alias-label-tertiary));\n border-radius: 1px;\n flex: none;\n min-width: 2px;\n height: 100%;\n}\n\n.swatch_1877794043 {\n background: var(--meter-tint);\n vertical-align: baseline;\n border-radius: 2px;\n width: 8px;\n height: 8px;\n margin-right: 6px;\n display: inline-block;\n}\n\n.colorSystem_1877794043 { --meter-tint: var(--dsw-static-neutral-bluish-400); }\n.colorTools_1877794043 { --meter-tint: #a78bfa; }\n.colorMessages_1877794043 { --meter-tint: var(--dsw-static-blue-450); }\n.rows_1877794043 { margin: 6px 0 0; }\n\n.row_1877794043 {\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n padding: 2px 0;\n display: flex;\n}\n\n.row_1877794043 dt { color: var(--dsw-alias-label-secondary); }\n.row_1877794043 dd {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n margin: 0;\n}\n";
|
|
31892
|
-
const tagId = "dsh-token-total/ContextUsageEntry.module.css";
|
|
31893
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
32065
|
+
const css$1 = "/* Mirror the host ContextMeter, including its theme and font-size tokens. */\n.root_1877794043 {\n flex: none;\n display: inline-flex;\n}\n\n.trigger_1877794043 {\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-tertiary);\n font-family: inherit;\n font-size: var(--dsh-content-font-size-secondary, 13px);\n font-variant-numeric: tabular-nums;\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n white-space: nowrap;\n cursor: pointer;\n background: transparent;\n border: none;\n flex: none;\n align-items: center;\n gap: 6px;\n padding: 1px 8px;\n display: inline-flex;\n}\n\n.trigger_1877794043:hover,\n.trigger_1877794043[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.track_1877794043 {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2px;\n}\n\n.fill_1877794043 {\n fill: none;\n stroke: var(--dsw-alias-label-tertiary);\n stroke-width: 2px;\n stroke-linecap: round;\n}\n\n.panel_1877794043 {\n z-index: 1100;\n box-sizing: border-box;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu);\n width: min(264px, calc(100vw - 24px));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n border: 0;\n padding: 12px;\n font-size: 12px;\n line-height: 20px;\n position: fixed;\n}\n\n.header_1877794043 {\n align-items: center;\n gap: 6px;\n display: flex;\n}\n\n.figures_1877794043 {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n margin-left: auto;\n font-weight: 500;\n}\n\n.percent_1877794043 {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.headline_1877794043 { color: var(--dsw-alias-label-tertiary); }\n.headline_1877794043:empty { display: none; }\n\n.bar_1877794043 {\n corner-shape: round;\n background: var(--dsw-alias-interactive-bg-hover);\n border-radius: 999px;\n gap: 1px;\n height: 4px;\n margin: 10px 0 12px;\n display: flex;\n overflow: hidden;\n}\n\n.segment_1877794043 {\n background: var(--meter-tint, var(--dsw-alias-label-tertiary));\n border-radius: 1px;\n flex: none;\n min-width: 2px;\n height: 100%;\n}\n\n.swatch_1877794043 {\n background: var(--meter-tint);\n vertical-align: baseline;\n border-radius: 2px;\n width: 8px;\n height: 8px;\n margin-right: 6px;\n display: inline-block;\n}\n\n.colorSystem_1877794043 { --meter-tint: var(--dsw-static-neutral-bluish-400); }\n.colorTools_1877794043 { --meter-tint: #a78bfa; }\n.colorMessages_1877794043 { --meter-tint: var(--dsw-static-blue-450); }\n.rows_1877794043 { margin: 6px 0 0; }\n\n.row_1877794043 {\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n padding: 2px 0;\n display: flex;\n}\n\n.row_1877794043 dt { color: var(--dsw-alias-label-secondary); }\n.row_1877794043 dd {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n margin: 0;\n}\n";
|
|
32066
|
+
const tagId$1 = "dsh-token-total/ContextUsageEntry.module.css";
|
|
32067
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$1 + "\"]") === null) {
|
|
31894
32068
|
const tag = document.createElement("style");
|
|
31895
32069
|
tag.dataset.plugin = "dsh-token-total";
|
|
31896
|
-
tag.dataset.pluginCss = tagId;
|
|
31897
|
-
tag.textContent = css;
|
|
32070
|
+
tag.dataset.pluginCss = tagId$1;
|
|
32071
|
+
tag.textContent = css$1;
|
|
31898
32072
|
document.head.appendChild(tag);
|
|
31899
32073
|
}
|
|
31900
32074
|
var ContextUsageEntry_module_default = {
|
|
@@ -32169,6 +32343,25 @@ window.__ModuleLoader__.load({
|
|
|
32169
32343
|
heatmap: "Activity heatmap",
|
|
32170
32344
|
heatLess: "Less",
|
|
32171
32345
|
heatMore: "More",
|
|
32346
|
+
"dow.mon": "Mon",
|
|
32347
|
+
"dow.tue": "Tue",
|
|
32348
|
+
"dow.wed": "Wed",
|
|
32349
|
+
"dow.thu": "Thu",
|
|
32350
|
+
"dow.fri": "Fri",
|
|
32351
|
+
"dow.sat": "Sat",
|
|
32352
|
+
"dow.sun": "Sun",
|
|
32353
|
+
"month.jan": "Jan",
|
|
32354
|
+
"month.feb": "Feb",
|
|
32355
|
+
"month.mar": "Mar",
|
|
32356
|
+
"month.apr": "Apr",
|
|
32357
|
+
"month.may": "May",
|
|
32358
|
+
"month.jun": "Jun",
|
|
32359
|
+
"month.jul": "Jul",
|
|
32360
|
+
"month.aug": "Aug",
|
|
32361
|
+
"month.sep": "Sep",
|
|
32362
|
+
"month.oct": "Oct",
|
|
32363
|
+
"month.nov": "Nov",
|
|
32364
|
+
"month.dec": "Dec",
|
|
32172
32365
|
dailyTrend: "Daily token trend",
|
|
32173
32366
|
hourlyTrend: "Hourly token trend",
|
|
32174
32367
|
trendLimited: "Showing the latest {n} days",
|
|
@@ -32238,6 +32431,25 @@ window.__ModuleLoader__.load({
|
|
|
32238
32431
|
heatmap: "活跃热力图",
|
|
32239
32432
|
heatLess: "较少",
|
|
32240
32433
|
heatMore: "较多",
|
|
32434
|
+
"dow.mon": "周一",
|
|
32435
|
+
"dow.tue": "周二",
|
|
32436
|
+
"dow.wed": "周三",
|
|
32437
|
+
"dow.thu": "周四",
|
|
32438
|
+
"dow.fri": "周五",
|
|
32439
|
+
"dow.sat": "周六",
|
|
32440
|
+
"dow.sun": "周日",
|
|
32441
|
+
"month.jan": "1月",
|
|
32442
|
+
"month.feb": "2月",
|
|
32443
|
+
"month.mar": "3月",
|
|
32444
|
+
"month.apr": "4月",
|
|
32445
|
+
"month.may": "5月",
|
|
32446
|
+
"month.jun": "6月",
|
|
32447
|
+
"month.jul": "7月",
|
|
32448
|
+
"month.aug": "8月",
|
|
32449
|
+
"month.sep": "9月",
|
|
32450
|
+
"month.oct": "10月",
|
|
32451
|
+
"month.nov": "11月",
|
|
32452
|
+
"month.dec": "12月",
|
|
32241
32453
|
dailyTrend: "按天 Token 趋势",
|
|
32242
32454
|
hourlyTrend: "按小时 Token 趋势",
|
|
32243
32455
|
trendLimited: "仅显示最近 {n} 天",
|
|
@@ -32307,6 +32519,25 @@ window.__ModuleLoader__.load({
|
|
|
32307
32519
|
heatmap: "活躍熱力圖",
|
|
32308
32520
|
heatLess: "較少",
|
|
32309
32521
|
heatMore: "較多",
|
|
32522
|
+
"dow.mon": "週一",
|
|
32523
|
+
"dow.tue": "週二",
|
|
32524
|
+
"dow.wed": "週三",
|
|
32525
|
+
"dow.thu": "週四",
|
|
32526
|
+
"dow.fri": "週五",
|
|
32527
|
+
"dow.sat": "週六",
|
|
32528
|
+
"dow.sun": "週日",
|
|
32529
|
+
"month.jan": "1月",
|
|
32530
|
+
"month.feb": "2月",
|
|
32531
|
+
"month.mar": "3月",
|
|
32532
|
+
"month.apr": "4月",
|
|
32533
|
+
"month.may": "5月",
|
|
32534
|
+
"month.jun": "6月",
|
|
32535
|
+
"month.jul": "7月",
|
|
32536
|
+
"month.aug": "8月",
|
|
32537
|
+
"month.sep": "9月",
|
|
32538
|
+
"month.oct": "10月",
|
|
32539
|
+
"month.nov": "11月",
|
|
32540
|
+
"month.dec": "12月",
|
|
32310
32541
|
dailyTrend: "每日 Token 趨勢",
|
|
32311
32542
|
hourlyTrend: "每小時 Token 趨勢",
|
|
32312
32543
|
trendLimited: "僅顯示最近 {n} 天",
|
|
@@ -32397,6 +32628,904 @@ window.__ModuleLoader__.load({
|
|
|
32397
32628
|
return glass !== null && glass.bridgeId === "deepseek-harness-background" && glass.version === 1;
|
|
32398
32629
|
}
|
|
32399
32630
|
//#endregion
|
|
32631
|
+
//#region src/client/use-session-detail.ts
|
|
32632
|
+
/** Serialized polling for one visible Usage view, never leaking another
|
|
32633
|
+
* session's answer. Abort and stop all timers when the tab unmounts. */
|
|
32634
|
+
const DETAIL_POLL_MS = 1500;
|
|
32635
|
+
function useSessionDetail(sessionId) {
|
|
32636
|
+
const [reading, setReading] = (0, react.useState)(null);
|
|
32637
|
+
const refresh = (0, react.useRef)(null);
|
|
32638
|
+
(0, react.useEffect)(() => {
|
|
32639
|
+
let active = true;
|
|
32640
|
+
let pending = false;
|
|
32641
|
+
let timer;
|
|
32642
|
+
let controller;
|
|
32643
|
+
const request = () => {
|
|
32644
|
+
if (!active || pending || !sessionId) return;
|
|
32645
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
32646
|
+
pending = true;
|
|
32647
|
+
controller = new AbortController();
|
|
32648
|
+
fetchSessionDetail(sessionId, controller.signal).then((detail) => {
|
|
32649
|
+
if (active) setReading({
|
|
32650
|
+
sessionId,
|
|
32651
|
+
detail,
|
|
32652
|
+
error: false
|
|
32653
|
+
});
|
|
32654
|
+
}).catch(() => {
|
|
32655
|
+
if (active) setReading((previous) => ({
|
|
32656
|
+
sessionId,
|
|
32657
|
+
detail: previous?.sessionId === sessionId ? previous.detail : null,
|
|
32658
|
+
error: true
|
|
32659
|
+
}));
|
|
32660
|
+
}).finally(() => {
|
|
32661
|
+
pending = false;
|
|
32662
|
+
if (active) timer = setTimeout(request, DETAIL_POLL_MS);
|
|
32663
|
+
});
|
|
32664
|
+
};
|
|
32665
|
+
const wake = () => {
|
|
32666
|
+
if (document.visibilityState === "visible") request();
|
|
32667
|
+
};
|
|
32668
|
+
refresh.current = request;
|
|
32669
|
+
document.addEventListener("visibilitychange", wake);
|
|
32670
|
+
request();
|
|
32671
|
+
return () => {
|
|
32672
|
+
active = false;
|
|
32673
|
+
refresh.current = null;
|
|
32674
|
+
controller?.abort();
|
|
32675
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
32676
|
+
document.removeEventListener("visibilitychange", wake);
|
|
32677
|
+
};
|
|
32678
|
+
}, [sessionId]);
|
|
32679
|
+
const current = reading?.sessionId === sessionId ? reading : null;
|
|
32680
|
+
return {
|
|
32681
|
+
detail: current?.detail ?? null,
|
|
32682
|
+
loading: current === null,
|
|
32683
|
+
error: current?.error ?? false,
|
|
32684
|
+
retry: () => refresh.current?.()
|
|
32685
|
+
};
|
|
32686
|
+
}
|
|
32687
|
+
//#endregion
|
|
32688
|
+
//#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/SessionUsageView.module.css?inline
|
|
32689
|
+
const css = "/* A light, self-contained Usage surface. Tables fit the available width:\n * no horizontal scroll containers, fixed column contracts, titles for names.\n * Stat cards follow the reference dashboard: tinted left edge + icon chip,\n * label, big value, small subtitle. Request log folds by conversation turn. */\n.scroll_900471699 { height:100%; min-height:0; overflow-y:auto; background:#ffffff; color:#252936; color-scheme:light; container-type:inline-size; }\n.page_900471699 { box-sizing:border-box; width:100%; max-width:1160px; margin:0 auto; padding:24px 26px 32px; font-family:system-ui,-apple-system,\"Segoe UI\",sans-serif; font-size:13px; }\n.page_900471699 * { box-sizing:border-box; }\n.scope_900471699 { color:#8790a3; font-size:11px; margin:0 0 14px; }\n.stats_900471699 { display:grid; gap:12px; margin-bottom:18px; }\n.statRow_900471699 { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; }\n.stat_900471699 { min-width:0; position:relative; overflow:hidden; background:#fff; border:1px solid #e4e7ee; border-left:4px solid var(--tone); border-radius:12px; padding:12px 16px 11px; }\n.statHead_900471699 { display:flex; align-items:center; gap:8px; margin-bottom:8px; }\n.statIcon_900471699 { display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:10px; color:#fff; background:linear-gradient(135deg, var(--tone), var(--tone-soft)); flex-shrink:0; box-shadow:0 3px 8px var(--tone-soft); }\n.statMark_900471699 { position:absolute; right:-14px; bottom:-18px; width:92px; height:92px; color:var(--tone); pointer-events:none; }\n.statLabel_900471699 { font-size:12px; color:#7d8495; }\n.statValue_900471699 { display:block; font-size:25px; font-weight:650; letter-spacing:-0.5px; white-space:nowrap; font-variant-numeric:tabular-nums; }\n.statSub_900471699 { display:block; margin-top:5px; font-size:11px; color:#939aac; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.toneBlue_900471699 { --tone:#4f79f6; --tone-soft:#eaf0fe; }\n.tonePurple_900471699 { --tone:#8b5cf6; --tone-soft:#f1ebfe; }\n.toneGreen_900471699 { --tone:#18a97b; --tone-soft:#e5f6f0; }\n.toneOrange_900471699 { --tone:#e8892c; --tone-soft:#fcf1e4; }\n.toneTeal_900471699 { --tone:#16a4b8; --tone-soft:#e4f5f8; }\n.tonePink_900471699 { --tone:#d9538c; --tone-soft:#fbeaf1; }\n.card_900471699 { min-width:0; background:#fff; border:1px solid #e4e7ee; border-radius:12px; margin-bottom:18px; overflow:hidden; }\n.cardHead_900471699 { display:flex; align-items:center; justify-content:space-between; gap:10px; min-height:54px; padding:15px 18px; border-bottom:1px solid #eef0f4; }\n.cardHead_900471699 h2 { font-size:14px; font-weight:600; margin:0; }\n.cardHead_900471699 > span { display:flex; gap:7px; align-items:center; font-size:11px; }\n.count_900471699 { display:inline-flex; align-items:center; justify-content:center; min-width:21px; padding:2px 6px; font-size:10px; color:#8b93a3; background:#f3f5f8; border-radius:5px; margin-left:5px; font-weight:500; }\n.mid_900471699 { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:18px; align-items:stretch; }\n.mid_900471699 .card_900471699 { height:100%; margin-bottom:0; }\n.mid_900471699 { margin-bottom:18px; }\n.table_900471699 { border-collapse:collapse; width:100%; table-layout:fixed; font-size:11px; }\n.table_900471699 th { color:#929aaa; font-weight:500; padding:12px 5px; text-align:right; vertical-align:middle; line-height:1.3; overflow-wrap:anywhere; }\n.table_900471699 td { padding:13px 5px; text-align:right; border-top:1px solid #eef0f4; color:#626c80; font-variant-numeric:tabular-nums; white-space:nowrap; }\n.table_900471699 th:first-child,.table_900471699 td:first-child { text-align:left; padding-left:16px; }\n.table_900471699 th:last-child,.table_900471699 td:last-child { padding-right:16px; }\n.childName_900471699 { width:28%; }\n.childRequests_900471699 { width:11%; }\n.modelName_900471699 { width:30%; }\n.modelRequests_900471699 { width:11%; }\n.modelHit_900471699 { width:16%; }\n.ellipsis_900471699 { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#394354; }\n/* Gray provider label under a model name shown by several rows (同名模型). */\n.modelProvider_900471699 { display:block; margin-top:2px; font-size:10px; color:#939aaa; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.number_900471699 { font-variant-numeric:tabular-nums; }\n.speedCell_900471699 { display:inline-flex; align-items:baseline; gap:3px; white-space:nowrap; }\n.speedCell_900471699 small { font-size:9px; color:#929aaa; }\n.good_900471699 { color:#21a17b; }\n.warn_900471699 { color:#d69a21; }\n.bad_900471699 { color:#e06571; }\n.live_900471699 { color:#169bb3; }\n.muted_900471699 { color:#939aaa; }\n.empty_900471699 { margin:0; padding:28px 18px; color:#9aa2b1; font-size:12px; text-align:center; }\n.action_900471699 { color:#818b9e; background:none; border:0; padding:3px 0; font:inherit; font-size:11px; cursor:pointer; }\n.action_900471699:hover { color:#279cb6; }\n.action_900471699:focus-visible,.turnHead_900471699:focus-visible { outline:2px solid #2ab1c6; outline-offset:-2px; }\n.turnList_900471699 { padding:0 14px 14px; }\n.turn_900471699 { margin-top:10px; border:1px solid #e7eaf0; border-radius:9px; background:#fff; overflow:hidden; }\n.turnHead_900471699 { display:grid; grid-template-columns:12px minmax(0,1fr) auto; align-items:center; gap:12px; padding:13px 14px; width:100%; text-align:left; background:#f8f9fc; border:0; cursor:pointer; color:#616c7f; font:inherit; font-size:11px; }\n.chevron_900471699 { color:#99a1af; transition:transform 0.2s; }\n.rotated_900471699 { transform:rotate(90deg); }\n.turnTitle_900471699 { display:flex; align-items:center; gap:6px; min-width:0; font-size:12px; font-weight:600; color:#394354; }\n/* Turn topic is the group title now (the page header owns the session name),\n * so it carries the primary text color and truncates before the metrics. */\n.turnTopic_900471699 { min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }\n/* A subagent's turns look like this session's turns apart from the agent tag;\n * the tag keeps the full session name in its tooltip. */\n.turnAgent_900471699 { flex:none; padding:1px 6px; border-radius:5px; background:#e9edf4; color:#6f7889; font-size:10px; font-weight:500; }\n.turnSummary_900471699 { display:flex; gap:5px; align-items:center; white-space:nowrap; }\n.turnSummary_900471699 > span:last-child { margin-left:8px; }\n.turnBody_900471699 { border-top:1px solid #eef0f4; }\n.turnBody_900471699[hidden] { display:none; }\n/* A request row is one line: time, what answered, and its metrics pinned to the\n * row end. The metrics never wrap; they truncate with a tooltip before the head\n * gives up its model name. */\n.turnRequest_900471699 { display:grid; grid-template-columns:64px minmax(0,1fr) auto; align-items:center; gap:12px; padding:10px 16px; border-top:1px solid #f3f5f9; }\n.turnRequest_900471699:first-child { border-top:0; }\n.turnRequestHead_900471699 { display:flex; align-items:center; gap:8px; font-size:11px; color:#616c7f; min-width:0; }\n.requestTime_900471699 { color:#9aa3b3; font-variant-numeric:tabular-nums; flex-shrink:0; }\n.statusDot_900471699 { width:7px; height:7px; border-radius:50%; background:currentColor; flex-shrink:0; }\n.routeDot_900471699 { width:6px; height:6px; border-radius:50%; background:#21a17b; flex-shrink:0; }\n.whale_900471699 { color:#4d6bfe; flex-shrink:0; }\n.recordModel_900471699 { min-width:0; display:flex; align-items:center; gap:6px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:#394354; font-weight:500; }\n.routeText_900471699 { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:#8b93a4; flex-shrink:1; }\n.effort_900471699 { white-space:nowrap; color:#9aa3b3; background:#f1f3f7; border-radius:5px; padding:1px 6px; }\n.recordMeta_900471699 { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:right; font-size:10px; color:#949cab; font-variant-numeric:tabular-nums; }\n.note_900471699 { font-size:10px; line-height:1.7; color:#9aa3b3; margin:0; }\n.error_900471699 { background:#fff4f1; color:#b86751; border:1px solid #f4d8ce; border-radius:9px; padding:12px 16px; margin-bottom:16px; font-size:12px; display:flex; gap:16px; justify-content:space-between; }\n@container (max-width:1050px) {\n .mid_900471699 { grid-template-columns:minmax(0,1fr); }\n .statRow_900471699 { grid-template-columns:repeat(2,minmax(0,1fr)); }\n .statValue_900471699 { font-size:23px; }\n .table_900471699 { font-size:12px; }\n .childName_900471699 { width:34%; }\n}\n@container (max-width:700px) {\n .page_900471699 { padding:18px 14px 24px; }\n .stat_900471699 { padding:12px 12px 11px; }\n .statValue_900471699 { font-size:20px; }\n .table_900471699 { font-size:10px; }\n .table_900471699 th,.table_900471699 td { padding:10px 3px; }\n .table_900471699 th:first-child,.table_900471699 td:first-child { padding-left:10px; }\n .table_900471699 th:last-child,.table_900471699 td:last-child { padding-right:10px; }\n .childName_900471699 { width:25%; }\n .childRequests_900471699 { width:11%; }\n .modelName_900471699 { width:25%; }\n .modelRequests_900471699 { width:11%; }\n .modelHit_900471699 { width:17%; }\n .turnHead_900471699 { gap:8px; padding:11px 10px; }\n .turnSummary_900471699 { grid-column:2; font-size:10px; }\n .turnRequest_900471699 { grid-template-columns:56px minmax(120px,1fr) auto; gap:8px; padding:10px; }\n .turnRequestHead_900471699 { gap:6px; }\n .routeText_900471699 { display:none; }\n}\n@media (prefers-reduced-motion:reduce) {\n .chevron_900471699 { transition:none; }\n}\n";
|
|
32690
|
+
const tagId = "dsh-token-total/SessionUsageView.module.css";
|
|
32691
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
32692
|
+
const tag = document.createElement("style");
|
|
32693
|
+
tag.dataset.plugin = "dsh-token-total";
|
|
32694
|
+
tag.dataset.pluginCss = tagId;
|
|
32695
|
+
tag.textContent = css;
|
|
32696
|
+
document.head.appendChild(tag);
|
|
32697
|
+
}
|
|
32698
|
+
var SessionUsageView_module_default = {
|
|
32699
|
+
"scroll": "scroll_900471699",
|
|
32700
|
+
"page": "page_900471699",
|
|
32701
|
+
"scope": "scope_900471699",
|
|
32702
|
+
"stats": "stats_900471699",
|
|
32703
|
+
"statRow": "statRow_900471699",
|
|
32704
|
+
"stat": "stat_900471699",
|
|
32705
|
+
"statHead": "statHead_900471699",
|
|
32706
|
+
"statIcon": "statIcon_900471699",
|
|
32707
|
+
"statMark": "statMark_900471699",
|
|
32708
|
+
"statLabel": "statLabel_900471699",
|
|
32709
|
+
"statValue": "statValue_900471699",
|
|
32710
|
+
"statSub": "statSub_900471699",
|
|
32711
|
+
"toneBlue": "toneBlue_900471699",
|
|
32712
|
+
"tonePurple": "tonePurple_900471699",
|
|
32713
|
+
"toneGreen": "toneGreen_900471699",
|
|
32714
|
+
"toneOrange": "toneOrange_900471699",
|
|
32715
|
+
"toneTeal": "toneTeal_900471699",
|
|
32716
|
+
"tonePink": "tonePink_900471699",
|
|
32717
|
+
"card": "card_900471699",
|
|
32718
|
+
"cardHead": "cardHead_900471699",
|
|
32719
|
+
"count": "count_900471699",
|
|
32720
|
+
"mid": "mid_900471699",
|
|
32721
|
+
"table": "table_900471699",
|
|
32722
|
+
"childName": "childName_900471699",
|
|
32723
|
+
"childRequests": "childRequests_900471699",
|
|
32724
|
+
"modelName": "modelName_900471699",
|
|
32725
|
+
"modelRequests": "modelRequests_900471699",
|
|
32726
|
+
"modelHit": "modelHit_900471699",
|
|
32727
|
+
"ellipsis": "ellipsis_900471699",
|
|
32728
|
+
"modelProvider": "modelProvider_900471699",
|
|
32729
|
+
"number": "number_900471699",
|
|
32730
|
+
"speedCell": "speedCell_900471699",
|
|
32731
|
+
"good": "good_900471699",
|
|
32732
|
+
"warn": "warn_900471699",
|
|
32733
|
+
"bad": "bad_900471699",
|
|
32734
|
+
"live": "live_900471699",
|
|
32735
|
+
"muted": "muted_900471699",
|
|
32736
|
+
"empty": "empty_900471699",
|
|
32737
|
+
"action": "action_900471699",
|
|
32738
|
+
"turnHead": "turnHead_900471699",
|
|
32739
|
+
"turnList": "turnList_900471699",
|
|
32740
|
+
"turn": "turn_900471699",
|
|
32741
|
+
"chevron": "chevron_900471699",
|
|
32742
|
+
"rotated": "rotated_900471699",
|
|
32743
|
+
"turnTitle": "turnTitle_900471699",
|
|
32744
|
+
"turnTopic": "turnTopic_900471699",
|
|
32745
|
+
"turnAgent": "turnAgent_900471699",
|
|
32746
|
+
"turnSummary": "turnSummary_900471699",
|
|
32747
|
+
"turnBody": "turnBody_900471699",
|
|
32748
|
+
"turnRequest": "turnRequest_900471699",
|
|
32749
|
+
"turnRequestHead": "turnRequestHead_900471699",
|
|
32750
|
+
"requestTime": "requestTime_900471699",
|
|
32751
|
+
"statusDot": "statusDot_900471699",
|
|
32752
|
+
"routeDot": "routeDot_900471699",
|
|
32753
|
+
"whale": "whale_900471699",
|
|
32754
|
+
"recordModel": "recordModel_900471699",
|
|
32755
|
+
"routeText": "routeText_900471699",
|
|
32756
|
+
"effort": "effort_900471699",
|
|
32757
|
+
"recordMeta": "recordMeta_900471699",
|
|
32758
|
+
"note": "note_900471699",
|
|
32759
|
+
"error": "error_900471699"
|
|
32760
|
+
};
|
|
32761
|
+
//#endregion
|
|
32762
|
+
//#region src/client/SessionUsageView.tsx
|
|
32763
|
+
const seconds = (ms) => ms !== null && Number.isFinite(ms) ? `${(ms / 1e3).toFixed(1)}s` : "—";
|
|
32764
|
+
const secText = (ms) => ms !== null && Number.isFinite(ms) ? String(Math.round(ms / 100) / 10) : "—";
|
|
32765
|
+
const tokShort = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
|
|
32766
|
+
const pctInt = (rate) => rate === null ? "—" : `${Math.round(rate)}%`;
|
|
32767
|
+
const speed = (n) => n !== null && Number.isFinite(n) ? `${Math.round(n)} tok/s` : "—";
|
|
32768
|
+
const clock = (ms) => new Date(ms).toLocaleTimeString([], {
|
|
32769
|
+
hour: "2-digit",
|
|
32770
|
+
minute: "2-digit",
|
|
32771
|
+
second: "2-digit"
|
|
32772
|
+
});
|
|
32773
|
+
const whale = "M8 39.5C13.5 27.6 26.8 19.4 42.5 19.1c-1.6 2.4-2.3 5-2 7.9-5.6-1.7-11.7-.4-16.2 3.4 4.7 1.1 8.4 4.3 10.2 8.7 1.3-.4 2.7-1 4-1.9.4 2.1-.2 4.3-1.6 5.9-7.7.6-15.5-1.6-20.7-6.6-.7 1.9-.9 3.9-.6 5.9-3.1-1.2-5.6-3-7.6-2.9z";
|
|
32774
|
+
const modelShort = (ref) => ref.includes("/") ? ref.slice(ref.indexOf("/") + 1) : ref;
|
|
32775
|
+
const zero = () => ({
|
|
32776
|
+
uncachedInputTokens: 0,
|
|
32777
|
+
outputTokens: 0,
|
|
32778
|
+
cacheReadTokens: 0,
|
|
32779
|
+
cacheWriteTokens: 0
|
|
32780
|
+
});
|
|
32781
|
+
const sum = (rows) => rows.reduce((acc, r) => ({
|
|
32782
|
+
uncachedInputTokens: acc.uncachedInputTokens + r.totals.uncachedInputTokens,
|
|
32783
|
+
outputTokens: acc.outputTokens + r.totals.outputTokens,
|
|
32784
|
+
cacheReadTokens: acc.cacheReadTokens + r.totals.cacheReadTokens,
|
|
32785
|
+
cacheWriteTokens: acc.cacheWriteTokens + r.totals.cacheWriteTokens
|
|
32786
|
+
}), zero());
|
|
32787
|
+
function Token({ n }) {
|
|
32788
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32789
|
+
className: SessionUsageView_module_default.number,
|
|
32790
|
+
title: formatTokens(n),
|
|
32791
|
+
children: formatCompact(n)
|
|
32792
|
+
});
|
|
32793
|
+
}
|
|
32794
|
+
function Hit({ totals }) {
|
|
32795
|
+
const rate = cacheHitPercentOf(totals);
|
|
32796
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32797
|
+
className: rate === null ? SessionUsageView_module_default.muted : rate >= 80 ? SessionUsageView_module_default.good : rate >= 50 ? SessionUsageView_module_default.warn : SessionUsageView_module_default.bad,
|
|
32798
|
+
children: rate === null ? "—" : `${rate.toFixed(1)}%`
|
|
32799
|
+
});
|
|
32800
|
+
}
|
|
32801
|
+
const icons = {
|
|
32802
|
+
requests: "M4 9h16M4 15h16M10 3 8 21M16 3l-2 18",
|
|
32803
|
+
tokens: "M4 8c0-2.2 3.6-4 8-4s8 1.8 8 4-3.6 4-8 4-8-1.8-8-4Zm0 0v8c0 2.2 3.6 4 8 4s8-1.8 8-4V8",
|
|
32804
|
+
hit: "M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z",
|
|
32805
|
+
speed: "M13 2 4 14h7l-1 8 9-12h-7l1-8Z",
|
|
32806
|
+
input: "M12 3v12M7 10l5 5 5-5M4 21h16",
|
|
32807
|
+
output: "M12 21V9M7 14l5-5 5 5M4 3h16",
|
|
32808
|
+
read: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7ZM15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z",
|
|
32809
|
+
write: "M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5Z"
|
|
32810
|
+
};
|
|
32811
|
+
function Stat({ tone, name, label, value, sub, title }) {
|
|
32812
|
+
const icon = icons[name];
|
|
32813
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
32814
|
+
className: `${SessionUsageView_module_default.stat} ${tone}`,
|
|
32815
|
+
children: [
|
|
32816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
32817
|
+
className: SessionUsageView_module_default.statMark,
|
|
32818
|
+
viewBox: "0 0 24 24",
|
|
32819
|
+
"aria-hidden": "true",
|
|
32820
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
|
|
32821
|
+
id: `usage-mark-${name}`,
|
|
32822
|
+
x1: "0",
|
|
32823
|
+
y1: "0",
|
|
32824
|
+
x2: "1",
|
|
32825
|
+
y2: "1",
|
|
32826
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
|
|
32827
|
+
offset: "0",
|
|
32828
|
+
stopColor: "currentColor",
|
|
32829
|
+
stopOpacity: "0.3"
|
|
32830
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
|
|
32831
|
+
offset: "1",
|
|
32832
|
+
stopColor: "currentColor",
|
|
32833
|
+
stopOpacity: "0.03"
|
|
32834
|
+
})]
|
|
32835
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
32836
|
+
d: icon,
|
|
32837
|
+
fill: `url(#usage-mark-${name})`,
|
|
32838
|
+
stroke: `url(#usage-mark-${name})`,
|
|
32839
|
+
strokeWidth: "1.1",
|
|
32840
|
+
strokeLinecap: "round",
|
|
32841
|
+
strokeLinejoin: "round"
|
|
32842
|
+
})]
|
|
32843
|
+
}),
|
|
32844
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32845
|
+
className: SessionUsageView_module_default.statHead,
|
|
32846
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32847
|
+
className: SessionUsageView_module_default.statIcon,
|
|
32848
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
32849
|
+
width: "18",
|
|
32850
|
+
height: "18",
|
|
32851
|
+
viewBox: "0 0 24 24",
|
|
32852
|
+
fill: "none",
|
|
32853
|
+
stroke: "currentColor",
|
|
32854
|
+
strokeWidth: "1.8",
|
|
32855
|
+
strokeLinecap: "round",
|
|
32856
|
+
strokeLinejoin: "round",
|
|
32857
|
+
"aria-hidden": "true",
|
|
32858
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: icon })
|
|
32859
|
+
})
|
|
32860
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32861
|
+
className: SessionUsageView_module_default.statLabel,
|
|
32862
|
+
children: label
|
|
32863
|
+
})]
|
|
32864
|
+
}),
|
|
32865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
32866
|
+
className: SessionUsageView_module_default.statValue,
|
|
32867
|
+
title,
|
|
32868
|
+
children: value
|
|
32869
|
+
}),
|
|
32870
|
+
sub !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32871
|
+
className: SessionUsageView_module_default.statSub,
|
|
32872
|
+
children: sub
|
|
32873
|
+
})
|
|
32874
|
+
]
|
|
32875
|
+
});
|
|
32876
|
+
}
|
|
32877
|
+
function Metrics({ detail, t }) {
|
|
32878
|
+
const billed = billedTotal(detail.totals);
|
|
32879
|
+
const failed = detail.records.filter((row) => row.status === "failed").length;
|
|
32880
|
+
const failRate = detail.requests > 0 ? (failed / detail.requests * 100).toFixed(1) : "0.0";
|
|
32881
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32882
|
+
className: SessionUsageView_module_default.stats,
|
|
32883
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32884
|
+
className: SessionUsageView_module_default.statRow,
|
|
32885
|
+
children: [
|
|
32886
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32887
|
+
tone: SessionUsageView_module_default.toneBlue,
|
|
32888
|
+
name: "requests",
|
|
32889
|
+
label: t("requestsCount"),
|
|
32890
|
+
value: formatTokens(detail.requests),
|
|
32891
|
+
sub: `${t("failed")} ${failed} · ${failRate}%`
|
|
32892
|
+
}),
|
|
32893
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32894
|
+
tone: SessionUsageView_module_default.tonePurple,
|
|
32895
|
+
name: "tokens",
|
|
32896
|
+
label: t("tokens"),
|
|
32897
|
+
value: formatCompact(billed),
|
|
32898
|
+
title: formatTokens(billed),
|
|
32899
|
+
sub: `${formatTokens(billed)} tokens`
|
|
32900
|
+
}),
|
|
32901
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32902
|
+
tone: SessionUsageView_module_default.toneGreen,
|
|
32903
|
+
name: "hit",
|
|
32904
|
+
label: t("hit"),
|
|
32905
|
+
value: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hit, { totals: detail.totals }),
|
|
32906
|
+
sub: `${formatCompact(detail.totals.cacheReadTokens)} ${t("cacheHit")}`
|
|
32907
|
+
}),
|
|
32908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32909
|
+
tone: SessionUsageView_module_default.toneOrange,
|
|
32910
|
+
name: "speed",
|
|
32911
|
+
label: t("speed"),
|
|
32912
|
+
value: speed(detail.tokensPerSecond),
|
|
32913
|
+
sub: `${t("first")} ${seconds(detail.averageFirstTokenMs)}`
|
|
32914
|
+
})
|
|
32915
|
+
]
|
|
32916
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32917
|
+
className: SessionUsageView_module_default.statRow,
|
|
32918
|
+
children: [
|
|
32919
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32920
|
+
tone: SessionUsageView_module_default.toneTeal,
|
|
32921
|
+
name: "input",
|
|
32922
|
+
label: t("inputFull"),
|
|
32923
|
+
value: formatCompact(detail.totals.uncachedInputTokens),
|
|
32924
|
+
title: formatTokens(detail.totals.uncachedInputTokens),
|
|
32925
|
+
sub: formatTokens(detail.totals.uncachedInputTokens)
|
|
32926
|
+
}),
|
|
32927
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32928
|
+
tone: SessionUsageView_module_default.toneOrange,
|
|
32929
|
+
name: "output",
|
|
32930
|
+
label: t("outputFull"),
|
|
32931
|
+
value: formatCompact(detail.totals.outputTokens),
|
|
32932
|
+
title: formatTokens(detail.totals.outputTokens),
|
|
32933
|
+
sub: formatTokens(detail.totals.outputTokens)
|
|
32934
|
+
}),
|
|
32935
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32936
|
+
tone: SessionUsageView_module_default.tonePurple,
|
|
32937
|
+
name: "read",
|
|
32938
|
+
label: t("readFull"),
|
|
32939
|
+
value: formatCompact(detail.totals.cacheReadTokens),
|
|
32940
|
+
title: formatTokens(detail.totals.cacheReadTokens),
|
|
32941
|
+
sub: formatTokens(detail.totals.cacheReadTokens)
|
|
32942
|
+
}),
|
|
32943
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
32944
|
+
tone: SessionUsageView_module_default.tonePink,
|
|
32945
|
+
name: "write",
|
|
32946
|
+
label: t("writeFull"),
|
|
32947
|
+
value: formatCompact(detail.totals.cacheWriteTokens),
|
|
32948
|
+
title: formatTokens(detail.totals.cacheWriteTokens),
|
|
32949
|
+
sub: formatTokens(detail.totals.cacheWriteTokens)
|
|
32950
|
+
})
|
|
32951
|
+
]
|
|
32952
|
+
})]
|
|
32953
|
+
});
|
|
32954
|
+
}
|
|
32955
|
+
function Tables({ detail, t }) {
|
|
32956
|
+
const nameCounts = /* @__PURE__ */ new Map();
|
|
32957
|
+
for (const row of detail.models) {
|
|
32958
|
+
const key = normalizeModelName(row.name?.trim() || row.model);
|
|
32959
|
+
nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
|
|
32960
|
+
}
|
|
32961
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32962
|
+
className: SessionUsageView_module_default.mid,
|
|
32963
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
32964
|
+
className: SessionUsageView_module_default.card,
|
|
32965
|
+
children: [
|
|
32966
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
32967
|
+
className: SessionUsageView_module_default.cardHead,
|
|
32968
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", { children: [
|
|
32969
|
+
t("children"),
|
|
32970
|
+
" ",
|
|
32971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32972
|
+
className: SessionUsageView_module_default.count,
|
|
32973
|
+
children: detail.subagents.length
|
|
32974
|
+
})
|
|
32975
|
+
] })
|
|
32976
|
+
}),
|
|
32977
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
32978
|
+
className: `${SessionUsageView_module_default.table} ${SessionUsageView_module_default.childTable}`,
|
|
32979
|
+
children: [
|
|
32980
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("colgroup", { children: [
|
|
32981
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: SessionUsageView_module_default.childName }),
|
|
32982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: SessionUsageView_module_default.childRequests }),
|
|
32983
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
|
|
32984
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
|
|
32985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
|
|
32986
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {})
|
|
32987
|
+
] }),
|
|
32988
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
32989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("session") }),
|
|
32990
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("requests") }),
|
|
32991
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("tokens") }),
|
|
32992
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("hit") }),
|
|
32993
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("first") }),
|
|
32994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("speed") })
|
|
32995
|
+
] }) }),
|
|
32996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: detail.subagents.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
32997
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32998
|
+
className: SessionUsageView_module_default.ellipsis,
|
|
32999
|
+
title: row.name || row.sessionId,
|
|
33000
|
+
children: row.name || row.sessionId
|
|
33001
|
+
}) }),
|
|
33002
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatTokens(row.requests) }),
|
|
33003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Token, { n: billedTotal(row.totals) }) }),
|
|
33004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hit, { totals: row.totals }) }),
|
|
33005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: seconds(row.averageFirstTokenMs) }),
|
|
33006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33007
|
+
className: SessionUsageView_module_default.speedCell,
|
|
33008
|
+
children: row.tokensPerSecond === null ? "—" : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [Math.round(row.tokensPerSecond), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "tok/s" })] })
|
|
33009
|
+
}) })
|
|
33010
|
+
] }, row.sessionId)) })
|
|
33011
|
+
]
|
|
33012
|
+
}),
|
|
33013
|
+
detail.subagents.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
33014
|
+
className: SessionUsageView_module_default.empty,
|
|
33015
|
+
children: t("noChildren")
|
|
33016
|
+
})
|
|
33017
|
+
]
|
|
33018
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
33019
|
+
className: SessionUsageView_module_default.card,
|
|
33020
|
+
children: [
|
|
33021
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
33022
|
+
className: SessionUsageView_module_default.cardHead,
|
|
33023
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", { children: [
|
|
33024
|
+
t("models"),
|
|
33025
|
+
" ",
|
|
33026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33027
|
+
className: SessionUsageView_module_default.count,
|
|
33028
|
+
children: detail.models.length
|
|
33029
|
+
})
|
|
33030
|
+
] })
|
|
33031
|
+
}),
|
|
33032
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
33033
|
+
className: `${SessionUsageView_module_default.table} ${SessionUsageView_module_default.modelTable}`,
|
|
33034
|
+
children: [
|
|
33035
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("colgroup", { children: [
|
|
33036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: SessionUsageView_module_default.modelName }),
|
|
33037
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: SessionUsageView_module_default.modelRequests }),
|
|
33038
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
|
|
33039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
|
|
33040
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
|
|
33041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
|
|
33042
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: SessionUsageView_module_default.modelHit })
|
|
33043
|
+
] }),
|
|
33044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
33045
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("model") }),
|
|
33046
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("requests") }),
|
|
33047
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("input") }),
|
|
33048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("output") }),
|
|
33049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("read") }),
|
|
33050
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("write") }),
|
|
33051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("hit") })
|
|
33052
|
+
] }) }),
|
|
33053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: detail.models.map((row) => {
|
|
33054
|
+
const name = row.name?.trim() || row.model;
|
|
33055
|
+
const providers = row.providers ?? [];
|
|
33056
|
+
const shared = (nameCounts.get(normalizeModelName(name)) ?? 0) > 1;
|
|
33057
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
33058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33059
|
+
className: SessionUsageView_module_default.ellipsis,
|
|
33060
|
+
title: row.model,
|
|
33061
|
+
children: name
|
|
33062
|
+
}), shared && providers.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
|
|
33063
|
+
className: SessionUsageView_module_default.modelProvider,
|
|
33064
|
+
title: providers.join(" / "),
|
|
33065
|
+
children: providers.join(" / ")
|
|
33066
|
+
}) : null] }),
|
|
33067
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatTokens(row.requests) }),
|
|
33068
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Token, { n: row.totals.uncachedInputTokens }) }),
|
|
33069
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Token, { n: row.totals.outputTokens }) }),
|
|
33070
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Token, { n: row.totals.cacheReadTokens }) }),
|
|
33071
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Token, { n: row.totals.cacheWriteTokens }) }),
|
|
33072
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hit, { totals: row.totals }) })
|
|
33073
|
+
] }, row.model);
|
|
33074
|
+
}) })
|
|
33075
|
+
]
|
|
33076
|
+
}),
|
|
33077
|
+
detail.models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
33078
|
+
className: SessionUsageView_module_default.empty,
|
|
33079
|
+
children: t("noModels")
|
|
33080
|
+
})
|
|
33081
|
+
]
|
|
33082
|
+
})]
|
|
33083
|
+
});
|
|
33084
|
+
}
|
|
33085
|
+
function Records({ detail, t }) {
|
|
33086
|
+
const [opened, setOpened] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
33087
|
+
const [allOpen, setAllOpen] = (0, react.useState)(false);
|
|
33088
|
+
const toggleAll = () => {
|
|
33089
|
+
setAllOpen(!allOpen);
|
|
33090
|
+
setOpened(/* @__PURE__ */ new Set());
|
|
33091
|
+
};
|
|
33092
|
+
const toggle = (id) => setOpened((previous) => {
|
|
33093
|
+
const next = new Set(previous);
|
|
33094
|
+
if (next.has(id)) next.delete(id);
|
|
33095
|
+
else next.add(id);
|
|
33096
|
+
return next;
|
|
33097
|
+
});
|
|
33098
|
+
const providerNames = new Map(detail.routes.map((route) => [route.provider, route.name]));
|
|
33099
|
+
const groups = [];
|
|
33100
|
+
const index = /* @__PURE__ */ new Map();
|
|
33101
|
+
for (const record of detail.records) {
|
|
33102
|
+
const key = `${record.sessionId}:${record.turn}`;
|
|
33103
|
+
let group = index.get(key);
|
|
33104
|
+
if (!group) {
|
|
33105
|
+
group = {
|
|
33106
|
+
key,
|
|
33107
|
+
turn: record.turn,
|
|
33108
|
+
name: record.sessionName || record.sessionId,
|
|
33109
|
+
topic: record.turnName || `${t("turn")} ${record.turn}`,
|
|
33110
|
+
main: record.sessionId === detail.sessionId,
|
|
33111
|
+
rows: []
|
|
33112
|
+
};
|
|
33113
|
+
index.set(key, group);
|
|
33114
|
+
groups.push(group);
|
|
33115
|
+
}
|
|
33116
|
+
group.rows.push(record);
|
|
33117
|
+
}
|
|
33118
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
33119
|
+
className: SessionUsageView_module_default.card,
|
|
33120
|
+
children: [
|
|
33121
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
33122
|
+
className: SessionUsageView_module_default.cardHead,
|
|
33123
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", { children: [
|
|
33124
|
+
t("records"),
|
|
33125
|
+
" ",
|
|
33126
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33127
|
+
className: SessionUsageView_module_default.count,
|
|
33128
|
+
children: detail.records.length
|
|
33129
|
+
})
|
|
33130
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
33131
|
+
type: "button",
|
|
33132
|
+
className: SessionUsageView_module_default.action,
|
|
33133
|
+
onClick: toggleAll,
|
|
33134
|
+
children: t(allOpen ? "collapse" : "expand")
|
|
33135
|
+
})]
|
|
33136
|
+
}),
|
|
33137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
33138
|
+
className: SessionUsageView_module_default.turnList,
|
|
33139
|
+
children: groups.map((group) => {
|
|
33140
|
+
const open = allOpen !== opened.has(group.key);
|
|
33141
|
+
const totals = sum(group.rows);
|
|
33142
|
+
const billed = billedTotal(totals);
|
|
33143
|
+
const running = group.rows.filter((row) => row.status === "running").length;
|
|
33144
|
+
const rates = group.rows.map((row) => row.tokensPerSecond).filter((n) => n !== null);
|
|
33145
|
+
const avgRate = rates.length ? Math.round(rates.reduce((a, b) => a + b, 0) / rates.length) : null;
|
|
33146
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
33147
|
+
className: SessionUsageView_module_default.turn,
|
|
33148
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
33149
|
+
type: "button",
|
|
33150
|
+
className: SessionUsageView_module_default.turnHead,
|
|
33151
|
+
onClick: () => toggle(group.key),
|
|
33152
|
+
"aria-expanded": open,
|
|
33153
|
+
"aria-controls": `usage-turn-${encodeURIComponent(group.key)}`,
|
|
33154
|
+
children: [
|
|
33155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
33156
|
+
className: `${SessionUsageView_module_default.chevron} ${open ? SessionUsageView_module_default.rotated : ""}`,
|
|
33157
|
+
width: "12",
|
|
33158
|
+
height: "12",
|
|
33159
|
+
viewBox: "0 0 12 12",
|
|
33160
|
+
"aria-hidden": "true",
|
|
33161
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
33162
|
+
d: "m4 2 4 4-4 4",
|
|
33163
|
+
stroke: "currentColor",
|
|
33164
|
+
strokeWidth: "1.5",
|
|
33165
|
+
fill: "none"
|
|
33166
|
+
})
|
|
33167
|
+
}),
|
|
33168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
33169
|
+
className: SessionUsageView_module_default.turnTitle,
|
|
33170
|
+
children: [group.main ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33171
|
+
className: SessionUsageView_module_default.turnAgent,
|
|
33172
|
+
title: group.name,
|
|
33173
|
+
children: t("children")
|
|
33174
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33175
|
+
className: SessionUsageView_module_default.turnTopic,
|
|
33176
|
+
title: group.turn === 0 ? t("compaction") : group.topic,
|
|
33177
|
+
children: group.turn === 0 ? t("compaction") : group.topic
|
|
33178
|
+
})]
|
|
33179
|
+
}),
|
|
33180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
33181
|
+
className: SessionUsageView_module_default.turnSummary,
|
|
33182
|
+
children: [
|
|
33183
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
33184
|
+
group.rows.length,
|
|
33185
|
+
" ",
|
|
33186
|
+
t("reqTimes"),
|
|
33187
|
+
" · ",
|
|
33188
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33189
|
+
className: SessionUsageView_module_default.number,
|
|
33190
|
+
title: formatTokens(billed),
|
|
33191
|
+
children: tokShort(billed)
|
|
33192
|
+
}),
|
|
33193
|
+
" ",
|
|
33194
|
+
t("tokenWord"),
|
|
33195
|
+
running > 0 ? ` · ${running} ${t("inProgress")}` : ""
|
|
33196
|
+
] }),
|
|
33197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
33198
|
+
t("avgCache"),
|
|
33199
|
+
" ",
|
|
33200
|
+
pctInt(cacheHitPercentOf(totals))
|
|
33201
|
+
] }),
|
|
33202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
33203
|
+
t("avgRate"),
|
|
33204
|
+
" ",
|
|
33205
|
+
avgRate === null ? "—" : avgRate,
|
|
33206
|
+
" ",
|
|
33207
|
+
t("perSec")
|
|
33208
|
+
] })
|
|
33209
|
+
]
|
|
33210
|
+
})
|
|
33211
|
+
]
|
|
33212
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
33213
|
+
id: `usage-turn-${encodeURIComponent(group.key)}`,
|
|
33214
|
+
className: SessionUsageView_module_default.turnBody,
|
|
33215
|
+
hidden: !open,
|
|
33216
|
+
children: group.rows.map((record) => {
|
|
33217
|
+
const duration = record.finishedAt !== null ? record.finishedAt - record.startedAt : null;
|
|
33218
|
+
const billed = billedTotal(record.totals);
|
|
33219
|
+
const hit = cacheHitPercentOf(record.totals);
|
|
33220
|
+
const meta = [];
|
|
33221
|
+
if (duration !== null) meta.push({ text: `${secText(duration)} ${t("sec")}` });
|
|
33222
|
+
if (record.firstTokenMs !== null) meta.push({ text: `${t("firstWait")} ${secText(record.firstTokenMs)} ${t("sec")}` });
|
|
33223
|
+
meta.push({
|
|
33224
|
+
text: `${tokShort(billed)} ${t("tokenWord")}`,
|
|
33225
|
+
title: formatTokens(billed)
|
|
33226
|
+
});
|
|
33227
|
+
if (hit !== null) meta.push({ text: `${t("cacheShort")} ${pctInt(hit)}` });
|
|
33228
|
+
if (record.tokensPerSecond !== null) meta.push({ text: `${Math.round(record.tokensPerSecond)} ${t("perSec")}` });
|
|
33229
|
+
const statusText = record.status === "running" ? t("answering") : record.status === "completed" ? "" : t(record.status);
|
|
33230
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33231
|
+
className: SessionUsageView_module_default.turnRequest,
|
|
33232
|
+
children: [
|
|
33233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33234
|
+
className: SessionUsageView_module_default.requestTime,
|
|
33235
|
+
children: clock(record.startedAt)
|
|
33236
|
+
}),
|
|
33237
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33238
|
+
className: SessionUsageView_module_default.turnRequestHead,
|
|
33239
|
+
children: [
|
|
33240
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `${SessionUsageView_module_default.statusDot} ${record.status === "running" ? SessionUsageView_module_default.live : record.status === "failed" ? SessionUsageView_module_default.bad : record.status === "interrupted" ? SessionUsageView_module_default.muted : SessionUsageView_module_default.good}` }),
|
|
33241
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
33242
|
+
className: SessionUsageView_module_default.recordModel,
|
|
33243
|
+
title: `${record.provider} / ${record.model}`,
|
|
33244
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
33245
|
+
className: SessionUsageView_module_default.whale,
|
|
33246
|
+
width: "13",
|
|
33247
|
+
height: "13",
|
|
33248
|
+
viewBox: "0 0 64 64",
|
|
33249
|
+
"aria-hidden": "true",
|
|
33250
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
33251
|
+
d: whale,
|
|
33252
|
+
fill: "currentColor"
|
|
33253
|
+
})
|
|
33254
|
+
}), record.model || "—"]
|
|
33255
|
+
}),
|
|
33256
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SessionUsageView_module_default.routeDot }),
|
|
33257
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
33258
|
+
className: SessionUsageView_module_default.routeText,
|
|
33259
|
+
title: record.provider,
|
|
33260
|
+
children: [
|
|
33261
|
+
providerNames.get(record.provider) || record.provider || "—",
|
|
33262
|
+
" · ",
|
|
33263
|
+
modelShort(record.model) || "—"
|
|
33264
|
+
]
|
|
33265
|
+
}),
|
|
33266
|
+
statusText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33267
|
+
className: record.status === "running" ? SessionUsageView_module_default.live : record.status === "failed" ? SessionUsageView_module_default.bad : SessionUsageView_module_default.muted,
|
|
33268
|
+
children: statusText
|
|
33269
|
+
}),
|
|
33270
|
+
record.effort ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
33271
|
+
className: SessionUsageView_module_default.effort,
|
|
33272
|
+
children: ["· ", record.effort]
|
|
33273
|
+
}) : null
|
|
33274
|
+
]
|
|
33275
|
+
}),
|
|
33276
|
+
record.finishedAt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
33277
|
+
className: SessionUsageView_module_default.recordMeta,
|
|
33278
|
+
children: meta.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
33279
|
+
title: item.title,
|
|
33280
|
+
children: [i > 0 ? " · " : "", item.text]
|
|
33281
|
+
}, i))
|
|
33282
|
+
})
|
|
33283
|
+
]
|
|
33284
|
+
}, record.id);
|
|
33285
|
+
})
|
|
33286
|
+
})]
|
|
33287
|
+
}, group.key);
|
|
33288
|
+
})
|
|
33289
|
+
}),
|
|
33290
|
+
detail.records.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
33291
|
+
className: SessionUsageView_module_default.empty,
|
|
33292
|
+
children: t("empty")
|
|
33293
|
+
})
|
|
33294
|
+
]
|
|
33295
|
+
});
|
|
33296
|
+
}
|
|
33297
|
+
/** Pure presentation export also used by offline visual regression fixtures. */
|
|
33298
|
+
function SessionUsageContent({ detail, t }) {
|
|
33299
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
33300
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
33301
|
+
className: SessionUsageView_module_default.scope,
|
|
33302
|
+
children: t("scope")
|
|
33303
|
+
}),
|
|
33304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metrics, {
|
|
33305
|
+
detail,
|
|
33306
|
+
t
|
|
33307
|
+
}),
|
|
33308
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tables, {
|
|
33309
|
+
detail,
|
|
33310
|
+
t
|
|
33311
|
+
}),
|
|
33312
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Records, {
|
|
33313
|
+
detail,
|
|
33314
|
+
t
|
|
33315
|
+
}, detail.sessionId),
|
|
33316
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
33317
|
+
className: SessionUsageView_module_default.note,
|
|
33318
|
+
children: t("note")
|
|
33319
|
+
})
|
|
33320
|
+
] });
|
|
33321
|
+
}
|
|
33322
|
+
function SessionUsageView({ sessionId, t }) {
|
|
33323
|
+
const { detail, loading, error, retry } = useSessionDetail(sessionId);
|
|
33324
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
33325
|
+
className: SessionUsageView_module_default.scroll,
|
|
33326
|
+
"data-dsh-session-usage": true,
|
|
33327
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33328
|
+
className: SessionUsageView_module_default.page,
|
|
33329
|
+
children: [
|
|
33330
|
+
loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
33331
|
+
className: SessionUsageView_module_default.empty,
|
|
33332
|
+
role: "status",
|
|
33333
|
+
children: t("loading")
|
|
33334
|
+
}),
|
|
33335
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33336
|
+
className: SessionUsageView_module_default.error,
|
|
33337
|
+
role: "status",
|
|
33338
|
+
children: [t("failedLoad"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
33339
|
+
type: "button",
|
|
33340
|
+
className: SessionUsageView_module_default.action,
|
|
33341
|
+
onClick: retry,
|
|
33342
|
+
children: t("retry")
|
|
33343
|
+
})]
|
|
33344
|
+
}),
|
|
33345
|
+
detail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionUsageContent, {
|
|
33346
|
+
detail,
|
|
33347
|
+
t
|
|
33348
|
+
})
|
|
33349
|
+
]
|
|
33350
|
+
})
|
|
33351
|
+
});
|
|
33352
|
+
}
|
|
33353
|
+
//#endregion
|
|
33354
|
+
//#region src/client/session-usage-locales.ts
|
|
33355
|
+
/** Session Usage view translations, independent of the global dashboard. */
|
|
33356
|
+
const SESSION_USAGE_NS = "sessionUsage";
|
|
33357
|
+
const usageZh = {
|
|
33358
|
+
tab: "用量",
|
|
33359
|
+
requests: "请求数",
|
|
33360
|
+
tokens: "总 tokens",
|
|
33361
|
+
hit: "命中率",
|
|
33362
|
+
first: "平均首 token",
|
|
33363
|
+
speed: "token 速度",
|
|
33364
|
+
input: "入",
|
|
33365
|
+
output: "出",
|
|
33366
|
+
read: "读",
|
|
33367
|
+
write: "写",
|
|
33368
|
+
requestsCount: "请求数量",
|
|
33369
|
+
inputFull: "输入",
|
|
33370
|
+
outputFull: "输出",
|
|
33371
|
+
readFull: "缓存读取",
|
|
33372
|
+
writeFull: "缓存写入",
|
|
33373
|
+
cacheHit: "缓存命中",
|
|
33374
|
+
compaction: "压缩调用",
|
|
33375
|
+
reqTimes: "个请求",
|
|
33376
|
+
children: "子代理",
|
|
33377
|
+
session: "会话",
|
|
33378
|
+
models: "按模型",
|
|
33379
|
+
model: "模型",
|
|
33380
|
+
records: "请求记录",
|
|
33381
|
+
expand: "全部展开",
|
|
33382
|
+
collapse: "全部收起",
|
|
33383
|
+
loading: "正在读取会话用量…",
|
|
33384
|
+
empty: "本会话还没有请求记录",
|
|
33385
|
+
noChildren: "暂无子代理",
|
|
33386
|
+
noModels: "暂无模型用量",
|
|
33387
|
+
scope: "本会话及所有子代理 · 历史日志累计",
|
|
33388
|
+
note: "统计包含本会话与全部后代的独立请求、重试和压缩调用,不重复计算继承历史。首 token 与速度仅使用已记录的时间信息,缺失时显示 —。",
|
|
33389
|
+
failedLoad: "用量读取失败,已保留上次结果。",
|
|
33390
|
+
retry: "重新读取",
|
|
33391
|
+
completed: "已完成",
|
|
33392
|
+
running: "进行中",
|
|
33393
|
+
failed: "失败",
|
|
33394
|
+
interrupted: "已中断",
|
|
33395
|
+
main: "主会话",
|
|
33396
|
+
turn: "轮",
|
|
33397
|
+
step: "步",
|
|
33398
|
+
started: "开始",
|
|
33399
|
+
duration: "耗时",
|
|
33400
|
+
firstDetail: "首 token",
|
|
33401
|
+
provider: "供应商",
|
|
33402
|
+
status: "状态",
|
|
33403
|
+
exact: "完整 token 数",
|
|
33404
|
+
answering: "正在回答...",
|
|
33405
|
+
firstWait: "首字耗时",
|
|
33406
|
+
sec: "秒",
|
|
33407
|
+
tokenWord: "token",
|
|
33408
|
+
perSec: "token/秒",
|
|
33409
|
+
cacheShort: "缓存",
|
|
33410
|
+
avgCache: "平均缓存",
|
|
33411
|
+
avgRate: "平均速率",
|
|
33412
|
+
inProgress: "个进行中"
|
|
33413
|
+
};
|
|
33414
|
+
const usageEn = {
|
|
33415
|
+
tab: "Usage",
|
|
33416
|
+
requests: "Requests",
|
|
33417
|
+
tokens: "Total tokens",
|
|
33418
|
+
hit: "Cache hit rate",
|
|
33419
|
+
first: "Avg. first token",
|
|
33420
|
+
speed: "Token speed",
|
|
33421
|
+
input: "In",
|
|
33422
|
+
output: "Out",
|
|
33423
|
+
read: "Read",
|
|
33424
|
+
write: "Write",
|
|
33425
|
+
requestsCount: "Requests",
|
|
33426
|
+
inputFull: "Input",
|
|
33427
|
+
outputFull: "Output",
|
|
33428
|
+
readFull: "Cache read",
|
|
33429
|
+
writeFull: "Cache write",
|
|
33430
|
+
cacheHit: "cache hits",
|
|
33431
|
+
compaction: "Compaction",
|
|
33432
|
+
reqTimes: "requests",
|
|
33433
|
+
children: "Subagents",
|
|
33434
|
+
session: "Session",
|
|
33435
|
+
models: "By model",
|
|
33436
|
+
model: "Model",
|
|
33437
|
+
records: "Request log",
|
|
33438
|
+
expand: "Expand all",
|
|
33439
|
+
collapse: "Collapse all",
|
|
33440
|
+
loading: "Reading session usage…",
|
|
33441
|
+
empty: "No requests in this session yet",
|
|
33442
|
+
noChildren: "No subagents yet",
|
|
33443
|
+
noModels: "No model usage yet",
|
|
33444
|
+
scope: "This session and all subagents · Historical log totals",
|
|
33445
|
+
note: "Includes independent requests, retries and compaction calls from this session and all descendants, without counting inherited history twice. Timing uses recorded timestamps only; missing values appear as —.",
|
|
33446
|
+
failedLoad: "Could not read usage. The last successful result is preserved.",
|
|
33447
|
+
retry: "Retry",
|
|
33448
|
+
completed: "Completed",
|
|
33449
|
+
running: "Running",
|
|
33450
|
+
failed: "Failed",
|
|
33451
|
+
interrupted: "Interrupted",
|
|
33452
|
+
main: "Main session",
|
|
33453
|
+
turn: "Turn",
|
|
33454
|
+
step: "Step",
|
|
33455
|
+
started: "Started",
|
|
33456
|
+
duration: "Duration",
|
|
33457
|
+
firstDetail: "First token",
|
|
33458
|
+
provider: "Provider",
|
|
33459
|
+
status: "Status",
|
|
33460
|
+
exact: "Exact token count",
|
|
33461
|
+
answering: "answering...",
|
|
33462
|
+
firstWait: "first token",
|
|
33463
|
+
sec: "s",
|
|
33464
|
+
tokenWord: "token",
|
|
33465
|
+
perSec: "token/s",
|
|
33466
|
+
cacheShort: "cache",
|
|
33467
|
+
avgCache: "avg cache",
|
|
33468
|
+
avgRate: "avg rate",
|
|
33469
|
+
inProgress: "in progress"
|
|
33470
|
+
};
|
|
33471
|
+
const usageZhTW = {
|
|
33472
|
+
...usageZh,
|
|
33473
|
+
tab: "用量",
|
|
33474
|
+
requests: "請求數",
|
|
33475
|
+
tokens: "總 tokens",
|
|
33476
|
+
hit: "命中率",
|
|
33477
|
+
first: "平均首 token",
|
|
33478
|
+
speed: "token 速度",
|
|
33479
|
+
input: "入",
|
|
33480
|
+
output: "出",
|
|
33481
|
+
read: "讀",
|
|
33482
|
+
write: "寫",
|
|
33483
|
+
requestsCount: "請求數量",
|
|
33484
|
+
inputFull: "輸入",
|
|
33485
|
+
outputFull: "輸出",
|
|
33486
|
+
readFull: "快取讀取",
|
|
33487
|
+
writeFull: "快取寫入",
|
|
33488
|
+
cacheHit: "快取命中",
|
|
33489
|
+
compaction: "壓縮呼叫",
|
|
33490
|
+
reqTimes: "個請求",
|
|
33491
|
+
children: "子代理",
|
|
33492
|
+
session: "會話",
|
|
33493
|
+
models: "按模型",
|
|
33494
|
+
records: "請求記錄",
|
|
33495
|
+
expand: "全部展開",
|
|
33496
|
+
collapse: "全部收起",
|
|
33497
|
+
loading: "正在讀取會話用量…",
|
|
33498
|
+
empty: "本會話還沒有請求記錄",
|
|
33499
|
+
noChildren: "暫無子代理",
|
|
33500
|
+
noModels: "暫無模型用量",
|
|
33501
|
+
scope: "本會話及所有子代理 · 歷史日誌累計",
|
|
33502
|
+
note: "統計包含本會話與全部後代的獨立請求、重試和壓縮呼叫,不重複計算繼承歷史。首 token 與速度僅使用已記錄的時間資訊,缺失時顯示 —。",
|
|
33503
|
+
failedLoad: "用量讀取失敗,已保留上次結果。",
|
|
33504
|
+
retry: "重新讀取",
|
|
33505
|
+
completed: "已完成",
|
|
33506
|
+
running: "進行中",
|
|
33507
|
+
failed: "失敗",
|
|
33508
|
+
interrupted: "已中斷",
|
|
33509
|
+
main: "主會話",
|
|
33510
|
+
turn: "輪",
|
|
33511
|
+
step: "步",
|
|
33512
|
+
started: "開始",
|
|
33513
|
+
duration: "耗時",
|
|
33514
|
+
firstDetail: "首 token",
|
|
33515
|
+
provider: "供應商",
|
|
33516
|
+
status: "狀態",
|
|
33517
|
+
exact: "完整 token 數",
|
|
33518
|
+
answering: "正在回答...",
|
|
33519
|
+
firstWait: "首字耗時",
|
|
33520
|
+
sec: "秒",
|
|
33521
|
+
tokenWord: "token",
|
|
33522
|
+
perSec: "token/秒",
|
|
33523
|
+
cacheShort: "快取",
|
|
33524
|
+
avgCache: "平均快取",
|
|
33525
|
+
avgRate: "平均速率",
|
|
33526
|
+
inProgress: "個進行中"
|
|
33527
|
+
};
|
|
33528
|
+
//#endregion
|
|
32400
33529
|
//#region src/client/index.tsx
|
|
32401
33530
|
/** This bundle's npm package name — the key the Plugins page matches on. */
|
|
32402
33531
|
const BUNDLE_NAME = "dsh-token-total";
|
|
@@ -32513,11 +33642,16 @@ window.__ModuleLoader__.load({
|
|
|
32513
33642
|
const offEn = ctx.locale.register(LOCALE_NS, "en", en);
|
|
32514
33643
|
const offZhTw = ctx.locale.register(LOCALE_NS, "zh-TW", zhTW);
|
|
32515
33644
|
const offContext = Object.entries(CONTEXT_LOCALES).map(([locale, dict]) => ctx.locale.register(CONTEXT_LOCALE_NS, locale, dict));
|
|
33645
|
+
const offSession = [
|
|
33646
|
+
ctx.locale.register(SESSION_USAGE_NS, "zh", usageZh),
|
|
33647
|
+
ctx.locale.register(SESSION_USAGE_NS, "en", usageEn),
|
|
33648
|
+
ctx.locale.register(SESSION_USAGE_NS, "zh-TW", usageZhTW)
|
|
33649
|
+
];
|
|
32516
33650
|
return () => {
|
|
32517
33651
|
offZh();
|
|
32518
33652
|
offEn();
|
|
32519
33653
|
offZhTw();
|
|
32520
|
-
for (const off of offContext) off();
|
|
33654
|
+
for (const off of [...offContext, ...offSession]) off();
|
|
32521
33655
|
};
|
|
32522
33656
|
}, "dsh-token-total: dictionaries");
|
|
32523
33657
|
ctx.effect(() => {
|
|
@@ -32578,6 +33712,14 @@ window.__ModuleLoader__.load({
|
|
|
32578
33712
|
label: () => t("nav"),
|
|
32579
33713
|
locale: LOCALE_NS
|
|
32580
33714
|
}, StatsPanelIcon));
|
|
33715
|
+
const sessionT = ctx.locale.bind(SESSION_USAGE_NS);
|
|
33716
|
+
ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
33717
|
+
name: "conversation.view",
|
|
33718
|
+
id: "token-total-usage",
|
|
33719
|
+
order: 20,
|
|
33720
|
+
label: () => sessionT("tab"),
|
|
33721
|
+
locale: SESSION_USAGE_NS
|
|
33722
|
+
}, SessionUsageView));
|
|
32581
33723
|
ctx.slots.inject(COMPOSER_DOCK, () => {
|
|
32582
33724
|
const dockIds = () => new Set(ctx.slots.entries(COMPOSER_DOCK).map((entry) => entry.options.id).filter((id) => id !== void 0));
|
|
32583
33725
|
let decided = false;
|