@wannanbigpig/dsh-usage-stats 0.1.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/lib/client.js ADDED
@@ -0,0 +1,2428 @@
1
+ /**
2
+ * dsh-usage-stats — browser half.
3
+ *
4
+ * Hand-written `__ModuleLoader__` bundle (no build step): registers a native
5
+ * sidebar footer action that opens a floating Usage & Balance panel. The
6
+ * panel keeps the DeepSeek balance, local provider-reported usage, calendar
7
+ * heatmap, hourly/model drill-down and limit controls in one place.
8
+ */
9
+ window.__ModuleLoader__.load({
10
+ id: "dsh-usage-stats",
11
+ factory: (require) => {
12
+ var module = { exports: {} };
13
+ var exports = module.exports;
14
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
15
+
16
+ let react = require("react");
17
+ let react_jsx_runtime = require("react/jsx-runtime");
18
+ let react_dom = require("react-dom");
19
+ let primitives = require("@deepseek-ai/dsh-client-ui-primitives");
20
+
21
+ //#region css
22
+ const css = [
23
+ ".usg_layer{flex:none;align-items:center;width:100%;height:49px;margin:8px 0 0;display:flex;position:relative}",
24
+ ".usg_footerButtons{align-items:center;width:100%;display:flex}",
25
+ ".usg_sidebarButton{width:100%;height:49px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:8px;padding:0 8px 0 6px;font:inherit;font-size:14px;display:inline-flex;overflow:hidden}",
26
+ ".usg_sidebarButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}",
27
+ ".usg_sidebarButton[data-active]{background:var(--dsw-alias-interactive-bg-hover)}",
28
+ ".usg_sidebarText{min-width:0;flex:1;flex-direction:column;align-items:flex-start;display:flex;overflow:hidden}",
29
+ ".usg_sidebarLabel{text-overflow:ellipsis;white-space:nowrap;width:100%;line-height:20px;text-align:left;overflow:hidden}",
30
+ ".usg_sidebarSummary{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:10px;line-height:14px;text-align:left;font-variant-numeric:tabular-nums;overflow:hidden}",
31
+ ".usg_statusItem{display:inline-flex;align-items:center;white-space:nowrap;vertical-align:middle}",
32
+ ".usg_statusDot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-caption);display:inline-block;flex:none;margin-inline-end:5px;vertical-align:middle;box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-label-caption) 12%,transparent)}",
33
+ ".usg_statusDot[data-tone=ok]{background:#22a06b;box-shadow:0 0 0 2px color-mix(in srgb,#22a06b 14%,transparent)}",
34
+ ".usg_statusDot[data-tone=warn]{background:#d99b00;box-shadow:0 0 0 2px color-mix(in srgb,#d99b00 14%,transparent)}",
35
+ ".usg_statusDot[data-tone=bad]{background:#e5484d;box-shadow:0 0 0 2px color-mix(in srgb,#e5484d 14%,transparent)}",
36
+ ".usg_layer.usg_rail{width:36px;height:36px;margin:0}",
37
+ ".usg_layer.usg_rail .usg_sidebarButton{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}",
38
+ ".usg_panel{z-index:100;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));width:760px;max-width:calc(100vw - 32px);max-height:86vh;box-shadow:var(--dsw-shadow-lv2);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);--usg-blue:#1f6feb;--usg-green:#23a878;--usg-cellEmpty:rgba(128,128,128,.16);border-radius:14px;position:fixed;bottom:72px;left:12px;overflow:hidden}",
39
+ ".usg_panelBody{box-sizing:border-box;max-height:86vh;padding:14px;overflow-y:auto}",
40
+ ".usg_panel .usg_section{max-width:none}",
41
+ ".usg_section{--usg-blue:#1f6feb;--usg-action:var(--dsw-alias-label-primary,#171717);--usg-action-text:var(--dsw-alias-bg-base,#fff);--usg-control-bg:var(--dsw-alias-bg-base,#fff);--usg-control-text:var(--dsw-alias-label-primary,#171717);--usg-control-border:rgba(128,128,128,.24);--usg-control-placeholder:rgba(128,128,128,.48);--usg-success:#22a06b;--usg-warning:#d99b00;--usg-danger:#e5484d;box-sizing:border-box;flex-direction:column;gap:12px;width:100%;max-width:720px;display:flex}",
42
+ ".usg_section[data-usage-billing-settings]{gap:12px;max-width:760px}",
43
+ ".usg_section[data-usage-billing-settings] .usg_title{margin:0;font-size:18px;font-weight:600;line-height:25px}",
44
+ ".usg_section[data-usage-billing-settings] .usg_note{margin:0;font-size:13px;line-height:20px}",
45
+ ".usg_header{position:sticky;top:-14px;z-index:11;justify-content:space-between;align-items:center;gap:8px;padding-top:14px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));display:flex}",
46
+ ".usg_headerLeft{align-items:center;gap:8px;min-width:0;display:flex}",
47
+ ".usg_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
48
+ ".usg_headerActions{align-items:center;gap:2px;display:flex}",
49
+ ".usg_iconButton{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:8px;justify-content:center;align-items:center;padding:0;display:inline-flex;flex:none}",
50
+ ".usg_iconButton:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
51
+ ".usg_iconButton[disabled]{cursor:default;opacity:.45}",
52
+ "@keyframes usg_spin{to{transform:rotate(360deg)}}",
53
+ ".usg_iconButton[data-loading=true] svg{animation:usg_spin .75s linear infinite;transform-origin:center}",
54
+ ".usg_note{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}",
55
+ ".usg_error{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}",
56
+ ".usg_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0}",
57
+ ".usg_updated{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}",
58
+ // Balance card — the 消费金额 icon seat.
59
+ ".usg_balance{border:1px solid var(--dsw-alias-border-l2);background:linear-gradient(135deg,color-mix(in srgb,var(--usg-blue) 7%,transparent),transparent 45%);border-radius:14px;padding:12px 14px;flex-direction:column;gap:10px;display:flex}",
60
+ ".usg_balanceHead{align-items:center;gap:10px;display:flex}",
61
+ ".usg_balanceIcon{width:40px;height:40px;color:#fff;background:linear-gradient(135deg,#1f6feb,#0b3d91);border-radius:12px;justify-content:center;align-items:center;font-size:15px;font-weight:700;display:flex;flex:none;box-shadow:0 4px 12px rgba(31,111,235,.3)}",
62
+ ".usg_balanceIdentity{min-width:0;flex:1;display:flex;flex-direction:column}",
63
+ ".usg_balanceLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
64
+ ".usg_balanceMain{align-items:baseline;gap:8px;display:flex}",
65
+ ".usg_balanceAmount{color:var(--dsw-alias-label-primary);font-size:24px;font-weight:600;line-height:32px;font-variant-numeric:tabular-nums}",
66
+ ".usg_balanceCurrency{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;font-weight:500}",
67
+ ".usg_badge{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;white-space:nowrap;flex:none}",
68
+ ".usg_badge[data-tone=ok]{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent)}",
69
+ ".usg_badge[data-tone=warn]{color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 12%,transparent)}",
70
+ ".usg_badge[data-tone=bad]{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}",
71
+ ".usg_balanceRows{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:2px;font-size:12px;line-height:18px;display:flex}",
72
+ ".usg_balanceRow{justify-content:space-between;gap:8px;display:flex}",
73
+ ".usg_balanceRow b{color:var(--dsw-alias-label-primary);font-weight:600;font-variant-numeric:tabular-nums}",
74
+ ".usg_pickerRow{align-items:center;gap:8px;display:flex;flex-wrap:wrap}",
75
+ ".usg_pickerLabel{color:var(--dsw-alias-label-tertiary);align-items:center;gap:5px;font-size:12px;line-height:18px;display:inline-flex;flex:none}",
76
+ ".usg_select{box-sizing:border-box;min-width:0;height:34px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:0 12px;font:inherit;font-size:13px;line-height:20px;cursor:pointer}",
77
+ ".usg_statsRow{display:flex;gap:8px;flex-wrap:wrap}",
78
+ ".usg_stat{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex:1;min-width:110px;flex-direction:column;gap:1px;padding:8px 10px;display:flex}",
79
+ ".usg_statValue{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:22px;font-variant-numeric:tabular-nums;white-space:nowrap}",
80
+ ".usg_statValue.usg_statMoney{color:var(--usg-blue)}",
81
+ ".usg_valueWithIndicator{display:inline-flex;align-items:center;gap:6px}",
82
+ ".usg_statLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
83
+ ".usg_card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;flex-direction:column;gap:10px;display:flex}",
84
+ ".usg_cardHead{justify-content:space-between;align-items:center;gap:8px;display:flex}",
85
+ ".usg_cardTitle{color:var(--dsw-alias-label-primary);align-items:center;gap:6px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}",
86
+ ".usg_dayNav{align-items:center;gap:2px;display:flex}",
87
+ ".usg_navButton{cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}",
88
+ ".usg_navButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
89
+ ".usg_navButton:disabled{color:var(--dsw-alias-label-caption);cursor:default}",
90
+ ".usg_dayTitle{color:var(--dsw-alias-label-primary);min-width:92px;font-size:12px;font-weight:500;line-height:24px;text-align:center;font-variant-numeric:tabular-nums}",
91
+ ".usg_todayButton{cursor:pointer;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:6px;padding:0 6px;font-size:11px;line-height:24px}",
92
+ ".usg_todayButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
93
+ // Hourly bars.
94
+ ".usg_chart{width:100%;overflow-x:auto}",
95
+ ".usg_chartInner{min-width:560px;flex-direction:column;gap:6px;display:flex}",
96
+ ".usg_chartBody{height:132px;border-bottom:1px solid var(--dsw-alias-border-l2);align-items:flex-end;gap:2px;display:flex;position:relative;overflow:visible}",
97
+ ".usg_hourSlot{box-sizing:border-box;flex:1;min-width:0;height:100%;border:0;padding:0;background:transparent;flex-direction:column;justify-content:flex-end;align-items:stretch;display:flex;position:relative;border-radius:3px;cursor:crosshair}",
98
+ ".usg_hourSlot:hover,.usg_hourSlot:focus-visible{outline:none;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--usg-blue) 45%,transparent)}",
99
+ ".usg_hourBar{width:72%;margin:0 auto;border-radius:3px 3px 0 0;display:flex;flex:none;flex-direction:column;justify-content:flex-end;overflow:hidden}",
100
+ ".usg_hourInput{background:var(--usg-blue);width:100%;min-height:1px;flex-basis:0}",
101
+ ".usg_hourOutput{background:var(--usg-green);width:100%;min-height:1px;flex-basis:0}",
102
+ ".usg_peakRegion{position:absolute;top:0;bottom:0;background:color-mix(in srgb,var(--usg-blue) 5%,transparent);border-left:1px solid color-mix(in srgb,var(--usg-blue) 14%,transparent);border-right:1px solid color-mix(in srgb,var(--usg-blue) 14%,transparent);pointer-events:none;z-index:0}",
103
+ ".usg_hourTooltip{position:absolute;z-index:60;top:8px;min-width:174px;max-width:230px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 10px;box-shadow:var(--dsw-shadow-lv2);font-size:11px;line-height:17px;pointer-events:none}",
104
+ ".usg_hourTooltipHead{justify-content:space-between;align-items:center;gap:16px;margin-bottom:3px;font-size:12px;font-weight:600;display:flex}",
105
+ ".usg_hourTooltipRow{justify-content:space-between;gap:12px;color:var(--dsw-alias-label-secondary);display:flex}",
106
+ ".usg_hourTooltipModels{border-top:1px solid var(--dsw-alias-border-l1);margin-top:5px;padding-top:4px;flex-direction:column;gap:1px;display:flex}",
107
+ ".usg_hourTooltipModel{justify-content:space-between;gap:10px;display:flex}",
108
+ ".usg_hourTooltipModel span:first-child{max-width:145px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}",
109
+ ".usg_chartAxis{justify-content:space-between;font-size:9px;line-height:14px;color:var(--dsw-alias-label-caption);font-variant-numeric:tabular-nums;display:flex}",
110
+ ".usg_legend{align-items:center;gap:10px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);display:flex;flex-wrap:wrap}",
111
+ ".usg_legendItem{align-items:center;gap:5px;display:inline-flex}",
112
+ ".usg_legendSwatch{width:10px;height:10px;border-radius:2px;display:inline-block}",
113
+ ".usg_peakNote{color:var(--dsw-alias-label-caption);font-size:10px;line-height:14px}",
114
+ // Days list + day detail.
115
+ ".usg_days{flex-direction:column;display:flex}",
116
+ ".usg_day{width:100%;min-height:30px;align-items:center;gap:8px;border:0;background:0 0;border-bottom:1px solid var(--dsw-alias-border-l1);padding:5px 4px;font:inherit;text-align:left;cursor:pointer;display:flex;border-radius:6px}",
117
+ ".usg_day:last-child{border-bottom:0}",
118
+ ".usg_day:hover{background:var(--dsw-alias-interactive-bg-hover)}",
119
+ ".usg_day[data-active]{background:var(--dsw-alias-interactive-bg-hover)}",
120
+ ".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:none;width:96px;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
121
+ ".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
122
+ ".usg_dayCost{color:var(--dsw-alias-label-tertiary);flex:none;width:74px;font-size:11px;line-height:18px;font-variant-numeric:tabular-nums;text-align:right}",
123
+ ".usg_dayBar{background:var(--usg-blue);border-radius:2px;height:6px;flex:1;min-width:4px;opacity:.65}",
124
+ ".usg_detailSummary{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
125
+ ".usg_modelRow{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 10px;flex-direction:column;gap:4px;display:flex}",
126
+ ".usg_modelHead{align-items:center;gap:8px;display:flex}",
127
+ ".usg_modelName{color:var(--dsw-alias-label-primary);min-width:0;text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:12px;font-weight:500;line-height:18px;overflow:hidden}",
128
+ ".usg_modelTokens{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
129
+ ".usg_modelCost{color:var(--dsw-alias-label-tertiary);flex:none;font-size:11px;line-height:18px;font-variant-numeric:tabular-nums;text-align:right}",
130
+ ".usg_modelBarTrack{background:var(--dsw-alias-fill-l2);border-radius:2px;height:5px;overflow:hidden}",
131
+ ".usg_modelBar{background:var(--usg-blue);border-radius:2px;height:5px}",
132
+ ".usg_modelMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}",
133
+ // GitHub-contributions-style rolling daily token heatmap.
134
+ ".usg_heatHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}",
135
+ ".usg_contribScroll{width:100%;padding-bottom:2px;overflow-x:auto}",
136
+ ".usg_contribGrid{grid-template-rows:16px repeat(7,10px);gap:3px;min-width:max-content;display:grid}",
137
+ ".usg_contribMonth{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:14px;white-space:nowrap;align-self:end}",
138
+ ".usg_contribWeekday{color:var(--dsw-alias-label-tertiary);font-size:9px;line-height:10px;white-space:nowrap;align-self:center}",
139
+ ".usg_yearSelect{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:2px 24px 2px 8px;font:inherit;font-size:11px;line-height:20px}",
140
+ ".usg_heatCell{width:10px;height:10px;border-radius:2px;background:var(--usg-cellEmpty);border:0;padding:0;cursor:pointer;display:block}",
141
+ ".usg_heatCell:hover{box-shadow:0 0 0 1px var(--dsw-alias-label-secondary)}",
142
+ ".usg_heatCellToday{box-shadow:0 0 0 1px var(--usg-blue)}",
143
+ ".usg_heatCellSelected{box-shadow:0 0 0 2px var(--dsw-alias-label-primary)}",
144
+ ".usg_emptyCell{width:10px;height:10px;display:block}",
145
+ ".usg_heatLegend{justify-content:flex-end;align-items:center;gap:4px;margin-top:8px;font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary);display:flex}",
146
+ ".usg_heatSwatch{width:10px;height:10px;border-radius:2px;background:var(--dsw-alias-fill-l2)}",
147
+ // Limits & alert card
148
+ ".usg_limitCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:14px 16px;flex-direction:column;gap:12px;display:flex}",
149
+ ".usg_limitCardHead{justify-content:space-between;align-items:center;gap:8px;display:flex}",
150
+ ".usg_limitTitle{color:var(--dsw-alias-label-primary);align-items:center;gap:6px;font-size:15px;font-weight:600;line-height:22px;display:inline-flex}",
151
+ ".usg_limitGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}",
152
+ ".usg_limitField{flex-direction:column;gap:4px;display:flex}",
153
+ ".usg_limitFieldLabel{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px;display:flex;align-items:center;justify-content:space-between}",
154
+ ".usg_inputWrapper{position:relative;display:flex;align-items:center}",
155
+ ".usg_inputPrefix{position:absolute;left:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;pointer-events:none}",
156
+ ".usg_inputSuffix{position:absolute;right:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;pointer-events:none}",
157
+ ".usg_input{box-sizing:border-box;width:100%;height:34px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:0 12px;font:inherit;font-size:13px;line-height:1.5;box-shadow:none}",
158
+ ".usg_input.has_prefix{padding-left:28px}",
159
+ ".usg_input.has_suffix{padding-right:28px}",
160
+ ".usg_input::placeholder{color:var(--dsw-alias-label-tertiary);opacity:1}",
161
+ ".usg_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}",
162
+ ".usg_toggleGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}",
163
+ ".usg_toggleRow{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-base)}",
164
+ ".usg_toggleInfo{display:flex;flex-direction:column;gap:1px}",
165
+ ".usg_toggleTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}",
166
+ ".usg_toggleDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
167
+ ".usg_switch{position:relative;display:inline-block;width:32px;height:18px;flex:none;cursor:pointer}",
168
+ ".usg_switch input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;appearance:none;z-index:2}",
169
+ ".usg_switchSlider{position:absolute;cursor:pointer;inset:0;background-color:rgba(128,128,128,.28);border:1px solid rgba(128,128,128,.18);box-sizing:border-box;border-radius:20px;transition:.2s}",
170
+ ".usg_switchSlider:before{position:absolute;content:\"\";height:12px;width:12px;left:3px;bottom:3px;background-color:#fff;border-radius:50%;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,0.2)}",
171
+ ".usg_switch input:focus-visible + .usg_switchSlider{outline:2px solid color-mix(in srgb,var(--usg-action) 35%,transparent);outline-offset:2px}",
172
+ ".usg_switch input:checked + .usg_switchSlider{background-color:var(--usg-action);border-color:var(--usg-action)}",
173
+ ".usg_switch input:checked + .usg_switchSlider:before{transform:translateX(14px)}",
174
+ ".usg_alertCard{grid-column:1/-1;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:9px;background:var(--dsw-alias-bg-base)}",
175
+ ".usg_alertHead{display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}",
176
+ ".usg_alertValue{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;font-variant-numeric:tabular-nums}",
177
+ ".usg_alertTrack{position:relative;height:20px}",
178
+ ".usg_alertCard input.usg_alertRange{appearance:none!important;display:block;width:100%;height:20px!important;margin:1px 0!important;padding:0!important;border:0!important;outline:none;background:transparent!important;box-shadow:none!important;cursor:pointer;position:relative;z-index:1}",
179
+ ".usg_alertCard input.usg_alertRange.is-overlay{position:absolute;inset:0;z-index:2;pointer-events:none}",
180
+ ".usg_alertCard input.usg_alertRange::-webkit-slider-runnable-track{height:6px;border-radius:999px;background:linear-gradient(to right,var(--usg-danger) 0 var(--alert-percent),var(--usg-warning) var(--alert-percent) var(--critical-percent),var(--usg-success) var(--critical-percent) 100%);box-shadow:inset 0 0 0 1px rgba(0,0,0,.12)}",
181
+ ".usg_alertCard input.usg_alertRange::-webkit-slider-thumb{appearance:none;width:16px;height:16px;margin-top:-5px;border:3px solid var(--usg-control-bg);border-radius:50%;background:var(--usg-action);box-shadow:0 0 0 2px rgba(0,0,0,.18);cursor:pointer;pointer-events:auto}",
182
+ ".usg_alertCard input.usg_alertRange::-moz-range-track{height:6px;border:0;border-radius:999px;background:linear-gradient(to right,var(--usg-danger) 0 var(--alert-percent),var(--usg-warning) var(--alert-percent) var(--critical-percent),var(--usg-success) var(--critical-percent) 100%);box-shadow:inset 0 0 0 1px rgba(0,0,0,.12)}",
183
+ ".usg_alertCard input.usg_alertRange::-moz-range-thumb{width:10px;height:10px;border:3px solid var(--usg-control-bg);border-radius:50%;background:var(--usg-action);box-shadow:0 0 0 2px rgba(0,0,0,.18);cursor:pointer;pointer-events:auto}",
184
+ ".usg_alertCard input.usg_alertRange.is-overlay::-webkit-slider-runnable-track{background:transparent;box-shadow:none}",
185
+ ".usg_alertCard input.usg_alertRange.is-overlay::-moz-range-track{background:transparent;box-shadow:none}",
186
+ ".usg_alertLegend{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px}",
187
+ ".usg_alertLegend span:nth-child(2){text-align:center}",
188
+ ".usg_alertLegend span:last-child{text-align:right}",
189
+ ".usg_statusBanner{position:relative;overflow:hidden;border-radius:8px;padding:8px 10px;font-size:12px;line-height:18px;display:flex;flex-direction:column;gap:6px;border:1px solid rgba(128,128,128,.22);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}",
190
+ ".usg_statusBanner:before{content:\"\";position:absolute;inset:0 auto 0 0;width:var(--usage-progress,0%);background:color-mix(in srgb,var(--usg-blue,#1f6feb) 12%,transparent);z-index:0}",
191
+ ".usg_statusBanner>*{position:relative;z-index:1}",
192
+ ".usg_statusBanner[data-status=warning]:before{background:rgba(217,155,0,.16)}",
193
+ ".usg_statusBanner[data-status=warning]{border-color:rgba(128,128,128,.22)!important;color:var(--dsw-alias-label-primary)}",
194
+ ".usg_statusBanner[data-status=warning] .usg_bannerTitle{color:var(--dsw-alias-state-warning-primary,#b26a00)}",
195
+ ".usg_statusBanner[data-status=exceeded]:before,.usg_statusBanner[data-status=blocked]:before{background:rgba(229,72,77,.16)}",
196
+ ".usg_statusBanner[data-status=stale],.usg_statusBanner[data-status=unavailable]{background:var(--dsw-alias-fill-l2);border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}",
197
+ ".usg_usageLimitBanner{border-radius:10px;padding:10px 12px;display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:14px;line-height:20px;background:color-mix(in srgb,var(--usg-blue,#1f6feb) 8%,transparent);border:1px solid color-mix(in srgb,var(--usg-blue,#1f6feb) 22%,transparent);color:var(--dsw-alias-label-primary)}",
198
+ ".usg_usageLimitBanner strong{font-size:16px;line-height:22px;font-weight:600}",
199
+ ".usg_usageLimitBanner span{font-size:14px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
200
+ ".usg_usageLimitBanner{position:relative;overflow:hidden}",
201
+ ".usg_usageLimitBanner:before{content:\"\";position:absolute;inset:0 auto 0 0;width:var(--usage-progress,0%);background:color-mix(in srgb,var(--usg-blue,#1f6feb) 12%,transparent);z-index:0}",
202
+ ".usg_usageLimitBanner>*{position:relative;z-index:1}",
203
+ ".usg_usageLimitBanner[data-status=warning]{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 12%,transparent);border-color:rgba(128,128,128,.22)!important}",
204
+ ".usg_usageLimitBanner[data-status=warning]:before{background:rgba(217,155,0,.16)}",
205
+ ".usg_usageLimitBanner[data-status=exceeded],.usg_usageLimitBanner[data-status=blocked]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);border-color:rgba(128,128,128,.22)!important}",
206
+ ".usg_usageLimitBanner[data-status=exceeded]:before,.usg_usageLimitBanner[data-status=blocked]:before{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 14%,transparent)}",
207
+ ".usg_bannerHead{display:flex;align-items:center;justify-content:space-between;gap:8px}",
208
+ ".usg_bannerTitle{font-weight:600;display:flex;align-items:center;gap:6px}",
209
+ ".usg_bannerMsg{font-size:11px;color:var(--dsw-alias-label-secondary)}",
210
+ ".usg_progressTrack{background:var(--dsw-alias-fill-l2);border-radius:999px;height:5px;overflow:hidden;width:100%}",
211
+ ".usg_progressBar{border-radius:999px;height:5px;transition:width .3s ease;background:var(--usg-blue)}",
212
+ ".usg_progressBar[data-status=warning]{background:var(--dsw-alias-state-warning-primary)}",
213
+ ".usg_progressBar[data-status=exceeded],.usg_progressBar[data-status=blocked]{background:var(--dsw-alias-state-error-primary)}",
214
+ ".usg_limitActions{display:flex;justify-content:flex-end;align-items:center;gap:8px;margin-top:2px}",
215
+ ".usg_saveBtn{cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;font-weight:500;line-height:1.5;display:inline-flex;align-items:center;gap:6px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);transition:background .16s,opacity .15s}",
216
+ ".usg_saveBtn:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}",
217
+ ".usg_saveBtn:disabled{opacity:.4;cursor:default}",
218
+ // Query/settings split: in-panel tabs, the go-to-settings affordance.
219
+ ".usg_tabs{position:sticky;top:22px;z-index:10;border-bottom:1px solid var(--dsw-alias-border-l1);gap:2px;margin-bottom:12px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));display:flex}",
220
+ ".usg_tab{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:0;border-bottom:2px solid transparent;margin-bottom:-1px;padding:6px 10px;font:inherit;font-size:13px;line-height:20px;font-weight:500}",
221
+ ".usg_tab:hover{color:var(--dsw-alias-label-primary)}",
222
+ ".usg_tab[data-active=true]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--usg-blue)}",
223
+ ".usg_settingsLink{cursor:pointer;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:6px;padding:2px 6px;font:inherit;font-size:12px;line-height:20px;flex:none;white-space:nowrap}",
224
+ ".usg_settingsLink:hover{color:var(--usg-blue);background:var(--dsw-alias-interactive-bg-hover)}",
225
+ "@media(max-width:640px){.usg_toggleGrid,.usg_limitGrid{grid-template-columns:1fr}}"
226
+ ].join("");
227
+ const tagId = "dsh-usage-stats/UsageStats.module.css";
228
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
229
+ const tag = document.createElement("style");
230
+ tag.dataset.plugin = "dsh-usage-stats";
231
+ tag.dataset.pluginCss = tagId;
232
+ tag.textContent = css;
233
+ document.head.appendChild(tag);
234
+ }
235
+ const S = {
236
+ layer: "usg_layer",
237
+ rail: "usg_rail",
238
+ footerButtons: "usg_footerButtons",
239
+ sidebarButton: "usg_sidebarButton",
240
+ sidebarText: "usg_sidebarText",
241
+ sidebarLabel: "usg_sidebarLabel",
242
+ sidebarSummary: "usg_sidebarSummary",
243
+ statusItem: "usg_statusItem",
244
+ statusDot: "usg_statusDot",
245
+ panel: "usg_panel",
246
+ panelBody: "usg_panelBody",
247
+ section: "usg_section",
248
+ header: "usg_header",
249
+ headerLeft: "usg_headerLeft",
250
+ title: "usg_title",
251
+ headerActions: "usg_headerActions",
252
+ iconButton: "usg_iconButton",
253
+ note: "usg_note",
254
+ error: "usg_error",
255
+ retry: "usg_retry",
256
+ updated: "usg_updated",
257
+ balance: "usg_balance",
258
+ balanceHead: "usg_balanceHead",
259
+ balanceIcon: "usg_balanceIcon",
260
+ balanceIdentity: "usg_balanceIdentity",
261
+ balanceLabel: "usg_balanceLabel",
262
+ balanceMain: "usg_balanceMain",
263
+ balanceAmount: "usg_balanceAmount",
264
+ balanceCurrency: "usg_balanceCurrency",
265
+ badge: "usg_badge",
266
+ balanceRows: "usg_balanceRows",
267
+ balanceRow: "usg_balanceRow",
268
+ pickerRow: "usg_pickerRow",
269
+ pickerLabel: "usg_pickerLabel",
270
+ select: "usg_select",
271
+ statsRow: "usg_statsRow",
272
+ stat: "usg_stat",
273
+ statValue: "usg_statValue",
274
+ statMoney: "usg_statMoney",
275
+ valueWithIndicator: "usg_valueWithIndicator",
276
+ statLabel: "usg_statLabel",
277
+ card: "usg_card",
278
+ cardHead: "usg_cardHead",
279
+ cardTitle: "usg_cardTitle",
280
+ dayNav: "usg_dayNav",
281
+ navButton: "usg_navButton",
282
+ dayTitle: "usg_dayTitle",
283
+ todayButton: "usg_todayButton",
284
+ chart: "usg_chart",
285
+ chartInner: "usg_chartInner",
286
+ chartBody: "usg_chartBody",
287
+ hourSlot: "usg_hourSlot",
288
+ peak: "usg_peak",
289
+ hourBar: "usg_hourBar",
290
+ hourInput: "usg_hourInput",
291
+ hourOutput: "usg_hourOutput",
292
+ hourTooltip: "usg_hourTooltip",
293
+ hourTooltipHead: "usg_hourTooltipHead",
294
+ hourTooltipRow: "usg_hourTooltipRow",
295
+ hourTooltipModels: "usg_hourTooltipModels",
296
+ hourTooltipModel: "usg_hourTooltipModel",
297
+ chartAxis: "usg_chartAxis",
298
+ legend: "usg_legend",
299
+ legendItem: "usg_legendItem",
300
+ legendSwatch: "usg_legendSwatch",
301
+ peakNote: "usg_peakNote",
302
+ days: "usg_days",
303
+ day: "usg_day",
304
+ dayDate: "usg_dayDate",
305
+ dayTokens: "usg_dayTokens",
306
+ dayCost: "usg_dayCost",
307
+ dayBar: "usg_dayBar",
308
+ detailSummary: "usg_detailSummary",
309
+ modelRow: "usg_modelRow",
310
+ modelHead: "usg_modelHead",
311
+ modelName: "usg_modelName",
312
+ modelTokens: "usg_modelTokens",
313
+ modelCost: "usg_modelCost",
314
+ modelBarTrack: "usg_modelBarTrack",
315
+ modelBar: "usg_modelBar",
316
+ modelMeta: "usg_modelMeta",
317
+ heatHeader: "usg_heatHeader",
318
+ contribScroll: "usg_contribScroll",
319
+ contribGrid: "usg_contribGrid",
320
+ contribMonth: "usg_contribMonth",
321
+ contribWeekday: "usg_contribWeekday",
322
+ yearSelect: "usg_yearSelect",
323
+ heatCell: "usg_heatCell",
324
+ heatCellToday: "usg_heatCellToday",
325
+ heatCellSelected: "usg_heatCellSelected",
326
+ emptyCell: "usg_emptyCell",
327
+ heatLegend: "usg_heatLegend",
328
+ heatSwatch: "usg_heatSwatch",
329
+ limitCard: "usg_limitCard",
330
+ limitCardHead: "usg_limitCardHead",
331
+ limitTitle: "usg_limitTitle",
332
+ limitGrid: "usg_limitGrid",
333
+ limitField: "usg_limitField",
334
+ limitFieldLabel: "usg_limitFieldLabel",
335
+ inputWrapper: "usg_inputWrapper",
336
+ inputPrefix: "usg_inputPrefix",
337
+ inputSuffix: "usg_inputSuffix",
338
+ input: "usg_input",
339
+ toggleGrid: "usg_toggleGrid",
340
+ toggleRow: "usg_toggleRow",
341
+ toggleInfo: "usg_toggleInfo",
342
+ toggleTitle: "usg_toggleTitle",
343
+ toggleDesc: "usg_toggleDesc",
344
+ switch: "usg_switch",
345
+ switchSlider: "usg_switchSlider",
346
+ alertCard: "usg_alertCard",
347
+ alertHead: "usg_alertHead",
348
+ alertValue: "usg_alertValue",
349
+ alertTrack: "usg_alertTrack",
350
+ alertRange: "usg_alertRange",
351
+ alertLegend: "usg_alertLegend",
352
+ statusBanner: "usg_statusBanner",
353
+ usageLimitBanner: "usg_usageLimitBanner",
354
+ bannerHead: "usg_bannerHead",
355
+ bannerTitle: "usg_bannerTitle",
356
+ bannerMsg: "usg_bannerMsg",
357
+ progressTrack: "usg_progressTrack",
358
+ progressBar: "usg_progressBar",
359
+ limitActions: "usg_limitActions",
360
+ saveBtn: "usg_saveBtn",
361
+ peakRegion: "usg_peakRegion",
362
+ tabs: "usg_tabs",
363
+ tab: "usg_tab",
364
+ settingsLink: "usg_settingsLink"
365
+ };
366
+ //#endregion
367
+
368
+ //#region helpers
369
+ const BLUE_RGB = [31, 111, 235];
370
+ const GREEN_RGB = [35, 168, 120];
371
+
372
+ /** Local `YYYY-MM-DD` for a Date. */
373
+ function dayKeyOf(date) {
374
+ const month = String(date.getMonth() + 1).padStart(2, "0");
375
+ const day = String(date.getDate()).padStart(2, "0");
376
+ return `${date.getFullYear()}-${month}-${day}`;
377
+ }
378
+
379
+ /** Today's local `YYYY-MM-DD`. */
380
+ function todayKey() {
381
+ return dayKeyOf(new Date());
382
+ }
383
+
384
+ function monthName(month, translate) {
385
+ const names = translate("month.names").split(",");
386
+ return names[month] ?? String(month + 1);
387
+ }
388
+
389
+ /** Build a GitHub-style Sunday-first grid for one natural calendar year. */
390
+ function buildYearContributionHeatmap(dayMap, year) {
391
+ const start = new Date(year, 0, 1);
392
+ start.setDate(start.getDate() - start.getDay());
393
+ const end = new Date(year, 11, 31);
394
+ end.setDate(end.getDate() + (6 - end.getDay()));
395
+ const weekCount = Math.floor((end - start) / (7 * 86400000)) + 1;
396
+ const weeks = [];
397
+ const months = Array.from({ length: 12 }, (_, month) => {
398
+ const first = new Date(year, month, 1);
399
+ return { weekIndex: Math.floor((first - start) / (7 * 86400000)), month };
400
+ });
401
+ let max = 0;
402
+ for (let weekIndex = 0; weekIndex < weekCount; weekIndex += 1) {
403
+ const week = [];
404
+ for (let dayIndex = 0; dayIndex < 7; dayIndex += 1) {
405
+ const date = new Date(start);
406
+ date.setDate(start.getDate() + weekIndex * 7 + dayIndex);
407
+ if (date.getFullYear() !== year) {
408
+ week.push(null);
409
+ continue;
410
+ }
411
+ const key = dayKeyOf(date);
412
+ const entry = dayMap.get(key);
413
+ const tokens = entry?.tokens ?? 0;
414
+ week.push({ key, tokens, hitRate: entry?.cacheHitRate ?? null });
415
+ if (tokens > max) max = tokens;
416
+ }
417
+ weeks.push(week);
418
+ }
419
+ return { year, weeks, months, max };
420
+ }
421
+
422
+ /** Continuous blue intensity for a daily contribution cell. */
423
+ function cellColor(tokens, max) {
424
+ if (tokens <= 0) return { background: "var(--usg-cellEmpty)", color: "var(--dsw-alias-label-secondary)" };
425
+ const ratio = max > 0 ? Math.sqrt(tokens / max) : 1;
426
+ const alpha = Math.min(1, 0.22 + 0.78 * ratio);
427
+ return {
428
+ background: `rgba(${BLUE_RGB[0]}, ${BLUE_RGB[1]}, ${BLUE_RGB[2]}, ${alpha.toFixed(3)})`,
429
+ color: alpha >= 0.6 ? "rgba(255,255,255,.95)" : "var(--dsw-alias-label-primary)"
430
+ };
431
+ }
432
+
433
+ /** Group thousands. */
434
+ function fmt(n) {
435
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
436
+ }
437
+
438
+ /** Compact form: 1234 → "1.2k". */
439
+ function fmtCompact(n) {
440
+ if (n < 1000) return String(n);
441
+ if (n < 1000000) return `${(n / 1000).toFixed(n < 10000 ? 1 : 0)}k`;
442
+ return `${(n / 1000000).toFixed(1)}m`;
443
+ }
444
+
445
+ /** Hit-rate display: null/undefined → "—". */
446
+ function fmtHit(hitRate) {
447
+ return hitRate === null || hitRate === void 0 ? "—" : `${hitRate}%`;
448
+ }
449
+
450
+ /** Currency-aware amount: `¥36.44` / `$12.00` (Intl, fallback keeps the raw value). */
451
+ function fmtCurrency(amount, currency) {
452
+ if (amount === void 0 || amount === null || !Number.isFinite(Number(amount))) return "—";
453
+ try {
454
+ return new Intl.NumberFormat(undefined, { style: "currency", currency: currency ?? "CNY" }).format(Number(amount));
455
+ } catch {
456
+ return `${currency ?? "CNY"} ${amount}`;
457
+ }
458
+ }
459
+
460
+ /** Short money: 3.14159 → "3.14" (USD cost), 0 → "0.00". */
461
+ function fmtMoney(value) {
462
+ if (value === null || value === void 0) return "—";
463
+ const numeric = Number(value);
464
+ if (!Number.isFinite(numeric) || numeric === 0) return "0.00";
465
+ if (Math.abs(numeric) < 0.01) return "<0.01";
466
+ if (Math.abs(numeric) >= 1000) return numeric.toFixed(0);
467
+ return numeric.toFixed(2);
468
+ }
469
+
470
+ /** Currency symbol for cost displays. */
471
+ function currencySymbol(currency) {
472
+ if (currency === "CNY") return "¥";
473
+ if (currency === void 0 || currency === null || currency === "") return "¥";
474
+ if (currency === "USD") return "$";
475
+ return `${currency} `;
476
+ }
477
+
478
+ /** Bare model id of a `provider/model` key. */
479
+ function modelIdOf(modelKey) {
480
+ if (typeof modelKey !== "string") return "";
481
+ const slash = modelKey.indexOf("/");
482
+ return slash === -1 ? modelKey : modelKey.slice(slash + 1);
483
+ }
484
+
485
+ /** Prompt-side cache hit rate percent, or null. */
486
+ function hitRateOf(buckets) {
487
+ const input = buckets?.inputTokens ?? 0;
488
+ const cacheRead = buckets?.cacheReadTokens ?? 0;
489
+ const cacheWrite = buckets?.cacheWriteTokens ?? 0;
490
+ const promptTokens = input + cacheRead + cacheWrite;
491
+ if (promptTokens <= 0) return null;
492
+ return Math.round((cacheRead / promptTokens) * 1000) / 10;
493
+ }
494
+
495
+ /** Sum a list of model rows into one bucket view. */
496
+ function sumRows(rows) {
497
+ const totals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
498
+ let tokens = 0;
499
+ let cost = 0;
500
+ let priced = true;
501
+ for (const row of rows) {
502
+ totals.inputTokens += row?.inputTokens ?? 0;
503
+ totals.outputTokens += row?.outputTokens ?? 0;
504
+ totals.cacheReadTokens += row?.cacheReadTokens ?? 0;
505
+ totals.cacheWriteTokens += row?.cacheWriteTokens ?? 0;
506
+ tokens += row?.tokens ?? 0;
507
+ if (row?.cost === null) priced = false;
508
+ else if (Number.isFinite(Number(row?.cost))) cost += Number(row.cost);
509
+ }
510
+ return { ...totals, tokens, cost: priced ? cost : null };
511
+ }
512
+
513
+ /**
514
+ * Filter a wire day entry by a bare model id (`""`/null → all models).
515
+ * Recomputes totals from the matching model rows so charts and stats
516
+ * stay consistent with the filter.
517
+ */
518
+ function filterDay(day, modelId) {
519
+ if (day === null || day === void 0) return null;
520
+ if (modelId === null || modelId === "") return day;
521
+ const models = (day.models ?? []).filter((model) => modelIdOf(model.model) === modelId);
522
+ const totals = sumRows(models);
523
+ const hours = (day.hours ?? []).map((hour) => {
524
+ const rows = (hour.models ?? []).filter((model) => modelIdOf(model.model) === modelId);
525
+ const summed = sumRows(rows);
526
+ return { hour: hour.hour, ...summed };
527
+ });
528
+ return {
529
+ date: day.date,
530
+ ...totals,
531
+ cacheHitRate: hitRateOf(totals),
532
+ models,
533
+ hours
534
+ };
535
+ }
536
+
537
+ /** Distinct bare model ids across all days, ascending. */
538
+ function modelChoicesOf(days) {
539
+ const seen = new Set();
540
+ for (const day of days) {
541
+ for (const model of day?.models ?? []) {
542
+ const id = modelIdOf(model.model);
543
+ if (id !== "" && id !== "unknown") seen.add(id);
544
+ }
545
+ }
546
+ return [...seen].sort();
547
+ }
548
+
549
+ /** Today / month / all-time stats from the wire days list. */
550
+ function summarize(days, modelId, today) {
551
+ const dayKey = today ?? todayKey();
552
+ const month = dayKey.slice(0, 7);
553
+ let todayTokens = 0;
554
+ let todayCost = 0;
555
+ let todayPriced = true;
556
+ let monthTokens = 0;
557
+ let monthCost = 0;
558
+ let monthPriced = true;
559
+ let totalTokens = 0;
560
+ let totalCost = 0;
561
+ let totalPriced = true;
562
+ for (const raw of days) {
563
+ const day = filterDay(raw, modelId);
564
+ if (day === null) continue;
565
+ totalTokens += day.tokens ?? 0;
566
+ if (day.cost === null) totalPriced = false;
567
+ else totalCost += Number(day.cost) || 0;
568
+ if (day.date.startsWith(month)) {
569
+ monthTokens += day.tokens ?? 0;
570
+ if (day.cost === null) monthPriced = false;
571
+ else monthCost += Number(day.cost) || 0;
572
+ }
573
+ if (day.date === dayKey) {
574
+ todayTokens = day.tokens ?? 0;
575
+ todayPriced = day.cost !== null;
576
+ todayCost = day.cost === null ? null : Number(day.cost) || 0;
577
+ }
578
+ }
579
+ return {
580
+ todayTokens,
581
+ todayCost: todayPriced ? todayCost : null,
582
+ monthTokens,
583
+ monthCost: monthPriced ? monthCost : null,
584
+ totalTokens,
585
+ totalCost: totalPriced ? totalCost : null
586
+ };
587
+ }
588
+
589
+ /** Last 14 CALENDAR days with usage, descending (newest first). */
590
+ function recentDays(usageDays, today) {
591
+ if (!Array.isArray(usageDays)) return [];
592
+ const cutoff = new Date();
593
+ cutoff.setDate(cutoff.getDate() - 13);
594
+ const cutoffKey = dayKeyOf(cutoff);
595
+ const dayKey = today ?? todayKey();
596
+ return usageDays.filter((day) => day.date >= cutoffKey && day.date <= dayKey).reverse();
597
+ }
598
+
599
+ /** Locale-safe template interpolation: `t("key", {a})` replaces `{a}`. */
600
+ function interpolate(template, params) {
601
+ if (params === void 0) return template;
602
+ return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(params, key) ? String(params[key]) : match));
603
+ }
604
+
605
+ /**
606
+ * Per-request staleness guard: each `start()` bumps a counter and only
607
+ * the most recent start may `isCurrent()`.
608
+ */
609
+ function createLoader() {
610
+ let current = 0;
611
+ return {
612
+ start: () => ++current,
613
+ isCurrent: (id) => id === current
614
+ };
615
+ }
616
+
617
+ async function fetchJson(path) {
618
+ let response;
619
+ try {
620
+ response = await fetch(path, {
621
+ headers: { accept: "application/json" },
622
+ signal: AbortSignal.timeout(30000)
623
+ });
624
+ } catch (err) {
625
+ if (err !== null && typeof err === "object" && (err.name === "TimeoutError" || err.name === "AbortError")) {
626
+ throw new Error("request timed out");
627
+ }
628
+ throw err;
629
+ }
630
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
631
+ const payload = await response.json();
632
+ if (payload === null || typeof payload !== "object") throw new Error("unexpected response");
633
+ return payload;
634
+ }
635
+
636
+ /** Normalize the usage payload's pricing extras (server-provided). */
637
+ function pricingOf(payload) {
638
+ const pricing = payload?.pricing;
639
+ return {
640
+ currency: typeof pricing?.currency === "string" && pricing.currency !== "" ? pricing.currency : "CNY",
641
+ peakHours: Array.isArray(pricing?.peakHours) ? pricing.peakHours : [[9, 12], [14, 18]],
642
+ peakMultiplier: Number(pricing?.peakMultiplier) || 2
643
+ };
644
+ }
645
+
646
+ /** Derive the compact balance + today-spend line shown in the sidebar. */
647
+ function sidebarSummaryOf(usagePayload, balancePayload, limitsPayload, today) {
648
+ const balance = balancePayload?.ok === true ? balancePayload.account?.balance ?? null : null;
649
+ const usageReady = usagePayload?.ok === true && Array.isArray(usagePayload.days);
650
+ const dayKey = today ?? usagePayload?.today ?? todayKey();
651
+ const todayDay = usageReady ? usagePayload.days.find((day) => day.date === dayKey) : null;
652
+ const pricing = pricingOf(usagePayload);
653
+ const defaultKeyRef = limitsPayload?.defaultKeyRef;
654
+ const limitStatus = limitsPayload?.ok === true
655
+ ? (limitsPayload.status?.[defaultKeyRef] || Object.values(limitsPayload.status || {})[0])
656
+ : null;
657
+ const status = limitStatus?.status;
658
+ return {
659
+ balance: balance === null ? "—" : fmtCurrency(balance.total, balance.currency),
660
+ today: usageReady && todayDay?.cost !== null && todayDay?.cost !== void 0 ? `${currencySymbol(pricing.currency)}${fmtMoney(todayDay.cost)}` : "—",
661
+ todayTokens: usageReady ? todayDay?.tokens ?? 0 : null,
662
+ status: limitToneOf(status),
663
+ balanceStatus: limitToneOf(limitStatus?.balanceAlertStatus ?? (Number(limitStatus?.lowBalanceWarning) > 0 ? limitStatus?.status : null)),
664
+ todayStatus: limitToneOf(limitStatus?.spendStatus ?? (Number(limitStatus?.dailyCostLimit) > 0 ? limitStatus?.status : null))
665
+ };
666
+ }
667
+
668
+ function limitToneOf(status) {
669
+ if (status === "blocked" || status === "exceeded") return "bad";
670
+ if (status === "warning") return "warn";
671
+ if (status === "normal" || status === "ok") return "ok";
672
+ return "muted";
673
+ }
674
+
675
+ function limitStatusLabelKey(status) {
676
+ return ["normal", "warning", "exceeded", "blocked", "stale", "unavailable"].includes(status)
677
+ ? `limits.status.${status}`
678
+ : "limits.status.normal";
679
+ }
680
+
681
+ /** True when a local hour lands in a peak window. */
682
+ function isPeak(hour, peakHours) {
683
+ for (const [start, end] of peakHours) {
684
+ if (hour >= start && hour < end) return true;
685
+ }
686
+ return false;
687
+ }
688
+ //#endregion
689
+
690
+ //#region components
691
+ function UsageCurrencyIcon({ size = 18 }) {
692
+ return react_jsx_runtime.jsx("svg", { width: size, height: size, viewBox: "0 0 1024 1024", fill: "none", "aria-hidden": true, children: react_jsx_runtime.jsx("path", { d: "M929.984 394.688a318.976 318.976 0 0 0-182.656-91.072V195.008h-0.128l0.064-2.368c0-34.24-35.968-63.296-103.936-83.968-60.544-18.432-140.8-28.544-225.792-28.544s-165.184 10.176-225.728 28.608c-67.968 20.608-103.936 49.664-103.936 83.904 0 2.432 0.192 4.736 0.576 7.104h-0.128v78.4a317.824 317.824 0 0 0-32.064 15.36C19.072 314.368 0.32 338.112 0.448 363.968c0 2.368 0.192 4.672 0.64 7.04H0.96V491.52H0.64c0 17.024 7.552 32.832 22.464 46.976 14.912 14.144 37.12 26.24 64.064 36.224v101.056h-0.256c0 17.472 8.128 33.984 24.256 49.152 53.056 49.472 189.696 77.632 301.76 77.632 7.744 0 15.36-0.128 22.784-0.384 11.456 16.896 24.576 32.768 39.232 47.488a320 320 0 0 0 227.52 94.272c85.952 0 166.72-33.472 227.52-94.272s94.208-141.504 94.208-227.52a319.296 319.296 0 0 0-94.208-227.456zM123.776 300.48v-53.824c16.896 11.264 39.68 21.376 68.032 30.016 60.544 18.368 140.736 28.544 225.728 28.544s165.248-10.176 225.792-28.544c28.672-8.704 51.712-18.944 68.608-30.4v54.4l-9.408-0.192a319.616 319.616 0 0 0-243.392 111.232c-142.336 6.208-285.312-31.488-325.568-69.76-5.888-5.568-9.088-10.752-9.088-14.656h-0.704V302.72a8.064 8.064 0 0 1 0.64-0.192l-0.64-2.048z m78.4-157.888c57.28-17.344 133.76-26.944 215.36-26.944s158.144 9.6 215.488 26.944c58.176 17.664 78.784 38.016 78.784 50.048s-20.608 32.384-78.784 50.048c-57.344 17.408-133.888 27.008-215.488 27.008s-158.144-9.6-215.36-27.008c-58.24-17.6-78.848-38.016-78.848-50.048s20.608-32.384 78.848-50.048z m-113.856 174.208v10.496h0.704c0 14.08 6.72 27.648 20.096 40.32 48.768 46.4 185.792 80 318.016 80.128-106.24 12.8-188.992 3.136-240.064-7.36-59.968-12.352-111.168-33.28-137.024-56.064-10.432-9.152-14.144-16.512-14.144-20.48-0.064-15.36 23.296-33.152 52.416-47.04z m-40.768 195.968c-6.592-6.272-10.24-12.16-11.2-18.176V418.752c32 23.68 83.392 43.968 143.616 56.32 45.888 9.472 95.552 14.208 146.944 14.208 27.712 0 55.872-1.344 84.288-4.096-13.44 28.544-22.528 58.944-27.072 90.56A929.152 929.152 0 0 1 182.144 563.2c-65.024-11.776-112.832-29.696-134.592-50.432z m334.016 131.264c-89.152 2.24-172.992-22.4-220.032-48.768a938.688 938.688 0 0 0 219.456 16c-0.064 3.648-0.192 7.232-0.192 10.88 0 7.36 0.32 14.592 0.768 21.888z m-246.272 54.848c-8.192-7.616-12.544-14.976-12.992-22.016h0.256V612.352c26.496 19.52 67.008 37.376 113.728 49.728 32.64 8.64 79.424 17.472 133.568 17.472 5.248 0 10.56-0.128 15.936-0.256 5.44 30.656 15.296 60.096 29.248 87.744-111.616 0.576-237.888-29.184-279.744-68.16z m567.232 209.472A286.592 286.592 0 0 1 416.256 622.08a286.656 286.656 0 0 1 286.272-286.272 286.592 286.592 0 0 1 286.336 286.272 286.656 286.656 0 0 1-286.336 286.272z m93.824-466.176l-56.96 104.96c-15.168 28.48-24.512 47.424-27.904 56.768h-0.96c-10.048-22.72-38.4-76.672-84.928-161.728h-53.504l99.584 171.2H590.016v37.76h95.808v52.096H590.016v38.272h95.808v75.904h47.488v-75.904h92.288v-38.272h-92.288v-52.096h92.288v-37.76h-79.232l100.544-171.2h-50.56z", fill: "currentColor" }) });
693
+ }
694
+ /** Rolling 53-week daily token graph, matching GitHub Contributions. */
695
+ function ContributionHeatmap({ heat, translate, selectedKey, onSelect, today }) {
696
+ const weekdayLabels = [
697
+ { dayIndex: 1, label: translate("weekday.mon") },
698
+ { dayIndex: 3, label: translate("weekday.wed") },
699
+ { dayIndex: 5, label: translate("weekday.fri") }
700
+ ];
701
+ return react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
702
+ children: [
703
+ react_jsx_runtime.jsx("div", {
704
+ className: S.contribScroll,
705
+ children: react_jsx_runtime.jsxs("div", {
706
+ className: S.contribGrid,
707
+ style: { gridTemplateColumns: `28px repeat(${heat.weeks.length},10px)` },
708
+ children: [
709
+ heat.months.map((item, index) => react_jsx_runtime.jsx("span", {
710
+ className: S.contribMonth,
711
+ style: { gridColumn: item.weekIndex + 2, gridRow: 1 },
712
+ children: monthName(item.month, translate)
713
+ }, `${item.weekIndex}-${index}`)),
714
+ weekdayLabels.map((item) => react_jsx_runtime.jsx("span", {
715
+ className: S.contribWeekday,
716
+ style: { gridColumn: 1, gridRow: item.dayIndex + 2 },
717
+ children: item.label
718
+ }, item.dayIndex)),
719
+ heat.weeks.flatMap((week, weekIndex) => week.map((cell, dayIndex) => {
720
+ const position = { gridColumn: weekIndex + 2, gridRow: dayIndex + 2 };
721
+ if (cell === null) return react_jsx_runtime.jsx("span", { className: S.emptyCell, style: position, "aria-hidden": true }, `${weekIndex}-${dayIndex}`);
722
+ const colors = cellColor(cell.tokens, heat.max);
723
+ const isToday = cell.key === (today ?? todayKey());
724
+ const hit = cell.hitRate === null || cell.hitRate === void 0 ? "" : ` · ${translate("usage.hitRate")} ${fmtHit(cell.hitRate)}`;
725
+ const title = `${cell.key} · ${fmt(cell.tokens)} tokens${hit}`;
726
+ return react_jsx_runtime.jsx("button", {
727
+ type: "button",
728
+ className: `${S.heatCell}${isToday ? ` ${S.heatCellToday}` : ""}${selectedKey === cell.key ? ` ${S.heatCellSelected}` : ""}`,
729
+ style: { ...position, background: colors.background },
730
+ title,
731
+ "aria-label": title,
732
+ onClick: () => onSelect(cell.key)
733
+ }, cell.key);
734
+ }))
735
+ ]
736
+ })
737
+ }),
738
+ react_jsx_runtime.jsxs("div", {
739
+ className: S.heatLegend,
740
+ children: [
741
+ react_jsx_runtime.jsx("span", { children: translate("usage.legendLess") }),
742
+ [0.22, 0.42, 0.6, 0.8, 1].map((alpha, index) => react_jsx_runtime.jsx("span", {
743
+ className: S.heatSwatch,
744
+ style: { background: `rgba(${BLUE_RGB[0]}, ${BLUE_RGB[1]}, ${BLUE_RGB[2]}, ${alpha})` }
745
+ }, index)),
746
+ react_jsx_runtime.jsx("span", { children: translate("usage.legendMore") })
747
+ ]
748
+ })
749
+ ]
750
+ });
751
+ }
752
+
753
+ /**
754
+ * Balance card: the 消费金额 icon seat. Shows the selected API key's
755
+ * DeepSeek official balance with a key switcher.
756
+ */
757
+ function BalanceCard({ keys, selectedKey, onSelectKey, account, accountLoading, accountError, balanceTone = "muted", translate, onRefresh }) {
758
+ const balance = account?.balance ?? null;
759
+ const status = accountLoading && account === null ? "loading"
760
+ : accountError !== null ? "error"
761
+ : account === null ? "empty"
762
+ : account.status === "ok" || account.status === "unavailable" ? account.status
763
+ : account.status ?? "empty";
764
+ const tone = status === "ok" ? "ok"
765
+ : status === "loading" || status === "empty" ? "ok"
766
+ : "bad";
767
+ const statusText = status === "loading" ? translate("balance.status.loading")
768
+ : status === "error" ? translate("balance.status.error")
769
+ : status === "empty" ? translate("balance.status.empty")
770
+ : status === "not-configured" ? translate("balance.status.notConfigured")
771
+ : status === "unauthorized" ? translate("balance.status.unauthorized")
772
+ : status === "rate-limited" ? translate("balance.status.rateLimited")
773
+ : status === "unavailable" ? translate("balance.status.unavailable")
774
+ : translate("balance.status.ok");
775
+ const currencySymbol = balance?.currency === "USD" ? "$" : balance?.currency === "CNY" || balance?.currency === void 0 ? "¥" : balance?.currency;
776
+ return react_jsx_runtime.jsxs("section", {
777
+ className: S.balance,
778
+ children: [
779
+ react_jsx_runtime.jsxs("div", {
780
+ className: S.balanceHead,
781
+ children: [
782
+ react_jsx_runtime.jsx("span", {
783
+ className: S.balanceIcon,
784
+ "aria-hidden": true,
785
+ children: currencySymbol
786
+ }),
787
+ react_jsx_runtime.jsxs("span", {
788
+ className: S.balanceIdentity,
789
+ children: [
790
+ react_jsx_runtime.jsx("span", { className: S.balanceLabel, children: translate("balance.label") }),
791
+ react_jsx_runtime.jsxs("span", {
792
+ className: S.balanceMain,
793
+ children: [
794
+ balanceTone !== "muted" ? react_jsx_runtime.jsx("span", {
795
+ className: S.statusDot,
796
+ "data-tone": balanceTone,
797
+ "data-balance-indicator": true,
798
+ "aria-label": `balance ${balanceTone}`
799
+ }) : null,
800
+ react_jsx_runtime.jsx("span", {
801
+ className: S.balanceAmount,
802
+ children: status === "loading" ? "…" : balance === null ? "—" : fmtCurrency(balance.total, balance.currency)
803
+ }),
804
+ balance !== null && react_jsx_runtime.jsx("span", { className: S.balanceCurrency, children: balance.currency })
805
+ ]
806
+ })
807
+ ]
808
+ }),
809
+ react_jsx_runtime.jsx("span", { className: S.badge, "data-tone": tone, children: statusText })
810
+ ]
811
+ }),
812
+ keys.length > 1 && react_jsx_runtime.jsxs("label", {
813
+ className: S.pickerRow,
814
+ children: [
815
+ react_jsx_runtime.jsx("span", {
816
+ className: S.pickerLabel,
817
+ children: [
818
+ react_jsx_runtime.jsx(primitives.IconApiOutline14, { size: 14 }),
819
+ translate("balance.apiKey")
820
+ ]
821
+ }),
822
+ react_jsx_runtime.jsx("select", {
823
+ className: S.select,
824
+ value: selectedKey ?? "",
825
+ "aria-label": translate("balance.apiKey"),
826
+ onChange: (event) => onSelectKey(event.target.value),
827
+ children: keys.map((key) => react_jsx_runtime.jsx("option", {
828
+ value: key.id,
829
+ children: `${key.label}${key.configured ? "" : ` (${translate("balance.unconfigured")})`}`
830
+ }, key.id))
831
+ })
832
+ ]
833
+ }),
834
+ status === "error" ? react_jsx_runtime.jsxs("div", {
835
+ className: S.error,
836
+ children: [
837
+ react_jsx_runtime.jsx("span", { children: translate("balance.error", { message: accountError ?? "" }) }),
838
+ react_jsx_runtime.jsx("button", { type: "button", className: S.retry, onClick: onRefresh, children: translate("action.retry") })
839
+ ]
840
+ }) : balance !== null && react_jsx_runtime.jsx("div", {
841
+ className: S.balanceRows,
842
+ children: [
843
+ { value: balance.toppedUp, label: translate("balance.toppedUp") },
844
+ { value: balance.granted, label: translate("balance.granted") }
845
+ ].filter((row) => row.value !== void 0 && row.value !== null).map((row, index) => react_jsx_runtime.jsxs("div", {
846
+ className: S.balanceRow,
847
+ children: [
848
+ react_jsx_runtime.jsx("span", { children: row.label }),
849
+ react_jsx_runtime.jsx("b", { children: fmtCurrency(row.value, balance.currency) })
850
+ ]
851
+ }, `${row.label}-${index}`))
852
+ })
853
+ ]
854
+ });
855
+ }
856
+
857
+ /** 24-hour token/cost bars for one (filtered) day. */
858
+ function HourlyChart({ day, peakHours, money, translate }) {
859
+ const [hoveredHour, setHoveredHour] = react.useState(null);
860
+ const hours = day?.hours ?? [];
861
+ if (hours.length === 0) return react_jsx_runtime.jsx("p", { className: S.note, children: translate("chart.empty") });
862
+ const max = Math.max(...hours.map((hour) => hour.tokens ?? 0), 1);
863
+ const hovered = hours.find((hour) => hour.hour === hoveredHour) ?? null;
864
+ const tooltipPosition = hovered === null ? null : {
865
+ left: `${((hovered.hour + 0.5) / 24 * 100).toFixed(2)}%`,
866
+ transform: hovered.hour <= 4 ? "translateX(0)" : hovered.hour >= 19 ? "translateX(-100%)" : "translateX(-50%)"
867
+ };
868
+ // Peak windows are rendered as full-height background REGIONS across
869
+ // the plot area — a time-of-day hint that must never be mistaken for
870
+ // a token bar (zero-token hours render no bar element at all).
871
+ const peakRegions = Array.isArray(peakHours)
872
+ ? peakHours.filter(([start, end]) => Number.isFinite(start) && Number.isFinite(end) && end > start)
873
+ .map(([start, end]) => ({
874
+ left: `${((start / 24) * 100).toFixed(2)}%`,
875
+ width: `${(((end - start) / 24) * 100).toFixed(2)}%`
876
+ }))
877
+ : [];
878
+ return react_jsx_runtime.jsxs("div", {
879
+ className: S.chart,
880
+ children: [
881
+ react_jsx_runtime.jsxs("div", {
882
+ className: S.chartInner,
883
+ children: [
884
+ react_jsx_runtime.jsxs("div", {
885
+ className: S.chartBody,
886
+ children: [
887
+ peakRegions.map((region, index) => react_jsx_runtime.jsx("div", {
888
+ className: S.peakRegion,
889
+ "data-peak-region": true,
890
+ "aria-hidden": true,
891
+ style: { left: region.left, width: region.width }
892
+ }, `peak-${index}`)),
893
+ hours.map((hour) => {
894
+ const input = hour.inputTokens ?? 0;
895
+ const output = hour.outputTokens ?? 0;
896
+ const cacheRead = hour.cacheReadTokens ?? 0;
897
+ const cacheWrite = hour.cacheWriteTokens ?? 0;
898
+ const visualInput = input + cacheRead + cacheWrite;
899
+ const tokens = hour.tokens ?? 0;
900
+ const parts = Math.max(visualInput + output, 1);
901
+ const peak = isPeak(hour.hour, peakHours);
902
+ const barHeight = tokens > 0 ? Math.max(1, Math.round(100 * tokens / max)) : 0;
903
+ const title = `${String(hour.hour).padStart(2, "0")}:00 · ${translate("usage.input")} ${fmt(input)} · ${translate("usage.output")} ${fmt(output)} · ${translate("usage.cost")} ${money !== void 0 ? money(hour.cost) : ""}`;
904
+ return react_jsx_runtime.jsx("button", {
905
+ type: "button",
906
+ className: S.hourSlot,
907
+ "data-hour": hour.hour,
908
+ "data-peak": peak ? "true" : void 0,
909
+ "aria-label": title,
910
+ onMouseEnter: () => setHoveredHour(hour.hour),
911
+ onMouseLeave: () => setHoveredHour(null),
912
+ onFocus: () => setHoveredHour(hour.hour),
913
+ onBlur: () => setHoveredHour(null),
914
+ // Tap (touch) support: toggle the pinned tooltip.
915
+ onClick: () => setHoveredHour(hoveredHour === hour.hour ? null : hour.hour),
916
+ children: tokens > 0 && react_jsx_runtime.jsxs("div", {
917
+ className: S.hourBar,
918
+ style: { height: `${barHeight}%` },
919
+ children: [
920
+ visualInput > 0 && react_jsx_runtime.jsx("div", { className: S.hourInput, style: { flexGrow: visualInput / parts } }),
921
+ output > 0 && react_jsx_runtime.jsx("div", { className: S.hourOutput, style: { flexGrow: output / parts } }),
922
+ ]
923
+ })
924
+ }, hour.hour);
925
+ }),
926
+ hovered !== null && react_jsx_runtime.jsxs("div", {
927
+ className: S.hourTooltip,
928
+ role: "tooltip",
929
+ style: tooltipPosition,
930
+ children: [
931
+ react_jsx_runtime.jsxs("div", { className: S.hourTooltipHead, children: [
932
+ react_jsx_runtime.jsx("span", { children: `${String(hovered.hour).padStart(2, "0")}:00` }),
933
+ react_jsx_runtime.jsx("strong", { children: `${fmt(hovered.tokens ?? 0)} tokens` })
934
+ ] }),
935
+ react_jsx_runtime.jsxs("div", { className: S.hourTooltipRow, children: [react_jsx_runtime.jsx("span", { children: translate("usage.input") }), react_jsx_runtime.jsx("span", { children: fmt(hovered.inputTokens ?? 0) })] }),
936
+ react_jsx_runtime.jsxs("div", { className: S.hourTooltipRow, children: [react_jsx_runtime.jsx("span", { children: translate("usage.output") }), react_jsx_runtime.jsx("span", { children: fmt(hovered.outputTokens ?? 0) })] }),
937
+ react_jsx_runtime.jsxs("div", { className: S.hourTooltipRow, children: [react_jsx_runtime.jsx("span", { children: translate("usage.cacheRead") }), react_jsx_runtime.jsx("span", { children: fmt(hovered.cacheReadTokens ?? Math.max(0, (hovered.tokens ?? 0) - (hovered.inputTokens ?? 0) - (hovered.outputTokens ?? 0))) })] }),
938
+ react_jsx_runtime.jsxs("div", { className: S.hourTooltipRow, children: [react_jsx_runtime.jsx("span", { children: translate("usage.cost") }), react_jsx_runtime.jsx("span", { children: money !== void 0 ? money(hovered.cost) : "" })] }),
939
+ Array.isArray(hovered.models) && hovered.models.length > 0 && react_jsx_runtime.jsx("div", {
940
+ className: S.hourTooltipModels,
941
+ children: hovered.models.slice(0, 4).map((model) => react_jsx_runtime.jsxs("div", {
942
+ className: S.hourTooltipModel,
943
+ children: [
944
+ react_jsx_runtime.jsx("span", { children: modelIdOf(model.model) }),
945
+ react_jsx_runtime.jsx("span", { children: `${fmt(model.tokens ?? 0)} · ${money !== void 0 ? money(model.cost) : ""}` })
946
+ ]
947
+ }, model.model))
948
+ })
949
+ ]
950
+ })
951
+ ]
952
+ }),
953
+ react_jsx_runtime.jsx("div", {
954
+ className: S.chartAxis,
955
+ children: [0, 3, 6, 9, 12, 15, 18, 21, 23].map((hour) => react_jsx_runtime.jsx("span", { children: `${String(hour).padStart(2, "0")}:00` }, hour))
956
+ }),
957
+ react_jsx_runtime.jsxs("div", {
958
+ className: S.legend,
959
+ children: [
960
+ react_jsx_runtime.jsx("span", {
961
+ className: S.legendItem,
962
+ children: [
963
+ react_jsx_runtime.jsx("span", {
964
+ className: S.legendSwatch,
965
+ style: { background: `rgba(${BLUE_RGB[0]}, ${BLUE_RGB[1]}, ${BLUE_RGB[2]}, 1)` }
966
+ }),
967
+ translate("chart.inputWithCache")
968
+ ]
969
+ }),
970
+ react_jsx_runtime.jsx("span", {
971
+ className: S.legendItem,
972
+ children: [
973
+ react_jsx_runtime.jsx("span", {
974
+ className: S.legendSwatch,
975
+ style: { background: `rgba(${GREEN_RGB[0]}, ${GREEN_RGB[1]}, ${GREEN_RGB[2]}, 1)` }
976
+ }),
977
+ translate("usage.output")
978
+ ]
979
+ }),
980
+ Array.isArray(peakHours) && peakHours.length > 0 && react_jsx_runtime.jsx("span", { className: S.peakNote, children: translate("chart.peakNote") })
981
+ ]
982
+ })
983
+ ]
984
+ })
985
+ ]
986
+ });
987
+ }
988
+
989
+ /** Recent-days list; clicking selects the day for the hourly chart. */
990
+ function DayList({ days, selectedDay, onSelect, money, translate }) {
991
+ if (days.length === 0) return react_jsx_runtime.jsx("p", { className: S.note, children: translate("usage.noDays") });
992
+ const max = Math.max(...days.map((day) => day.tokens ?? 0), 1);
993
+ return react_jsx_runtime.jsx("div", {
994
+ className: S.days,
995
+ children: days.map((day) => {
996
+ const tokens = day.tokens ?? 0;
997
+ return react_jsx_runtime.jsxs("button", {
998
+ type: "button",
999
+ className: S.day,
1000
+ "data-active": day.date === selectedDay ? "true" : void 0,
1001
+ onClick: () => onSelect(day.date),
1002
+ children: [
1003
+ react_jsx_runtime.jsx("span", { className: S.dayDate, children: dayLabel(day.date, translate) }),
1004
+ react_jsx_runtime.jsx("span", { className: S.dayTokens, children: fmt(tokens) }),
1005
+ react_jsx_runtime.jsx("span", { className: S.dayCost, children: money !== void 0 ? money(day.cost) : "" }),
1006
+ react_jsx_runtime.jsx("div", {
1007
+ className: S.dayBar,
1008
+ style: { width: `${Math.max(4, Math.round(100 * tokens / max))}%` }
1009
+ })
1010
+ ]
1011
+ }, day.date);
1012
+ })
1013
+ });
1014
+ }
1015
+
1016
+ /** Per-model breakdown of one day (respects the model filter). */
1017
+ function DayDetail({ day, money, translate }) {
1018
+ const models = Array.isArray(day?.models) ? day.models : [];
1019
+ const tokens = day?.tokens ?? 0;
1020
+ return react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
1021
+ children: [
1022
+ react_jsx_runtime.jsx("p", {
1023
+ className: S.detailSummary,
1024
+ children: `${translate("usage.total")} ${fmt(tokens)} · ${translate("usage.input")} ${fmt(day?.inputTokens ?? 0)} · ${translate("usage.output")} ${fmt(day?.outputTokens ?? 0)} · ${translate("usage.cacheRead")} ${fmt(day?.cacheReadTokens ?? 0)}`
1025
+ }),
1026
+ models.length === 0 ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("usage.noModels") }) : models.map((model) => {
1027
+ const share = tokens > 0 ? Math.max(3, Math.round(100 * (model.tokens ?? 0) / tokens)) : 0;
1028
+ return react_jsx_runtime.jsxs("div", {
1029
+ className: S.modelRow,
1030
+ children: [
1031
+ react_jsx_runtime.jsxs("div", {
1032
+ className: S.modelHead,
1033
+ children: [
1034
+ react_jsx_runtime.jsx("span", { className: S.modelName, title: model.model, children: modelLabelOf(model.model, translate) }),
1035
+ react_jsx_runtime.jsx("span", { className: S.modelTokens, children: fmt(model.tokens ?? 0) }),
1036
+ react_jsx_runtime.jsx("span", { className: S.modelCost, children: money !== void 0 ? money(model.cost) : "" })
1037
+ ]
1038
+ }),
1039
+ react_jsx_runtime.jsx("div", {
1040
+ className: S.modelBarTrack,
1041
+ children: react_jsx_runtime.jsx("div", { className: S.modelBar, style: { width: `${share}%` } })
1042
+ }),
1043
+ react_jsx_runtime.jsx("div", {
1044
+ className: S.modelMeta,
1045
+ children: `${translate("usage.input")} ${fmt(model.inputTokens ?? 0)} · ${translate("usage.output")} ${fmt(model.outputTokens ?? 0)} · ${translate("usage.cacheRead")} ${fmt(model.cacheReadTokens ?? 0)} · ${translate("usage.hitRate")} ${fmtHit(model.cacheHitRate)}`
1046
+ })
1047
+ ]
1048
+ }, model.model);
1049
+ })
1050
+ ]
1051
+ });
1052
+ }
1053
+
1054
+ /** `YYYY-MM-DD` → `MM-DD 周X` display label. */
1055
+ function dayLabel(key, translate) {
1056
+ const [, month, day] = key.split("-");
1057
+ const date = new Date(Number(key.slice(0, 4)), Number(month) - 1, Number(day));
1058
+ const weekdays = [translate("weekday.sun"), translate("weekday.mon"), translate("weekday.tue"), translate("weekday.wed"), translate("weekday.thu"), translate("weekday.fri"), translate("weekday.sat")];
1059
+ return `${month}-${day} ${weekdays[date.getDay()]}`;
1060
+ }
1061
+
1062
+ /** Display label for a `provider/model` attribution key. */
1063
+ function modelLabelOf(key, translate) {
1064
+ if (typeof key !== "string") return "";
1065
+ const slash = key.indexOf("/");
1066
+ if (slash === -1) return key;
1067
+ const provider = key.slice(0, slash);
1068
+ const model = key.slice(slash + 1);
1069
+ const providerLabel = provider === "unknown" ? translate("usage.unknownModel") : provider;
1070
+ const modelLabel = model === "unknown" || model === "" ? translate("usage.unknownModel") : model;
1071
+ return `${providerLabel} · ${modelLabel}`;
1072
+ }
1073
+
1074
+ function safeGetStorage(key) {
1075
+ try {
1076
+ if (typeof window !== "undefined" && window.localStorage && typeof window.localStorage.getItem === "function") {
1077
+ return window.localStorage.getItem(key);
1078
+ }
1079
+ } catch {}
1080
+ return null;
1081
+ }
1082
+
1083
+ function safeSetStorage(key, value) {
1084
+ try {
1085
+ if (typeof window !== "undefined" && window.localStorage && typeof window.localStorage.setItem === "function") {
1086
+ window.localStorage.setItem(key, value);
1087
+ }
1088
+ } catch {}
1089
+ }
1090
+
1091
+ /**
1092
+ * Limits and alert configuration card.
1093
+ */
1094
+ function LimitsCard({ keys, selectedKey, onSelectKey, pricing, todayCost, translate, onLimitsUpdated }) {
1095
+ const [limits, setLimits] = react.useState(null);
1096
+ const limitsRef = react.useRef(null);
1097
+ const [statusMap, setStatusMap] = react.useState({});
1098
+ const [activeKey, setActiveKey] = react.useState(() => {
1099
+ const stored = safeGetStorage("dsh_usage_limits_active_key");
1100
+ if (stored !== null && stored !== "") return stored;
1101
+ return selectedKey ?? "__global__";
1102
+ });
1103
+ const [saving, setSaving] = react.useState(false);
1104
+ const [error, setError] = react.useState(null);
1105
+ const pendingRef = react.useRef(null);
1106
+ const debounceTimerRef = react.useRef(null);
1107
+ const savingRef = react.useRef(false);
1108
+
1109
+ // Form state
1110
+ const [enabled, setEnabled] = react.useState(false);
1111
+ const [dailyCostLimit, setDailyCostLimit] = react.useState("");
1112
+ const [lowBalanceWarning, setLowBalanceWarning] = react.useState("");
1113
+ const [alertPercent, setAlertPercent] = react.useState(80);
1114
+ const [criticalPercent, setCriticalPercent] = react.useState(90);
1115
+ const [stopOnExceed, setStopOnExceed] = react.useState(false);
1116
+
1117
+ const loadLimits = react.useCallback(async () => {
1118
+ try {
1119
+ setError(null);
1120
+ const payload = await fetchJson("/api/usage-stats/limits");
1121
+ if (payload.ok === true) {
1122
+ limitsRef.current = payload.limits;
1123
+ setLimits(payload.limits);
1124
+ setStatusMap(payload.status || {});
1125
+ }
1126
+ } catch (err) {
1127
+ setError(err instanceof Error ? err.message : String(err));
1128
+ }
1129
+ }, []);
1130
+
1131
+ react.useEffect(() => {
1132
+ loadLimits();
1133
+ }, [loadLimits]);
1134
+
1135
+ // Sync activeKey when selectedKey becomes available (if not explicitly chosen in storage)
1136
+ react.useEffect(() => {
1137
+ if (selectedKey !== null && selectedKey !== undefined && selectedKey !== "") {
1138
+ const stored = safeGetStorage("dsh_usage_limits_active_key");
1139
+ if (stored) {
1140
+ setActiveKey(stored);
1141
+ return;
1142
+ }
1143
+ setActiveKey(selectedKey);
1144
+ }
1145
+ }, [selectedKey]);
1146
+
1147
+ // Update form fields when activeKey or limits payload change
1148
+ react.useEffect(() => {
1149
+ const currentLimits = limitsRef.current || limits;
1150
+ if (!currentLimits) return;
1151
+ const rule = activeKey === "__global__"
1152
+ ? (currentLimits.global || {})
1153
+ : (currentLimits.keys?.[activeKey] || currentLimits.global || {});
1154
+ setEnabled(rule.enabled === true);
1155
+ setDailyCostLimit(rule.dailyCostLimit !== null && rule.dailyCostLimit !== undefined ? String(rule.dailyCostLimit) : "");
1156
+ setLowBalanceWarning(rule.lowBalanceWarning !== null && rule.lowBalanceWarning !== undefined ? String(rule.lowBalanceWarning) : "");
1157
+ setAlertPercent(rule.alertPercent || 80);
1158
+ setCriticalPercent(Math.max(rule.alertPercent || 80, rule.criticalPercent || 90));
1159
+ setStopOnExceed(rule.stopOnExceed === true);
1160
+ }, [activeKey, limits]);
1161
+
1162
+ const handleKeySelect = (newKey) => {
1163
+ // Do not strand an unsaved batch on the previous key's form.
1164
+ if (pendingRef.current !== null && !savingRef.current) flushSave();
1165
+ setActiveKey(newKey);
1166
+ safeSetStorage("dsh_usage_limits_active_key", newKey);
1167
+ if (newKey !== "__global__" && typeof onSelectKey === "function") {
1168
+ onSelectKey(newKey);
1169
+ }
1170
+ };
1171
+
1172
+ /** Snapshot the full rule for the active key from current form state plus this change. */
1173
+ const buildRuleFrom = (overrideRule) => {
1174
+ const currentLimits = limitsRef.current || limits || { version: 2, global: {}, keys: {} };
1175
+ const currentActiveKey = activeKey;
1176
+ const currentRule = currentActiveKey === "__global__"
1177
+ ? (currentLimits.global || {})
1178
+ : (currentLimits.keys?.[currentActiveKey] || {});
1179
+ return {
1180
+ ...currentRule,
1181
+ enabled: overrideRule?.enabled !== undefined ? overrideRule.enabled : enabled,
1182
+ dailyCostLimit: overrideRule?.dailyCostLimit !== undefined
1183
+ ? overrideRule.dailyCostLimit
1184
+ : (dailyCostLimit.trim() !== "" && !isNaN(Number(dailyCostLimit)) ? Number(dailyCostLimit) : null),
1185
+ lowBalanceWarning: overrideRule?.lowBalanceWarning !== undefined
1186
+ ? overrideRule.lowBalanceWarning
1187
+ : (lowBalanceWarning.trim() !== "" && !isNaN(Number(lowBalanceWarning)) ? Number(lowBalanceWarning) : null),
1188
+ alertPercent: overrideRule?.alertPercent !== undefined
1189
+ ? Number(overrideRule.alertPercent)
1190
+ : (Number(alertPercent) || 20),
1191
+ criticalPercent: overrideRule?.criticalPercent !== undefined
1192
+ ? Number(overrideRule.criticalPercent)
1193
+ : Math.max(Number(alertPercent) || 20, Number(criticalPercent) || 60),
1194
+ stopOnExceed: overrideRule?.stopOnExceed !== undefined ? overrideRule.stopOnExceed : stopOnExceed
1195
+ };
1196
+ };
1197
+
1198
+ const handleSave = async (overrideRule, immediate = false) => {
1199
+ // Debounce + latest-value compensation: every change replaces the
1200
+ // pending batch with a full rule snapshot, so the flush always sends
1201
+ // the newest values even while a previous request is still in flight.
1202
+ pendingRef.current = { activeKey, rule: buildRuleFrom(overrideRule) };
1203
+ if (debounceTimerRef.current !== null) {
1204
+ window.clearTimeout(debounceTimerRef.current);
1205
+ debounceTimerRef.current = null;
1206
+ }
1207
+ if (immediate) {
1208
+ await flushSave();
1209
+ } else {
1210
+ debounceTimerRef.current = window.setTimeout(() => {
1211
+ debounceTimerRef.current = null;
1212
+ flushSave();
1213
+ }, 250);
1214
+ }
1215
+ };
1216
+
1217
+ const flushSave = async () => {
1218
+ if (savingRef.current) return;
1219
+ const batch = pendingRef.current;
1220
+ if (batch === null) return;
1221
+ pendingRef.current = null;
1222
+ savingRef.current = true;
1223
+ setSaving(true);
1224
+ setError(null);
1225
+ try {
1226
+ const currentLimits = limitsRef.current || limits || { version: 2, global: {}, keys: {} };
1227
+ const currentActiveKey = batch.activeKey;
1228
+ const newRule = batch.rule;
1229
+ let updatedPayload;
1230
+ if (currentActiveKey === "__global__") {
1231
+ updatedPayload = {
1232
+ ...currentLimits,
1233
+ global: newRule
1234
+ };
1235
+ } else {
1236
+ updatedPayload = {
1237
+ ...currentLimits,
1238
+ keys: {
1239
+ ...(currentLimits.keys || {}),
1240
+ [currentActiveKey]: newRule
1241
+ }
1242
+ };
1243
+ }
1244
+
1245
+ const response = await fetch("/api/usage-stats/limits", {
1246
+ method: "POST",
1247
+ headers: { "content-type": "application/json; charset=utf-8", "accept": "application/json" },
1248
+ body: JSON.stringify(updatedPayload)
1249
+ });
1250
+ const payload = await response.json();
1251
+ if (!response.ok || !payload.ok) throw new Error(payload?.message || `HTTP ${response.status}`);
1252
+ // Keep the latest server configuration for future key switches and
1253
+ // saves, but do not resync the active form from the response. A
1254
+ // response arriving after the user edits a field must never clear it.
1255
+ limitsRef.current = payload.limits;
1256
+ setStatusMap(payload.status || {});
1257
+ if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("usage-stats:limits-updated"));
1258
+ if (onLimitsUpdated) onLimitsUpdated(payload);
1259
+ } catch (err) {
1260
+ setError(translate("limits.saveError", { message: err instanceof Error ? err.message : String(err) }));
1261
+ } finally {
1262
+ savingRef.current = false;
1263
+ setSaving(false);
1264
+ // Edits landed while the request was in flight: re-arm the debounce
1265
+ // so the merged pending batch goes out as the next save.
1266
+ if (pendingRef.current !== null && debounceTimerRef.current === null) {
1267
+ debounceTimerRef.current = window.setTimeout(() => {
1268
+ debounceTimerRef.current = null;
1269
+ flushSave();
1270
+ }, 250);
1271
+ }
1272
+ }
1273
+ };
1274
+
1275
+ const currentStatus = activeKey === "__global__"
1276
+ ? (statusMap[selectedKey] || Object.values(statusMap)[0] || { status: "normal", todayCost, dailyCostLimit: null })
1277
+ : (statusMap[activeKey] || { status: "normal", todayCost, dailyCostLimit: null });
1278
+
1279
+ const isLimitActive = enabled && (Number(dailyCostLimit) > 0 || Number(lowBalanceWarning) > 0);
1280
+ // Prefer the per-key status cost from the server (per-key attribution
1281
+ // via keyProviders); fall back to the section-level today cost.
1282
+ const spentNum = Number(currentStatus?.todayCost ?? todayCost) || 0;
1283
+ const limitNum = Number(dailyCostLimit) || 0;
1284
+ const percent = limitNum > 0 ? Math.min(100, Math.round((spentNum / limitNum) * 100)) : 0;
1285
+ const currSymbol = currencySymbol(pricing?.currency);
1286
+
1287
+ return react_jsx_runtime.jsxs("section", {
1288
+ className: S.limitCard,
1289
+ children: [
1290
+ react_jsx_runtime.jsxs("div", {
1291
+ className: S.limitCardHead,
1292
+ children: [
1293
+ react_jsx_runtime.jsxs("span", {
1294
+ className: S.limitTitle,
1295
+ children: [
1296
+ translate("limits.title")
1297
+ ]
1298
+ }),
1299
+ react_jsx_runtime.jsx("span", {
1300
+ className: S.badge,
1301
+ "data-tone": !enabled ? "empty" : limitToneOf(currentStatus?.status),
1302
+ children: !enabled ? translate("limits.status.unlimited")
1303
+ : translate(limitStatusLabelKey(currentStatus?.status))
1304
+ })
1305
+ ]
1306
+ }),
1307
+ react_jsx_runtime.jsx("p", { className: S.note, children: translate("limits.desc") }),
1308
+ error && react_jsx_runtime.jsx("div", { className: S.error, children: error }),
1309
+ // Key selection
1310
+ react_jsx_runtime.jsxs("label", {
1311
+ className: S.pickerRow,
1312
+ children: [
1313
+ react_jsx_runtime.jsxs("span", {
1314
+ className: S.pickerLabel,
1315
+ children: [
1316
+ translate("limits.apiKey")
1317
+ ]
1318
+ }),
1319
+ react_jsx_runtime.jsxs("select", {
1320
+ className: S.select,
1321
+ value: activeKey,
1322
+ "aria-label": translate("limits.apiKey"),
1323
+ onChange: (e) => handleKeySelect(e.target.value),
1324
+ children: [
1325
+ react_jsx_runtime.jsx("option", { value: "__global__", children: translate("limits.global") }, "__global__"),
1326
+ keys.map((k) => react_jsx_runtime.jsx("option", { value: k.id, children: k.label }, k.id))
1327
+ ]
1328
+ })
1329
+ ]
1330
+ }),
1331
+ // Status Banner
1332
+ enabled && isLimitActive ? react_jsx_runtime.jsxs("div", {
1333
+ className: S.statusBanner,
1334
+ style: { "--usage-progress": `${percent}%` },
1335
+ "data-status": currentStatus?.status || "normal",
1336
+ children: [
1337
+ react_jsx_runtime.jsxs("div", {
1338
+ className: S.bannerHead,
1339
+ children: [
1340
+ react_jsx_runtime.jsx("span", {
1341
+ className: S.bannerTitle,
1342
+ children: translate(limitStatusLabelKey(currentStatus?.status))
1343
+ }),
1344
+ limitNum > 0 ? react_jsx_runtime.jsx("span", {
1345
+ className: S.bannerMsg,
1346
+ children: translate("limits.progress", {
1347
+ spent: `${currSymbol}${spentNum.toFixed(2)}`,
1348
+ limit: `${currSymbol}${limitNum.toFixed(2)}`,
1349
+ percent: String(percent)
1350
+ })
1351
+ }) : null,
1352
+ ]
1353
+ }),
1354
+ currentStatus?.message ? react_jsx_runtime.jsx("span", { className: S.bannerMsg, children: currentStatus.message }) : null
1355
+ ]
1356
+ }) : null,
1357
+ // Primary controls share one compact row on wide settings pages.
1358
+ react_jsx_runtime.jsxs("div", {
1359
+ className: S.toggleGrid,
1360
+ children: [
1361
+ react_jsx_runtime.jsxs("div", {
1362
+ className: S.toggleRow,
1363
+ children: [
1364
+ react_jsx_runtime.jsxs("div", { className: S.toggleInfo, children: [
1365
+ react_jsx_runtime.jsx("span", { className: S.toggleTitle, children: translate("limits.enable") }),
1366
+ react_jsx_runtime.jsx("span", { className: S.toggleDesc, children: translate("limits.enableDesc") })
1367
+ ] }),
1368
+ react_jsx_runtime.jsxs("label", { className: S.switch, children: [
1369
+ react_jsx_runtime.jsx("input", {
1370
+ type: "checkbox",
1371
+ checked: enabled,
1372
+ onChange: (e) => {
1373
+ const nextVal = e.target.checked;
1374
+ setEnabled(nextVal);
1375
+ handleSave({ enabled: nextVal }, true);
1376
+ }
1377
+ }),
1378
+ react_jsx_runtime.jsx("span", { className: S.switchSlider })
1379
+ ] })
1380
+ ]
1381
+ }),
1382
+ enabled ? react_jsx_runtime.jsxs("div", {
1383
+ className: S.toggleRow,
1384
+ children: [
1385
+ react_jsx_runtime.jsxs("div", { className: S.toggleInfo, children: [
1386
+ react_jsx_runtime.jsx("span", { className: S.toggleTitle, children: translate("limits.stopOnExceed") }),
1387
+ react_jsx_runtime.jsx("span", { className: S.toggleDesc, children: translate("limits.stopDesc") })
1388
+ ] }),
1389
+ react_jsx_runtime.jsxs("label", { className: S.switch, children: [
1390
+ react_jsx_runtime.jsx("input", {
1391
+ type: "checkbox",
1392
+ checked: stopOnExceed,
1393
+ onChange: (e) => {
1394
+ const nextVal = e.target.checked;
1395
+ setStopOnExceed(nextVal);
1396
+ handleSave({ stopOnExceed: nextVal }, true);
1397
+ }
1398
+ }),
1399
+ react_jsx_runtime.jsx("span", { className: S.switchSlider })
1400
+ ] })
1401
+ ]
1402
+ }) : null
1403
+ ]
1404
+ }),
1405
+ // Input fields
1406
+ enabled ? react_jsx_runtime.jsxs("div", {
1407
+ className: S.limitGrid,
1408
+ children: [
1409
+ react_jsx_runtime.jsxs("div", { className: S.limitField, children: [
1410
+ react_jsx_runtime.jsx("label", { className: S.limitFieldLabel, children: translate("limits.dailyLimit") }),
1411
+ react_jsx_runtime.jsxs("div", { className: S.inputWrapper, children: [
1412
+ react_jsx_runtime.jsx("span", { className: S.inputPrefix, children: currSymbol }),
1413
+ react_jsx_runtime.jsx("input", { type: "number", inputMode: "decimal", step: "0.01", min: "0", className: `${S.input} has_prefix`, placeholder: translate("limits.dailyLimitPlaceholder"), value: dailyCostLimit, onChange: (e) => { const value = e.target.value; setDailyCostLimit(value); handleSave({ dailyCostLimit: value.trim() !== "" && !isNaN(Number(value)) ? Number(value) : null }); } })
1414
+ ] })
1415
+ ] }),
1416
+ react_jsx_runtime.jsxs("div", { className: S.limitField, children: [
1417
+ react_jsx_runtime.jsx("label", { className: S.limitFieldLabel, children: translate("limits.lowBalance") }),
1418
+ react_jsx_runtime.jsxs("div", { className: S.inputWrapper, children: [
1419
+ react_jsx_runtime.jsx("span", { className: S.inputPrefix, children: currSymbol }),
1420
+ react_jsx_runtime.jsx("input", { type: "number", inputMode: "decimal", step: "0.01", min: "0", className: `${S.input} has_prefix`, placeholder: translate("limits.lowBalancePlaceholder"), value: lowBalanceWarning, onChange: (e) => { const value = e.target.value; setLowBalanceWarning(value); handleSave({ lowBalanceWarning: value.trim() !== "" && !isNaN(Number(value)) ? Number(value) : null }); } })
1421
+ ] })
1422
+ ] }),
1423
+ Number(dailyCostLimit) > 0 ? react_jsx_runtime.jsxs("div", {
1424
+ className: S.alertCard,
1425
+ style: { "--alert-percent": `${alertPercent}%`, "--critical-percent": `${criticalPercent}%` },
1426
+ children: [
1427
+ react_jsx_runtime.jsxs("div", { className: S.alertHead, children: [
1428
+ react_jsx_runtime.jsx("span", { children: translate("limits.alertPercent") }),
1429
+ null
1430
+ ] }),
1431
+ react_jsx_runtime.jsxs("div", { className: S.alertTrack, children: [
1432
+ react_jsx_runtime.jsx("input", {
1433
+ type: "range",
1434
+ min: "1",
1435
+ max: "99",
1436
+ step: "1",
1437
+ className: S.alertRange,
1438
+ value: alertPercent,
1439
+ "aria-label": translate("limits.alertPercent"),
1440
+ onChange: (e) => { const value = Math.min(Number(e.target.value), criticalPercent - 1); setAlertPercent(value); handleSave({ alertPercent: value }); }
1441
+ }),
1442
+ react_jsx_runtime.jsx("input", { type: "range", min: "2", max: "100", step: "1", className: `${S.alertRange} is-overlay`, value: criticalPercent, "aria-label": translate("limits.criticalPercent"), onChange: (e) => { const value = Math.max(Number(e.target.value), alertPercent + 1); setCriticalPercent(value); handleSave({ criticalPercent: value }); } })
1443
+ ] }),
1444
+ react_jsx_runtime.jsxs("div", { className: S.alertLegend, children: [
1445
+ react_jsx_runtime.jsx("span", { children: translate("limits.alertExceeded", { percent: alertPercent }) }),
1446
+ react_jsx_runtime.jsx("span", { children: translate("limits.alertWarning", { percent: alertPercent, critical: criticalPercent }) }),
1447
+ react_jsx_runtime.jsx("span", { children: translate("limits.alertNormal", { percent: criticalPercent }) })
1448
+ ] })
1449
+ ]
1450
+ }) : null
1451
+ ]
1452
+ }) : null,
1453
+ // All controls save immediately after changes.
1454
+ ]
1455
+ });
1456
+ }
1457
+
1458
+ /**
1459
+ * The settings-section root: balance + key switcher, model-filterable
1460
+ * stats, limits & quota configuration, per-hour chart for a selected day,
1461
+ * recent-days list and per-model detail.
1462
+ * @param props - `t` bound by the slot runtime.
1463
+ */
1464
+ function UsageStatsSection({ t, onClose, onOpenSettings }) {
1465
+ const [tab, setTab] = react.useState("overview");
1466
+ const translate = (key, params) => interpolate(t !== void 0 ? t(key) : key, params);
1467
+ const [usage, setUsage] = react.useState(null);
1468
+ const [usageError, setUsageError] = react.useState(null);
1469
+ const [refreshedAt, setRefreshedAt] = react.useState(null);
1470
+ const [pricing, setPricing] = react.useState({ currency: "CNY", peakHours: [[9, 12], [14, 18]], peakMultiplier: 2 });
1471
+ const [keys, setKeys] = react.useState([]);
1472
+ const [selectedKey, setSelectedKey] = react.useState(null);
1473
+ const [account, setAccount] = react.useState(null);
1474
+ const [accountLoading, setAccountLoading] = react.useState(false);
1475
+ const [accountError, setAccountError] = react.useState(null);
1476
+ const [limitStatusMap, setLimitStatusMap] = react.useState({});
1477
+ const [serverToday, setServerToday] = react.useState(null);
1478
+ const [modelFilter, setModelFilter] = react.useState("");
1479
+ const [selectedDay, setSelectedDay] = react.useState(null);
1480
+ const [selectedYear, setSelectedYear] = react.useState(() => new Date().getFullYear());
1481
+ const mountedRef = react.useRef(true);
1482
+ const usageLoaderRef = react.useRef(null);
1483
+ const accountLoaderRef = react.useRef(null);
1484
+ if (usageLoaderRef.current === null) usageLoaderRef.current = createLoader();
1485
+ if (accountLoaderRef.current === null) accountLoaderRef.current = createLoader();
1486
+
1487
+ const loadUsage = react.useCallback(() => {
1488
+ const seq = usageLoaderRef.current.start();
1489
+ setUsageError(null);
1490
+ fetchJson("/api/usage-stats/usage").then((payload) => {
1491
+ if (!mountedRef.current || !usageLoaderRef.current.isCurrent(seq)) return;
1492
+ if (payload.ok !== true) {
1493
+ setUsageError(payload.message ?? "usage aggregation failed");
1494
+ return;
1495
+ }
1496
+ setUsage(payload);
1497
+ setPricing(pricingOf(payload));
1498
+ setServerToday(payload.today ?? null);
1499
+ setRefreshedAt(Date.now());
1500
+ }).catch((error) => {
1501
+ if (!mountedRef.current || !usageLoaderRef.current.isCurrent(seq)) return;
1502
+ setUsageError(error instanceof Error ? error.message : String(error));
1503
+ });
1504
+ }, []);
1505
+
1506
+ const loadLimitStatuses = react.useCallback(() => {
1507
+ fetchJson("/api/usage-stats/limits").then((payload) => {
1508
+ if (!mountedRef.current || payload.ok !== true) return;
1509
+ setLimitStatusMap(payload.status || {});
1510
+ }).catch(() => {});
1511
+ }, []);
1512
+
1513
+ const loadKeys = react.useCallback(() => {
1514
+ fetchJson("/api/usage-stats/keys").then((payload) => {
1515
+ if (!mountedRef.current) return;
1516
+ if (payload.ok !== true) return;
1517
+ const list = Array.isArray(payload.keys) ? payload.keys : [];
1518
+ setKeys(list);
1519
+ setSelectedKey((current) => {
1520
+ if (current !== null && list.some((key) => key.id === current)) return current;
1521
+ return list.find((key) => key.default)?.id ?? list.find((key) => key.configured)?.id ?? list[0]?.id ?? null;
1522
+ });
1523
+ }).catch(() => {});
1524
+ }, []);
1525
+
1526
+ const loadAccount = react.useCallback((keyId, force = false) => {
1527
+ const seq = accountLoaderRef.current.start();
1528
+ setAccountLoading(true);
1529
+ setAccountError(null);
1530
+ if (keyId === null) {
1531
+ setAccountLoading(false);
1532
+ return;
1533
+ }
1534
+ const query = `?key=${encodeURIComponent(keyId)}${force ? "&refresh=1" : ""}`;
1535
+ fetchJson(`/api/usage-stats/balance${query}`).then((payload) => {
1536
+ if (!mountedRef.current || !accountLoaderRef.current.isCurrent(seq)) return;
1537
+ if (payload.ok !== true) {
1538
+ setAccountError(payload.message ?? "balance fetch failed");
1539
+ return;
1540
+ }
1541
+ setAccount(payload.account);
1542
+ }).catch((error) => {
1543
+ if (!mountedRef.current || !accountLoaderRef.current.isCurrent(seq)) return;
1544
+ setAccountError(error instanceof Error ? error.message : String(error));
1545
+ }).finally(() => {
1546
+ if (mountedRef.current && accountLoaderRef.current.isCurrent(seq)) setAccountLoading(false);
1547
+ });
1548
+ }, []);
1549
+
1550
+ react.useEffect(() => {
1551
+ mountedRef.current = true;
1552
+ return () => {
1553
+ mountedRef.current = false;
1554
+ };
1555
+ }, []);
1556
+
1557
+ // Initial load + periodic refresh while the settings section is open.
1558
+ react.useEffect(() => {
1559
+ loadUsage();
1560
+ loadKeys();
1561
+ loadLimitStatuses();
1562
+ const usageTimer = window.setInterval(loadUsage, 60000);
1563
+ const limitsTimer = window.setInterval(loadLimitStatuses, 60000);
1564
+ const keysTimer = window.setInterval(loadKeys, 300000);
1565
+ return () => {
1566
+ window.clearInterval(usageTimer);
1567
+ window.clearInterval(limitsTimer);
1568
+ window.clearInterval(keysTimer);
1569
+ };
1570
+ }, [loadUsage, loadKeys, loadLimitStatuses]);
1571
+
1572
+ // Balance follows the selected key; re-read the server cache every five minutes
1573
+ react.useEffect(() => {
1574
+ if (selectedKey === null) return;
1575
+ loadAccount(selectedKey);
1576
+ const cacheTimer = window.setInterval(() => loadAccount(selectedKey), 300000);
1577
+ return () => {
1578
+ window.clearInterval(cacheTimer);
1579
+ };
1580
+ }, [selectedKey, loadAccount]);
1581
+
1582
+ const dayMap = react.useMemo(() => {
1583
+ const map = new Map();
1584
+ if (usage !== null && Array.isArray(usage.days)) {
1585
+ for (const day of usage.days) map.set(day.date, day);
1586
+ }
1587
+ return map;
1588
+ }, [usage]);
1589
+
1590
+ const yearOptions = react.useMemo(() => {
1591
+ const current = new Date().getFullYear();
1592
+ const years = new Set(Array.from({ length: 6 }, (_, index) => current - index));
1593
+ for (const key of dayMap.keys()) {
1594
+ const year = Number(key.slice(0, 4));
1595
+ if (Number.isInteger(year) && year <= current) years.add(year);
1596
+ }
1597
+ return [...years].sort((left, right) => right - left);
1598
+ }, [dayMap]);
1599
+ const heat = react.useMemo(() => buildYearContributionHeatmap(dayMap, selectedYear), [dayMap, selectedYear]);
1600
+
1601
+ react.useEffect(() => {
1602
+ if (selectedDay !== null && !dayMap.has(selectedDay)) setSelectedDay(null);
1603
+ }, [dayMap, selectedDay]);
1604
+
1605
+ const days = react.useMemo(() => (usage !== null && Array.isArray(usage.days) ? usage.days : []), [usage]);
1606
+ const models = react.useMemo(() => modelChoicesOf(days), [days]);
1607
+ const recent = react.useMemo(() => recentDays(days, serverToday).map((day) => filterDay(day, modelFilter)), [days, serverToday, modelFilter]);
1608
+ const activeDayKey = selectedDay ?? serverToday ?? todayKey();
1609
+ const activeDay = react.useMemo(() => filterDay(dayMap.get(activeDayKey) ?? null, modelFilter), [dayMap, activeDayKey, modelFilter]);
1610
+ const stats = react.useMemo(() => summarize(days, modelFilter, serverToday), [days, modelFilter, serverToday]);
1611
+ const activeLimitStatus = selectedKey === null
1612
+ ? (Object.values(limitStatusMap)[0] || null)
1613
+ : (limitStatusMap[selectedKey] || null);
1614
+ const todaySpendTone = limitToneOf(activeLimitStatus?.spendStatus);
1615
+ const todayLimitNum = Number(activeLimitStatus?.dailyCostLimit) || 0;
1616
+ const todaySpentNum = Number(activeLimitStatus?.todayCost ?? stats.todayCost) || 0;
1617
+ const todayLimitPercent = todayLimitNum > 0 ? Math.min(100, Math.round(todaySpentNum / todayLimitNum * 100)) : 0;
1618
+
1619
+ const retry = () => {
1620
+ loadUsage();
1621
+ loadKeys();
1622
+ loadLimitStatuses();
1623
+ if (selectedKey !== null) loadAccount(selectedKey, true);
1624
+ };
1625
+
1626
+ const money = react.useCallback((value) => value === null || value === void 0 ? "—" : `${currencySymbol(pricing.currency)}${fmtMoney(value)}`, [pricing.currency]);
1627
+
1628
+ const updatedLabel = refreshedAt === null ? "" : translate("panel.updatedAt", {
1629
+ time: new Date(refreshedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
1630
+ });
1631
+
1632
+ return react_jsx_runtime.jsxs("div", {
1633
+ className: S.section,
1634
+ "data-usage-stats-section": true,
1635
+ children: [
1636
+ react_jsx_runtime.jsxs("div", {
1637
+ className: S.header,
1638
+ children: [
1639
+ react_jsx_runtime.jsxs("div", {
1640
+ className: S.headerLeft,
1641
+ children: [
1642
+ react_jsx_runtime.jsx("span", { className: S.title, children: translate("panel.title") })
1643
+ ]
1644
+ }),
1645
+ react_jsx_runtime.jsxs("div", {
1646
+ className: S.headerActions,
1647
+ children: [
1648
+ typeof onOpenSettings === "function" && react_jsx_runtime.jsx("button", {
1649
+ type: "button",
1650
+ className: S.settingsLink,
1651
+ "data-usage-stats-settings-link": true,
1652
+ "aria-label": translate("panel.gotoSettings"),
1653
+ onClick: onOpenSettings,
1654
+ children: translate("panel.gotoSettings")
1655
+ }),
1656
+ react_jsx_runtime.jsx(primitives.Tooltip, {
1657
+ label: translate("action.refresh"),
1658
+ side: "bottom",
1659
+ delayMs: 500,
1660
+ children: react_jsx_runtime.jsx("button", {
1661
+ type: "button",
1662
+ className: S.iconButton,
1663
+ "aria-label": translate("action.refresh"),
1664
+ "data-loading": accountLoading,
1665
+ onClick: retry,
1666
+ children: react_jsx_runtime.jsx(primitives.IconRefreshOutline14, { size: 14 })
1667
+ })
1668
+ }),
1669
+ updatedLabel !== "" && react_jsx_runtime.jsx("span", { className: S.updated, children: updatedLabel }),
1670
+ typeof onClose === "function" && react_jsx_runtime.jsx(primitives.Tooltip, {
1671
+ label: translate("action.close"),
1672
+ side: "bottom",
1673
+ delayMs: 500,
1674
+ children: react_jsx_runtime.jsx("button", {
1675
+ type: "button",
1676
+ className: S.iconButton,
1677
+ "aria-label": translate("action.close"),
1678
+ onClick: onClose,
1679
+ children: react_jsx_runtime.jsx(primitives.IconCloseOutline16, { size: 14 })
1680
+ })
1681
+ })
1682
+ ]
1683
+ })
1684
+ ]
1685
+ }),
1686
+ usageError !== null ? react_jsx_runtime.jsxs("div", {
1687
+ className: S.error,
1688
+ children: [
1689
+ react_jsx_runtime.jsx("span", { children: translate("usage.error", { message: usageError }) }),
1690
+ react_jsx_runtime.jsx("button", { type: "button", className: S.retry, onClick: loadUsage, children: translate("action.retry") })
1691
+ ]
1692
+ }) : null,
1693
+ react_jsx_runtime.jsxs("div", {
1694
+ className: S.tabs,
1695
+ role: "tablist",
1696
+ "aria-label": translate("panel.tabs"),
1697
+ children: [
1698
+ react_jsx_runtime.jsx("button", {
1699
+ type: "button",
1700
+ role: "tab",
1701
+ className: S.tab,
1702
+ "data-active": tab === "overview" ? "true" : void 0,
1703
+ "aria-selected": tab === "overview",
1704
+ "data-usage-stats-tab": "overview",
1705
+ onClick: () => setTab("overview"),
1706
+ children: translate("panel.tabOverview")
1707
+ }),
1708
+ react_jsx_runtime.jsx("button", {
1709
+ type: "button",
1710
+ role: "tab",
1711
+ className: S.tab,
1712
+ "data-active": tab === "details" ? "true" : void 0,
1713
+ "aria-selected": tab === "details",
1714
+ "data-usage-stats-tab": "details",
1715
+ onClick: () => setTab("details"),
1716
+ children: translate("panel.tabDetails")
1717
+ })
1718
+ ]
1719
+ }),
1720
+ tab === "overview" && react_jsx_runtime.jsx(BalanceCard, {
1721
+ keys,
1722
+ selectedKey,
1723
+ onSelectKey: setSelectedKey,
1724
+ account,
1725
+ accountLoading,
1726
+ accountError,
1727
+ balanceTone: limitToneOf(activeLimitStatus?.balanceAlertStatus),
1728
+ translate,
1729
+ onRefresh: () => {
1730
+ if (selectedKey !== null) loadAccount(selectedKey, true);
1731
+ }
1732
+ }),
1733
+ tab === "details" && models.length > 0 && react_jsx_runtime.jsxs("label", {
1734
+ className: S.pickerRow,
1735
+ children: [
1736
+ react_jsx_runtime.jsx("span", { className: S.pickerLabel, children: translate("usage.model") }),
1737
+ react_jsx_runtime.jsx("select", {
1738
+ className: S.select,
1739
+ value: modelFilter,
1740
+ "aria-label": translate("usage.model"),
1741
+ onChange: (event) => setModelFilter(event.target.value),
1742
+ children: [
1743
+ react_jsx_runtime.jsx("option", { value: "", children: translate("usage.allModels") }),
1744
+ models.map((model) => react_jsx_runtime.jsx("option", { value: model, children: model }, model))
1745
+ ]
1746
+ })
1747
+ ]
1748
+ }),
1749
+ usage === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("usage.loading") }) : tab === "overview" ? react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
1750
+ children: [
1751
+ todayLimitNum > 0 && react_jsx_runtime.jsxs("div", {
1752
+ className: S.usageLimitBanner,
1753
+ style: { "--usage-progress": `${todayLimitPercent}%` },
1754
+ "data-status": activeLimitStatus?.status || "normal",
1755
+ children: [
1756
+ react_jsx_runtime.jsx("strong", { children: translate(limitStatusLabelKey(activeLimitStatus?.status)) }),
1757
+ react_jsx_runtime.jsx("span", { children: translate("limits.progress", { spent: money(todaySpentNum), limit: money(todayLimitNum), percent: String(todayLimitPercent) }) })
1758
+ ]
1759
+ }),
1760
+ react_jsx_runtime.jsxs("div", {
1761
+ className: S.statsRow,
1762
+ children: [
1763
+ react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.todayTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.today") })] }),
1764
+ react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.monthTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.month") })] }),
1765
+ react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.totalTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.total") })] }),
1766
+ react_jsx_runtime.jsx("div", { className: S.stat, children: [
1767
+ react_jsx_runtime.jsxs("span", { className: `${S.statValue} ${S.statMoney} ${S.valueWithIndicator}`, children: [
1768
+ todaySpendTone !== "muted" ? react_jsx_runtime.jsx("span", { className: S.statusDot, "data-tone": todaySpendTone, "data-usage-cost-indicator": true, "aria-label": `today ${todaySpendTone}` }) : null,
1769
+ money(stats.todayCost)
1770
+ ] }),
1771
+ react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.costToday") })
1772
+ ] })
1773
+ ]
1774
+ }),
1775
+ react_jsx_runtime.jsxs("section", {
1776
+ className: S.card,
1777
+ children: [
1778
+ react_jsx_runtime.jsxs("div", {
1779
+ className: S.heatHeader,
1780
+ children: [
1781
+ react_jsx_runtime.jsx("span", { className: S.cardTitle, children: translate("usage.heatmap", { year: selectedYear }) }),
1782
+ react_jsx_runtime.jsx("select", {
1783
+ className: S.yearSelect,
1784
+ "aria-label": translate("usage.year"),
1785
+ value: selectedYear,
1786
+ onChange: (event) => {
1787
+ setSelectedYear(Number(event.target.value));
1788
+ setSelectedDay(null);
1789
+ },
1790
+ children: yearOptions.map((year) => react_jsx_runtime.jsx("option", { value: year, children: year }, year))
1791
+ })
1792
+ ]
1793
+ }),
1794
+ react_jsx_runtime.jsx(ContributionHeatmap, { heat, translate, selectedKey: selectedDay, onSelect: setSelectedDay, today: serverToday })
1795
+ ]
1796
+ }),
1797
+ react_jsx_runtime.jsxs("section", {
1798
+ className: S.card,
1799
+ children: [
1800
+ react_jsx_runtime.jsxs("div", {
1801
+ className: S.cardHead,
1802
+ children: [
1803
+ react_jsx_runtime.jsx("span", {
1804
+ className: S.cardTitle,
1805
+ children: [
1806
+ react_jsx_runtime.jsx(primitives.IconDataOutline16, { size: 14 }),
1807
+ translate("usage.hourly")
1808
+ ]
1809
+ }),
1810
+ react_jsx_runtime.jsxs("div", {
1811
+ className: S.dayNav,
1812
+ children: [
1813
+ react_jsx_runtime.jsx("button", {
1814
+ type: "button",
1815
+ className: S.navButton,
1816
+ "aria-label": translate("action.prevDay"),
1817
+ disabled: recent.length === 0 || activeDayKey >= recent[0].date,
1818
+ onClick: () => {
1819
+ const index = recent.findIndex((day) => day.date === activeDayKey);
1820
+ if (index < recent.length - 1) setSelectedDay(recent[index + 1].date);
1821
+ },
1822
+ children: react_jsx_runtime.jsx(primitives.IconChevronLeftOutline14, { size: 12 })
1823
+ }),
1824
+ react_jsx_runtime.jsx("span", { className: S.dayTitle, children: dayLabel(activeDayKey, translate) }),
1825
+ react_jsx_runtime.jsx("button", {
1826
+ type: "button",
1827
+ className: S.navButton,
1828
+ "aria-label": translate("action.nextDay"),
1829
+ disabled: activeDayKey <= recent[recent.length - 1]?.date,
1830
+ onClick: () => {
1831
+ const index = recent.findIndex((day) => day.date === activeDayKey);
1832
+ if (index > 0) setSelectedDay(recent[index - 1].date);
1833
+ },
1834
+ children: react_jsx_runtime.jsx(primitives.IconChevronRightOutline14, { size: 12 })
1835
+ }),
1836
+ selectedDay !== null && react_jsx_runtime.jsx("button", {
1837
+ type: "button",
1838
+ className: S.todayButton,
1839
+ onClick: () => setSelectedDay(null),
1840
+ children: translate("action.today")
1841
+ }),
1842
+ ]
1843
+ })
1844
+ ]
1845
+ }),
1846
+ activeDay === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("chart.empty") }) : react_jsx_runtime.jsx(HourlyChart, {
1847
+ day: activeDay,
1848
+ peakHours: pricing.peakHours,
1849
+ money,
1850
+ translate
1851
+ }),
1852
+ activeDay !== null && react_jsx_runtime.jsx(DayDetail, { day: activeDay, money, translate })
1853
+ ]
1854
+ }),
1855
+ ]
1856
+ }) : react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
1857
+ children: [
1858
+ react_jsx_runtime.jsxs("section", {
1859
+ className: S.card,
1860
+ children: [
1861
+ react_jsx_runtime.jsx("span", { className: S.cardTitle, children: translate("usage.recent") }),
1862
+ react_jsx_runtime.jsx(DayList, {
1863
+ days: recent,
1864
+ selectedDay: activeDayKey,
1865
+ onSelect: (date) => setSelectedDay(date === (serverToday ?? todayKey()) ? null : date),
1866
+ money,
1867
+ translate
1868
+ })
1869
+ ]
1870
+ })
1871
+ ]
1872
+ })
1873
+ ]
1874
+ });
1875
+ }
1876
+ //#endregion
1877
+
1878
+
1879
+ /**
1880
+ * Open the native Harness settings dialog from the query panel. The
1881
+ * settings shell exposes no programmatic open API to plugins, so this
1882
+ * clicks the sidebar settings trigger — identified by its dialog
1883
+ * affordance and scoped to the sidebar footer host that also contains
1884
+ * our own trigger (never a broad substring match on labels). The
1885
+ * registered「用量与计费」section then appears in the settings nav.
1886
+ * @param layerEl - our sidebar layer element; the search walks up to
1887
+ * the footer host that contains both triggers.
1888
+ * @returns whether the trigger was found and clicked.
1889
+ */
1890
+ function openHarnessSettings(layerEl) {
1891
+ let host = layerEl?.parentElement ?? null;
1892
+ let opened = false;
1893
+ for (let depth = 0; host !== null && depth < 4; depth += 1) {
1894
+ const button = host.querySelector?.('button[aria-haspopup="dialog"]');
1895
+ if (button !== null && button !== void 0 && typeof button.click === "function") {
1896
+ button.click();
1897
+ opened = true;
1898
+ break;
1899
+ }
1900
+ host = host.parentElement;
1901
+ }
1902
+ if (!opened || typeof document === "undefined") return opened;
1903
+ const labels = new Set(["用量与计费", "Usage & Billing"]);
1904
+ let attempts = 0;
1905
+ const selectSection = () => {
1906
+ const candidates = document.querySelectorAll("button,[role=button],a,[tabindex]");
1907
+ for (const candidate of candidates) {
1908
+ if (labels.has(candidate.textContent?.trim()) && candidate.getClientRects().length > 0) {
1909
+ candidate.click();
1910
+ return;
1911
+ }
1912
+ }
1913
+ if (attempts++ < 12) window.setTimeout(selectSection, 50);
1914
+ };
1915
+ window.setTimeout(selectSection, 0);
1916
+ return true;
1917
+ }
1918
+ //#region settings section
1919
+ /**
1920
+ * The「用量与计费」settings page, registered as a native
1921
+ * `settings.section`. Hosts configuration that changes billing or call
1922
+ * behavior — starting with the quota/limit card migrated out of the query
1923
+ * panel. The query panel stays read-only; every write-affecting control
1924
+ * lives here (账户/价格/通知/数据管理 tabs arrive in later phases).
1925
+ * @param props - t bound by the slot runtime (locale seat).
1926
+ */
1927
+ function UsageBillingSettingsSection({ t }) {
1928
+ const translate = (key, params) => interpolate(t !== void 0 ? t(key) : key, params);
1929
+ const [keys, setKeys] = react.useState([]);
1930
+ const [selectedKey, setSelectedKey] = react.useState(null);
1931
+ const [pricing, setPricing] = react.useState({ currency: "CNY", peakHours: [[9, 12], [14, 18]], peakMultiplier: 2 });
1932
+ const [todayCost, setTodayCost] = react.useState(0);
1933
+ const [serverToday, setServerToday] = react.useState(null);
1934
+ const [loaded, setLoaded] = react.useState(false);
1935
+ const [error, setError] = react.useState(null);
1936
+ const mountedRef = react.useRef(true);
1937
+ const usageLoaderRef = react.useRef(null);
1938
+ if (usageLoaderRef.current === null) usageLoaderRef.current = createLoader();
1939
+
1940
+ const loadUsage = react.useCallback(() => {
1941
+ const seq = usageLoaderRef.current.start();
1942
+ setError(null);
1943
+ fetchJson("/api/usage-stats/usage").then((payload) => {
1944
+ if (!mountedRef.current || !usageLoaderRef.current.isCurrent(seq)) return;
1945
+ if (payload.ok !== true) {
1946
+ setError(payload.message ?? "usage aggregation failed");
1947
+ return;
1948
+ }
1949
+ setPricing(pricingOf(payload));
1950
+ setServerToday(payload.today ?? null);
1951
+ const todayDay = (payload.days ?? []).find((day) => day.date === (payload.today ?? todayKey()));
1952
+ setTodayCost(todayDay?.cost ?? null);
1953
+ setLoaded(true);
1954
+ }).catch((loadError) => {
1955
+ if (!mountedRef.current || !usageLoaderRef.current.isCurrent(seq)) return;
1956
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
1957
+ });
1958
+ }, []);
1959
+
1960
+ const loadKeys = react.useCallback(() => {
1961
+ fetchJson("/api/usage-stats/keys").then((payload) => {
1962
+ if (!mountedRef.current) return;
1963
+ if (payload.ok !== true) return;
1964
+ const list = Array.isArray(payload.keys) ? payload.keys : [];
1965
+ setKeys(list);
1966
+ setSelectedKey((current) => {
1967
+ if (current !== null && list.some((key) => key.id === current)) return current;
1968
+ return list.find((key) => key.default)?.id ?? list.find((key) => key.configured)?.id ?? list[0]?.id ?? null;
1969
+ });
1970
+ }).catch(() => {});
1971
+ }, []);
1972
+
1973
+ react.useEffect(() => {
1974
+ mountedRef.current = true;
1975
+ loadUsage();
1976
+ loadKeys();
1977
+ const usageTimer = window.setInterval(loadUsage, 60000);
1978
+ return () => {
1979
+ mountedRef.current = false;
1980
+ window.clearInterval(usageTimer);
1981
+ };
1982
+ }, [loadUsage, loadKeys]);
1983
+
1984
+ return react_jsx_runtime.jsxs("div", {
1985
+ className: S.section,
1986
+ "data-usage-billing-settings": true,
1987
+ children: [
1988
+ react_jsx_runtime.jsx("h2", { className: S.title, children: translate("settings.title") }),
1989
+ react_jsx_runtime.jsx("p", { className: S.note, children: translate("settings.desc") }),
1990
+ error !== null ? react_jsx_runtime.jsxs("div", {
1991
+ className: S.error,
1992
+ children: [
1993
+ react_jsx_runtime.jsx("span", { children: translate("usage.error", { message: error }) }),
1994
+ react_jsx_runtime.jsx("button", { type: "button", className: S.retry, onClick: loadUsage, children: translate("action.retry") })
1995
+ ]
1996
+ }) : null,
1997
+ loaded === false ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("usage.loading") }) : react_jsx_runtime.jsx(LimitsCard, {
1998
+ keys,
1999
+ selectedKey,
2000
+ onSelectKey: setSelectedKey,
2001
+ pricing,
2002
+ todayCost,
2003
+ translate,
2004
+ onLimitsUpdated: () => loadUsage()
2005
+ })
2006
+ ]
2007
+ });
2008
+ }
2009
+ //#endregion
2010
+ /** Sidebar summary poll cadence: 60s while the panel is open, 5min while closed. */
2011
+ const SIDEBAR_POLL_MS_OPEN = 60000;
2012
+ const SIDEBAR_POLL_MS_CLOSED = 300000;
2013
+ //#region sidebar panel
2014
+ /** Native Harness sidebar action that owns the floating usage panel. */
2015
+ function UsageStatsPanel({ wide, t }) {
2016
+ const [open, setOpen] = react.useState(false);
2017
+ const [panelLeft, setPanelLeft] = react.useState(12);
2018
+ const [summary, setSummary] = react.useState({ balance: "—", today: "—", todayTokens: null, status: "muted", balanceStatus: "muted", todayStatus: "muted" });
2019
+ const layerRef = react.useRef(null);
2020
+ const summaryRequestRef = react.useRef(0);
2021
+ const loadSummary = react.useCallback(() => {
2022
+ const request = ++summaryRequestRef.current;
2023
+ Promise.allSettled([
2024
+ fetchJson("/api/usage-stats/usage"),
2025
+ fetchJson("/api/usage-stats/balance"),
2026
+ fetchJson("/api/usage-stats/limits")
2027
+ ]).then(([usageResult, balanceResult, limitsResult]) => {
2028
+ if (summaryRequestRef.current !== request) return;
2029
+ setSummary(sidebarSummaryOf(
2030
+ usageResult.status === "fulfilled" ? usageResult.value : null,
2031
+ balanceResult.status === "fulfilled" ? balanceResult.value : null,
2032
+ limitsResult.status === "fulfilled" ? limitsResult.value : null
2033
+ ));
2034
+ });
2035
+ }, []);
2036
+ const summaryText = interpolate(t("panel.summary"), summary);
2037
+
2038
+ react.useEffect(() => {
2039
+ loadSummary();
2040
+ // Open panel: refresh every 60s; closed: back off to every 5min so an
2041
+ // idle sidebar never hammers the loopback endpoints.
2042
+ const pollMs = open ? SIDEBAR_POLL_MS_OPEN : SIDEBAR_POLL_MS_CLOSED;
2043
+ const timer = window.setInterval(loadSummary, pollMs);
2044
+ const onLimitsUpdated = () => loadSummary();
2045
+ window.addEventListener("usage-stats:limits-updated", onLimitsUpdated);
2046
+ return () => {
2047
+ window.clearInterval(timer);
2048
+ window.removeEventListener("usage-stats:limits-updated", onLimitsUpdated);
2049
+ summaryRequestRef.current += 1;
2050
+ };
2051
+ }, [loadSummary, open]);
2052
+ react.useEffect(() => {
2053
+ if (!open) return void 0;
2054
+ const updatePosition = () => {
2055
+ const right = layerRef.current?.getBoundingClientRect?.().right;
2056
+ if (!Number.isFinite(right)) return;
2057
+ // The panel is 760px wide (max-width: calc(100vw - 32px)); clamp its
2058
+ // left edge so the right edge keeps a 12px viewport margin.
2059
+ const panelWidth = Math.min(760, window.innerWidth - 32);
2060
+ const maxPanelLeft = Math.max(0, window.innerWidth - panelWidth - 12);
2061
+ setPanelLeft(Math.min(Math.max(0, Math.round(right)), maxPanelLeft));
2062
+ };
2063
+ updatePosition();
2064
+ window.addEventListener("resize", updatePosition);
2065
+ return () => window.removeEventListener("resize", updatePosition);
2066
+ }, [open]);
2067
+
2068
+ // Harness lays footer action registrations out in a row. Stack them so
2069
+ // this action and the built-in Settings action remain independently
2070
+ // visible in both wide and rail sidebar modes.
2071
+ react.useEffect(() => {
2072
+ let host = layerRef.current?.parentElement ?? null;
2073
+ for (let depth = 0; host !== null && depth < 3; depth += 1) {
2074
+ if (window.getComputedStyle(host).display.includes("flex")) break;
2075
+ host = host.parentElement;
2076
+ }
2077
+ if (host === null) return void 0;
2078
+ const style = window.getComputedStyle(host);
2079
+ if (!style.display.includes("flex") || style.flexDirection === "column") return void 0;
2080
+ const previous = host.style.flexDirection;
2081
+ host.style.flexDirection = "column";
2082
+ return () => { host.style.flexDirection = previous; };
2083
+ }, []);
2084
+
2085
+ return react_jsx_runtime.jsxs("div", {
2086
+ ref: layerRef,
2087
+ className: wide ? S.layer : `${S.layer} ${S.rail}`,
2088
+ children: [
2089
+ open && react_dom.createPortal(react_jsx_runtime.jsx("section", {
2090
+ className: S.panel,
2091
+ style: { left: `${panelLeft}px` },
2092
+ "data-usage-stats-panel": true,
2093
+ "aria-label": t("panel.title"),
2094
+ children: react_jsx_runtime.jsx("div", {
2095
+ className: S.panelBody,
2096
+ children: react_jsx_runtime.jsx(UsageStatsSection, {
2097
+ t,
2098
+ onClose: () => setOpen(false),
2099
+ onOpenSettings: () => {
2100
+ setOpen(false);
2101
+ openHarnessSettings(layerRef.current);
2102
+ }
2103
+ })
2104
+ })
2105
+ }), document.body),
2106
+ react_jsx_runtime.jsx("div", {
2107
+ className: S.footerButtons,
2108
+ children: react_jsx_runtime.jsxs("button", {
2109
+ type: "button",
2110
+ className: S.sidebarButton,
2111
+ "data-usage-stats-trigger": true,
2112
+ "data-active": open ? "true" : void 0,
2113
+ "aria-label": t("panel.badge"),
2114
+ "aria-expanded": open,
2115
+ onClick: () => {
2116
+ if (!open) loadSummary();
2117
+ setOpen((value) => !value);
2118
+ },
2119
+ children: [
2120
+ react_jsx_runtime.jsx(UsageCurrencyIcon, { size: wide ? 18 : 20 }),
2121
+ wide && react_jsx_runtime.jsxs("span", {
2122
+ className: S.sidebarText,
2123
+ children: [
2124
+ react_jsx_runtime.jsx("span", { className: S.sidebarLabel, children: t("panel.badge") }),
2125
+ react_jsx_runtime.jsxs("span", {
2126
+ className: S.sidebarSummary,
2127
+ title: summaryText,
2128
+ children: [
2129
+ summary.balanceStatus !== "muted" ? react_jsx_runtime.jsx("span", { className: S.statusDot, "data-tone": summary.balanceStatus }) : null,
2130
+ summary.todayStatus !== "muted" ? react_jsx_runtime.jsx("span", { className: S.statusDot, "data-tone": summary.todayStatus }) : null,
2131
+ summaryText
2132
+ ]
2133
+ })
2134
+ ]
2135
+ })
2136
+ ]
2137
+ })
2138
+ })
2139
+ ]
2140
+ });
2141
+ }
2142
+ //#endregion
2143
+
2144
+ //#region locales
2145
+ /** `usageStats` namespace dictionaries (the zh key set is the source of truth). */
2146
+ const NS = "usageStats";
2147
+ const zh = {
2148
+ "nav": "用量统计",
2149
+ "panel.title": "用量与余额",
2150
+ "panel.badge": "用量/余额",
2151
+ "panel.summary": "余额 {balance} · 今日 {today}",
2152
+ "panel.updatedAt": "更新于 {time}",
2153
+ "panel.gotoSettings": "前往设置",
2154
+ "panel.tabs": "查询标签",
2155
+ "panel.tabOverview": "概览",
2156
+ "panel.tabDetails": "明细",
2157
+ "action.refresh": "刷新",
2158
+ "action.retry": "重试",
2159
+ "action.close": "关闭",
2160
+ "action.today": "回到今天",
2161
+ "action.prevMonth": "上个月",
2162
+ "action.nextMonth": "下个月",
2163
+ "action.prevDay": "前一天",
2164
+ "action.nextDay": "后一天",
2165
+ "balance.label": "DeepSeek 官方账户余额",
2166
+ "balance.apiKey": "API Key",
2167
+ "balance.unconfigured": "未配置",
2168
+ "balance.toppedUp": "充值余额",
2169
+ "balance.granted": "赠送余额",
2170
+ "balance.error": "余额获取失败:{message}",
2171
+ "balance.status.loading": "查询中",
2172
+ "balance.status.empty": "暂无数据",
2173
+ "balance.status.ok": "实时",
2174
+ "balance.status.notConfigured": "未配置",
2175
+ "balance.status.unauthorized": "密钥无效",
2176
+ "balance.status.rateLimited": "请求受限",
2177
+ "balance.status.unavailable": "暂不可用",
2178
+ "balance.status.error": "获取失败",
2179
+ "usage.model": "模型",
2180
+ "usage.allModels": "全部模型",
2181
+ "usage.today": "今日",
2182
+ "usage.month": "本月",
2183
+ "usage.total": "累计",
2184
+ "usage.costToday": "今日消费(估算)",
2185
+ "usage.cost": "费用",
2186
+ "usage.loading": "正在统计用量…",
2187
+ "usage.error": "用量统计失败:{message}",
2188
+ "usage.hourly": "按小时统计",
2189
+ "usage.heatmap": "{year} 年每日用量",
2190
+ "usage.year": "选择年份",
2191
+ "usage.legendLess": "少",
2192
+ "usage.legendMore": "多",
2193
+ "usage.recent": "最近 14 天",
2194
+ "usage.noDays": "最近 14 天没有用量记录。",
2195
+ "usage.input": "输入",
2196
+ "usage.output": "输出",
2197
+ "usage.cacheRead": "缓存读",
2198
+ "usage.hitRate": "缓存命中",
2199
+ "usage.unknownModel": "未知模型",
2200
+ "usage.noModels": "这一天没有分模型数据。",
2201
+ "chart.empty": "这一天没有用量数据。",
2202
+ "chart.inputWithCache": "输入(含缓存)",
2203
+ "chart.peakNote": "高峰:北京时间 09-12 / 14-18,费用×2",
2204
+ "limits.title": "用量提醒与限额设置",
2205
+ "limits.desc": "按指定 API Key 或全局设置每日消费限额与预警比例;硬停止默认关闭,仅在主动开启后阻止新调用。",
2206
+ "limits.apiKey": "目标 API Key",
2207
+ "limits.global": "全局默认 (全部 Key)",
2208
+ "limits.enable": "启用用量提醒",
2209
+ "limits.enableDesc": "启用后根据每日消费限额和预警比例显示状态提醒",
2210
+ "limits.dailyLimit": "每日消费限额",
2211
+ "limits.dailyLimitPlaceholder": "例如 20.00 (不限留空)",
2212
+ "limits.lowBalance": "余额提醒",
2213
+ "limits.lowBalancePlaceholder": "例如 10.00 (不限留空)",
2214
+ "limits.alertPercent": "预警提醒比例",
2215
+ "limits.criticalPercent": "严重预警比例",
2216
+ "limits.alertNormal": "正常 ≥ {percent}%",
2217
+ "limits.alertWarning": "预警 {percent}%–{critical}%",
2218
+ "limits.alertExceeded": "严重 ≤ {percent}%",
2219
+ "limits.stopOnExceed": "超限时停止新调用",
2220
+ "limits.stopDesc": "仅使用可信的本地消费和新鲜余额判断;数据过期或查询失败时放行。",
2221
+ "limits.stopConfirm": "确定开启硬停止吗?费用为估算值,达到限额后新的模型调用会被阻止。",
2222
+ "limits.status.ok": "用量正常",
2223
+ "limits.status.normal": "用量正常",
2224
+ "limits.status.warning": "用量预警",
2225
+ "limits.status.exceeded": "已超限",
2226
+ "limits.status.blocked": "已停止新调用",
2227
+ "limits.status.stale": "余额数据已过期",
2228
+ "limits.status.unavailable": "余额暂不可用",
2229
+ "limits.status.unlimited": "未限制",
2230
+ "limits.progress": "今日已消费 {spent} / 限额 {limit} ({percent}%)",
2231
+ "limits.save": "保存设置",
2232
+ "limits.saving": "保存中…",
2233
+ "limits.saveError": "保存失败:{message}",
2234
+ "settings.nav": "用量与计费",
2235
+ "settings.title": "用量与计费",
2236
+ "settings.desc": "这里承载会改变计费或调用行为的配置。查询与配置已分离:余额、用量与图表请在侧栏「用量/余额」查询中心查看。",
2237
+ "weekday.mon": "一",
2238
+ "weekday.tue": "二",
2239
+ "weekday.wed": "三",
2240
+ "weekday.thu": "四",
2241
+ "weekday.fri": "五",
2242
+ "weekday.sat": "六",
2243
+ "weekday.sun": "日",
2244
+ "month.year": "{year}年{month}",
2245
+ "month.names": "1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月"
2246
+ };
2247
+ const en = {
2248
+ "nav": "Usage Stats",
2249
+ "panel.title": "Usage & Balance",
2250
+ "panel.badge": "Usage/Balance",
2251
+ "panel.summary": "Balance {balance} · today {today}",
2252
+ "panel.updatedAt": "Updated at {time}",
2253
+ "panel.gotoSettings": "Settings",
2254
+ "panel.tabs": "Panel sections",
2255
+ "panel.tabOverview": "Overview",
2256
+ "panel.tabDetails": "Details",
2257
+ "action.refresh": "Refresh",
2258
+ "action.retry": "Retry",
2259
+ "action.close": "Close",
2260
+ "action.today": "Today",
2261
+ "action.prevMonth": "Previous month",
2262
+ "action.nextMonth": "Next month",
2263
+ "action.prevDay": "Previous day",
2264
+ "action.nextDay": "Next day",
2265
+ "balance.label": "DeepSeek official balance",
2266
+ "balance.apiKey": "API Key",
2267
+ "balance.unconfigured": "not configured",
2268
+ "balance.toppedUp": "Topped up",
2269
+ "balance.granted": "Granted",
2270
+ "balance.error": "Balance fetch failed: {message}",
2271
+ "balance.status.loading": "Loading",
2272
+ "balance.status.empty": "No data",
2273
+ "balance.status.ok": "Live",
2274
+ "balance.status.notConfigured": "Not configured",
2275
+ "balance.status.unauthorized": "Invalid key",
2276
+ "balance.status.rateLimited": "Rate limited",
2277
+ "balance.status.unavailable": "Unavailable",
2278
+ "balance.status.error": "Failed",
2279
+ "usage.model": "Model",
2280
+ "usage.allModels": "All models",
2281
+ "usage.today": "Today",
2282
+ "usage.month": "This month",
2283
+ "usage.total": "All time",
2284
+ "usage.costToday": "Spend today (est.)",
2285
+ "usage.cost": "Cost",
2286
+ "usage.loading": "Aggregating usage…",
2287
+ "usage.error": "Usage aggregation failed: {message}",
2288
+ "usage.hourly": "Per-hour usage",
2289
+ "usage.heatmap": "Daily usage in {year}",
2290
+ "usage.year": "Select year",
2291
+ "usage.legendLess": "Less",
2292
+ "usage.legendMore": "More",
2293
+ "usage.recent": "Last 14 days",
2294
+ "usage.noDays": "No usage in the last 14 days.",
2295
+ "usage.input": "Input",
2296
+ "usage.output": "Output",
2297
+ "usage.cacheRead": "Cache read",
2298
+ "usage.hitRate": "Cache hit",
2299
+ "usage.unknownModel": "Unknown model",
2300
+ "usage.noModels": "No per-model data for this day.",
2301
+ "chart.empty": "No usage data for this day.",
2302
+ "chart.inputWithCache": "Input (incl. cache)",
2303
+ "chart.peakNote": "Peak: Beijing 09-12 / 14-18, ×2 price",
2304
+ "limits.title": "Usage Alerts & Quota Limits",
2305
+ "limits.desc": "Configure daily spend limits and alert thresholds per API key or globally. Hard stop is off by default and only blocks new calls after you opt in.",
2306
+ "limits.apiKey": "Target API Key",
2307
+ "limits.global": "Global Default (All Keys)",
2308
+ "limits.enable": "Enable Usage Alerts",
2309
+ "limits.enableDesc": "Show status alerts based on daily spend and the selected alert percentage",
2310
+ "limits.dailyLimit": "Daily Spend Limit",
2311
+ "limits.dailyLimitPlaceholder": "e.g. 20.00 (blank for no limit)",
2312
+ "limits.lowBalance": "Balance Alert",
2313
+ "limits.lowBalancePlaceholder": "e.g. 10.00 (blank for no alert)",
2314
+ "limits.alertPercent": "Alert Threshold",
2315
+ "limits.criticalPercent": "Critical Threshold",
2316
+ "limits.alertNormal": "Normal ≥ {percent}%",
2317
+ "limits.alertWarning": "Warning {percent}%–{critical}%",
2318
+ "limits.alertExceeded": "Critical ≤ {percent}%",
2319
+ "limits.stopOnExceed": "Stop New Calls on Exceed",
2320
+ "limits.stopDesc": "Only trusted local usage and fresh balance data can block; stale or failed balance checks fail open.",
2321
+ "limits.stopConfirm": "Enable hard stop? Costs are estimates, and new model calls will be blocked after a limit is reached.",
2322
+ "limits.status.ok": "Normal",
2323
+ "limits.status.normal": "Normal",
2324
+ "limits.status.warning": "Warning",
2325
+ "limits.status.exceeded": "Exceeded",
2326
+ "limits.status.blocked": "New Calls Blocked",
2327
+ "limits.status.stale": "Balance Data Stale",
2328
+ "limits.status.unavailable": "Balance Unavailable",
2329
+ "limits.status.unlimited": "No Limit",
2330
+ "limits.progress": "Spent today {spent} / Limit {limit} ({percent}%)",
2331
+ "limits.save": "Save Settings",
2332
+ "limits.saving": "Saving…",
2333
+ "limits.saveError": "Save failed: {message}",
2334
+ "settings.nav": "Usage & Billing",
2335
+ "settings.title": "Usage & Billing",
2336
+ "settings.desc": "Configuration that changes billing or call behavior lives here. Querying is separate: open the sidebar Usage/Balance panel for balances, usage and charts.",
2337
+ "weekday.mon": "M",
2338
+ "weekday.tue": "T",
2339
+ "weekday.wed": "W",
2340
+ "weekday.thu": "T",
2341
+ "weekday.fri": "F",
2342
+ "weekday.sat": "S",
2343
+ "weekday.sun": "S",
2344
+ "month.year": "{month} {year}",
2345
+ "month.names": "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec"
2346
+ };
2347
+ //#endregion
2348
+
2349
+ //#region plugin body
2350
+ /** Services required by the client plugin body. */
2351
+ const inject = ["slots", "locale"];
2352
+
2353
+ /**
2354
+ * Client plugin body: register dictionaries and the sidebar footer action.
2355
+ * @param ctx - client root context.
2356
+ */
2357
+ function apply(ctx) {
2358
+ const t = ctx.locale.bind(NS);
2359
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "usage-stats: dictionaries");
2360
+ ctx.effect(() => {
2361
+ if (typeof document === "undefined") return void 0;
2362
+ const markNav = () => {
2363
+ const source = document.querySelector('[data-usage-stats-trigger] svg');
2364
+ for (const node of document.querySelectorAll("button,a,[role=button]")) {
2365
+ if (node.textContent?.trim() !== "用量与计费" && node.textContent?.trim() !== "Usage & Billing") continue;
2366
+ const item = node.closest("li,[role=tab],.settings-nav-item") || node;
2367
+ item.dataset.usageBillingNav = "true";
2368
+ if (source && !item.querySelector("svg[data-usage-currency-icon]")) {
2369
+ const icon = source.cloneNode(true);
2370
+ icon.dataset.usageCurrencyIcon = "true";
2371
+ icon.setAttribute("width", "20");
2372
+ icon.setAttribute("height", "20");
2373
+ const existing = item.querySelector("svg");
2374
+ if (existing) existing.replaceWith(icon);
2375
+ }
2376
+ }
2377
+ };
2378
+ markNav();
2379
+ const observer = new MutationObserver(markNav);
2380
+ observer.observe(document.body, { childList: true, subtree: true });
2381
+ return () => observer.disconnect();
2382
+ }, "usage-stats: settings navigation icon");
2383
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
2384
+ name: "sidebar.footer.action",
2385
+ id: "usage-stats",
2386
+ order: 10,
2387
+ locale: NS
2388
+ }, UsageStatsPanel));
2389
+ // 设置 → 用量与计费:独立设置入口承载配置(限额等),查询弹窗保持只读。
2390
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2391
+ name: "settings.section",
2392
+ id: "usage-stats",
2393
+ order: 60,
2394
+ label: () => t("settings.nav"),
2395
+ icon: () => react_jsx_runtime.jsx(UsageCurrencyIcon, { size: 20 }),
2396
+ locale: NS
2397
+ }, UsageBillingSettingsSection));
2398
+ }
2399
+ //#endregion
2400
+
2401
+ exports.apply = apply;
2402
+ exports.inject = inject;
2403
+ exports.UsageStatsPanel = UsageStatsPanel;
2404
+ exports.UsageStatsSection = UsageStatsSection;
2405
+ exports.UsageBillingSettingsSection = UsageBillingSettingsSection;
2406
+ exports.openHarnessSettings = openHarnessSettings;
2407
+ exports.BalanceCard = BalanceCard;
2408
+ exports.LimitsCard = LimitsCard;
2409
+ exports.ContributionHeatmap = ContributionHeatmap;
2410
+ exports.HourlyChart = HourlyChart;
2411
+ exports.DayList = DayList;
2412
+ exports.DayDetail = DayDetail;
2413
+ exports.filterDay = filterDay;
2414
+ exports.summarize = summarize;
2415
+ exports.modelChoicesOf = modelChoicesOf;
2416
+ exports.recentDays = recentDays;
2417
+ exports.modelIdOf = modelIdOf;
2418
+ exports.fmt = fmt;
2419
+ exports.fmtMoney = fmtMoney;
2420
+ exports.currencySymbol = currencySymbol;
2421
+ exports.sidebarSummaryOf = sidebarSummaryOf;
2422
+ exports.fmtCurrency = fmtCurrency;
2423
+ exports.isPeak = isPeak;
2424
+ exports.buildYearContributionHeatmap = buildYearContributionHeatmap;
2425
+ exports.cellColor = cellColor;
2426
+ return module.exports;
2427
+ }
2428
+ });