@kenz1117/dsh-ui-usage-billing 0.2.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -36
- package/lib/client.js +1634 -276
- package/lib/index.js +829 -80
- package/lib/types/aggregate.d.ts +121 -13
- package/lib/types/client/TrendChart.d.ts +5 -4
- package/lib/types/client/UsageBilling.d.ts +30 -4
- package/lib/types/client/apply.d.ts +31 -0
- package/lib/types/client/billing-service.d.ts +40 -0
- package/lib/types/client/budget-store.d.ts +32 -0
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/pricing.d.ts +39 -10
- package/lib/types/index.d.ts +22 -1
- package/lib/types/pricing-shared.d.ts +39 -0
- package/lib/types/subscriptions.d.ts +59 -0
- package/package.json +5 -1
package/lib/client.js
CHANGED
|
@@ -7,6 +7,7 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
10
11
|
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
11
12
|
function r(e) {
|
|
12
13
|
var t, f, n = "";
|
|
@@ -23,7 +24,7 @@ window.__ModuleLoader__.load({
|
|
|
23
24
|
}
|
|
24
25
|
//#endregion
|
|
25
26
|
//#region \0dsh-css:/Users/ken/deepseek-harness/packages/client/ui-usage-billing/src/client/UsageBilling.module.css.mjs
|
|
26
|
-
const css = ".VWh0dG_railButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:36px;height:36px;color:var(--dsw-static-blue-500);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;padding:0;transition:background-color .14s,border-color .14s,color .14s;display:flex}.VWh0dG_railButton:hover,.VWh0dG_railButton:focus-visible{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3);color:var(--dsw-static-blue-400);outline:none}.VWh0dG_railButton svg{stroke-width:2px;width:18px;height:18px}.VWh0dG_trigger{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border-radius:12px;align-items:center;gap:12px;padding:10px 14px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_triggerIcon{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:1.8px;width:15px;height:15px}.VWh0dG_triggerToday,.VWh0dG_triggerMonth{align-items:baseline;gap:4px;min-width:0;display:flex}.VWh0dG_triggerDivider{background:var(--dsw-alias-border-l1);align-self:stretch;width:1px}.VWh0dG_triggerAmount{color:var(--dsw-alias-label-primary);letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:17px;font-weight:600;line-height:22px}.VWh0dG_triggerAmountSub{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:13px;font-weight:600;line-height:18px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:10.5px;line-height:14px}.VWh0dG_dashboardModal{width:min(760px,100vw - 48px);max-height:min(760px,88vh)}.VWh0dG_dashboard{flex-direction:column;width:100%;max-height:min(760px,88vh);display:flex}.VWh0dG_dashboardHead{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:flex-start;gap:12px;padding:20px 24px 14px;display:flex}.VWh0dG_dashboardTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:17px;font-weight:600;line-height:24px}.VWh0dG_dashboardSubtitle{color:var(--dsw-alias-label-caption);margin:3px 0 0;font-size:12px;line-height:17px}.VWh0dG_closeButton{width:30px;height:30px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;transition:background-color .14s;display:inline-flex}.VWh0dG_closeButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_closeButton svg{stroke-width:2px;width:16px;height:16px}.VWh0dG_dashboardBody{flex-direction:column;gap:14px;padding:16px 24px 24px;display:flex;overflow-y:auto}.VWh0dG_hero{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;justify-content:space-between;align-items:center;gap:24px;padding:20px 24px;display:flex}.VWh0dG_heroMain{flex-direction:column;gap:4px;min-width:0;display:flex}.VWh0dG_heroLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:40px;font-weight:600;line-height:48px}.VWh0dG_heroMeta{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSide{flex-direction:column;flex:none;gap:10px;display:flex}.VWh0dG_heroSideItem{align-items:baseline;gap:8px;display:flex}.VWh0dG_heroSideLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:16px;font-weight:600;line-height:22px}.VWh0dG_delta{align-items:center;gap:2px;margin-left:4px;font-size:11px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:var(--dsw-static-green-500)}.VWh0dG_deltaDown{color:var(--dsw-static-red-500)}.VWh0dG_kpiGrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;display:grid}.VWh0dG_kpiTile{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:14px;flex-direction:column;gap:3px;padding:14px 15px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_kpiTile:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_kpiLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}.VWh0dG_kpiValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:21px;font-weight:600;line-height:27px}.VWh0dG_kpiGreen{color:var(--dsw-static-green-500)}.VWh0dG_kpiDetail{color:var(--dsw-alias-label-caption);white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:15px;overflow:hidden}.VWh0dG_panel{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:10px;padding:16px 18px;display:flex}.VWh0dG_panelHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.VWh0dG_panelTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:600;line-height:20px}.VWh0dG_panelHint{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:11px;line-height:16px}.VWh0dG_chartWrap{width:100%;position:relative}.VWh0dG_chartSvg{width:100%;height:auto;display:block}.VWh0dG_chartEmpty{height:140px;color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;font-size:13px;display:flex}.VWh0dG_emptyRow{text-align:center;color:var(--dsw-alias-label-caption);padding:28px 0;font-size:13px}.VWh0dG_chartGrid{stroke:var(--dsw-alias-border-l1);stroke-width:1px}.VWh0dG_chartAxisLabel{fill:var(--dsw-alias-label-tertiary);font-size:10px}.VWh0dG_chartBar{fill:var(--dsw-static-blue-500);opacity:.35}.VWh0dG_chartBar:hover{opacity:.6}.VWh0dG_chartStack{stroke:var(--dsw-alias-bg-layer-1);stroke-width:.75px}.VWh0dG_chartStack:hover{opacity:.85}.VWh0dG_chartLine{stroke:var(--dsw-static-blue-500);stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.VWh0dG_chartCrosshair{stroke:var(--dsw-alias-label-dimmed);stroke-width:1px;stroke-dasharray:3 3}.VWh0dG_chartDot{fill:var(--dsw-static-neutral-bluish-00);stroke:var(--dsw-static-blue-500);stroke-width:2.5px}.VWh0dG_chartTooltip{pointer-events:none;z-index:2;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);white-space:nowrap;border-radius:10px;padding:8px 10px;position:absolute;transform:translate(-50%,calc(-100% - 10px))}.VWh0dG_chartTooltipDate{color:var(--dsw-alias-label-primary);margin-bottom:3px;font-size:11px;font-weight:600}.VWh0dG_chartTooltipRow{color:var(--dsw-alias-label-secondary);align-items:center;gap:5px;font-size:11px;line-height:17px;display:flex}.VWh0dG_chartTooltipRow strong{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:600}.VWh0dG_chartLegendLine{background:var(--dsw-static-blue-500);border-radius:2px;width:10px;height:3px;display:inline-block}.VWh0dG_chartLegendBar{background:var(--dsw-static-blue-500);opacity:.5;border-radius:2px;width:7px;height:7px;display:inline-block}.VWh0dG_chartTooltipSwatch{border-radius:3px;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_chartLegend{color:var(--dsw-alias-label-tertiary);justify-content:flex-end;gap:14px;margin-top:2px;font-size:11px;display:flex}.VWh0dG_chartLegend span{align-items:center;gap:5px;display:inline-flex}.VWh0dG_tableScroll{border:1px solid var(--dsw-alias-border-l1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;overflow:auto}.VWh0dG_modelTable,.VWh0dG_pricingTable{border-collapse:collapse;width:100%;font-size:12.5px}.VWh0dG_modelTable th,.VWh0dG_modelTable td,.VWh0dG_pricingTable th,.VWh0dG_pricingTable td{border-bottom:1px solid var(--dsw-alias-border-l1);text-align:left;white-space:nowrap;padding:9px 12px}.VWh0dG_modelTable tbody tr:last-child td,.VWh0dG_pricingTable tbody tr:last-child td{border-bottom:0}.VWh0dG_modelTable thead th,.VWh0dG_pricingTable thead th{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);font-size:11px;font-weight:500}.VWh0dG_modelTable tbody tr,.VWh0dG_pricingTable tbody tr{transition:background-color .12s}.VWh0dG_modelTable tbody tr:hover,.VWh0dG_pricingTable tbody tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_numCol{text-align:right;font-variant-numeric:tabular-nums}.VWh0dG_costCol{color:var(--dsw-static-blue-500);font-weight:600}.VWh0dG_na{color:var(--dsw-alias-label-dimmed)}.VWh0dG_modelCell{align-items:center;gap:8px;display:inline-flex}.VWh0dG_modelDot{width:9px;height:9px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 60%, transparent);border-radius:50%;flex:none}.VWh0dG_modelName{color:var(--dsw-alias-label-primary);font-weight:500;line-height:16px;display:block}.VWh0dG_modelProvider{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px;display:block}.VWh0dG_pricingToggle{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:10px;width:100%;padding:0;display:flex}.VWh0dG_pricingToggle:hover .VWh0dG_panelTitle,.VWh0dG_pricingToggle:focus-visible .VWh0dG_panelTitle{color:var(--dsw-static-blue-500)}.VWh0dG_rateBadge{vertical-align:1px;white-space:nowrap;border-radius:999px;align-items:center;margin-left:6px;padding:1px 6px;font-size:10px;font-weight:500;line-height:14px;display:inline-flex}.VWh0dG_rateBadgeLive{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_rateBadgeBuiltin{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform)}.VWh0dG_pricingToggleText{align-items:baseline;gap:10px;min-width:0;display:flex}.VWh0dG_pricingChevron{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.VWh0dG_pricingChevronOpen{transform:rotate(180deg)}.VWh0dG_bandPrice{align-items:baseline;gap:6px;display:inline-flex}.VWh0dG_bandPriceOff{color:var(--dsw-alias-label-caption)}.VWh0dG_bandPriceOff:before{content:\"/\";color:var(--dsw-alias-label-dimmed);margin-right:4px}.VWh0dG_bandTag{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:1px;font-size:10.5px;line-height:14px;display:inline-flex}.VWh0dG_bandTagOff{color:var(--dsw-static-green-500);font-weight:600}.VWh0dG_flatTag{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px}.VWh0dG_healthDot{border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_healthOk{background:var(--dsw-static-green-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-green-500) 18%, transparent)}.VWh0dG_healthBad{background:var(--dsw-static-red-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-red-500) 18%, transparent)}.VWh0dG_healthIdle{background:var(--dsw-static-neutral-bluish-400)}.VWh0dG_dashboardRight{align-items:center;gap:8px;min-width:0;display:flex}.VWh0dG_healthBadge{white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;line-height:16px;display:inline-flex}.VWh0dG_healthBadgeOk{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_healthBadgeBad{color:var(--dsw-static-red-500);background:color-mix(in srgb, var(--dsw-static-red-500) 10%, transparent)}.VWh0dG_planTag{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 12%, transparent);border-radius:999px;align-items:center;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}@media (width<=640px){.VWh0dG_kpiGrid{grid-template-columns:repeat(2,minmax(0,1fr))}.VWh0dG_hero{flex-direction:column;align-items:flex-start;gap:14px}.VWh0dG_heroSide{flex-direction:row;gap:20px}}";
|
|
27
|
+
const css = ".VWh0dG_railButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:36px;height:36px;color:var(--dsw-static-blue-500);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;padding:0;transition:background-color .14s,border-color .14s,color .14s;display:flex}.VWh0dG_railButton:hover,.VWh0dG_railButton:focus-visible{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3);color:var(--dsw-static-blue-400);outline:none}.VWh0dG_railButton svg{stroke-width:2px;width:18px;height:18px}.VWh0dG_trigger{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border-radius:12px;align-items:center;gap:12px;padding:10px 14px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_triggerIcon{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:1.8px;width:15px;height:15px}.VWh0dG_triggerToday,.VWh0dG_triggerMonth{align-items:baseline;gap:4px;min-width:0;display:flex}.VWh0dG_triggerDivider{background:var(--dsw-alias-border-l1);align-self:stretch;width:1px}.VWh0dG_triggerAmount{color:var(--dsw-alias-label-primary);letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:17px;font-weight:600;line-height:22px}.VWh0dG_triggerAmountSub{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:13px;font-weight:600;line-height:18px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:10.5px;line-height:14px}.VWh0dG_dashboardModal{width:min(760px,100vw - 48px);max-height:min(760px,88vh)}.VWh0dG_dashboard{flex-direction:column;width:100%;max-height:min(760px,88vh);display:flex}.VWh0dG_dashboardHead{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:flex-start;gap:12px;padding:20px 24px 14px;display:flex}.VWh0dG_dashboardTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:17px;font-weight:600;line-height:24px}.VWh0dG_dashboardSubtitle{color:var(--dsw-alias-label-caption);margin:3px 0 0;font-size:12px;line-height:17px}.VWh0dG_closeButton{width:30px;height:30px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;transition:background-color .14s;display:inline-flex}.VWh0dG_closeButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_closeButton svg{stroke-width:2px;width:16px;height:16px}.VWh0dG_dashboardBody{flex-direction:column;gap:14px;padding:16px 24px 24px;display:flex;overflow-y:auto}.VWh0dG_hero{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;justify-content:space-between;align-items:center;gap:24px;padding:20px 24px;display:flex}.VWh0dG_heroMain{flex-direction:column;gap:4px;min-width:0;display:flex}.VWh0dG_heroLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:40px;font-weight:600;line-height:48px}.VWh0dG_heroMeta{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSide{flex-direction:column;flex:none;gap:10px;display:flex}.VWh0dG_heroSideItem{align-items:baseline;gap:8px;display:flex}.VWh0dG_heroSideLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:16px;font-weight:600;line-height:22px}.VWh0dG_delta{align-items:center;gap:2px;margin-left:4px;font-size:11px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:var(--dsw-static-green-500)}.VWh0dG_deltaDown{color:var(--dsw-static-red-500)}.VWh0dG_kpiGrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;display:grid}.VWh0dG_kpiTile{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:14px;flex-direction:column;gap:3px;padding:14px 15px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_kpiTile:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_kpiLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}.VWh0dG_kpiValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:21px;font-weight:600;line-height:27px}.VWh0dG_kpiGreen{color:var(--dsw-static-green-500)}.VWh0dG_kpiDetail{color:var(--dsw-alias-label-caption);white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:15px;overflow:hidden}.VWh0dG_panel{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:10px;padding:16px 18px;display:flex}.VWh0dG_panelHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.VWh0dG_panelTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:600;line-height:20px}.VWh0dG_panelHint{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:11px;line-height:16px}.VWh0dG_rangeToggle{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:8px;gap:2px;padding:2px;display:inline-flex}.VWh0dG_rangeButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:2px 8px;font-size:11px;line-height:16px;transition:background-color .14s,color .14s}.VWh0dG_rangeButtonActive{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);font-weight:600}.VWh0dG_budget{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:14px;flex-direction:column;gap:8px;padding:12px 16px;display:flex}.VWh0dG_budgetHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.VWh0dG_budgetLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px}.VWh0dG_budgetValue{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_budgetControls{align-items:center;gap:8px;min-width:0;display:inline-flex}.VWh0dG_budgetInputWrap{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:6px;align-items:center;gap:2px;padding:2px 6px;display:inline-flex}.VWh0dG_budgetInputWrap:focus-within{border-color:var(--dsw-alias-border-l3)}.VWh0dG_budgetUnit{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.VWh0dG_budgetInput{width:64px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-align:right;background:0 0;border:none;padding:0;font-size:12px;line-height:16px}.VWh0dG_budgetInput:focus-visible{outline:none}.VWh0dG_switch{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);cursor:pointer;border-radius:999px;flex:none;width:30px;height:17px;padding:0;transition:background-color .16s,border-color .16s;position:relative}.VWh0dG_switchOn{border-color:var(--dsw-static-blue-500);background:var(--dsw-static-blue-500)}.VWh0dG_switchKnob{background:var(--dsw-static-neutral-bluish-00);border-radius:50%;width:11px;height:11px;transition:transform .16s;position:absolute;top:2px;left:2px}.VWh0dG_switchOn .VWh0dG_switchKnob{transform:translate(13px)}.VWh0dG_budgetTrack{background:var(--dsw-alias-bg-module-platform);border-radius:999px;height:6px;overflow:hidden}.VWh0dG_budgetFill{background:var(--dsw-static-blue-500);border-radius:999px;height:100%;transition:width .2s}.VWh0dG_budgetFillOver{background:var(--dsw-static-red-500);animation:1.6s ease-in-out infinite VWh0dG_budgetOverPulse}@keyframes VWh0dG_budgetOverPulse{50%{opacity:.55}}.VWh0dG_chartWrap{width:100%;position:relative}.VWh0dG_chartSvg{width:100%;height:auto;display:block}.VWh0dG_chartEmpty{height:140px;color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;font-size:13px;display:flex}.VWh0dG_emptyRow{text-align:center;color:var(--dsw-alias-label-caption);padding:28px 0;font-size:13px}.VWh0dG_chartGrid{stroke:var(--dsw-alias-border-l1);stroke-width:1px}.VWh0dG_chartAxisLabel{fill:var(--dsw-alias-label-tertiary);font-size:10px}.VWh0dG_chartBar{fill:var(--dsw-static-blue-500);opacity:.35}.VWh0dG_chartBar:hover{opacity:.6}.VWh0dG_chartStack{stroke:var(--dsw-alias-bg-layer-1);stroke-width:.75px}.VWh0dG_chartStack:hover{opacity:.85}.VWh0dG_chartLine{stroke:var(--dsw-static-blue-500);stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.VWh0dG_chartCrosshair{stroke:var(--dsw-alias-label-dimmed);stroke-width:1px;stroke-dasharray:3 3}.VWh0dG_chartDot{fill:var(--dsw-static-neutral-bluish-00);stroke:var(--dsw-static-blue-500);stroke-width:2.5px}.VWh0dG_chartTooltip{pointer-events:none;z-index:2;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);white-space:nowrap;border-radius:10px;padding:8px 10px;position:absolute;transform:translate(-50%,calc(-100% - 10px))}.VWh0dG_chartTooltipDate{color:var(--dsw-alias-label-primary);margin-bottom:3px;font-size:11px;font-weight:600}.VWh0dG_chartTooltipRow{color:var(--dsw-alias-label-secondary);align-items:center;gap:5px;font-size:11px;line-height:17px;display:flex}.VWh0dG_chartTooltipRow strong{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:600}.VWh0dG_chartLegendLine{background:var(--dsw-static-blue-500);border-radius:2px;width:10px;height:3px;display:inline-block}.VWh0dG_chartLegendBar{background:var(--dsw-static-blue-500);opacity:.5;border-radius:2px;width:7px;height:7px;display:inline-block}.VWh0dG_chartTooltipSwatch{border-radius:3px;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_chartLegend{color:var(--dsw-alias-label-tertiary);justify-content:flex-end;gap:14px;margin-top:2px;font-size:11px;display:flex}.VWh0dG_chartLegend span{align-items:center;gap:5px;display:inline-flex}.VWh0dG_tableScroll{border:1px solid var(--dsw-alias-border-l1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;overflow:auto}.VWh0dG_modelTable,.VWh0dG_pricingTable{border-collapse:collapse;width:100%;font-size:12.5px}.VWh0dG_modelTable th,.VWh0dG_modelTable td,.VWh0dG_pricingTable th,.VWh0dG_pricingTable td{border-bottom:1px solid var(--dsw-alias-border-l1);text-align:left;white-space:nowrap;padding:9px 12px}.VWh0dG_modelTable tbody tr:last-child td,.VWh0dG_pricingTable tbody tr:last-child td{border-bottom:0}.VWh0dG_modelTable thead th,.VWh0dG_pricingTable thead th{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);font-size:11px;font-weight:500}.VWh0dG_modelTable tbody tr,.VWh0dG_pricingTable tbody tr{transition:background-color .12s}.VWh0dG_modelTable tbody tr:hover,.VWh0dG_pricingTable tbody tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_numCol{text-align:right;font-variant-numeric:tabular-nums}.VWh0dG_costCol{color:var(--dsw-static-blue-500);font-weight:600}.VWh0dG_na{color:var(--dsw-alias-label-dimmed)}.VWh0dG_modelCell{align-items:center;gap:8px;display:inline-flex}.VWh0dG_modelDot{width:9px;height:9px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 60%, transparent);border-radius:50%;flex:none}.VWh0dG_modelName{color:var(--dsw-alias-label-primary);font-weight:500;line-height:16px;display:block}.VWh0dG_modelProvider{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px;display:block}.VWh0dG_pricingToggle{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:10px;width:100%;padding:0;display:flex}.VWh0dG_pricingToggle:hover .VWh0dG_panelTitle,.VWh0dG_pricingToggle:focus-visible .VWh0dG_panelTitle{color:var(--dsw-static-blue-500)}.VWh0dG_rateBadge{vertical-align:1px;white-space:nowrap;border-radius:999px;align-items:center;margin-left:6px;padding:1px 6px;font-size:10px;font-weight:500;line-height:14px;display:inline-flex}.VWh0dG_rateBadgeLive{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_rateBadgeBuiltin{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform)}.VWh0dG_pricingToggleText{align-items:baseline;gap:10px;min-width:0;display:flex}.VWh0dG_pricingChevron{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.VWh0dG_pricingChevronOpen{transform:rotate(180deg)}.VWh0dG_bandPrice{align-items:baseline;gap:6px;display:inline-flex}.VWh0dG_bandPriceOff{color:var(--dsw-alias-label-caption)}.VWh0dG_bandPriceOff:before{content:\"/\";color:var(--dsw-alias-label-dimmed);margin-right:4px}.VWh0dG_bandTag{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:1px;font-size:10.5px;line-height:14px;display:inline-flex}.VWh0dG_bandTagOff{color:var(--dsw-static-green-500);font-weight:600}.VWh0dG_flatTag{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px}.VWh0dG_healthDot{border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_healthOk{background:var(--dsw-static-green-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-green-500) 18%, transparent)}.VWh0dG_healthBad{background:var(--dsw-static-red-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-red-500) 18%, transparent)}.VWh0dG_healthIdle{background:var(--dsw-static-neutral-bluish-400)}.VWh0dG_dashboardRight{align-items:center;gap:8px;min-width:0;display:flex}.VWh0dG_healthBadge{white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;line-height:16px;display:inline-flex}.VWh0dG_healthBadgeOk{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_healthBadgeBad{color:var(--dsw-static-red-500);background:color-mix(in srgb, var(--dsw-static-red-500) 10%, transparent)}.VWh0dG_planTag{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 12%, transparent);border-radius:999px;align-items:center;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.VWh0dG_uncataloguedTag{vertical-align:1px;color:var(--dsw-static-amber-500);background:color-mix(in srgb, var(--dsw-static-amber-500) 12%, transparent);border-radius:999px;margin-left:6px;padding:0 6px;font-size:10px;font-weight:600;line-height:16px;display:inline-block}.VWh0dG_balanceCell{flex-direction:column;align-items:flex-end;gap:2px;display:inline-flex}.VWh0dG_balanceDays{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:10.5px;line-height:14px}.VWh0dG_balanceDaysLow{color:var(--dsw-static-red-500);font-weight:600}body[data-zine-mode] .VWh0dG_trigger,body[data-zine-mode] .VWh0dG_railButton{display:none}body[data-zine-mode] .VWh0dG_dashboardHead{background:#000;border-bottom:2px solid #e8ff00}body[data-zine-mode] .VWh0dG_headTitleRow{align-items:center;gap:8px;margin-top:4px;display:flex}body[data-zine-mode] .VWh0dG_dashboardTitle{color:#e8ff00;letter-spacing:.04em;text-transform:uppercase;text-shadow:0 0 8px #e8ff0066;font-weight:900}body[data-zine-mode] .VWh0dG_dashboardSubtitle{color:#c9d98a;letter-spacing:.08em;text-transform:uppercase;font-size:10px}body[data-zine-mode] .VWh0dG_closeButton{color:#0a0a05;background:#ff2d95;border:1.5px solid #ff2d95;border-radius:0;box-shadow:0 0 10px #ff2d9573}body[data-zine-mode] .VWh0dG_hero{background:#000;border:2.5px solid #e8ff00;border-radius:0;position:relative;box-shadow:0 0 0 1px #0009,0 0 24px #e8ff002e}body[data-zine-mode] .VWh0dG_heroLabel{color:#c9d98a;letter-spacing:.12em;text-transform:uppercase;font-size:10px;font-weight:900}body[data-zine-mode] .VWh0dG_heroValue{color:#e8ff00;letter-spacing:-.02em;text-shadow:0 0 12px #e8ff0073;font-size:38px;font-weight:900}body[data-zine-mode] .VWh0dG_heroMeta{color:#c9d98a;letter-spacing:.06em;text-transform:uppercase;font-size:10.5px}body[data-zine-mode] .VWh0dG_heroSideLabel{color:#c9d98a;letter-spacing:.1em;text-transform:uppercase;font-size:10px;font-weight:900}body[data-zine-mode] .VWh0dG_heroSideValue{color:#e8ff00;text-shadow:0 0 6px #e8ff0059}body[data-zine-mode] .VWh0dG_delta{font-weight:900}body[data-zine-mode] .VWh0dG_deltaUp{color:#ff2d95}body[data-zine-mode] .VWh0dG_deltaDown{color:#c9d98a}body[data-zine-mode] .VWh0dG_panel{background:#000;border:2px solid #e8ff00;border-radius:0}body[data-zine-mode] .VWh0dG_trendPanel{position:relative}body[data-zine-mode] .VWh0dG_panelTitle{color:#e8ff00;letter-spacing:.06em;text-transform:uppercase;text-shadow:0 0 6px #e8ff0066;font-weight:900}body[data-zine-mode] .VWh0dG_panelHint{color:#c9d98a;letter-spacing:.08em;text-transform:uppercase;font-size:10px;font-weight:900}body[data-zine-mode] .VWh0dG_modelTableScroll{background:#000;border:1.5px solid #e8ff0080;border-radius:0}.VWh0dG_currencyToggle{border:1px solid var(--dsw-alias-border-muted,#3a4250);background:var(--dsw-alias-bg-layer-2,#232a33);border-radius:6px;align-items:center;gap:2px;margin-right:8px;padding:2px;display:inline-flex}.VWh0dG_currencyButton{color:var(--dsw-alias-text-secondary,#9aa5b1);cursor:pointer;background:0 0;border:0;border-radius:4px;padding:4px 7px;font-size:11px;font-weight:700;line-height:1}.VWh0dG_currencyButtonActive{background:var(--dsw-alias-accent-muted,#2f6fed);color:#fff}.VWh0dG_subscriptionGrid{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;margin-top:10px;display:grid}.VWh0dG_subscriptionCard{border:1px solid var(--dsw-alias-border-muted,#3a4250);background:var(--dsw-alias-bg-layer-2,#232a33);border-radius:8px;padding:10px 12px}.VWh0dG_subscriptionHead{align-items:baseline;gap:8px;margin-bottom:6px;display:flex}.VWh0dG_subscriptionName{font-size:13px;font-weight:700}.VWh0dG_subscriptionPlan{color:var(--dsw-alias-text-secondary,#9aa5b1);font-size:11px}.VWh0dG_subscriptionStatus{color:var(--dsw-alias-warning,#e0a63c);margin-bottom:6px;font-size:11px}.VWh0dG_subscriptionWindow{align-items:center;gap:8px;margin-top:6px;font-size:11px;display:flex}.VWh0dG_subscriptionWindowLabel{color:var(--dsw-alias-text-secondary,#9aa5b1);flex:0 0 44px}.VWh0dG_subscriptionTrack{background:var(--dsw-alias-bg-layer-3,#1a2028);border-radius:3px;flex:1;height:5px;overflow:hidden}.VWh0dG_subscriptionFill{background:var(--dsw-alias-accent,#3b82f6);border-radius:3px;height:100%;display:block}.VWh0dG_subscriptionPct{text-align:right;font-variant-numeric:tabular-nums;flex:none;min-width:52px}.VWh0dG_subscriptionReset{color:var(--dsw-alias-text-secondary,#9aa5b1);flex:none}.VWh0dG_providerGroupList{flex-direction:column;gap:10px;display:flex}.VWh0dG_providerGroup{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:12px 14px}.VWh0dG_providerGroupHead{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.VWh0dG_providerGroupTitle{align-items:center;gap:7px;min-width:0;display:inline-flex}.VWh0dG_providerGroupName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px}.VWh0dG_providerGroupMeta{flex:none;align-items:center;gap:10px;display:inline-flex}.VWh0dG_providerGroupBadge{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 12%, transparent);border-radius:999px;align-items:center;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.VWh0dG_providerGroupBalance{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;align-items:baseline;gap:6px;font-size:12px;line-height:17px;display:inline-flex}.VWh0dG_providerGroupBalanceLabel{color:var(--dsw-alias-label-caption);font-size:11px;line-height:17px}.VWh0dG_roundsFlagBadge{color:#fff;background:#ef4444;border-radius:999px;margin-left:8px;padding:1px 7px;font-size:10px;font-weight:800}.VWh0dG_rounds{margin-top:10px}.VWh0dG_roundsBars{align-items:flex-end;gap:3px;height:110px;padding:4px 2px 0;display:flex;overflow-x:auto}.VWh0dG_roundsBarCol{flex-direction:column;flex:1 0 28px;justify-content:flex-end;align-items:stretch;height:100%;display:flex}.VWh0dG_roundsBarLabel{text-align:center;color:var(--dsw-alias-text-secondary,#9aa5b1);white-space:nowrap;text-overflow:ellipsis;max-width:100%;margin-bottom:2px;font-size:8px;line-height:1;overflow:hidden}.VWh0dG_roundsBarWrap{flex:1;align-items:flex-end;min-height:0;display:flex}.VWh0dG_roundsBar{background:var(--dsw-alias-accent,#3b82f6);border-radius:2px 2px 0 0;width:100%;min-height:2px;position:relative}.VWh0dG_roundsBarFlagged{outline-offset:1px;background:#ef4444;border-radius:2px 2px 0 0;outline:1.5px solid #ef4444;width:100%;min-height:2px;position:relative}.VWh0dG_roundsFlagMark{background:#ef4444;border-radius:50%;width:7px;height:7px;position:absolute;top:-3px;right:-3px;box-shadow:0 0 4px #ef4444}.VWh0dG_roundsAxis{color:var(--dsw-alias-text-secondary,#9aa5b1);justify-content:space-between;margin-top:6px;font-size:10px;display:flex}.VWh0dG_roundsEmpty{color:var(--dsw-alias-text-secondary,#9aa5b1);margin-top:10px;font-size:12px}.VWh0dG_heatmap{margin-top:10px}.VWh0dG_heatmapGrid{grid-template-columns:repeat(7,minmax(0,1fr));gap:8px;display:grid}.VWh0dG_heatmapCellEmpty{background:var(--dsw-alias-bg-layer-1);height:30px;color:var(--dsw-alias-label-dimmed,#6b7480);font-variant-numeric:tabular-nums;border-radius:8px;justify-content:center;align-items:center;font-size:11px;display:flex}.VWh0dG_heatmapCell{font-variant-numeric:tabular-nums;cursor:pointer;border:0;border-radius:8px;justify-content:center;align-items:center;height:30px;padding:0;font-size:11px;display:flex}.VWh0dG_heatmapCell[data-level=\"0\"],.VWh0dG_heatmapCell[data-level=\"1\"],.VWh0dG_heatmapCell[data-level=\"2\"]{color:var(--dsw-alias-label-primary)}.VWh0dG_heatmapCell[data-level=\"3\"],.VWh0dG_heatmapCell[data-level=\"4\"]{color:var(--dsw-static-neutral-bluish-00,#fff)}.VWh0dG_heatmapFooter{color:var(--dsw-alias-text-secondary,#9aa5b1);align-items:center;gap:8px;margin-top:10px;font-size:10px;display:flex}.VWh0dG_heatmapLegend{gap:3px;display:inline-flex}.VWh0dG_heatmapLegend i{border-radius:4px;width:14px;height:14px;display:block}.VWh0dG_heatmapHover{font-variant-numeric:tabular-nums;margin-left:auto}@media (width<=640px){.VWh0dG_kpiGrid{grid-template-columns:repeat(2,minmax(0,1fr))}.VWh0dG_hero{flex-direction:column;align-items:flex-start;gap:14px}.VWh0dG_heroSide{flex-direction:row;gap:20px}}";
|
|
27
28
|
const tagId = "@kenz1117/dsh-ui-usage-billing/UsageBilling.module.css";
|
|
28
29
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
29
30
|
const tag = document.createElement("style");
|
|
@@ -33,94 +34,171 @@ window.__ModuleLoader__.load({
|
|
|
33
34
|
document.head.appendChild(tag);
|
|
34
35
|
}
|
|
35
36
|
var UsageBilling_module_css_default = {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"chartGrid": "VWh0dG_chartGrid",
|
|
45
|
-
"kpiTile": "VWh0dG_kpiTile",
|
|
46
|
-
"dashboard": "VWh0dG_dashboard",
|
|
47
|
-
"heroSideLabel": "VWh0dG_heroSideLabel",
|
|
48
|
-
"triggerDivider": "VWh0dG_triggerDivider",
|
|
49
|
-
"dashboardModal": "VWh0dG_dashboardModal",
|
|
50
|
-
"heroSide": "VWh0dG_heroSide",
|
|
51
|
-
"chartDot": "VWh0dG_chartDot",
|
|
37
|
+
"providerGroupBalance": "VWh0dG_providerGroupBalance",
|
|
38
|
+
"chartCrosshair": "VWh0dG_chartCrosshair",
|
|
39
|
+
"panelHead": "VWh0dG_panelHead",
|
|
40
|
+
"triggerAmountSub": "VWh0dG_triggerAmountSub",
|
|
41
|
+
"heatmapCellEmpty": "VWh0dG_heatmapCellEmpty",
|
|
42
|
+
"subscriptionTrack": "VWh0dG_subscriptionTrack",
|
|
43
|
+
"numCol": "VWh0dG_numCol",
|
|
44
|
+
"rangeButton": "VWh0dG_rangeButton",
|
|
52
45
|
"healthOk": "VWh0dG_healthOk",
|
|
53
|
-
"deltaUp": "VWh0dG_deltaUp",
|
|
54
|
-
"chartSvg": "VWh0dG_chartSvg",
|
|
55
|
-
"chartLegend": "VWh0dG_chartLegend",
|
|
56
|
-
"kpiGrid": "VWh0dG_kpiGrid",
|
|
57
|
-
"pricingChevron": "VWh0dG_pricingChevron",
|
|
58
46
|
"chartLegendLine": "VWh0dG_chartLegendLine",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"costCol": "VWh0dG_costCol",
|
|
62
|
-
"heroLabel": "VWh0dG_heroLabel",
|
|
63
|
-
"kpiValue": "VWh0dG_kpiValue",
|
|
64
|
-
"heroMain": "VWh0dG_heroMain",
|
|
65
|
-
"triggerMonth": "VWh0dG_triggerMonth",
|
|
66
|
-
"rateBadge": "VWh0dG_rateBadge",
|
|
47
|
+
"switchOn": "VWh0dG_switchOn",
|
|
48
|
+
"rangeButtonActive": "VWh0dG_rangeButtonActive",
|
|
67
49
|
"rateBadgeBuiltin": "VWh0dG_rateBadgeBuiltin",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
50
|
+
"flatTag": "VWh0dG_flatTag",
|
|
51
|
+
"balanceDays": "VWh0dG_balanceDays",
|
|
52
|
+
"triggerMeta": "VWh0dG_triggerMeta",
|
|
53
|
+
"kpiLabel": "VWh0dG_kpiLabel",
|
|
54
|
+
"bandPriceOff": "VWh0dG_bandPriceOff",
|
|
55
|
+
"triggerAmount": "VWh0dG_triggerAmount",
|
|
56
|
+
"budgetFillOver": "VWh0dG_budgetFillOver",
|
|
57
|
+
"bandTagOff": "VWh0dG_bandTagOff",
|
|
58
|
+
"subscriptionPlan": "VWh0dG_subscriptionPlan",
|
|
59
|
+
"rounds": "VWh0dG_rounds",
|
|
70
60
|
"dashboardHead": "VWh0dG_dashboardHead",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
61
|
+
"chartSvg": "VWh0dG_chartSvg",
|
|
62
|
+
"pricingTable": "VWh0dG_pricingTable",
|
|
63
|
+
"pricingToggle": "VWh0dG_pricingToggle",
|
|
64
|
+
"modelTable": "VWh0dG_modelTable",
|
|
65
|
+
"kpiTile": "VWh0dG_kpiTile",
|
|
66
|
+
"budget": "VWh0dG_budget",
|
|
67
|
+
"hero": "VWh0dG_hero",
|
|
68
|
+
"subscriptionStatus": "VWh0dG_subscriptionStatus",
|
|
69
|
+
"dashboardRight": "VWh0dG_dashboardRight",
|
|
70
|
+
"kpiGreen": "VWh0dG_kpiGreen",
|
|
71
|
+
"emptyRow": "VWh0dG_emptyRow",
|
|
72
|
+
"heatmapLegend": "VWh0dG_heatmapLegend",
|
|
73
|
+
"chartAxisLabel": "VWh0dG_chartAxisLabel",
|
|
74
|
+
"providerGroupBalanceLabel": "VWh0dG_providerGroupBalanceLabel",
|
|
75
|
+
"budgetValue": "VWh0dG_budgetValue",
|
|
76
|
+
"deltaDown": "VWh0dG_deltaDown",
|
|
77
|
+
"currencyButton": "VWh0dG_currencyButton",
|
|
78
|
+
"pricingChevron": "VWh0dG_pricingChevron",
|
|
79
|
+
"rangeToggle": "VWh0dG_rangeToggle",
|
|
80
|
+
"budgetLabel": "VWh0dG_budgetLabel",
|
|
81
|
+
"budgetOverPulse": "VWh0dG_budgetOverPulse",
|
|
75
82
|
"pricingChevronOpen": "VWh0dG_pricingChevronOpen",
|
|
83
|
+
"heatmapFooter": "VWh0dG_heatmapFooter",
|
|
84
|
+
"triggerMonth": "VWh0dG_triggerMonth",
|
|
85
|
+
"heroMain": "VWh0dG_heroMain",
|
|
86
|
+
"budgetInputWrap": "VWh0dG_budgetInputWrap",
|
|
87
|
+
"modelProvider": "VWh0dG_modelProvider",
|
|
88
|
+
"dashboardTitle": "VWh0dG_dashboardTitle",
|
|
89
|
+
"kpiDetail": "VWh0dG_kpiDetail",
|
|
90
|
+
"chartTooltipRow": "VWh0dG_chartTooltipRow",
|
|
91
|
+
"currencyButtonActive": "VWh0dG_currencyButtonActive",
|
|
92
|
+
"closeButton": "VWh0dG_closeButton",
|
|
93
|
+
"deltaUp": "VWh0dG_deltaUp",
|
|
94
|
+
"chartTooltip": "VWh0dG_chartTooltip",
|
|
95
|
+
"subscriptionHead": "VWh0dG_subscriptionHead",
|
|
96
|
+
"roundsBarWrap": "VWh0dG_roundsBarWrap",
|
|
97
|
+
"costCol": "VWh0dG_costCol",
|
|
76
98
|
"healthDot": "VWh0dG_healthDot",
|
|
77
|
-
"
|
|
78
|
-
"
|
|
99
|
+
"headTitleRow": "VWh0dG_headTitleRow",
|
|
100
|
+
"roundsBarCol": "VWh0dG_roundsBarCol",
|
|
79
101
|
"healthBadgeBad": "VWh0dG_healthBadgeBad",
|
|
80
|
-
"
|
|
81
|
-
"
|
|
102
|
+
"chartEmpty": "VWh0dG_chartEmpty",
|
|
103
|
+
"heatmapGrid": "VWh0dG_heatmapGrid",
|
|
104
|
+
"subscriptionGrid": "VWh0dG_subscriptionGrid",
|
|
105
|
+
"subscriptionWindow": "VWh0dG_subscriptionWindow",
|
|
106
|
+
"chartLegend": "VWh0dG_chartLegend",
|
|
107
|
+
"subscriptionFill": "VWh0dG_subscriptionFill",
|
|
82
108
|
"panelTitle": "VWh0dG_panelTitle",
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"panelHint": "VWh0dG_panelHint",
|
|
89
|
-
"bandPrice": "VWh0dG_bandPrice",
|
|
90
|
-
"closeButton": "VWh0dG_closeButton",
|
|
91
|
-
"modelTable": "VWh0dG_modelTable",
|
|
109
|
+
"roundsBarLabel": "VWh0dG_roundsBarLabel",
|
|
110
|
+
"heatmapCell": "VWh0dG_heatmapCell",
|
|
111
|
+
"heroLabel": "VWh0dG_heroLabel",
|
|
112
|
+
"heroValue": "VWh0dG_heroValue",
|
|
113
|
+
"heroSideItem": "VWh0dG_heroSideItem",
|
|
92
114
|
"modelDot": "VWh0dG_modelDot",
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"triggerMeta": "VWh0dG_triggerMeta",
|
|
97
|
-
"hero": "VWh0dG_hero",
|
|
98
|
-
"healthIdle": "VWh0dG_healthIdle",
|
|
99
|
-
"dashboardTitle": "VWh0dG_dashboardTitle",
|
|
100
|
-
"chartTooltipRow": "VWh0dG_chartTooltipRow",
|
|
115
|
+
"providerGroupMeta": "VWh0dG_providerGroupMeta",
|
|
116
|
+
"healthBadgeOk": "VWh0dG_healthBadgeOk",
|
|
117
|
+
"triggerIcon": "VWh0dG_triggerIcon",
|
|
101
118
|
"healthBadge": "VWh0dG_healthBadge",
|
|
119
|
+
"budgetHead": "VWh0dG_budgetHead",
|
|
120
|
+
"balanceCell": "VWh0dG_balanceCell",
|
|
121
|
+
"heroSide": "VWh0dG_heroSide",
|
|
122
|
+
"currencyToggle": "VWh0dG_currencyToggle",
|
|
123
|
+
"railButton": "VWh0dG_railButton",
|
|
124
|
+
"roundsEmpty": "VWh0dG_roundsEmpty",
|
|
125
|
+
"heatmap": "VWh0dG_heatmap",
|
|
126
|
+
"subscriptionWindowLabel": "VWh0dG_subscriptionWindowLabel",
|
|
127
|
+
"chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
|
|
128
|
+
"chartBar": "VWh0dG_chartBar",
|
|
129
|
+
"subscriptionCard": "VWh0dG_subscriptionCard",
|
|
130
|
+
"heroSideLabel": "VWh0dG_heroSideLabel",
|
|
131
|
+
"modelTableScroll": "VWh0dG_modelTableScroll",
|
|
132
|
+
"trigger": "VWh0dG_trigger",
|
|
102
133
|
"chartTooltipDate": "VWh0dG_chartTooltipDate",
|
|
134
|
+
"bandPrice": "VWh0dG_bandPrice",
|
|
135
|
+
"trendPanel": "VWh0dG_trendPanel",
|
|
136
|
+
"panel": "VWh0dG_panel",
|
|
137
|
+
"providerGroupList": "VWh0dG_providerGroupList",
|
|
138
|
+
"roundsBars": "VWh0dG_roundsBars",
|
|
139
|
+
"dashboardSubtitle": "VWh0dG_dashboardSubtitle",
|
|
140
|
+
"healthIdle": "VWh0dG_healthIdle",
|
|
141
|
+
"roundsAxis": "VWh0dG_roundsAxis",
|
|
142
|
+
"budgetControls": "VWh0dG_budgetControls",
|
|
143
|
+
"chartLine": "VWh0dG_chartLine",
|
|
144
|
+
"balanceDaysLow": "VWh0dG_balanceDaysLow",
|
|
145
|
+
"providerGroupBadge": "VWh0dG_providerGroupBadge",
|
|
146
|
+
"heroSideValue": "VWh0dG_heroSideValue",
|
|
147
|
+
"pricingToggleText": "VWh0dG_pricingToggleText",
|
|
148
|
+
"panelHint": "VWh0dG_panelHint",
|
|
149
|
+
"chartWrap": "VWh0dG_chartWrap",
|
|
103
150
|
"bandTag": "VWh0dG_bandTag",
|
|
104
|
-
"
|
|
151
|
+
"triggerDivider": "VWh0dG_triggerDivider",
|
|
152
|
+
"heatmapHover": "VWh0dG_heatmapHover",
|
|
153
|
+
"kpiValue": "VWh0dG_kpiValue",
|
|
154
|
+
"budgetUnit": "VWh0dG_budgetUnit",
|
|
155
|
+
"chartLegendBar": "VWh0dG_chartLegendBar",
|
|
156
|
+
"switch": "VWh0dG_switch",
|
|
157
|
+
"modelName": "VWh0dG_modelName",
|
|
158
|
+
"chartStack": "VWh0dG_chartStack",
|
|
159
|
+
"roundsBar": "VWh0dG_roundsBar",
|
|
160
|
+
"budgetFill": "VWh0dG_budgetFill",
|
|
161
|
+
"roundsFlagMark": "VWh0dG_roundsFlagMark",
|
|
162
|
+
"dashboard": "VWh0dG_dashboard",
|
|
163
|
+
"budgetInput": "VWh0dG_budgetInput",
|
|
164
|
+
"na": "VWh0dG_na",
|
|
165
|
+
"uncataloguedTag": "VWh0dG_uncataloguedTag",
|
|
166
|
+
"subscriptionPct": "VWh0dG_subscriptionPct",
|
|
167
|
+
"providerGroupTitle": "VWh0dG_providerGroupTitle",
|
|
168
|
+
"dashboardModal": "VWh0dG_dashboardModal",
|
|
105
169
|
"dashboardBody": "VWh0dG_dashboardBody",
|
|
106
|
-
"
|
|
107
|
-
"healthBadgeOk": "VWh0dG_healthBadgeOk",
|
|
108
|
-
"kpiLabel": "VWh0dG_kpiLabel",
|
|
109
|
-
"tableScroll": "VWh0dG_tableScroll",
|
|
110
|
-
"pricingToggle": "VWh0dG_pricingToggle",
|
|
111
|
-
"pricingToggleText": "VWh0dG_pricingToggleText",
|
|
112
|
-
"chartEmpty": "VWh0dG_chartEmpty",
|
|
113
|
-
"panel": "VWh0dG_panel",
|
|
114
|
-
"pricingTable": "VWh0dG_pricingTable",
|
|
115
|
-
"modelCell": "VWh0dG_modelCell",
|
|
116
|
-
"dashboardRight": "VWh0dG_dashboardRight",
|
|
117
|
-
"railButton": "VWh0dG_railButton",
|
|
170
|
+
"heroMeta": "VWh0dG_heroMeta",
|
|
118
171
|
"triggerToday": "VWh0dG_triggerToday",
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
172
|
+
"delta": "VWh0dG_delta",
|
|
173
|
+
"planTag": "VWh0dG_planTag",
|
|
174
|
+
"providerGroup": "VWh0dG_providerGroup",
|
|
175
|
+
"modelCell": "VWh0dG_modelCell",
|
|
176
|
+
"tableScroll": "VWh0dG_tableScroll",
|
|
177
|
+
"subscriptionReset": "VWh0dG_subscriptionReset",
|
|
178
|
+
"providerGroupHead": "VWh0dG_providerGroupHead",
|
|
179
|
+
"chartDot": "VWh0dG_chartDot",
|
|
180
|
+
"budgetTrack": "VWh0dG_budgetTrack",
|
|
181
|
+
"kpiGrid": "VWh0dG_kpiGrid",
|
|
182
|
+
"rateBadge": "VWh0dG_rateBadge",
|
|
183
|
+
"chartGrid": "VWh0dG_chartGrid",
|
|
184
|
+
"providerGroupName": "VWh0dG_providerGroupName",
|
|
185
|
+
"roundsFlagBadge": "VWh0dG_roundsFlagBadge",
|
|
186
|
+
"healthBad": "VWh0dG_healthBad",
|
|
187
|
+
"roundsBarFlagged": "VWh0dG_roundsBarFlagged",
|
|
188
|
+
"switchKnob": "VWh0dG_switchKnob",
|
|
189
|
+
"rateBadgeLive": "VWh0dG_rateBadgeLive",
|
|
190
|
+
"subscriptionName": "VWh0dG_subscriptionName"
|
|
123
191
|
};
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/client/pricing.ts
|
|
194
|
+
/**
|
|
195
|
+
* USD → CNY rate for display. Source: China Foreign Exchange Trade System
|
|
196
|
+
* mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
|
|
197
|
+
* USD-priced models — domestic models never pass through this rate.
|
|
198
|
+
* The node half may refresh this at boot via `/api/billing/pricing`; until a
|
|
199
|
+
* live rate arrives the built-in value stays in force.
|
|
200
|
+
*/
|
|
201
|
+
const USD_TO_CNY = 6.79;
|
|
124
202
|
/** 运行时实时覆盖:undefined = 用内置目录与内置汇率(默认值降级)。 */
|
|
125
203
|
let liveRate;
|
|
126
204
|
let livePrices;
|
|
@@ -183,6 +261,24 @@ window.__ModuleLoader__.load({
|
|
|
183
261
|
},
|
|
184
262
|
peakHours: "09:00-12:00 / 14:00-18:00"
|
|
185
263
|
},
|
|
264
|
+
{
|
|
265
|
+
key: "flash-vision-exp",
|
|
266
|
+
name: "DeepSeek V4 Flash Vision (Exp)",
|
|
267
|
+
provider: "DeepSeek",
|
|
268
|
+
colorVar: "dsw-static-blue-500",
|
|
269
|
+
price: {
|
|
270
|
+
currency: "CNY",
|
|
271
|
+
input: 3,
|
|
272
|
+
cacheHit: .1,
|
|
273
|
+
output: 9,
|
|
274
|
+
offPeak: {
|
|
275
|
+
input: 1.5,
|
|
276
|
+
cacheHit: .05,
|
|
277
|
+
output: 4.5
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
peakHours: "09:00-12:00 / 14:00-18:00"
|
|
281
|
+
},
|
|
186
282
|
{
|
|
187
283
|
key: "pro",
|
|
188
284
|
name: "DeepSeek V4 Pro",
|
|
@@ -623,14 +719,35 @@ window.__ModuleLoader__.load({
|
|
|
623
719
|
}
|
|
624
720
|
}
|
|
625
721
|
];
|
|
722
|
+
/**
|
|
723
|
+
* 真实 provider model id → 计费目录键(`MODEL_CATALOG[].key`)的映射。未知 id
|
|
724
|
+
* 原样保留并落回 `other`(未知模型不估算费用)。聚合层(aggregate.ts)在折叠时
|
|
725
|
+
* 用同一张表把日志里的 model id 归并为目录键,客户端渲染(`modelOf`)也按它
|
|
726
|
+
* 解析,两侧共用一份映射,避免同一模型两侧不一致导致「未收录」。
|
|
727
|
+
*/
|
|
728
|
+
const MODEL_KEY_ALIASES = {
|
|
729
|
+
"deepseek-v4-flash": "flash",
|
|
730
|
+
"deepseek-v4-flash-vision-exp": "flash-vision-exp",
|
|
731
|
+
"deepseek-v4-pro": "pro",
|
|
732
|
+
"glm-5.2": "glm",
|
|
733
|
+
"qwen3.8-max": "qwen-3.8-max",
|
|
734
|
+
"qwen3.7-max": "qwen-max",
|
|
735
|
+
"qwen-max": "qwen-max",
|
|
736
|
+
"hunyuan-t1": "hunyuan-t1",
|
|
737
|
+
"step-3.7-flash": "step",
|
|
738
|
+
"seed-2.0-mini": "doubao-mini",
|
|
739
|
+
"k3": "kimi-k3",
|
|
740
|
+
"kimi-k3": "kimi-k3"
|
|
741
|
+
};
|
|
626
742
|
/** Lookup a model by its stats key; falls back to the generic `other` entry. */
|
|
627
743
|
function modelOf(key) {
|
|
628
|
-
const
|
|
744
|
+
const resolved = MODEL_KEY_ALIASES[key] ?? key;
|
|
745
|
+
const base = MODEL_CATALOG.find((entry) => entry.key === resolved) ?? (() => {
|
|
629
746
|
const fallback = MODEL_CATALOG.at(-1);
|
|
630
747
|
if (fallback !== void 0) return fallback;
|
|
631
748
|
throw new Error("MODEL_CATALOG must not be empty");
|
|
632
749
|
})();
|
|
633
|
-
const live = livePrices?.[
|
|
750
|
+
const live = livePrices?.[resolved];
|
|
634
751
|
if (live === void 0) return base;
|
|
635
752
|
return {
|
|
636
753
|
...base,
|
|
@@ -681,14 +798,24 @@ window.__ModuleLoader__.load({
|
|
|
681
798
|
const off = entry.price.offPeak === void 0 ? peak : priceBandCost(entry.price.offPeak, buckets, entry.price.currency);
|
|
682
799
|
return peak * peakShare + off * (1 - peakShare);
|
|
683
800
|
}
|
|
684
|
-
/**
|
|
685
|
-
function
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
801
|
+
/** 人民币 → 美元(显示换算用):1 USD = {@link USD_TO_CNY} CNY。 */
|
|
802
|
+
function cnyToUsd(cny) {
|
|
803
|
+
return cny / USD_TO_CNY;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Format an amount with adaptive precision and the given currency symbol.
|
|
807
|
+
* @param amount - the amount (CNY by default; pass `usd` for dollar display).
|
|
808
|
+
* @param currency - display currency; default `cny`.
|
|
809
|
+
*/
|
|
810
|
+
function formatMoney(amount, currency = "cny") {
|
|
811
|
+
const value = Number(amount);
|
|
812
|
+
if (!Number.isFinite(value)) return currency === "cny" ? "¥0" : "$0";
|
|
813
|
+
const symbol = currency === "cny" ? "¥" : "$";
|
|
814
|
+
if (value <= 0) return `${symbol}0`;
|
|
815
|
+
if (value >= 1e3) return `${symbol}${value.toFixed(0)}`;
|
|
816
|
+
if (value >= 10) return `${symbol}${value.toFixed(1)}`;
|
|
817
|
+
if (value >= .1) return `${symbol}${value.toFixed(2)}`;
|
|
818
|
+
return `${symbol}${value.toFixed(3)}`;
|
|
692
819
|
}
|
|
693
820
|
/**
|
|
694
821
|
* Format a per-1M-token price in its native currency (free when the rate is
|
|
@@ -721,9 +848,10 @@ window.__ModuleLoader__.load({
|
|
|
721
848
|
/**
|
|
722
849
|
* TrendChart: dependency-free SVG chart of daily cost + calls.
|
|
723
850
|
*
|
|
724
|
-
* The columns are
|
|
725
|
-
*
|
|
726
|
-
*
|
|
851
|
+
* The columns are STACKED per day — one bar per day, with each model's cost
|
|
852
|
+
* as a colored segment inside the bar, so the daily total reads at a glance
|
|
853
|
+
* and the model mix stays visible. The blue line is the total call volume
|
|
854
|
+
* across all models, plotted on its own right-hand axis.
|
|
727
855
|
* A hover crosshair shows the day's model breakdown. No chart library — the
|
|
728
856
|
* surface stays self-contained and offline.
|
|
729
857
|
*/
|
|
@@ -761,12 +889,14 @@ window.__ModuleLoader__.load({
|
|
|
761
889
|
color: ""
|
|
762
890
|
};
|
|
763
891
|
/**
|
|
764
|
-
* Render the daily
|
|
892
|
+
* Render the daily stacked cost bars plus the total-calls line.
|
|
765
893
|
* @param props.data - sorted daily rows (ascending date).
|
|
766
894
|
* @param props.models - the model legend, in bar order.
|
|
895
|
+
* @param props.currency - display currency for the cost labels.
|
|
767
896
|
*/
|
|
768
|
-
function TrendChart({ data, models = [] }) {
|
|
897
|
+
function TrendChart({ data, models = [], currency = "cny" }) {
|
|
769
898
|
const [hover, setHover] = (0, react.useState)(null);
|
|
899
|
+
const money = (cny) => formatMoney(currency === "usd" ? cnyToUsd(cny) : cny, currency);
|
|
770
900
|
const layout = (0, react.useMemo)(() => {
|
|
771
901
|
const n = data.length;
|
|
772
902
|
if (n === 0) return null;
|
|
@@ -776,13 +906,12 @@ window.__ModuleLoader__.load({
|
|
|
776
906
|
if (n === 1) return PAD.left + plotW / 2;
|
|
777
907
|
return PAD.left + plotW * i / (n - 1);
|
|
778
908
|
};
|
|
779
|
-
const maxCost = Math.max(...data.
|
|
909
|
+
const maxCost = Math.max(...data.map((d) => Math.max(d.cost, Object.values(d.byModel ?? {}).reduce((sum, v) => sum + v, 0))), 1e-4);
|
|
780
910
|
const yCost = (value) => PAD.top + plotH - value / maxCost * plotH;
|
|
781
911
|
const maxCalls = Math.max(...data.map((d) => d.calls), 1);
|
|
782
912
|
const yCalls = (value) => PAD.top + plotH - value / maxCalls * plotH;
|
|
783
913
|
const groupW = plotW / n;
|
|
784
|
-
const
|
|
785
|
-
const barW = Math.min(14, groupW / barCount * .7);
|
|
914
|
+
const barW = Math.min(18, groupW * .6);
|
|
786
915
|
return {
|
|
787
916
|
n,
|
|
788
917
|
plotW,
|
|
@@ -792,18 +921,31 @@ window.__ModuleLoader__.load({
|
|
|
792
921
|
yCalls,
|
|
793
922
|
barW,
|
|
794
923
|
bars: data.flatMap((d, i) => {
|
|
924
|
+
const x = inner(i) - barW / 2;
|
|
795
925
|
if (models.length === 0) return [{
|
|
796
926
|
date: d.date,
|
|
797
927
|
model: TOTAL_MODEL,
|
|
798
|
-
x
|
|
799
|
-
|
|
928
|
+
x,
|
|
929
|
+
base: 0,
|
|
930
|
+
value: d.cost,
|
|
931
|
+
topRounded: true
|
|
800
932
|
}];
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
value
|
|
806
|
-
|
|
933
|
+
let topKey = null;
|
|
934
|
+
for (const model of models) if ((d.byModel?.[model.key] ?? 0) > 0) topKey = model.key;
|
|
935
|
+
let acc = 0;
|
|
936
|
+
return models.map((model) => {
|
|
937
|
+
const value = d.byModel?.[model.key] ?? 0;
|
|
938
|
+
const bar = {
|
|
939
|
+
date: d.date,
|
|
940
|
+
model,
|
|
941
|
+
x,
|
|
942
|
+
base: acc,
|
|
943
|
+
value,
|
|
944
|
+
topRounded: model.key === topKey
|
|
945
|
+
};
|
|
946
|
+
acc += value;
|
|
947
|
+
return bar;
|
|
948
|
+
});
|
|
807
949
|
}),
|
|
808
950
|
costTicks: [
|
|
809
951
|
0,
|
|
@@ -863,15 +1005,15 @@ window.__ModuleLoader__.load({
|
|
|
863
1005
|
y: y + 3,
|
|
864
1006
|
textAnchor: "end",
|
|
865
1007
|
className: UsageBilling_module_css_default.chartAxisLabel,
|
|
866
|
-
children:
|
|
1008
|
+
children: money(value)
|
|
867
1009
|
})] }, `cost-${idx}`);
|
|
868
1010
|
}),
|
|
869
1011
|
bars.map((bar) => bar.value > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
870
1012
|
x: bar.x,
|
|
871
|
-
y: yCost(bar.value),
|
|
1013
|
+
y: yCost(bar.base + bar.value),
|
|
872
1014
|
width: barW,
|
|
873
|
-
height: yCost(
|
|
874
|
-
rx: 2,
|
|
1015
|
+
height: yCost(bar.base) - yCost(bar.base + bar.value),
|
|
1016
|
+
rx: bar.topRounded ? 2 : 0,
|
|
875
1017
|
className: bar.model.color === "" ? UsageBilling_module_css_default.chartBar : UsageBilling_module_css_default.chartStack,
|
|
876
1018
|
style: bar.model.color === "" ? void 0 : { fill: bar.model.color }
|
|
877
1019
|
}, `${bar.date}-${bar.model.key}`) : null),
|
|
@@ -930,7 +1072,7 @@ window.__ModuleLoader__.load({
|
|
|
930
1072
|
}),
|
|
931
1073
|
model.name,
|
|
932
1074
|
" ",
|
|
933
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children:
|
|
1075
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: money(activePoint.byModel?.[model.key] ?? 0) })
|
|
934
1076
|
]
|
|
935
1077
|
}, model.key)),
|
|
936
1078
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -938,7 +1080,7 @@ window.__ModuleLoader__.load({
|
|
|
938
1080
|
children: [
|
|
939
1081
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendBar }),
|
|
940
1082
|
"总计 ",
|
|
941
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children:
|
|
1083
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: money(activePoint.cost) })
|
|
942
1084
|
]
|
|
943
1085
|
}),
|
|
944
1086
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -962,6 +1104,318 @@ window.__ModuleLoader__.load({
|
|
|
962
1104
|
});
|
|
963
1105
|
}
|
|
964
1106
|
//#endregion
|
|
1107
|
+
//#region src/client/round-chart.tsx
|
|
1108
|
+
/**
|
|
1109
|
+
* RoundCostChart: dependency-free per-turn cost bars with spike markers.
|
|
1110
|
+
*
|
|
1111
|
+
* One bar per turn (most recent N, newest last), height scaled to the window
|
|
1112
|
+
* maximum. Turns flagged by {@link flagAnomalies} get a warning outline and a
|
|
1113
|
+
* corner marker; hover shows the turn's model, cost, and window time. Styling
|
|
1114
|
+
* lives in the billing CSS module (`.rounds*`).
|
|
1115
|
+
*/
|
|
1116
|
+
/** Local time `HH:MM`. */
|
|
1117
|
+
function clock(time) {
|
|
1118
|
+
const date = new Date(time);
|
|
1119
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1120
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1121
|
+
}
|
|
1122
|
+
/** Local date + time for the hover line. */
|
|
1123
|
+
function dateTime(time) {
|
|
1124
|
+
const date = new Date(time);
|
|
1125
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1126
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Render the per-turn cost bars.
|
|
1130
|
+
* @param props.rounds - turns, most recent last (ascending startedAt); oldest beyond the limit are dropped.
|
|
1131
|
+
* @param props.flags - spike flags matched by sessionId+turn.
|
|
1132
|
+
* @param props.currency - display currency for the amount labels.
|
|
1133
|
+
* @param props.t - locale function for the model label.
|
|
1134
|
+
*/
|
|
1135
|
+
function RoundCostChart({ rounds, flags, currency, t }) {
|
|
1136
|
+
const visible = (0, react.useMemo)(() => rounds.slice(-40), [rounds]);
|
|
1137
|
+
const flagKey = (0, react.useMemo)(() => new Set(flags.map((flag) => `${flag.sessionId}:${flag.turn}`)), [flags]);
|
|
1138
|
+
const maxCost = (0, react.useMemo)(() => Math.max(1e-4, ...visible.map((round) => round.cost)), [visible]);
|
|
1139
|
+
if (visible.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1140
|
+
className: UsageBilling_module_css_default.roundsEmpty,
|
|
1141
|
+
children: [t("billing.model"), " —"]
|
|
1142
|
+
});
|
|
1143
|
+
const money = (cny) => formatMoney(currency === "usd" ? cnyToUsd(cny) : cny, currency);
|
|
1144
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1145
|
+
className: UsageBilling_module_css_default.rounds,
|
|
1146
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1147
|
+
className: UsageBilling_module_css_default.roundsBars,
|
|
1148
|
+
role: "img",
|
|
1149
|
+
"aria-label": "cost per turn",
|
|
1150
|
+
children: visible.map((round) => {
|
|
1151
|
+
const flagged = flagKey.has(`${round.sessionId}:${round.turn}`);
|
|
1152
|
+
const height = Math.max(1, round.cost / maxCost * 100);
|
|
1153
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1154
|
+
className: UsageBilling_module_css_default.roundsBarCol,
|
|
1155
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1156
|
+
className: UsageBilling_module_css_default.roundsBarLabel,
|
|
1157
|
+
children: money(round.cost)
|
|
1158
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1159
|
+
className: UsageBilling_module_css_default.roundsBarWrap,
|
|
1160
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1161
|
+
className: flagged ? UsageBilling_module_css_default.roundsBarFlagged : UsageBilling_module_css_default.roundsBar,
|
|
1162
|
+
style: { height: `${height}%` },
|
|
1163
|
+
"data-testid": "round-bar",
|
|
1164
|
+
title: `${t("billing.model")} ${round.model} · ${money(round.cost)} · ${dateTime(round.startedAt)}${round.endedAt !== void 0 ? ` → ${clock(round.endedAt)}` : ""}`,
|
|
1165
|
+
children: flagged && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1166
|
+
className: UsageBilling_module_css_default.roundsFlagMark,
|
|
1167
|
+
"aria-hidden": "true"
|
|
1168
|
+
})
|
|
1169
|
+
})
|
|
1170
|
+
})]
|
|
1171
|
+
}, `${round.sessionId}:${round.turn}`);
|
|
1172
|
+
})
|
|
1173
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1174
|
+
className: UsageBilling_module_css_default.roundsAxis,
|
|
1175
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1176
|
+
t("billing.costAbbr"),
|
|
1177
|
+
" ",
|
|
1178
|
+
money(maxCost)
|
|
1179
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [visible.length, " 轮"] })]
|
|
1180
|
+
})]
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
//#endregion
|
|
1184
|
+
//#region src/client/heatmap.tsx
|
|
1185
|
+
/**
|
|
1186
|
+
* UsageHeatmap: dependency-free month calendar heatmap of daily cost.
|
|
1187
|
+
*
|
|
1188
|
+
* Styled after an "activity map": one large rounded cell per day with the
|
|
1189
|
+
* date number printed inside, laid out in a 7-column grid (Sunday-first).
|
|
1190
|
+
* Week-first data layout: each week is one array element of 7 cells, so the
|
|
1191
|
+
* grid auto-rows place them correctly without per-cell gridColumnStart hacks.
|
|
1192
|
+
* Cell intensity is the day's cost quantized to five levels against the month
|
|
1193
|
+
* maximum (mint-green gradient, like the reference activity map). Leading
|
|
1194
|
+
* slots before the 1st and trailing slots after the last day carry the
|
|
1195
|
+
* cross-month dates as gray placeholders; future days of this month render as
|
|
1196
|
+
* gray placeholders too. Hover shows the exact date and amount.
|
|
1197
|
+
*/
|
|
1198
|
+
const LEVEL_COLORS = [
|
|
1199
|
+
"var(--dsw-alias-bg-layer-2)",
|
|
1200
|
+
"color-mix(in srgb, #10b981 22%, var(--dsw-alias-bg-layer-2))",
|
|
1201
|
+
"color-mix(in srgb, #10b981 45%, var(--dsw-alias-bg-layer-2))",
|
|
1202
|
+
"color-mix(in srgb, #10b981 70%, var(--dsw-alias-bg-layer-2))",
|
|
1203
|
+
"#10b981"
|
|
1204
|
+
];
|
|
1205
|
+
/** Local-time `YYYY-MM-DD` stamp (matches the dashboard's day keys). */
|
|
1206
|
+
function dayStamp(date) {
|
|
1207
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1208
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Build the current-month cells arranged in week rows (Sunday-first).
|
|
1212
|
+
* The grid is a complete calendar month view: leading slots before the 1st
|
|
1213
|
+
* carry the previous month's date number, trailing slots after the last day
|
|
1214
|
+
* carry the next month's date number, and every future day of this month is a
|
|
1215
|
+
* gray placeholder — so the rectangle always fills whole weeks, like the
|
|
1216
|
+
* reference activity map.
|
|
1217
|
+
*/
|
|
1218
|
+
function buildMonthWeeks(days, now) {
|
|
1219
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
1220
|
+
for (const day of days) byDate.set(day.date, day.value);
|
|
1221
|
+
let max = 0;
|
|
1222
|
+
for (const value of byDate.values()) if (value > max) max = value;
|
|
1223
|
+
const year = now.getFullYear();
|
|
1224
|
+
const month = now.getMonth();
|
|
1225
|
+
const today = now.getDate();
|
|
1226
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
1227
|
+
const firstDow = new Date(year, month, 1).getDay();
|
|
1228
|
+
const totalCells = firstDow + daysInMonth;
|
|
1229
|
+
const totalWeeks = Math.ceil(totalCells / 7);
|
|
1230
|
+
const weeks = [];
|
|
1231
|
+
for (let week = 0; week < totalWeeks; week += 1) {
|
|
1232
|
+
const row = [];
|
|
1233
|
+
for (let col = 0; col < 7; col += 1) {
|
|
1234
|
+
const dayNum = week * 7 + col - firstDow + 1;
|
|
1235
|
+
if (dayNum < 1) {
|
|
1236
|
+
const d = new Date(year, month, dayNum);
|
|
1237
|
+
row.push({
|
|
1238
|
+
date: dayStamp(d),
|
|
1239
|
+
dayNum: d.getDate(),
|
|
1240
|
+
value: 0,
|
|
1241
|
+
level: 0,
|
|
1242
|
+
placeholder: true
|
|
1243
|
+
});
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (dayNum > daysInMonth) {
|
|
1247
|
+
const d = new Date(year, month + 1, dayNum - daysInMonth);
|
|
1248
|
+
row.push({
|
|
1249
|
+
date: dayStamp(d),
|
|
1250
|
+
dayNum: d.getDate(),
|
|
1251
|
+
value: 0,
|
|
1252
|
+
level: 0,
|
|
1253
|
+
placeholder: true
|
|
1254
|
+
});
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const iso = dayStamp(new Date(year, month, dayNum));
|
|
1258
|
+
const value = byDate.get(iso) ?? 0;
|
|
1259
|
+
let level = 0;
|
|
1260
|
+
if (value > 0 && max > 0) {
|
|
1261
|
+
const scaled = Math.ceil(value / max * 4);
|
|
1262
|
+
level = Math.min(4, Math.max(1, scaled));
|
|
1263
|
+
}
|
|
1264
|
+
const isFuture = dayNum > today;
|
|
1265
|
+
row.push({
|
|
1266
|
+
date: iso,
|
|
1267
|
+
dayNum,
|
|
1268
|
+
value: isFuture ? 0 : value,
|
|
1269
|
+
level: isFuture ? 0 : level,
|
|
1270
|
+
placeholder: isFuture
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
weeks.push(row);
|
|
1274
|
+
}
|
|
1275
|
+
return weeks;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Render the month heatmap.
|
|
1279
|
+
* @param props.days - daily cost rows (keys are `YYYY-MM-DD`).
|
|
1280
|
+
* @param props.currency - display currency for the hover amount.
|
|
1281
|
+
* @param props.now - anchor date (defaults to today); injectable for tests.
|
|
1282
|
+
* @param props.t - locale function (used for the legend labels).
|
|
1283
|
+
*/
|
|
1284
|
+
function UsageHeatmap({ days, currency, now, t }) {
|
|
1285
|
+
const [hover, setHover] = (0, react.useState)(null);
|
|
1286
|
+
const weeks = (0, react.useMemo)(() => buildMonthWeeks(days, now ?? /* @__PURE__ */ new Date()), [days, now]);
|
|
1287
|
+
const money = (cny) => formatMoney(currency === "usd" ? cnyToUsd(cny) : cny, currency);
|
|
1288
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1289
|
+
className: UsageBilling_module_css_default.heatmap,
|
|
1290
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1291
|
+
className: UsageBilling_module_css_default.heatmapGrid,
|
|
1292
|
+
role: "img",
|
|
1293
|
+
"aria-label": "daily cost heatmap",
|
|
1294
|
+
children: weeks.map((week) => week.map((cell) => {
|
|
1295
|
+
if (cell.placeholder) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1296
|
+
className: UsageBilling_module_css_default.heatmapCellEmpty,
|
|
1297
|
+
children: cell.dayNum
|
|
1298
|
+
}, cell.date);
|
|
1299
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1300
|
+
type: "button",
|
|
1301
|
+
className: UsageBilling_module_css_default.heatmapCell,
|
|
1302
|
+
"data-testid": "heatmap-cell",
|
|
1303
|
+
"data-level": cell.level,
|
|
1304
|
+
style: { background: LEVEL_COLORS[cell.level] },
|
|
1305
|
+
title: `${cell.date} · ${money(cell.value)}`,
|
|
1306
|
+
"aria-label": `${cell.date}: ${money(cell.value)}`,
|
|
1307
|
+
onMouseEnter: () => {
|
|
1308
|
+
setHover(cell);
|
|
1309
|
+
},
|
|
1310
|
+
onMouseLeave: () => {
|
|
1311
|
+
setHover(null);
|
|
1312
|
+
},
|
|
1313
|
+
onFocus: () => {
|
|
1314
|
+
setHover(cell);
|
|
1315
|
+
},
|
|
1316
|
+
onBlur: () => {
|
|
1317
|
+
setHover(null);
|
|
1318
|
+
},
|
|
1319
|
+
children: cell.dayNum
|
|
1320
|
+
}, cell.date);
|
|
1321
|
+
}))
|
|
1322
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1323
|
+
className: UsageBilling_module_css_default.heatmapFooter,
|
|
1324
|
+
children: [
|
|
1325
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1326
|
+
className: UsageBilling_module_css_default.heatmapLegendText,
|
|
1327
|
+
children: t("billing.heatmapLess")
|
|
1328
|
+
}),
|
|
1329
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1330
|
+
className: UsageBilling_module_css_default.heatmapLegend,
|
|
1331
|
+
children: LEVEL_COLORS.map((color, level) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { background: color } }, level))
|
|
1332
|
+
}),
|
|
1333
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1334
|
+
className: UsageBilling_module_css_default.heatmapLegendText,
|
|
1335
|
+
children: t("billing.heatmapMore")
|
|
1336
|
+
}),
|
|
1337
|
+
hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1338
|
+
className: UsageBilling_module_css_default.heatmapHover,
|
|
1339
|
+
"data-testid": "heatmap-hover",
|
|
1340
|
+
children: [
|
|
1341
|
+
hover.date,
|
|
1342
|
+
" · ",
|
|
1343
|
+
money(hover.value)
|
|
1344
|
+
]
|
|
1345
|
+
})
|
|
1346
|
+
]
|
|
1347
|
+
})]
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#region src/client/anomaly.ts
|
|
1352
|
+
const DEFAULTS = {
|
|
1353
|
+
window: 6,
|
|
1354
|
+
threshold: 2,
|
|
1355
|
+
reasonFactor: 1.8,
|
|
1356
|
+
reasonHitDropPp: 15
|
|
1357
|
+
};
|
|
1358
|
+
function mean(values) {
|
|
1359
|
+
if (values.length === 0) return null;
|
|
1360
|
+
let sum = 0;
|
|
1361
|
+
for (const value of values) sum += value;
|
|
1362
|
+
return sum / values.length;
|
|
1363
|
+
}
|
|
1364
|
+
/** 输入侧缓存命中率(百分比);无输入时 null。 */
|
|
1365
|
+
function cacheHitRate(round) {
|
|
1366
|
+
const denominator = round.cacheHit + round.cacheMiss;
|
|
1367
|
+
return denominator <= 0 ? null : round.cacheHit / denominator * 100;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* 标记成本异常轮次(按时间顺序传入;最近的轮次排在末尾)。
|
|
1371
|
+
* @param rounds - 按起始时间升序的轮次序列(最早在前)。
|
|
1372
|
+
* @param options - 窗口/阈值/归因灵敏度。
|
|
1373
|
+
* @returns 异常标记数组(保持输入顺序)。
|
|
1374
|
+
*/
|
|
1375
|
+
function flagAnomalies(rounds, options) {
|
|
1376
|
+
const opts = {
|
|
1377
|
+
...DEFAULTS,
|
|
1378
|
+
...options
|
|
1379
|
+
};
|
|
1380
|
+
const flags = [];
|
|
1381
|
+
if (opts.window <= 0 || opts.threshold <= 0) return flags;
|
|
1382
|
+
const baselineCosts = [];
|
|
1383
|
+
const baselineOutputs = [];
|
|
1384
|
+
const baselineInputs = [];
|
|
1385
|
+
const baselineHits = [];
|
|
1386
|
+
for (const round of rounds) {
|
|
1387
|
+
if (round.cost <= 0) continue;
|
|
1388
|
+
const output = round.output;
|
|
1389
|
+
const input = round.input;
|
|
1390
|
+
const hit = cacheHitRate(round);
|
|
1391
|
+
const baselineCost = mean(baselineCosts);
|
|
1392
|
+
const reasons = [];
|
|
1393
|
+
if (baselineCost !== null && baselineCost > 0 && round.cost > baselineCost * opts.threshold) {
|
|
1394
|
+
const baselineOutput = mean(baselineOutputs);
|
|
1395
|
+
const baselineInput = mean(baselineInputs);
|
|
1396
|
+
const baselineHit = mean(baselineHits);
|
|
1397
|
+
if (baselineOutput !== null && baselineOutput > 0 && output > baselineOutput * opts.reasonFactor) reasons.push("output-growth");
|
|
1398
|
+
if (baselineInput !== null && baselineInput > 0 && input > baselineInput * opts.reasonFactor) reasons.push("context-bloat");
|
|
1399
|
+
if (baselineHit !== null && hit !== null && hit < baselineHit - opts.reasonHitDropPp) reasons.push("cache-hit-drop");
|
|
1400
|
+
flags.push({
|
|
1401
|
+
sessionId: round.sessionId,
|
|
1402
|
+
turn: round.turn,
|
|
1403
|
+
cost: round.cost,
|
|
1404
|
+
reasons
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
baselineCosts.push(round.cost);
|
|
1408
|
+
baselineOutputs.push(output);
|
|
1409
|
+
baselineInputs.push(input);
|
|
1410
|
+
if (hit !== null) baselineHits.push(hit);
|
|
1411
|
+
while (baselineCosts.length > opts.window) baselineCosts.shift();
|
|
1412
|
+
while (baselineOutputs.length > opts.window) baselineOutputs.shift();
|
|
1413
|
+
while (baselineInputs.length > opts.window) baselineInputs.shift();
|
|
1414
|
+
while (baselineHits.length > opts.window) baselineHits.shift();
|
|
1415
|
+
}
|
|
1416
|
+
return flags;
|
|
1417
|
+
}
|
|
1418
|
+
//#endregion
|
|
965
1419
|
//#region src/client/UsageBilling.tsx
|
|
966
1420
|
/**
|
|
967
1421
|
* UsageBilling: sidebar footer trigger + full billing dashboard modal.
|
|
@@ -974,11 +1428,18 @@ window.__ModuleLoader__.load({
|
|
|
974
1428
|
* before real data arrives the dashboard shows an empty (zero) snapshot,
|
|
975
1429
|
* never fabricated samples.
|
|
976
1430
|
*/
|
|
1431
|
+
/** 会话明细面板最多展示的行数(完整长尾在服务端另有一层封顶)。 */
|
|
1432
|
+
const SESSION_DISPLAY_LIMIT = 20;
|
|
1433
|
+
/** 项目名取 cwd 的末级目录;无 cwd 时由调用方回退为 em dash。 */
|
|
1434
|
+
function projectName(cwd) {
|
|
1435
|
+
if (cwd === void 0) return void 0;
|
|
1436
|
+
return cwd.split(/[\\/]/).filter(Boolean).pop() ?? cwd;
|
|
1437
|
+
}
|
|
977
1438
|
/** Idle health state before the probe settles. */
|
|
978
1439
|
const IDLE_HEALTH = {
|
|
979
1440
|
checked: false,
|
|
980
1441
|
available: false,
|
|
981
|
-
|
|
1442
|
+
models: 0,
|
|
982
1443
|
failures: 0,
|
|
983
1444
|
okProviders: [],
|
|
984
1445
|
badProviders: []
|
|
@@ -987,6 +1448,8 @@ window.__ModuleLoader__.load({
|
|
|
987
1448
|
* The dashboard's display names (中文厂商名) never equal the provider names a
|
|
988
1449
|
* user actually configures (deepseek, zhipu, qwen…), so the dot match also
|
|
989
1450
|
* accepts a bidirectional substring hit and a display-name alias list.
|
|
1451
|
+
* 导出供一致性守卫测试:catalog 每个厂商都必须在此登记(Custom 除外),
|
|
1452
|
+
* 防止新增厂商漏配导致健康绿灯不亮。
|
|
990
1453
|
*/
|
|
991
1454
|
const PROVIDER_ALIASES = {
|
|
992
1455
|
"DeepSeek": ["deepseek"],
|
|
@@ -1007,6 +1470,11 @@ window.__ModuleLoader__.load({
|
|
|
1007
1470
|
"ark"
|
|
1008
1471
|
],
|
|
1009
1472
|
"月之暗面": ["moonshot", "kimi"],
|
|
1473
|
+
"小米": [
|
|
1474
|
+
"xiaomi",
|
|
1475
|
+
"mi",
|
|
1476
|
+
"mimo"
|
|
1477
|
+
],
|
|
1010
1478
|
"MiniMax": ["minimax"],
|
|
1011
1479
|
"百度文心": [
|
|
1012
1480
|
"ernie",
|
|
@@ -1056,6 +1524,71 @@ window.__ModuleLoader__.load({
|
|
|
1056
1524
|
const aliases = PROVIDER_ALIASES[display];
|
|
1057
1525
|
return aliases !== void 0 && aliases.some((alias) => providerNameHits(normalizeProvider(alias), liveKey));
|
|
1058
1526
|
}
|
|
1527
|
+
/**
|
|
1528
|
+
* 从真实 model id 反推提供方显示名:目录未收录的模型(key 落回「其他」)
|
|
1529
|
+
* 只靠 entry.provider(Custom)永远点不亮健康灯,这里用厂商别名对 model id
|
|
1530
|
+
* 做强匹配(别名作为完整 id / 前缀 / 独立段)与弱匹配(长别名子串),
|
|
1531
|
+
* 命中即显示厂商名并点亮健康点;无命中保持 Custom。
|
|
1532
|
+
* 导出供守卫测试:短别名(mi/yi)仅允许前缀形式,防止 minimax 等误吞。
|
|
1533
|
+
*/
|
|
1534
|
+
function providerFromModelKey(modelKey) {
|
|
1535
|
+
const key = modelKey.trim().toLowerCase();
|
|
1536
|
+
const compact = key.replace(/[\s_/-]+/g, "");
|
|
1537
|
+
if (compact.length === 0) return void 0;
|
|
1538
|
+
for (const [display, aliases] of Object.entries(PROVIDER_ALIASES)) for (const alias of aliases) {
|
|
1539
|
+
const a = normalizeProvider(alias);
|
|
1540
|
+
if (a.length === 0) continue;
|
|
1541
|
+
if (key === a) return display;
|
|
1542
|
+
if (key.startsWith(`${a}-`) || key.startsWith(`${a}/`) || key.startsWith(`${a}_`)) return display;
|
|
1543
|
+
if (key.includes(`${a}-`) || key.includes(`${a}/`) || key.includes(`${a}_`)) return display;
|
|
1544
|
+
}
|
|
1545
|
+
for (const [display, aliases] of Object.entries(PROVIDER_ALIASES)) for (const alias of aliases) {
|
|
1546
|
+
const a = normalizeProvider(alias);
|
|
1547
|
+
if (a.length < 4) continue;
|
|
1548
|
+
if (compact.includes(a)) return display;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* 订阅套餐 provider id → 所属模型厂商(用于把订阅额度归并到对应厂商组)。
|
|
1553
|
+
* 厂商名与 PROVIDER_ALIASES 保持一致,使订阅卡片与模型用量落在同一组下。
|
|
1554
|
+
* opencode 是跨厂商订阅通道、无单一模型厂商,按自身显示名独立成组。
|
|
1555
|
+
*/
|
|
1556
|
+
const SUBSCRIPTION_VENDORS = {
|
|
1557
|
+
"kimi-coding": "月之暗面",
|
|
1558
|
+
"zai-coding-cn": "智谱 AI",
|
|
1559
|
+
"zai-coding": "智谱 AI",
|
|
1560
|
+
"qwen-token-plan": "阿里通义",
|
|
1561
|
+
"qwen-token-plan-cn": "阿里通义",
|
|
1562
|
+
"xiaomi-token-plan-ams": "小米",
|
|
1563
|
+
"xiaomi-token-plan-cn": "小米",
|
|
1564
|
+
"xiaomi-token-plan-sgp": "小米",
|
|
1565
|
+
"volcengine-token-plan": "字节豆包",
|
|
1566
|
+
"ark-token-plan": "字节豆包",
|
|
1567
|
+
"doubao-token-plan": "字节豆包",
|
|
1568
|
+
"ernie": "百度文心",
|
|
1569
|
+
"baidu": "百度文心",
|
|
1570
|
+
"wenxin": "百度文心",
|
|
1571
|
+
"minimax": "MiniMax",
|
|
1572
|
+
"opencode": "OpenCode",
|
|
1573
|
+
"opencode-go": "OpenCode"
|
|
1574
|
+
};
|
|
1575
|
+
/** 订阅套餐归并到的厂商显示名;未知 id 回退为从 model id 反推或 id 本身。 */
|
|
1576
|
+
function subscriptionVendorOf(provider) {
|
|
1577
|
+
const mapped = SUBSCRIPTION_VENDORS[provider];
|
|
1578
|
+
if (mapped !== void 0) return mapped;
|
|
1579
|
+
return providerFromModelKey(provider) ?? provider;
|
|
1580
|
+
}
|
|
1581
|
+
/** 余额不足告警的默认阈值(人民币元):宿主 Config 未配置时客户端兜底。 */
|
|
1582
|
+
const DEFAULT_LOW_BALANCE_THRESHOLD = 50;
|
|
1583
|
+
/**
|
|
1584
|
+
* 日均消耗(元/天):取最近 7 天(含今天)总花费 ÷ 有记录天数;无记录返回 0
|
|
1585
|
+
* (此时可用天数无法估算,调用方不显示天数提示)。日期戳字典序即时间序。
|
|
1586
|
+
*/
|
|
1587
|
+
function dailyBurnRate(byDay, today) {
|
|
1588
|
+
const dates = Object.keys(byDay).filter((d) => d <= today).sort().slice(-7);
|
|
1589
|
+
if (dates.length === 0) return 0;
|
|
1590
|
+
return dates.reduce((sum, d) => sum + (byDay[d]?.cost ?? 0), 0) / dates.length;
|
|
1591
|
+
}
|
|
1059
1592
|
/** Resolve one provider's dot state: green when live, red when failed, gray when unknown. */
|
|
1060
1593
|
function providerDot(health, provider) {
|
|
1061
1594
|
if (!health.checked) return UsageBilling_module_css_default.healthIdle;
|
|
@@ -1063,12 +1596,34 @@ window.__ModuleLoader__.load({
|
|
|
1063
1596
|
if (health.badProviders.some((live) => providerMatches(provider, live))) return UsageBilling_module_css_default.healthBad;
|
|
1064
1597
|
return UsageBilling_module_css_default.healthIdle;
|
|
1065
1598
|
}
|
|
1599
|
+
/** 订阅额度查询状态的文案(ok 时无需额外标注,返回空串)。 */
|
|
1600
|
+
function subscriptionStatusText(status, t) {
|
|
1601
|
+
switch (status) {
|
|
1602
|
+
case "ok": return "";
|
|
1603
|
+
case "not-configured": return t("billing.subscriptionNotConfigured");
|
|
1604
|
+
case "unauthorized": return t("billing.subscriptionUnauthorized");
|
|
1605
|
+
case "rate-limited": return t("billing.subscriptionRateLimited");
|
|
1606
|
+
case "invalid-response": return t("billing.subscriptionInvalid");
|
|
1607
|
+
default: return t("billing.subscriptionUnavailable");
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
/** 订阅额度窗口的类型标签(本次 / 本周 / 本月 / 计费周期)。 */
|
|
1611
|
+
function subscriptionWindowLabel(kind, t) {
|
|
1612
|
+
switch (kind) {
|
|
1613
|
+
case "session": return t("billing.subscriptionSession");
|
|
1614
|
+
case "weekly": return t("billing.subscriptionWeekly");
|
|
1615
|
+
case "monthly": return t("billing.subscriptionMonthly");
|
|
1616
|
+
case "billing": return t("billing.subscriptionBilling");
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1066
1619
|
/** Path to the usage-stats endpoint served by this plugin's node half. */
|
|
1067
1620
|
const USAGE_STATS_PATH = "/api/billing/usage-stats";
|
|
1068
1621
|
/** Path to the live-pricing endpoint served by this plugin's node half. */
|
|
1069
1622
|
const PRICING_PATH = "/api/billing/pricing";
|
|
1070
1623
|
/** Path to the account-balance endpoint served by this plugin's node half. */
|
|
1071
1624
|
const BALANCE_PATH = "/api/billing/balance";
|
|
1625
|
+
/** Path to the subscription-plan quota endpoint served by this plugin's node half. */
|
|
1626
|
+
const SUBSCRIPTIONS_PATH = "/api/billing/subscriptions";
|
|
1072
1627
|
/** 弹窗打开期间统计与定价的自动刷新间隔(毫秒)。 */
|
|
1073
1628
|
const STATS_REFRESH_INTERVAL_MS = 3e4;
|
|
1074
1629
|
/**
|
|
@@ -1130,7 +1685,12 @@ window.__ModuleLoader__.load({
|
|
|
1130
1685
|
byModel: candidate.byModel ?? {},
|
|
1131
1686
|
byDay: candidate.byDay ?? {},
|
|
1132
1687
|
...candidate.byDayModels !== void 0 ? { byDayModels: candidate.byDayModels } : {},
|
|
1133
|
-
...candidate.updatedAt !== void 0 ? { updatedAt: candidate.updatedAt } : {}
|
|
1688
|
+
...candidate.updatedAt !== void 0 ? { updatedAt: candidate.updatedAt } : {},
|
|
1689
|
+
...typeof candidate.budget === "number" ? { budget: candidate.budget } : {},
|
|
1690
|
+
...typeof candidate.lowBalanceThreshold === "number" ? { lowBalanceThreshold: candidate.lowBalanceThreshold } : {},
|
|
1691
|
+
...Array.isArray(candidate.bySession) ? { bySession: candidate.bySession } : {},
|
|
1692
|
+
...Array.isArray(candidate.byTurn) ? { byTurn: candidate.byTurn } : {},
|
|
1693
|
+
...Array.isArray(candidate.byWorkspace) ? { byWorkspace: candidate.byWorkspace } : {}
|
|
1134
1694
|
};
|
|
1135
1695
|
} catch {
|
|
1136
1696
|
return null;
|
|
@@ -1179,7 +1739,25 @@ window.__ModuleLoader__.load({
|
|
|
1179
1739
|
}
|
|
1180
1740
|
}
|
|
1181
1741
|
/**
|
|
1742
|
+
* 拉取订阅套餐剩余额度(供订阅面板);失败返回空列表。
|
|
1743
|
+
* @returns the quota rows, or an empty list on any failure.
|
|
1744
|
+
*/
|
|
1745
|
+
async function fetchSubscriptions() {
|
|
1746
|
+
try {
|
|
1747
|
+
const response = await fetch(SUBSCRIPTIONS_PATH);
|
|
1748
|
+
if (!response.ok) return [];
|
|
1749
|
+
const text = await response.text();
|
|
1750
|
+
const parsed = JSON.parse(text);
|
|
1751
|
+
if (parsed !== null && typeof parsed === "object" && "quotas" in parsed) return parsed.quotas;
|
|
1752
|
+
return [];
|
|
1753
|
+
} catch {
|
|
1754
|
+
return [];
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1182
1758
|
* Sidebar footer trigger: compact pill in wide mode, icon in rail mode.
|
|
1759
|
+
* ZINE 模式下入口由主题插件的贴纸层承担,本触发器由 CSS
|
|
1760
|
+
* (body[data-zine-mode] 选择器)隐藏,组件本身无 zine 分支。
|
|
1183
1761
|
* @param props - framework props plus `wide` column state.
|
|
1184
1762
|
*/
|
|
1185
1763
|
function UsageBillingTrigger(props) {
|
|
@@ -1210,6 +1788,7 @@ window.__ModuleLoader__.load({
|
|
|
1210
1788
|
if (!wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1211
1789
|
type: "button",
|
|
1212
1790
|
className: UsageBilling_module_css_default.railButton,
|
|
1791
|
+
"data-testid": "billing-rail-button",
|
|
1213
1792
|
onClick: onOpen,
|
|
1214
1793
|
title: `${t("billing.title")} · ${formatMoney(monthCost)}`,
|
|
1215
1794
|
children: cardIcon
|
|
@@ -1217,15 +1796,18 @@ window.__ModuleLoader__.load({
|
|
|
1217
1796
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1218
1797
|
type: "button",
|
|
1219
1798
|
className: UsageBilling_module_css_default.trigger,
|
|
1799
|
+
"data-testid": "billing-trigger",
|
|
1220
1800
|
onClick: onOpen,
|
|
1221
1801
|
title: `${t("billing.title")} · 本月 ${formatMoney(monthCost)}`,
|
|
1222
1802
|
children: [
|
|
1223
1803
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1224
1804
|
className: UsageBilling_module_css_default.triggerIcon,
|
|
1805
|
+
"data-testid": "billing-trigger-icon",
|
|
1225
1806
|
children: cardIcon
|
|
1226
1807
|
}),
|
|
1227
1808
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1228
1809
|
className: UsageBilling_module_css_default.triggerToday,
|
|
1810
|
+
"data-testid": "billing-trigger-today",
|
|
1229
1811
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1230
1812
|
className: UsageBilling_module_css_default.triggerMeta,
|
|
1231
1813
|
children: "今日"
|
|
@@ -1237,6 +1819,7 @@ window.__ModuleLoader__.load({
|
|
|
1237
1819
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.triggerDivider }),
|
|
1238
1820
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1239
1821
|
className: UsageBilling_module_css_default.triggerMonth,
|
|
1822
|
+
"data-testid": "billing-trigger-month",
|
|
1240
1823
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1241
1824
|
className: UsageBilling_module_css_default.triggerMeta,
|
|
1242
1825
|
children: "当月"
|
|
@@ -1250,12 +1833,20 @@ window.__ModuleLoader__.load({
|
|
|
1250
1833
|
}
|
|
1251
1834
|
/**
|
|
1252
1835
|
* The centered billing dashboard modal.
|
|
1253
|
-
* @param props - stats, locale function, close handler, model health, balances.
|
|
1836
|
+
* @param props - stats, locale function, close handler, model health, balances, renderSlot.
|
|
1254
1837
|
*/
|
|
1255
|
-
function BillingDashboard({ stats, t, onClose, health, balances }) {
|
|
1838
|
+
function BillingDashboard({ stats, t, onClose, health, balances, quotas, currency, onCurrency, turns, renderSlot, budgetEnabled, budgetAmount, onToggleBudget, onBudgetAmount }) {
|
|
1256
1839
|
const { total, byModel, byDay } = stats;
|
|
1257
1840
|
const [pricingOpen, setPricingOpen] = (0, react.useState)(false);
|
|
1841
|
+
const [sessionsOpen, setSessionsOpen] = (0, react.useState)(false);
|
|
1842
|
+
const [roundsOpen, setRoundsOpen] = (0, react.useState)(false);
|
|
1843
|
+
const [heatmapOpen, setHeatmapOpen] = (0, react.useState)(true);
|
|
1844
|
+
const [workspacesOpen, setWorkspacesOpen] = (0, react.useState)(false);
|
|
1845
|
+
const [trendDays, setTrendDays] = (0, react.useState)(7);
|
|
1258
1846
|
const rateInfo = getRateInfo();
|
|
1847
|
+
const money = (cny) => formatMoney(currency === "usd" ? cnyToUsd(cny) : cny, currency);
|
|
1848
|
+
const roundFlags = (0, react.useMemo)(() => flagAnomalies([...turns].reverse()), [turns]);
|
|
1849
|
+
const dailyBurn = dailyBurnRate(byDay, localDayStamp());
|
|
1259
1850
|
const balanceFor = (provider) => balances.find((balance) => normalizeProvider(balance.provider) === normalizeProvider(provider));
|
|
1260
1851
|
const renderBalance = (balance) => {
|
|
1261
1852
|
if (balance === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1269,7 +1860,18 @@ window.__ModuleLoader__.load({
|
|
|
1269
1860
|
className: UsageBilling_module_css_default.na,
|
|
1270
1861
|
children: "—"
|
|
1271
1862
|
});
|
|
1272
|
-
|
|
1863
|
+
const amount = balance.currency === "USD" ? `$${balance.totalBalance.toFixed(2)}` : money(balance.totalBalance);
|
|
1864
|
+
const balanceCny = balance.currency === "USD" ? balance.totalBalance * rateInfo.rate : balance.totalBalance;
|
|
1865
|
+
if (dailyBurn <= 0) return amount;
|
|
1866
|
+
const days = Math.floor(balanceCny / dailyBurn);
|
|
1867
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1868
|
+
className: UsageBilling_module_css_default.balanceCell,
|
|
1869
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: amount }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1870
|
+
className: clsx(UsageBilling_module_css_default.balanceDays, days <= 3 && UsageBilling_module_css_default.balanceDaysLow),
|
|
1871
|
+
"data-testid": "billing-balance-days",
|
|
1872
|
+
children: t("billing.balanceDays").replace("{days}", String(days))
|
|
1873
|
+
})]
|
|
1874
|
+
});
|
|
1273
1875
|
};
|
|
1274
1876
|
const cacheHitRate = total.cacheHit + total.cacheMiss > 0 ? total.cacheHit / (total.cacheHit + total.cacheMiss) * 100 : 0;
|
|
1275
1877
|
const dates = Object.keys(byDay).sort();
|
|
@@ -1282,14 +1884,18 @@ window.__ModuleLoader__.load({
|
|
|
1282
1884
|
const yearCost = dates.reduce((sum, d) => sum + (d.startsWith(yearPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
|
|
1283
1885
|
const trendDates = (0, react.useMemo)(() => {
|
|
1284
1886
|
const out = [];
|
|
1285
|
-
for (let offset =
|
|
1887
|
+
for (let offset = trendDays - 1; offset >= 0; offset -= 1) {
|
|
1286
1888
|
const day = /* @__PURE__ */ new Date();
|
|
1287
1889
|
day.setDate(day.getDate() - offset);
|
|
1288
1890
|
out.push(localDayStamp(day.getTime()));
|
|
1289
1891
|
}
|
|
1290
1892
|
return out;
|
|
1291
|
-
}, []);
|
|
1893
|
+
}, [trendDays]);
|
|
1292
1894
|
const latestDate = trendDates.at(-1) ?? today;
|
|
1895
|
+
const heatmapDays = (0, react.useMemo)(() => Object.entries(byDay).map(([date, day]) => ({
|
|
1896
|
+
date,
|
|
1897
|
+
value: day.cost
|
|
1898
|
+
})), [byDay]);
|
|
1293
1899
|
const trend = (0, react.useMemo)(() => trendDates.map((date) => {
|
|
1294
1900
|
const byModel = {};
|
|
1295
1901
|
const dayModels = stats.byDayModels?.[date];
|
|
@@ -1310,6 +1916,8 @@ window.__ModuleLoader__.load({
|
|
|
1310
1916
|
]);
|
|
1311
1917
|
const modelRows = (0, react.useMemo)(() => Object.entries(byModel).filter(([, data]) => data.calls > 0).map(([key, data]) => {
|
|
1312
1918
|
const entry = modelOf(key);
|
|
1919
|
+
const uncatalogued = entry.key === "other";
|
|
1920
|
+
const inferredProvider = uncatalogued ? providerFromModelKey(key) : void 0;
|
|
1313
1921
|
const buckets = {
|
|
1314
1922
|
input: data.input,
|
|
1315
1923
|
cacheHit: data.cacheHit,
|
|
@@ -1318,20 +1926,57 @@ window.__ModuleLoader__.load({
|
|
|
1318
1926
|
};
|
|
1319
1927
|
return {
|
|
1320
1928
|
key,
|
|
1321
|
-
name: entry.name,
|
|
1322
|
-
provider: entry.provider,
|
|
1929
|
+
name: uncatalogued ? key : entry.name,
|
|
1930
|
+
provider: inferredProvider ?? entry.provider,
|
|
1323
1931
|
calls: data.calls,
|
|
1324
1932
|
input: data.input,
|
|
1325
1933
|
output: data.output,
|
|
1326
1934
|
cacheHitRate: data.cacheHit + data.cacheMiss > 0 ? data.cacheHit / (data.cacheHit + data.cacheMiss) * 100 : 0,
|
|
1327
1935
|
estimated: computeCost(entry, buckets),
|
|
1328
1936
|
plan: data.plan === true,
|
|
1329
|
-
...data.cost > 0 ? { actual: data.cost } : {}
|
|
1937
|
+
...data.cost > 0 ? { actual: data.cost } : {},
|
|
1938
|
+
uncatalogued
|
|
1330
1939
|
};
|
|
1331
1940
|
}).sort((a, b) => (b.actual ?? b.estimated) - (a.actual ?? a.estimated)).map((row, index) => ({
|
|
1332
1941
|
...row,
|
|
1333
1942
|
color: CHART_PALETTE[index % CHART_PALETTE.length] ?? "#8b95a3"
|
|
1334
1943
|
})), [byModel]);
|
|
1944
|
+
const providerGroups = (0, react.useMemo)(() => {
|
|
1945
|
+
const subscriptionsByVendor = /* @__PURE__ */ new Map();
|
|
1946
|
+
for (const quota of quotas) {
|
|
1947
|
+
if (quota.status === "not-configured") continue;
|
|
1948
|
+
const vendor = subscriptionVendorOf(quota.provider);
|
|
1949
|
+
const list = subscriptionsByVendor.get(vendor);
|
|
1950
|
+
if (list === void 0) subscriptionsByVendor.set(vendor, [quota]);
|
|
1951
|
+
else list.push(quota);
|
|
1952
|
+
}
|
|
1953
|
+
const modelsByVendor = /* @__PURE__ */ new Map();
|
|
1954
|
+
for (const row of modelRows) {
|
|
1955
|
+
const list = modelsByVendor.get(row.provider);
|
|
1956
|
+
if (list === void 0) modelsByVendor.set(row.provider, [row]);
|
|
1957
|
+
else list.push(row);
|
|
1958
|
+
}
|
|
1959
|
+
return [...new Set([...modelsByVendor.keys(), ...subscriptionsByVendor.keys()])].map((name) => ({
|
|
1960
|
+
name,
|
|
1961
|
+
models: modelsByVendor.get(name) ?? [],
|
|
1962
|
+
subscriptions: subscriptionsByVendor.get(name) ?? [],
|
|
1963
|
+
balance: balanceFor(name),
|
|
1964
|
+
dot: providerDot(health, name)
|
|
1965
|
+
})).sort((a, b) => {
|
|
1966
|
+
const costOf = (group) => group.models.reduce((sum, m) => sum + (m.actual ?? m.estimated), 0);
|
|
1967
|
+
const diff = costOf(b) - costOf(a);
|
|
1968
|
+
if (diff !== 0) return diff;
|
|
1969
|
+
if (a.models.length === 0 && b.models.length === 0) return 0;
|
|
1970
|
+
if (b.models.length === 0) return -1;
|
|
1971
|
+
if (a.models.length === 0) return 1;
|
|
1972
|
+
return a.name.localeCompare(b.name, "zh");
|
|
1973
|
+
});
|
|
1974
|
+
}, [
|
|
1975
|
+
modelRows,
|
|
1976
|
+
quotas,
|
|
1977
|
+
balances,
|
|
1978
|
+
health
|
|
1979
|
+
]);
|
|
1335
1980
|
const estimatedTotal = modelRows.reduce((sum, row) => sum + row.estimated, 0);
|
|
1336
1981
|
const displayTotal = total.cost > 0 ? total.cost : estimatedTotal;
|
|
1337
1982
|
const avgPerCall = total.calls > 0 ? displayTotal / total.calls : 0;
|
|
@@ -1350,101 +1995,209 @@ window.__ModuleLoader__.load({
|
|
|
1350
1995
|
className: UsageBilling_module_css_default.dashboardModal ?? "",
|
|
1351
1996
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1352
1997
|
className: UsageBilling_module_css_default.dashboard,
|
|
1998
|
+
"data-testid": "billing-dashboard",
|
|
1353
1999
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1354
2000
|
className: UsageBilling_module_css_default.dashboardHead,
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2001
|
+
"data-testid": "billing-dashboard-head",
|
|
2002
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
2003
|
+
renderSlot("billing.dashboard.decor", { position: "head" }),
|
|
2004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2005
|
+
className: UsageBilling_module_css_default.headTitleRow,
|
|
2006
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
2007
|
+
className: UsageBilling_module_css_default.dashboardTitle,
|
|
2008
|
+
children: t("billing.title")
|
|
2009
|
+
}), renderSlot("billing.dashboard.decor", { position: "headTitle" })]
|
|
2010
|
+
}),
|
|
2011
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2012
|
+
className: UsageBilling_module_css_default.dashboardSubtitle,
|
|
2013
|
+
children: [
|
|
2014
|
+
t("billing.lastUpdated"),
|
|
2015
|
+
" ",
|
|
2016
|
+
latestDate
|
|
2017
|
+
]
|
|
2018
|
+
})
|
|
2019
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1366
2020
|
className: UsageBilling_module_css_default.dashboardRight,
|
|
1367
|
-
children: [
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
"aria-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
2021
|
+
children: [
|
|
2022
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2023
|
+
className: UsageBilling_module_css_default.currencyToggle,
|
|
2024
|
+
role: "group",
|
|
2025
|
+
"aria-label": t("billing.currency"),
|
|
2026
|
+
children: ["cny", "usd"].map((unit) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2027
|
+
type: "button",
|
|
2028
|
+
className: clsx(UsageBilling_module_css_default.currencyButton, currency === unit && UsageBilling_module_css_default.currencyButtonActive),
|
|
2029
|
+
"aria-pressed": currency === unit,
|
|
2030
|
+
"data-testid": `billing-currency-${unit}`,
|
|
2031
|
+
title: unit === "cny" ? t("billing.currencyCny") : t("billing.currencyUsd"),
|
|
2032
|
+
onClick: () => {
|
|
2033
|
+
onCurrency(unit);
|
|
2034
|
+
},
|
|
2035
|
+
children: unit === "cny" ? "¥" : "$"
|
|
2036
|
+
}, unit))
|
|
2037
|
+
}),
|
|
2038
|
+
health.checked && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2039
|
+
className: clsx(UsageBilling_module_css_default.healthBadge, health.available ? UsageBilling_module_css_default.healthBadgeOk : UsageBilling_module_css_default.healthBadgeBad),
|
|
2040
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2041
|
+
className: clsx(UsageBilling_module_css_default.healthDot, health.available ? UsageBilling_module_css_default.healthOk : UsageBilling_module_css_default.healthBad),
|
|
2042
|
+
"aria-hidden": "true"
|
|
2043
|
+
}), health.available ? `${health.models} 模型可用${health.failures > 0 ? ` · ${health.failures} 厂商失效` : ""}` : `${health.failures} 厂商不可用`]
|
|
2044
|
+
}),
|
|
2045
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2046
|
+
type: "button",
|
|
2047
|
+
className: UsageBilling_module_css_default.closeButton,
|
|
2048
|
+
"aria-label": t("billing.close"),
|
|
2049
|
+
"data-testid": "billing-close",
|
|
2050
|
+
onClick: onClose,
|
|
2051
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2052
|
+
viewBox: "0 0 24 24",
|
|
2053
|
+
fill: "none",
|
|
2054
|
+
stroke: "currentColor",
|
|
2055
|
+
"aria-hidden": "true",
|
|
2056
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })]
|
|
2057
|
+
})
|
|
1384
2058
|
})
|
|
1385
|
-
|
|
2059
|
+
]
|
|
1386
2060
|
})]
|
|
1387
2061
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1388
2062
|
className: UsageBilling_module_css_default.dashboardBody,
|
|
1389
2063
|
children: [
|
|
1390
2064
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1391
2065
|
className: UsageBilling_module_css_default.hero,
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
t("billing.calls")
|
|
1409
|
-
]
|
|
1410
|
-
})
|
|
1411
|
-
]
|
|
1412
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1413
|
-
className: UsageBilling_module_css_default.heroSide,
|
|
1414
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1415
|
-
className: UsageBilling_module_css_default.heroSideItem,
|
|
1416
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1417
|
-
className: UsageBilling_module_css_default.heroSideLabel,
|
|
1418
|
-
children: t("billing.yearCost")
|
|
1419
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1420
|
-
className: UsageBilling_module_css_default.heroSideValue,
|
|
1421
|
-
children: formatMoney(yearCost)
|
|
1422
|
-
})]
|
|
1423
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1424
|
-
className: UsageBilling_module_css_default.heroSideItem,
|
|
1425
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1426
|
-
className: UsageBilling_module_css_default.heroSideLabel,
|
|
1427
|
-
children: t("billing.todayCost")
|
|
1428
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1429
|
-
className: UsageBilling_module_css_default.heroSideValue,
|
|
1430
|
-
children: [formatMoney(todayCost), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1431
|
-
className: clsx(UsageBilling_module_css_default.delta, deltaPct >= 0 ? UsageBilling_module_css_default.deltaUp : UsageBilling_module_css_default.deltaDown),
|
|
2066
|
+
"data-testid": "billing-hero",
|
|
2067
|
+
children: [
|
|
2068
|
+
renderSlot("billing.dashboard.decor", { position: "hero" }),
|
|
2069
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2070
|
+
className: UsageBilling_module_css_default.heroMain,
|
|
2071
|
+
children: [
|
|
2072
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2073
|
+
className: UsageBilling_module_css_default.heroLabel,
|
|
2074
|
+
children: t("billing.monthCost")
|
|
2075
|
+
}),
|
|
2076
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2077
|
+
className: UsageBilling_module_css_default.heroValue,
|
|
2078
|
+
children: money(monthCost)
|
|
2079
|
+
}),
|
|
2080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2081
|
+
className: UsageBilling_module_css_default.heroMeta,
|
|
1432
2082
|
children: [
|
|
1433
|
-
|
|
2083
|
+
monthCalls.toLocaleString(),
|
|
1434
2084
|
" ",
|
|
1435
|
-
|
|
1436
|
-
"%"
|
|
2085
|
+
t("billing.calls")
|
|
1437
2086
|
]
|
|
2087
|
+
})
|
|
2088
|
+
]
|
|
2089
|
+
}),
|
|
2090
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2091
|
+
className: UsageBilling_module_css_default.heroSide,
|
|
2092
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2093
|
+
className: UsageBilling_module_css_default.heroSideItem,
|
|
2094
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2095
|
+
className: UsageBilling_module_css_default.heroSideLabel,
|
|
2096
|
+
children: t("billing.yearCost")
|
|
2097
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2098
|
+
className: UsageBilling_module_css_default.heroSideValue,
|
|
2099
|
+
children: money(yearCost)
|
|
2100
|
+
})]
|
|
2101
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2102
|
+
className: UsageBilling_module_css_default.heroSideItem,
|
|
2103
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2104
|
+
className: UsageBilling_module_css_default.heroSideLabel,
|
|
2105
|
+
children: t("billing.todayCost")
|
|
2106
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2107
|
+
className: UsageBilling_module_css_default.heroSideValue,
|
|
2108
|
+
children: [money(todayCost), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2109
|
+
className: clsx(UsageBilling_module_css_default.delta, deltaPct >= 0 ? UsageBilling_module_css_default.deltaUp : UsageBilling_module_css_default.deltaDown),
|
|
2110
|
+
children: [
|
|
2111
|
+
deltaPct >= 0 ? "▲" : "▼",
|
|
2112
|
+
" ",
|
|
2113
|
+
Math.abs(deltaPct).toFixed(1),
|
|
2114
|
+
"%"
|
|
2115
|
+
]
|
|
2116
|
+
})]
|
|
1438
2117
|
})]
|
|
1439
2118
|
})]
|
|
2119
|
+
})
|
|
2120
|
+
]
|
|
2121
|
+
}),
|
|
2122
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2123
|
+
className: UsageBilling_module_css_default.budget,
|
|
2124
|
+
"data-testid": "billing-budget",
|
|
2125
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2126
|
+
className: UsageBilling_module_css_default.budgetHead,
|
|
2127
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2128
|
+
className: UsageBilling_module_css_default.budgetLabel,
|
|
2129
|
+
children: t("billing.budget")
|
|
2130
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2131
|
+
className: UsageBilling_module_css_default.budgetControls,
|
|
2132
|
+
children: [
|
|
2133
|
+
budgetEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2134
|
+
className: UsageBilling_module_css_default.budgetInputWrap,
|
|
2135
|
+
"data-testid": "billing-budget-input-wrap",
|
|
2136
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2137
|
+
className: UsageBilling_module_css_default.budgetUnit,
|
|
2138
|
+
"aria-hidden": "true",
|
|
2139
|
+
children: "¥"
|
|
2140
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2141
|
+
className: UsageBilling_module_css_default.budgetInput,
|
|
2142
|
+
"data-testid": "billing-budget-input",
|
|
2143
|
+
type: "number",
|
|
2144
|
+
min: 0,
|
|
2145
|
+
step: 1,
|
|
2146
|
+
value: budgetAmount === 0 ? "" : budgetAmount,
|
|
2147
|
+
placeholder: stats.budget !== void 0 ? String(stats.budget) : "0",
|
|
2148
|
+
"aria-label": `${t("billing.budget")}(元)`,
|
|
2149
|
+
title: `${t("billing.budget")}(元)`,
|
|
2150
|
+
onChange: (e) => {
|
|
2151
|
+
onBudgetAmount(e.target.valueAsNumber);
|
|
2152
|
+
}
|
|
2153
|
+
})]
|
|
2154
|
+
}),
|
|
2155
|
+
budgetEnabled && budgetAmount > 0 && (() => {
|
|
2156
|
+
const pct = monthCost / budgetAmount * 100;
|
|
2157
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2158
|
+
className: UsageBilling_module_css_default.budgetValue,
|
|
2159
|
+
"data-testid": "billing-budget-value",
|
|
2160
|
+
children: [
|
|
2161
|
+
money(monthCost),
|
|
2162
|
+
" / ",
|
|
2163
|
+
money(budgetAmount),
|
|
2164
|
+
" · ",
|
|
2165
|
+
pct.toFixed(1),
|
|
2166
|
+
"%"
|
|
2167
|
+
]
|
|
2168
|
+
});
|
|
2169
|
+
})(),
|
|
2170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2171
|
+
type: "button",
|
|
2172
|
+
role: "switch",
|
|
2173
|
+
"aria-checked": budgetEnabled,
|
|
2174
|
+
"aria-label": t("billing.budget"),
|
|
2175
|
+
"data-testid": "billing-budget-toggle",
|
|
2176
|
+
className: clsx(UsageBilling_module_css_default.switch, budgetEnabled && UsageBilling_module_css_default.switchOn),
|
|
2177
|
+
onClick: onToggleBudget,
|
|
2178
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.switchKnob })
|
|
2179
|
+
})
|
|
2180
|
+
]
|
|
1440
2181
|
})]
|
|
1441
|
-
})
|
|
2182
|
+
}), budgetEnabled && budgetAmount > 0 && (() => {
|
|
2183
|
+
const pct = monthCost / budgetAmount * 100;
|
|
2184
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2185
|
+
className: UsageBilling_module_css_default.budgetTrack,
|
|
2186
|
+
"data-testid": "billing-budget-track",
|
|
2187
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2188
|
+
className: clsx(UsageBilling_module_css_default.budgetFill, pct >= 100 && UsageBilling_module_css_default.budgetFillOver),
|
|
2189
|
+
style: { width: `${Math.min(pct, 100)}%` }
|
|
2190
|
+
})
|
|
2191
|
+
});
|
|
2192
|
+
})()]
|
|
1442
2193
|
}),
|
|
1443
2194
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1444
2195
|
className: UsageBilling_module_css_default.kpiGrid,
|
|
2196
|
+
"data-testid": "billing-kpi-grid",
|
|
1445
2197
|
children: [
|
|
1446
2198
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1447
2199
|
className: UsageBilling_module_css_default.kpiTile,
|
|
2200
|
+
"data-testid": "billing-kpi-tile",
|
|
1448
2201
|
children: [
|
|
1449
2202
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1450
2203
|
className: UsageBilling_module_css_default.kpiLabel,
|
|
@@ -1498,7 +2251,7 @@ window.__ModuleLoader__.load({
|
|
|
1498
2251
|
}),
|
|
1499
2252
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1500
2253
|
className: UsageBilling_module_css_default.kpiValue,
|
|
1501
|
-
children:
|
|
2254
|
+
children: money(avgPerCall)
|
|
1502
2255
|
}),
|
|
1503
2256
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1504
2257
|
className: UsageBilling_module_css_default.kpiDetail,
|
|
@@ -1534,38 +2287,333 @@ window.__ModuleLoader__.load({
|
|
|
1534
2287
|
]
|
|
1535
2288
|
}),
|
|
1536
2289
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1537
|
-
className: UsageBilling_module_css_default.panel,
|
|
2290
|
+
className: clsx(UsageBilling_module_css_default.panel, UsageBilling_module_css_default.trendPanel),
|
|
2291
|
+
"data-testid": "billing-panel-trend",
|
|
1538
2292
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1539
2293
|
className: UsageBilling_module_css_default.panelHead,
|
|
1540
|
-
children: [
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2294
|
+
children: [
|
|
2295
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2296
|
+
className: UsageBilling_module_css_default.panelTitle,
|
|
2297
|
+
children: t("billing.trend")
|
|
2298
|
+
}),
|
|
2299
|
+
renderSlot("billing.dashboard.decor", { position: "trend" }),
|
|
2300
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2301
|
+
className: UsageBilling_module_css_default.rangeToggle,
|
|
2302
|
+
role: "group",
|
|
2303
|
+
"aria-label": t("billing.trend"),
|
|
2304
|
+
children: [7, 30].map((days) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2305
|
+
type: "button",
|
|
2306
|
+
className: clsx(UsageBilling_module_css_default.rangeButton, trendDays === days && UsageBilling_module_css_default.rangeButtonActive),
|
|
2307
|
+
"aria-pressed": trendDays === days,
|
|
2308
|
+
"data-testid": `billing-trend-${days}d`,
|
|
2309
|
+
onClick: () => {
|
|
2310
|
+
setTrendDays(days);
|
|
2311
|
+
},
|
|
2312
|
+
children: days === 7 ? t("billing.trend7d") : t("billing.trend30d")
|
|
2313
|
+
}, days))
|
|
2314
|
+
}),
|
|
2315
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2316
|
+
className: UsageBilling_module_css_default.panelHint,
|
|
2317
|
+
children: latestDate
|
|
2318
|
+
})
|
|
2319
|
+
]
|
|
1547
2320
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendChart, {
|
|
1548
2321
|
data: trend,
|
|
1549
|
-
models: chartModels
|
|
2322
|
+
models: chartModels,
|
|
2323
|
+
currency
|
|
1550
2324
|
})]
|
|
1551
2325
|
}),
|
|
1552
2326
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1553
2327
|
className: UsageBilling_module_css_default.panel,
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
className: UsageBilling_module_css_default.
|
|
1558
|
-
children:
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2328
|
+
"data-testid": "billing-panel-providers",
|
|
2329
|
+
children: [
|
|
2330
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2331
|
+
className: UsageBilling_module_css_default.panelHead,
|
|
2332
|
+
children: [
|
|
2333
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2334
|
+
className: UsageBilling_module_css_default.panelTitle,
|
|
2335
|
+
children: t("billing.providerBilling")
|
|
2336
|
+
}),
|
|
2337
|
+
renderSlot("billing.dashboard.decor", { position: "models" }),
|
|
2338
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2339
|
+
className: UsageBilling_module_css_default.panelHint,
|
|
2340
|
+
children: stats.updatedAt !== void 0 ? `${t("billing.lastUpdated")} ${formatClock(stats.updatedAt)}` : ""
|
|
2341
|
+
})
|
|
2342
|
+
]
|
|
2343
|
+
}),
|
|
2344
|
+
providerGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2345
|
+
className: UsageBilling_module_css_default.emptyRow,
|
|
2346
|
+
"data-testid": "billing-provider-empty",
|
|
2347
|
+
children: t("billing.noData")
|
|
2348
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2349
|
+
className: UsageBilling_module_css_default.providerGroupList,
|
|
2350
|
+
"data-testid": "billing-provider-groups",
|
|
2351
|
+
children: providerGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2352
|
+
className: UsageBilling_module_css_default.providerGroup,
|
|
2353
|
+
"data-testid": "billing-provider-group",
|
|
2354
|
+
children: [
|
|
2355
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2356
|
+
className: UsageBilling_module_css_default.providerGroupHead,
|
|
2357
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2358
|
+
className: UsageBilling_module_css_default.providerGroupTitle,
|
|
2359
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2360
|
+
className: clsx(UsageBilling_module_css_default.healthDot, group.dot),
|
|
2361
|
+
"aria-hidden": "true"
|
|
2362
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2363
|
+
className: UsageBilling_module_css_default.providerGroupName,
|
|
2364
|
+
children: group.name
|
|
2365
|
+
})]
|
|
2366
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2367
|
+
className: UsageBilling_module_css_default.providerGroupMeta,
|
|
2368
|
+
children: [group.subscriptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2369
|
+
className: UsageBilling_module_css_default.providerGroupBadge,
|
|
2370
|
+
"data-testid": "billing-provider-sub-count",
|
|
2371
|
+
children: [group.subscriptions.length, " 套餐"]
|
|
2372
|
+
}), group.balance !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2373
|
+
className: UsageBilling_module_css_default.providerGroupBalance,
|
|
2374
|
+
"data-testid": "billing-provider-balance",
|
|
2375
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2376
|
+
className: UsageBilling_module_css_default.providerGroupBalanceLabel,
|
|
2377
|
+
children: t("billing.balance")
|
|
2378
|
+
}), renderBalance(group.balance)]
|
|
2379
|
+
})]
|
|
2380
|
+
})]
|
|
2381
|
+
}),
|
|
2382
|
+
group.models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2383
|
+
className: clsx(UsageBilling_module_css_default.tableScroll, UsageBilling_module_css_default.modelTableScroll),
|
|
2384
|
+
"data-testid": "billing-table-scroll",
|
|
2385
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
2386
|
+
className: UsageBilling_module_css_default.modelTable,
|
|
2387
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
2388
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("billing.model") }),
|
|
2389
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2390
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2391
|
+
children: t("billing.calls")
|
|
2392
|
+
}),
|
|
2393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2394
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2395
|
+
children: t("billing.inputTokens")
|
|
2396
|
+
}),
|
|
2397
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2398
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2399
|
+
children: t("billing.outputTokens")
|
|
2400
|
+
}),
|
|
2401
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2402
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2403
|
+
children: t("billing.cacheHitRate")
|
|
2404
|
+
}),
|
|
2405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2406
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2407
|
+
children: t("billing.actual")
|
|
2408
|
+
})
|
|
2409
|
+
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: group.models.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
2410
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2411
|
+
className: UsageBilling_module_css_default.modelCell,
|
|
2412
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2413
|
+
className: UsageBilling_module_css_default.modelName,
|
|
2414
|
+
children: [row.name, row.uncatalogued && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2415
|
+
className: UsageBilling_module_css_default.uncataloguedTag,
|
|
2416
|
+
"data-testid": "billing-uncatalogued-tag",
|
|
2417
|
+
children: t("billing.uncatalogued")
|
|
2418
|
+
})]
|
|
2419
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2420
|
+
className: UsageBilling_module_css_default.modelProvider,
|
|
2421
|
+
children: row.provider
|
|
2422
|
+
})] })
|
|
2423
|
+
}) }),
|
|
2424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2425
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2426
|
+
children: row.calls.toLocaleString()
|
|
2427
|
+
}),
|
|
2428
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2429
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2430
|
+
children: formatTokens(row.input)
|
|
2431
|
+
}),
|
|
2432
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2433
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2434
|
+
children: formatTokens(row.output)
|
|
2435
|
+
}),
|
|
2436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2437
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2438
|
+
children: formatPercent(row.cacheHitRate)
|
|
2439
|
+
}),
|
|
2440
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2441
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2442
|
+
children: row.plan ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2443
|
+
className: UsageBilling_module_css_default.planTag,
|
|
2444
|
+
children: "订阅包含"
|
|
2445
|
+
}) : row.actual !== void 0 ? money(row.actual) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2446
|
+
className: UsageBilling_module_css_default.na,
|
|
2447
|
+
children: "—"
|
|
2448
|
+
})
|
|
2449
|
+
})
|
|
2450
|
+
] }, row.key)) })]
|
|
2451
|
+
})
|
|
2452
|
+
}),
|
|
2453
|
+
group.subscriptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2454
|
+
className: UsageBilling_module_css_default.subscriptionGrid,
|
|
2455
|
+
"data-testid": "billing-subscriptions-grid",
|
|
2456
|
+
children: group.subscriptions.map((quota) => {
|
|
2457
|
+
const statusText = subscriptionStatusText(quota.status, t);
|
|
2458
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2459
|
+
className: UsageBilling_module_css_default.subscriptionCard,
|
|
2460
|
+
"data-testid": "billing-subscription-card",
|
|
2461
|
+
children: [
|
|
2462
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2463
|
+
className: UsageBilling_module_css_default.subscriptionHead,
|
|
2464
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2465
|
+
className: UsageBilling_module_css_default.subscriptionName,
|
|
2466
|
+
children: quota.displayName
|
|
2467
|
+
}), quota.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2468
|
+
className: UsageBilling_module_css_default.subscriptionPlan,
|
|
2469
|
+
children: quota.plan
|
|
2470
|
+
})]
|
|
2471
|
+
}),
|
|
2472
|
+
statusText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2473
|
+
className: UsageBilling_module_css_default.subscriptionStatus,
|
|
2474
|
+
children: statusText
|
|
2475
|
+
}),
|
|
2476
|
+
quota.windows.length === 0 && statusText === "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2477
|
+
className: UsageBilling_module_css_default.subscriptionStatus,
|
|
2478
|
+
children: t("billing.subscriptionNoApi")
|
|
2479
|
+
}),
|
|
2480
|
+
quota.windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2481
|
+
className: UsageBilling_module_css_default.subscriptionWindow,
|
|
2482
|
+
children: [
|
|
2483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2484
|
+
className: UsageBilling_module_css_default.subscriptionWindowLabel,
|
|
2485
|
+
children: subscriptionWindowLabel(window.kind, t)
|
|
2486
|
+
}),
|
|
2487
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2488
|
+
className: UsageBilling_module_css_default.subscriptionTrack,
|
|
2489
|
+
"aria-hidden": "true",
|
|
2490
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2491
|
+
className: UsageBilling_module_css_default.subscriptionFill,
|
|
2492
|
+
style: { width: `${Math.min(100, Math.max(0, window.remainingPercent))}%` }
|
|
2493
|
+
})
|
|
2494
|
+
}),
|
|
2495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2496
|
+
className: UsageBilling_module_css_default.subscriptionPct,
|
|
2497
|
+
children: t("billing.subscriptionRemaining").replace("{pct}", String(window.remainingPercent))
|
|
2498
|
+
}),
|
|
2499
|
+
window.resetsAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2500
|
+
className: UsageBilling_module_css_default.subscriptionReset,
|
|
2501
|
+
children: t("billing.subscriptionReset").replace("{date}", window.resetsAt.slice(0, 10))
|
|
2502
|
+
})
|
|
2503
|
+
]
|
|
2504
|
+
}, window.kind))
|
|
2505
|
+
]
|
|
2506
|
+
}, quota.provider);
|
|
2507
|
+
})
|
|
2508
|
+
})
|
|
2509
|
+
]
|
|
2510
|
+
}, group.name))
|
|
2511
|
+
}),
|
|
2512
|
+
renderSlot("billing.dashboard.decor", { position: "footer" })
|
|
2513
|
+
]
|
|
2514
|
+
}),
|
|
2515
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2516
|
+
className: UsageBilling_module_css_default.panel,
|
|
2517
|
+
"data-testid": "billing-panel-heatmap",
|
|
2518
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2519
|
+
type: "button",
|
|
2520
|
+
className: UsageBilling_module_css_default.pricingToggle,
|
|
2521
|
+
"data-testid": "billing-heatmap-toggle",
|
|
2522
|
+
onClick: () => {
|
|
2523
|
+
setHeatmapOpen((prev) => !prev);
|
|
2524
|
+
},
|
|
2525
|
+
"aria-expanded": heatmapOpen,
|
|
2526
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2527
|
+
className: UsageBilling_module_css_default.pricingToggleText,
|
|
2528
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2529
|
+
className: UsageBilling_module_css_default.panelTitle,
|
|
2530
|
+
children: t("billing.heatmap")
|
|
2531
|
+
})
|
|
2532
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2533
|
+
className: clsx(UsageBilling_module_css_default.pricingChevron, heatmapOpen && UsageBilling_module_css_default.pricingChevronOpen),
|
|
2534
|
+
viewBox: "0 0 24 24",
|
|
2535
|
+
fill: "none",
|
|
2536
|
+
stroke: "currentColor",
|
|
2537
|
+
"aria-hidden": "true",
|
|
2538
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 9 6 6 6-6" })
|
|
2539
|
+
})]
|
|
2540
|
+
}), heatmapOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageHeatmap, {
|
|
2541
|
+
days: heatmapDays,
|
|
2542
|
+
currency,
|
|
2543
|
+
t
|
|
2544
|
+
})]
|
|
2545
|
+
}),
|
|
2546
|
+
turns.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2547
|
+
className: UsageBilling_module_css_default.panel,
|
|
2548
|
+
"data-testid": "billing-panel-rounds",
|
|
2549
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2550
|
+
type: "button",
|
|
2551
|
+
className: UsageBilling_module_css_default.pricingToggle,
|
|
2552
|
+
"data-testid": "billing-rounds-toggle",
|
|
2553
|
+
onClick: () => {
|
|
2554
|
+
setRoundsOpen((prev) => !prev);
|
|
2555
|
+
},
|
|
2556
|
+
"aria-expanded": roundsOpen,
|
|
2557
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2558
|
+
className: UsageBilling_module_css_default.pricingToggleText,
|
|
2559
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2560
|
+
className: UsageBilling_module_css_default.panelTitle,
|
|
2561
|
+
children: t("billing.rounds")
|
|
2562
|
+
}), roundFlags.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2563
|
+
className: UsageBilling_module_css_default.roundsFlagBadge,
|
|
2564
|
+
"data-testid": "billing-rounds-flag-count",
|
|
2565
|
+
children: [
|
|
2566
|
+
roundFlags.length,
|
|
2567
|
+
" ",
|
|
2568
|
+
t("billing.anomaly")
|
|
2569
|
+
]
|
|
2570
|
+
})]
|
|
2571
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2572
|
+
className: clsx(UsageBilling_module_css_default.pricingChevron, roundsOpen && UsageBilling_module_css_default.pricingChevronOpen),
|
|
2573
|
+
viewBox: "0 0 24 24",
|
|
2574
|
+
fill: "none",
|
|
2575
|
+
stroke: "currentColor",
|
|
2576
|
+
"aria-hidden": "true",
|
|
2577
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 9 6 6 6-6" })
|
|
2578
|
+
})]
|
|
2579
|
+
}), roundsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundCostChart, {
|
|
2580
|
+
rounds: turns,
|
|
2581
|
+
flags: roundFlags,
|
|
2582
|
+
currency,
|
|
2583
|
+
t
|
|
2584
|
+
})]
|
|
2585
|
+
}),
|
|
2586
|
+
stats.byWorkspace !== void 0 && stats.byWorkspace.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2587
|
+
className: UsageBilling_module_css_default.panel,
|
|
2588
|
+
"data-testid": "billing-panel-workspaces",
|
|
2589
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2590
|
+
type: "button",
|
|
2591
|
+
className: UsageBilling_module_css_default.pricingToggle,
|
|
2592
|
+
"data-testid": "billing-workspaces-toggle",
|
|
2593
|
+
onClick: () => {
|
|
2594
|
+
setWorkspacesOpen((prev) => !prev);
|
|
2595
|
+
},
|
|
2596
|
+
"aria-expanded": workspacesOpen,
|
|
2597
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2598
|
+
className: UsageBilling_module_css_default.pricingToggleText,
|
|
2599
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2600
|
+
className: UsageBilling_module_css_default.panelTitle,
|
|
2601
|
+
children: t("billing.workspaces")
|
|
2602
|
+
})
|
|
2603
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2604
|
+
className: clsx(UsageBilling_module_css_default.pricingChevron, workspacesOpen && UsageBilling_module_css_default.pricingChevronOpen),
|
|
2605
|
+
viewBox: "0 0 24 24",
|
|
2606
|
+
fill: "none",
|
|
2607
|
+
stroke: "currentColor",
|
|
2608
|
+
"aria-hidden": "true",
|
|
2609
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 9 6 6 6-6" })
|
|
1562
2610
|
})]
|
|
1563
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2611
|
+
}), workspacesOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1564
2612
|
className: UsageBilling_module_css_default.tableScroll,
|
|
1565
2613
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
1566
2614
|
className: UsageBilling_module_css_default.modelTable,
|
|
1567
2615
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
1568
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("billing.
|
|
2616
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("billing.project") }),
|
|
1569
2617
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1570
2618
|
className: UsageBilling_module_css_default.numCol,
|
|
1571
2619
|
children: t("billing.calls")
|
|
@@ -1578,35 +2626,18 @@ window.__ModuleLoader__.load({
|
|
|
1578
2626
|
className: UsageBilling_module_css_default.numCol,
|
|
1579
2627
|
children: t("billing.outputTokens")
|
|
1580
2628
|
}),
|
|
1581
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1582
|
-
className: UsageBilling_module_css_default.numCol,
|
|
1583
|
-
children: t("billing.cacheHitRate")
|
|
1584
|
-
}),
|
|
1585
2629
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1586
2630
|
className: UsageBilling_module_css_default.numCol,
|
|
1587
2631
|
children: t("billing.actual")
|
|
1588
2632
|
}),
|
|
1589
2633
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1590
2634
|
className: UsageBilling_module_css_default.numCol,
|
|
1591
|
-
children: t("billing.
|
|
2635
|
+
children: t("billing.lastActive")
|
|
1592
2636
|
})
|
|
1593
|
-
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
}) }), modelRows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
1598
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1599
|
-
className: UsageBilling_module_css_default.modelCell,
|
|
1600
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1601
|
-
className: clsx(UsageBilling_module_css_default.modelDot, providerDot(health, row.provider)),
|
|
1602
|
-
"aria-hidden": "true"
|
|
1603
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1604
|
-
className: UsageBilling_module_css_default.modelName,
|
|
1605
|
-
children: row.name
|
|
1606
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1607
|
-
className: UsageBilling_module_css_default.modelProvider,
|
|
1608
|
-
children: row.provider
|
|
1609
|
-
})] })]
|
|
2637
|
+
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: stats.byWorkspace.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
2638
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2639
|
+
className: UsageBilling_module_css_default.modelName,
|
|
2640
|
+
children: row.name
|
|
1610
2641
|
}) }),
|
|
1611
2642
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1612
2643
|
className: UsageBilling_module_css_default.numCol,
|
|
@@ -1622,31 +2653,100 @@ window.__ModuleLoader__.load({
|
|
|
1622
2653
|
}),
|
|
1623
2654
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1624
2655
|
className: UsageBilling_module_css_default.numCol,
|
|
1625
|
-
children:
|
|
2656
|
+
children: money(row.cost)
|
|
1626
2657
|
}),
|
|
1627
2658
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1628
2659
|
className: UsageBilling_module_css_default.numCol,
|
|
1629
|
-
children: row.
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
2660
|
+
children: row.lastActive > 0 ? `${localDayStamp(row.lastActive)}` : "—"
|
|
2661
|
+
})
|
|
2662
|
+
] }, row.name)) })]
|
|
2663
|
+
})
|
|
2664
|
+
})]
|
|
2665
|
+
}),
|
|
2666
|
+
stats.bySession !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2667
|
+
className: UsageBilling_module_css_default.panel,
|
|
2668
|
+
"data-testid": "billing-panel-sessions",
|
|
2669
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2670
|
+
type: "button",
|
|
2671
|
+
className: UsageBilling_module_css_default.pricingToggle,
|
|
2672
|
+
"data-testid": "billing-sessions-toggle",
|
|
2673
|
+
onClick: () => {
|
|
2674
|
+
setSessionsOpen((prev) => !prev);
|
|
2675
|
+
},
|
|
2676
|
+
"aria-expanded": sessionsOpen,
|
|
2677
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2678
|
+
className: UsageBilling_module_css_default.pricingToggleText,
|
|
2679
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2680
|
+
className: UsageBilling_module_css_default.panelTitle,
|
|
2681
|
+
children: t("billing.sessions")
|
|
2682
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2683
|
+
className: UsageBilling_module_css_default.panelHint,
|
|
2684
|
+
children: stats.bySession.length > SESSION_DISPLAY_LIMIT ? t("billing.sessionOverflow").replace("{limit}", String(SESSION_DISPLAY_LIMIT)).replace("{total}", String(stats.bySession.length)) : `${stats.bySession.length}`
|
|
2685
|
+
})]
|
|
2686
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2687
|
+
className: clsx(UsageBilling_module_css_default.pricingChevron, sessionsOpen && UsageBilling_module_css_default.pricingChevronOpen),
|
|
2688
|
+
viewBox: "0 0 24 24",
|
|
2689
|
+
fill: "none",
|
|
2690
|
+
stroke: "currentColor",
|
|
2691
|
+
"aria-hidden": "true",
|
|
2692
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 9 6 6 6-6" })
|
|
2693
|
+
})]
|
|
2694
|
+
}), sessionsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2695
|
+
className: UsageBilling_module_css_default.tableScroll,
|
|
2696
|
+
"data-testid": "billing-sessions-table",
|
|
2697
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
2698
|
+
className: UsageBilling_module_css_default.modelTable,
|
|
2699
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
2700
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("billing.sessions") }),
|
|
2701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("billing.project") }),
|
|
2702
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2703
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2704
|
+
children: t("billing.calls")
|
|
2705
|
+
}),
|
|
2706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2707
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2708
|
+
children: t("billing.actual")
|
|
1636
2709
|
}),
|
|
2710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2711
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2712
|
+
children: t("billing.lastActive")
|
|
2713
|
+
})
|
|
2714
|
+
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [stats.bySession.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2715
|
+
colSpan: 5,
|
|
2716
|
+
className: UsageBilling_module_css_default.emptyRow,
|
|
2717
|
+
children: t("billing.noData")
|
|
2718
|
+
}) }), stats.bySession.slice(0, SESSION_DISPLAY_LIMIT).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
2719
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2720
|
+
className: UsageBilling_module_css_default.modelName,
|
|
2721
|
+
children: row.title ?? row.id.slice(0, 8)
|
|
2722
|
+
}) }),
|
|
2723
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2724
|
+
className: UsageBilling_module_css_default.modelProvider,
|
|
2725
|
+
children: projectName(row.cwd) ?? "—"
|
|
2726
|
+
}) }),
|
|
1637
2727
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1638
2728
|
className: UsageBilling_module_css_default.numCol,
|
|
1639
|
-
children:
|
|
2729
|
+
children: row.calls.toLocaleString()
|
|
2730
|
+
}),
|
|
2731
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2732
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2733
|
+
children: money(row.cost)
|
|
2734
|
+
}),
|
|
2735
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2736
|
+
className: UsageBilling_module_css_default.numCol,
|
|
2737
|
+
children: row.lastActive > 0 ? `${localDayStamp(row.lastActive)} ${formatClock(row.lastActive)}` : "—"
|
|
1640
2738
|
})
|
|
1641
|
-
] }, row.
|
|
2739
|
+
] }, row.id))] })]
|
|
1642
2740
|
})
|
|
1643
2741
|
})]
|
|
1644
2742
|
}),
|
|
1645
2743
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1646
2744
|
className: UsageBilling_module_css_default.panel,
|
|
2745
|
+
"data-testid": "billing-panel-pricing",
|
|
1647
2746
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1648
2747
|
type: "button",
|
|
1649
2748
|
className: UsageBilling_module_css_default.pricingToggle,
|
|
2749
|
+
"data-testid": "billing-pricing-toggle",
|
|
1650
2750
|
onClick: () => {
|
|
1651
2751
|
setPricingOpen((prev) => !prev);
|
|
1652
2752
|
},
|
|
@@ -1767,10 +2867,12 @@ window.__ModuleLoader__.load({
|
|
|
1767
2867
|
* @param props - framework-provided sidebar and locale props.
|
|
1768
2868
|
*/
|
|
1769
2869
|
function UsageBilling(props) {
|
|
1770
|
-
const { t, checkModels } = props;
|
|
2870
|
+
const { t, checkModels, publishCosts, registerOpen, renderSlot, useStore, actions } = props;
|
|
1771
2871
|
const [stats, setStats] = (0, react.useState)(EMPTY_STATS);
|
|
1772
2872
|
const [health, setHealth] = (0, react.useState)(IDLE_HEALTH);
|
|
1773
2873
|
const [balances, setBalances] = (0, react.useState)([]);
|
|
2874
|
+
const [quotas, setQuotas] = (0, react.useState)([]);
|
|
2875
|
+
const [currency, setCurrency] = (0, react.useState)("cny");
|
|
1774
2876
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1775
2877
|
const close = (0, react.useCallback)(() => {
|
|
1776
2878
|
setOpen(false);
|
|
@@ -1782,6 +2884,9 @@ window.__ModuleLoader__.load({
|
|
|
1782
2884
|
fetchBalances().then((list) => {
|
|
1783
2885
|
if (list.length > 0) setBalances(list);
|
|
1784
2886
|
});
|
|
2887
|
+
fetchSubscriptions().then((list) => {
|
|
2888
|
+
if (list.length > 0) setQuotas(list);
|
|
2889
|
+
});
|
|
1785
2890
|
}, []);
|
|
1786
2891
|
const openDashboard = (0, react.useCallback)(() => {
|
|
1787
2892
|
reloadStats();
|
|
@@ -1812,17 +2917,107 @@ window.__ModuleLoader__.load({
|
|
|
1812
2917
|
}, [checkModels]);
|
|
1813
2918
|
const today = localDayStamp();
|
|
1814
2919
|
const monthCost = Object.entries(stats.byDay).filter(([date]) => date.startsWith(today.slice(0, 7))).reduce((sum, [, day]) => sum + day.cost, 0);
|
|
2920
|
+
const todayCost = stats.byDay[today]?.cost ?? 0;
|
|
2921
|
+
const budgetEnabled = useStore((s) => s.enabled);
|
|
2922
|
+
const budgetAmount = useStore((s) => s.amount);
|
|
2923
|
+
const budgetAlertedDay = useStore((s) => s.lastAlertDay);
|
|
2924
|
+
const effectiveBudget = budgetAmount > 0 ? budgetAmount : stats.budget ?? 0;
|
|
2925
|
+
const toggleBudget = (0, react.useCallback)(() => {
|
|
2926
|
+
const next = !budgetEnabled;
|
|
2927
|
+
actions.setEnabled(next);
|
|
2928
|
+
if (next && typeof Notification !== "undefined" && Notification.permission === "default") Notification.requestPermission();
|
|
2929
|
+
}, [actions, budgetEnabled]);
|
|
2930
|
+
(0, react.useEffect)(() => {
|
|
2931
|
+
if (!budgetEnabled || effectiveBudget <= 0) return;
|
|
2932
|
+
const pct = monthCost / effectiveBudget * 100;
|
|
2933
|
+
if (pct < 100) return;
|
|
2934
|
+
const day = localDayStamp();
|
|
2935
|
+
if (budgetAlertedDay === day) return;
|
|
2936
|
+
actions.markAlerted(day);
|
|
2937
|
+
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
2938
|
+
const body = t("billing.budgetOverBody").replace("{cost}", formatMoney(monthCost)).replace("{budget}", formatMoney(effectiveBudget)).replace("{pct}", pct.toFixed(0));
|
|
2939
|
+
try {
|
|
2940
|
+
new Notification(t("billing.budget"), { body });
|
|
2941
|
+
} catch {}
|
|
2942
|
+
}, [
|
|
2943
|
+
budgetEnabled,
|
|
2944
|
+
effectiveBudget,
|
|
2945
|
+
monthCost,
|
|
2946
|
+
budgetAlertedDay,
|
|
2947
|
+
actions,
|
|
2948
|
+
t
|
|
2949
|
+
]);
|
|
2950
|
+
const lastBalanceAlertDay = useStore((s) => s.lastBalanceAlertDay);
|
|
2951
|
+
const lowBalanceRow = (0, react.useMemo)(() => {
|
|
2952
|
+
if (balances.length === 0) return void 0;
|
|
2953
|
+
const threshold = stats.lowBalanceThreshold ?? DEFAULT_LOW_BALANCE_THRESHOLD;
|
|
2954
|
+
const burn = dailyBurnRate(stats.byDay, today);
|
|
2955
|
+
const rate = getRateInfo().rate;
|
|
2956
|
+
for (const balance of balances) {
|
|
2957
|
+
if (balance.totalBalance === void 0 || balance.error !== void 0) continue;
|
|
2958
|
+
const cny = balance.currency === "USD" ? balance.totalBalance * rate : balance.totalBalance;
|
|
2959
|
+
if (cny >= threshold) continue;
|
|
2960
|
+
const days = burn > 0 ? Math.floor(cny / burn) : void 0;
|
|
2961
|
+
return {
|
|
2962
|
+
name: balance.displayName,
|
|
2963
|
+
cny,
|
|
2964
|
+
days
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
}, [
|
|
2968
|
+
balances,
|
|
2969
|
+
stats.lowBalanceThreshold,
|
|
2970
|
+
stats.byDay,
|
|
2971
|
+
today
|
|
2972
|
+
]);
|
|
2973
|
+
(0, react.useEffect)(() => {
|
|
2974
|
+
if (lowBalanceRow === void 0) return;
|
|
2975
|
+
const day = localDayStamp();
|
|
2976
|
+
if (lastBalanceAlertDay === day) return;
|
|
2977
|
+
actions.markBalanceAlerted(day);
|
|
2978
|
+
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
2979
|
+
const body = t("billing.balanceLowBody").replace("{name}", lowBalanceRow.name).replace("{balance}", formatMoney(lowBalanceRow.cny)).replace("{days}", lowBalanceRow.days === void 0 ? "—" : String(lowBalanceRow.days));
|
|
2980
|
+
try {
|
|
2981
|
+
new Notification(t("billing.balance"), { body });
|
|
2982
|
+
} catch {}
|
|
2983
|
+
}, [
|
|
2984
|
+
lowBalanceRow,
|
|
2985
|
+
lastBalanceAlertDay,
|
|
2986
|
+
actions,
|
|
2987
|
+
t
|
|
2988
|
+
]);
|
|
2989
|
+
(0, react.useEffect)(() => {
|
|
2990
|
+
publishCosts({
|
|
2991
|
+
todayCost,
|
|
2992
|
+
monthCost
|
|
2993
|
+
});
|
|
2994
|
+
}, [
|
|
2995
|
+
todayCost,
|
|
2996
|
+
monthCost,
|
|
2997
|
+
publishCosts
|
|
2998
|
+
]);
|
|
2999
|
+
(0, react.useEffect)(() => registerOpen(openDashboard), [registerOpen, openDashboard]);
|
|
3000
|
+
const turns = (0, react.useMemo)(() => stats.byTurn ?? [], [stats.byTurn]);
|
|
1815
3001
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBillingTrigger, {
|
|
1816
3002
|
...props,
|
|
1817
3003
|
onOpen: openDashboard,
|
|
1818
3004
|
monthCost,
|
|
1819
|
-
todayCost
|
|
3005
|
+
todayCost
|
|
1820
3006
|
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BillingDashboard, {
|
|
1821
3007
|
stats,
|
|
1822
3008
|
t,
|
|
1823
3009
|
onClose: close,
|
|
1824
3010
|
health,
|
|
1825
|
-
balances
|
|
3011
|
+
balances,
|
|
3012
|
+
quotas,
|
|
3013
|
+
currency,
|
|
3014
|
+
onCurrency: setCurrency,
|
|
3015
|
+
turns,
|
|
3016
|
+
renderSlot,
|
|
3017
|
+
budgetEnabled,
|
|
3018
|
+
budgetAmount: effectiveBudget,
|
|
3019
|
+
onToggleBudget: toggleBudget,
|
|
3020
|
+
onBudgetAmount: actions.setAmount
|
|
1826
3021
|
})] });
|
|
1827
3022
|
}
|
|
1828
3023
|
//#endregion
|
|
@@ -1843,8 +3038,17 @@ window.__ModuleLoader__.load({
|
|
|
1843
3038
|
"billing.outputTokens": "输出",
|
|
1844
3039
|
"billing.avgCost": "平均成本",
|
|
1845
3040
|
"billing.trend": "每日费用与调用趋势",
|
|
3041
|
+
"billing.trend7d": "7 天",
|
|
3042
|
+
"billing.trend30d": "30 天",
|
|
1846
3043
|
"billing.trendEmpty": "暂无趋势数据",
|
|
3044
|
+
"billing.budget": "本月预算",
|
|
3045
|
+
"billing.sessions": "会话明细",
|
|
3046
|
+
"billing.project": "项目",
|
|
3047
|
+
"billing.lastActive": "最后活跃",
|
|
3048
|
+
"billing.sessionOverflow": "仅显示花费前 {limit} 个,共 {total} 个会话",
|
|
3049
|
+
"billing.budgetOverBody": "本月花费 {cost} 已超过预算 {budget}({pct}%)",
|
|
1847
3050
|
"billing.models": "模型计费明细",
|
|
3051
|
+
"billing.providerBilling": "厂商计费与订阅",
|
|
1848
3052
|
"billing.estimated": "估算",
|
|
1849
3053
|
"billing.actual": "实际",
|
|
1850
3054
|
"billing.pricing": "模型单价表",
|
|
@@ -1869,7 +3073,38 @@ window.__ModuleLoader__.load({
|
|
|
1869
3073
|
"billing.balance": "余额",
|
|
1870
3074
|
"billing.balanceUnconfigured": "未配置",
|
|
1871
3075
|
"billing.balanceUnauthorized": "密钥无效",
|
|
1872
|
-
"billing.balanceUnreachable": "查询失败"
|
|
3076
|
+
"billing.balanceUnreachable": "查询失败",
|
|
3077
|
+
"billing.uncatalogued": "未收录",
|
|
3078
|
+
"billing.balanceDays": "约可撑 {days} 天",
|
|
3079
|
+
"billing.balanceLowBody": "{name} 余额 {balance},约可撑 {days} 天,请及时充值",
|
|
3080
|
+
"billing.subscriptions": "订阅套餐",
|
|
3081
|
+
"billing.subscriptionNotConfigured": "未配置密钥",
|
|
3082
|
+
"billing.subscriptionUnauthorized": "密钥无效",
|
|
3083
|
+
"billing.subscriptionUnavailable": "查询失败",
|
|
3084
|
+
"billing.subscriptionInvalid": "响应异常",
|
|
3085
|
+
"billing.subscriptionRateLimited": "触发限流",
|
|
3086
|
+
"billing.subscriptionSession": "本次",
|
|
3087
|
+
"billing.subscriptionWeekly": "本周",
|
|
3088
|
+
"billing.subscriptionMonthly": "本月",
|
|
3089
|
+
"billing.subscriptionBilling": "计费周期",
|
|
3090
|
+
"billing.subscriptionRemaining": "剩余 {pct}%",
|
|
3091
|
+
"billing.subscriptionReset": "{date} 重置",
|
|
3092
|
+
"billing.subscriptionNoApi": "该厂商暂未提供用量查询接口",
|
|
3093
|
+
"billing.heatmapLess": "少",
|
|
3094
|
+
"billing.heatmapMore": "多",
|
|
3095
|
+
"billing.currency": "币种",
|
|
3096
|
+
"billing.currencyCny": "人民币",
|
|
3097
|
+
"billing.currencyUsd": "美元",
|
|
3098
|
+
"billing.heatmap": "用量热力图",
|
|
3099
|
+
"billing.rounds": "每轮费用",
|
|
3100
|
+
"billing.anomaly": "成本突增",
|
|
3101
|
+
"billing.workspaces": "工作区统计",
|
|
3102
|
+
"billing.plan": "套餐",
|
|
3103
|
+
"billing.remaining": "剩余",
|
|
3104
|
+
"billing.unknownModel": "未定价",
|
|
3105
|
+
"billing.model": "模型",
|
|
3106
|
+
"billing.currentRound": "当前",
|
|
3107
|
+
"billing.costAbbr": "费用"
|
|
1873
3108
|
};
|
|
1874
3109
|
const en = {
|
|
1875
3110
|
"billing.title": "Usage",
|
|
@@ -1886,8 +3121,17 @@ window.__ModuleLoader__.load({
|
|
|
1886
3121
|
"billing.outputTokens": "Output",
|
|
1887
3122
|
"billing.avgCost": "Avg cost",
|
|
1888
3123
|
"billing.trend": "Daily cost & calls",
|
|
3124
|
+
"billing.trend7d": "7D",
|
|
3125
|
+
"billing.trend30d": "30D",
|
|
1889
3126
|
"billing.trendEmpty": "No trend data yet",
|
|
3127
|
+
"billing.budget": "Monthly budget",
|
|
3128
|
+
"billing.sessions": "Sessions",
|
|
3129
|
+
"billing.project": "Project",
|
|
3130
|
+
"billing.lastActive": "Last active",
|
|
3131
|
+
"billing.sessionOverflow": "Top {limit} of {total} sessions by cost",
|
|
3132
|
+
"billing.budgetOverBody": "This month {cost} exceeded the budget {budget} ({pct}%)",
|
|
1890
3133
|
"billing.models": "Model billing",
|
|
3134
|
+
"billing.providerBilling": "Provider billing & subscriptions",
|
|
1891
3135
|
"billing.estimated": "Est.",
|
|
1892
3136
|
"billing.actual": "Actual",
|
|
1893
3137
|
"billing.pricing": "Model pricing",
|
|
@@ -1912,9 +3156,109 @@ window.__ModuleLoader__.load({
|
|
|
1912
3156
|
"billing.balance": "Balance",
|
|
1913
3157
|
"billing.balanceUnconfigured": "Not set",
|
|
1914
3158
|
"billing.balanceUnauthorized": "Bad key",
|
|
1915
|
-
"billing.balanceUnreachable": "Unavailable"
|
|
3159
|
+
"billing.balanceUnreachable": "Unavailable",
|
|
3160
|
+
"billing.uncatalogued": "Not catalogued",
|
|
3161
|
+
"billing.balanceDays": "~{days} days left",
|
|
3162
|
+
"billing.balanceLowBody": "{name} balance {balance}, ~{days} days left, please top up",
|
|
3163
|
+
"billing.subscriptions": "Subscriptions",
|
|
3164
|
+
"billing.subscriptionNotConfigured": "Key not set",
|
|
3165
|
+
"billing.subscriptionUnauthorized": "Bad key",
|
|
3166
|
+
"billing.subscriptionUnavailable": "Unavailable",
|
|
3167
|
+
"billing.subscriptionInvalid": "Bad response",
|
|
3168
|
+
"billing.subscriptionRateLimited": "Rate limited",
|
|
3169
|
+
"billing.subscriptionSession": "Current",
|
|
3170
|
+
"billing.subscriptionWeekly": "Weekly",
|
|
3171
|
+
"billing.subscriptionMonthly": "Monthly",
|
|
3172
|
+
"billing.subscriptionBilling": "Billing",
|
|
3173
|
+
"billing.subscriptionRemaining": "{pct}% left",
|
|
3174
|
+
"billing.subscriptionReset": "Resets {date}",
|
|
3175
|
+
"billing.subscriptionNoApi": "Provider does not offer a usage API",
|
|
3176
|
+
"billing.heatmapLess": "Less",
|
|
3177
|
+
"billing.heatmapMore": "More",
|
|
3178
|
+
"billing.currency": "Currency",
|
|
3179
|
+
"billing.currencyCny": "CNY",
|
|
3180
|
+
"billing.currencyUsd": "USD",
|
|
3181
|
+
"billing.heatmap": "Usage heatmap",
|
|
3182
|
+
"billing.rounds": "Cost per turn",
|
|
3183
|
+
"billing.anomaly": "Cost spike",
|
|
3184
|
+
"billing.workspaces": "Workspaces",
|
|
3185
|
+
"billing.plan": "Plan",
|
|
3186
|
+
"billing.remaining": "Left",
|
|
3187
|
+
"billing.unknownModel": "Unpriced",
|
|
3188
|
+
"billing.model": "Model",
|
|
3189
|
+
"billing.currentRound": "current",
|
|
3190
|
+
"billing.costAbbr": "cost"
|
|
1916
3191
|
};
|
|
1917
3192
|
//#endregion
|
|
3193
|
+
//#region src/client/billing-service.ts
|
|
3194
|
+
/** 创建计费指标运行时(apply 内调用,随插件纤维存活)。 */
|
|
3195
|
+
function createBillingMetrics() {
|
|
3196
|
+
let costs;
|
|
3197
|
+
let open;
|
|
3198
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3199
|
+
return {
|
|
3200
|
+
readCosts: () => costs,
|
|
3201
|
+
subscribeCosts: (listener) => {
|
|
3202
|
+
listener(costs);
|
|
3203
|
+
listeners.add(listener);
|
|
3204
|
+
return () => {
|
|
3205
|
+
listeners.delete(listener);
|
|
3206
|
+
};
|
|
3207
|
+
},
|
|
3208
|
+
openDashboard: () => {
|
|
3209
|
+
open?.();
|
|
3210
|
+
},
|
|
3211
|
+
publishCosts: (next) => {
|
|
3212
|
+
costs = next;
|
|
3213
|
+
for (const listener of listeners) listener(next);
|
|
3214
|
+
},
|
|
3215
|
+
registerOpen: (handler) => {
|
|
3216
|
+
open = handler;
|
|
3217
|
+
return () => {
|
|
3218
|
+
if (open === handler) open = void 0;
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
//#endregion
|
|
3224
|
+
//#region src/client/budget-store.ts
|
|
3225
|
+
/**
|
|
3226
|
+
* 预算偏好 store:本月预算的开关与金额。
|
|
3227
|
+
*
|
|
3228
|
+
* 用户在仪表盘里用开关控制预算条显隐、用数字输入框设置金额;状态经框架
|
|
3229
|
+
* store 引擎持久化到 localStorage(persist key 即存储身份),重启后保留。
|
|
3230
|
+
* 宿主 Config 的 monthlyBudget 仅作为金额未设置时的默认值,用户输入优先。
|
|
3231
|
+
*/
|
|
3232
|
+
/**
|
|
3233
|
+
* Declare the budget-preferences store handle.
|
|
3234
|
+
* @returns the store handle for the register call's store seat.
|
|
3235
|
+
*/
|
|
3236
|
+
function createBillingBudgetStore() {
|
|
3237
|
+
return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
|
|
3238
|
+
init: () => ({
|
|
3239
|
+
enabled: false,
|
|
3240
|
+
amount: 0,
|
|
3241
|
+
lastAlertDay: "",
|
|
3242
|
+
lastBalanceAlertDay: ""
|
|
3243
|
+
}),
|
|
3244
|
+
persist: "dsh.ui-usage-billing.budget",
|
|
3245
|
+
actions: {
|
|
3246
|
+
setEnabled: (d, on) => {
|
|
3247
|
+
d.enabled = on;
|
|
3248
|
+
},
|
|
3249
|
+
setAmount: (d, value) => {
|
|
3250
|
+
d.amount = Number.isFinite(value) && value > 0 ? value : 0;
|
|
3251
|
+
},
|
|
3252
|
+
markAlerted: (d, day) => {
|
|
3253
|
+
d.lastAlertDay = day;
|
|
3254
|
+
},
|
|
3255
|
+
markBalanceAlerted: (d, day) => {
|
|
3256
|
+
d.lastBalanceAlertDay = day;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
//#endregion
|
|
1918
3262
|
//#region src/client/apply.ts
|
|
1919
3263
|
/** Required services for the usage billing surface. */
|
|
1920
3264
|
const inject = [
|
|
@@ -1927,6 +3271,9 @@ window.__ModuleLoader__.load({
|
|
|
1927
3271
|
* @param ctx - client root context.
|
|
1928
3272
|
*/
|
|
1929
3273
|
function apply(ctx) {
|
|
3274
|
+
const metrics = createBillingMetrics();
|
|
3275
|
+
ctx.provide("billingMetrics", metrics);
|
|
3276
|
+
const budgetStore = createBillingBudgetStore();
|
|
1930
3277
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
1931
3278
|
zh,
|
|
1932
3279
|
en
|
|
@@ -1936,36 +3283,47 @@ window.__ModuleLoader__.load({
|
|
|
1936
3283
|
id: "usage-billing",
|
|
1937
3284
|
order: -10,
|
|
1938
3285
|
locale: NS,
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
3286
|
+
children: { "billing.dashboard.decor": {
|
|
3287
|
+
kind: "list",
|
|
3288
|
+
scope: "root"
|
|
3289
|
+
} },
|
|
3290
|
+
store: budgetStore,
|
|
3291
|
+
inject: () => ({
|
|
3292
|
+
checkModels: async () => {
|
|
3293
|
+
try {
|
|
3294
|
+
const { result } = await ctx.connection.api.llm.models({});
|
|
3295
|
+
if (!result.ok) return {
|
|
3296
|
+
checked: true,
|
|
3297
|
+
available: false,
|
|
3298
|
+
models: 0,
|
|
3299
|
+
failures: 0,
|
|
3300
|
+
okProviders: [],
|
|
3301
|
+
badProviders: []
|
|
3302
|
+
};
|
|
3303
|
+
return {
|
|
3304
|
+
checked: true,
|
|
3305
|
+
available: result.value.groups.length > 0,
|
|
3306
|
+
models: result.value.groups.reduce((sum, group) => sum + group.models.length, 0),
|
|
3307
|
+
failures: result.value.failures.length,
|
|
3308
|
+
okProviders: result.value.groups.map((group) => group.name),
|
|
3309
|
+
badProviders: result.value.failures.map((failure) => failure.name)
|
|
3310
|
+
};
|
|
3311
|
+
} catch {
|
|
3312
|
+
return {
|
|
3313
|
+
checked: true,
|
|
3314
|
+
available: false,
|
|
3315
|
+
models: 0,
|
|
3316
|
+
failures: 0,
|
|
3317
|
+
okProviders: [],
|
|
3318
|
+
badProviders: []
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
},
|
|
3322
|
+
publishCosts: (costs) => {
|
|
3323
|
+
metrics.publishCosts(costs);
|
|
3324
|
+
},
|
|
3325
|
+
registerOpen: (handler) => metrics.registerOpen(handler)
|
|
3326
|
+
})
|
|
1969
3327
|
}, UsageBilling));
|
|
1970
3328
|
}
|
|
1971
3329
|
//#endregion
|