@lyk308/dsh-token-dashboard 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -64
- package/README.zh.md +69 -64
- package/docs/screenshot.png +0 -0
- package/docs/screenshot2.png +0 -0
- package/lib/client.js +561 -393
- package/lib/index.js +73 -43
- package/package.json +45 -45
package/lib/client.js
CHANGED
|
@@ -1,393 +1,561 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-token-dashboard — client half (Token Ledger style).
|
|
3
|
-
*
|
|
4
|
-
* Layout mirrors a token ledger: Balance card, Token-usage stat cards
|
|
5
|
-
* (Today / This month / All time + requests + cache rate), a grouped share bar
|
|
6
|
-
* (by model OR by workspace, toggleable), an Activity heatmap with a
|
|
7
|
-
* 30/90/180-day range switch, and a Models table (Req / Input / Cache /
|
|
8
|
-
* Output). Uses react/jsx-runtime jsx/jsxs correctly and never puts a raw null
|
|
9
|
-
* inside a children array. Wrapped in an ErrorBoundary.
|
|
10
|
-
*/
|
|
11
|
-
window.__ModuleLoader__.load({
|
|
12
|
-
id: "dsh-token-dashboard",
|
|
13
|
-
factory: (require) => {
|
|
14
|
-
const module = { exports: {} };
|
|
15
|
-
const exports = module.exports;
|
|
16
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
17
|
-
|
|
18
|
-
const React = require("react");
|
|
19
|
-
const { useEffect, useState, useCallback, Component } = React;
|
|
20
|
-
const jsx = require("react/jsx-runtime").jsx;
|
|
21
|
-
const jsxs = require("react/jsx-runtime").jsxs;
|
|
22
|
-
|
|
23
|
-
// ---- CSS-ish tokens (follow DSH theme vars) ----
|
|
24
|
-
const CARD = {
|
|
25
|
-
border: "1px solid var(--dsw-alias-border-l1)",
|
|
26
|
-
borderRadius: "12px",
|
|
27
|
-
padding: "16px",
|
|
28
|
-
margin: "12px 0",
|
|
29
|
-
background: "var(--dsw-alias-bg-overlay, transparent)"
|
|
30
|
-
};
|
|
31
|
-
const SEC_TITLE = { fontSize: "13px", fontWeight: 600, color: "var(--dsw-alias-label-secondary)", marginBottom: "10px", letterSpacing: "0.02em" };
|
|
32
|
-
const NUM = { fontSize: "26px", fontWeight: 700, color: "var(--dsw-alias-label-primary)", lineHeight: 1.3 };
|
|
33
|
-
const LABEL = { fontSize: "12px", color: "var(--dsw-alias-label-tertiary)" };
|
|
34
|
-
const ROW = { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "6px 0" };
|
|
35
|
-
const MONO = { fontVariantNumeric: "tabular-nums" };
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
function
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
//
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
jsx("
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
jsx("
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
jsxs("
|
|
336
|
-
jsx("
|
|
337
|
-
jsx(
|
|
338
|
-
] })
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* dsh-token-dashboard — client half (Token Ledger style).
|
|
3
|
+
*
|
|
4
|
+
* Layout mirrors a token ledger: Balance card, Token-usage stat cards
|
|
5
|
+
* (Today / This month / All time + requests + cache rate), a grouped share bar
|
|
6
|
+
* (by model OR by workspace, toggleable), an Activity heatmap with a
|
|
7
|
+
* 30/90/180-day range switch, and a Models table (Req / Input / Cache /
|
|
8
|
+
* Output). Uses react/jsx-runtime jsx/jsxs correctly and never puts a raw null
|
|
9
|
+
* inside a children array. Wrapped in an ErrorBoundary.
|
|
10
|
+
*/
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: "dsh-token-dashboard",
|
|
13
|
+
factory: (require) => {
|
|
14
|
+
const module = { exports: {} };
|
|
15
|
+
const exports = module.exports;
|
|
16
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
17
|
+
|
|
18
|
+
const React = require("react");
|
|
19
|
+
const { useEffect, useState, useCallback, Component } = React;
|
|
20
|
+
const jsx = require("react/jsx-runtime").jsx;
|
|
21
|
+
const jsxs = require("react/jsx-runtime").jsxs;
|
|
22
|
+
|
|
23
|
+
// ---- CSS-ish tokens (follow DSH theme vars) ----
|
|
24
|
+
const CARD = {
|
|
25
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
26
|
+
borderRadius: "12px",
|
|
27
|
+
padding: "16px",
|
|
28
|
+
margin: "12px 0",
|
|
29
|
+
background: "var(--dsw-alias-bg-overlay, transparent)"
|
|
30
|
+
};
|
|
31
|
+
const SEC_TITLE = { fontSize: "13px", fontWeight: 600, color: "var(--dsw-alias-label-secondary)", marginBottom: "10px", letterSpacing: "0.02em" };
|
|
32
|
+
const NUM = { fontSize: "26px", fontWeight: 700, color: "var(--dsw-alias-label-primary)", lineHeight: 1.3 };
|
|
33
|
+
const LABEL = { fontSize: "12px", color: "var(--dsw-alias-label-tertiary)" };
|
|
34
|
+
const ROW = { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "6px 0" };
|
|
35
|
+
const MONO = { fontVariantNumeric: "tabular-nums" };
|
|
36
|
+
const btnStyle = { cursor: "pointer", fontSize: "12px", padding: "4px 10px", borderRadius: "6px", border: "1px solid var(--dsw-alias-border-l1)", background: "var(--dsw-alias-interactive-bg-active, rgba(127,127,127,.14))", color: "var(--dsw-alias-label-primary)", whiteSpace: "nowrap" };
|
|
37
|
+
|
|
38
|
+
function fmt(n) { return (n == null || isNaN(n)) ? "0" : Number(n).toLocaleString(); }
|
|
39
|
+
function fmtMoney(n) { return (n == null || isNaN(n)) ? "0.00" : Number(n).toFixed(2); }
|
|
40
|
+
function pct(v, max) { return max > 0 ? ((v / max) * 100) : 0; }
|
|
41
|
+
|
|
42
|
+
// A segmented control ("By model" / "By workspace" and the heatmap range).
|
|
43
|
+
function Seg({ options, value, onChange }) {
|
|
44
|
+
return jsxs("div", {
|
|
45
|
+
style: { display: "flex", gap: "4px", background: "var(--dsw-alias-interactive-bg-active, rgba(127,127,127,.14))", borderRadius: "8px", padding: "3px" },
|
|
46
|
+
children: options.map((o) => jsx("button", {
|
|
47
|
+
onClick: () => onChange(o.value),
|
|
48
|
+
style: {
|
|
49
|
+
flex: "1 1 auto", minWidth: "0", padding: "5px 14px", fontSize: "12px", borderRadius: "6px", cursor: "pointer",
|
|
50
|
+
border: "none", color: "var(--dsw-alias-label-primary)",
|
|
51
|
+
whiteSpace: "nowrap",
|
|
52
|
+
background: (o.value === value)
|
|
53
|
+
? "var(--dsw-alias-bg-overlay, rgba(255,255,255,.9))"
|
|
54
|
+
: "transparent",
|
|
55
|
+
fontWeight: (o.value === value) ? 600 : 400
|
|
56
|
+
},
|
|
57
|
+
children: o.label
|
|
58
|
+
}, o.value))
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Color scale for heatmap (green shades like the reference).
|
|
63
|
+
function heatColor(val, max) {
|
|
64
|
+
if (val <= 0) return "var(--dsw-alias-border-l1)";
|
|
65
|
+
const t = max > 0 ? (val / max) : 0;
|
|
66
|
+
const a = 0.25 + 0.75 * t;
|
|
67
|
+
return "rgba(76,175,80," + a.toFixed(2) + ")";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A stable color per group entry; the "其他" (other) aggregate is grey so it
|
|
71
|
+
// reads as "everything else", distinct from the named top groups.
|
|
72
|
+
const GROUP_COLORS = ["#4caf50", "#2196f3", "#ff9800", "#9c27b0", "#00bcd4"];
|
|
73
|
+
function groupColor(key, i) {
|
|
74
|
+
if (key === "其他" || key === "other") return "var(--dsw-alias-label-tertiary, #9e9e9e)";
|
|
75
|
+
return GROUP_COLORS[i % GROUP_COLORS.length];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- export helpers: build a CSV/JSON string from stats and download it.
|
|
79
|
+
function downloadText(filename, text, mime) {
|
|
80
|
+
try {
|
|
81
|
+
const blob = new Blob([text], { type: mime });
|
|
82
|
+
const url = URL.createObjectURL(blob);
|
|
83
|
+
const a = document.createElement("a");
|
|
84
|
+
a.href = url; a.download = filename;
|
|
85
|
+
document.body.appendChild(a); a.click();
|
|
86
|
+
document.body.removeChild(a);
|
|
87
|
+
URL.revokeObjectURL(url);
|
|
88
|
+
} catch (e) { console.error("[token-dashboard] export failed", e); }
|
|
89
|
+
}
|
|
90
|
+
function exportJson(stats) {
|
|
91
|
+
downloadText("dsh-token-dashboard.json", JSON.stringify(stats, null, 2), "application/json");
|
|
92
|
+
}
|
|
93
|
+
function exportCsv(stats, metric) {
|
|
94
|
+
const esc = (v) => { const s = String(v ?? ""); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
|
|
95
|
+
const rows = [];
|
|
96
|
+
// by-model lines
|
|
97
|
+
rows.push("type,key,req,input,output,cacheRead,cost,total");
|
|
98
|
+
for (const m of Object.keys(stats.byModel || {})) {
|
|
99
|
+
const x = stats.byModel[m];
|
|
100
|
+
rows.push(["model", m, x.req, x.input, x.output, x.cacheRead, (x.cost || 0).toFixed(4), x.total].map(esc).join(","));
|
|
101
|
+
}
|
|
102
|
+
for (const w of Object.keys(stats.byWorkspace || {})) {
|
|
103
|
+
const x = stats.byWorkspace[w];
|
|
104
|
+
rows.push(["workspace", w, x.req, x.input, x.output, x.cacheRead, (x.cost || 0).toFixed(4), x.total].map(esc).join(","));
|
|
105
|
+
}
|
|
106
|
+
for (const d of Object.keys(stats.byDay || {}).sort()) {
|
|
107
|
+
const x = stats.byDay[d];
|
|
108
|
+
rows.push(["day", d, "", "", "", "", (x.cost || 0).toFixed(4), x.total].map(esc).join(","));
|
|
109
|
+
}
|
|
110
|
+
downloadText("dsh-token-dashboard.csv", rows.join("\n"), "text/csv");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class Boundary extends Component {
|
|
114
|
+
constructor(props) { super(props); this.state = { error: null }; }
|
|
115
|
+
static getDerivedStateFromError(error) { return { error }; }
|
|
116
|
+
componentDidCatch(error, info) { console.error("[token-dashboard] render error:", error, info); }
|
|
117
|
+
render() {
|
|
118
|
+
if (this.state.error) {
|
|
119
|
+
return jsx("div", { style: { color: "var(--dsw-alias-danger, #e5484d)", fontSize: "12px", padding: "8px 0" }, children: "渲染出错:" + String(this.state.error?.message || this.state.error) });
|
|
120
|
+
}
|
|
121
|
+
return jsx("div", { children: this.props.children });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function TokenDashboardPanel() {
|
|
126
|
+
const [stats, setStats] = useState(null);
|
|
127
|
+
const [balance, setBalance] = useState(null);
|
|
128
|
+
const [error, setError] = useState(null);
|
|
129
|
+
const [loading, setLoading] = useState(true);
|
|
130
|
+
const [balanceLoading, setBalanceLoading] = useState(true);
|
|
131
|
+
const [groupBy, setGroupBy] = useState("model"); // "model" | "workspace"
|
|
132
|
+
const [range, setRange] = useState(90); // 30 | 90 | 180 days
|
|
133
|
+
const [metric, setMetric] = useState("cost"); // "cost" | "token"
|
|
134
|
+
const [shareMetric, setShareMetric] = useState("total"); // "total" | "cost" for the share bar
|
|
135
|
+
const [hoverIdx, setHoverIdx] = useState(null); // hovered trend day index
|
|
136
|
+
const [days, setDays] = useState(30); // global window: 7 | 30 | 90 | 0(all)
|
|
137
|
+
|
|
138
|
+
const loadStats = useCallback(async (winDays) => {
|
|
139
|
+
try {
|
|
140
|
+
const d = winDays || days;
|
|
141
|
+
const res = await fetch("/api/token-dashboard/stats?refresh=1&days=" + d);
|
|
142
|
+
if (!res.ok) throw new Error("stats http " + res.status);
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
if (data && data.status === "computing") { setTimeout(() => loadStats(winDays), 1200); return; }
|
|
145
|
+
setStats((data && typeof data === "object") ? data : {});
|
|
146
|
+
setLoading(false);
|
|
147
|
+
} catch (e2) { setError(String(e2)); setLoading(false); }
|
|
148
|
+
}, [days]);
|
|
149
|
+
const loadBalance = useCallback(async (force) => {
|
|
150
|
+
setBalanceLoading(true);
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch("/api/token-dashboard/balance" + (force ? "?force=1" : ""));
|
|
153
|
+
if (!res.ok) throw new Error("balance http " + res.status);
|
|
154
|
+
setBalance((await res.json()) || {});
|
|
155
|
+
} catch (e2) { setError(String(e2)); }
|
|
156
|
+
finally { setBalanceLoading(false); }
|
|
157
|
+
}, []);
|
|
158
|
+
useEffect(() => { loadStats(); loadBalance(false); }, [loadStats, loadBalance]);
|
|
159
|
+
|
|
160
|
+
if (!loading && stats) {
|
|
161
|
+
// ---- derived ----
|
|
162
|
+
const infos = balance?.data?.balance_infos || [];
|
|
163
|
+
let balInfo = infos.find((x) => x && x.currency === "CNY")
|
|
164
|
+
|| infos.find((x) => x && Number(x.total_balance ?? x.balance) > 0)
|
|
165
|
+
|| infos[0] || balance?.data || null;
|
|
166
|
+
const balValue = balInfo ? (balInfo.total_balance ?? balInfo.balance ?? null) : null;
|
|
167
|
+
const balCur = balInfo ? (balInfo.currency || "") : "";
|
|
168
|
+
|
|
169
|
+
const total = Number(stats.total) || 0;
|
|
170
|
+
const req = Number(stats.req) || Number(stats.events) || 0;
|
|
171
|
+
const today = Number(stats.today) || 0;
|
|
172
|
+
const thisMonth = Number(stats.thisMonth) || 0;
|
|
173
|
+
const cacheRate = (Number(stats.cacheHits) && total) ? (Number(stats.cacheHits) / total * 100).toFixed(1) : "0.0";
|
|
174
|
+
|
|
175
|
+
// grouped share data. Show top TOP_N entries by total, then fold the
|
|
176
|
+
// rest into a single "其他" (other) aggregate so the card never grows
|
|
177
|
+
// unbounded and tiny shares don't get lost in the bar.
|
|
178
|
+
const TOP_N = 8;
|
|
179
|
+
const groups = groupBy === "model" ? stats.byModel : stats.byWorkspace;
|
|
180
|
+
// shareBy = "total" or "cost" — pick the field used for share sorting/percent.
|
|
181
|
+
const shareBy = shareMetric === "cost" ? "cost" : "total";
|
|
182
|
+
const allEntries = Object.keys(groups || {}).map((k) => ({ key: k, ...(typeof groups[k] === "object" ? groups[k] : { total: groups[k], cost: 0 }) }))
|
|
183
|
+
.sort((a, b) => (b[shareBy] || 0) - (a[shareBy] || 0));
|
|
184
|
+
let groupEntries;
|
|
185
|
+
if (allEntries.length > TOP_N) {
|
|
186
|
+
const top = allEntries.slice(0, TOP_N);
|
|
187
|
+
// fold the remainder into a synthetic "其他" entry; keep broken-down
|
|
188
|
+
// counters for the bar/list (we only display total/req, but stay safe).
|
|
189
|
+
const rest = allEntries.slice(TOP_N);
|
|
190
|
+
const other = { key: "其他", req: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0, total: 0, cost: 0 };
|
|
191
|
+
for (const r of rest) {
|
|
192
|
+
other.total += Number(r.total) || 0;
|
|
193
|
+
other.cost += Number(r.cost) || 0;
|
|
194
|
+
other.req += Number(r.req) || 0;
|
|
195
|
+
other.input += Number(r.input) || 0;
|
|
196
|
+
other.output += Number(r.output) || 0;
|
|
197
|
+
other.cacheRead += Number(r.cacheRead) || 0;
|
|
198
|
+
other.reasoning += Number(r.reasoning) || 0;
|
|
199
|
+
}
|
|
200
|
+
groupEntries = [...top, other];
|
|
201
|
+
} else {
|
|
202
|
+
groupEntries = allEntries;
|
|
203
|
+
}
|
|
204
|
+
// Percentages use the SUM of all group values as the denominator, so the
|
|
205
|
+
// bar segments (flex-grow by value) and the printed % agree exactly and
|
|
206
|
+
// both reflect "share of all usage" (by tokens or by cost).
|
|
207
|
+
const groupSum = groupEntries.reduce((s, g) => s + (Number(g[shareBy]) || 0), 0);
|
|
208
|
+
const groupMax = Math.max(...groupEntries.map((g) => (Number(g[shareBy]) || 0)), 1);
|
|
209
|
+
|
|
210
|
+
// heatmap: build days from byDay within range. byDay entries are either
|
|
211
|
+
// {total, cost, ...} objects (new) or a bare number (legacy), so normalize.
|
|
212
|
+
const byDay = stats.byDay || {};
|
|
213
|
+
const todayD = new Date();
|
|
214
|
+
const cells = [];
|
|
215
|
+
for (let i = range - 1; i >= 0; i--) {
|
|
216
|
+
const d = new Date(todayD); d.setDate(d.getDate() - i);
|
|
217
|
+
const key = d.toISOString().slice(0, 10);
|
|
218
|
+
const rec = byDay[key];
|
|
219
|
+
const total = (rec && typeof rec === "object") ? Number(rec.total) || 0 : Number(rec) || 0;
|
|
220
|
+
const cost = (rec && typeof rec === "object") ? Number(rec.cost) || 0 : 0;
|
|
221
|
+
cells.push({ key, val: total, cost });
|
|
222
|
+
}
|
|
223
|
+
const heatMax = Math.max(1, ...cells.map((c) => c.val));
|
|
224
|
+
// group cells into weeks for the grid (7 per week)
|
|
225
|
+
const weeks = [];
|
|
226
|
+
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
|
|
227
|
+
|
|
228
|
+
// models table rows
|
|
229
|
+
const modelRows = Object.keys(stats.byModel || {})
|
|
230
|
+
.map((m) => ({ key: m, ...stats.byModel[m] }))
|
|
231
|
+
.sort((a, b) => b.total - a.total);
|
|
232
|
+
|
|
233
|
+
// recent sessions: sort by last activity, take newest N
|
|
234
|
+
const sessionRows = Object.keys(stats.bySession || {})
|
|
235
|
+
.map((id) => ({ key: id, ...stats.bySession[id] }))
|
|
236
|
+
.sort((a, b) => (b.last || 0) - (a.last || 0))
|
|
237
|
+
.slice(0, 12);
|
|
238
|
+
function fmtDate(ts) {
|
|
239
|
+
if (!ts) return "-";
|
|
240
|
+
const d = new Date(ts);
|
|
241
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
242
|
+
return (d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---- render ----
|
|
246
|
+
const balanceInner = balanceLoading ? jsx("div", { style: { color: "var(--dsw-alias-label-secondary)" }, children: "加载中…" })
|
|
247
|
+
: (balance && balance.status === "ok")
|
|
248
|
+
? jsxs("div", { children: [
|
|
249
|
+
jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "6px" }, children: [
|
|
250
|
+
jsx("span", { style: { ...NUM, ...MONO }, children: (balCur === "CNY" ? "¥" : "$") + fmtMoney(balValue) }),
|
|
251
|
+
jsx("span", { style: LABEL, children: balCur })
|
|
252
|
+
] }),
|
|
253
|
+
jsxs("div", { style: { marginTop: "8px" }, children: [
|
|
254
|
+
jsx("button", { onClick: () => loadBalance(true), style: { cursor: "pointer", fontSize: "12px", padding: "4px 10px", borderRadius: "6px", border: "1px solid var(--dsw-alias-border-l1)", background: "transparent", color: "var(--dsw-alias-label-primary)" }, children: "强制刷新" }),
|
|
255
|
+
balance.cached ? jsx("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)", marginLeft: "8px" }, children: "缓存" }) : null
|
|
256
|
+
] })
|
|
257
|
+
] })
|
|
258
|
+
: jsx("div", { style: { color: "var(--dsw-alias-label-secondary)" }, children: (balance?.message || "未配置或不可用") });
|
|
259
|
+
|
|
260
|
+
const statCards = [
|
|
261
|
+
{ label: "今日", val: today },
|
|
262
|
+
{ label: "本月", val: thisMonth },
|
|
263
|
+
{ label: "总计", val: total }
|
|
264
|
+
];
|
|
265
|
+
const statBar = jsxs("div", { style: { display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: "10px" }, children: statCards.map((s) =>
|
|
266
|
+
jsxs("div", { style: { background: "var(--dsw-alias-interactive-bg-active, rgba(127,127,127,.08))", borderRadius: "10px", padding: "12px" }, children: [
|
|
267
|
+
jsx("div", { style: LABEL, children: s.label }),
|
|
268
|
+
jsx("div", { style: { ...NUM, ...MONO, fontSize: "22px" }, children: fmt(s.val) })
|
|
269
|
+
] }, s.label)
|
|
270
|
+
) });
|
|
271
|
+
|
|
272
|
+
const shareBar = jsxs("div", { children: [
|
|
273
|
+
jsxs("div", { style: { display: "flex", height: "12px", borderRadius: "6px", overflow: "hidden", gap: "2px", backgroundColor: "var(--dsw-alias-border-l1)" }, children: groupEntries.map((g, i) =>
|
|
274
|
+
// flex-grow proportional to the chosen share field (tokens or cost).
|
|
275
|
+
jsx("div", { style: { flex: (Number(g[shareBy]) || 0) + " 0 0", background: groupColor(g.key, i) }, title: g.key + " " + (shareMetric === "cost" ? ("¥" + fmtMoney(g.cost)) : fmt(g.total)) }, g.key)
|
|
276
|
+
) }),
|
|
277
|
+
jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px", marginTop: "10px" }, children: groupEntries.map((g, i) =>
|
|
278
|
+
jsxs("div", { style: ROW, children: [
|
|
279
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: "0" }, children: [
|
|
280
|
+
jsx("div", { style: { width: "10px", height: "10px", borderRadius: "2px", flexShrink: "0", background: groupColor(g.key, i) } }),
|
|
281
|
+
jsx("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: "240px", color: "var(--dsw-alias-label-primary)", fontSize: "13px" }, children: g.key })
|
|
282
|
+
] }),
|
|
283
|
+
jsxs("div", { style: { display: "flex", gap: "14px", color: "var(--dsw-alias-label-secondary)", ...MONO, fontSize: "13px", flexShrink: "0" }, children: [
|
|
284
|
+
jsx("span", { children: (shareMetric === "cost" ? ("¥" + fmtMoney(g.cost)) : fmt(g.total)) }),
|
|
285
|
+
jsx("span", { style: { color: "var(--dsw-alias-label-tertiary)" }, children: pct((Number(g[shareBy]) || 0), groupSum).toFixed(0) + "%" })
|
|
286
|
+
] })
|
|
287
|
+
] }, g.key)
|
|
288
|
+
) })
|
|
289
|
+
] });
|
|
290
|
+
|
|
291
|
+
// heatmap grid (weeks as columns is complex; render day cells in a wrap grid)
|
|
292
|
+
const heatGrid = jsxs("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(14px, 1fr))", gap: "3px" }, children: cells.map((c) =>
|
|
293
|
+
jsx("div", { style: { width: "14px", height: "14px", borderRadius: "2px", background: heatColor(c.val, heatMax) }, title: c.key + ": " + fmt(c.val) }, c.key)
|
|
294
|
+
) });
|
|
295
|
+
|
|
296
|
+
// --- trend chart: a lightweight SVG polyline of daily metric over `range`.
|
|
297
|
+
// metric = "cost" or "token". Full mini area chart (grid + axes + fill).
|
|
298
|
+
const trendValues = cells.map((c) => (metric === "cost" ? c.cost : c.val));
|
|
299
|
+
const tMax = Math.max(1, ...trendValues);
|
|
300
|
+
// viewBox: leave room on left for y-axis labels and bottom for x-axis dates.
|
|
301
|
+
const TW = 320, TH = 120, PAD = 6, PAD_LEFT = 34, PAD_BOTTOM = 22, PAD_TOP = 12;
|
|
302
|
+
const plotW = TW - PAD_LEFT - PAD;
|
|
303
|
+
const plotH = TH - PAD_TOP - PAD_BOTTOM;
|
|
304
|
+
let trendSvg;
|
|
305
|
+
if (trendValues.length <= 1) {
|
|
306
|
+
trendSvg = jsx("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "8px 0" }, children: "数据不足" });
|
|
307
|
+
} else {
|
|
308
|
+
const step = plotW / (trendValues.length - 1);
|
|
309
|
+
const xAt = (i) => PAD_LEFT + i * step;
|
|
310
|
+
const yAt = (v) => PAD_TOP + plotH - (v / tMax) * plotH;
|
|
311
|
+
const linePts = trendValues.map((v, i) => (xAt(i).toFixed(1) + "," + yAt(v).toFixed(1))).join(" ");
|
|
312
|
+
// area polygon = line points + bottom-right + bottom-left to close the fill.
|
|
313
|
+
const areaPts = linePts + " " + (PAD_LEFT + plotW).toFixed(1) + "," + (PAD_TOP + plotH).toFixed(1) + " " + PAD_LEFT.toFixed(1) + "," + (PAD_TOP + plotH).toFixed(1);
|
|
314
|
+
|
|
315
|
+
// grid: 3 horizontal lines at 0, 50%, 100% of max.
|
|
316
|
+
const gridLines = [0, 0.5, 1].map((f) => {
|
|
317
|
+
const gy = PAD_TOP + plotH - f * plotH;
|
|
318
|
+
return jsx("line", { x1: PAD_LEFT, x2: PAD_LEFT + plotW, y1: gy, y2: gy, stroke: "var(--dsw-alias-border-l1)", strokeWidth: "1", strokeDasharray: "2 3" });
|
|
319
|
+
});
|
|
320
|
+
// y-axis labels (0, mid, max) at left.
|
|
321
|
+
const yLabels = [0, 0.5, 1].map((f) => {
|
|
322
|
+
const val = (tMax * f);
|
|
323
|
+
const gy = PAD_TOP + plotH - f * plotH;
|
|
324
|
+
const lbl = (metric === "cost" ? ("¥" + (val >= 1 ? val.toFixed(0) : val.toFixed(1))) : (val >= 1000000 ? (val / 1000000).toFixed(0) + "M" : (val >= 1000 ? (val / 1000).toFixed(0) + "K" : val.toFixed(0))));
|
|
325
|
+
return jsx("text", { x: PAD_LEFT - 4, y: gy + 3, fill: "var(--dsw-alias-label-tertiary)", fontSize: "8", textAnchor: "end", children: lbl });
|
|
326
|
+
});
|
|
327
|
+
// x-axis dates: first, middle, last (and every ~quarter).
|
|
328
|
+
const dateIdx = [0, Math.floor((trendValues.length - 1) / 3), Math.floor(((trendValues.length - 1) * 2) / 3), trendValues.length - 1];
|
|
329
|
+
const xLabels = dateIdx.map((idx, di) => {
|
|
330
|
+
const c = cells[idx];
|
|
331
|
+
const label = c && c.key ? String(c.key).slice(5) : "";
|
|
332
|
+
const lx = xAt(idx);
|
|
333
|
+
const ly = TH - PAD_BOTTOM + 15;
|
|
334
|
+
// clear backdrop so date text reads against the dark card
|
|
335
|
+
return jsxs("g", { key: "x" + di, children: [
|
|
336
|
+
jsx("rect", { x: lx - 15, y: ly - 10, width: 30, height: 14, rx: 3, fill: "var(--dsw-alias-bg-overlay, #2a2a2a)", opacity: "0.9" }),
|
|
337
|
+
jsx("text", { x: lx, y: ly, fill: "var(--dsw-alias-label-primary)", fontSize: "9", fontWeight: 500, textAnchor: "middle", children: label })
|
|
338
|
+
] });
|
|
339
|
+
});
|
|
340
|
+
// data points (small circles) + highlight last point.
|
|
341
|
+
const dotPts = trendValues.map((v, i) => jsx("circle", { key: "d" + i, cx: xAt(i), cy: yAt(v), r: "1.8", fill: "var(--dsw-alias-label-primary)", opacity: "0.5" }));
|
|
342
|
+
const lastIdx = trendValues.length - 1;
|
|
343
|
+
const lastV = trendValues[lastIdx];
|
|
344
|
+
|
|
345
|
+
// hover: transparent overlay computes nearest day from mouse x.
|
|
346
|
+
const overlay = jsx("rect", {
|
|
347
|
+
x: PAD_LEFT, y: PAD_TOP, width: plotW, height: plotH, fill: "transparent",
|
|
348
|
+
onMouseMove: (ev) => {
|
|
349
|
+
const rect = ev.currentTarget.getBoundingClientRect();
|
|
350
|
+
const relX = (((ev.clientX - rect.left) / rect.width) * plotW);
|
|
351
|
+
const idx = Math.round(relX / step);
|
|
352
|
+
const clamped = Math.max(0, Math.min(trendValues.length - 1, idx));
|
|
353
|
+
if (clamped !== hoverIdx) setHoverIdx(clamped);
|
|
354
|
+
},
|
|
355
|
+
onMouseLeave: () => setHoverIdx(null),
|
|
356
|
+
style: { cursor: "crosshair" }
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// hover indicator: vertical line + highlighted point + tooltip.
|
|
360
|
+
let hoverMarkers = null;
|
|
361
|
+
if (hoverIdx !== null && cells[hoverIdx]) {
|
|
362
|
+
const hx = xAt(hoverIdx);
|
|
363
|
+
const hy = yAt(trendValues[hoverIdx]);
|
|
364
|
+
const hv = trendValues[hoverIdx];
|
|
365
|
+
const hc = cells[hoverIdx];
|
|
366
|
+
const tooltipLabel = (metric === "cost" ? ("¥" + fmtMoney(hv)) : fmt(hv));
|
|
367
|
+
// Place the tooltip box above the point when there is room, else below it,
|
|
368
|
+
// clamping within the plot so tall points never push it off the chart.
|
|
369
|
+
const boxW = 64, boxH = 18;
|
|
370
|
+
let boxTop = hy - (boxH + 8);
|
|
371
|
+
if (boxTop < PAD_TOP) boxTop = hy + 10; // too high → place below
|
|
372
|
+
const boxLeft = Math.max(PAD_LEFT, Math.min(PAD_LEFT + plotW - boxW, hx - boxW / 2));
|
|
373
|
+
const dateLbl = String(hc.key).slice(5);
|
|
374
|
+
hoverMarkers = jsxs("g", { children: [
|
|
375
|
+
jsx("line", { x1: hx, x2: hx, y1: PAD_TOP, y2: PAD_TOP + plotH, stroke: "var(--dsw-alias-label-secondary)", strokeWidth: "1", strokeDasharray: "3 3" }),
|
|
376
|
+
jsx("circle", { cx: hx, cy: hy, r: "4", fill: "var(--dsw-alias-label-primary)", stroke: "var(--dsw-alias-bg-overlay, #222)", strokeWidth: "1" }),
|
|
377
|
+
// tooltip box (auto-placed, clear backdrop for readability)
|
|
378
|
+
jsx("g", { children: [
|
|
379
|
+
jsx("rect", { x: boxLeft, y: boxTop, width: boxW, height: boxH, rx: 4, fill: "var(--dsw-alias-bg-overlay, #2a2a2a)", stroke: "var(--dsw-alias-border-l2)", strokeWidth: "1" }),
|
|
380
|
+
jsx("text", { x: boxLeft + boxW / 2, y: boxTop + boxH / 2 + 3, fill: "var(--dsw-alias-label-primary)", fontSize: "9", fontWeight: 600, textAnchor: "middle", children: tooltipLabel }),
|
|
381
|
+
jsx("text", { x: boxLeft + boxW / 2, y: boxTop + boxH + 12, fill: "var(--dsw-alias-label-accent, #4dd0e1)", fontSize: "9", fontWeight: 600, textAnchor: "middle", children: dateLbl })
|
|
382
|
+
] })
|
|
383
|
+
] });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
trendSvg = jsxs("svg", { width: "100%", viewBox: "0 0 " + TW + " " + TH, style: { display: "block", maxHeight: "150px", width: "100%" }, children: [
|
|
387
|
+
jsx("defs", { children: jsx("linearGradient", { id: "tdfill", x1: "0", y1: "0", x2: "0", y2: "1", children: [
|
|
388
|
+
jsx("stop", { offset: "0%", stopColor: "var(--dsw-alias-label-accent, #4caf50)", stopOpacity: "0.35" }),
|
|
389
|
+
jsx("stop", { offset: "100%", stopColor: "var(--dsw-alias-label-accent, #4caf50)", stopOpacity: "0.02" })
|
|
390
|
+
] })
|
|
391
|
+
}),
|
|
392
|
+
gridLines,
|
|
393
|
+
jsx("polygon", { points: areaPts, fill: "url(#tdfill)" }),
|
|
394
|
+
jsx("polyline", { points: linePts, fill: "none", stroke: "var(--dsw-alias-label-primary)", strokeWidth: "2", strokeLinejoin: "round", strokeLinecap: "round" }),
|
|
395
|
+
dotPts,
|
|
396
|
+
jsx("circle", { cx: xAt(lastIdx), cy: yAt(lastV), r: "3", fill: "var(--dsw-alias-label-primary)" }),
|
|
397
|
+
yLabels,
|
|
398
|
+
xLabels,
|
|
399
|
+
hoverMarkers,
|
|
400
|
+
overlay,
|
|
401
|
+
jsx("text", { x: PAD_LEFT, y: PAD_TOP - 2, fill: "var(--dsw-alias-label-tertiary)", fontSize: "9", children: (metric === "cost" ? ("¥" + fmtMoney(lastV)) : fmt(lastV)) + " 最新" })
|
|
402
|
+
] });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const modelTable = jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: "13px" }, children: [
|
|
406
|
+
jsxs("thead", { children: [jsxs("tr", { children: [
|
|
407
|
+
jsx("th", { style: thStyle, children: "模型" }), jsx("th", { style: thR, children: "Req" }),
|
|
408
|
+
jsx("th", { style: thR, children: "Input" }), jsx("th", { style: thR, children: "Cache" }),
|
|
409
|
+
jsx("th", { style: thR, children: "Output" }), jsx("th", { style: thR, children: "Cost" })
|
|
410
|
+
] })] }),
|
|
411
|
+
jsxs("tbody", { children: modelRows.map((r) => jsxs("tr", { style: { borderTop: "1px solid var(--dsw-alias-border-l1)" }, children: [
|
|
412
|
+
jsx("td", { style: tdStyle, children: r.key }),
|
|
413
|
+
jsx("td", { style: tdR, children: r.req }),
|
|
414
|
+
jsx("td", { style: tdR, children: fmt(r.input) }),
|
|
415
|
+
jsx("td", { style: tdR, children: fmt(r.cacheRead) }),
|
|
416
|
+
jsx("td", { style: tdR, children: fmt(r.output) }),
|
|
417
|
+
jsx("td", { style: { ...tdR, color: "var(--dsw-alias-label-primary)", fontWeight: 600 }, children: "¥" + fmtMoney(r.cost || 0) })
|
|
418
|
+
] }, r.key)) })
|
|
419
|
+
] });
|
|
420
|
+
|
|
421
|
+
return jsxs("div", { children: [
|
|
422
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", marginBottom: "6px", flexWrap: "wrap" }, children: [
|
|
423
|
+
jsx("h3", { style: { fontSize: "18px", fontWeight: 700, color: "var(--dsw-alias-label-primary)", marginBottom: "0" }, children: "用量看板" }),
|
|
424
|
+
jsx(Seg, { options: [{ label: "近7天", value: 7 }, { label: "近30天", value: 30 }, { label: "近90天", value: 90 }, { label: "全部", value: 0 }], value: days, onChange: (v) => { setDays(v); setLoading(true); } })
|
|
425
|
+
] }),
|
|
426
|
+
jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: "8px", marginBottom: "4px" }, children: [
|
|
427
|
+
jsx("button", { onClick: () => exportCsv(stats, metric), style: { ...btnStyle }, children: "导出 CSV" }),
|
|
428
|
+
jsx("button", { onClick: () => exportJson(stats), style: { ...btnStyle }, children: "导出 JSON" })
|
|
429
|
+
] }),
|
|
430
|
+
(error) ? jsx("div", { style: { color: "var(--dsw-alias-danger, #e5484d)", fontSize: "12px", margin: "6px 0" }, children: "加载出错:" + error }) : null,
|
|
431
|
+
|
|
432
|
+
// Balance
|
|
433
|
+
jsxs("div", { style: CARD, children: [
|
|
434
|
+
jsx("div", { style: SEC_TITLE, children: "💰 账户余额(DeepSeek 官方)" }), balanceInner
|
|
435
|
+
] }),
|
|
436
|
+
|
|
437
|
+
// Token usage
|
|
438
|
+
jsxs("div", { style: CARD, children: [
|
|
439
|
+
jsx("div", { style: SEC_TITLE, children: "📊 Token 使用量" }),
|
|
440
|
+
statBar,
|
|
441
|
+
jsxs("div", { style: { ...ROW, marginTop: "10px" }, children: [
|
|
442
|
+
jsx("span", { children: "今日花费" }), jsx("span", { ...MONO, children: "¥" + fmtMoney(stats.todayCost) })
|
|
443
|
+
] }),
|
|
444
|
+
jsxs("div", { style: ROW, children: [
|
|
445
|
+
jsx("span", { children: "本月花费" }), jsx("span", { ...MONO, children: "¥" + fmtMoney(stats.thisMonthCost) })
|
|
446
|
+
] }),
|
|
447
|
+
jsxs("div", { style: ROW, children: [
|
|
448
|
+
jsx("span", { children: "总花费" }), jsx("span", { ...MONO, children: "¥" + fmtMoney(stats.cost) })
|
|
449
|
+
] }),
|
|
450
|
+
jsxs("div", { style: { ...ROW, marginTop: "6px", borderTop: "1px solid var(--dsw-alias-border-l1)", paddingTop: "6px" }, children: [
|
|
451
|
+
jsx("span", { children: "请求数" }), jsx("span", { ...MONO, children: fmt(req) })
|
|
452
|
+
] }),
|
|
453
|
+
jsxs("div", { style: ROW, children: [
|
|
454
|
+
jsx("span", { children: "缓存命中率" }), jsx("span", { ...MONO, children: cacheRate + "%" })
|
|
455
|
+
] }),
|
|
456
|
+
jsxs("div", { style: ROW, children: [
|
|
457
|
+
jsx("span", { children: "会话数" }), jsx("span", { ...MONO, children: Number(stats.sessions) || 0 })
|
|
458
|
+
] })
|
|
459
|
+
] }),
|
|
460
|
+
|
|
461
|
+
// Grouped share
|
|
462
|
+
jsxs("div", { style: CARD, children: [
|
|
463
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", marginBottom: "8px" }, children: [
|
|
464
|
+
jsx("div", { style: SEC_TITLE, children: "📈 用量分布" }),
|
|
465
|
+
jsx(Seg, { options: [{ label: "按模型", value: "model" }, { label: "按工作区", value: "workspace" }], value: groupBy, onChange: setGroupBy })
|
|
466
|
+
] }),
|
|
467
|
+
jsxs("div", { style: { display: "flex", justifyContent: "flex-end", marginBottom: "8px" }, children: [
|
|
468
|
+
jsx(Seg, { options: [{ label: "按 Token", value: "total" }, { label: "按成本", value: "cost" }], value: shareMetric, onChange: setShareMetric })
|
|
469
|
+
] }),
|
|
470
|
+
shareBar
|
|
471
|
+
] }),
|
|
472
|
+
|
|
473
|
+
// Activity heatmap
|
|
474
|
+
jsxs("div", { style: CARD, children: [
|
|
475
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px" }, children: [
|
|
476
|
+
jsx("div", { style: SEC_TITLE, children: "🗓 活跃度" }),
|
|
477
|
+
jsx(Seg, { options: [{ label: "30天", value: 30 }, { label: "90天", value: 90 }, { label: "180天", value: 180 }], value: range, onChange: setRange })
|
|
478
|
+
] }),
|
|
479
|
+
heatGrid
|
|
480
|
+
] }),
|
|
481
|
+
|
|
482
|
+
// Trend chart
|
|
483
|
+
jsxs("div", { style: CARD, children: [
|
|
484
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px" }, children: [
|
|
485
|
+
jsx("div", { style: SEC_TITLE, children: "📉 趋势" }),
|
|
486
|
+
jsx(Seg, { options: [{ label: "成本", value: "cost" }, { label: "Token", value: "token" }], value: metric, onChange: setMetric })
|
|
487
|
+
] }),
|
|
488
|
+
trendSvg
|
|
489
|
+
] }),
|
|
490
|
+
|
|
491
|
+
// Models table
|
|
492
|
+
jsxs("div", { style: CARD, children: [
|
|
493
|
+
jsx("div", { style: SEC_TITLE, children: "🤖 按模型" }),
|
|
494
|
+
modelTable
|
|
495
|
+
] }),
|
|
496
|
+
|
|
497
|
+
// Recent sessions
|
|
498
|
+
jsxs("div", { style: CARD, children: [
|
|
499
|
+
jsx("div", { style: SEC_TITLE, children: "🗂 最近会话" }),
|
|
500
|
+
sessionRows.length === 0
|
|
501
|
+
? jsx("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "6px 0" }, children: "暂无数据" })
|
|
502
|
+
: jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: sessionRows.map((r) =>
|
|
503
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "6px 0", borderTop: "1px solid var(--dsw-alias-border-l1)" }, children: [
|
|
504
|
+
jsxs("div", { style: { display: "flex", flexDirection: "column", minWidth: "0", flex: "1" }, children: [
|
|
505
|
+
jsx("span", { style: { color: "var(--dsw-alias-label-primary)", fontSize: "12px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: (r.model || "会话").slice(0, 40) + " · " + fmtDate(r.last) }),
|
|
506
|
+
jsx("span", { style: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: r.id })
|
|
507
|
+
] }),
|
|
508
|
+
jsxs("div", { style: { display: "flex", gap: "14px", color: "var(--dsw-alias-label-secondary)", ...MONO, fontSize: "12px", flexShrink: "0" }, children: [
|
|
509
|
+
jsx("span", { children: fmt(r.total) }),
|
|
510
|
+
jsx("span", { style: { color: "var(--dsw-alias-label-primary)", fontWeight: 600 }, children: "¥" + fmtMoney(r.cost) })
|
|
511
|
+
] })
|
|
512
|
+
] }, r.id)
|
|
513
|
+
) })
|
|
514
|
+
] })
|
|
515
|
+
] });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// loading / no data -> simple spinner-ish text
|
|
519
|
+
return jsxs("div", { children: [
|
|
520
|
+
jsx("h3", { style: { fontSize: "18px", fontWeight: 700, color: "var(--dsw-alias-label-primary)", marginBottom: "12px" }, children: "用量看板" }),
|
|
521
|
+
(error) ? jsx("div", { style: { color: "var(--dsw-alias-danger, #e5484d)", fontSize: "12px", margin: "6px 0" }, children: "加载出错:" + error }) : null,
|
|
522
|
+
jsx("div", { style: { color: "var(--dsw-alias-label-secondary)", padding: "8px 0" }, children: "加载中…" })
|
|
523
|
+
] });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const thStyle = { textAlign: "left", padding: "8px 6px", color: "var(--dsw-alias-label-tertiary)", fontWeight: 500, fontSize: "12px" };
|
|
527
|
+
const thR = { textAlign: "right", padding: "8px 6px", color: "var(--dsw-alias-label-tertiary)", fontWeight: 500, fontSize: "12px", ...MONO };
|
|
528
|
+
const tdStyle = { padding: "8px 6px", color: "var(--dsw-alias-label-primary)" };
|
|
529
|
+
const tdR = { textAlign: "right", padding: "8px 6px", color: "var(--dsw-alias-label-secondary)", ...MONO };
|
|
530
|
+
|
|
531
|
+
function apply(ctx) {
|
|
532
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register(
|
|
533
|
+
{
|
|
534
|
+
name: "settings.section",
|
|
535
|
+
id: "token-dashboard",
|
|
536
|
+
order: 30,
|
|
537
|
+
label: () => {
|
|
538
|
+
try {
|
|
539
|
+
const bound = ctx.locale && typeof ctx.locale.bind === "function" && ctx.locale.bind("token-dashboard");
|
|
540
|
+
const v = bound && typeof bound === "function" ? bound("title") : null;
|
|
541
|
+
return (typeof v === "string" && v !== "title") ? v : "用量看板";
|
|
542
|
+
} catch {
|
|
543
|
+
return "用量看板";
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
inject: () => ({})
|
|
547
|
+
},
|
|
548
|
+
(props) => jsx(Boundary, { children: jsx(TokenDashboardPanel, props) })
|
|
549
|
+
));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
exports.apply = apply;
|
|
553
|
+
exports.inject = ["slots", "locale"];
|
|
554
|
+
exports.TokenDashboardPanel = TokenDashboardPanel;
|
|
555
|
+
exports.Boundary = Boundary;
|
|
556
|
+
exports.fmt = fmt;
|
|
557
|
+
exports.fmtMoney = fmtMoney;
|
|
558
|
+
|
|
559
|
+
return module.exports;
|
|
560
|
+
}
|
|
561
|
+
});
|