@kenz1117/dsh-ui-usage-billing 0.1.0 → 0.2.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 +8 -3
- package/lib/client.js +219 -197
- package/lib/index.js +30 -5
- package/lib/types/aggregate.d.ts +12 -1
- package/lib/types/client/TrendChart.d.ts +21 -6
- package/lib/types/client/locales.d.ts +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ DeepSeek Harness 计费仪表盘插件。从持久化会话日志实时聚合模
|
|
|
9
9
|
|
|
10
10
|
## 特性
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- **侧边栏入口**:设置按钮上方的触发胶囊,显示**当月费用 + 今日费用**;折叠栏自动切换为图标。
|
|
13
|
+
- **计费仪表盘**:Hero 按 **当年 / 当月 / 当日** 三维展示费用,KPI 指标(缓存命中率 / Token 总量 / 单次平均成本 / 调用次数)、按模型分色的**堆叠趋势图**、按模型计费明细、可展开的单价表。
|
|
14
14
|
- **真实用量**:服务端从会话日志实时聚合,无需手工维护统计文件。
|
|
15
15
|
- **模型健康探测**:模型行的圆点反映各厂商接入状态(正常绿 / 异常红 / 未接入灰)。
|
|
16
16
|
- **订阅计划豁免**:走 coding/token 套餐的模型照常统计 token,费用记 0。
|
|
@@ -119,11 +119,16 @@ cost(CNY)= (missInput × p_input + cacheHit × p_cacheHit + output × p_outp
|
|
|
119
119
|
},
|
|
120
120
|
"byDay": {
|
|
121
121
|
"2026-08-15": { "calls": 74, "input": 32593373, "output": 35375, "cacheHit": 32558208, "cacheMiss": 35165, "cost": 0.52 }
|
|
122
|
+
},
|
|
123
|
+
"byDayModels": {
|
|
124
|
+
"2026-08-15": {
|
|
125
|
+
"flash": { "calls": 74, "input": 32593373, "output": 35375, "cacheHit": 32558208, "cacheMiss": 35165, "cost": 0.52 }
|
|
126
|
+
}
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
```
|
|
125
130
|
|
|
126
|
-
字段含义:`input` 为总输入 token;`cacheHit` / `cacheMiss` 为缓存命中 / 未命中分桶;`cost` 为人民币估算费用。`sessionPersistence` 不可用时回退到配置文件(见下)。
|
|
131
|
+
字段含义:`input` 为总输入 token;`cacheHit` / `cacheMiss` 为缓存命中 / 未命中分桶;`cost` 为人民币估算费用。`byDayModels` 是 **模型 × 日期** 二维统计(`[date][modelKey]`),趋势图按模型堆叠的输入;当年 / 当月 / 当日三维费用由浏览器端按 `byDay` 日期前缀归并。`sessionPersistence` 不可用时回退到配置文件(见下)。
|
|
127
132
|
|
|
128
133
|
## 配置
|
|
129
134
|
|
package/lib/client.js
CHANGED
|
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
25
|
//#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-l2);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:10px;padding:9px 12px;transition:border-color .14s,background-color .14s,transform .12s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_trigger:active{transform:scale(.985)}.VWh0dG_triggerIcon{background:linear-gradient(135deg, var(--dsw-static-blue-500), var(--dsw-static-deepseek-500));width:30px;height:30px;color:var(--dsw-static-neutral-bluish-00);border-radius:9px;flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:2.2px;width:15px;height:15px}.VWh0dG_triggerText{flex-direction:column;gap:1px;min-width:0;display:flex}.VWh0dG_triggerLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:16px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;align-items:baseline;gap:6px;font-size:11px;line-height:15px;display:flex}.VWh0dG_triggerMeta strong{color:var(--dsw-static-blue-500);font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}.VWh0dG_triggerMeta em{color:var(--dsw-alias-label-caption);font-style:normal}.VWh0dG_triggerChevron{width:14px;height:14px;color:var(--dsw-alias-label-caption);flex:none;margin-left:auto}.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{background:linear-gradient(135deg, var(--dsw-static-blue-800) 0%, var(--dsw-static-deepseek-600) 55%, var(--dsw-static-deepseek-500) 100%);min-height:118px;color:var(--dsw-static-neutral-bluish-00);border-radius:16px;align-items:center;gap:24px;padding:18px 22px;display:flex;position:relative;overflow:hidden}.VWh0dG_hero:after{content:\"\";pointer-events:none;background:radial-gradient(circle,#ffffff29 0%,#fff0 70%);border-radius:50%;width:200px;height:200px;position:absolute;top:-60px;right:-40px}.VWh0dG_heroMain{flex-direction:column;flex:auto;justify-content:center;gap:3px;min-width:0;display:flex}.VWh0dG_heroLabel{opacity:.82;white-space:nowrap;font-size:12px;font-weight:500;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:38px;font-weight:700;line-height:46px}.VWh0dG_heroMeta{opacity:.82;white-space:nowrap;align-items:center;gap:6px;font-size:12px;line-height:17px;display:flex}.VWh0dG_heroMeta em{background:#ffffff29;border-radius:6px;margin-left:2px;padding:1px 6px;font-style:normal}.VWh0dG_heroDivider{background:#ffffff38;flex:none;align-self:stretch;width:1px}.VWh0dG_heroSide{flex:none;align-items:center;min-width:140px;display:flex}.VWh0dG_heroSideItem{flex-direction:column;justify-content:center;gap:3px;display:flex}.VWh0dG_heroSideLabel{opacity:.82;white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:22px;font-weight:600;line-height:28px}.VWh0dG_delta{align-items:center;gap:2px;font-size:12px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:#9ff0b8}.VWh0dG_deltaDown{color:#ffb4b4}.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_chartLine{stroke:var(--dsw-static-blue-500);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_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);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_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:12px}.VWh0dG_heroMain{width:100%;min-width:0}.VWh0dG_heroDivider{align-self:stretch;width:100%;height:1px}.VWh0dG_heroSide{min-width:0}}";
|
|
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-l2);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:10px;padding:9px 12px;transition:border-color .14s,background-color .14s,transform .12s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_trigger:active{transform:scale(.985)}.VWh0dG_triggerIcon{background:linear-gradient(135deg, var(--dsw-static-blue-500), var(--dsw-static-deepseek-500));width:30px;height:30px;color:var(--dsw-static-neutral-bluish-00);border-radius:9px;flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:2.2px;width:15px;height:15px}.VWh0dG_triggerText{flex-direction:column;gap:1px;min-width:0;display:flex}.VWh0dG_triggerLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:16px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;align-items:baseline;gap:6px;font-size:11px;line-height:15px;display:flex}.VWh0dG_triggerMeta strong{color:var(--dsw-static-blue-500);font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}.VWh0dG_triggerMeta em{color:var(--dsw-alias-label-caption);font-style:normal}.VWh0dG_triggerChevron{width:14px;height:14px;color:var(--dsw-alias-label-caption);flex:none;margin-left:auto}.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{background:linear-gradient(135deg, var(--dsw-static-blue-800) 0%, var(--dsw-static-deepseek-600) 55%, var(--dsw-static-deepseek-500) 100%);min-height:118px;color:var(--dsw-static-neutral-bluish-00);border-radius:16px;align-items:center;gap:24px;padding:18px 22px;display:flex;position:relative;overflow:hidden}.VWh0dG_hero:after{content:\"\";pointer-events:none;background:radial-gradient(circle,#ffffff29 0%,#fff0 70%);border-radius:50%;width:200px;height:200px;position:absolute;top:-60px;right:-40px}.VWh0dG_heroMain{flex-direction:column;flex:auto;justify-content:center;gap:3px;min-width:0;display:flex}.VWh0dG_heroLabel{opacity:.82;white-space:nowrap;font-size:12px;font-weight:500;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:38px;font-weight:700;line-height:46px}.VWh0dG_heroMeta{opacity:.82;white-space:nowrap;align-items:center;gap:6px;font-size:12px;line-height:17px;display:flex}.VWh0dG_heroMeta em{background:#ffffff29;border-radius:6px;margin-left:2px;padding:1px 6px;font-style:normal}.VWh0dG_heroDivider{background:#ffffff38;flex:none;align-self:stretch;width:1px}.VWh0dG_heroSide{flex:none;align-items:center;gap:28px;min-width:140px;display:flex}.VWh0dG_heroSideItem{flex-direction:column;justify-content:center;gap:3px;display:flex}.VWh0dG_heroSideLabel{opacity:.82;white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:22px;font-weight:600;line-height:28px}.VWh0dG_delta{align-items:center;gap:2px;font-size:12px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:#9ff0b8}.VWh0dG_deltaDown{color:#ffb4b4}.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-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_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:12px}.VWh0dG_heroMain{width:100%;min-width:0}.VWh0dG_heroDivider{align-self:stretch;width:100%;height:1px}.VWh0dG_heroSide{min-width:0}}";
|
|
27
27
|
const tagId = "@kenz1117/dsh-ui-usage-billing/UsageBilling.module.css";
|
|
28
28
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
29
29
|
const tag = document.createElement("style");
|
|
@@ -33,87 +33,89 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
document.head.appendChild(tag);
|
|
34
34
|
}
|
|
35
35
|
var UsageBilling_module_css_default = {
|
|
36
|
-
"
|
|
37
|
-
"modelCell": "VWh0dG_modelCell",
|
|
38
|
-
"healthBad": "VWh0dG_healthBad",
|
|
39
|
-
"dashboardModal": "VWh0dG_dashboardModal",
|
|
40
|
-
"numCol": "VWh0dG_numCol",
|
|
41
|
-
"healthOk": "VWh0dG_healthOk",
|
|
42
|
-
"chartAxisLabel": "VWh0dG_chartAxisLabel",
|
|
43
|
-
"triggerText": "VWh0dG_triggerText",
|
|
44
|
-
"panelTitle": "VWh0dG_panelTitle",
|
|
45
|
-
"triggerChevron": "VWh0dG_triggerChevron",
|
|
46
|
-
"triggerIcon": "VWh0dG_triggerIcon",
|
|
36
|
+
"chartTooltipDate": "VWh0dG_chartTooltipDate",
|
|
47
37
|
"chartCrosshair": "VWh0dG_chartCrosshair",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"healthDot": "VWh0dG_healthDot",
|
|
38
|
+
"panelTitle": "VWh0dG_panelTitle",
|
|
39
|
+
"pricingToggleText": "VWh0dG_pricingToggleText",
|
|
40
|
+
"heroValue": "VWh0dG_heroValue",
|
|
41
|
+
"trigger": "VWh0dG_trigger",
|
|
42
|
+
"pricingChevronOpen": "VWh0dG_pricingChevronOpen",
|
|
54
43
|
"kpiLabel": "VWh0dG_kpiLabel",
|
|
55
|
-
"
|
|
56
|
-
"tableScroll": "VWh0dG_tableScroll",
|
|
57
|
-
"panel": "VWh0dG_panel",
|
|
44
|
+
"heroDivider": "VWh0dG_heroDivider",
|
|
58
45
|
"dashboardSubtitle": "VWh0dG_dashboardSubtitle",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
46
|
+
"modelCell": "VWh0dG_modelCell",
|
|
47
|
+
"bandTag": "VWh0dG_bandTag",
|
|
48
|
+
"healthDot": "VWh0dG_healthDot",
|
|
49
|
+
"deltaDown": "VWh0dG_deltaDown",
|
|
50
|
+
"triggerChevron": "VWh0dG_triggerChevron",
|
|
51
|
+
"dashboardRight": "VWh0dG_dashboardRight",
|
|
52
|
+
"chartLegend": "VWh0dG_chartLegend",
|
|
53
|
+
"heroSideValue": "VWh0dG_heroSideValue",
|
|
63
54
|
"chartWrap": "VWh0dG_chartWrap",
|
|
64
|
-
"healthBadgeBad": "VWh0dG_healthBadgeBad",
|
|
65
|
-
"heroMain": "VWh0dG_heroMain",
|
|
66
|
-
"chartEmpty": "VWh0dG_chartEmpty",
|
|
67
|
-
"kpiGrid": "VWh0dG_kpiGrid",
|
|
68
|
-
"chartTooltip": "VWh0dG_chartTooltip",
|
|
69
55
|
"heroMeta": "VWh0dG_heroMeta",
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"healthBadge": "VWh0dG_healthBadge",
|
|
76
|
-
"modelName": "VWh0dG_modelName",
|
|
77
|
-
"emptyRow": "VWh0dG_emptyRow",
|
|
78
|
-
"heroValue": "VWh0dG_heroValue",
|
|
79
|
-
"na": "VWh0dG_na",
|
|
80
|
-
"dashboardRight": "VWh0dG_dashboardRight",
|
|
56
|
+
"heroSide": "VWh0dG_heroSide",
|
|
57
|
+
"triggerText": "VWh0dG_triggerText",
|
|
58
|
+
"tableScroll": "VWh0dG_tableScroll",
|
|
59
|
+
"flatTag": "VWh0dG_flatTag",
|
|
60
|
+
"chartLegendBar": "VWh0dG_chartLegendBar",
|
|
81
61
|
"dashboardHead": "VWh0dG_dashboardHead",
|
|
62
|
+
"chartAxisLabel": "VWh0dG_chartAxisLabel",
|
|
63
|
+
"pricingChevron": "VWh0dG_pricingChevron",
|
|
64
|
+
"modelProvider": "VWh0dG_modelProvider",
|
|
82
65
|
"chartLine": "VWh0dG_chartLine",
|
|
83
|
-
"healthIdle": "VWh0dG_healthIdle",
|
|
84
|
-
"costCol": "VWh0dG_costCol",
|
|
85
|
-
"bandTag": "VWh0dG_bandTag",
|
|
86
|
-
"panelHead": "VWh0dG_panelHead",
|
|
87
|
-
"dashboardBody": "VWh0dG_dashboardBody",
|
|
88
|
-
"chartSvg": "VWh0dG_chartSvg",
|
|
89
66
|
"chartDot": "VWh0dG_chartDot",
|
|
67
|
+
"bandPrice": "VWh0dG_bandPrice",
|
|
90
68
|
"bandTagOff": "VWh0dG_bandTagOff",
|
|
91
|
-
"dashboardTitle": "VWh0dG_dashboardTitle",
|
|
92
|
-
"heroDivider": "VWh0dG_heroDivider",
|
|
93
|
-
"chartGrid": "VWh0dG_chartGrid",
|
|
94
|
-
"pricingToggleText": "VWh0dG_pricingToggleText",
|
|
95
|
-
"heroSide": "VWh0dG_heroSide",
|
|
96
|
-
"kpiDetail": "VWh0dG_kpiDetail",
|
|
97
|
-
"chartLegend": "VWh0dG_chartLegend",
|
|
98
|
-
"modelTable": "VWh0dG_modelTable",
|
|
99
|
-
"pricingTable": "VWh0dG_pricingTable",
|
|
100
|
-
"modelProvider": "VWh0dG_modelProvider",
|
|
101
69
|
"hero": "VWh0dG_hero",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
70
|
+
"modelTable": "VWh0dG_modelTable",
|
|
71
|
+
"planTag": "VWh0dG_planTag",
|
|
72
|
+
"chartGrid": "VWh0dG_chartGrid",
|
|
73
|
+
"triggerIcon": "VWh0dG_triggerIcon",
|
|
74
|
+
"healthOk": "VWh0dG_healthOk",
|
|
75
|
+
"chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
|
|
76
|
+
"healthIdle": "VWh0dG_healthIdle",
|
|
106
77
|
"pricingToggle": "VWh0dG_pricingToggle",
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
78
|
+
"kpiTile": "VWh0dG_kpiTile",
|
|
79
|
+
"panelHead": "VWh0dG_panelHead",
|
|
80
|
+
"chartBar": "VWh0dG_chartBar",
|
|
81
|
+
"na": "VWh0dG_na",
|
|
82
|
+
"chartTooltip": "VWh0dG_chartTooltip",
|
|
83
|
+
"bandPriceOff": "VWh0dG_bandPriceOff",
|
|
110
84
|
"triggerLabel": "VWh0dG_triggerLabel",
|
|
85
|
+
"numCol": "VWh0dG_numCol",
|
|
111
86
|
"panelHint": "VWh0dG_panelHint",
|
|
87
|
+
"healthBadgeOk": "VWh0dG_healthBadgeOk",
|
|
88
|
+
"healthBadgeBad": "VWh0dG_healthBadgeBad",
|
|
89
|
+
"chartStack": "VWh0dG_chartStack",
|
|
90
|
+
"dashboardModal": "VWh0dG_dashboardModal",
|
|
91
|
+
"kpiValue": "VWh0dG_kpiValue",
|
|
92
|
+
"chartSvg": "VWh0dG_chartSvg",
|
|
93
|
+
"heroMain": "VWh0dG_heroMain",
|
|
94
|
+
"chartLegendLine": "VWh0dG_chartLegendLine",
|
|
95
|
+
"dashboardTitle": "VWh0dG_dashboardTitle",
|
|
112
96
|
"closeButton": "VWh0dG_closeButton",
|
|
113
|
-
"
|
|
114
|
-
"
|
|
97
|
+
"costCol": "VWh0dG_costCol",
|
|
98
|
+
"healthBad": "VWh0dG_healthBad",
|
|
99
|
+
"triggerMeta": "VWh0dG_triggerMeta",
|
|
100
|
+
"modelName": "VWh0dG_modelName",
|
|
101
|
+
"delta": "VWh0dG_delta",
|
|
102
|
+
"healthBadge": "VWh0dG_healthBadge",
|
|
103
|
+
"chartEmpty": "VWh0dG_chartEmpty",
|
|
115
104
|
"dashboard": "VWh0dG_dashboard",
|
|
116
|
-
"
|
|
105
|
+
"heroLabel": "VWh0dG_heroLabel",
|
|
106
|
+
"deltaUp": "VWh0dG_deltaUp",
|
|
107
|
+
"heroSideItem": "VWh0dG_heroSideItem",
|
|
108
|
+
"kpiGrid": "VWh0dG_kpiGrid",
|
|
109
|
+
"dashboardBody": "VWh0dG_dashboardBody",
|
|
110
|
+
"kpiDetail": "VWh0dG_kpiDetail",
|
|
111
|
+
"panel": "VWh0dG_panel",
|
|
112
|
+
"railButton": "VWh0dG_railButton",
|
|
113
|
+
"emptyRow": "VWh0dG_emptyRow",
|
|
114
|
+
"chartTooltipRow": "VWh0dG_chartTooltipRow",
|
|
115
|
+
"pricingTable": "VWh0dG_pricingTable",
|
|
116
|
+
"heroSideLabel": "VWh0dG_heroSideLabel",
|
|
117
|
+
"kpiGreen": "VWh0dG_kpiGreen",
|
|
118
|
+
"modelDot": "VWh0dG_modelDot"
|
|
117
119
|
};
|
|
118
120
|
//#endregion
|
|
119
121
|
//#region src/client/pricing.ts
|
|
@@ -615,7 +617,11 @@ window.__ModuleLoader__.load({
|
|
|
615
617
|
];
|
|
616
618
|
/** Lookup a model by its stats key; falls back to the generic `other` entry. */
|
|
617
619
|
function modelOf(key) {
|
|
618
|
-
|
|
620
|
+
const found = MODEL_CATALOG.find((entry) => entry.key === key);
|
|
621
|
+
if (found !== void 0) return found;
|
|
622
|
+
const fallback = MODEL_CATALOG.at(-1);
|
|
623
|
+
if (fallback !== void 0) return fallback;
|
|
624
|
+
throw new Error("MODEL_CATALOG must not be empty");
|
|
619
625
|
}
|
|
620
626
|
/** Resolve a price-table row by its CSS variable name (theme token or fallback color). */
|
|
621
627
|
function resolveToken(name) {
|
|
@@ -685,9 +691,11 @@ window.__ModuleLoader__.load({
|
|
|
685
691
|
//#endregion
|
|
686
692
|
//#region src/client/TrendChart.tsx
|
|
687
693
|
/**
|
|
688
|
-
* TrendChart: dependency-free SVG
|
|
689
|
-
*
|
|
690
|
-
*
|
|
694
|
+
* TrendChart: dependency-free SVG stacked bar chart of daily cost per model.
|
|
695
|
+
* Each day's column stacks every model's share in its brand color, so the
|
|
696
|
+
* total trend and the per-model composition are visible at once. A hover
|
|
697
|
+
* crosshair shows the day's model breakdown. No chart library — the surface
|
|
698
|
+
* stays self-contained and offline.
|
|
691
699
|
*/
|
|
692
700
|
/** Fixed viewBox; the SVG scales to its container. */
|
|
693
701
|
const W = 680;
|
|
@@ -703,19 +711,6 @@ window.__ModuleLoader__.load({
|
|
|
703
711
|
const [, month, day] = iso.split("-");
|
|
704
712
|
return `${Number(month)}/${Number(day)}`;
|
|
705
713
|
}
|
|
706
|
-
/** Build an SVG path string through points with a smooth monotone-ish curve. */
|
|
707
|
-
function linePath(points) {
|
|
708
|
-
if (points.length === 0) return "";
|
|
709
|
-
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
|
710
|
-
let d = `M ${points[0].x} ${points[0].y}`;
|
|
711
|
-
for (let i = 1; i < points.length; i += 1) {
|
|
712
|
-
const prev = points[i - 1];
|
|
713
|
-
const curr = points[i];
|
|
714
|
-
const mx = (prev.x + curr.x) / 2;
|
|
715
|
-
d += ` C ${mx} ${prev.y}, ${mx} ${curr.y}, ${curr.x} ${curr.y}`;
|
|
716
|
-
}
|
|
717
|
-
return d;
|
|
718
|
-
}
|
|
719
714
|
/** Ticks every `step` items for sparse axis labels. */
|
|
720
715
|
function tickIndexes(length, step) {
|
|
721
716
|
const out = [];
|
|
@@ -724,16 +719,16 @@ window.__ModuleLoader__.load({
|
|
|
724
719
|
return out;
|
|
725
720
|
}
|
|
726
721
|
/**
|
|
727
|
-
* Render the daily
|
|
722
|
+
* Render the daily per-model stacked cost chart.
|
|
728
723
|
* @param props.data - sorted daily rows (ascending date).
|
|
724
|
+
* @param props.models - the model legend, in stack order (bottom first).
|
|
729
725
|
*/
|
|
730
|
-
function TrendChart({ data }) {
|
|
726
|
+
function TrendChart({ data, models = [] }) {
|
|
731
727
|
const [hover, setHover] = (0, react.useState)(null);
|
|
732
728
|
const layout = (0, react.useMemo)(() => {
|
|
733
729
|
const n = data.length;
|
|
734
730
|
if (n === 0) return null;
|
|
735
731
|
const maxCost = Math.max(...data.map((d) => d.cost), 1e-4);
|
|
736
|
-
const maxCalls = Math.max(...data.map((d) => d.calls), 1);
|
|
737
732
|
const plotW = W - PAD.left - PAD.right;
|
|
738
733
|
const plotH = H - PAD.top - PAD.bottom;
|
|
739
734
|
const inner = (i) => {
|
|
@@ -741,22 +736,35 @@ window.__ModuleLoader__.load({
|
|
|
741
736
|
return PAD.left + plotW * i / (n - 1);
|
|
742
737
|
};
|
|
743
738
|
const yCost = (value) => PAD.top + plotH - value / maxCost * plotH;
|
|
744
|
-
const yCalls = (value) => PAD.top + plotH - value / maxCalls * plotH;
|
|
745
|
-
const barW = Math.min(16, plotW / n * .5);
|
|
746
|
-
const line = linePath(data.map((d, i) => ({
|
|
747
|
-
x: inner(i),
|
|
748
|
-
y: yCost(d.cost)
|
|
749
|
-
})));
|
|
750
739
|
return {
|
|
751
740
|
n,
|
|
752
741
|
plotW,
|
|
753
742
|
plotH,
|
|
754
743
|
inner,
|
|
755
744
|
yCost,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
745
|
+
barW: Math.min(18, plotW / n * .5),
|
|
746
|
+
columns: data.map((d, i) => {
|
|
747
|
+
let acc = 0;
|
|
748
|
+
const segments = models.map((model) => {
|
|
749
|
+
const value = d.byModel?.[model.key] ?? 0;
|
|
750
|
+
const y0 = acc;
|
|
751
|
+
acc += value;
|
|
752
|
+
return {
|
|
753
|
+
model,
|
|
754
|
+
y0,
|
|
755
|
+
y1: acc,
|
|
756
|
+
rounded: false
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
const top = segments.at(-1);
|
|
760
|
+
if (top !== void 0) top.rounded = true;
|
|
761
|
+
return {
|
|
762
|
+
date: d.date,
|
|
763
|
+
x: inner(i),
|
|
764
|
+
segments,
|
|
765
|
+
total: acc
|
|
766
|
+
};
|
|
767
|
+
}),
|
|
760
768
|
costTicks: [
|
|
761
769
|
0,
|
|
762
770
|
.25,
|
|
@@ -764,23 +772,16 @@ window.__ModuleLoader__.load({
|
|
|
764
772
|
.75,
|
|
765
773
|
1
|
|
766
774
|
].map((f) => maxCost * f).reverse(),
|
|
767
|
-
|
|
768
|
-
0,
|
|
769
|
-
.25,
|
|
770
|
-
.5,
|
|
771
|
-
.75,
|
|
772
|
-
1
|
|
773
|
-
].map((f) => maxCalls * f).reverse(),
|
|
774
|
-
maxCost,
|
|
775
|
-
maxCalls
|
|
775
|
+
maxCost
|
|
776
776
|
};
|
|
777
|
-
}, [data]);
|
|
777
|
+
}, [data, models]);
|
|
778
778
|
if (layout === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
779
779
|
className: UsageBilling_module_css_default.chartEmpty,
|
|
780
780
|
children: "暂无趋势数据"
|
|
781
781
|
});
|
|
782
|
-
const { n, plotW, inner, yCost,
|
|
783
|
-
const
|
|
782
|
+
const { n, plotW, inner, yCost, barW, columns, costTicks } = layout;
|
|
783
|
+
const activeColumn = hover === null ? void 0 : columns[hover];
|
|
784
|
+
const activePoint = hover === null ? void 0 : data[hover];
|
|
784
785
|
const indices = tickIndexes(n, Math.max(1, Math.ceil(n / 8)));
|
|
785
786
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
786
787
|
className: UsageBilling_module_css_default.chartWrap,
|
|
@@ -789,7 +790,7 @@ window.__ModuleLoader__.load({
|
|
|
789
790
|
viewBox: `0 0 ${W} ${H}`,
|
|
790
791
|
className: UsageBilling_module_css_default.chartSvg,
|
|
791
792
|
role: "img",
|
|
792
|
-
"aria-label": "Daily cost
|
|
793
|
+
"aria-label": "Daily cost by model",
|
|
793
794
|
onMouseLeave: () => {
|
|
794
795
|
setHover(null);
|
|
795
796
|
},
|
|
@@ -800,22 +801,6 @@ window.__ModuleLoader__.load({
|
|
|
800
801
|
setHover(Math.min(Math.max(index, 0), n - 1));
|
|
801
802
|
},
|
|
802
803
|
children: [
|
|
803
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
|
|
804
|
-
id: "billing-cost-fill",
|
|
805
|
-
x1: "0",
|
|
806
|
-
y1: "0",
|
|
807
|
-
x2: "0",
|
|
808
|
-
y2: "1",
|
|
809
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
|
|
810
|
-
offset: "0%",
|
|
811
|
-
stopColor: "var(--dsw-static-blue-500)",
|
|
812
|
-
stopOpacity: "0.28"
|
|
813
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
|
|
814
|
-
offset: "100%",
|
|
815
|
-
stopColor: "var(--dsw-static-blue-500)",
|
|
816
|
-
stopOpacity: "0.02"
|
|
817
|
-
})]
|
|
818
|
-
}) }),
|
|
819
804
|
costTicks.map((value, idx) => {
|
|
820
805
|
const y = yCost(value);
|
|
821
806
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
@@ -832,86 +817,82 @@ window.__ModuleLoader__.load({
|
|
|
832
817
|
children: formatMoney(value)
|
|
833
818
|
})] }, `cost-${idx}`);
|
|
834
819
|
}),
|
|
835
|
-
|
|
836
|
-
|
|
820
|
+
columns.map((column) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", { children: column.segments.map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
821
|
+
x: column.x - barW / 2,
|
|
822
|
+
y: yCost(segment.y1),
|
|
823
|
+
width: barW,
|
|
824
|
+
height: Math.max(segment.y1 - segment.y0 > 0 ? 1 : 0, yCost(segment.y0) - yCost(segment.y1)),
|
|
825
|
+
rx: segment.rounded ? 3 : 0,
|
|
826
|
+
className: UsageBilling_module_css_default.chartStack,
|
|
827
|
+
style: { fill: segment.model.color }
|
|
828
|
+
}, segment.model.key)) }, column.date)),
|
|
829
|
+
indices.map((i) => {
|
|
830
|
+
const point = data[i];
|
|
831
|
+
if (point === void 0) return null;
|
|
837
832
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
838
|
-
x:
|
|
839
|
-
y:
|
|
833
|
+
x: inner(i),
|
|
834
|
+
y: H - 6,
|
|
835
|
+
textAnchor: "middle",
|
|
840
836
|
className: UsageBilling_module_css_default.chartAxisLabel,
|
|
841
|
-
children:
|
|
842
|
-
},
|
|
843
|
-
}),
|
|
844
|
-
data.map((d, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
845
|
-
x: inner(i) - barW / 2,
|
|
846
|
-
y: yCalls(d.calls),
|
|
847
|
-
width: barW,
|
|
848
|
-
height: Math.max(1, PAD.top + layout.plotH - yCalls(d.calls)),
|
|
849
|
-
rx: 2,
|
|
850
|
-
className: UsageBilling_module_css_default.chartBar
|
|
851
|
-
}, d.date)),
|
|
852
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
853
|
-
d: area,
|
|
854
|
-
fill: "url(#billing-cost-fill)"
|
|
837
|
+
children: shortDate(point.date)
|
|
838
|
+
}, point.date);
|
|
855
839
|
}),
|
|
856
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
857
|
-
d: line,
|
|
858
|
-
fill: "none",
|
|
859
|
-
className: UsageBilling_module_css_default.chartLine,
|
|
860
|
-
strokeWidth: 2
|
|
861
|
-
}),
|
|
862
|
-
indices.map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
863
|
-
x: inner(i),
|
|
864
|
-
y: H - 6,
|
|
865
|
-
textAnchor: "middle",
|
|
866
|
-
className: UsageBilling_module_css_default.chartAxisLabel,
|
|
867
|
-
children: shortDate(data[i].date)
|
|
868
|
-
}, data[i].date)),
|
|
869
|
-
active !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
840
|
+
activeColumn !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
870
841
|
x1: inner(hover),
|
|
871
842
|
x2: inner(hover),
|
|
872
843
|
y1: PAD.top,
|
|
873
844
|
y2: PAD.top + layout.plotH,
|
|
874
845
|
className: UsageBilling_module_css_default.chartCrosshair
|
|
875
|
-
})
|
|
876
|
-
cx: inner(hover),
|
|
877
|
-
cy: yCost(active.cost),
|
|
878
|
-
r: 4.5,
|
|
879
|
-
className: UsageBilling_module_css_default.chartDot
|
|
880
|
-
})] })
|
|
846
|
+
})
|
|
881
847
|
]
|
|
882
848
|
}),
|
|
883
|
-
|
|
849
|
+
activeColumn !== void 0 && activePoint !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
884
850
|
className: UsageBilling_module_css_default.chartTooltip,
|
|
885
851
|
style: {
|
|
886
852
|
left: `${inner(hover) / W * 100}%`,
|
|
887
|
-
top: `${yCost(
|
|
853
|
+
top: `${yCost(activeColumn.total) / H * 100}%`
|
|
888
854
|
},
|
|
889
855
|
children: [
|
|
890
856
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
891
857
|
className: UsageBilling_module_css_default.chartTooltipDate,
|
|
892
|
-
children:
|
|
858
|
+
children: activePoint.date
|
|
893
859
|
}),
|
|
860
|
+
activeColumn.segments.filter((segment) => segment.y1 - segment.y0 > 0).map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
861
|
+
className: UsageBilling_module_css_default.chartTooltipRow,
|
|
862
|
+
children: [
|
|
863
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
864
|
+
className: UsageBilling_module_css_default.chartTooltipSwatch,
|
|
865
|
+
style: { background: segment.model.color }
|
|
866
|
+
}),
|
|
867
|
+
segment.model.name,
|
|
868
|
+
" ",
|
|
869
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(segment.y1 - segment.y0) })
|
|
870
|
+
]
|
|
871
|
+
}, segment.model.key)),
|
|
894
872
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
895
873
|
className: UsageBilling_module_css_default.chartTooltipRow,
|
|
896
874
|
children: [
|
|
897
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.
|
|
898
|
-
"
|
|
899
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(
|
|
875
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendBar }),
|
|
876
|
+
"总计 ",
|
|
877
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(activeColumn.total) })
|
|
900
878
|
]
|
|
901
879
|
}),
|
|
902
880
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
903
881
|
className: UsageBilling_module_css_default.chartTooltipRow,
|
|
904
882
|
children: [
|
|
905
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.
|
|
883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendLine }),
|
|
906
884
|
"调用 ",
|
|
907
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children:
|
|
885
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: activePoint.calls.toLocaleString() })
|
|
908
886
|
]
|
|
909
887
|
})
|
|
910
888
|
]
|
|
911
889
|
}),
|
|
912
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
890
|
+
models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
913
891
|
className: UsageBilling_module_css_default.chartLegend,
|
|
914
|
-
children:
|
|
892
|
+
children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
893
|
+
className: UsageBilling_module_css_default.chartTooltipSwatch,
|
|
894
|
+
style: { background: model.color }
|
|
895
|
+
}), model.name] }, model.key))
|
|
915
896
|
})
|
|
916
897
|
]
|
|
917
898
|
});
|
|
@@ -1031,7 +1012,8 @@ window.__ModuleLoader__.load({
|
|
|
1031
1012
|
cost: 0
|
|
1032
1013
|
},
|
|
1033
1014
|
byModel: {},
|
|
1034
|
-
byDay: {}
|
|
1015
|
+
byDay: {},
|
|
1016
|
+
byDayModels: {}
|
|
1035
1017
|
};
|
|
1036
1018
|
/** Try to load stats from the server; returns null when no valid JSON stats are served. */
|
|
1037
1019
|
async function loadUsageStats() {
|
|
@@ -1051,12 +1033,12 @@ window.__ModuleLoader__.load({
|
|
|
1051
1033
|
* @param props - framework props plus `wide` column state.
|
|
1052
1034
|
*/
|
|
1053
1035
|
function UsageBillingTrigger(props) {
|
|
1054
|
-
const { wide, onOpen,
|
|
1036
|
+
const { wide, onOpen, monthCost, todayCost } = props;
|
|
1055
1037
|
if (!wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1056
1038
|
type: "button",
|
|
1057
1039
|
className: UsageBilling_module_css_default.railButton,
|
|
1058
1040
|
onClick: onOpen,
|
|
1059
|
-
title: formatMoney(
|
|
1041
|
+
title: formatMoney(monthCost),
|
|
1060
1042
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1061
1043
|
viewBox: "0 0 24 24",
|
|
1062
1044
|
fill: "none",
|
|
@@ -1088,8 +1070,8 @@ window.__ModuleLoader__.load({
|
|
|
1088
1070
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1089
1071
|
className: UsageBilling_module_css_default.triggerMeta,
|
|
1090
1072
|
children: [
|
|
1091
|
-
"
|
|
1092
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(
|
|
1073
|
+
"当月 ",
|
|
1074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(monthCost) }),
|
|
1093
1075
|
todayCost > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("em", { children: ["今日 ", formatMoney(todayCost)] })
|
|
1094
1076
|
]
|
|
1095
1077
|
})]
|
|
@@ -1115,12 +1097,31 @@ window.__ModuleLoader__.load({
|
|
|
1115
1097
|
const cacheHitRate = total.cacheHit + total.cacheMiss > 0 ? total.cacheHit / (total.cacheHit + total.cacheMiss) * 100 : 0;
|
|
1116
1098
|
const dates = Object.keys(byDay).sort();
|
|
1117
1099
|
const latestDate = dates.at(-1) ?? "";
|
|
1118
|
-
const
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1100
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1101
|
+
const todayCost = byDay[today]?.cost ?? 0;
|
|
1102
|
+
const monthPrefix = today.slice(0, 7);
|
|
1103
|
+
const yearPrefix = today.slice(0, 4);
|
|
1104
|
+
const monthCost = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
|
|
1105
|
+
const monthCalls = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.calls ?? 0 : 0), 0);
|
|
1106
|
+
const yearCost = dates.reduce((sum, d) => sum + (d.startsWith(yearPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
|
|
1107
|
+
const trend = (0, react.useMemo)(() => dates.map((date) => {
|
|
1108
|
+
const byModel = {};
|
|
1109
|
+
const dayModels = stats.byDayModels?.[date];
|
|
1110
|
+
if (dayModels !== void 0) {
|
|
1111
|
+
for (const [key, data] of Object.entries(dayModels)) if (data.cost > 0) byModel[key] = data.cost;
|
|
1112
|
+
}
|
|
1113
|
+
const day = byDay[date];
|
|
1114
|
+
return {
|
|
1115
|
+
date,
|
|
1116
|
+
cost: day?.cost ?? 0,
|
|
1117
|
+
calls: day?.calls ?? 0,
|
|
1118
|
+
byModel
|
|
1119
|
+
};
|
|
1120
|
+
}), [
|
|
1121
|
+
dates,
|
|
1122
|
+
byDay,
|
|
1123
|
+
stats.byDayModels
|
|
1124
|
+
]);
|
|
1124
1125
|
const modelRows = (0, react.useMemo)(() => Object.entries(byModel).filter(([, data]) => data.calls > 0).map(([key, data]) => {
|
|
1125
1126
|
const entry = modelOf(key);
|
|
1126
1127
|
const buckets = {
|
|
@@ -1146,7 +1147,12 @@ window.__ModuleLoader__.load({
|
|
|
1146
1147
|
const estimatedTotal = modelRows.reduce((sum, row) => sum + row.estimated, 0);
|
|
1147
1148
|
const displayTotal = total.cost > 0 ? total.cost : estimatedTotal;
|
|
1148
1149
|
const avgPerCall = total.calls > 0 ? displayTotal / total.calls : 0;
|
|
1149
|
-
const
|
|
1150
|
+
const chartModels = (0, react.useMemo)(() => modelRows.map((row) => ({
|
|
1151
|
+
key: row.key,
|
|
1152
|
+
name: row.name,
|
|
1153
|
+
color: row.color
|
|
1154
|
+
})), [modelRows]);
|
|
1155
|
+
const prevDayCost = trend.length >= 2 ? trend.at(-2)?.cost ?? 0 : 0;
|
|
1150
1156
|
const deltaPct = prevDayCost > 0 ? (todayCost - prevDayCost) / prevDayCost * 100 : 0;
|
|
1151
1157
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1152
1158
|
open: true,
|
|
@@ -1201,27 +1207,35 @@ window.__ModuleLoader__.load({
|
|
|
1201
1207
|
children: [
|
|
1202
1208
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1203
1209
|
className: UsageBilling_module_css_default.heroLabel,
|
|
1204
|
-
children: t("billing.
|
|
1210
|
+
children: t("billing.monthCost")
|
|
1205
1211
|
}),
|
|
1206
1212
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1207
1213
|
className: UsageBilling_module_css_default.heroValue,
|
|
1208
|
-
children: formatMoney(
|
|
1214
|
+
children: formatMoney(monthCost)
|
|
1209
1215
|
}),
|
|
1210
1216
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1211
1217
|
className: UsageBilling_module_css_default.heroMeta,
|
|
1212
1218
|
children: [
|
|
1213
|
-
|
|
1219
|
+
monthCalls.toLocaleString(),
|
|
1214
1220
|
" ",
|
|
1215
|
-
t("billing.calls")
|
|
1216
|
-
total.cost > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: "· 实际" })
|
|
1221
|
+
t("billing.calls")
|
|
1217
1222
|
]
|
|
1218
1223
|
})
|
|
1219
1224
|
]
|
|
1220
1225
|
}),
|
|
1221
1226
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: UsageBilling_module_css_default.heroDivider }),
|
|
1222
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1227
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1223
1228
|
className: UsageBilling_module_css_default.heroSide,
|
|
1224
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1229
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1230
|
+
className: UsageBilling_module_css_default.heroSideItem,
|
|
1231
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1232
|
+
className: UsageBilling_module_css_default.heroSideLabel,
|
|
1233
|
+
children: t("billing.yearCost")
|
|
1234
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1235
|
+
className: UsageBilling_module_css_default.heroSideValue,
|
|
1236
|
+
children: formatMoney(yearCost)
|
|
1237
|
+
})]
|
|
1238
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1225
1239
|
className: UsageBilling_module_css_default.heroSideItem,
|
|
1226
1240
|
children: [
|
|
1227
1241
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1242,7 +1256,7 @@ window.__ModuleLoader__.load({
|
|
|
1242
1256
|
]
|
|
1243
1257
|
})
|
|
1244
1258
|
]
|
|
1245
|
-
})
|
|
1259
|
+
})]
|
|
1246
1260
|
})
|
|
1247
1261
|
]
|
|
1248
1262
|
}),
|
|
@@ -1350,7 +1364,10 @@ window.__ModuleLoader__.load({
|
|
|
1350
1364
|
className: UsageBilling_module_css_default.panelHint,
|
|
1351
1365
|
children: latestDate
|
|
1352
1366
|
})]
|
|
1353
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendChart, {
|
|
1367
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendChart, {
|
|
1368
|
+
data: trend,
|
|
1369
|
+
models: chartModels
|
|
1370
|
+
})]
|
|
1354
1371
|
}),
|
|
1355
1372
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1356
1373
|
className: UsageBilling_module_css_default.panel,
|
|
@@ -1523,7 +1540,7 @@ window.__ModuleLoader__.load({
|
|
|
1523
1540
|
}),
|
|
1524
1541
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1525
1542
|
className: UsageBilling_module_css_default.numCol,
|
|
1526
|
-
children: entry.price.offPeak !== void 0
|
|
1543
|
+
children: entry.price.offPeak !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1527
1544
|
className: UsageBilling_module_css_default.bandPrice,
|
|
1528
1545
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatUnitPrice(entry.price.cacheHit, entry.price.currency) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1529
1546
|
className: UsageBilling_module_css_default.bandPriceOff,
|
|
@@ -1598,10 +1615,11 @@ window.__ModuleLoader__.load({
|
|
|
1598
1615
|
};
|
|
1599
1616
|
}, [checkModels]);
|
|
1600
1617
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1618
|
+
const monthCost = Object.entries(stats.byDay).filter(([date]) => date.startsWith(today.slice(0, 7))).reduce((sum, [, day]) => sum + day.cost, 0);
|
|
1601
1619
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBillingTrigger, {
|
|
1602
1620
|
...props,
|
|
1603
1621
|
onOpen: openDashboard,
|
|
1604
|
-
|
|
1622
|
+
monthCost,
|
|
1605
1623
|
todayCost: stats.byDay[today]?.cost ?? 0
|
|
1606
1624
|
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BillingDashboard, {
|
|
1607
1625
|
stats,
|
|
@@ -1618,6 +1636,8 @@ window.__ModuleLoader__.load({
|
|
|
1618
1636
|
"billing.subtitle": "计费仪表盘",
|
|
1619
1637
|
"billing.cost": "费用",
|
|
1620
1638
|
"billing.todayCost": "今日费用",
|
|
1639
|
+
"billing.monthCost": "本月费用",
|
|
1640
|
+
"billing.yearCost": "本年费用",
|
|
1621
1641
|
"billing.totalCost": "总费用",
|
|
1622
1642
|
"billing.calls": "调用",
|
|
1623
1643
|
"billing.cacheHitRate": "缓存命中率",
|
|
@@ -1652,6 +1672,8 @@ window.__ModuleLoader__.load({
|
|
|
1652
1672
|
"billing.subtitle": "Billing dashboard",
|
|
1653
1673
|
"billing.cost": "Cost",
|
|
1654
1674
|
"billing.todayCost": "Today",
|
|
1675
|
+
"billing.monthCost": "This month",
|
|
1676
|
+
"billing.yearCost": "This year",
|
|
1655
1677
|
"billing.totalCost": "Total",
|
|
1656
1678
|
"billing.calls": "Calls",
|
|
1657
1679
|
"billing.cacheHitRate": "Cache Hit",
|
package/lib/index.js
CHANGED
|
@@ -500,7 +500,11 @@ const MODEL_CATALOG = [
|
|
|
500
500
|
];
|
|
501
501
|
/** Lookup a model by its stats key; falls back to the generic `other` entry. */
|
|
502
502
|
function modelOf(key) {
|
|
503
|
-
|
|
503
|
+
const found = MODEL_CATALOG.find((entry) => entry.key === key);
|
|
504
|
+
if (found !== void 0) return found;
|
|
505
|
+
const fallback = MODEL_CATALOG.at(-1);
|
|
506
|
+
if (fallback !== void 0) return fallback;
|
|
507
|
+
throw new Error("MODEL_CATALOG must not be empty");
|
|
504
508
|
}
|
|
505
509
|
/**
|
|
506
510
|
* Price one band's token usage in CNY. The stats `input` field is the TOTAL
|
|
@@ -607,6 +611,23 @@ function dayStamp(time) {
|
|
|
607
611
|
const pad = (n) => String(n).padStart(2, "0");
|
|
608
612
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
609
613
|
}
|
|
614
|
+
/** Get-or-create one model cell inside a usage map (avoids non-null assertions). */
|
|
615
|
+
function usageCell(map, key) {
|
|
616
|
+
const existing = map.get(key);
|
|
617
|
+
if (existing !== void 0) return existing;
|
|
618
|
+
const fresh = emptyUsage();
|
|
619
|
+
map.set(key, fresh);
|
|
620
|
+
return fresh;
|
|
621
|
+
}
|
|
622
|
+
/** Get-or-create one day's model cell inside the two-dimensional map. */
|
|
623
|
+
function modelDayCell(map, day, modelKey) {
|
|
624
|
+
let models = map.get(day);
|
|
625
|
+
if (models === void 0) {
|
|
626
|
+
models = /* @__PURE__ */ new Map();
|
|
627
|
+
map.set(day, models);
|
|
628
|
+
}
|
|
629
|
+
return usageCell(models, modelKey);
|
|
630
|
+
}
|
|
610
631
|
/**
|
|
611
632
|
* Aggregate real usage from every persisted session log.
|
|
612
633
|
* @param persistence - the session persistence service.
|
|
@@ -618,6 +639,7 @@ async function aggregateUsage(persistence, options = {}) {
|
|
|
618
639
|
const total = emptyUsage();
|
|
619
640
|
const byModel = /* @__PURE__ */ new Map();
|
|
620
641
|
const byDay = /* @__PURE__ */ new Map();
|
|
642
|
+
const byModelDay = /* @__PURE__ */ new Map();
|
|
621
643
|
for (const meta of await persistence.list()) {
|
|
622
644
|
const { events } = await persistence.readFrom(meta.id, 0);
|
|
623
645
|
let key = "other";
|
|
@@ -633,18 +655,21 @@ async function aggregateUsage(persistence, options = {}) {
|
|
|
633
655
|
const modelKey = key;
|
|
634
656
|
const day = dayStamp(event.time);
|
|
635
657
|
foldUsage(total, event.data.usage, modelKey, subscription);
|
|
636
|
-
foldUsage(
|
|
637
|
-
foldUsage(
|
|
658
|
+
foldUsage(usageCell(byModel, modelKey), event.data.usage, modelKey, subscription);
|
|
659
|
+
foldUsage(usageCell(byDay, day), event.data.usage, modelKey, subscription);
|
|
660
|
+
foldUsage(modelDayCell(byModelDay, day, modelKey), event.data.usage, modelKey, subscription);
|
|
638
661
|
}
|
|
639
662
|
}
|
|
640
663
|
const toRecord = (map) => Object.fromEntries(map);
|
|
664
|
+
const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
|
|
641
665
|
return {
|
|
642
|
-
version:
|
|
666
|
+
version: 2,
|
|
643
667
|
updatedAt: Date.now(),
|
|
644
668
|
source: "session-logs",
|
|
645
669
|
total,
|
|
646
670
|
byModel: toRecord(byModel),
|
|
647
|
-
byDay: toRecord(byDay)
|
|
671
|
+
byDay: toRecord(byDay),
|
|
672
|
+
byDayModels: toModelDayRecord(byModelDay)
|
|
648
673
|
};
|
|
649
674
|
}
|
|
650
675
|
//#endregion
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -56,11 +56,22 @@ export declare function dayStamp(time: number): string;
|
|
|
56
56
|
* `SessionPersistence` to list sessions and read each log once.
|
|
57
57
|
*/
|
|
58
58
|
export type UsagePersistence = Pick<SessionPersistence, 'list' | 'readFrom'>;
|
|
59
|
+
/** The usage-stats document served to the billing dashboard. */
|
|
60
|
+
export interface UsageStatsDocument {
|
|
61
|
+
version: number;
|
|
62
|
+
updatedAt: number;
|
|
63
|
+
source: 'session-logs';
|
|
64
|
+
total: ModelUsage;
|
|
65
|
+
byModel: Record<string, ModelUsage>;
|
|
66
|
+
byDay: Record<string, ModelUsage>;
|
|
67
|
+
/** 模型 × 日期 二维统计:趋势图按模型堆叠的输入([date][modelKey])。 */
|
|
68
|
+
byDayModels: Record<string, Record<string, ModelUsage>>;
|
|
69
|
+
}
|
|
59
70
|
/**
|
|
60
71
|
* Aggregate real usage from every persisted session log.
|
|
61
72
|
* @param persistence - the session persistence service.
|
|
62
73
|
* @param options - aggregation tuning (e.g. subscription-plan providers).
|
|
63
74
|
* @returns the usage-stats document (same shape the dashboard expects).
|
|
64
75
|
*/
|
|
65
|
-
export declare function aggregateUsage(persistence: UsagePersistence, options?: AggregateOptions): Promise<
|
|
76
|
+
export declare function aggregateUsage(persistence: UsagePersistence, options?: AggregateOptions): Promise<UsageStatsDocument>;
|
|
66
77
|
//# sourceMappingURL=aggregate.d.ts.map
|
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TrendChart: dependency-free SVG
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* TrendChart: dependency-free SVG stacked bar chart of daily cost per model.
|
|
3
|
+
* Each day's column stacks every model's share in its brand color, so the
|
|
4
|
+
* total trend and the per-model composition are visible at once. A hover
|
|
5
|
+
* crosshair shows the day's model breakdown. No chart library — the surface
|
|
6
|
+
* stays self-contained and offline.
|
|
5
7
|
*/
|
|
8
|
+
/** One model's legend identity: key, display name, and brand color. */
|
|
9
|
+
export interface TrendSeriesModel {
|
|
10
|
+
/** Stats key (`byModel` key), also the `byModel` map key. */
|
|
11
|
+
key: string;
|
|
12
|
+
/** Human-readable model name. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Resolved brand color for the stack segment and legend swatch. */
|
|
15
|
+
color: string;
|
|
16
|
+
}
|
|
6
17
|
/** One day row fed to the chart. */
|
|
7
18
|
export interface TrendPoint {
|
|
8
19
|
/** ISO date `YYYY-MM-DD`. */
|
|
9
20
|
date: string;
|
|
10
|
-
/**
|
|
21
|
+
/** Total cost that day. */
|
|
11
22
|
cost: number;
|
|
12
23
|
/** API calls that day. */
|
|
13
24
|
calls: number;
|
|
25
|
+
/** Per-model cost that day (stats key → CNY); absent entries stack zero. */
|
|
26
|
+
byModel?: Readonly<Record<string, number>>;
|
|
14
27
|
}
|
|
15
28
|
/**
|
|
16
|
-
* Render the daily
|
|
29
|
+
* Render the daily per-model stacked cost chart.
|
|
17
30
|
* @param props.data - sorted daily rows (ascending date).
|
|
31
|
+
* @param props.models - the model legend, in stack order (bottom first).
|
|
18
32
|
*/
|
|
19
|
-
export declare function TrendChart({ data }: {
|
|
33
|
+
export declare function TrendChart({ data, models }: {
|
|
20
34
|
data: readonly TrendPoint[];
|
|
35
|
+
models?: readonly TrendSeriesModel[];
|
|
21
36
|
}): React.ReactNode;
|
|
22
37
|
//# sourceMappingURL=TrendChart.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Locale dictionaries for the usage billing surface. */
|
|
2
|
-
export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData';
|
|
2
|
+
export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData';
|
|
3
3
|
export declare const NS = "usageBilling";
|
|
4
4
|
export declare const zh: Record<UsageBillingKey, string>;
|
|
5
5
|
export declare const en: Record<UsageBillingKey, string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenz1117/dsh-ui-usage-billing",
|
|
3
3
|
"description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -64,16 +64,16 @@
|
|
|
64
64
|
"clsx": "^2.1.1"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
68
|
-
"@deepseek-ai/dsh-client-runtime": "
|
|
69
|
-
"@deepseek-ai/dsh-client-test-runtime": "
|
|
70
|
-
"@deepseek-ai/dsh-client-ui-conversation": "
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
72
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
73
|
-
"@deepseek-ai/dsh-invariants": "
|
|
67
|
+
"@deepseek-ai/dsh-client-locale": "*",
|
|
68
|
+
"@deepseek-ai/dsh-client-runtime": "*",
|
|
69
|
+
"@deepseek-ai/dsh-client-test-runtime": "*",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-conversation": "*",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-slots": "*",
|
|
73
|
+
"@deepseek-ai/dsh-invariants": "*",
|
|
74
74
|
"@testing-library/react": "^16.1.0",
|
|
75
75
|
"@types/react": "~18.3.1",
|
|
76
|
-
"@deepseek-ai/cordis": "
|
|
76
|
+
"@deepseek-ai/cordis": "*",
|
|
77
77
|
"react": "^18.2.0",
|
|
78
78
|
"react-dom": "^18.2.0"
|
|
79
79
|
},
|