@router-ai/dsh-plugin-routerai 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1565 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@router-ai/dsh-plugin-routerai",
3
+ factory: function(require) {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react = require("react");
10
+ // src/links.ts
11
+ const KEYS_PAGE = "https://routerai.ru/settings/keys";
12
+ const BILLING_PAGE = "https://routerai.ru/settings/billing";
13
+ /** Имя инструмента рисования — по нему же браузерная часть подставляет свою карточку вызова. */
14
+ const DRAW_TOOL = "routerai_draw_image";
15
+ // src/client/logo.ts
16
+ const LOGO = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20fill%3D%22%232d67f6%22%20d%3D%22M8.22%2021.06a4.09%204.09%200%200%200%207.55%200%204.1%204.1%200%200%200%205.34-5.32%204.08%204.08%200%200%200%200-7.53%204.07%204.07%200%200%200-5.34-5.33%204.09%204.09%200%200%200-7.55%200A4.1%204.1%200%200%200%202.9%208.21a4.08%204.08%200%200%200%200%207.53%204.07%204.07%200%200%200%205.33%205.32%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M23.25%2011.97a3.7%203.7%200%200%200-2.28-3.42l-.35-.14.14-.35a3.7%203.7%200%200%200-4.84-4.83l-.35.14-.14-.34A3.7%203.7%200%200%200%2012.29.76L12%20.75c-1.54%200-2.87.94-3.43%202.28l-.14.34-.35-.14a3.72%203.72%200%200%200-4.85%204.83l.15.35-.35.14a3.7%203.7%200%200%200-2.28%203.42v.29a3.7%203.7%200%200%200%202.28%203.13l.35.14-.15.35a3.7%203.7%200%200%200%204.85%204.83l.35-.14.14.35a3.71%203.71%200%200%200%206.86%200l.14-.35.35.14a3.72%203.72%200%200%200%204.85-4.83l-.15-.35.35-.14a3.7%203.7%200%200%200%202.28-3.42M12%201.97c1.03%200%201.91.63%202.29%201.53l.61%201.48%201.49-.61a2.48%202.48%200%200%201%203.23%203.23l-.6%201.47%201.47.62c.9.37%201.53%201.26%201.53%202.28v.09a2.5%202.5%200%200%201-1.53%202.2l-1.48.6.61%201.49a2.47%202.47%200%200%201-3.23%203.23l-1.49-.61-.61%201.47a2.48%202.48%200%200%201-4.58%200l-.61-1.47-1.49.6a2.48%202.48%200%200%201-3.23-3.22l.6-1.48-1.47-.62a2.5%202.5%200%200%201-1.53-2.28v-.09c.04-.99.66-1.83%201.53-2.2l1.48-.6-.61-1.48A2.47%202.47%200%200%201%207.6%204.37l1.49.6.61-1.47A2.5%202.5%200%200%201%2012%201.98m0%2015.8q-.36%200-.66.13l-1.6.66.66%201.6a1.73%201.73%200%200%200%203.2%200l.67-1.6-1.61-.66q-.3-.13-.66-.13m-6.93-1.14a1.72%201.72%200%200%200%202.26%202.25l1.6-.65-.66-1.6a1.7%201.7%200%200%200-.93-.93l-1.61-.67zm11.6-.93a1.7%201.7%200%200%200-.94.93l-.66%201.6%201.6.65a1.74%201.74%200%200%200%202.26-2.25l-.66-1.6zM6.18%2011.97q0-.36-.13-.66l-.66-1.6-1.6.67c-.61.25-1.04.84-1.07%201.52v.08c0%20.7.44%201.32%201.07%201.58l1.6.67.66-1.6q.13-.3.13-.66m8.89%200q0-.71.21-1.36a4.5%204.5%200%200%201-1.93-1.92%204.5%204.5%200%200%201-2.72%200A4.4%204.4%200%200%201%208.7%2010.6a4.4%204.4%200%200%201%200%202.72%204.5%204.5%200%200%201%201.93%201.92%204.5%204.5%200%200%201%202.72%200%204.4%204.4%200%200%201%201.93-1.92%204%204%200%200%201-.21-1.36M7.33%205.06A1.74%201.74%200%200%200%205.07%207.3l.66%201.6%201.6-.66a1.7%201.7%200%200%200%20.94-.93l.66-1.6zm11.22.37c-.5-.5-1.25-.63-1.88-.37l-1.6.66.66%201.6a1.7%201.7%200%200%200%20.93.93l1.61.66.66-1.6c.26-.63.13-1.37-.38-1.88M12%202.73c-.72%200-1.34.43-1.6%201.06l-.67%201.6%201.61.66a1.7%201.7%200%200%200%201.32%200l1.6-.67-.66-1.6A1.7%201.7%200%200%200%2012%202.74m-5.06%209.24q0%20.5-.19.94l-.61%201.48%201.48.62a2.5%202.5%200%200%201%201.34%201.33l.61%201.48%201.49-.61a2.5%202.5%200%200%201%201.88%200l1.49.6.61-1.47A2.5%202.5%200%200%201%2016.38%2015l1.48-.62-.61-1.48a2.5%202.5%200%200%201%200-1.88l.6-1.48-1.47-.61a2.5%202.5%200%200%201-1.34-1.34l-.61-1.47-1.49.6a2.5%202.5%200%200%201-1.88%200l-1.49-.6-.61%201.47a2.5%202.5%200%200%201-1.34%201.34l-1.48.61.61%201.48q.19.43.19.94m8.89%200q0%20.76.27%201.41l.15.35-.35.14a3.7%203.7%200%200%200-2%202l-.14.34-.35-.14a3.7%203.7%200%200%200-2.82%200l-.35.14-.14-.34a3.7%203.7%200%200%200-2-2l-.35-.14.15-.35a3.7%203.7%200%200%200%200-2.82l-.15-.34.35-.15a3.7%203.7%200%200%200%202-2l.14-.34.35.15a3.7%203.7%200%200%200%202.82%200l.35-.15.14.35a3.7%203.7%200%200%200%202%202l.35.14-.15.34q-.26.65-.27%201.41m1.98%200q0%20.36.13.66l.66%201.6%201.6-.67a1.7%201.7%200%200%200%201.07-1.52v-.2a1.7%201.7%200%200%200-1.07-1.46l-1.6-.67-.66%201.6q-.12.3-.13.66m6.19%200c0%201.73-.98%203.22-2.42%203.96a4.44%204.44%200%200%201-5.61%205.6%204.46%204.46%200%200%201-7.94%200%204.47%204.47%200%200%201-5.6-5.6A4.5%204.5%200%200%201%200%2012.14v-.17c0-1.72.98-3.22%202.42-3.96a4.44%204.44%200%200%201%205.61-5.6A4.5%204.5%200%200%201%2012%200h.17a4.5%204.5%200%200%201%203.8%202.41%204.47%204.47%200%200%201%205.6%205.6A4.5%204.5%200%200%201%2024%2011.97%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M10.44%208.22a4%204%200%200%200%203.12%200%204%204%200%200%200%202.2%202.2%204%204%200%200%200%200%203.1%204%204%200%200%200-2.2%202.2%204%204%200%200%200-3.12%200%204%204%200%200%200-2.2-2.2%204%204%200%200%200%200-3.1%204%204%200%200%200%202.2-2.2%22%2F%3E%3C%2Fsvg%3E";
17
+ // src/client/parts.tsx
18
+ function Logo({ size }) {
19
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
20
+ src: LOGO,
21
+ width: size,
22
+ height: size,
23
+ alt: "",
24
+ style: {
25
+ display: "block",
26
+ flexShrink: 0
27
+ }
28
+ });
29
+ }
30
+ const SHAPES = {
31
+ up: "M3.5 9.5 8 5l4.5 4.5",
32
+ down: "M3.5 6.5 8 11l4.5-4.5",
33
+ cross: "M4.5 4.5l7 7m0-7-7 7"
34
+ };
35
+ function Icon({ glyph }) {
36
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
37
+ viewBox: "0 0 16 16",
38
+ width: "14",
39
+ height: "14",
40
+ fill: "none",
41
+ stroke: "currentColor",
42
+ strokeWidth: "1.5",
43
+ strokeLinecap: "round",
44
+ strokeLinejoin: "round",
45
+ "aria-hidden": "true",
46
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: SHAPES[glyph] })
47
+ });
48
+ }
49
+ // src/client/remote.ts
50
+ async function request(path, body) {
51
+ const response = await fetch(new URL(`api/routerai/${path}`, document.baseURI), body === void 0 ? { headers: { accept: "application/json" } } : {
52
+ method: "POST",
53
+ headers: {
54
+ accept: "application/json",
55
+ "content-type": "application/json"
56
+ },
57
+ body: JSON.stringify(body)
58
+ });
59
+ const payload = await response.json().catch(() => null);
60
+ if (!response.ok || payload === null) throw new Error(payload?.error ?? `Сервер ответил ${response.status}`);
61
+ return payload;
62
+ }
63
+ const IDLE_REFRESH_MS = 45e3;
64
+ const DISCONNECTED_REFRESH_MS = 8e3;
65
+ const WAITING_REFRESH_MS = 1200;
66
+ /** Состояние плагина в этой вкладке. Опрашивает сервер редко, а пока идёт вход — часто. */
67
+ var Remote = class {
68
+ view = {
69
+ overview: null,
70
+ failure: null
71
+ };
72
+ subscribers = /* @__PURE__ */ new Set();
73
+ timer;
74
+ catalogRequest;
75
+ stopped = false;
76
+ read = () => this.view;
77
+ subscribe = (subscriber) => {
78
+ this.subscribers.add(subscriber);
79
+ return () => {
80
+ this.subscribers.delete(subscriber);
81
+ };
82
+ };
83
+ /** Запустить опрос; возвращает остановку. */
84
+ run() {
85
+ this.pull();
86
+ return () => {
87
+ this.stopped = true;
88
+ clearTimeout(this.timer);
89
+ };
90
+ }
91
+ async pull(fresh = false) {
92
+ clearTimeout(this.timer);
93
+ try {
94
+ this.publish({
95
+ overview: await request(fresh ? "overview?fresh=1" : "overview"),
96
+ failure: null
97
+ });
98
+ } catch (error) {
99
+ this.publish({
100
+ ...this.view,
101
+ failure: error instanceof Error ? error.message : String(error)
102
+ });
103
+ }
104
+ if (this.stopped) return;
105
+ const waiting = this.view.overview?.signIn.stage === "waiting";
106
+ const pause = waiting ? WAITING_REFRESH_MS : this.view.overview?.connected === false ? DISCONNECTED_REFRESH_MS : IDLE_REFRESH_MS;
107
+ this.timer = setTimeout(() => {
108
+ this.pull(waiting);
109
+ }, pause);
110
+ }
111
+ publish(view) {
112
+ this.view = view;
113
+ for (const subscriber of this.subscribers) subscriber();
114
+ }
115
+ async act(path, body, fresh = true) {
116
+ await request(path, body);
117
+ await this.pull(fresh);
118
+ }
119
+ signIn = () => this.act("sign-in", {}, false);
120
+ cancelSignIn = () => this.act("sign-in/cancel", {}, false);
121
+ signOut = () => this.act("sign-out", {});
122
+ saveKey = (key) => this.act("key", { key });
123
+ savePreferences = (patch) => this.act("preferences", patch, false);
124
+ /** Каталог запрашивается один раз за жизнь вкладки. */
125
+ catalog() {
126
+ this.catalogRequest ??= request("catalog").then((reply) => reply.models);
127
+ this.catalogRequest.catch(() => {
128
+ this.catalogRequest = void 0;
129
+ });
130
+ return this.catalogRequest;
131
+ }
132
+ async sessionCost(sessionId) {
133
+ return (await request(`session-cost?id=${encodeURIComponent(sessionId)}`)).cost;
134
+ }
135
+ };
136
+ function useRemote(remote) {
137
+ return (0, react.useSyncExternalStore)(remote.subscribe, remote.read);
138
+ }
139
+ function openTab(url) {
140
+ window.open(url, "_blank", "noopener,noreferrer");
141
+ }
142
+ // src/client/MoneyNotice.tsx
143
+ const HEADLINES = {
144
+ "no-funds": "problem_no_funds",
145
+ "key-cap": "problem_key_cap",
146
+ "key-off": "problem_key_off"
147
+ };
148
+ /** `message` и `dismiss` передаёт harness: текст ошибки и закрытие уведомления. */
149
+ function MoneyNotice({ t, remote, message, dismiss }) {
150
+ const problem = remote.read().overview?.problem ?? null;
151
+ const funds = problem === null || problem.kind === "no-funds";
152
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
153
+ className: "rai-banner",
154
+ role: "alert",
155
+ "data-routerai": "money-notice",
156
+ children: [
157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 28 }),
158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t(HEADLINES[problem?.kind ?? "no-funds"] ?? "problem_no_funds") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
159
+ className: "rai-note",
160
+ children: problem?.text ?? message
161
+ })] }),
162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
163
+ variant: "primary",
164
+ size: "sm",
165
+ onClick: () => {
166
+ openTab(funds ? BILLING_PAGE : KEYS_PAGE);
167
+ },
168
+ children: funds ? t("addFunds") : t("openKeys")
169
+ }),
170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
171
+ variant: "ghost",
172
+ size: "sm",
173
+ onClick: () => {
174
+ remote.pull(true);
175
+ dismiss();
176
+ },
177
+ children: t("close")
178
+ })
179
+ ]
180
+ });
181
+ }
182
+ // src/money.ts
183
+ /** Сколько знаков после запятой показать, чтобы сумма не выглядела нулём и не шумела копейками. */
184
+ function fractionDigits(amount) {
185
+ const size = Math.abs(amount);
186
+ if (size === 0 || size >= 100) return 0;
187
+ if (size >= 1) return 2;
188
+ if (size >= .01) return 3;
189
+ return 4;
190
+ }
191
+ function rub(amount) {
192
+ if (amount === null || amount === void 0 || !Number.isFinite(amount)) return "—";
193
+ const digits = fractionDigits(amount);
194
+ return `${new Intl.NumberFormat("ru-RU", {
195
+ minimumFractionDigits: digits,
196
+ maximumFractionDigits: digits
197
+ }).format(amount)} ₽`;
198
+ }
199
+ /** «216 ₽ / 1 079 ₽» — вход и выход за миллион токенов. */
200
+ function pricePair(price) {
201
+ return price === void 0 ? "" : `${rub(price.input)} / ${rub(price.output)}`;
202
+ }
203
+ /** Размер контекстного окна: 1048576 → «1M», 200000 → «200K». */
204
+ function windowLabel(tokens) {
205
+ if (tokens === void 0) return "";
206
+ if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
207
+ return `${Math.round(tokens / 1e3)}K`;
208
+ }
209
+ // src/client/DrawCard.tsx
210
+ /** Описание картинки из аргументов вызова; пока модель их дописывает, JSON может быть неполным. */
211
+ function promptOf(block) {
212
+ for (const raw of [block.argsRaw, block.call?.argsRaw]) {
213
+ if (raw === void 0) continue;
214
+ try {
215
+ const prompt = JSON.parse(raw).prompt;
216
+ if (typeof prompt === "string") return prompt;
217
+ } catch {
218
+ const partial = /"prompt"\s*:\s*"((?:[^"\\]|\\.)*)/.exec(raw)?.[1];
219
+ if (partial !== void 0) return partial.replace(/\\n/g, " ").replace(/\\(.)/g, "$1");
220
+ }
221
+ }
222
+ return "";
223
+ }
224
+ function Picture({ attachment, loadImage }) {
225
+ const [url, setUrl] = (0, react.useState)(null);
226
+ (0, react.useEffect)(() => {
227
+ let current = true;
228
+ loadImage(attachment).then((loaded) => {
229
+ if (current) setUrl(loaded);
230
+ }, () => {});
231
+ return () => {
232
+ current = false;
233
+ };
234
+ }, [attachment, loadImage]);
235
+ return url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
236
+ src: url,
237
+ alt: attachment.name ?? "",
238
+ width: attachment.width,
239
+ height: attachment.height,
240
+ style: { height: "auto" }
241
+ });
242
+ }
243
+ function DrawCard({ t, phase, block, loadImage, openFile }) {
244
+ const prompt = promptOf(block);
245
+ if (phase !== "result") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
246
+ className: "rai-drawn",
247
+ "data-routerai": "draw-card",
248
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
249
+ className: "rai-line",
250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "rai-busy" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("drawing") })]
251
+ }), prompt === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
252
+ className: "rai-note",
253
+ children: prompt
254
+ })]
255
+ });
256
+ if (block.isError === true) {
257
+ const reason = block.content?.find((item) => item.type === "text")?.text ?? block.error?.reason ?? "";
258
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
259
+ className: "rai-drawn",
260
+ "data-routerai": "draw-card",
261
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("drawFailed") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
262
+ className: "rai-alert",
263
+ children: reason
264
+ })]
265
+ });
266
+ }
267
+ const meta = block.meta ?? {};
268
+ const shown = (block.content ?? []).flatMap((item) => item.type === "image" && item.attachment !== void 0 ? [item.attachment] : []);
269
+ const facts = [meta.model, typeof meta.cost === "number" ? t("drawnCost", { cost: rub(meta.cost) }) : void 0].filter(Boolean).join(" · ");
270
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
271
+ className: "rai-drawn",
272
+ "data-routerai": "draw-card",
273
+ children: [
274
+ shown.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
275
+ className: "rai-gallery",
276
+ children: shown.map((attachment) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picture, {
277
+ attachment,
278
+ loadImage
279
+ }, attachment.attachmentId))
280
+ }),
281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
282
+ className: "rai-line",
283
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("drawn") }), facts === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
284
+ className: "rai-faint",
285
+ children: facts
286
+ })]
287
+ }),
288
+ prompt === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
289
+ className: "rai-note",
290
+ children: prompt
291
+ }),
292
+ (meta.files ?? []).length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
293
+ className: "rai-line",
294
+ children: (meta.files ?? []).map((path) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
295
+ variant: "outline",
296
+ size: "sm",
297
+ onClick: () => {
298
+ openFile(path);
299
+ },
300
+ children: t("openFile", { name: path.split(/[\\/]/).pop() ?? path })
301
+ }, path))
302
+ })
303
+ ]
304
+ });
305
+ }
306
+ // src/client/SessionCost.tsx
307
+ const REFRESH_MS = 4e3;
308
+ const count = (value) => value.toLocaleString("ru-RU");
309
+ function SessionCost({ t, remote, sessionId }) {
310
+ const [cost, setCost] = (0, react.useState)(null);
311
+ const [open, setOpen] = (0, react.useState)(false);
312
+ const anchor = (0, react.useRef)(null);
313
+ (0, react.useEffect)(() => {
314
+ setCost(null);
315
+ if (sessionId === void 0) return;
316
+ let active = true;
317
+ const refresh = () => {
318
+ if (document.hidden) return;
319
+ remote.sessionCost(sessionId).then((next) => {
320
+ if (active) setCost(next);
321
+ }, () => {});
322
+ };
323
+ refresh();
324
+ const timer = setInterval(refresh, REFRESH_MS);
325
+ return () => {
326
+ active = false;
327
+ clearInterval(timer);
328
+ };
329
+ }, [remote, sessionId]);
330
+ (0, react.useEffect)(() => {
331
+ if (!open) return;
332
+ const outside = (event) => {
333
+ if (!anchor.current?.contains(event.target)) setOpen(false);
334
+ };
335
+ const escape = (event) => {
336
+ if (event.key === "Escape") setOpen(false);
337
+ };
338
+ document.addEventListener("pointerdown", outside);
339
+ document.addEventListener("keydown", escape);
340
+ return () => {
341
+ document.removeEventListener("pointerdown", outside);
342
+ document.removeEventListener("keydown", escape);
343
+ };
344
+ }, [open]);
345
+ if (cost === null || cost.calls === 0) return null;
346
+ const models = Object.entries(cost.perModel).sort(([, a], [, b]) => b.rub - a.rub);
347
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
348
+ className: "rai-cost",
349
+ ref: anchor,
350
+ "data-routerai": "session-cost",
351
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
352
+ type: "button",
353
+ "aria-expanded": open,
354
+ title: t("costTitle"),
355
+ onClick: () => {
356
+ setOpen((value) => !value);
357
+ },
358
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 13 }), rub(cost.total)]
359
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
360
+ className: "rai-sheet",
361
+ role: "dialog",
362
+ "aria-label": t("costTitle"),
363
+ children: [
364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costTotal") }),
366
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: rub(cost.total) }),
367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costLast") }),
368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: rub(cost.last?.rub) }),
369
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costCalls") }),
370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: cost.calls }),
371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costCached") }),
372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: cost.cachedShare === null ? "—" : `${Math.round(cost.cachedShare * 100)}%` }),
373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costInput") }),
374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(cost.tokens.input + cost.tokens.cached) }),
375
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costCacheWrites") }),
376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(cost.tokens.cacheWrites) }),
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costOutput") }),
378
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(cost.tokens.output) })
379
+ ] }),
380
+ models.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: models.map(([slug, share]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
382
+ className: "rai-clip",
383
+ children: slug
384
+ }),
385
+ share.calls,
386
+ "× ",
387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: rub(share.rub) })
388
+ ] }, slug)) }) : null,
389
+ cost.unpriced > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
390
+ className: "rai-caution",
391
+ children: t("costUnpriced", { count: cost.unpriced })
392
+ }) : null,
393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
394
+ className: "rai-faint",
395
+ children: t("costNote")
396
+ })
397
+ ]
398
+ }) : null]
399
+ });
400
+ }
401
+ // src/starter.ts
402
+ const MODEL_SETS = [
403
+ {
404
+ key: "everyday",
405
+ models: [
406
+ "anthropic/claude-sonnet-5.5",
407
+ "deepseek/deepseek-v4.1-flash",
408
+ "openai/gpt-6.1-sol",
409
+ "google/gemini-4-argon",
410
+ "anthropic/claude-opus-5.5",
411
+ "moonshotai/kimi-k3.1-pro",
412
+ "z-ai/glm-5.3-flash"
413
+ ]
414
+ },
415
+ {
416
+ key: "coding",
417
+ models: [
418
+ "anthropic/claude-opus-5.5",
419
+ "google/gemini-4-argon",
420
+ "z-ai/glm-5.3-flash",
421
+ "openai/gpt-6.1-sol",
422
+ "moonshotai/kimi-k3.1-pro"
423
+ ]
424
+ },
425
+ {
426
+ key: "budget",
427
+ models: ["deepseek/deepseek-v4.1-flash", "z-ai/glm-5.3-flash"]
428
+ }
429
+ ];
430
+ MODEL_SETS[0].models;
431
+ // src/client/SettingsPage.tsx
432
+ /** Выполнить действие и показать его ошибку рядом с кнопкой, а не потерять в консоли. */
433
+ function useAction() {
434
+ const [busy, setBusy] = (0, react.useState)(false);
435
+ const [error, setError] = (0, react.useState)(null);
436
+ const run = (action) => {
437
+ setBusy(true);
438
+ setError(null);
439
+ action().catch((problem) => {
440
+ setError(problem instanceof Error ? problem.message : String(problem));
441
+ }).finally(() => {
442
+ setBusy(false);
443
+ });
444
+ };
445
+ return {
446
+ busy,
447
+ error,
448
+ run
449
+ };
450
+ }
451
+ function Panel({ title, hint, children }) {
452
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
453
+ className: "rai-panel",
454
+ children: [
455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: title }),
456
+ hint === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
457
+ className: "rai-note",
458
+ children: hint
459
+ }),
460
+ children
461
+ ]
462
+ });
463
+ }
464
+ function Connect({ t, remote, overview }) {
465
+ const [draft, setDraft] = (0, react.useState)("");
466
+ const action = useAction();
467
+ const stage = overview.signIn;
468
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Panel, {
469
+ title: t("connectTitle"),
470
+ hint: stage.stage === "waiting" ? t("connectWaiting") : t("connectHint"),
471
+ children: [
472
+ stage.stage === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
473
+ className: "rai-line",
474
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
475
+ variant: "outline",
476
+ onClick: () => {
477
+ openTab(stage.url);
478
+ },
479
+ children: t("connectReopen")
480
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
481
+ variant: "ghost",
482
+ onClick: () => {
483
+ action.run(remote.cancelSignIn);
484
+ },
485
+ children: t("connectAbort")
486
+ })]
487
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
488
+ className: "rai-line",
489
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
490
+ variant: "primary",
491
+ disabled: action.busy,
492
+ onClick: () => {
493
+ action.run(remote.signIn);
494
+ },
495
+ children: t("connectButton")
496
+ })
497
+ }),
498
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
499
+ className: "rai-line",
500
+ children: [
501
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
502
+ className: "rai-note",
503
+ children: t("pasteLabel")
504
+ }),
505
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
506
+ className: "rai-fill",
507
+ type: "password",
508
+ autoComplete: "off",
509
+ placeholder: t("pasteHolder"),
510
+ value: draft,
511
+ onChange: (event) => {
512
+ setDraft(event.target.value);
513
+ }
514
+ }),
515
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
516
+ variant: "outline",
517
+ disabled: action.busy || draft.trim() === "",
518
+ onClick: () => {
519
+ action.run(() => remote.saveKey(draft.trim()).then(() => {
520
+ setDraft("");
521
+ }));
522
+ },
523
+ children: t("pasteButton")
524
+ })
525
+ ]
526
+ }),
527
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
528
+ className: "rai-alert",
529
+ children: action.error
530
+ }) : stage.stage === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
531
+ className: "rai-alert",
532
+ children: stage.reason
533
+ }) : null
534
+ ]
535
+ });
536
+ }
537
+ function Account({ t, remote, overview }) {
538
+ const action = useAction();
539
+ const account = overview.account;
540
+ const key = account?.key;
541
+ const cap = key?.cap;
542
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
543
+ className: "rai-panel",
544
+ "data-routerai": "account",
545
+ children: [
546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
547
+ className: "rai-line",
548
+ children: [
549
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
550
+ className: "rai-fill",
551
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
552
+ className: "rai-clip",
553
+ style: {
554
+ margin: 0,
555
+ font: "var(--dsw-font-s-strong-14)"
556
+ },
557
+ children: key?.label || t("accountNoLabel")
558
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
559
+ className: "rai-faint",
560
+ children: overview.keyMask
561
+ })]
562
+ }),
563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
564
+ variant: "outline",
565
+ onClick: () => {
566
+ openTab(BILLING_PAGE);
567
+ },
568
+ children: t("addFunds")
569
+ }),
570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
571
+ variant: "ghost",
572
+ disabled: action.busy,
573
+ onClick: () => {
574
+ action.run(() => remote.pull(true));
575
+ },
576
+ children: t("reload")
577
+ }),
578
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
579
+ variant: "ghost",
580
+ disabled: action.busy,
581
+ onClick: () => {
582
+ action.run(remote.signOut);
583
+ },
584
+ children: t("disconnect")
585
+ })
586
+ ]
587
+ }),
588
+ account?.state === "rejected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
589
+ className: "rai-alert",
590
+ children: t("keyRejected")
591
+ }) : null,
592
+ account?.state === "offline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
593
+ className: "rai-caution",
594
+ children: t("offline")
595
+ }) : null,
596
+ key?.disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
597
+ className: "rai-alert",
598
+ children: t("keyDisabled")
599
+ }) : null,
600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
601
+ className: "rai-figures",
602
+ children: [
603
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
604
+ className: "rai-figure rai-main",
605
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
606
+ className: "rai-note",
607
+ children: t("balance")
608
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rub(account?.balance) })]
609
+ }),
610
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
611
+ className: "rai-figure",
612
+ children: [
613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
614
+ className: "rai-note",
615
+ children: t("keyCap")
616
+ }),
617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: cap === void 0 ? t("keyCapNone") : rub(cap.left) }),
618
+ cap === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
619
+ className: "rai-faint",
620
+ children: t("keyCapLeft", {
621
+ amount: rub(cap.amount),
622
+ period: t(`period_${cap.period}`)
623
+ })
624
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
625
+ className: "rai-gauge",
626
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(100 * Math.min(1, Math.max(0, 1 - cap.left / cap.amount)))}%` } })
627
+ })] })
628
+ ]
629
+ }),
630
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
631
+ className: "rai-figure",
632
+ children: [
633
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
634
+ className: "rai-note",
635
+ children: t("keySpent")
636
+ }),
637
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rub(key?.spent ?? 0) }),
638
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
639
+ className: "rai-faint",
640
+ children: t("keySpentMonth", { amount: rub(key?.spentThisMonth ?? 0) })
641
+ })
642
+ ]
643
+ })
644
+ ]
645
+ }),
646
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
647
+ className: "rai-alert",
648
+ children: action.error
649
+ }) : null
650
+ ]
651
+ });
652
+ }
653
+ function modelFacts(entry) {
654
+ return [
655
+ entry.slug,
656
+ pricePair(entry.price),
657
+ windowLabel(entry.window)
658
+ ].filter((part) => part !== "").join(" · ");
659
+ }
660
+ function Picker({ t, remote, overview, catalog }) {
661
+ const saved = overview.picker.models;
662
+ const savedKey = saved.join("|");
663
+ const [draft, setDraft] = (0, react.useState)(saved);
664
+ const [search, setSearch] = (0, react.useState)("");
665
+ const [maker, setMaker] = (0, react.useState)("");
666
+ const [toolsOnly, setToolsOnly] = (0, react.useState)(true);
667
+ const [justApplied, setJustApplied] = (0, react.useState)(false);
668
+ const action = useAction();
669
+ (0, react.useEffect)(() => {
670
+ setDraft(saved);
671
+ }, [savedKey]);
672
+ const chat = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("text")), [catalog]);
673
+ const bySlug = (0, react.useMemo)(() => new Map(chat.map((entry) => [entry.slug, entry])), [chat]);
674
+ const makers = (0, react.useMemo)(() => [...new Set(chat.map((entry) => entry.maker))].sort((a, b) => a.localeCompare(b)), [chat]);
675
+ const matches = (0, react.useMemo)(() => {
676
+ const needle = search.trim().toLowerCase();
677
+ return chat.filter((entry) => (maker === "" || entry.maker === maker) && (!toolsOnly || entry.tools) && (needle === "" || `${entry.slug} ${entry.maker} ${entry.title}`.toLowerCase().includes(needle)));
678
+ }, [
679
+ chat,
680
+ search,
681
+ maker,
682
+ toolsOnly
683
+ ]);
684
+ const changed = draft.join("|") !== savedKey;
685
+ const edit = (next) => {
686
+ setJustApplied(false);
687
+ setDraft(next);
688
+ };
689
+ const shift = (from, to) => {
690
+ const next = [...draft];
691
+ next.splice(to, 0, ...next.splice(from, 1));
692
+ edit(next);
693
+ };
694
+ const toggle = (slug) => {
695
+ edit(draft.includes(slug) ? draft.filter((item) => item !== slug) : [...draft, slug]);
696
+ };
697
+ const visible = matches.slice(0, 250);
698
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Panel, {
699
+ title: t("pickerTitle"),
700
+ hint: t("pickerHint"),
701
+ children: [
702
+ draft.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
703
+ className: "rai-note",
704
+ children: t("pickerEmpty")
705
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
706
+ className: "rai-rows",
707
+ "data-routerai": "picked",
708
+ children: draft.map((slug, position) => {
709
+ const entry = bySlug.get(slug);
710
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
711
+ className: "rai-row",
712
+ children: [
713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
714
+ className: "rai-order",
715
+ children: position + 1
716
+ }),
717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
718
+ style: { minWidth: 0 },
719
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
720
+ className: "rai-clip",
721
+ children: entry === void 0 ? slug : `${entry.maker} ${entry.title}`
722
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
723
+ className: "rai-faint rai-clip",
724
+ children: entry === void 0 ? catalog === null ? slug : `${slug} · ${t("pickerUnknown")}` : modelFacts(entry)
725
+ })]
726
+ }),
727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
728
+ className: "rai-tools",
729
+ children: [
730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
731
+ type: "button",
732
+ title: t("moveUp"),
733
+ "aria-label": t("moveUp"),
734
+ disabled: position === 0,
735
+ onClick: () => {
736
+ shift(position, position - 1);
737
+ },
738
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { glyph: "up" })
739
+ }),
740
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
741
+ type: "button",
742
+ title: t("moveDown"),
743
+ "aria-label": t("moveDown"),
744
+ disabled: position === draft.length - 1,
745
+ onClick: () => {
746
+ shift(position, position + 1);
747
+ },
748
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { glyph: "down" })
749
+ }),
750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
751
+ type: "button",
752
+ title: t("drop"),
753
+ "aria-label": t("drop"),
754
+ onClick: () => {
755
+ toggle(slug);
756
+ },
757
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { glyph: "cross" })
758
+ })
759
+ ]
760
+ })
761
+ ]
762
+ }, slug);
763
+ })
764
+ }),
765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
766
+ className: "rai-line",
767
+ children: [
768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
769
+ variant: "primary",
770
+ disabled: !changed || action.busy,
771
+ onClick: () => {
772
+ action.run(() => remote.savePreferences({ models: draft }).then(() => {
773
+ setJustApplied(true);
774
+ }));
775
+ },
776
+ children: t("apply")
777
+ }),
778
+ changed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
779
+ variant: "ghost",
780
+ onClick: () => {
781
+ edit(saved);
782
+ },
783
+ children: t("revert")
784
+ }) : null,
785
+ justApplied && !changed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
786
+ className: "rai-note",
787
+ role: "status",
788
+ children: t("applied")
789
+ }) : null,
790
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
791
+ className: "rai-alert",
792
+ children: action.error
793
+ }) : null
794
+ ]
795
+ }),
796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
797
+ className: "rai-line",
798
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
799
+ className: "rai-note",
800
+ children: [t("sets"), ":"]
801
+ }), MODEL_SETS.map((set) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
802
+ variant: "outline",
803
+ size: "sm",
804
+ disabled: catalog === null,
805
+ onClick: () => {
806
+ edit(set.models.filter((slug) => bySlug.has(slug)));
807
+ },
808
+ children: t(`set_${set.key}`)
809
+ }, set.key))]
810
+ }),
811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
812
+ style: {
813
+ margin: "6px 0 0",
814
+ font: "var(--dsw-font-s-strong-14)"
815
+ },
816
+ children: t("catalogTitle")
817
+ }),
818
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
819
+ className: "rai-line",
820
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
821
+ className: "rai-fill",
822
+ placeholder: t("catalogSearch"),
823
+ value: search,
824
+ onChange: (event) => {
825
+ setSearch(event.target.value);
826
+ }
827
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
828
+ className: "rai-choice",
829
+ value: maker,
830
+ "aria-label": t("catalogAnyMaker"),
831
+ onChange: (event) => {
832
+ setMaker(event.target.value);
833
+ },
834
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
835
+ value: "",
836
+ children: t("catalogAnyMaker")
837
+ }), makers.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
838
+ value: item,
839
+ children: item
840
+ }, item))]
841
+ })]
842
+ }),
843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
844
+ className: "rai-line",
845
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
846
+ className: "rai-line rai-note",
847
+ style: { cursor: "pointer" },
848
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
849
+ type: "checkbox",
850
+ checked: toolsOnly,
851
+ onChange: (event) => {
852
+ setToolsOnly(event.target.checked);
853
+ }
854
+ }), t("catalogToolsOnly")]
855
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
856
+ className: "rai-faint",
857
+ style: { marginLeft: "auto" },
858
+ children: catalog === null ? t("catalogLoading") : t("catalogShown", {
859
+ shown: visible.length,
860
+ total: matches.length
861
+ })
862
+ })]
863
+ }),
864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
865
+ className: "rai-rows rai-tall",
866
+ "data-routerai": "catalog",
867
+ children: visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
868
+ className: "rai-row",
869
+ children: [
870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
871
+ type: "checkbox",
872
+ checked: draft.includes(entry.slug),
873
+ onChange: () => {
874
+ toggle(entry.slug);
875
+ }
876
+ }),
877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
878
+ style: { minWidth: 0 },
879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
880
+ className: "rai-clip",
881
+ children: [
882
+ entry.maker,
883
+ " ",
884
+ entry.title
885
+ ]
886
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
887
+ className: "rai-faint rai-clip",
888
+ children: modelFacts(entry)
889
+ })]
890
+ }),
891
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
892
+ className: "rai-line",
893
+ children: [
894
+ entry.alias ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
895
+ tone: "quiet",
896
+ children: t("tagAlias")
897
+ }) : null,
898
+ entry.reasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
899
+ tone: "quiet",
900
+ children: t("tagReasoning")
901
+ }) : null,
902
+ entry.vision ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
903
+ tone: "quiet",
904
+ children: t("tagVision")
905
+ }) : null,
906
+ entry.tools ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
907
+ tone: "warning",
908
+ children: t("tagNoTools")
909
+ })
910
+ ]
911
+ })
912
+ ]
913
+ }, entry.slug))
914
+ })
915
+ ]
916
+ });
917
+ }
918
+ function Extras({ t, remote, overview, catalog }) {
919
+ const [threshold, setThreshold] = (0, react.useState)(String(overview.lowBalance));
920
+ const action = useAction();
921
+ const cheapestFirst = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("text") && entry.tools && entry.price !== void 0 && !entry.alias).sort((a, b) => (a.price?.input ?? 0) - (b.price?.input ?? 0)), [catalog]);
922
+ const current = overview.utilityModel;
923
+ const painters = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("image")).sort((a, b) => (a.perImage ?? 1e400) - (b.perImage ?? 1e400) || a.title.localeCompare(b.title)), [catalog]);
924
+ const listeners = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("transcription")).sort((a, b) => (a.perSecond ?? 1e400) - (b.perSecond ?? 1e400) || a.title.localeCompare(b.title)), [catalog]);
925
+ const known = (list, slug) => slug === null || list.some((entry) => entry.slug === slug);
926
+ const commitThreshold = () => {
927
+ const value = Number(threshold.replace(",", "."));
928
+ if (Number.isFinite(value) && value >= 0) action.run(() => remote.savePreferences({ lowBalance: value }));
929
+ else setThreshold(String(overview.lowBalance));
930
+ };
931
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Panel, {
932
+ title: t("extrasTitle"),
933
+ children: [
934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
935
+ style: {
936
+ display: "grid",
937
+ gap: 6
938
+ },
939
+ children: [
940
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("utilityLabel") }),
941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
942
+ className: "rai-choice",
943
+ value: current ?? "",
944
+ onChange: (event) => {
945
+ action.run(() => remote.savePreferences({ utilityModel: event.target.value === "" ? null : event.target.value }));
946
+ },
947
+ children: [
948
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
949
+ value: "",
950
+ children: t("utilityOff")
951
+ }),
952
+ current !== null && !cheapestFirst.some((entry) => entry.slug === current) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
953
+ value: current,
954
+ children: current
955
+ }) : null,
956
+ cheapestFirst.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
957
+ value: entry.slug,
958
+ children: [
959
+ entry.maker,
960
+ " ",
961
+ entry.title,
962
+ " · ",
963
+ pricePair(entry.price)
964
+ ]
965
+ }, entry.slug))
966
+ ]
967
+ }),
968
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
969
+ className: "rai-note",
970
+ children: t("utilityHint")
971
+ })
972
+ ]
973
+ }),
974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
975
+ style: {
976
+ display: "grid",
977
+ gap: 6
978
+ },
979
+ children: [
980
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageLabel") }),
981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
982
+ className: "rai-choice",
983
+ "data-routerai": "image-model",
984
+ value: overview.imageModel ?? "",
985
+ onChange: (event) => {
986
+ action.run(() => remote.savePreferences({ imageModel: event.target.value === "" ? null : event.target.value }));
987
+ },
988
+ children: [
989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
990
+ value: "",
991
+ children: t("imageOff")
992
+ }),
993
+ known(painters, overview.imageModel) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
994
+ value: overview.imageModel ?? "",
995
+ children: overview.imageModel
996
+ }),
997
+ painters.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
998
+ value: entry.slug,
999
+ children: [
1000
+ entry.maker,
1001
+ " ",
1002
+ entry.title,
1003
+ entry.perImage === void 0 ? "" : ` · ${t("perImage", { price: rub(entry.perImage) })}`
1004
+ ]
1005
+ }, entry.slug))
1006
+ ]
1007
+ }),
1008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1009
+ className: "rai-note",
1010
+ children: t("imageHint")
1011
+ })
1012
+ ]
1013
+ }),
1014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1015
+ style: {
1016
+ display: "grid",
1017
+ gap: 6
1018
+ },
1019
+ children: [
1020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("speechLabel") }),
1021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1022
+ className: "rai-choice",
1023
+ "data-routerai": "speech-model",
1024
+ value: overview.speechModel,
1025
+ onChange: (event) => {
1026
+ action.run(() => remote.savePreferences({ speechModel: event.target.value }));
1027
+ },
1028
+ children: [known(listeners, overview.speechModel) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1029
+ value: overview.speechModel,
1030
+ children: overview.speechModel
1031
+ }), listeners.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1032
+ value: entry.slug,
1033
+ children: [
1034
+ entry.maker,
1035
+ " ",
1036
+ entry.title,
1037
+ entry.perSecond === void 0 ? "" : ` · ${t("perMinute", { price: rub(entry.perSecond * 60) })}`
1038
+ ]
1039
+ }, entry.slug))]
1040
+ }),
1041
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1042
+ className: "rai-note",
1043
+ children: t("speechHint")
1044
+ })
1045
+ ]
1046
+ }),
1047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1048
+ className: "rai-line",
1049
+ children: [
1050
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1051
+ className: "rai-fill",
1052
+ children: t("lowBalanceLabel")
1053
+ }),
1054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1055
+ style: { width: 96 },
1056
+ inputMode: "decimal",
1057
+ value: threshold,
1058
+ onChange: (event) => {
1059
+ setThreshold(event.target.value);
1060
+ },
1061
+ onBlur: commitThreshold
1062
+ }),
1063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "₽" })
1064
+ ]
1065
+ }),
1066
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1067
+ className: "rai-alert",
1068
+ children: action.error
1069
+ }) : null
1070
+ ]
1071
+ });
1072
+ }
1073
+ function SettingsPage({ t, remote }) {
1074
+ const { overview, failure } = useRemote(remote);
1075
+ const [catalog, setCatalog] = (0, react.useState)(null);
1076
+ const [catalogFailure, setCatalogFailure] = (0, react.useState)(null);
1077
+ (0, react.useEffect)(() => {
1078
+ let current = true;
1079
+ remote.catalog().then((entries) => {
1080
+ if (current) setCatalog(entries);
1081
+ }, (problem) => {
1082
+ if (current) setCatalogFailure(problem instanceof Error ? problem.message : String(problem));
1083
+ });
1084
+ return () => {
1085
+ current = false;
1086
+ };
1087
+ }, [remote]);
1088
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1089
+ className: "rai-page",
1090
+ children: [
1091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1092
+ className: "rai-head",
1093
+ children: [
1094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 40 }),
1095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("heading") }),
1096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("lead") })
1097
+ ]
1098
+ }),
1099
+ failure !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1100
+ className: "rai-alert",
1101
+ children: failure
1102
+ }) : null,
1103
+ overview === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1104
+ overview.connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
1105
+ t,
1106
+ remote,
1107
+ overview
1108
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Connect, {
1109
+ t,
1110
+ remote,
1111
+ overview
1112
+ }),
1113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
1114
+ t,
1115
+ remote,
1116
+ overview,
1117
+ catalog
1118
+ }),
1119
+ catalogFailure !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1120
+ className: "rai-alert",
1121
+ children: catalogFailure
1122
+ }) : null,
1123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Extras, {
1124
+ t,
1125
+ remote,
1126
+ overview,
1127
+ catalog
1128
+ })
1129
+ ] })
1130
+ ]
1131
+ });
1132
+ }
1133
+ // src/client/SidebarBalance.tsx
1134
+ /** `wide` сообщает harness: развёрнута ли боковая панель. */
1135
+ function SidebarBalance({ t, remote, wide }) {
1136
+ const { overview } = useRemote(remote);
1137
+ const [shown, setShown] = (0, react.useState)(false);
1138
+ if (overview === null) return null;
1139
+ const balance = overview.account?.balance;
1140
+ const running = overview.problem !== null || balance !== void 0 && balance < overview.lowBalance;
1141
+ const cap = overview.account?.key?.cap;
1142
+ const classes = [
1143
+ "rai-side",
1144
+ wide ? "" : "rai-compact",
1145
+ running ? "rai-warn" : ""
1146
+ ].join(" ").trim();
1147
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1148
+ type: "button",
1149
+ className: classes,
1150
+ "data-routerai": "sidebar-balance",
1151
+ title: t("sidebarTitle"),
1152
+ onClick: () => {
1153
+ setShown(true);
1154
+ remote.pull(true);
1155
+ },
1156
+ children: [
1157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 16 }),
1158
+ wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sidebarTitle") }) : null,
1159
+ wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: overview.connected ? rub(balance) : t("sidebarConnect") }) : null
1160
+ ]
1161
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1162
+ open: shown,
1163
+ onClose: () => {
1164
+ setShown(false);
1165
+ },
1166
+ title: t("sidebarTitle"),
1167
+ closeLabel: t("close"),
1168
+ footer: overview.connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1169
+ variant: "primary",
1170
+ onClick: () => {
1171
+ openTab(BILLING_PAGE);
1172
+ },
1173
+ children: t("addFunds")
1174
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1175
+ variant: "primary",
1176
+ onClick: () => {
1177
+ remote.signIn();
1178
+ },
1179
+ children: t("connectButton")
1180
+ }),
1181
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1182
+ style: {
1183
+ display: "grid",
1184
+ gap: 12
1185
+ },
1186
+ children: [
1187
+ overview.problem !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1188
+ className: "rai-alert",
1189
+ children: overview.problem.text
1190
+ }) : running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1191
+ className: "rai-caution",
1192
+ children: t("sidebarLow")
1193
+ }) : null,
1194
+ overview.connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1195
+ className: "rai-figures",
1196
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1197
+ className: "rai-figure rai-main",
1198
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1199
+ className: "rai-note",
1200
+ children: t("balance")
1201
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rub(balance) })]
1202
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1203
+ className: "rai-figure",
1204
+ children: [
1205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1206
+ className: "rai-note",
1207
+ children: t("keyCap")
1208
+ }),
1209
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: cap === void 0 ? t("keyCapNone") : rub(cap.left) }),
1210
+ cap === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1211
+ className: "rai-faint",
1212
+ children: t("keyCapLeft", {
1213
+ amount: rub(cap.amount),
1214
+ period: t(`period_${cap.period}`)
1215
+ })
1216
+ })
1217
+ ]
1218
+ })]
1219
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1220
+ className: "rai-note",
1221
+ children: t("connectHint")
1222
+ }),
1223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1224
+ className: "rai-faint",
1225
+ children: t("sidebarHint")
1226
+ })
1227
+ ]
1228
+ })
1229
+ })] });
1230
+ }
1231
+ // src/client/styles.ts
1232
+ const ACCENT = "#2d67f6";
1233
+ const SHEET = `
1234
+ .rai-page{display:grid;gap:18px;padding-bottom:28px;font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary)}
1235
+ .rai-head{display:grid;grid-template-columns:auto 1fr;column-gap:12px;align-items:center}
1236
+ .rai-head h2{margin:0;font:var(--dsw-font-l-20)}
1237
+ .rai-head p{margin:0;grid-column:2;color:var(--dsw-alias-label-secondary)}
1238
+ .rai-panel{display:grid;gap:12px;padding:16px;border-radius:var(--dsw-radius-md);border:1px solid var(--dsw-alias-settings-card-stroke);background:var(--dsw-alias-settings-card-fill)}
1239
+ .rai-panel>h3{margin:0;font:var(--dsw-font-s-strong-14)}
1240
+ .rai-note{margin:0;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-secondary)}
1241
+ .rai-faint{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-tertiary)}
1242
+ .rai-alert{margin:0;font:var(--dsw-font-xs-13);color:var(--dsw-alias-state-error-primary)}
1243
+ .rai-caution{margin:0;font:var(--dsw-font-xs-13);color:var(--dsw-alias-state-warn-label)}
1244
+ .rai-line{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
1245
+ .rai-line>.rai-fill{flex:1 1 160px;min-width:0}
1246
+ .rai-clip{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1247
+ .rai-figures{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}
1248
+ .rai-figure{display:grid;gap:2px;padding:10px 12px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3)}
1249
+ .rai-figure>strong{font:var(--dsw-font-l-20)}
1250
+ .rai-figure.rai-main{border-color:${ACCENT}}
1251
+ .rai-gauge{height:3px;border-radius:2px;background:var(--dsw-alias-border-l2)}
1252
+ .rai-gauge>span{display:block;height:100%;border-radius:2px;background:${ACCENT}}
1253
+ .rai-rows{display:grid;border-radius:var(--dsw-radius-sm);border:1px solid var(--dsw-alias-border-l3);overflow:hidden}
1254
+ .rai-rows.rai-tall{max-height:380px;overflow-y:auto}
1255
+ .rai-row{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;padding:8px 10px}
1256
+ .rai-row+.rai-row{border-top:1px solid var(--dsw-alias-border-l2)}
1257
+ .rai-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
1258
+ label.rai-row{cursor:pointer}
1259
+ .rai-row input[type=checkbox]{width:16px;height:16px;margin:0;accent-color:${ACCENT}}
1260
+ .rai-order{width:1.5em;text-align:right;color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-xs-13)}
1261
+ .rai-tools{display:flex;gap:2px}
1262
+ .rai-tools button{all:unset;display:grid;place-items:center;width:26px;height:26px;border-radius:6px;cursor:pointer;color:var(--dsw-alias-label-secondary)}
1263
+ .rai-tools button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1264
+ .rai-tools button:disabled{opacity:.3;cursor:default}
1265
+ .rai-choice{height:36px;padding:0 8px;font:inherit;color:inherit;border-radius:var(--dsw-radius-sm);border:1px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1)}
1266
+ .rai-side{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:8px;width:100%;padding:6px 8px;border-radius:var(--dsw-radius-sm);cursor:pointer;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-secondary)}
1267
+ .rai-side:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1268
+ .rai-side>span{flex:1}
1269
+ .rai-side>b{font-weight:500;color:var(--dsw-alias-label-primary)}
1270
+ .rai-side.rai-compact{width:auto;justify-content:center}
1271
+ .rai-side.rai-warn,.rai-side.rai-warn>b{color:var(--dsw-alias-state-warn-label)}
1272
+ .rai-cost{position:relative;display:inline-flex}
1273
+ .rai-cost>button{all:unset;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 9px;border-radius:12px;cursor:pointer;white-space:nowrap;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
1274
+ .rai-cost>button:hover{color:var(--dsw-alias-label-primary)}
1275
+ .rai-sheet{position:absolute;z-index:60;right:0;bottom:calc(100% + 8px);width:300px;max-width:calc(100vw - 24px);box-sizing:border-box;display:grid;gap:8px;padding:12px 14px;border-radius:var(--dsw-radius-md);font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 12px 32px rgba(0,0,0,.18)}
1276
+ .rai-sheet dl{display:grid;grid-template-columns:1fr auto;gap:4px 14px;margin:0}
1277
+ .rai-sheet dt{color:var(--dsw-alias-label-secondary)}
1278
+ .rai-sheet dd{margin:0;text-align:right;font-weight:500}
1279
+ .rai-sheet ul{display:grid;gap:2px;margin:0;padding:6px 0 0;list-style:none;border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
1280
+ .rai-sheet li{display:flex;gap:8px}
1281
+ .rai-sheet li>span{flex:1;min-width:0}
1282
+ .rai-banner{display:flex;align-items:center;gap:12px;max-width:540px;padding:12px 14px;border-radius:var(--dsw-radius-md);font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 8px 24px rgba(0,0,0,.14)}
1283
+ .rai-banner>div{flex:1;min-width:0;display:grid;gap:2px}
1284
+ .rai-drawn{display:grid;gap:8px;padding:10px 12px;border-radius:var(--dsw-radius-md);font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3)}
1285
+ .rai-gallery{display:flex;flex-wrap:wrap;gap:8px}
1286
+ .rai-gallery img,.rai-gallery i{display:block;max-width:min(320px,100%);max-height:320px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-2)}
1287
+ .rai-gallery i{width:200px;height:200px}
1288
+ .rai-busy{width:14px;height:14px;flex:none;border-radius:50%;border:2px solid var(--dsw-alias-border-l3);border-top-color:${ACCENT};animation:rai-turn .8s linear infinite}
1289
+ @keyframes rai-turn{to{transform:rotate(1turn)}}
1290
+ `;
1291
+ /** Подключить стили. Атрибуты data-plugin* — соглашение harness: по ним он снимает стили выгруженного плагина. */
1292
+ function mountStyles(owner) {
1293
+ const element = document.createElement("style");
1294
+ element.dataset["plugin"] = owner;
1295
+ element.dataset["pluginCss"] = `${owner}/styles`;
1296
+ element.textContent = SHEET;
1297
+ document.head.append(element);
1298
+ return () => {
1299
+ element.remove();
1300
+ };
1301
+ }
1302
+ // src/client/texts.ts
1303
+ const NAMESPACE = "routerai";
1304
+ const russian = {
1305
+ section: "RouterAI",
1306
+ heading: "RouterAI",
1307
+ lead: "Модели разных производителей по одному ключу, оплата в рублях.",
1308
+ connectTitle: "Подключение",
1309
+ connectHint: "Нажмите «Войти» — откроется routerai.ru, где вы подтвердите доступ и зададите лимит расходов. Ключ сохранится сам.",
1310
+ connectButton: "Войти через RouterAI",
1311
+ connectWaiting: "Подтвердите доступ во вкладке браузера, которая только что открылась.",
1312
+ connectReopen: "Открыть вкладку снова",
1313
+ connectAbort: "Прервать",
1314
+ pasteLabel: "Уже есть ключ?",
1315
+ pasteHolder: "sk-…",
1316
+ pasteButton: "Сохранить",
1317
+ accountNoLabel: "Ключ RouterAI",
1318
+ balance: "На счёте",
1319
+ keyCap: "Остаток лимита ключа",
1320
+ keyCapLeft: "из {amount} {period}",
1321
+ keyCapNone: "не задан",
1322
+ keySpent: "Потрачено по ключу",
1323
+ keySpentMonth: "{amount} за месяц",
1324
+ addFunds: "Пополнить",
1325
+ reload: "Обновить",
1326
+ disconnect: "Выйти",
1327
+ keyRejected: "RouterAI больше не принимает этот ключ. Войдите заново.",
1328
+ keyDisabled: "Ключ отключён в кабинете RouterAI.",
1329
+ offline: "Нет связи с RouterAI — данные аккаунта недоступны.",
1330
+ pickerTitle: "Модели в пикере",
1331
+ pickerHint: "Порядок здесь — порядок в пикере. Цены указаны в рублях за миллион токенов: вход / выход.",
1332
+ pickerEmpty: "Ничего не выбрано. Отметьте модели в каталоге ниже.",
1333
+ pickerUnknown: "в каталоге не найдена",
1334
+ moveUp: "Поднять",
1335
+ moveDown: "Опустить",
1336
+ drop: "Убрать из пикера",
1337
+ apply: "Применить",
1338
+ applied: "Изменения сохранены",
1339
+ revert: "Отменить правки",
1340
+ sets: "Готовые наборы",
1341
+ set_everyday: "На каждый день",
1342
+ set_coding: "Для кода",
1343
+ set_budget: "Экономный",
1344
+ catalogTitle: "Каталог RouterAI",
1345
+ catalogSearch: "Название, производитель или идентификатор",
1346
+ catalogAnyMaker: "Любой производитель",
1347
+ catalogToolsOnly: "Скрыть модели без инструментов",
1348
+ catalogShown: "Показано {shown} из {total}",
1349
+ catalogLoading: "Загружаю каталог…",
1350
+ tagReasoning: "думает",
1351
+ tagVision: "видит картинки",
1352
+ tagNoTools: "без инструментов",
1353
+ tagAlias: "всегда последняя версия",
1354
+ extrasTitle: "Дополнительно",
1355
+ utilityLabel: "Служебная модель",
1356
+ utilityHint: "На ней придумываются названия сессий и сжимается длинный контекст. Дешёвая модель экономит деньги и не влияет на ответы.",
1357
+ utilityOff: "Не использовать — всё делает модель разговора",
1358
+ lowBalanceLabel: "Предупреждать о балансе ниже",
1359
+ imageLabel: "Модель для картинок",
1360
+ imageHint: "Попросите агента нарисовать что-нибудь — картинка появится в чате и сохранится в папку routerai-media проекта. Каждая картинка оплачивается отдельно.",
1361
+ imageOff: "Не рисовать картинки",
1362
+ perImage: "{price} за картинку",
1363
+ speechLabel: "Модель распознавания речи",
1364
+ speechHint: "Работает с голосовым вводом DeepSeek Harness: включите плагин «Голосовой ввод» и выберите в нём службу RouterAI.",
1365
+ perMinute: "{price} за минуту",
1366
+ sidebarConnect: "Войти",
1367
+ sidebarTitle: "RouterAI",
1368
+ sidebarLow: "Баланс подходит к концу.",
1369
+ sidebarHint: "Модели и ключ настраиваются в Настройках → RouterAI.",
1370
+ close: "Закрыть",
1371
+ drawing: "Рисую картинку…",
1372
+ drawn: "Картинка",
1373
+ drawFailed: "Нарисовать не получилось",
1374
+ drawnCost: "стоимость {cost}",
1375
+ openFile: "Открыть {name}",
1376
+ costTitle: "Стоимость сессии",
1377
+ costTotal: "Всего",
1378
+ costLast: "Последний запрос",
1379
+ costCalls: "Запросов к модели",
1380
+ costCached: "Вход из кэша",
1381
+ costInput: "Вход, токены",
1382
+ costCacheWrites: "Записано в кэш, токены",
1383
+ costOutput: "Выход, токены",
1384
+ costUnpriced: "Запросов без цены в каталоге: {count}",
1385
+ costNote: "Расчёт по числу токенов и ценам каталога. Точные списания — в кабинете RouterAI.",
1386
+ problem_no_funds: "На балансе RouterAI закончились деньги",
1387
+ problem_key_cap: "Ключ RouterAI исчерпал лимит расходов",
1388
+ problem_key_off: "Ключ RouterAI отключён",
1389
+ openKeys: "К ключам",
1390
+ period_daily: "в сутки",
1391
+ period_weekly: "в неделю",
1392
+ period_monthly: "в месяц",
1393
+ period_other: ""
1394
+ };
1395
+ const english = {
1396
+ section: "RouterAI",
1397
+ heading: "RouterAI",
1398
+ lead: "Models from many vendors behind one key, billed in rubles.",
1399
+ connectTitle: "Connection",
1400
+ connectHint: "Press “Sign in”: routerai.ru opens, you approve access and set a spending limit. The key is saved for you.",
1401
+ connectButton: "Sign in with RouterAI",
1402
+ connectWaiting: "Approve access in the browser tab that has just opened.",
1403
+ connectReopen: "Open the tab again",
1404
+ connectAbort: "Stop",
1405
+ pasteLabel: "Already have a key?",
1406
+ pasteHolder: "sk-…",
1407
+ pasteButton: "Save",
1408
+ accountNoLabel: "RouterAI key",
1409
+ balance: "Balance",
1410
+ keyCap: "Key limit left",
1411
+ keyCapLeft: "of {amount} {period}",
1412
+ keyCapNone: "not set",
1413
+ keySpent: "Spent with this key",
1414
+ keySpentMonth: "{amount} this month",
1415
+ addFunds: "Add funds",
1416
+ reload: "Refresh",
1417
+ disconnect: "Sign out",
1418
+ keyRejected: "RouterAI no longer accepts this key. Sign in again.",
1419
+ keyDisabled: "The key is disabled in the RouterAI dashboard.",
1420
+ offline: "RouterAI is unreachable — account data is unavailable.",
1421
+ pickerTitle: "Models in the picker",
1422
+ pickerHint: "The order here is the order in the picker. Prices are rubles per million tokens: input / output.",
1423
+ pickerEmpty: "Nothing selected. Tick models in the catalog below.",
1424
+ pickerUnknown: "not found in the catalog",
1425
+ moveUp: "Move up",
1426
+ moveDown: "Move down",
1427
+ drop: "Remove from the picker",
1428
+ apply: "Apply",
1429
+ applied: "Changes saved",
1430
+ revert: "Discard edits",
1431
+ sets: "Ready-made sets",
1432
+ set_everyday: "Everyday",
1433
+ set_coding: "Coding",
1434
+ set_budget: "Budget",
1435
+ catalogTitle: "RouterAI catalog",
1436
+ catalogSearch: "Name, vendor or identifier",
1437
+ catalogAnyMaker: "Any vendor",
1438
+ catalogToolsOnly: "Hide models without tool calling",
1439
+ catalogShown: "Showing {shown} of {total}",
1440
+ catalogLoading: "Loading the catalog…",
1441
+ tagReasoning: "reasoning",
1442
+ tagVision: "reads images",
1443
+ tagNoTools: "no tools",
1444
+ tagAlias: "always the latest version",
1445
+ extrasTitle: "More",
1446
+ utilityLabel: "Utility model",
1447
+ utilityHint: "It names sessions and compacts long context. A cheap model saves money and does not affect answers.",
1448
+ utilityOff: "Off — the conversation model does everything",
1449
+ lowBalanceLabel: "Warn when the balance is below",
1450
+ imageLabel: "Image model",
1451
+ imageHint: "Ask the agent to draw something: the image appears in the chat and is saved to the routerai-media folder of the project. Each image is billed separately.",
1452
+ imageOff: "Do not draw images",
1453
+ perImage: "{price} per image",
1454
+ speechLabel: "Speech recognition model",
1455
+ speechHint: "Used by DeepSeek Harness voice input: enable the Voice input plugin and choose the RouterAI service there.",
1456
+ perMinute: "{price} per minute",
1457
+ sidebarConnect: "Sign in",
1458
+ sidebarTitle: "RouterAI",
1459
+ sidebarLow: "The balance is running out.",
1460
+ sidebarHint: "Models and the key are configured in Settings → RouterAI.",
1461
+ close: "Close",
1462
+ drawing: "Drawing the image…",
1463
+ drawn: "Image",
1464
+ drawFailed: "Could not draw the image",
1465
+ drawnCost: "cost {cost}",
1466
+ openFile: "Open {name}",
1467
+ costTitle: "Session cost",
1468
+ costTotal: "Total",
1469
+ costLast: "Last request",
1470
+ costCalls: "Model requests",
1471
+ costCached: "Input served from cache",
1472
+ costInput: "Input tokens",
1473
+ costCacheWrites: "Tokens written to cache",
1474
+ costOutput: "Output tokens",
1475
+ costUnpriced: "Requests with no catalog price: {count}",
1476
+ costNote: "Calculated from token counts and catalog prices. Exact charges are in the RouterAI dashboard.",
1477
+ problem_no_funds: "The RouterAI balance has run out",
1478
+ problem_key_cap: "The RouterAI key has reached its spending limit",
1479
+ problem_key_off: "The RouterAI key is disabled",
1480
+ openKeys: "Open keys",
1481
+ period_daily: "per day",
1482
+ period_weekly: "per week",
1483
+ period_monthly: "per month",
1484
+ period_other: ""
1485
+ };
1486
+ const DICTIONARIES = {
1487
+ ru: russian,
1488
+ en: english,
1489
+ zh: english
1490
+ };
1491
+ // src/client/main.tsx
1492
+ const name = "routerai-client";
1493
+ const inject = ["slots", "locale"];
1494
+ const PACKAGE = "@router-ai/dsh-plugin-routerai";
1495
+ function apply(ctx) {
1496
+ const slots = ctx.slots;
1497
+ const locale = ctx.locale;
1498
+ ctx.effect(() => locale.register(NAMESPACE, DICTIONARIES), "routerai: texts");
1499
+ ctx.effect(() => mountStyles(PACKAGE), "routerai: styles");
1500
+ const t = locale.bind(NAMESPACE);
1501
+ const remote = new Remote();
1502
+ ctx.effect(() => remote.run(), "routerai: remote state");
1503
+ const shared = () => ({ remote });
1504
+ const placements = [
1505
+ {
1506
+ slot: "settings.section",
1507
+ options: {
1508
+ id: "routerai",
1509
+ order: 5,
1510
+ label: () => t("section"),
1511
+ inject: shared
1512
+ },
1513
+ component: SettingsPage
1514
+ },
1515
+ {
1516
+ slot: "settings.section.icon",
1517
+ options: { id: "routerai" },
1518
+ component: ({ size }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size })
1519
+ },
1520
+ {
1521
+ slot: "sidebar.footer.action",
1522
+ options: {
1523
+ id: "routerai-balance",
1524
+ inject: shared
1525
+ },
1526
+ component: SidebarBalance
1527
+ },
1528
+ {
1529
+ slot: "conversation.composer.dock",
1530
+ options: {
1531
+ id: "routerai-session-cost",
1532
+ order: 5,
1533
+ inject: (sessionId) => ({
1534
+ remote,
1535
+ sessionId
1536
+ })
1537
+ },
1538
+ component: SessionCost
1539
+ },
1540
+ {
1541
+ slot: "shell.quota-notice",
1542
+ options: {
1543
+ select: (owner) => owner.code === "QUOTA" && remote.read().overview?.problem != null ? owner : null,
1544
+ inject: shared
1545
+ },
1546
+ component: MoneyNotice
1547
+ }
1548
+ ];
1549
+ placements.push({
1550
+ slot: "tool.call.toolview",
1551
+ options: { key: DRAW_TOOL },
1552
+ component: DrawCard
1553
+ });
1554
+ for (const { slot, options, component } of placements) slots.inject(slot, () => slots.register({
1555
+ name: slot,
1556
+ locale: NAMESPACE,
1557
+ ...options
1558
+ }, component));
1559
+ }
1560
+ exports.apply = apply;
1561
+ exports.inject = inject;
1562
+ exports.name = name;
1563
+ return module.exports;
1564
+ }
1565
+ });