@guojinpeng/dsh-cost-meter 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/dist/client.js CHANGED
@@ -1,3 +1,173 @@
1
+ // src/identity.js
2
+ var PACKAGE_ID = "@guojinpeng/dsh-cost-meter";
3
+ var PLUGIN_ID = "guojinpeng/dsh-cost-meter";
4
+ var HTTP_BASE = "/guojinpeng/dsh-cost-meter";
5
+ var PROJECTION_KEY = "guojinpengCostUsage";
6
+ var LOCALE_NAMESPACE = "guojinpengDshCostMeter";
7
+
8
+ // src/client/config-store.js
9
+ function createConfigStore(fetcher = (...args) => fetch(...args)) {
10
+ let snapshot = null, sequence = 0, loading = null, loadStatus = "idle";
11
+ const listeners = /* @__PURE__ */ new Set();
12
+ const notify = () => {
13
+ for (const listener of listeners) listener();
14
+ };
15
+ const publish = (value) => {
16
+ snapshot = value;
17
+ notify();
18
+ return value;
19
+ };
20
+ async function request(path, body) {
21
+ const response = await fetcher(
22
+ `${HTTP_BASE}/${path}`,
23
+ body === void 0 ? void 0 : {
24
+ method: "POST",
25
+ headers: { "content-type": "application/json" },
26
+ body: JSON.stringify(body)
27
+ }
28
+ );
29
+ const payload = await response.json();
30
+ if (!response.ok) throw new Error(payload.error ?? `HTTP ${response.status}`);
31
+ return payload;
32
+ }
33
+ return {
34
+ getSnapshot: () => snapshot,
35
+ getLoadStatus: () => loadStatus,
36
+ subscribe: (listener) => {
37
+ listeners.add(listener);
38
+ return () => listeners.delete(listener);
39
+ },
40
+ load: () => {
41
+ if (loading) return loading;
42
+ const ticket = ++sequence;
43
+ loadStatus = "loading";
44
+ loading = request("config").then((value) => ticket === sequence ? publish(value) : snapshot ?? value).catch((error) => {
45
+ loadStatus = "error";
46
+ throw error;
47
+ }).finally(() => {
48
+ loading = null;
49
+ if (loadStatus !== "error") loadStatus = "ready";
50
+ notify();
51
+ });
52
+ notify();
53
+ return loading;
54
+ },
55
+ save: async (config) => {
56
+ const ticket = ++sequence;
57
+ const value = await request("config", { config, revision: snapshot == null ? void 0 : snapshot.revision });
58
+ return ticket === sequence ? publish(value) : snapshot;
59
+ },
60
+ sync: async () => {
61
+ const ticket = ++sequence;
62
+ const value = await request("pricing/sync", { source: "models.dev", revision: snapshot == null ? void 0 : snapshot.revision });
63
+ return ticket === sequence ? publish(value) : snapshot;
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/client/locales.js
69
+ var zh = {
70
+ modelName: `\u6A21\u578B`,
71
+ initializing: `\u521D\u59CB\u5316\u4E2D`,
72
+ sessionTitle: `\u4F1A\u8BDD\u8D39\u7528`,
73
+ turnTitle: `\u672C\u8F6E\u8D39\u7528`,
74
+ costValue: `\u8D39\u7528 {amount}`,
75
+ turnCost: `{amount}`,
76
+ input: `\u672A\u7F13\u5B58\u8F93\u5165`,
77
+ cacheRead: `\u7F13\u5B58\u8BFB\u53D6`,
78
+ cacheWrite: `\u7F13\u5B58\u5199\u5165`,
79
+ output: `\u8F93\u51FA`,
80
+ model: `\u63D0\u4F9B\u65B9 / \u6A21\u578B`,
81
+ unpriced: `\u672A\u8BA1\u4EF7 token`,
82
+ unpricedValue: `{count} \u8F6E`,
83
+ skipped: `\u672A\u53D6\u5230\u7528\u91CF\u7684\u8F6E\u6B21`,
84
+ noPricing: `\u672A\u914D\u7F6E\u5355\u4EF7`,
85
+ byRoute: `\u6309\u6A21\u578B`,
86
+ count: `{count} tok`,
87
+ note: `\u8D39\u7528\u6309\u5404\u6A21\u578B\u5355\u4EF7\u4F30\u7B97,\u5355\u4EF7\u53EF\u5728\u63D2\u4EF6\u8BE6\u60C5\u9875\u7684\u8D39\u7528\u914D\u7F6E\u4E2D\u4FEE\u6539\u3002`,
88
+ pluginTitle: `\u8D39\u7528`,
89
+ pluginDescription: `\u6309 token \u7528\u91CF\u4F30\u7B97\u4F1A\u8BDD\u4E0E\u6BCF\u8F6E\u8D39\u7528,\u53EF\u81EA\u5B9A\u4E49\u5404\u6A21\u578B\u5355\u4EF7\u3002`,
90
+ configSectionGeneral: `\u901A\u7528`,
91
+ configCurrency: `\u663E\u793A\u8D27\u5E01`,
92
+ configCnyPerUsd: `\u6C47\u7387(1 USD = ? CNY)`,
93
+ configShowDockChip: `\u663E\u793A\u4F1A\u8BDD\u8D39\u7528\u80F6\u56CA`,
94
+ configShowTurnChip: `\u663E\u793A\u8F6E\u6B21\u8D39\u7528\u80F6\u56CA`,
95
+ configPricing: `\u6A21\u578B\u5355\u4EF7`,
96
+ configSave: `\u4FDD\u5B58`,
97
+ configSaving: `\u4FDD\u5B58\u4E2D\u2026`,
98
+ configSaved: `\u5DF2\u4FDD\u5B58`,
99
+ configLoading: `\u8BFB\u53D6\u914D\u7F6E\u4E2D\u2026`,
100
+ configLoadFailed: `\u8BFB\u53D6\u914D\u7F6E\u5931\u8D25:{message}`,
101
+ configSyncConfirm: "\u540C\u6B65\u5C06\u8986\u76D6\u914D\u7F6E\u4E2D\u5339\u914D\u6A21\u578B\u7684\u4EF7\u683C\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",
102
+ configUpdated: "\u5DF2\u66F4\u65B0",
103
+ configPreserved: "\u4FDD\u7559\u539F\u4EF7",
104
+ configReset: "\u4EF7\u683C\u5F52\u96F6",
105
+ configFrom: "\u4ECE",
106
+ configPriceSource: "\u4EF7\u683C\u6765\u6E90",
107
+ configSync: "\u540C\u6B65\u4EF7\u683C",
108
+ configDirty: "\u8BF7\u5148\u4FDD\u5B58\u4FEE\u6539\uFF0C\u518D\u540C\u6B65\u4EF7\u683C\u3002",
109
+ configEmpty: "\u6682\u65E0\u6A21\u578B\u8C03\u7528\u5386\u53F2",
110
+ configReload: "\u91CD\u65B0\u52A0\u8F7D"
111
+ };
112
+ var en = {
113
+ modelName: `Model`,
114
+ initializing: `Initializing`,
115
+ sessionTitle: `Session cost`,
116
+ turnTitle: `Turn cost`,
117
+ costValue: `Cost {amount}`,
118
+ turnCost: `{amount}`,
119
+ input: `Uncached input`,
120
+ cacheRead: `Cache read`,
121
+ cacheWrite: `Cache write`,
122
+ output: `Output`,
123
+ model: `Provider / model`,
124
+ unpriced: `Unpriced tokens`,
125
+ unpricedValue: `{count}`,
126
+ skipped: `Turns without usage`,
127
+ noPricing: `no rate configured`,
128
+ byRoute: `By model`,
129
+ count: `{count} tok`,
130
+ note: `Estimated from per-model rates; edit them on this plugin's detail page.`,
131
+ pluginTitle: `Cost`,
132
+ pluginDescription: `Estimates session and per-turn cost from token usage. Model rates are editable.`,
133
+ configSectionGeneral: `General`,
134
+ configCurrency: `Currency`,
135
+ configCnyPerUsd: `Rate (1 USD = ? CNY)`,
136
+ configShowDockChip: `Show session cost chip`,
137
+ configShowTurnChip: `Show per-turn cost chip`,
138
+ configPricing: `Model rates`,
139
+ configSave: `Save`,
140
+ configSaving: `Saving\u2026`,
141
+ configSaved: `Saved`,
142
+ configLoading: `Loading configuration\u2026`,
143
+ configLoadFailed: `Could not load configuration: {message}`,
144
+ configSyncConfirm: "Sync will overwrite prices of matched configured models. Continue?",
145
+ configUpdated: "Updated",
146
+ configPreserved: "Preserved",
147
+ configReset: "Reset prices to zero",
148
+ configFrom: "From",
149
+ configPriceSource: "Price source",
150
+ configSync: "Sync prices",
151
+ configDirty: "Save changes before syncing prices.",
152
+ configEmpty: "No model call history",
153
+ configReload: "Reload"
154
+ };
155
+
156
+ // src/client/styles.css
157
+ var styles_default = ":root {\n --guojinpeng-cost-money: var(--dsw-alias-label-secondary);\n}\n.gdcc_dockRoot {\n box-sizing: border-box;\n min-width: 0;\n font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n display: inline-flex;\n justify-content: center;\n gap: 12px;\n}\n.gdcc_dockPill {\n box-sizing: border-box;\n corner-shape: round;\n max-width: 100%;\n color: var(--dsw-alias-label-tertiary);\n font: inherit;\n font-variant-numeric: tabular-nums;\n line-height: inherit;\n white-space: nowrap;\n background: 0 0;\n border: none;\n border-radius: 999px;\n align-items: center;\n gap: 6px;\n padding: 1px 8px;\n display: inline-flex;\n cursor: pointer;\n}\n.gdcc_dockPill svg {\n flex: none;\n width: 14px;\n height: 14px;\n}\n.gdcc_dockPill:hover,\n.gdcc_dockPill[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.gdcc_dockLabel {\n text-overflow: ellipsis;\n min-width: 0;\n overflow: hidden;\n}\n.gdcc_money {\n color: var(--guojinpeng-cost-money);\n font-variant-numeric: tabular-nums;\n}\n.gdcc_turnRoot {\n min-width: 0;\n display: inline-flex;\n order: 1;\n}\n.gdcc_turnTrigger {\n min-width: 0;\n height: calc(28px + var(--dsh-content-font-delta, 0px));\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-tertiary);\n font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px);\n font-variant-numeric: tabular-nums;\n line-height: calc(24px + var(--dsh-content-font-delta, 0px));\n white-space: nowrap;\n cursor: pointer;\n background: 0 0;\n border: none;\n align-items: center;\n gap: 4px;\n padding: 6px 8px;\n display: inline-flex;\n}\n.gdcc_turnLabel {\n text-overflow: ellipsis;\n min-width: 0;\n overflow: hidden;\n}\n.gdcc_turnTrigger svg {\n width: calc(15px + var(--dsh-content-font-delta, 0px));\n height: calc(15px + var(--dsh-content-font-delta, 0px));\n flex: none;\n}\n.gdcc_turnTrigger:hover,\n.gdcc_turnTrigger[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-tertiary);\n}\n.gdcc_initializing {\n pointer-events: none;\n}\n@media (width<=480px) {\n .gdcc_turnTrigger {\n width: calc(28px + var(--dsh-content-font-delta, 0px));\n justify-content: center;\n padding: 6px;\n }\n .gdcc_turnTrigger .gdcc_turnLabel {\n display: none;\n }\n}\n.gdcc_turnTrigger.gdcc_initializing {\n width: auto;\n}\n/* Cost detail popovers. */\n.gdcc_panel {\n z-index: 1100;\n box-sizing: border-box;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu);\n width: max-content;\n min-width: min(300px, 100vw - 24px);\n max-width: min(440px, 100vw - 24px);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n border: 0;\n padding: 16px;\n font-size: 12px;\n line-height: 18px;\n position: fixed;\n}\n.gdcc_title {\n color: var(--dsw-alias-label-primary);\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n font-weight: 500;\n display: flex;\n}\n.gdcc_titleRule {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin-bottom: 10px;\n}\n.gdcc_titleValue {\n font-variant-numeric: tabular-nums;\n}\n.gdcc_titleLabel {\n align-items: center;\n gap: 6px;\n min-width: 0;\n display: inline-flex;\n}\n.gdcc_titleLabel svg {\n flex: none;\n width: 14px;\n height: 14px;\n}\n.gdcc_details {\n color: var(--dsw-alias-label-tertiary);\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n display: grid;\n}\n.gdcc_details dt,\n.gdcc_details dd {\n min-width: 0;\n margin: 0;\n}\n.gdcc_details dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n.gdcc_details .gdcc_route {\n overflow-wrap: anywhere;\n}\n.gdcc_sub {\n color: var(--dsw-alias-label-tertiary);\n margin: 12px 0 6px;\n font-weight: 500;\n}\n.gdcc_note {\n color: var(--dsw-alias-label-tertiary);\n margin-top: 10px;\n}\n/* Plugin configuration page. */\n.gdcc_form {\n border: 0;\n padding: 0;\n margin: 0;\n min-width: 0;\n display: grid;\n gap: 12px;\n max-width: 760px;\n}\n.gdcc_fields {\n display: grid;\n gap: 8px;\n}\n.gdcc_field {\n display: grid;\n grid-template-columns: minmax(96px, auto) minmax(0, 1fr);\n align-items: center;\n gap: 12px;\n}\n.gdcc_fieldLabel {\n color: var(--dsw-alias-label-secondary);\n}\n.gdcc_input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n font: inherit;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-sm);\n padding: 4px 8px;\n}\n.gdcc_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: not-allowed;\n}\n.gdcc_input[type='checkbox'] {\n width: auto;\n justify-self: start;\n}\n.gdcc_rows {\n display: grid;\n gap: 6px;\n overflow-x: auto;\n}\n.gdcc_model {\n overflow-wrap: anywhere;\n}\n.gdcc_row {\n min-width: 620px;\n}\n.gdcc_actions {\n flex-wrap: wrap;\n}\n.gdcc_ok,\n.gdcc_error {\n overflow-wrap: anywhere;\n min-width: 0;\n}\n.gdcc_row {\n display: grid;\n grid-template-columns: minmax(0, 1.6fr) repeat(4, minmax(0, 1fr)) auto;\n align-items: center;\n gap: 6px;\n}\n.gdcc_rowHead {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n.gdcc_provider {\n min-width: 620px;\n margin-top: 8px;\n padding: 8px 0 5px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.gdcc_model {\n padding-left: 12px;\n}\n.gdcc_btn {\n font: inherit;\n color: var(--dsw-alias-label-secondary);\n background: 0 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-sm);\n padding: 3px 10px;\n cursor: pointer;\n}\n.gdcc_btn:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.gdcc_btn:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: not-allowed;\n}\n.gdcc_btnPrimary {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-border-l2);\n}\n.gdcc_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n justify-content: flex-end;\n}\n.gdcc_hint {\n color: var(--dsw-alias-label-tertiary);\n}\n.gdcc_error {\n color: var(--dsw-alias-label-error, var(--dsw-alias-label-primary));\n}\n.gdcc_ok {\n color: var(--dsw-alias-label-tertiary);\n}\n";
158
+
159
+ // src/client/styles.js
160
+ function installStyles() {
161
+ const CSS_TAG = `${PLUGIN_ID}/cost-chip.module.css`;
162
+ if (typeof document !== `undefined` && document.querySelector(`style[data-plugin-css=` + JSON.stringify(CSS_TAG) + `]`) === null) {
163
+ const tag = document.createElement(`style`);
164
+ tag.dataset.plugin = PACKAGE_ID;
165
+ tag.dataset.pluginCss = CSS_TAG;
166
+ tag.textContent = styles_default;
167
+ document.head.appendChild(tag);
168
+ }
169
+ }
170
+
1
171
  // src/pricing.js
2
172
  var RATE_KEYS = ["input", "output", "cacheRead", "cacheWrite"];
3
173
  var zeroRate = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
@@ -85,72 +255,341 @@ function buildCostView(accounting, config) {
85
255
  };
86
256
  }
87
257
 
88
- // src/identity.js
89
- var PACKAGE_ID = "@guojinpeng/dsh-cost-meter";
90
- var PLUGIN_ID = "guojinpeng/dsh-cost-meter";
91
- var HTTP_BASE = "/guojinpeng/dsh-cost-meter";
92
- var PROJECTION_KEY = "guojinpengCostUsage";
93
- var LOCALE_NAMESPACE = "guojinpengDshCostMeter";
258
+ // src/client/cost-view.js
259
+ function createUseCostView(react, store) {
260
+ function useCostView(props) {
261
+ const raw = props.useProjection(PROJECTION_KEY);
262
+ const snapshot = react.useSyncExternalStore(store.subscribe, store.getSnapshot);
263
+ const loadStatus = react.useSyncExternalStore(store.subscribe, store.getLoadStatus);
264
+ const missing = snapshot ? (raw == null ? void 0 : raw.accounting) && Object.keys(raw.accounting.byRoute).some((key) => key && !snapshot.config.pricing[key]) : !raw || raw.unpricedTokens !== 0;
265
+ react.useEffect(() => {
266
+ if (!snapshot || (raw == null ? void 0 : raw.accounting) && Object.keys(raw.accounting.byRoute).some((key) => key && !snapshot.config.pricing[key]))
267
+ store.load().catch(() => {
268
+ });
269
+ }, [raw]);
270
+ return react.useMemo(() => {
271
+ const view = (raw == null ? void 0 : raw.accounting) && snapshot ? buildCostView(raw.accounting, snapshot.config) : raw;
272
+ return missing && loadStatus !== "error" ? { ...view, initializing: true } : view;
273
+ }, [raw, snapshot, missing, loadStatus]);
274
+ }
275
+ return useCostView;
276
+ }
94
277
 
95
- // src/client-store.js
96
- function createConfigStore(fetcher = (...args) => fetch(...args)) {
97
- let snapshot = null, sequence = 0, loading = null, loadStatus = "idle";
98
- const listeners = /* @__PURE__ */ new Set();
99
- const notify = () => {
100
- for (const listener of listeners) listener();
101
- };
102
- const publish = (value) => {
103
- snapshot = value;
104
- notify();
105
- return value;
106
- };
107
- async function request(path, body) {
108
- const response = await fetcher(
109
- `${HTTP_BASE}/${path}`,
110
- body === void 0 ? void 0 : {
111
- method: "POST",
112
- headers: { "content-type": "application/json" },
113
- body: JSON.stringify(body)
278
+ // src/client/cost-dialog.js
279
+ function createCostDialog(react, reactDom) {
280
+ const h = react.createElement;
281
+ function overlayTopMargin(min) {
282
+ const root = document.documentElement;
283
+ const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue(`--dsh-frame-top-clearance`));
284
+ if (Number.isNaN(clearance)) return min;
285
+ return Math.max(min, (root.hasAttribute(`data-fullscreen`) ? 0 : clearance) + 20);
286
+ }
287
+ function useAnchoredPosition(options) {
288
+ const { open, anchorRef, panelRef, side = `bottom`, align = `start`, gap, margin } = options;
289
+ const [position, setPosition] = react.useState(null);
290
+ react.useLayoutEffect(() => {
291
+ if (!open) {
292
+ setPosition(null);
293
+ return void 0;
294
+ }
295
+ const place = () => {
296
+ var _a;
297
+ const rect = (_a = anchorRef.current) == null ? void 0 : _a.getBoundingClientRect();
298
+ if (rect === void 0) return;
299
+ const panel3 = panelRef.current;
300
+ const width = (panel3 == null ? void 0 : panel3.offsetWidth) ?? 0;
301
+ const height = (panel3 == null ? void 0 : panel3.offsetHeight) ?? 0;
302
+ let left = align === `end` ? rect.right - width : rect.left;
303
+ let top = side === `top` ? rect.top - gap - height : rect.bottom + gap;
304
+ if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
305
+ if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin);
306
+ setPosition({ left, top });
307
+ };
308
+ place();
309
+ window.addEventListener(`scroll`, place, true);
310
+ window.addEventListener(`resize`, place);
311
+ const panel2 = panelRef.current;
312
+ let observer = null;
313
+ if (typeof ResizeObserver !== `undefined` && panel2 !== null) {
314
+ observer = new ResizeObserver(place);
315
+ observer.observe(panel2);
114
316
  }
317
+ return () => {
318
+ observer == null ? void 0 : observer.disconnect();
319
+ window.removeEventListener(`scroll`, place, true);
320
+ window.removeEventListener(`resize`, place);
321
+ };
322
+ }, [open, anchorRef, panelRef, side, align, gap, margin]);
323
+ return position;
324
+ }
325
+ function useDismissOnOutsidePointer(root, open, setOpen, portal) {
326
+ react.useEffect(() => {
327
+ if (!open) return void 0;
328
+ const closeOutside = (event) => {
329
+ var _a, _b;
330
+ if (event.target instanceof Node && ((_a = root.current) == null ? void 0 : _a.contains(event.target)) !== true && ((_b = portal == null ? void 0 : portal.current) == null ? void 0 : _b.contains(event.target)) !== true) {
331
+ setOpen(false);
332
+ }
333
+ };
334
+ document.addEventListener(`pointerdown`, closeOutside);
335
+ return () => {
336
+ document.removeEventListener(`pointerdown`, closeOutside);
337
+ };
338
+ }, [root, open, setOpen, portal]);
339
+ }
340
+ function IconGaugeOutlineRegular(props) {
341
+ const { size = 16, className } = props ?? {};
342
+ return h(
343
+ `svg`,
344
+ {
345
+ width: size,
346
+ height: size,
347
+ className,
348
+ viewBox: `0 0 16 16`,
349
+ fill: `none`,
350
+ xmlns: `http://www.w3.org/2000/svg`,
351
+ "aria-hidden": `true`,
352
+ strokeWidth: 1
353
+ },
354
+ h(`path`, {
355
+ 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`,
356
+ stroke: `currentColor`
357
+ }),
358
+ h(`path`, { d: `M8 8.49994L11.6114 4.88855`, stroke: `currentColor` }),
359
+ h(`path`, {
360
+ 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`,
361
+ fill: `currentColor`
362
+ })
115
363
  );
116
- const payload = await response.json();
117
- if (!response.ok) throw new Error(payload.error ?? `HTTP ${response.status}`);
118
- return payload;
119
364
  }
120
- return {
121
- getSnapshot: () => snapshot,
122
- getLoadStatus: () => loadStatus,
123
- subscribe: (listener) => {
124
- listeners.add(listener);
125
- return () => listeners.delete(listener);
126
- },
127
- load: () => {
128
- if (loading) return loading;
129
- const ticket = ++sequence;
130
- loadStatus = "loading";
131
- loading = request("config").then((value) => ticket === sequence ? publish(value) : snapshot ?? value).catch((error) => {
132
- loadStatus = "error";
133
- throw error;
134
- }).finally(() => {
135
- loading = null;
136
- if (loadStatus !== "error") loadStatus = "ready";
137
- notify();
138
- });
139
- notify();
140
- return loading;
141
- },
142
- save: async (config) => {
143
- const ticket = ++sequence;
144
- const value = await request("config", { config, revision: snapshot == null ? void 0 : snapshot.revision });
145
- return ticket === sequence ? publish(value) : snapshot;
146
- },
147
- sync: async () => {
148
- const ticket = ++sequence;
149
- const value = await request("pricing/sync", { source: "models.dev", revision: snapshot == null ? void 0 : snapshot.revision });
150
- return ticket === sequence ? publish(value) : snapshot;
365
+ const PANEL_MARGIN = 12;
366
+ const PANEL_GAP = 8;
367
+ const MEASURE_STYLE = { visibility: `hidden`, left: 0, top: 0 };
368
+ function useCostDialog() {
369
+ const [open, setOpen] = react.useState(false);
370
+ const rootRef = react.useRef(null);
371
+ const panelRef = react.useRef(null);
372
+ const pos = useAnchoredPosition({
373
+ open,
374
+ anchorRef: rootRef,
375
+ panelRef,
376
+ side: `top`,
377
+ gap: PANEL_GAP,
378
+ margin: PANEL_MARGIN
379
+ });
380
+ useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
381
+ react.useEffect(() => {
382
+ if (!open) return void 0;
383
+ const onKeyDown = (event) => {
384
+ if (event.key === `Escape`) setOpen(false);
385
+ };
386
+ document.addEventListener(`keydown`, onKeyDown);
387
+ return () => document.removeEventListener(`keydown`, onKeyDown);
388
+ }, [open]);
389
+ return { open, setOpen, rootRef, panelRef, pos };
390
+ }
391
+ function row(label, value, key) {
392
+ if (value === null || value === void 0 || value === false) return null;
393
+ return [h(`dt`, { key: key + `-k` }, label), h(`dd`, { key: key + `-v` }, value)];
394
+ }
395
+ function details(items, key) {
396
+ const children = [];
397
+ for (const item of items) {
398
+ const cells = row(item[0], item[1], `r${children.length}`);
399
+ if (cells !== null) children.push(cells[0], cells[1]);
151
400
  }
152
- };
401
+ return h(`dl`, { className: `gdcc_details`, key }, children);
402
+ }
403
+ function panel(props) {
404
+ return reactDom.createPortal(
405
+ h(`div`, {
406
+ ref: props.panelRef,
407
+ className: `gdcc_panel`,
408
+ role: `dialog`,
409
+ "aria-label": props.title,
410
+ style: props.pos ?? MEASURE_STYLE,
411
+ children: [
412
+ h(`div`, { className: `gdcc_title`, key: `title` }, [
413
+ h(`span`, { className: `gdcc_titleLabel`, key: `l` }, [
414
+ h(props.icon, { key: `i` }),
415
+ h(`span`, { key: `t` }, props.title)
416
+ ]),
417
+ h(`span`, { className: `gdcc_titleValue`, key: `v` }, props.value)
418
+ ]),
419
+ h(`div`, { className: `gdcc_titleRule`, key: `rule` }),
420
+ react.Children.toArray(props.children)
421
+ ]
422
+ }),
423
+ document.body
424
+ );
425
+ }
426
+ return { useCostDialog, panel, details, IconGaugeOutlineRegular };
153
427
  }
428
+
429
+ // src/client/format.js
430
+ function formatExactTokens(value) {
431
+ if (typeof value !== `number` || !Number.isFinite(value)) return `0`;
432
+ const sign = value < 0 ? `-` : ``;
433
+ const digits = String(Math.abs(Math.round(value)));
434
+ const groups = [];
435
+ for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
436
+ return sign + groups.join(`,`);
437
+ }
438
+ function toDisplay(micros, currency, cnyPerUsd) {
439
+ const usd = micros / 1e6;
440
+ return currency === `CNY` ? usd * (cnyPerUsd || 7.2) : usd;
441
+ }
442
+ function symbolOf(currency) {
443
+ if (currency === `CNY`) return `\xA5`;
444
+ if (currency === `USD`) return `$`;
445
+ return currency ? currency + ` ` : ``;
446
+ }
447
+ function formatMoney(micros, currency, cnyPerUsd) {
448
+ if (micros === null || micros === void 0) return `\u2014`;
449
+ const value = toDisplay(micros, currency, cnyPerUsd);
450
+ const symbol = symbolOf(currency);
451
+ if (value === 0) return symbol + `0.00`;
452
+ if (Math.abs(value) < 0.01) return symbol + value.toFixed(4);
453
+ if (Math.abs(value) < 1) return symbol + value.toFixed(3);
454
+ return symbol + value.toFixed(2);
455
+ }
456
+ function formatChipMoney(micros, currency, cnyPerUsd) {
457
+ if (micros === null || micros === void 0) return `\u2014`;
458
+ const value = toDisplay(micros, currency, cnyPerUsd);
459
+ const symbol = symbolOf(currency);
460
+ if (value === 0) return symbol + `0.00`;
461
+ if (Math.abs(value) < 0.01) return symbol + `<0.01`;
462
+ return symbol + value.toFixed(2);
463
+ }
464
+
465
+ // src/client/cost-chips.js
466
+ function createCostChips(react, useCostView, dialogs) {
467
+ const h = react.createElement;
468
+ const { useCostDialog, panel, details, IconGaugeOutlineRegular } = dialogs;
469
+ function InitializingChip({ t, turn }) {
470
+ return h(
471
+ "span",
472
+ { className: turn ? "gdcc_turnRoot" : "gdcc_dockRoot", role: "status", "aria-busy": true },
473
+ h(
474
+ "span",
475
+ { className: (turn ? "gdcc_turnTrigger" : "gdcc_dockPill") + " gdcc_initializing", title: t("initializing") },
476
+ [h(IconGaugeOutlineRegular, { key: "icon" }), h("span", { key: "label" }, t("initializing"))]
477
+ )
478
+ );
479
+ }
480
+ function CostDockChip(props) {
481
+ const view = useCostView(props);
482
+ const dialog = useCostDialog();
483
+ const t = props.t;
484
+ if (view === void 0 || view === null || view.showDock === false) return null;
485
+ if (view.initializing) return h(InitializingChip, { t });
486
+ const label = t(`costValue`, { amount: formatChipMoney(view.totalMicros, view.currency, view.cnyPerUsd) });
487
+ const buckets = view.bucketMicros ?? { input: 0, output: 0, read: 0, write: 0 };
488
+ const items = [
489
+ [t(`input`), formatMoney(buckets.input, view.currency, view.cnyPerUsd)],
490
+ [t(`cacheRead`), formatMoney(buckets.read, view.currency, view.cnyPerUsd)],
491
+ [t(`cacheWrite`), formatMoney(buckets.write, view.currency, view.cnyPerUsd)],
492
+ [t(`output`), formatMoney(buckets.output, view.currency, view.cnyPerUsd)]
493
+ ];
494
+ if (view.skippedTurns > 0) items.push([t(`skipped`), t(`unpricedValue`, { count: view.skippedTurns })]);
495
+ if (view.unpricedTokens > 0) items.push([t(`unpriced`), t(`count`, { count: view.unpricedTokens })]);
496
+ const routes = view.routes ?? [];
497
+ const routeRows = routes.length === 0 ? null : h(`div`, { key: `routes` }, [
498
+ h(`div`, { className: `gdcc_sub`, key: `h` }, t(`byRoute`)),
499
+ details(
500
+ routes.flatMap((entry) => [
501
+ [
502
+ entry.route,
503
+ formatMoney(entry.micros, view.currency, view.cnyPerUsd) + ` \xB7 ` + t(`count`, { count: formatExactTokens(entry.totalTokens) })
504
+ ]
505
+ ]),
506
+ `routes`
507
+ )
508
+ ]);
509
+ return h(`span`, { className: `gdcc_dockRoot`, ref: dialog.rootRef }, [
510
+ h(
511
+ `button`,
512
+ {
513
+ key: `b`,
514
+ type: `button`,
515
+ className: `gdcc_dockPill`,
516
+ "aria-haspopup": `dialog`,
517
+ "aria-expanded": dialog.open,
518
+ onClick: () => dialog.setOpen(!dialog.open)
519
+ },
520
+ [h(IconGaugeOutlineRegular, { key: `i` }), h(`span`, { className: `gdcc_dockLabel`, key: `l` }, label)]
521
+ ),
522
+ dialog.open ? panel({
523
+ panelRef: dialog.panelRef,
524
+ pos: dialog.pos,
525
+ title: t(`sessionTitle`),
526
+ value: formatMoney(view.totalMicros, view.currency, view.cnyPerUsd),
527
+ icon: IconGaugeOutlineRegular,
528
+ children: [details(items, `main`), routeRows, h(`div`, { className: `gdcc_note`, key: `n` }, t(`note`))]
529
+ }) : null
530
+ ]);
531
+ }
532
+ function CostTurnAction(props) {
533
+ const turn = props.useChat((snapshot) => {
534
+ const tail = snapshot.nodes.values().find((node) => {
535
+ var _a;
536
+ return node.kind === `turn-tail` && ((_a = node.data.closing) == null ? void 0 : _a.finalNode.messageId) === props.messageId;
537
+ });
538
+ return tail == null ? void 0 : tail.data.turn;
539
+ });
540
+ return turn === void 0 ? null : h(CostTurnChip, { ...props, turn });
541
+ }
542
+ function CostTurnChip(props) {
543
+ const view = useCostView(props);
544
+ const dialog = useCostDialog();
545
+ const t = props.t;
546
+ if (view === void 0 || view === null || view.showTurn === false) return null;
547
+ if (view.initializing) return h(InitializingChip, { t, turn: true });
548
+ const raw = props.turn;
549
+ const turnNo = raw !== null && typeof raw === `object` ? raw.turn : raw;
550
+ if (!Number.isSafeInteger(turnNo) || turnNo < 1) return null;
551
+ const entry = view.turns === void 0 ? void 0 : view.turns[String(turnNo)];
552
+ if (entry === void 0) return null;
553
+ const label = entry.priced ? formatChipMoney(entry.micros, view.currency, view.cnyPerUsd) : t(`noPricing`);
554
+ const priced = entry.priced === true;
555
+ const money = entry.bucketMicros;
556
+ const items = [
557
+ [t(`model`), entry.route === `` ? null : entry.route],
558
+ priced ? [t(`input`), formatMoney(money.input, view.currency, view.cnyPerUsd)] : [t(`input`), t(`noPricing`)],
559
+ priced ? [t(`cacheRead`), formatMoney(money.read, view.currency, view.cnyPerUsd)] : null,
560
+ priced ? [t(`cacheWrite`), formatMoney(money.write, view.currency, view.cnyPerUsd)] : null,
561
+ priced ? [t(`output`), formatMoney(money.output, view.currency, view.cnyPerUsd)] : null
562
+ ].filter((item) => item !== null);
563
+ return h(`span`, { className: `gdcc_turnRoot`, ref: dialog.rootRef }, [
564
+ h(
565
+ `button`,
566
+ {
567
+ key: `b`,
568
+ type: `button`,
569
+ className: `gdcc_turnTrigger`,
570
+ "aria-haspopup": `dialog`,
571
+ "aria-expanded": dialog.open,
572
+ onClick: () => dialog.setOpen(!dialog.open)
573
+ },
574
+ [
575
+ h(IconGaugeOutlineRegular, { key: `i` }),
576
+ h(`span`, { className: `gdcc_turnLabel`, key: `l` }, t(`turnCost`, { amount: label }))
577
+ ]
578
+ ),
579
+ dialog.open ? panel({
580
+ panelRef: dialog.panelRef,
581
+ pos: dialog.pos,
582
+ title: t(`turnTitle`),
583
+ value: formatMoney(entry.micros, view.currency, view.cnyPerUsd),
584
+ icon: IconGaugeOutlineRegular,
585
+ children: [details(items, `turn`)]
586
+ }) : null
587
+ ]);
588
+ }
589
+ return { InitializingChip, CostDockChip, CostTurnChip, CostTurnAction };
590
+ }
591
+
592
+ // src/client/config-draft.js
154
593
  function buildDraft(payload) {
155
594
  const config = payload.config;
156
595
  return {
@@ -182,685 +621,272 @@ function configOf(draft) {
182
621
  };
183
622
  }
184
623
 
185
- // src/client.js
186
- window.__ModuleLoader__.load({
187
- id: PACKAGE_ID,
188
- factory: (e) => {
189
- var t = { exports: {} }, n = t.exports;
190
- Object.defineProperty(n, Symbol.toStringTag, { value: `Module` });
191
- const react = e(`react`);
192
- const reactDom = e(`react-dom`);
193
- const h = react.createElement;
194
- const store = createConfigStore();
195
- function useCostView(props) {
196
- const raw = props.useProjection(PROJECTION_KEY);
197
- const snapshot = react.useSyncExternalStore(store.subscribe, store.getSnapshot);
198
- const loadStatus = react.useSyncExternalStore(store.subscribe, store.getLoadStatus);
199
- const missing = !snapshot || (raw == null ? void 0 : raw.accounting) && Object.keys(raw.accounting.byRoute).some((key) => key && !snapshot.config.pricing[key]);
200
- react.useEffect(() => {
201
- if (!snapshot || (raw == null ? void 0 : raw.accounting) && Object.keys(raw.accounting.byRoute).some((key) => key && !snapshot.config.pricing[key]))
202
- store.load().catch(() => {
203
- });
204
- }, [raw]);
205
- return react.useMemo(() => {
206
- const view = (raw == null ? void 0 : raw.accounting) && snapshot ? buildCostView(raw.accounting, snapshot.config) : raw;
207
- return missing && loadStatus !== "error" ? { ...view, initializing: true } : view;
208
- }, [raw, snapshot, missing, loadStatus]);
209
- }
210
- const NS = LOCALE_NAMESPACE;
211
- const CSS_TAG = `${PLUGIN_ID}/cost-chip.module.css`;
212
- const CSS = `:root{--guojinpeng-cost-money:var(--dsw-alias-label-secondary)}
213
- .gdcc_dockRoot{box-sizing:border-box;min-width:0;font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));display:inline-flex;justify-content:center;gap:12px}
214
- .gdcc_dockPill{box-sizing:border-box;corner-shape:round;max-width:100%;color:var(--dsw-alias-label-tertiary);font:inherit;font-variant-numeric:tabular-nums;line-height:inherit;white-space:nowrap;background:0 0;border:none;border-radius:999px;align-items:center;gap:6px;padding:1px 8px;display:inline-flex;cursor:pointer}
215
- .gdcc_dockPill svg{flex:none;width:14px;height:14px}
216
- .gdcc_dockPill:hover,.gdcc_dockPill[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
217
- .gdcc_dockLabel{text-overflow:ellipsis;min-width:0;overflow:hidden}
218
- .gdcc_money{color:var(--guojinpeng-cost-money);font-variant-numeric:tabular-nums}
219
- .gdcc_turnRoot{min-width:0;display:inline-flex;order:1}
220
- .gdcc_turnTrigger{min-width:0;height:calc(28px + var(--dsh-content-font-delta,0px));border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-tertiary);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);font-variant-numeric:tabular-nums;line-height:calc(24px + var(--dsh-content-font-delta,0px));white-space:nowrap;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:6px 8px;display:inline-flex}
221
- .gdcc_turnLabel{text-overflow:ellipsis;min-width:0;overflow:hidden}
222
- .gdcc_turnTrigger svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px));flex:none}
223
- .gdcc_turnTrigger:hover,.gdcc_turnTrigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}
224
- .gdcc_initializing{pointer-events:none}
225
- @media (width<=480px){.gdcc_turnTrigger{width:calc(28px + var(--dsh-content-font-delta,0px));justify-content:center;padding:6px}.gdcc_turnTrigger .gdcc_turnLabel{display:none}}
226
- .gdcc_turnTrigger.gdcc_initializing{width:auto}
227
- .gdcc_panel{z-index:1100;box-sizing:border-box;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu);width:max-content;min-width:min(300px,100vw - 24px);max-width:min(440px,100vw - 24px);backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;padding:16px;font-size:12px;line-height:18px;position:fixed}
228
- .gdcc_title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}
229
- .gdcc_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}
230
- .gdcc_titleValue{font-variant-numeric:tabular-nums}
231
- .gdcc_titleLabel{align-items:center;gap:6px;min-width:0;display:inline-flex}
232
- .gdcc_titleLabel svg{flex:none;width:14px;height:14px}
233
- .gdcc_details{color:var(--dsw-alias-label-tertiary);grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;display:grid}
234
- .gdcc_details dt,.gdcc_details dd{min-width:0;margin:0}
235
- .gdcc_details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}
236
- .gdcc_details .gdcc_route{overflow-wrap:anywhere}
237
- .gdcc_sub{color:var(--dsw-alias-label-tertiary);margin:12px 0 6px;font-weight:500}
238
- .gdcc_note{color:var(--dsw-alias-label-tertiary);margin-top:10px}
239
- .gdcc_form{border:0;padding:0;margin:0;min-width:0;display:grid;gap:12px;max-width:760px}
240
- .gdcc_fields{display:grid;gap:8px}
241
- .gdcc_field{display:grid;grid-template-columns:minmax(96px,auto) minmax(0,1fr);align-items:center;gap:12px}
242
- .gdcc_fieldLabel{color:var(--dsw-alias-label-secondary)}
243
- .gdcc_input{box-sizing:border-box;width:100%;min-width:0;font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-sm);padding:4px 8px}
244
- .gdcc_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
245
- .gdcc_input[type=checkbox]{width:auto;justify-self:start}
246
- .gdcc_rows{display:grid;gap:6px;overflow-x:auto}.gdcc_model{overflow-wrap:anywhere}.gdcc_row{min-width:620px}.gdcc_actions{flex-wrap:wrap}.gdcc_ok,.gdcc_error{overflow-wrap:anywhere;min-width:0}
247
- .gdcc_row{display:grid;grid-template-columns:minmax(0,1.6fr) repeat(4,minmax(0,1fr)) auto;align-items:center;gap:6px}
248
- .gdcc_rowHead{color:var(--dsw-alias-label-tertiary);font-size:11px}
249
- .gdcc_provider{min-width:620px;margin-top:8px;padding:8px 0 5px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);font-weight:500;overflow-wrap:anywhere}
250
- .gdcc_model{padding-left:12px}
251
- .gdcc_btn{font:inherit;color:var(--dsw-alias-label-secondary);background:0 0;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-sm);padding:3px 10px;cursor:pointer}
252
- .gdcc_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
253
- .gdcc_btn:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
254
- .gdcc_btnPrimary{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
255
- .gdcc_actions{display:flex;align-items:center;gap:8px;justify-content:flex-end}
256
- .gdcc_hint{color:var(--dsw-alias-label-tertiary)}
257
- .gdcc_error{color:var(--dsw-alias-label-error,var(--dsw-alias-label-primary))}
258
- .gdcc_ok{color:var(--dsw-alias-label-tertiary)}`;
259
- if (typeof document !== `undefined` && document.querySelector(`style[data-plugin-css=` + JSON.stringify(CSS_TAG) + `]`) === null) {
260
- const tag = document.createElement(`style`);
261
- tag.dataset.plugin = PACKAGE_ID;
262
- tag.dataset.pluginCss = CSS_TAG;
263
- tag.textContent = CSS;
264
- document.head.appendChild(tag);
265
- }
266
- function formatExactTokens(value) {
267
- if (typeof value !== `number` || !Number.isFinite(value)) return `0`;
268
- const sign = value < 0 ? `-` : ``;
269
- const digits = String(Math.abs(Math.round(value)));
270
- const groups = [];
271
- for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
272
- return sign + groups.join(`,`);
273
- }
274
- function toDisplay(micros, currency, cnyPerUsd) {
275
- const usd = micros / 1e6;
276
- return currency === `CNY` ? usd * (cnyPerUsd || 7.2) : usd;
277
- }
278
- function symbolOf(currency) {
279
- if (currency === `CNY`) return `\xA5`;
280
- if (currency === `USD`) return `$`;
281
- return currency ? currency + ` ` : ``;
282
- }
283
- function formatMoney(micros, currency, cnyPerUsd) {
284
- if (micros === null || micros === void 0) return `\u2014`;
285
- const value = toDisplay(micros, currency, cnyPerUsd);
286
- const symbol = symbolOf(currency);
287
- if (value === 0) return symbol + `0.00`;
288
- if (Math.abs(value) < 0.01) return symbol + value.toFixed(4);
289
- if (Math.abs(value) < 1) return symbol + value.toFixed(3);
290
- return symbol + value.toFixed(2);
291
- }
292
- function formatChipMoney(micros, currency, cnyPerUsd) {
293
- if (micros === null || micros === void 0) return `\u2014`;
294
- const value = toDisplay(micros, currency, cnyPerUsd);
295
- const symbol = symbolOf(currency);
296
- if (value === 0) return symbol + `0.00`;
297
- if (Math.abs(value) < 0.01) return symbol + `<0.01`;
298
- return symbol + value.toFixed(2);
299
- }
300
- function overlayTopMargin(min) {
301
- const root = document.documentElement;
302
- const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue(`--dsh-frame-top-clearance`));
303
- if (Number.isNaN(clearance)) return min;
304
- return Math.max(min, (root.hasAttribute(`data-fullscreen`) ? 0 : clearance) + 20);
305
- }
306
- function useAnchoredPosition(options) {
307
- const { open, anchorRef, panelRef, side = `bottom`, align = `start`, gap, margin } = options;
308
- const [position, setPosition] = react.useState(null);
309
- react.useLayoutEffect(() => {
310
- if (!open) {
311
- setPosition(null);
312
- return void 0;
313
- }
314
- const place = () => {
315
- var _a;
316
- const rect = (_a = anchorRef.current) == null ? void 0 : _a.getBoundingClientRect();
317
- if (rect === void 0) return;
318
- const panel3 = panelRef.current;
319
- const width = (panel3 == null ? void 0 : panel3.offsetWidth) ?? 0;
320
- const height = (panel3 == null ? void 0 : panel3.offsetHeight) ?? 0;
321
- let left = align === `end` ? rect.right - width : rect.left;
322
- let top = side === `top` ? rect.top - gap - height : rect.bottom + gap;
323
- if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
324
- if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin);
325
- setPosition({ left, top });
326
- };
327
- place();
328
- window.addEventListener(`scroll`, place, true);
329
- window.addEventListener(`resize`, place);
330
- const panel2 = panelRef.current;
331
- let observer = null;
332
- if (typeof ResizeObserver !== `undefined` && panel2 !== null) {
333
- observer = new ResizeObserver(place);
334
- observer.observe(panel2);
335
- }
336
- return () => {
337
- observer == null ? void 0 : observer.disconnect();
338
- window.removeEventListener(`scroll`, place, true);
339
- window.removeEventListener(`resize`, place);
340
- };
341
- }, [open, anchorRef, panelRef, side, align, gap, margin]);
342
- return position;
343
- }
344
- function useDismissOnOutsidePointer(root, open, setOpen, portal) {
345
- react.useEffect(() => {
346
- if (!open) return void 0;
347
- const closeOutside = (event) => {
348
- var _a, _b;
349
- if (event.target instanceof Node && ((_a = root.current) == null ? void 0 : _a.contains(event.target)) !== true && ((_b = portal == null ? void 0 : portal.current) == null ? void 0 : _b.contains(event.target)) !== true) {
350
- setOpen(false);
351
- }
352
- };
353
- document.addEventListener(`pointerdown`, closeOutside);
354
- return () => {
355
- document.removeEventListener(`pointerdown`, closeOutside);
356
- };
357
- }, [root, open, setOpen, portal]);
358
- }
359
- function IconGaugeOutlineRegular(props) {
360
- const { size = 16, className } = props ?? {};
361
- return h(
362
- `svg`,
363
- {
364
- width: size,
365
- height: size,
366
- className,
367
- viewBox: `0 0 16 16`,
368
- fill: `none`,
369
- xmlns: `http://www.w3.org/2000/svg`,
370
- "aria-hidden": `true`,
371
- strokeWidth: 1
372
- },
373
- h(`path`, {
374
- 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`,
375
- stroke: `currentColor`
376
- }),
377
- h(`path`, { d: `M8 8.49994L11.6114 4.88855`, stroke: `currentColor` }),
378
- h(`path`, {
379
- 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`,
380
- fill: `currentColor`
381
- })
382
- );
383
- }
384
- const PANEL_MARGIN = 12;
385
- const PANEL_GAP = 8;
386
- const MEASURE_STYLE = { visibility: `hidden`, left: 0, top: 0 };
387
- function useCostDialog() {
388
- const [open, setOpen] = react.useState(false);
389
- const rootRef = react.useRef(null);
390
- const panelRef = react.useRef(null);
391
- const pos = useAnchoredPosition({
392
- open,
393
- anchorRef: rootRef,
394
- panelRef,
395
- side: `top`,
396
- gap: PANEL_GAP,
397
- margin: PANEL_MARGIN
398
- });
399
- useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
400
- react.useEffect(() => {
401
- if (!open) return void 0;
402
- const onKeyDown = (event) => {
403
- if (event.key === `Escape`) setOpen(false);
404
- };
405
- document.addEventListener(`keydown`, onKeyDown);
406
- return () => document.removeEventListener(`keydown`, onKeyDown);
407
- }, [open]);
408
- return { open, setOpen, rootRef, panelRef, pos };
409
- }
410
- function row(label, value, key) {
411
- if (value === null || value === void 0 || value === false) return null;
412
- return [h(`dt`, { key: key + `-k` }, label), h(`dd`, { key: key + `-v` }, value)];
413
- }
414
- function details(items, key) {
415
- const children = [];
416
- for (const item of items) {
417
- const cells = row(item[0], item[1], `r${children.length}`);
418
- if (cells !== null) children.push(cells[0], cells[1]);
419
- }
420
- return h(`dl`, { className: `gdcc_details`, key }, children);
421
- }
422
- function panel(props) {
423
- return reactDom.createPortal(
424
- h(`div`, {
425
- ref: props.panelRef,
426
- className: `gdcc_panel`,
427
- role: `dialog`,
428
- "aria-label": props.title,
429
- style: props.pos ?? MEASURE_STYLE,
430
- children: [
431
- h(`div`, { className: `gdcc_title`, key: `title` }, [
432
- h(`span`, { className: `gdcc_titleLabel`, key: `l` }, [
433
- h(props.icon, { key: `i` }),
434
- h(`span`, { key: `t` }, props.title)
435
- ]),
436
- h(`span`, { className: `gdcc_titleValue`, key: `v` }, props.value)
437
- ]),
438
- h(`div`, { className: `gdcc_titleRule`, key: `rule` }),
439
- react.Children.toArray(props.children)
440
- ]
441
- }),
442
- document.body
443
- );
444
- }
445
- function InitializingChip({ t: t2, turn }) {
446
- return h(
447
- "span",
448
- { className: turn ? "gdcc_turnRoot" : "gdcc_dockRoot", role: "status", "aria-busy": true },
449
- h(
450
- "span",
451
- { className: (turn ? "gdcc_turnTrigger" : "gdcc_dockPill") + " gdcc_initializing", title: t2("initializing") },
452
- [h(IconGaugeOutlineRegular, { key: "icon" }), h("span", { key: "label" }, t2("initializing"))]
453
- )
454
- );
455
- }
456
- function CostDockChip(props) {
457
- const view = useCostView(props);
458
- const dialog = useCostDialog();
459
- const t2 = props.t;
460
- if (view === void 0 || view === null || view.showDock === false) return null;
461
- if (view.initializing) return h(InitializingChip, { t: t2 });
462
- const label = t2(`costValue`, { amount: formatChipMoney(view.totalMicros, view.currency, view.cnyPerUsd) });
463
- const buckets = view.bucketMicros ?? { input: 0, output: 0, read: 0, write: 0 };
464
- const items = [
465
- [t2(`input`), formatMoney(buckets.input, view.currency, view.cnyPerUsd)],
466
- [t2(`cacheRead`), formatMoney(buckets.read, view.currency, view.cnyPerUsd)],
467
- [t2(`cacheWrite`), formatMoney(buckets.write, view.currency, view.cnyPerUsd)],
468
- [t2(`output`), formatMoney(buckets.output, view.currency, view.cnyPerUsd)]
469
- ];
470
- if (view.skippedTurns > 0) items.push([t2(`skipped`), t2(`unpricedValue`, { count: view.skippedTurns })]);
471
- if (view.unpricedTokens > 0) items.push([t2(`unpriced`), t2(`count`, { count: view.unpricedTokens })]);
472
- const routes = view.routes ?? [];
473
- const routeRows = routes.length === 0 ? null : h(`div`, { key: `routes` }, [
474
- h(`div`, { className: `gdcc_sub`, key: `h` }, t2(`byRoute`)),
475
- details(
476
- routes.flatMap((entry) => [
477
- [
478
- entry.route,
479
- formatMoney(entry.micros, view.currency, view.cnyPerUsd) + ` \xB7 ` + t2(`count`, { count: formatExactTokens(entry.totalTokens) })
480
- ]
481
- ]),
482
- `routes`
483
- )
484
- ]);
485
- return h(`span`, { className: `gdcc_dockRoot`, ref: dialog.rootRef }, [
486
- h(
487
- `button`,
488
- {
489
- key: `b`,
490
- type: `button`,
491
- className: `gdcc_dockPill`,
492
- "aria-haspopup": `dialog`,
493
- "aria-expanded": dialog.open,
494
- onClick: () => dialog.setOpen(!dialog.open)
495
- },
496
- [h(IconGaugeOutlineRegular, { key: `i` }), h(`span`, { className: `gdcc_dockLabel`, key: `l` }, label)]
497
- ),
498
- dialog.open ? panel({
499
- panelRef: dialog.panelRef,
500
- pos: dialog.pos,
501
- title: t2(`sessionTitle`),
502
- value: formatMoney(view.totalMicros, view.currency, view.cnyPerUsd),
503
- icon: IconGaugeOutlineRegular,
504
- children: [details(items, `main`), routeRows, h(`div`, { className: `gdcc_note`, key: `n` }, t2(`note`))]
505
- }) : null
506
- ]);
507
- }
508
- function CostTurnAction(props) {
509
- const turn = props.useChat((snapshot) => {
510
- const tail = snapshot.nodes.values().find((node) => {
511
- var _a;
512
- return node.kind === `turn-tail` && ((_a = node.data.closing) == null ? void 0 : _a.finalNode.messageId) === props.messageId;
513
- });
514
- return tail == null ? void 0 : tail.data.turn;
515
- });
516
- return turn === void 0 ? null : h(CostTurnChip, { ...props, turn });
517
- }
518
- function CostTurnChip(props) {
519
- const view = useCostView(props);
520
- const dialog = useCostDialog();
521
- const t2 = props.t;
522
- if (view === void 0 || view === null || view.showTurn === false) return null;
523
- if (view.initializing) return h(InitializingChip, { t: t2, turn: true });
524
- const raw = props.turn;
525
- const turnNo = raw !== null && typeof raw === `object` ? raw.turn : raw;
526
- if (!Number.isSafeInteger(turnNo) || turnNo < 1) return null;
527
- const entry = view.turns === void 0 ? void 0 : view.turns[String(turnNo)];
528
- if (entry === void 0) return null;
529
- const label = entry.priced ? formatChipMoney(entry.micros, view.currency, view.cnyPerUsd) : t2(`noPricing`);
530
- const priced = entry.priced === true;
531
- const money = entry.bucketMicros;
532
- const items = [
533
- [t2(`model`), entry.route === `` ? null : entry.route],
534
- priced ? [t2(`input`), formatMoney(money.input, view.currency, view.cnyPerUsd)] : [t2(`input`), t2(`noPricing`)],
535
- priced ? [t2(`cacheRead`), formatMoney(money.read, view.currency, view.cnyPerUsd)] : null,
536
- priced ? [t2(`cacheWrite`), formatMoney(money.write, view.currency, view.cnyPerUsd)] : null,
537
- priced ? [t2(`output`), formatMoney(money.output, view.currency, view.cnyPerUsd)] : null
538
- ].filter((item) => item !== null);
539
- return h(`span`, { className: `gdcc_turnRoot`, ref: dialog.rootRef }, [
540
- h(
541
- `button`,
542
- {
543
- key: `b`,
544
- type: `button`,
545
- className: `gdcc_turnTrigger`,
546
- "aria-haspopup": `dialog`,
547
- "aria-expanded": dialog.open,
548
- onClick: () => dialog.setOpen(!dialog.open)
549
- },
550
- [
551
- h(IconGaugeOutlineRegular, { key: `i` }),
552
- h(`span`, { className: `gdcc_turnLabel`, key: `l` }, t2(`turnCost`, { amount: label }))
553
- ]
554
- ),
555
- dialog.open ? panel({
556
- panelRef: dialog.panelRef,
557
- pos: dialog.pos,
558
- title: t2(`turnTitle`),
559
- value: formatMoney(entry.micros, view.currency, view.cnyPerUsd),
560
- icon: IconGaugeOutlineRegular,
561
- children: [details(items, `turn`)]
562
- }) : null
563
- ]);
564
- }
565
- const zh = {
566
- modelName: `\u6A21\u578B`,
567
- initializing: `\u521D\u59CB\u5316\u4E2D`,
568
- sessionTitle: `\u4F1A\u8BDD\u8D39\u7528`,
569
- turnTitle: `\u672C\u8F6E\u8D39\u7528`,
570
- costValue: `\u8D39\u7528 {amount}`,
571
- turnCost: `{amount}`,
572
- input: `\u672A\u7F13\u5B58\u8F93\u5165`,
573
- cacheRead: `\u7F13\u5B58\u8BFB\u53D6`,
574
- cacheWrite: `\u7F13\u5B58\u5199\u5165`,
575
- output: `\u8F93\u51FA`,
576
- model: `\u63D0\u4F9B\u65B9 / \u6A21\u578B`,
577
- unpriced: `\u672A\u8BA1\u4EF7 token`,
578
- unpricedValue: `{count} \u8F6E`,
579
- skipped: `\u672A\u53D6\u5230\u7528\u91CF\u7684\u8F6E\u6B21`,
580
- noPricing: `\u672A\u914D\u7F6E\u5355\u4EF7`,
581
- byRoute: `\u6309\u6A21\u578B`,
582
- count: `{count} tok`,
583
- note: `\u8D39\u7528\u6309\u5404\u6A21\u578B\u5355\u4EF7\u4F30\u7B97,\u5355\u4EF7\u53EF\u5728\u63D2\u4EF6\u8BE6\u60C5\u9875\u7684\u8D39\u7528\u914D\u7F6E\u4E2D\u4FEE\u6539\u3002`,
584
- pluginTitle: `\u8D39\u7528`,
585
- pluginDescription: `\u6309 token \u7528\u91CF\u4F30\u7B97\u4F1A\u8BDD\u4E0E\u6BCF\u8F6E\u8D39\u7528,\u53EF\u81EA\u5B9A\u4E49\u5404\u6A21\u578B\u5355\u4EF7\u3002`,
586
- configSectionGeneral: `\u901A\u7528`,
587
- configCurrency: `\u663E\u793A\u8D27\u5E01`,
588
- configCnyPerUsd: `\u6C47\u7387(1 USD = ? CNY)`,
589
- configShowDockChip: `\u663E\u793A\u4F1A\u8BDD\u8D39\u7528\u80F6\u56CA`,
590
- configShowTurnChip: `\u663E\u793A\u8F6E\u6B21\u8D39\u7528\u80F6\u56CA`,
591
- configPricing: `\u6A21\u578B\u5355\u4EF7`,
592
- configSave: `\u4FDD\u5B58`,
593
- configSaving: `\u4FDD\u5B58\u4E2D\u2026`,
594
- configSaved: `\u5DF2\u4FDD\u5B58`,
595
- configLoading: `\u8BFB\u53D6\u914D\u7F6E\u4E2D\u2026`,
596
- configLoadFailed: `\u8BFB\u53D6\u914D\u7F6E\u5931\u8D25:{message}`
624
+ // src/client/config-controller.js
625
+ function createUseConfigController(react, store) {
626
+ return function useConfigController(t) {
627
+ const [payload, setPayload] = react.useState(null), [draft, setDraft] = react.useState(null);
628
+ const [busy, setBusy] = react.useState(false);
629
+ const [error, setError] = react.useState(null), [notice, setNotice] = react.useState("");
630
+ const accept = (value) => {
631
+ setPayload(value);
632
+ setDraft(buildDraft(value));
597
633
  };
598
- const en = {
599
- modelName: `Model`,
600
- initializing: `Initializing`,
601
- sessionTitle: `Session cost`,
602
- turnTitle: `Turn cost`,
603
- costValue: `Cost {amount}`,
604
- turnCost: `{amount}`,
605
- input: `Uncached input`,
606
- cacheRead: `Cache read`,
607
- cacheWrite: `Cache write`,
608
- output: `Output`,
609
- model: `Provider / model`,
610
- unpriced: `Unpriced tokens`,
611
- unpricedValue: `{count}`,
612
- skipped: `Turns without usage`,
613
- noPricing: `no rate configured`,
614
- byRoute: `By model`,
615
- count: `{count} tok`,
616
- note: `Estimated from per-model rates; edit them on this plugin's detail page.`,
617
- pluginTitle: `Cost`,
618
- pluginDescription: `Estimates session and per-turn cost from token usage. Model rates are editable.`,
619
- configSectionGeneral: `General`,
620
- configCurrency: `Currency`,
621
- configCnyPerUsd: `Rate (1 USD = ? CNY)`,
622
- configShowDockChip: `Show session cost chip`,
623
- configShowTurnChip: `Show per-turn cost chip`,
624
- configPricing: `Model rates`,
625
- configSave: `Save`,
626
- configSaving: `Saving\u2026`,
627
- configSaved: `Saved`,
628
- configLoading: `Loading configuration\u2026`,
629
- configLoadFailed: `Could not load configuration: {message}`
630
- };
631
- const ENTRY_ID = PACKAGE_ID;
632
- function field(label, control, key) {
633
- const value = control === null || control === void 0 ? null : react.cloneElement(control, { key: `v` });
634
- return h(`div`, { className: `gdcc_field`, key }, [
635
- h(`span`, { className: `gdcc_fieldLabel`, key: `l` }, label),
636
- value
637
- ]);
638
- }
639
- function numberInput(value, onChange, key) {
640
- return h(`input`, {
641
- key,
642
- className: `gdcc_input`,
643
- type: `number`,
644
- step: `any`,
645
- value: String(value ?? ``),
646
- onChange: (event) => onChange(event.target.value)
647
- });
648
- }
649
- function CostConfigCard(props) {
650
- const t2 = props.t, text = (zh2, en2) => t2("configSave") === "\u4FDD\u5B58" ? zh2 : en2;
651
- const [payload, setPayload] = react.useState(null), [draft, setDraft] = react.useState(null);
652
- const [busy, setBusy] = react.useState(false);
653
- const [error, setError] = react.useState(null), [notice, setNotice] = react.useState("");
654
- const accept = (value) => {
655
- setPayload(value);
656
- setDraft(buildDraft(value));
634
+ react.useEffect(() => {
635
+ let active = true;
636
+ store.load().then((v) => active && accept(v)).catch((e) => active && setError(e.message));
637
+ return () => {
638
+ active = false;
657
639
  };
658
- react.useEffect(() => {
659
- let active = true;
660
- store.load().then((v) => active && accept(v)).catch((e2) => active && setError(e2.message));
661
- return () => {
662
- active = false;
663
- };
664
- }, []);
665
- if (!draft) return h("div", { className: error ? "gdcc_error" : "gdcc_hint" }, error || t2("configLoading"));
666
- const dirty = JSON.stringify(draft) !== JSON.stringify(buildDraft(payload));
667
- const patch = (key, value) => setDraft((current) => ({ ...current, [key]: value }));
668
- const rowPatch = (index, changes) => setDraft((current) => ({
669
- ...current,
670
- rows: current.rows.map((row2, at) => at === index ? { ...row2, ...changes } : row2)
671
- }));
672
- const run = async (sync) => {
673
- if (sync && !window.confirm(
674
- text(
675
- "\u540C\u6B65\u5C06\u8986\u76D6\u914D\u7F6E\u4E2D\u5339\u914D\u6A21\u578B\u7684\u4EF7\u683C\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",
676
- "Sync will overwrite prices of matched configured models. Continue?"
677
- )
678
- ))
679
- return;
680
- setBusy(true);
640
+ }, []);
641
+ const dirty = draft !== null && JSON.stringify(draft) !== JSON.stringify(buildDraft(payload));
642
+ const patch = (key, value) => setDraft((current) => ({ ...current, [key]: value }));
643
+ const rowPatch = (index, changes) => setDraft((current) => ({
644
+ ...current,
645
+ rows: current.rows.map((row, at) => at === index ? { ...row, ...changes } : row)
646
+ }));
647
+ const run = async (sync) => {
648
+ if (sync && !window.confirm(t("configSyncConfirm"))) return;
649
+ setBusy(true);
650
+ setError(null);
651
+ setNotice("");
652
+ try {
653
+ const value = sync ? await store.sync() : await store.save(configOf(draft));
654
+ accept(value);
655
+ const skipped = value.report ? [...value.report.unmatched, ...value.report.unsupported, ...value.report.ambiguous] : [];
656
+ setNotice(
657
+ value.report ? t("configUpdated") + ": " + value.report.updated.length + "; " + t("configPreserved") + ": " + skipped.length + (skipped.length ? " (" + skipped.join(", ") + ")" : "") : t("configSaved")
658
+ );
659
+ } catch (e) {
660
+ setError(e.message);
661
+ } finally {
662
+ setBusy(false);
663
+ }
664
+ };
665
+ const reload = async () => {
666
+ setBusy(true);
667
+ try {
668
+ accept(await store.load());
681
669
  setError(null);
682
- setNotice("");
683
- try {
684
- const value = sync ? await store.sync() : await store.save(configOf(draft));
685
- accept(value);
686
- const skipped = value.report ? [...value.report.unmatched, ...value.report.unsupported, ...value.report.ambiguous] : [];
687
- setNotice(
688
- value.report ? text("\u5DF2\u66F4\u65B0", "Updated") + ": " + value.report.updated.length + "; " + text("\u4FDD\u7559\u539F\u4EF7", "Preserved") + ": " + skipped.length + (skipped.length ? " (" + skipped.join(", ") + ")" : "") : t2("configSaved")
689
- );
690
- } catch (e2) {
691
- setError(e2.message);
692
- } finally {
693
- setBusy(false);
694
- }
695
- };
696
- const groups = /* @__PURE__ */ new Map();
697
- draft.rows.forEach((row2, index) => {
698
- const { provider, model } = parseRouteKey(row2.key);
699
- if (!groups.has(provider)) groups.set(provider, []);
700
- groups.get(provider).push({ row: row2, index, model });
701
- });
702
- const rows = [...groups].flatMap(([provider, entries]) => [
703
- h(
704
- "div",
705
- { key: `provider:${provider}`, className: "gdcc_provider", role: "heading", "aria-level": 3 },
706
- provider
707
- ),
708
- ...entries.map(
709
- ({ row: row2, index, model }) => h("div", { className: "gdcc_row", key: row2.key }, [
710
- h("span", { key: "model", className: "gdcc_model", title: row2.key }, model),
711
- ...RATE_KEYS.map((key) => numberInput(row2[key], (value) => rowPatch(index, { [key]: value }), key)),
712
- h(
713
- "button",
714
- {
715
- key: "reset",
716
- type: "button",
717
- className: "gdcc_btn",
718
- title: text("\u4EF7\u683C\u5F52\u96F6", "Reset prices to zero"),
719
- onClick: () => rowPatch(index, zeroRate())
720
- },
721
- "\u21BA"
722
- )
723
- ])
724
- )
725
- ]);
726
- return h("fieldset", { className: "gdcc_form", disabled: busy }, [
727
- field(
728
- t2("configCurrency"),
729
- h(
730
- "select",
731
- { className: "gdcc_input", value: draft.currency, onChange: (e2) => patch("currency", e2.target.value) },
732
- ["USD", "CNY"].map((value) => h("option", { key: value, value }, value))
733
- ),
734
- "currency"
735
- ),
736
- field(
737
- t2("configCnyPerUsd"),
738
- numberInput(draft.cnyPerUsd, (value) => patch("cnyPerUsd", value)),
739
- "exchange"
740
- ),
741
- ...["showDockChip", "showTurnChip"].map(
742
- (key) => field(
743
- t2(key === "showDockChip" ? "configShowDockChip" : "configShowTurnChip"),
744
- h("input", {
745
- className: "gdcc_input",
746
- type: "checkbox",
747
- checked: draft[key],
748
- onChange: (e2) => patch(key, e2.target.checked)
749
- }),
750
- key
751
- )
752
- ),
753
- h("div", { key: "pricing", className: "gdcc_sub" }, t2("configPricing") + " (USD / 1M tokens)"),
754
- h("div", { key: "source", className: "gdcc_actions" }, [
755
- h("span", { key: "from" }, text("\u4ECE", "From")),
756
- h(
757
- "select",
758
- {
759
- key: "select",
760
- className: "gdcc_input",
761
- style: { width: "auto" },
762
- "aria-label": text("\u4EF7\u683C\u6765\u6E90", "Price source")
763
- },
764
- h("option", { value: "models.dev" }, "models.dev")
765
- ),
766
- h(
767
- "button",
768
- { key: "sync", type: "button", className: "gdcc_btn", disabled: dirty || busy, onClick: () => run(true) },
769
- text("\u540C\u6B65\u4EF7\u683C", "Sync prices")
770
- )
771
- ]),
772
- dirty ? h(
773
- "div",
774
- { key: "dirty", className: "gdcc_hint" },
775
- text("\u8BF7\u5148\u4FDD\u5B58\u4FEE\u6539\uFF0C\u518D\u540C\u6B65\u4EF7\u683C\u3002", "Save changes before syncing prices.")
776
- ) : null,
777
- h("div", { key: "rows", className: "gdcc_rows" }, [
778
- h(
779
- "div",
780
- { key: "head", className: "gdcc_row" },
781
- ["modelName", "input", "output", "cacheRead", "cacheWrite", ""].map(
782
- (key) => h("span", { key, className: "gdcc_rowHead" }, key ? t2(key) : "")
783
- )
784
- ),
785
- ...rows
786
- ]),
787
- draft.rows.length ? null : h("div", { key: "empty", className: "gdcc_hint" }, text("\u6682\u65E0\u6A21\u578B\u8C03\u7528\u5386\u53F2", "No model call history")),
788
- h("div", { key: "actions", className: "gdcc_actions" }, [
789
- error ? h("span", { key: "error", className: "gdcc_error" }, error) : null,
790
- notice ? h("span", { key: "notice", className: "gdcc_ok" }, notice) : null,
670
+ } catch (e) {
671
+ setError(e.message);
672
+ } finally {
673
+ setBusy(false);
674
+ }
675
+ };
676
+ return { draft, dirty, busy, error, notice, patch, rowPatch, run, reload };
677
+ };
678
+ }
679
+
680
+ // src/client/form-controls.js
681
+ function createFormControls(react) {
682
+ const h = react.createElement;
683
+ function field(label, control, key) {
684
+ const value = control === null || control === void 0 ? null : react.cloneElement(control, { key: `v` });
685
+ return h(`div`, { className: `gdcc_field`, key }, [
686
+ h(`span`, { className: `gdcc_fieldLabel`, key: `l` }, label),
687
+ value
688
+ ]);
689
+ }
690
+ function numberInput(value, onChange, key) {
691
+ return h(`input`, {
692
+ key,
693
+ className: `gdcc_input`,
694
+ type: `number`,
695
+ step: `any`,
696
+ value: String(value ?? ``),
697
+ onChange: (event) => onChange(event.target.value)
698
+ });
699
+ }
700
+ return { field, numberInput };
701
+ }
702
+
703
+ // src/client/pricing-table.js
704
+ function createPricingTable(react, numberInput) {
705
+ const h = react.createElement;
706
+ return function PricingTable({ rows: modelRows, rowPatch, t }) {
707
+ const groups = /* @__PURE__ */ new Map();
708
+ modelRows.forEach((row, index) => {
709
+ const { provider, model } = parseRouteKey(row.key);
710
+ if (!groups.has(provider)) groups.set(provider, []);
711
+ groups.get(provider).push({ row, index, model });
712
+ });
713
+ const rows = [...groups].flatMap(([provider, entries]) => [
714
+ h("div", { key: `provider:${provider}`, className: "gdcc_provider", role: "heading", "aria-level": 3 }, provider),
715
+ ...entries.map(
716
+ ({ row, index, model }) => h("div", { className: "gdcc_row", key: row.key }, [
717
+ h("span", { key: "model", className: "gdcc_model", title: row.key }, model),
718
+ ...RATE_KEYS.map((key) => numberInput(row[key], (value) => rowPatch(index, { [key]: value }), key)),
791
719
  h(
792
720
  "button",
793
721
  {
794
- key: "reload",
722
+ key: "reset",
795
723
  type: "button",
796
724
  className: "gdcc_btn",
797
- onClick: async () => {
798
- setBusy(true);
799
- try {
800
- accept(await store.load());
801
- setError(null);
802
- } catch (e2) {
803
- setError(e2.message);
804
- } finally {
805
- setBusy(false);
806
- }
807
- }
725
+ title: t("configReset"),
726
+ onClick: () => rowPatch(index, zeroRate())
808
727
  },
809
- text("\u91CD\u65B0\u52A0\u8F7D", "Reload")
810
- ),
811
- h(
812
- "button",
813
- { key: "save", type: "button", className: "gdcc_btn gdcc_btnPrimary", onClick: () => run(false) },
814
- busy ? t2("configSaving") : t2("configSave")
728
+ "\u21BA"
815
729
  )
816
730
  ])
817
- ]);
818
- }
819
- const inject = [`slots`, `locale`];
820
- function apply(ctx) {
821
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), `guojinpeng/dsh-cost-meter: dictionaries`);
822
- ctx.slots.inject(
823
- `conversation.composer.dock`,
824
- () => ctx.slots.register(
731
+ )
732
+ ]);
733
+ return h(react.Fragment, null, [
734
+ h("div", { key: "rows", className: "gdcc_rows" }, [
735
+ h(
736
+ "div",
737
+ { key: "head", className: "gdcc_row" },
738
+ ["modelName", "input", "output", "cacheRead", "cacheWrite", ""].map(
739
+ (key) => h("span", { key, className: "gdcc_rowHead" }, key ? t(key) : "")
740
+ )
741
+ ),
742
+ ...rows
743
+ ]),
744
+ modelRows.length ? null : h("div", { key: "empty", className: "gdcc_hint" }, t("configEmpty"))
745
+ ]);
746
+ };
747
+ }
748
+
749
+ // src/client/config-page.js
750
+ function createConfigPage(react, useConfigController) {
751
+ const h = react.createElement;
752
+ const { field, numberInput } = createFormControls(react);
753
+ const PricingTable = createPricingTable(react, numberInput);
754
+ function CostConfigCard(props) {
755
+ const t = props.t;
756
+ const { draft, dirty, busy, error, notice, patch, rowPatch, run, reload } = useConfigController(t);
757
+ if (!draft) return h("div", { className: error ? "gdcc_error" : "gdcc_hint" }, error || t("configLoading"));
758
+ return h("fieldset", { className: "gdcc_form", disabled: busy }, [
759
+ field(
760
+ t("configCurrency"),
761
+ h(
762
+ "select",
763
+ { className: "gdcc_input", value: draft.currency, onChange: (e) => patch("currency", e.target.value) },
764
+ ["USD", "CNY"].map((value) => h("option", { key: value, value }, value))
765
+ ),
766
+ "currency"
767
+ ),
768
+ field(
769
+ t("configCnyPerUsd"),
770
+ numberInput(draft.cnyPerUsd, (value) => patch("cnyPerUsd", value)),
771
+ "exchange"
772
+ ),
773
+ ...["showDockChip", "showTurnChip"].map(
774
+ (key) => field(
775
+ t(key === "showDockChip" ? "configShowDockChip" : "configShowTurnChip"),
776
+ h("input", {
777
+ className: "gdcc_input",
778
+ type: "checkbox",
779
+ checked: draft[key],
780
+ onChange: (e) => patch(key, e.target.checked)
781
+ }),
782
+ key
783
+ )
784
+ ),
785
+ h("div", { key: "pricing", className: "gdcc_sub" }, t("configPricing") + " (USD / 1M tokens)"),
786
+ h("div", { key: "source", className: "gdcc_actions" }, [
787
+ h("span", { key: "from" }, t("configFrom")),
788
+ h(
789
+ "select",
825
790
  {
826
- name: `conversation.composer.dock`,
827
- id: `${PLUGIN_ID}/dock`,
828
- order: 10,
829
- locale: NS
791
+ key: "select",
792
+ className: "gdcc_input",
793
+ style: { width: "auto" },
794
+ "aria-label": t("configPriceSource")
830
795
  },
831
- CostDockChip
796
+ h("option", { value: "models.dev" }, "models.dev")
797
+ ),
798
+ h(
799
+ "button",
800
+ { key: "sync", type: "button", className: "gdcc_btn", disabled: dirty || busy, onClick: () => run(true) },
801
+ t("configSync")
832
802
  )
833
- );
834
- ctx.slots.inject(
835
- `conversation.chat.assistant-actions`,
803
+ ]),
804
+ dirty ? h("div", { key: "dirty", className: "gdcc_hint" }, t("configDirty")) : null,
805
+ h(PricingTable, { key: "rows", rows: draft.rows, rowPatch, t }),
806
+ h("div", { key: "actions", className: "gdcc_actions" }, [
807
+ error ? h("span", { key: "error", className: "gdcc_error" }, error) : null,
808
+ notice ? h("span", { key: "notice", className: "gdcc_ok" }, notice) : null,
809
+ h(
810
+ "button",
811
+ {
812
+ key: "reload",
813
+ type: "button",
814
+ className: "gdcc_btn",
815
+ onClick: reload
816
+ },
817
+ t("configReload")
818
+ ),
819
+ h(
820
+ "button",
821
+ { key: "save", type: "button", className: "gdcc_btn gdcc_btnPrimary", onClick: () => run(false) },
822
+ busy ? t("configSaving") : t("configSave")
823
+ )
824
+ ])
825
+ ]);
826
+ }
827
+ return CostConfigCard;
828
+ }
829
+
830
+ // src/client/plugin.js
831
+ function createClientPlugin(react, reactDom) {
832
+ const store = createConfigStore();
833
+ const useCostView = createUseCostView(react, store);
834
+ const dialogs = createCostDialog(react, reactDom);
835
+ const { CostDockChip, CostTurnAction } = createCostChips(react, useCostView, dialogs);
836
+ const CostConfigCard = createConfigPage(react, createUseConfigController(react, store));
837
+ const NS = LOCALE_NAMESPACE;
838
+ const ENTRY_ID = PACKAGE_ID;
839
+ installStyles();
840
+ const inject = [`slots`, `locale`];
841
+ function apply(ctx) {
842
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), `guojinpeng/dsh-cost-meter: dictionaries`);
843
+ ctx.slots.inject(
844
+ `conversation.composer.dock`,
845
+ () => ctx.slots.register(
846
+ {
847
+ name: `conversation.composer.dock`,
848
+ id: `${PLUGIN_ID}/dock`,
849
+ order: 10,
850
+ locale: NS
851
+ },
852
+ CostDockChip
853
+ )
854
+ );
855
+ ctx.slots.inject(
856
+ `conversation.chat.assistant-actions`,
857
+ () => ctx.slots.register(
858
+ {
859
+ name: `conversation.chat.assistant-actions`,
860
+ id: `${PLUGIN_ID}/turn`,
861
+ order: 10,
862
+ locale: NS
863
+ },
864
+ CostTurnAction
865
+ )
866
+ );
867
+ ctx.effect(
868
+ () => ctx.slots.inject(
869
+ `plugins.bundle.config`,
836
870
  () => ctx.slots.register(
837
871
  {
838
- name: `conversation.chat.assistant-actions`,
839
- id: `${PLUGIN_ID}/turn`,
840
- order: 10,
841
- locale: NS
872
+ name: `plugins.bundle.config`,
873
+ key: ENTRY_ID,
874
+ locale: NS,
875
+ inject: () => ({})
842
876
  },
843
- CostTurnAction
877
+ CostConfigCard
844
878
  )
845
- );
846
- ctx.effect(
847
- () => ctx.slots.inject(
848
- `plugins.bundle.config`,
849
- () => ctx.slots.register(
850
- {
851
- name: `plugins.bundle.config`,
852
- key: ENTRY_ID,
853
- locale: NS,
854
- inject: () => ({})
855
- },
856
- CostConfigCard
857
- )
858
- ),
859
- `guojinpeng/dsh-cost-meter: bundle config page`
860
- );
861
- }
862
- n.apply = apply;
863
- n.inject = inject;
864
- return t.exports;
879
+ ),
880
+ `guojinpeng/dsh-cost-meter: bundle config page`
881
+ );
865
882
  }
883
+ const exports = { apply, inject };
884
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
885
+ return exports;
886
+ }
887
+
888
+ // src/client.js
889
+ window.__ModuleLoader__.load({
890
+ id: PACKAGE_ID,
891
+ factory: (load) => createClientPlugin(load("react"), load("react-dom"))
866
892
  });