@nexus-aethra/dshell-usage 0.1.6
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/LICENSE +21 -0
- package/lib/client/heatmap.js +96 -0
- package/lib/client/heatmap.js.map +1 -0
- package/lib/client/index.js +36 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/locales.js +95 -0
- package/lib/client/locales.js.map +1 -0
- package/lib/client/nav-icon.js +88 -0
- package/lib/client/nav-icon.js.map +1 -0
- package/lib/client/page.js +289 -0
- package/lib/client/page.js.map +1 -0
- package/lib/client.js +1146 -0
- package/lib/client.js.map +1 -0
- package/lib/index-store.js +250 -0
- package/lib/index-store.js.map +1 -0
- package/lib/index.js +41 -0
- package/lib/index.js.map +1 -0
- package/lib/protocol.js +9 -0
- package/lib/protocol.js.map +1 -0
- package/lib/route.js +63 -0
- package/lib/route.js.map +1 -0
- package/lib/scan.js +187 -0
- package/lib/scan.js.map +1 -0
- package/lib/types/client/heatmap.d.ts +69 -0
- package/lib/types/client/heatmap.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +62 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/nav-icon.d.ts +28 -0
- package/lib/types/client/nav-icon.d.ts.map +1 -0
- package/lib/types/client/page.d.ts +18 -0
- package/lib/types/client/page.d.ts.map +1 -0
- package/lib/types/index-store.d.ts +87 -0
- package/lib/types/index-store.d.ts.map +1 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/protocol.d.ts +115 -0
- package/lib/types/protocol.d.ts.map +1 -0
- package/lib/types/route.d.ts +19 -0
- package/lib/types/route.d.ts.map +1 -0
- package/lib/types/scan.d.ts +94 -0
- package/lib/types/scan.d.ts.map +1 -0
- package/package.json +73 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1146 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@nexus-aethra/dshell-usage",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
9
|
+
//#endregion
|
|
10
|
+
let react = require("react");
|
|
11
|
+
//#region lib/client/locales.js
|
|
12
|
+
/**
|
|
13
|
+
* `dshellUsage` namespace dictionaries, and the namespace's declaration.
|
|
14
|
+
*
|
|
15
|
+
* The namespace merge lives with its key set so that any module naming
|
|
16
|
+
* `TranslateNS<'dshellUsage'>` or `PropsLocale<'dshellUsage'>` needs only this
|
|
17
|
+
* file. Keys are lowercase and dotted: `nav.*` (the settings navigation row),
|
|
18
|
+
* `range.*` (the window selector), `chart.*` (the two charts' captions),
|
|
19
|
+
* `table.*` (the route breakdown), and `state.*` (empty, busy, freshness, and
|
|
20
|
+
* the load failure).
|
|
21
|
+
*/
|
|
22
|
+
/** Simplified Chinese dictionary and key-set source of truth. */
|
|
23
|
+
const zh = {
|
|
24
|
+
"nav": "用量",
|
|
25
|
+
"title": "模型 token 消耗",
|
|
26
|
+
"subtitle": "按模型汇总每个会话上报的用量。索引建在 dshell 自己的数据目录里,首次打开会扫一遍全部历史会话,之后只补新增的部分。",
|
|
27
|
+
"range.7": "近 7 天",
|
|
28
|
+
"range.30": "近 30 天",
|
|
29
|
+
"range.all": "全部",
|
|
30
|
+
"range.label": "时间范围",
|
|
31
|
+
"rebuild": "重新聚合",
|
|
32
|
+
"rebuild.busy": "正在聚合…",
|
|
33
|
+
"chart.heatmap": "每日热力(近 26 周内)",
|
|
34
|
+
"chart.heatmapDay": "{day} · {total} tok · {turns} 次调用",
|
|
35
|
+
"chart.heatmapLess": "少",
|
|
36
|
+
"chart.heatmapMore": "多",
|
|
37
|
+
"chart.heatmapPeak": "最忙一天 {total} tok",
|
|
38
|
+
"chart.heatmapTruncated": "只画最近 26 周",
|
|
39
|
+
"chart.curve": "每日消耗(按模型)",
|
|
40
|
+
"chart.pie": "模型占比",
|
|
41
|
+
"summary.total": "合计",
|
|
42
|
+
"summary.uncached": "未缓存输入",
|
|
43
|
+
"summary.output": "输出",
|
|
44
|
+
"summary.cacheRead": "缓存读",
|
|
45
|
+
"summary.turns": "调用",
|
|
46
|
+
"summary.hitRate": "缓存命中率",
|
|
47
|
+
"table.title": "按模型明细",
|
|
48
|
+
"table.model": "模型",
|
|
49
|
+
"table.input": "输入",
|
|
50
|
+
"table.output": "输出",
|
|
51
|
+
"table.cacheRead": "缓存读",
|
|
52
|
+
"table.cacheWrite": "缓存写",
|
|
53
|
+
"table.turns": "调用",
|
|
54
|
+
"table.total": "合计",
|
|
55
|
+
"state.empty.title": "还没有可统计的用量",
|
|
56
|
+
"state.empty.body": "索引里没有任何一条带用量上报的模型调用。跑一次 agent 会话后再点「重新聚合」。",
|
|
57
|
+
"state.freshness": "索引更新于 {day}",
|
|
58
|
+
"state.skipped": "有 {count} 个会话的日志读不了,未计入(详情见悬停提示)。",
|
|
59
|
+
"state.scanned": "本次扫描列出 {sessions} 个会话,读取了 {read} 个,命中 {turns} 次调用。",
|
|
60
|
+
"state.error": "读取用量失败:{reason}",
|
|
61
|
+
"state.loading": "正在读取索引…"
|
|
62
|
+
};
|
|
63
|
+
/** English dictionary, keyed by the same set. */
|
|
64
|
+
const en = {
|
|
65
|
+
"nav": "Usage",
|
|
66
|
+
"title": "Model token usage",
|
|
67
|
+
"subtitle": "Per-model totals folded from the usage every session reports. The index lives in dshell's own data directory: the first visit scans every session once, and later visits add only what is new.",
|
|
68
|
+
"range.7": "Last 7 days",
|
|
69
|
+
"range.30": "Last 30 days",
|
|
70
|
+
"range.all": "All time",
|
|
71
|
+
"range.label": "Range",
|
|
72
|
+
"rebuild": "Rebuild",
|
|
73
|
+
"rebuild.busy": "Rebuilding…",
|
|
74
|
+
"chart.heatmap": "Daily heatmap (last 26 weeks)",
|
|
75
|
+
"chart.heatmapDay": "{day} · {total} tok · {turns} calls",
|
|
76
|
+
"chart.heatmapLess": "Less",
|
|
77
|
+
"chart.heatmapMore": "More",
|
|
78
|
+
"chart.heatmapPeak": "Busiest day {total} tok",
|
|
79
|
+
"chart.heatmapTruncated": "Showing the last 26 weeks",
|
|
80
|
+
"chart.curve": "Daily usage by model",
|
|
81
|
+
"chart.pie": "Share by model",
|
|
82
|
+
"summary.total": "Total",
|
|
83
|
+
"summary.uncached": "Uncached input",
|
|
84
|
+
"summary.output": "Output",
|
|
85
|
+
"summary.cacheRead": "Cache read",
|
|
86
|
+
"summary.turns": "Calls",
|
|
87
|
+
"summary.hitRate": "Cache hit rate",
|
|
88
|
+
"table.title": "By model",
|
|
89
|
+
"table.model": "Model",
|
|
90
|
+
"table.input": "Input",
|
|
91
|
+
"table.output": "Output",
|
|
92
|
+
"table.cacheRead": "Cache read",
|
|
93
|
+
"table.cacheWrite": "Cache write",
|
|
94
|
+
"table.turns": "Calls",
|
|
95
|
+
"table.total": "Total",
|
|
96
|
+
"state.empty.title": "No usage to count yet",
|
|
97
|
+
"state.empty.body": "The index holds no model call that reported usage. Run an agent session, then press Rebuild.",
|
|
98
|
+
"state.freshness": "Index updated {day}",
|
|
99
|
+
"state.skipped": "{count} session logs could not be read and are not counted (hover for the reason).",
|
|
100
|
+
"state.scanned": "This scan listed {sessions} sessions, read {read}, and counted {turns} calls.",
|
|
101
|
+
"state.error": "Reading usage failed: {reason}",
|
|
102
|
+
"state.loading": "Reading the index…"
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region ../../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.js
|
|
106
|
+
/**
|
|
107
|
+
* @license React
|
|
108
|
+
* react-jsx-runtime.production.min.js
|
|
109
|
+
*
|
|
110
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
111
|
+
*
|
|
112
|
+
* This source code is licensed under the MIT license found in the
|
|
113
|
+
* LICENSE file in the root directory of this source tree.
|
|
114
|
+
*/
|
|
115
|
+
var require_react_jsx_runtime_production_min = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
116
|
+
var f = require("react");
|
|
117
|
+
var k = Symbol.for("react.element");
|
|
118
|
+
var m = Object.prototype.hasOwnProperty;
|
|
119
|
+
var n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
|
|
120
|
+
var p = {
|
|
121
|
+
key: !0,
|
|
122
|
+
ref: !0,
|
|
123
|
+
__self: !0,
|
|
124
|
+
__source: !0
|
|
125
|
+
};
|
|
126
|
+
function q(c, a, g) {
|
|
127
|
+
var b, d = {}, e = null, h = null;
|
|
128
|
+
void 0 !== g && (e = "" + g);
|
|
129
|
+
void 0 !== a.key && (e = "" + a.key);
|
|
130
|
+
void 0 !== a.ref && (h = a.ref);
|
|
131
|
+
for (b in a) m.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
|
|
132
|
+
if (c && c.defaultProps) for (b in a = c.defaultProps, a) void 0 === d[b] && (d[b] = a[b]);
|
|
133
|
+
return {
|
|
134
|
+
$$typeof: k,
|
|
135
|
+
type: c,
|
|
136
|
+
key: e,
|
|
137
|
+
ref: h,
|
|
138
|
+
props: d,
|
|
139
|
+
_owner: n.current
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
exports.jsx = q;
|
|
143
|
+
exports.jsxs = q;
|
|
144
|
+
}));
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region ../../../node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js
|
|
147
|
+
var require_jsx_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
148
|
+
module.exports = require_react_jsx_runtime_production_min();
|
|
149
|
+
}));
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region ../std/lib/contracts.js
|
|
152
|
+
/**
|
|
153
|
+
* The usage index route: one exact `/api` endpoint behind dsh's existing trust
|
|
154
|
+
* and authentication fence, in the shape every other dshell route uses.
|
|
155
|
+
*
|
|
156
|
+
* The page reads aggregates and triggers the scan through the same endpoint:
|
|
157
|
+
* the scan is the expensive half and belongs on the host, where the session
|
|
158
|
+
* events already are, so the browser asks for it rather than doing it.
|
|
159
|
+
*/
|
|
160
|
+
const DSHELL_USAGE_PATH = "/api/dshell/usage";
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region lib/client/heatmap.js
|
|
163
|
+
var import_jsx_runtime = require_jsx_runtime();
|
|
164
|
+
/** The local date of a `YYYY-MM-DD` label, at local midnight. */
|
|
165
|
+
function dateOf(day) {
|
|
166
|
+
return /* @__PURE__ */ new Date(`${day}T00:00:00`);
|
|
167
|
+
}
|
|
168
|
+
/** The `YYYY-MM-DD` label of a local date, the way the index spells days. */
|
|
169
|
+
function labelOf(date) {
|
|
170
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
171
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Lay the recorded days out in a full rectangle of `weeks` columns.
|
|
175
|
+
*
|
|
176
|
+
* The newest cell is the anchor: the last column's bottom cell holds the anchor
|
|
177
|
+
* day, and every earlier cell steps back one day, so the grid reads oldest at
|
|
178
|
+
* the top-left and newest at the bottom-right. The rectangle is always full —
|
|
179
|
+
* cells the history does not reach are present but empty — because a grid that
|
|
180
|
+
* grew and shrank with the data would move the corner as the history changed.
|
|
181
|
+
*
|
|
182
|
+
* @param days - one entry per recorded day, any order.
|
|
183
|
+
* @param weeks - how many columns to draw.
|
|
184
|
+
* @param anchor - the host's today, `YYYY-MM-DD`; the bottom-right cell.
|
|
185
|
+
* @returns the columns, left to right.
|
|
186
|
+
*/
|
|
187
|
+
function heatmapColumns(days, weeks, anchor) {
|
|
188
|
+
const byDay = new Map(days.map((day) => [day.day, day]));
|
|
189
|
+
const max = Math.max(1, ...days.map((day) => day.total));
|
|
190
|
+
const end = dateOf(anchor);
|
|
191
|
+
const columns = [];
|
|
192
|
+
for (let column = 0; column < weeks; column += 1) {
|
|
193
|
+
const cells = [];
|
|
194
|
+
for (let row = 0; row < 7; row += 1) {
|
|
195
|
+
const back = (weeks - 1 - column) * 7 + (6 - row);
|
|
196
|
+
const date = new Date(end);
|
|
197
|
+
date.setDate(date.getDate() - back);
|
|
198
|
+
const label = labelOf(date);
|
|
199
|
+
if (date.getTime() > end.getTime()) {
|
|
200
|
+
cells.push(void 0);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const recorded = byDay.get(label);
|
|
204
|
+
cells.push(recorded === void 0 ? void 0 : {
|
|
205
|
+
...recorded,
|
|
206
|
+
level: recorded.total === 0 ? 0 : Math.min(4, 1 + Math.floor(recorded.total / max * 3))
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
columns.push(cells);
|
|
210
|
+
}
|
|
211
|
+
return columns;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Whether the history reaches further back than the grid draws.
|
|
215
|
+
*
|
|
216
|
+
* @param days - the same days handed to {@link heatmapColumns}.
|
|
217
|
+
* @param weeks - the same width.
|
|
218
|
+
* @param anchor - the same anchor.
|
|
219
|
+
* @returns whether any recorded day fell off the left edge.
|
|
220
|
+
*/
|
|
221
|
+
function heatmapTruncated(days, weeks, anchor) {
|
|
222
|
+
if (days.length === 0) return false;
|
|
223
|
+
const oldest = days.reduce((earliest, day) => day.day < earliest ? day.day : earliest, days[0].day);
|
|
224
|
+
const start = dateOf(anchor);
|
|
225
|
+
start.setDate(start.getDate() - (weeks * 7 - 1));
|
|
226
|
+
return dateOf(oldest).getTime() < start.getTime();
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region lib/client/page.js
|
|
230
|
+
/**
|
|
231
|
+
* The usage page: one settings entry that charts what every model has cost.
|
|
232
|
+
*
|
|
233
|
+
* Everything on it comes from one host endpoint. `summary` reads the index,
|
|
234
|
+
* `scan` extends it first — the host owns both the session events and the
|
|
235
|
+
* index, so the browser asks for a rebuild rather than performing one.
|
|
236
|
+
*
|
|
237
|
+
* The two charts answer different questions and neither is derivable from the
|
|
238
|
+
* other: the stacked area shows how the total moved over time and which route
|
|
239
|
+
* moved it, and the pie shows what share each route holds across the whole
|
|
240
|
+
* window. Both are hand-rolled SVG — the client bundle inlines its imports, and
|
|
241
|
+
* a charting library would be the largest thing in it by an order of magnitude.
|
|
242
|
+
*/
|
|
243
|
+
/** A fixed palette: a route keeps its colour as the window changes. */
|
|
244
|
+
const PALETTE = [
|
|
245
|
+
"#5B8FF9",
|
|
246
|
+
"#61DDAA",
|
|
247
|
+
"#F6BD16",
|
|
248
|
+
"#7262FD",
|
|
249
|
+
"#78D3F8",
|
|
250
|
+
"#9661BC",
|
|
251
|
+
"#F6903D",
|
|
252
|
+
"#008685",
|
|
253
|
+
"#F08BB4",
|
|
254
|
+
"#E8684A"
|
|
255
|
+
];
|
|
256
|
+
const RANGES = [
|
|
257
|
+
{
|
|
258
|
+
days: 7,
|
|
259
|
+
key: "range.7"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
days: 30,
|
|
263
|
+
key: "range.30"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
days: null,
|
|
267
|
+
key: "range.all"
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
/** Compact token counts: the axis and the table both read better rounded. */
|
|
271
|
+
const counts = new Intl.NumberFormat(void 0, {
|
|
272
|
+
notation: "compact",
|
|
273
|
+
maximumFractionDigits: 1
|
|
274
|
+
});
|
|
275
|
+
/** Rough sizes; the page is a settings column, not a full viewport. */
|
|
276
|
+
const CURVE = {
|
|
277
|
+
width: 720,
|
|
278
|
+
height: 200,
|
|
279
|
+
padLeft: 56,
|
|
280
|
+
padRight: 12,
|
|
281
|
+
padTop: 12,
|
|
282
|
+
padBottom: 24
|
|
283
|
+
};
|
|
284
|
+
const PIE = {
|
|
285
|
+
size: 200,
|
|
286
|
+
radius: 88,
|
|
287
|
+
inner: 52
|
|
288
|
+
};
|
|
289
|
+
/** The heatmap's grid geometry: a fixed half-year block at settings scale. */
|
|
290
|
+
const HEAT = {
|
|
291
|
+
cell: 13,
|
|
292
|
+
gap: 3,
|
|
293
|
+
weeks: 26
|
|
294
|
+
};
|
|
295
|
+
/** The heatmap's shades, from "a little" to "the busiest day", tinted like the first curve. */
|
|
296
|
+
const HEAT_SHADES = [
|
|
297
|
+
"#5B8FF9",
|
|
298
|
+
"#4a78e0",
|
|
299
|
+
"#3c62c4",
|
|
300
|
+
"#2f4da6"
|
|
301
|
+
];
|
|
302
|
+
/** Every bucket summed — the measure both charts and the table sort on. */
|
|
303
|
+
function total(buckets) {
|
|
304
|
+
return buckets.uncachedInputTokens + buckets.outputTokens + buckets.cacheReadTokens + buckets.cacheWriteTokens;
|
|
305
|
+
}
|
|
306
|
+
/** One model's curve: the path across the days, and a point at each day. */
|
|
307
|
+
function linePoints(values, xs, y) {
|
|
308
|
+
return values.map((value, index) => `${String(xs[index])},${String(y(value))}`).join(" ");
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* One ring segment.
|
|
312
|
+
*
|
|
313
|
+
* A segment covering the whole circle needs its own spelling: its start and end
|
|
314
|
+
* points coincide, and an arc between coincident points draws nothing — which
|
|
315
|
+
* is exactly what a reader with one model would see. The full turn is two half
|
|
316
|
+
* turns, outer and inner wound in opposite directions so the middle stays open.
|
|
317
|
+
*/
|
|
318
|
+
function ringPath(start, end) {
|
|
319
|
+
const { size, radius, inner } = PIE;
|
|
320
|
+
const cx = size / 2;
|
|
321
|
+
const cy = size / 2;
|
|
322
|
+
const at = (r, angle) => [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];
|
|
323
|
+
if (end - start >= Math.PI * 2 - 1e-6) return `${`M${String(cx)},${String(cy - radius)} A${String(radius)},${String(radius)} 0 1 1 ${String(cx)},${String(cy + radius)} A${String(radius)},${String(radius)} 0 1 1 ${String(cx)},${String(cy - radius)} Z`} ${`M${String(cx)},${String(cy - inner)} A${String(inner)},${String(inner)} 0 1 0 ${String(cx)},${String(cy + inner)} A${String(inner)},${String(inner)} 0 1 0 ${String(cx)},${String(cy - inner)} Z`}`;
|
|
324
|
+
const [ox1, oy1] = at(radius, start);
|
|
325
|
+
const [ox2, oy2] = at(radius, end);
|
|
326
|
+
const [ix2, iy2] = at(inner, end);
|
|
327
|
+
const [ix1, iy1] = at(inner, start);
|
|
328
|
+
const large = end - start > Math.PI ? 1 : 0;
|
|
329
|
+
return `M${String(ox1)},${String(oy1)} A${String(radius)},${String(radius)} 0 ${String(large)} 1 ${String(ox2)},${String(oy2)} L${String(ix2)},${String(iy2)} A${String(inner)},${String(inner)} 0 ${String(large)} 0 ${String(ix1)},${String(iy1)} Z`;
|
|
330
|
+
}
|
|
331
|
+
/** The page body. */
|
|
332
|
+
function UsageSection({ t }) {
|
|
333
|
+
const [days, setDays] = (0, react.useState)(7);
|
|
334
|
+
const [summary, setSummary] = (0, react.useState)(void 0);
|
|
335
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
336
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
337
|
+
const [note, setNote] = (0, react.useState)(void 0);
|
|
338
|
+
/** Sessions the last scan had to skip, so the page can say the totals are partial. */
|
|
339
|
+
const [skipped, setSkipped] = (0, react.useState)(void 0);
|
|
340
|
+
const ask = (0, react.useCallback)(async (action, window) => {
|
|
341
|
+
setBusy(true);
|
|
342
|
+
setError(void 0);
|
|
343
|
+
try {
|
|
344
|
+
const body = await (await fetch(DSHELL_USAGE_PATH, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
credentials: "include",
|
|
347
|
+
headers: { "content-type": "application/json" },
|
|
348
|
+
body: JSON.stringify({
|
|
349
|
+
action,
|
|
350
|
+
days: window
|
|
351
|
+
})
|
|
352
|
+
})).json();
|
|
353
|
+
if ("error" in body) throw new Error(body.error);
|
|
354
|
+
setSummary(body);
|
|
355
|
+
setNote(action === "scan" && body.scanned !== void 0 ? t("state.scanned", {
|
|
356
|
+
sessions: counts.format(body.scanned.sessions),
|
|
357
|
+
read: counts.format(body.scanned.read),
|
|
358
|
+
turns: counts.format(body.scanned.turns)
|
|
359
|
+
}) : void 0);
|
|
360
|
+
setSkipped(body.scanned !== void 0 && body.scanned.skipped > 0 ? {
|
|
361
|
+
count: body.scanned.skipped,
|
|
362
|
+
reason: body.scanned.skipReason
|
|
363
|
+
} : void 0);
|
|
364
|
+
} catch (reason) {
|
|
365
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
366
|
+
} finally {
|
|
367
|
+
setBusy(false);
|
|
368
|
+
}
|
|
369
|
+
}, [t]);
|
|
370
|
+
const firstOpen = (0, react.useRef)(true);
|
|
371
|
+
(0, react.useEffect)(() => {
|
|
372
|
+
const action = firstOpen.current ? "scan" : "summary";
|
|
373
|
+
firstOpen.current = false;
|
|
374
|
+
ask(action, days);
|
|
375
|
+
}, [ask, days]);
|
|
376
|
+
const curve = (0, react.useMemo)(() => {
|
|
377
|
+
if (summary === void 0) return void 0;
|
|
378
|
+
const daysList = summary.daysList;
|
|
379
|
+
const models = summary.models;
|
|
380
|
+
if (daysList.length === 0 || models.length === 0) return void 0;
|
|
381
|
+
const span = CURVE.width - CURVE.padLeft - CURVE.padRight;
|
|
382
|
+
const xs = daysList.length === 1 ? [CURVE.padLeft + span / 2] : daysList.map((_, index) => CURVE.padLeft + index * span / (daysList.length - 1));
|
|
383
|
+
const seriesValues = models.map((model) => daysList.map((day) => summary.byDay.filter((row) => row.day === day && `${row.provider}/${row.model}` === model).reduce((sum, row) => sum + total(row), 0)));
|
|
384
|
+
const max = Math.max(1, ...seriesValues.flat());
|
|
385
|
+
const y = (value) => CURVE.height - CURVE.padBottom - value / max * (CURVE.height - CURVE.padTop - CURVE.padBottom);
|
|
386
|
+
return {
|
|
387
|
+
xs,
|
|
388
|
+
y,
|
|
389
|
+
max,
|
|
390
|
+
daysList,
|
|
391
|
+
series: models.map((model, index) => ({
|
|
392
|
+
model,
|
|
393
|
+
color: PALETTE[index % PALETTE.length],
|
|
394
|
+
points: linePoints(seriesValues[index], xs, y),
|
|
395
|
+
markers: seriesValues[index].map((value, position) => ({
|
|
396
|
+
x: xs[position],
|
|
397
|
+
y: y(value)
|
|
398
|
+
}))
|
|
399
|
+
}))
|
|
400
|
+
};
|
|
401
|
+
}, [summary]);
|
|
402
|
+
const pie = (0, react.useMemo)(() => {
|
|
403
|
+
if (summary === void 0) return void 0;
|
|
404
|
+
const rows = [...summary.totals];
|
|
405
|
+
const sum = rows.reduce((carry, row) => carry + total(row), 0);
|
|
406
|
+
if (sum === 0) return void 0;
|
|
407
|
+
let angle = -Math.PI / 2;
|
|
408
|
+
return {
|
|
409
|
+
sum,
|
|
410
|
+
slices: rows.map((row, index) => {
|
|
411
|
+
const share = total(row) / sum;
|
|
412
|
+
const start = angle;
|
|
413
|
+
const end = angle + share * Math.PI * 2;
|
|
414
|
+
angle = end;
|
|
415
|
+
return {
|
|
416
|
+
row,
|
|
417
|
+
color: PALETTE[index % PALETTE.length],
|
|
418
|
+
share,
|
|
419
|
+
path: ringPath(start, end)
|
|
420
|
+
};
|
|
421
|
+
})
|
|
422
|
+
};
|
|
423
|
+
}, [summary]);
|
|
424
|
+
/** The window's headline numbers: what the two charts are shares and shapes of. */
|
|
425
|
+
const headline = (0, react.useMemo)(() => {
|
|
426
|
+
if (summary === void 0) return void 0;
|
|
427
|
+
const sums = summary.totals.reduce((carry, row) => ({
|
|
428
|
+
uncachedInputTokens: carry.uncachedInputTokens + row.uncachedInputTokens,
|
|
429
|
+
outputTokens: carry.outputTokens + row.outputTokens,
|
|
430
|
+
cacheReadTokens: carry.cacheReadTokens + row.cacheReadTokens,
|
|
431
|
+
cacheWriteTokens: carry.cacheWriteTokens + row.cacheWriteTokens,
|
|
432
|
+
turns: carry.turns + row.turns
|
|
433
|
+
}), {
|
|
434
|
+
uncachedInputTokens: 0,
|
|
435
|
+
outputTokens: 0,
|
|
436
|
+
cacheReadTokens: 0,
|
|
437
|
+
cacheWriteTokens: 0,
|
|
438
|
+
turns: 0
|
|
439
|
+
});
|
|
440
|
+
const billed = sums.uncachedInputTokens + sums.cacheReadTokens;
|
|
441
|
+
return {
|
|
442
|
+
...sums,
|
|
443
|
+
total: total(sums),
|
|
444
|
+
hitRate: billed === 0 ? void 0 : sums.cacheReadTokens / billed
|
|
445
|
+
};
|
|
446
|
+
}, [summary]);
|
|
447
|
+
/** The calendar grid: a fixed rectangle of weeks, newest day at the bottom right. */
|
|
448
|
+
const heat = (0, react.useMemo)(() => {
|
|
449
|
+
if (summary === void 0 || summary.heat.length === 0) return void 0;
|
|
450
|
+
const columns = heatmapColumns(summary.heat, HEAT.weeks, summary.today);
|
|
451
|
+
if (columns.length === 0) return void 0;
|
|
452
|
+
return {
|
|
453
|
+
columns,
|
|
454
|
+
truncated: heatmapTruncated(summary.heat, HEAT.weeks, summary.today),
|
|
455
|
+
busiest: Math.max(...summary.heat.map((day) => day.total))
|
|
456
|
+
};
|
|
457
|
+
}, [summary]);
|
|
458
|
+
return (0, import_jsx_runtime.jsxs)("section", {
|
|
459
|
+
style: {
|
|
460
|
+
display: "flex",
|
|
461
|
+
flexDirection: "column",
|
|
462
|
+
gap: 20,
|
|
463
|
+
padding: "4px 2px"
|
|
464
|
+
},
|
|
465
|
+
children: [
|
|
466
|
+
(0, import_jsx_runtime.jsxs)("header", {
|
|
467
|
+
style: {
|
|
468
|
+
display: "flex",
|
|
469
|
+
flexDirection: "column",
|
|
470
|
+
gap: 6
|
|
471
|
+
},
|
|
472
|
+
children: [(0, import_jsx_runtime.jsx)("h2", {
|
|
473
|
+
style: {
|
|
474
|
+
margin: 0,
|
|
475
|
+
fontSize: 16
|
|
476
|
+
},
|
|
477
|
+
children: t("title")
|
|
478
|
+
}), (0, import_jsx_runtime.jsx)("p", {
|
|
479
|
+
style: {
|
|
480
|
+
margin: 0,
|
|
481
|
+
opacity: .7,
|
|
482
|
+
lineHeight: 1.6
|
|
483
|
+
},
|
|
484
|
+
children: t("subtitle")
|
|
485
|
+
})]
|
|
486
|
+
}),
|
|
487
|
+
heat !== void 0 && (0, import_jsx_runtime.jsxs)("figure", {
|
|
488
|
+
style: {
|
|
489
|
+
margin: 0,
|
|
490
|
+
display: "flex",
|
|
491
|
+
flexDirection: "column",
|
|
492
|
+
gap: 8
|
|
493
|
+
},
|
|
494
|
+
children: [
|
|
495
|
+
(0, import_jsx_runtime.jsx)("figcaption", {
|
|
496
|
+
style: { opacity: .8 },
|
|
497
|
+
children: t("chart.heatmap")
|
|
498
|
+
}),
|
|
499
|
+
(0, import_jsx_runtime.jsx)("div", {
|
|
500
|
+
style: {
|
|
501
|
+
display: "flex",
|
|
502
|
+
gap: HEAT.gap,
|
|
503
|
+
overflowX: "auto"
|
|
504
|
+
},
|
|
505
|
+
children: heat.columns.map((column, index) => (0, import_jsx_runtime.jsx)("div", {
|
|
506
|
+
style: {
|
|
507
|
+
display: "grid",
|
|
508
|
+
gridTemplateRows: `repeat(7, ${String(HEAT.cell)}px)`,
|
|
509
|
+
gap: HEAT.gap
|
|
510
|
+
},
|
|
511
|
+
children: column.map((cell, slot) => cell === void 0 ? (0, import_jsx_runtime.jsx)("span", { style: {
|
|
512
|
+
width: HEAT.cell,
|
|
513
|
+
height: HEAT.cell,
|
|
514
|
+
borderRadius: 3,
|
|
515
|
+
background: "currentColor",
|
|
516
|
+
opacity: .06
|
|
517
|
+
} }, `empty-${String(slot)}`) : (0, import_jsx_runtime.jsx)("span", {
|
|
518
|
+
title: t("chart.heatmapDay", {
|
|
519
|
+
day: cell.day,
|
|
520
|
+
total: counts.format(cell.total),
|
|
521
|
+
turns: counts.format(cell.turns)
|
|
522
|
+
}),
|
|
523
|
+
style: {
|
|
524
|
+
width: HEAT.cell,
|
|
525
|
+
height: HEAT.cell,
|
|
526
|
+
borderRadius: 3,
|
|
527
|
+
background: cell.level === 0 ? "currentColor" : HEAT_SHADES[cell.level - 1],
|
|
528
|
+
opacity: cell.level === 0 ? .06 : .35 + cell.level / 4 * .65
|
|
529
|
+
}
|
|
530
|
+
}, cell.day))
|
|
531
|
+
}, `column-${String(index)}`))
|
|
532
|
+
}),
|
|
533
|
+
(0, import_jsx_runtime.jsxs)("div", {
|
|
534
|
+
style: {
|
|
535
|
+
display: "flex",
|
|
536
|
+
alignItems: "center",
|
|
537
|
+
gap: 6,
|
|
538
|
+
fontSize: 11,
|
|
539
|
+
opacity: .7
|
|
540
|
+
},
|
|
541
|
+
children: [
|
|
542
|
+
(0, import_jsx_runtime.jsx)("span", { children: t("chart.heatmapLess") }),
|
|
543
|
+
[
|
|
544
|
+
0,
|
|
545
|
+
1,
|
|
546
|
+
2,
|
|
547
|
+
3,
|
|
548
|
+
4
|
|
549
|
+
].map((level) => (0, import_jsx_runtime.jsx)("span", { style: {
|
|
550
|
+
width: HEAT.cell,
|
|
551
|
+
height: HEAT.cell,
|
|
552
|
+
borderRadius: 3,
|
|
553
|
+
background: level === 0 ? "currentColor" : HEAT_SHADES[level - 1],
|
|
554
|
+
opacity: level === 0 ? .06 : .35 + level / 4 * .65
|
|
555
|
+
} }, String(level))),
|
|
556
|
+
(0, import_jsx_runtime.jsx)("span", { children: t("chart.heatmapMore") }),
|
|
557
|
+
(0, import_jsx_runtime.jsx)("span", {
|
|
558
|
+
style: {
|
|
559
|
+
marginLeft: 6,
|
|
560
|
+
opacity: .8
|
|
561
|
+
},
|
|
562
|
+
children: t("chart.heatmapPeak", { total: counts.format(heat.busiest) })
|
|
563
|
+
}),
|
|
564
|
+
heat.truncated ? (0, import_jsx_runtime.jsxs)("span", {
|
|
565
|
+
style: { opacity: .8 },
|
|
566
|
+
children: ["· ", t("chart.heatmapTruncated")]
|
|
567
|
+
}) : null
|
|
568
|
+
]
|
|
569
|
+
})
|
|
570
|
+
]
|
|
571
|
+
}),
|
|
572
|
+
(0, import_jsx_runtime.jsxs)("div", {
|
|
573
|
+
style: {
|
|
574
|
+
display: "flex",
|
|
575
|
+
alignItems: "center",
|
|
576
|
+
gap: 12,
|
|
577
|
+
flexWrap: "wrap"
|
|
578
|
+
},
|
|
579
|
+
children: [
|
|
580
|
+
(0, import_jsx_runtime.jsx)("span", {
|
|
581
|
+
style: { opacity: .7 },
|
|
582
|
+
children: t("range.label")
|
|
583
|
+
}),
|
|
584
|
+
RANGES.map((range) => (0, import_jsx_runtime.jsx)("button", {
|
|
585
|
+
type: "button",
|
|
586
|
+
onClick: () => {
|
|
587
|
+
setDays(range.days);
|
|
588
|
+
},
|
|
589
|
+
style: {
|
|
590
|
+
padding: "4px 12px",
|
|
591
|
+
borderRadius: 999,
|
|
592
|
+
cursor: "pointer",
|
|
593
|
+
border: days === range.days ? "1.5px solid currentColor" : "1px solid currentColor",
|
|
594
|
+
background: "transparent",
|
|
595
|
+
color: "inherit",
|
|
596
|
+
fontWeight: days === range.days ? 600 : 400,
|
|
597
|
+
opacity: days === range.days ? 1 : .55
|
|
598
|
+
},
|
|
599
|
+
children: t(range.key)
|
|
600
|
+
}, range.key)),
|
|
601
|
+
(0, import_jsx_runtime.jsx)("button", {
|
|
602
|
+
type: "button",
|
|
603
|
+
disabled: busy,
|
|
604
|
+
onClick: () => {
|
|
605
|
+
ask("scan", days);
|
|
606
|
+
},
|
|
607
|
+
style: {
|
|
608
|
+
marginLeft: "auto",
|
|
609
|
+
padding: "4px 14px",
|
|
610
|
+
borderRadius: 6,
|
|
611
|
+
cursor: busy ? "default" : "pointer"
|
|
612
|
+
},
|
|
613
|
+
children: busy ? t("rebuild.busy") : t("rebuild")
|
|
614
|
+
})
|
|
615
|
+
]
|
|
616
|
+
}),
|
|
617
|
+
summary?.scannedAt != null && (0, import_jsx_runtime.jsxs)("div", {
|
|
618
|
+
style: {
|
|
619
|
+
opacity: .6,
|
|
620
|
+
fontSize: 12
|
|
621
|
+
},
|
|
622
|
+
children: [t("state.freshness", { day: summary.scannedAt }), note === void 0 ? "" : ` · ${note}`]
|
|
623
|
+
}),
|
|
624
|
+
skipped !== void 0 && (0, import_jsx_runtime.jsxs)("div", {
|
|
625
|
+
style: {
|
|
626
|
+
fontSize: 12,
|
|
627
|
+
lineHeight: 1.6,
|
|
628
|
+
padding: "6px 10px",
|
|
629
|
+
borderRadius: 8,
|
|
630
|
+
color: "var(--dsw-alias-state-warning-primary, #b45309)",
|
|
631
|
+
background: "var(--dsw-alias-interactive-bg-hover-warning, rgba(245,158,11,.12))"
|
|
632
|
+
},
|
|
633
|
+
title: skipped.reason,
|
|
634
|
+
children: [t("state.skipped", { count: counts.format(skipped.count) }), skipped.reason === void 0 ? null : (0, import_jsx_runtime.jsxs)("span", {
|
|
635
|
+
style: { opacity: .7 },
|
|
636
|
+
children: [" · ", shorten(skipped.reason, 120)]
|
|
637
|
+
})]
|
|
638
|
+
}),
|
|
639
|
+
error !== void 0 && (0, import_jsx_runtime.jsx)("div", {
|
|
640
|
+
style: {
|
|
641
|
+
color: "var(--dsh-danger, #c0392b)",
|
|
642
|
+
fontSize: 13
|
|
643
|
+
},
|
|
644
|
+
children: t("state.error", { reason: error })
|
|
645
|
+
}),
|
|
646
|
+
summary === void 0 && error === void 0 && (0, import_jsx_runtime.jsx)("div", {
|
|
647
|
+
style: { opacity: .6 },
|
|
648
|
+
children: t("state.loading")
|
|
649
|
+
}),
|
|
650
|
+
headline !== void 0 && headline.turns > 0 && (0, import_jsx_runtime.jsx)("div", {
|
|
651
|
+
style: {
|
|
652
|
+
display: "flex",
|
|
653
|
+
gap: 18,
|
|
654
|
+
flexWrap: "wrap"
|
|
655
|
+
},
|
|
656
|
+
children: [
|
|
657
|
+
["summary.total", counts.format(headline.total)],
|
|
658
|
+
["summary.uncached", counts.format(headline.uncachedInputTokens)],
|
|
659
|
+
["summary.output", counts.format(headline.outputTokens)],
|
|
660
|
+
["summary.cacheRead", counts.format(headline.cacheReadTokens)],
|
|
661
|
+
["summary.turns", counts.format(headline.turns)],
|
|
662
|
+
...headline.hitRate === void 0 ? [] : [["summary.hitRate", `${(headline.hitRate * 100).toFixed(1)}%`]]
|
|
663
|
+
].map(([key, value]) => (0, import_jsx_runtime.jsxs)("div", {
|
|
664
|
+
style: {
|
|
665
|
+
display: "flex",
|
|
666
|
+
flexDirection: "column",
|
|
667
|
+
gap: 2
|
|
668
|
+
},
|
|
669
|
+
children: [(0, import_jsx_runtime.jsx)("span", {
|
|
670
|
+
style: {
|
|
671
|
+
fontSize: 11,
|
|
672
|
+
opacity: .6
|
|
673
|
+
},
|
|
674
|
+
children: t(key)
|
|
675
|
+
}), (0, import_jsx_runtime.jsx)("span", {
|
|
676
|
+
style: {
|
|
677
|
+
fontSize: 18,
|
|
678
|
+
fontVariantNumeric: "tabular-nums"
|
|
679
|
+
},
|
|
680
|
+
children: value
|
|
681
|
+
})]
|
|
682
|
+
}, key))
|
|
683
|
+
}),
|
|
684
|
+
summary !== void 0 && curve === void 0 && (0, import_jsx_runtime.jsxs)("div", {
|
|
685
|
+
style: {
|
|
686
|
+
display: "flex",
|
|
687
|
+
flexDirection: "column",
|
|
688
|
+
gap: 4
|
|
689
|
+
},
|
|
690
|
+
children: [(0, import_jsx_runtime.jsx)("strong", { children: t("state.empty.title") }), (0, import_jsx_runtime.jsx)("span", {
|
|
691
|
+
style: { opacity: .7 },
|
|
692
|
+
children: t("state.empty.body")
|
|
693
|
+
})]
|
|
694
|
+
}),
|
|
695
|
+
curve !== void 0 && (0, import_jsx_runtime.jsxs)("figure", {
|
|
696
|
+
style: {
|
|
697
|
+
margin: 0,
|
|
698
|
+
display: "flex",
|
|
699
|
+
flexDirection: "column",
|
|
700
|
+
gap: 8
|
|
701
|
+
},
|
|
702
|
+
children: [
|
|
703
|
+
(0, import_jsx_runtime.jsx)("figcaption", {
|
|
704
|
+
style: { opacity: .8 },
|
|
705
|
+
children: t("chart.curve")
|
|
706
|
+
}),
|
|
707
|
+
(0, import_jsx_runtime.jsxs)("svg", {
|
|
708
|
+
viewBox: `0 0 ${String(CURVE.width)} ${String(CURVE.height)}`,
|
|
709
|
+
style: {
|
|
710
|
+
width: "100%",
|
|
711
|
+
height: "auto"
|
|
712
|
+
},
|
|
713
|
+
role: "img",
|
|
714
|
+
children: [
|
|
715
|
+
[
|
|
716
|
+
0,
|
|
717
|
+
.5,
|
|
718
|
+
1
|
|
719
|
+
].map((fraction) => {
|
|
720
|
+
const value = curve.max * fraction;
|
|
721
|
+
return (0, import_jsx_runtime.jsxs)("g", { children: [(0, import_jsx_runtime.jsx)("line", {
|
|
722
|
+
x1: CURVE.padLeft,
|
|
723
|
+
x2: CURVE.width - CURVE.padRight,
|
|
724
|
+
y1: curve.y(value),
|
|
725
|
+
y2: curve.y(value),
|
|
726
|
+
stroke: "currentColor",
|
|
727
|
+
strokeOpacity: .15,
|
|
728
|
+
strokeDasharray: fraction === 0 ? void 0 : "3 3"
|
|
729
|
+
}), (0, import_jsx_runtime.jsx)("text", {
|
|
730
|
+
x: CURVE.padLeft - 8,
|
|
731
|
+
y: curve.y(value) + 4,
|
|
732
|
+
textAnchor: "end",
|
|
733
|
+
fontSize: 10,
|
|
734
|
+
fill: "currentColor",
|
|
735
|
+
fillOpacity: .6,
|
|
736
|
+
children: counts.format(value)
|
|
737
|
+
})] }, fraction);
|
|
738
|
+
}),
|
|
739
|
+
curve.series.map((series) => (0, import_jsx_runtime.jsxs)("g", { children: [(0, import_jsx_runtime.jsx)("polyline", {
|
|
740
|
+
points: series.points,
|
|
741
|
+
fill: "none",
|
|
742
|
+
stroke: series.color,
|
|
743
|
+
strokeWidth: 2,
|
|
744
|
+
strokeLinejoin: "round"
|
|
745
|
+
}), series.markers.map((marker, position) => (0, import_jsx_runtime.jsx)("circle", {
|
|
746
|
+
cx: marker.x,
|
|
747
|
+
cy: marker.y,
|
|
748
|
+
r: 3,
|
|
749
|
+
fill: series.color
|
|
750
|
+
}, `${series.model}-${String(position)}`))] }, series.model)),
|
|
751
|
+
curve.daysList.map((day, index) => index % Math.ceil(curve.daysList.length / 8) === 0 ? (0, import_jsx_runtime.jsx)("text", {
|
|
752
|
+
x: curve.xs[index],
|
|
753
|
+
y: CURVE.height - 6,
|
|
754
|
+
textAnchor: "middle",
|
|
755
|
+
fontSize: 10,
|
|
756
|
+
fill: "currentColor",
|
|
757
|
+
fillOpacity: .6,
|
|
758
|
+
children: day.slice(5)
|
|
759
|
+
}, day) : null)
|
|
760
|
+
]
|
|
761
|
+
}),
|
|
762
|
+
(0, import_jsx_runtime.jsx)(Legend, { series: curve.series })
|
|
763
|
+
]
|
|
764
|
+
}),
|
|
765
|
+
pie !== void 0 && (0, import_jsx_runtime.jsxs)("figure", {
|
|
766
|
+
style: {
|
|
767
|
+
margin: 0,
|
|
768
|
+
display: "flex",
|
|
769
|
+
gap: 20,
|
|
770
|
+
alignItems: "center",
|
|
771
|
+
flexWrap: "wrap"
|
|
772
|
+
},
|
|
773
|
+
children: [
|
|
774
|
+
(0, import_jsx_runtime.jsx)("figcaption", {
|
|
775
|
+
style: {
|
|
776
|
+
opacity: .8,
|
|
777
|
+
alignSelf: "flex-start"
|
|
778
|
+
},
|
|
779
|
+
children: t("chart.pie")
|
|
780
|
+
}),
|
|
781
|
+
(0, import_jsx_runtime.jsxs)("svg", {
|
|
782
|
+
viewBox: `0 0 ${String(PIE.size)} ${String(PIE.size)}`,
|
|
783
|
+
style: {
|
|
784
|
+
width: PIE.size,
|
|
785
|
+
height: PIE.size
|
|
786
|
+
},
|
|
787
|
+
role: "img",
|
|
788
|
+
children: [
|
|
789
|
+
pie.slices.map((slice) => (0, import_jsx_runtime.jsx)("path", {
|
|
790
|
+
d: slice.path,
|
|
791
|
+
fill: slice.color,
|
|
792
|
+
fillOpacity: .85,
|
|
793
|
+
fillRule: "evenodd"
|
|
794
|
+
}, `${slice.row.provider}/${slice.row.model}`)),
|
|
795
|
+
(0, import_jsx_runtime.jsx)("text", {
|
|
796
|
+
x: PIE.size / 2,
|
|
797
|
+
y: PIE.size / 2 - 2,
|
|
798
|
+
textAnchor: "middle",
|
|
799
|
+
fontSize: 16,
|
|
800
|
+
fill: "currentColor",
|
|
801
|
+
children: counts.format(pie.sum)
|
|
802
|
+
}),
|
|
803
|
+
(0, import_jsx_runtime.jsx)("text", {
|
|
804
|
+
x: PIE.size / 2,
|
|
805
|
+
y: PIE.size / 2 + 14,
|
|
806
|
+
textAnchor: "middle",
|
|
807
|
+
fontSize: 10,
|
|
808
|
+
fill: "currentColor",
|
|
809
|
+
fillOpacity: .6,
|
|
810
|
+
children: "tok"
|
|
811
|
+
})
|
|
812
|
+
]
|
|
813
|
+
}),
|
|
814
|
+
(0, import_jsx_runtime.jsxs)("table", {
|
|
815
|
+
style: {
|
|
816
|
+
borderCollapse: "collapse",
|
|
817
|
+
fontSize: 13
|
|
818
|
+
},
|
|
819
|
+
children: [(0, import_jsx_runtime.jsx)("thead", { children: (0, import_jsx_runtime.jsxs)("tr", {
|
|
820
|
+
style: { opacity: .7 },
|
|
821
|
+
children: [
|
|
822
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
823
|
+
style: cellStyle("left"),
|
|
824
|
+
children: t("table.model")
|
|
825
|
+
}),
|
|
826
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
827
|
+
style: cellStyle("right"),
|
|
828
|
+
children: t("table.total")
|
|
829
|
+
}),
|
|
830
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
831
|
+
style: cellStyle("right"),
|
|
832
|
+
children: t("table.turns")
|
|
833
|
+
})
|
|
834
|
+
]
|
|
835
|
+
}) }), (0, import_jsx_runtime.jsx)("tbody", { children: pie.slices.map((slice) => (0, import_jsx_runtime.jsxs)("tr", { children: [
|
|
836
|
+
(0, import_jsx_runtime.jsxs)("td", {
|
|
837
|
+
style: cellStyle("left"),
|
|
838
|
+
children: [
|
|
839
|
+
(0, import_jsx_runtime.jsx)("span", { style: {
|
|
840
|
+
display: "inline-block",
|
|
841
|
+
width: 8,
|
|
842
|
+
height: 8,
|
|
843
|
+
borderRadius: 2,
|
|
844
|
+
background: slice.color,
|
|
845
|
+
marginRight: 6
|
|
846
|
+
} }),
|
|
847
|
+
slice.row.model,
|
|
848
|
+
(0, import_jsx_runtime.jsx)("span", {
|
|
849
|
+
style: {
|
|
850
|
+
opacity: .5,
|
|
851
|
+
marginLeft: 6
|
|
852
|
+
},
|
|
853
|
+
children: slice.row.provider
|
|
854
|
+
})
|
|
855
|
+
]
|
|
856
|
+
}),
|
|
857
|
+
(0, import_jsx_runtime.jsxs)("td", {
|
|
858
|
+
style: cellStyle("right"),
|
|
859
|
+
children: [
|
|
860
|
+
counts.format(total(slice.row)),
|
|
861
|
+
" ",
|
|
862
|
+
(0, import_jsx_runtime.jsxs)("span", {
|
|
863
|
+
style: { opacity: .5 },
|
|
864
|
+
children: [
|
|
865
|
+
"(",
|
|
866
|
+
(slice.share * 100).toFixed(1),
|
|
867
|
+
"%)"
|
|
868
|
+
]
|
|
869
|
+
})
|
|
870
|
+
]
|
|
871
|
+
}),
|
|
872
|
+
(0, import_jsx_runtime.jsx)("td", {
|
|
873
|
+
style: cellStyle("right"),
|
|
874
|
+
children: counts.format(slice.row.turns)
|
|
875
|
+
})
|
|
876
|
+
] }, `${slice.row.provider}/${slice.row.model}`)) })]
|
|
877
|
+
})
|
|
878
|
+
]
|
|
879
|
+
}),
|
|
880
|
+
pie !== void 0 && (0, import_jsx_runtime.jsxs)("details", { children: [(0, import_jsx_runtime.jsx)("summary", {
|
|
881
|
+
style: {
|
|
882
|
+
cursor: "pointer",
|
|
883
|
+
opacity: .8
|
|
884
|
+
},
|
|
885
|
+
children: t("table.title")
|
|
886
|
+
}), (0, import_jsx_runtime.jsxs)("table", {
|
|
887
|
+
style: {
|
|
888
|
+
borderCollapse: "collapse",
|
|
889
|
+
fontSize: 13,
|
|
890
|
+
marginTop: 10,
|
|
891
|
+
width: "100%"
|
|
892
|
+
},
|
|
893
|
+
children: [(0, import_jsx_runtime.jsx)("thead", { children: (0, import_jsx_runtime.jsxs)("tr", {
|
|
894
|
+
style: { opacity: .7 },
|
|
895
|
+
children: [
|
|
896
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
897
|
+
style: cellStyle("left"),
|
|
898
|
+
children: t("table.model")
|
|
899
|
+
}),
|
|
900
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
901
|
+
style: cellStyle("right"),
|
|
902
|
+
children: t("table.input")
|
|
903
|
+
}),
|
|
904
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
905
|
+
style: cellStyle("right"),
|
|
906
|
+
children: t("table.output")
|
|
907
|
+
}),
|
|
908
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
909
|
+
style: cellStyle("right"),
|
|
910
|
+
children: t("table.cacheRead")
|
|
911
|
+
}),
|
|
912
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
913
|
+
style: cellStyle("right"),
|
|
914
|
+
children: t("table.cacheWrite")
|
|
915
|
+
}),
|
|
916
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
917
|
+
style: cellStyle("right"),
|
|
918
|
+
children: t("table.turns")
|
|
919
|
+
}),
|
|
920
|
+
(0, import_jsx_runtime.jsx)("th", {
|
|
921
|
+
style: cellStyle("right"),
|
|
922
|
+
children: t("table.total")
|
|
923
|
+
})
|
|
924
|
+
]
|
|
925
|
+
}) }), (0, import_jsx_runtime.jsx)("tbody", { children: summary?.totals.map((row, index) => (0, import_jsx_runtime.jsxs)("tr", { children: [
|
|
926
|
+
(0, import_jsx_runtime.jsxs)("td", {
|
|
927
|
+
style: cellStyle("left"),
|
|
928
|
+
children: [
|
|
929
|
+
(0, import_jsx_runtime.jsx)("span", { style: {
|
|
930
|
+
display: "inline-block",
|
|
931
|
+
width: 8,
|
|
932
|
+
height: 8,
|
|
933
|
+
borderRadius: 2,
|
|
934
|
+
background: PALETTE[index % PALETTE.length],
|
|
935
|
+
marginRight: 6
|
|
936
|
+
} }),
|
|
937
|
+
row.model,
|
|
938
|
+
(0, import_jsx_runtime.jsx)("span", {
|
|
939
|
+
style: {
|
|
940
|
+
opacity: .5,
|
|
941
|
+
marginLeft: 6
|
|
942
|
+
},
|
|
943
|
+
children: row.provider
|
|
944
|
+
})
|
|
945
|
+
]
|
|
946
|
+
}),
|
|
947
|
+
(0, import_jsx_runtime.jsx)("td", {
|
|
948
|
+
style: cellStyle("right"),
|
|
949
|
+
children: counts.format(row.uncachedInputTokens)
|
|
950
|
+
}),
|
|
951
|
+
(0, import_jsx_runtime.jsx)("td", {
|
|
952
|
+
style: cellStyle("right"),
|
|
953
|
+
children: counts.format(row.outputTokens)
|
|
954
|
+
}),
|
|
955
|
+
(0, import_jsx_runtime.jsx)("td", {
|
|
956
|
+
style: cellStyle("right"),
|
|
957
|
+
children: counts.format(row.cacheReadTokens)
|
|
958
|
+
}),
|
|
959
|
+
(0, import_jsx_runtime.jsx)("td", {
|
|
960
|
+
style: cellStyle("right"),
|
|
961
|
+
children: counts.format(row.cacheWriteTokens)
|
|
962
|
+
}),
|
|
963
|
+
(0, import_jsx_runtime.jsx)("td", {
|
|
964
|
+
style: cellStyle("right"),
|
|
965
|
+
children: counts.format(row.turns)
|
|
966
|
+
}),
|
|
967
|
+
(0, import_jsx_runtime.jsx)("td", {
|
|
968
|
+
style: cellStyle("right"),
|
|
969
|
+
children: counts.format(total(row))
|
|
970
|
+
})
|
|
971
|
+
] }, `${row.provider}/${row.model}`)) })]
|
|
972
|
+
})] })
|
|
973
|
+
]
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
/** The curve's colour key, in the order the lines are drawn. */
|
|
977
|
+
function Legend({ series }) {
|
|
978
|
+
return (0, import_jsx_runtime.jsx)("div", {
|
|
979
|
+
style: {
|
|
980
|
+
display: "flex",
|
|
981
|
+
gap: 14,
|
|
982
|
+
flexWrap: "wrap",
|
|
983
|
+
fontSize: 12
|
|
984
|
+
},
|
|
985
|
+
children: series.map((entry) => (0, import_jsx_runtime.jsxs)("span", {
|
|
986
|
+
style: {
|
|
987
|
+
display: "inline-flex",
|
|
988
|
+
alignItems: "center",
|
|
989
|
+
gap: 5
|
|
990
|
+
},
|
|
991
|
+
children: [(0, import_jsx_runtime.jsx)("span", { style: {
|
|
992
|
+
width: 8,
|
|
993
|
+
height: 8,
|
|
994
|
+
borderRadius: 2,
|
|
995
|
+
background: entry.color
|
|
996
|
+
} }), entry.model]
|
|
997
|
+
}, entry.model))
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
/** One table cell's alignment and padding, so the two tables match. */
|
|
1001
|
+
function cellStyle(align) {
|
|
1002
|
+
return {
|
|
1003
|
+
textAlign: align,
|
|
1004
|
+
padding: "3px 10px 3px 0"
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Shorten a host error for a one-line note.
|
|
1009
|
+
*
|
|
1010
|
+
* A refusal names the session, the reason and the log path — useful, and far too
|
|
1011
|
+
* long to sit in a settings line; the full text stays in the note's `title`.
|
|
1012
|
+
*/
|
|
1013
|
+
function shorten(text, limit) {
|
|
1014
|
+
return text.length <= limit ? text : `${text.slice(0, limit)}…`;
|
|
1015
|
+
}
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region lib/client/nav-icon.js
|
|
1018
|
+
/**
|
|
1019
|
+
* The usage entry's own icon in the settings navigation.
|
|
1020
|
+
*
|
|
1021
|
+
* dsh's settings shell picks a nav icon from a map of the section ids IT ships
|
|
1022
|
+
* (`account`, `models`, `plugins`, …) and draws a generic gear for anything
|
|
1023
|
+
* else, and rc.2's `settings.section` seat carries no icon of its own — so a
|
|
1024
|
+
* contributed section cannot ask for one and ours read as "some settings".
|
|
1025
|
+
*
|
|
1026
|
+
* Hence the one DOM patch in this package: find our own row by the label we
|
|
1027
|
+
* registered, and put a gauge where the gear was. It is the same technique
|
|
1028
|
+
* dshell's sidebar section uses (and the artwork is copied verbatim from
|
|
1029
|
+
* `@deepseek-ai/dsh-client-ui-primitives`, medium weight, 16 viewBox, 1.3px
|
|
1030
|
+
* stroke) because a surface mounted into a node dsh owns cannot render React
|
|
1031
|
+
* components into it.
|
|
1032
|
+
*
|
|
1033
|
+
* The row is re-created whenever the dialog opens, so a MutationObserver
|
|
1034
|
+
* watches for it; the patch is idempotent (it re-checks the label and a marker
|
|
1035
|
+
* attribute) and only touches the svg node React will not write to again.
|
|
1036
|
+
* Delete this file the day `settings.section` accepts an icon.
|
|
1037
|
+
*/
|
|
1038
|
+
/**
|
|
1039
|
+
* The gauge artwork, verbatim from the primitives package
|
|
1040
|
+
* (`IconGaugeOutlineMedium`): the same three paths, in the same order.
|
|
1041
|
+
*/
|
|
1042
|
+
const GAUGE_PATHS = [
|
|
1043
|
+
{
|
|
1044
|
+
d: "M3.4041 13.096C2.49514 12.187 1.87614 11.0288 1.62537 9.76798C1.37459 8.50716 1.50331 7.20028 1.99525 6.01261C2.48719 4.82494 3.32025 3.80981 4.3891 3.09557C5.45795 2.38134 6.71458 2.00008 8.0001 2C9.28563 2.00008 10.5423 2.38134 11.6111 3.09557C12.68 3.80981 13.513 4.82494 14.005 6.01261C14.4969 7.20028 14.6256 8.50716 14.3748 9.76798C14.1241 11.0288 13.5051 12.187 12.5961 13.096",
|
|
1045
|
+
stroke: true
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
d: "M8 8.49994L11.6114 4.88855",
|
|
1049
|
+
stroke: true
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
d: "M8 9.75C8.69036 9.75 9.25 9.19036 9.25 8.5C9.25 7.80964 8.69036 7.25 8 7.25C7.30964 7.25 6.75 7.80964 6.75 8.5C6.75 9.19036 7.30964 9.75 8 9.75Z",
|
|
1053
|
+
stroke: false
|
|
1054
|
+
}
|
|
1055
|
+
];
|
|
1056
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
1057
|
+
/** The marker that says a row already carries our artwork. */
|
|
1058
|
+
const MARKER = "data-dshell-usage-icon";
|
|
1059
|
+
/** One gauge, sized and coloured like the icons beside it. */
|
|
1060
|
+
function gauge(size, className) {
|
|
1061
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
1062
|
+
svg.setAttribute("width", String(size));
|
|
1063
|
+
svg.setAttribute("height", String(size));
|
|
1064
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
1065
|
+
svg.setAttribute("fill", "none");
|
|
1066
|
+
svg.setAttribute("stroke-width", "1.3");
|
|
1067
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1068
|
+
if (className !== null) svg.setAttribute("class", className);
|
|
1069
|
+
for (const spec of GAUGE_PATHS) {
|
|
1070
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
1071
|
+
path.setAttribute("d", spec.d);
|
|
1072
|
+
if (spec.stroke) path.setAttribute("stroke", "currentColor");
|
|
1073
|
+
else path.setAttribute("fill", "currentColor");
|
|
1074
|
+
svg.append(path);
|
|
1075
|
+
}
|
|
1076
|
+
return svg;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Keep the settings navigation's usage row drawn with the gauge.
|
|
1080
|
+
*
|
|
1081
|
+
* @param label - the section label as the shell renders it, in the live locale.
|
|
1082
|
+
* @returns the disposer that stops watching.
|
|
1083
|
+
*/
|
|
1084
|
+
function watchUsageNavIcon(label) {
|
|
1085
|
+
if (typeof document === "undefined") return () => {};
|
|
1086
|
+
const patch = () => {
|
|
1087
|
+
for (const dialog of document.querySelectorAll("[role=\"dialog\"]")) for (const row of dialog.querySelectorAll("nav button")) {
|
|
1088
|
+
if ((row.textContent?.trim() ?? "") !== label()) continue;
|
|
1089
|
+
const svg = row.querySelector("svg");
|
|
1090
|
+
if (svg === null || svg.getAttribute(MARKER) !== null) continue;
|
|
1091
|
+
const replacement = gauge(Number(svg.getAttribute("width") ?? 16) || 16, svg.getAttribute("class"));
|
|
1092
|
+
replacement.setAttribute(MARKER, "");
|
|
1093
|
+
svg.replaceWith(replacement);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
patch();
|
|
1097
|
+
const observer = new MutationObserver(() => {
|
|
1098
|
+
patch();
|
|
1099
|
+
});
|
|
1100
|
+
observer.observe(document.body, {
|
|
1101
|
+
childList: true,
|
|
1102
|
+
subtree: true
|
|
1103
|
+
});
|
|
1104
|
+
return () => {
|
|
1105
|
+
observer.disconnect();
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
//#endregion
|
|
1109
|
+
//#region lib/client/index.js
|
|
1110
|
+
/**
|
|
1111
|
+
* The usage page's client face: the locale dictionaries and the Settings
|
|
1112
|
+
* navigation row that mounts the page.
|
|
1113
|
+
*
|
|
1114
|
+
* `settings.section` is the seat dsh's own Plugins page uses, so this page is a
|
|
1115
|
+
* peer of it rather than a tab inside it — the charts want the full settings
|
|
1116
|
+
* column, and a reader looking for "what did this cost" looks in the
|
|
1117
|
+
* navigation, not under a plugin list.
|
|
1118
|
+
*/
|
|
1119
|
+
/** The locale namespace, matching the key `locales.ts` declares. */
|
|
1120
|
+
const NS = "dshellUsage";
|
|
1121
|
+
const name = "@nexus-aethra/dshell-usage/client";
|
|
1122
|
+
const inject = ["slots", "locale"];
|
|
1123
|
+
function apply(ctx) {
|
|
1124
|
+
const t = ctx.locale.bind(NS);
|
|
1125
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1126
|
+
zh,
|
|
1127
|
+
en
|
|
1128
|
+
}), "dshell-usage: dictionaries");
|
|
1129
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1130
|
+
name: "settings.section",
|
|
1131
|
+
id: "usage",
|
|
1132
|
+
order: 40,
|
|
1133
|
+
label: () => t("nav"),
|
|
1134
|
+
locale: NS
|
|
1135
|
+
}, UsageSection));
|
|
1136
|
+
ctx.effect(() => watchUsageNavIcon(() => t("nav")), "dshell-usage: navigation icon");
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
exports.apply = apply;
|
|
1140
|
+
exports.inject = inject;
|
|
1141
|
+
exports.name = name;
|
|
1142
|
+
return module.exports;
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
//# sourceMappingURL=client.js.map
|