@dickpy/dsh-imagegen 1.0.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,2413 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@dickpy/dsh-imagegen",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_dom_client = require("react-dom/client");
8
+ let react = require("react");
9
+ let react_dom = require("react-dom");
10
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
11
+ let react_jsx_runtime = require("react/jsx-runtime");
12
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
13
+ //#region src/protocol.ts
14
+ /** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
15
+ const SETTINGS_API = {
16
+ describe: "/api/dsh-imagegen/settings/describe",
17
+ mutate: "/api/dsh-imagegen/settings/mutate"
18
+ };
19
+ /** The image-generation proxy route. */
20
+ const GENERATE_API = "/api/dsh-imagegen/generate";
21
+ /**
22
+ * Same-origin route family for the host-persisted generation history. Images
23
+ * live as files under ~/.dsh/dsh-imagegen/images/ and are served back through
24
+ * the `image` prefix route, so list responses carry metadata only (never
25
+ * base64) and the browser loads thumbnails/previews lazily.
26
+ */
27
+ const HISTORY_API = {
28
+ list: "/api/dsh-imagegen/history/list",
29
+ append: "/api/dsh-imagegen/history/append",
30
+ remove: "/api/dsh-imagegen/history/remove",
31
+ clear: "/api/dsh-imagegen/history/clear",
32
+ image: "/api/dsh-imagegen/history/image"
33
+ };
34
+ //#endregion
35
+ //#region src/client/api.ts
36
+ /**
37
+ * Browser-side API client for the /api/dsh-imagegen route family. The only
38
+ * data access path the panel uses — plain fetch, same origin.
39
+ */
40
+ /** Error carrying the route's JSON error message. */
41
+ var ImageGenApiError = class extends Error {
42
+ /** Stable wire code from the host. */
43
+ code;
44
+ constructor(message, code = "generate-failed") {
45
+ super(message);
46
+ this.name = "ImageGenApiError";
47
+ this.code = code;
48
+ }
49
+ };
50
+ /** Parse the { ok, ... } envelope or throw an ImageGenApiError. */
51
+ async function readEnvelope(response) {
52
+ let body;
53
+ try {
54
+ body = await response.json();
55
+ } catch {
56
+ throw new ImageGenApiError(`HTTP ${response.status}: invalid JSON response`);
57
+ }
58
+ if (body === null || typeof body !== "object") throw new ImageGenApiError(`HTTP ${response.status}: malformed response`);
59
+ const record = body;
60
+ if (record.ok !== true) throw new ImageGenApiError(typeof record.message === "string" ? record.message : `HTTP ${response.status}`, typeof record.code === "string" ? record.code : "generate-failed");
61
+ return body;
62
+ }
63
+ /** The browser half's data entry point. */
64
+ var ImageGenApi = class {
65
+ /** Forward one generate request to the host proxy. */
66
+ async generate(request) {
67
+ return { images: (await readEnvelope(await fetch(GENERATE_API, {
68
+ method: "POST",
69
+ headers: { "content-type": "application/json" },
70
+ body: JSON.stringify(request)
71
+ }))).images };
72
+ }
73
+ /** List the host-persisted history (newest first). */
74
+ async historyList() {
75
+ return (await readEnvelope(await fetch(HISTORY_API.list, { method: "POST" }))).entries;
76
+ }
77
+ /** Append one generation to the host-persisted history. */
78
+ async historyAppend(entry) {
79
+ return (await readEnvelope(await fetch(HISTORY_API.append, {
80
+ method: "POST",
81
+ headers: { "content-type": "application/json" },
82
+ body: JSON.stringify({ entry })
83
+ }))).entries;
84
+ }
85
+ /** Remove one history entry by id. */
86
+ async historyRemove(id) {
87
+ return (await readEnvelope(await fetch(HISTORY_API.remove, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify({ id })
91
+ }))).entries;
92
+ }
93
+ /** Clear the entire history. */
94
+ async historyClear() {
95
+ return (await readEnvelope(await fetch(HISTORY_API.clear, { method: "POST" }))).entries;
96
+ }
97
+ };
98
+ //#endregion
99
+ //#region src/client/controller.ts
100
+ /** The panel state owner the sidebar entry toggles and the view renders from. */
101
+ var ImageGenController = class {
102
+ panelOpen = false;
103
+ listeners = /* @__PURE__ */ new Set();
104
+ getSnapshot() {
105
+ return { panelOpen: this.panelOpen };
106
+ }
107
+ subscribe(fn) {
108
+ this.listeners.add(fn);
109
+ return () => {
110
+ this.listeners.delete(fn);
111
+ };
112
+ }
113
+ open() {
114
+ if (this.panelOpen) return;
115
+ this.panelOpen = true;
116
+ this.notify();
117
+ }
118
+ close() {
119
+ if (!this.panelOpen) return;
120
+ this.panelOpen = false;
121
+ this.notify();
122
+ }
123
+ toggle() {
124
+ if (this.panelOpen) this.close();
125
+ else this.open();
126
+ }
127
+ notify() {
128
+ for (const fn of [...this.listeners]) fn();
129
+ }
130
+ };
131
+ //#endregion
132
+ //#region src/client/locales.ts
133
+ /**
134
+ * dsh-imagegen surface copy: zh is the key source, en mirrors every key.
135
+ */
136
+ const zh = {
137
+ "entry.label": "AI 生图",
138
+ "entry.tooltip": "AI 生图面板(gpt-image-2)",
139
+ "panel.title": "AI 生图",
140
+ "panel.subtitle": "gpt-image-2 图像生成",
141
+ "mode.text": "文生图",
142
+ "mode.edit": "图生图",
143
+ "prompt.placeholder": "描述你想要的画面,例如:一只戴着宇航员头盔的橘猫,在月球上举起望远镜,水彩风格,柔和光线…",
144
+ "prompt.required": "请输入提示词",
145
+ "prompt.count": "{count}/2000",
146
+ "params.size": "尺寸",
147
+ "params.quality": "清晰度",
148
+ "params.count": "生成数量",
149
+ "params.detail": "细节",
150
+ "size.auto": "自动",
151
+ "size.square": "1024×1024",
152
+ "size.landscape": "1536×1024",
153
+ "size.portrait": "1024×1536",
154
+ "size.small": "512×512",
155
+ "size.wide": "1792×1024",
156
+ "size.tall": "1024×1792",
157
+ "quality.auto": "自动",
158
+ "quality.low": "低",
159
+ "quality.medium": "中",
160
+ "quality.high": "高",
161
+ "count.one": "1 张",
162
+ "count.two": "2 张",
163
+ "count.three": "3 张",
164
+ "count.four": "4 张",
165
+ "detail.auto": "自动",
166
+ "detail.standard": "标准",
167
+ "detail.high": "高清",
168
+ "detail.hint": "透传参数,部分 gpt-image-2 网关支持;官方接口请保持「自动」",
169
+ "model.label": "模型",
170
+ "generate": "开始生成",
171
+ "generating": "生成中…",
172
+ "edit.upload": "点击或拖拽上传参考图片",
173
+ "edit.uploadHint": "PNG / JPG / WEBP,不超过 10MB",
174
+ "edit.change": "更换图片",
175
+ "edit.remove": "移除",
176
+ "edit.required": "请先上传参考图片",
177
+ "canvas.emptyTitle": "开始你的创作",
178
+ "canvas.emptyHint": "在左侧输入提示词并点击「开始生成」,结果将显示在这里",
179
+ "canvas.error": "生成失败:{error}",
180
+ "canvas.generating": "正在生成图片…",
181
+ "canvas.elapsed": "已用时 {seconds}s",
182
+ "canvas.images": "本次生成 {count} 张",
183
+ "download": "下载",
184
+ "revisedPrompt": "优化提示词:{prompt}",
185
+ "history.title": "历史记录",
186
+ "history.empty": "暂无历史记录,生成图片后会保存在这里",
187
+ "history.clear": "清空",
188
+ "history.restore": "恢复",
189
+ "history.delete": "删除",
190
+ "history.images": "张",
191
+ "history.viewing": "历史 · {time}",
192
+ "preview.title": "图片预览",
193
+ "preview.open": "点击预览",
194
+ "preview.close": "关闭",
195
+ "preview.prev": "上一张",
196
+ "preview.next": "下一张",
197
+ "preview.index": "{index} / {total}",
198
+ "config.missing": "尚未配置 API:请前往「设置 → 插件 → 可配置」为 AI 生图填写 api_url 与 api_key。",
199
+ "config.configured": "已连接 {url}",
200
+ "config.disabled": "插件已停用,请在设置中重新启用。",
201
+ "settings.title": "AI 生图(dsh-imagegen)",
202
+ "settings.description": "配置图像生成 API 地址与密钥",
203
+ "settings.apiUrl": "API 地址(api_url)",
204
+ "settings.apiUrlHint": "OpenAI 兼容接口基址,如 https://api.openai.com/v1;将自动拼接 /images/generations 与 /images/edits",
205
+ "settings.apiKey": "API 密钥(api_key)",
206
+ "settings.apiKeyHint": "Bearer 密钥,明文存于本机设置文档;界面只显示是否已设置",
207
+ "settings.apiKeySet": "已保存密钥;输入新值可更换,点击「清除」可删除",
208
+ "settings.apiKeyClear": "清除",
209
+ "settings.announceToAgent": "向 Agent 播报本插件",
210
+ "settings.announceToAgentHint": "开启后,本插件的存在与能力会写入每个 Agent 的系统提示词",
211
+ "settings.enabled": "启用插件",
212
+ "settings.enabledHint": "关闭后生图面板不可用(设置卡片始终可用)",
213
+ "settings.save": "保存",
214
+ "settings.saving": "保存中…",
215
+ "settings.discard": "放弃修改",
216
+ "settings.unsaved": "有未保存的修改",
217
+ "settings.saveFailed": "保存失败,请重试",
218
+ "settings.readOnly": "当前设置文档为只读,无法保存。",
219
+ "settings.notExposed": "设置命名空间不可用:本部署未提供该插件的设置服务。",
220
+ "settings.expand": "展开",
221
+ "settings.collapse": "收起",
222
+ "settings.inherit": "继承",
223
+ "settings.on": "开",
224
+ "settings.off": "关",
225
+ "settings.overridden": "已覆盖",
226
+ "settings.reset": "重置",
227
+ "settings.invalidNumber": "请输入有效数字"
228
+ };
229
+ const en = {
230
+ "entry.label": "AI Image",
231
+ "entry.tooltip": "AI image generation studio (gpt-image-2)",
232
+ "panel.title": "AI Image",
233
+ "panel.subtitle": "gpt-image-2 image generation",
234
+ "mode.text": "Text to Image",
235
+ "mode.edit": "Image to Image",
236
+ "prompt.placeholder": "Describe the picture you want, e.g. an orange cat in an astronaut helmet raising a telescope on the moon, watercolor style, soft light…",
237
+ "prompt.required": "Enter a prompt first",
238
+ "prompt.count": "{count}/2000",
239
+ "params.size": "Size",
240
+ "params.quality": "Quality",
241
+ "params.count": "Count",
242
+ "params.detail": "Detail",
243
+ "size.auto": "Auto",
244
+ "size.square": "1024×1024",
245
+ "size.landscape": "1536×1024",
246
+ "size.portrait": "1024×1536",
247
+ "size.small": "512×512",
248
+ "size.wide": "1792×1024",
249
+ "size.tall": "1024×1792",
250
+ "quality.auto": "Auto",
251
+ "quality.low": "Low",
252
+ "quality.medium": "Medium",
253
+ "quality.high": "High",
254
+ "count.one": "1",
255
+ "count.two": "2",
256
+ "count.three": "3",
257
+ "count.four": "4",
258
+ "detail.auto": "Auto",
259
+ "detail.standard": "Standard",
260
+ "detail.high": "High",
261
+ "detail.hint": "Passthrough parameter supported by some gpt-image-2 gateways; keep \"Auto\" for official endpoints",
262
+ "model.label": "Model",
263
+ "generate": "Generate",
264
+ "generating": "Generating…",
265
+ "edit.upload": "Click or drag to upload a reference image",
266
+ "edit.uploadHint": "PNG / JPG / WEBP, up to 10MB",
267
+ "edit.change": "Change image",
268
+ "edit.remove": "Remove",
269
+ "edit.required": "Upload a reference image first",
270
+ "canvas.emptyTitle": "Start creating",
271
+ "canvas.emptyHint": "Enter a prompt on the left and click \"Generate\"; results appear here",
272
+ "canvas.error": "Generation failed: {error}",
273
+ "canvas.generating": "Generating images…",
274
+ "canvas.elapsed": "Elapsed {seconds}s",
275
+ "canvas.images": "{count} image(s) generated",
276
+ "download": "Download",
277
+ "revisedPrompt": "Refined prompt: {prompt}",
278
+ "history.title": "History",
279
+ "history.empty": "No history yet — generations will be saved here",
280
+ "history.clear": "Clear",
281
+ "history.restore": "Restore",
282
+ "history.delete": "Delete",
283
+ "history.images": "images",
284
+ "history.viewing": "History · {time}",
285
+ "preview.title": "Image preview",
286
+ "preview.open": "Click to preview",
287
+ "preview.close": "Close",
288
+ "preview.prev": "Previous",
289
+ "preview.next": "Next",
290
+ "preview.index": "{index} / {total}",
291
+ "config.missing": "API not configured: open \"Settings → Web UI Plugins\" and fill in api_url and api_key for AI Image.",
292
+ "config.configured": "Connected to {url}",
293
+ "config.disabled": "The plugin is disabled — re-enable it in Settings.",
294
+ "settings.title": "AI Image (dsh-imagegen)",
295
+ "settings.description": "Configure the image generation API endpoint and key",
296
+ "settings.apiUrl": "API URL (api_url)",
297
+ "settings.apiUrlHint": "OpenAI-compatible base URL, e.g. https://api.openai.com/v1; /images/generations and /images/edits are appended",
298
+ "settings.apiKey": "API Key (api_key)",
299
+ "settings.apiKeyHint": "Bearer key, stored in plaintext in the local settings document; the UI only shows whether it is set",
300
+ "settings.apiKeySet": "A key is stored; type a new value to replace it, or click \"Clear\" to remove it",
301
+ "settings.apiKeyClear": "Clear",
302
+ "settings.announceToAgent": "Announce this plugin to agents",
303
+ "settings.announceToAgentHint": "When on, the plugin presence and capabilities are written into every agent system prompt",
304
+ "settings.enabled": "Enable plugin",
305
+ "settings.enabledHint": "When off, the generation studio is unavailable (this card stays available)",
306
+ "settings.save": "Save",
307
+ "settings.saving": "Saving…",
308
+ "settings.discard": "Discard",
309
+ "settings.unsaved": "Unsaved changes",
310
+ "settings.saveFailed": "Save failed, please retry",
311
+ "settings.readOnly": "The settings document is read-only; saving is disabled.",
312
+ "settings.notExposed": "Settings namespace unavailable: this deployment does not serve this plugin's settings.",
313
+ "settings.expand": "Expand",
314
+ "settings.collapse": "Collapse",
315
+ "settings.inherit": "Inherit",
316
+ "settings.on": "On",
317
+ "settings.off": "Off",
318
+ "settings.overridden": "Overridden",
319
+ "settings.reset": "Reset",
320
+ "settings.invalidNumber": "Enter a valid number"
321
+ };
322
+ //#endregion
323
+ //#region src/client/helpers.ts
324
+ /**
325
+ * Shared panel helpers: the active-dictionary pick (document-language based,
326
+ * dsh-ssh precedent) bound to the dsh-imagegen interpolator, plus a small
327
+ * error-message extractor. All copy stays in the locale dictionaries.
328
+ */
329
+ /** Active dictionary, picked by the document language at call time. */
330
+ function dictionary() {
331
+ return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
332
+ }
333
+ /** Translate a key with optional {name} template params (current language). */
334
+ function tt(key, values) {
335
+ const text = dictionary()[key] ?? key;
336
+ if (values === void 0) return text;
337
+ let rendered = text;
338
+ for (const [name, value] of Object.entries(values)) rendered = rendered.replaceAll(`{${name}}`, String(value));
339
+ return rendered;
340
+ }
341
+ /** Human-readable error text from an unknown thrown value. */
342
+ function errorMessage(error) {
343
+ if (error instanceof Error) return error.message;
344
+ return String(error);
345
+ }
346
+ //#endregion
347
+ //#region \0dsh-css:E:\dsh-plugin\src\client\panel.module.css.mjs
348
+ const css$1 = "[data-pane=conversation]{position:relative}[data-dsh-imagegen-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-dsh-imagegen-view]{display:block}html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-imagegen-view]),html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-imagegen-view]){display:none!important}.Yvqh9W_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.Yvqh9W_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.Yvqh9W_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.Yvqh9W_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.Yvqh9W_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .Yvqh9W_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .Yvqh9W_entryLabel{display:none}.Yvqh9W_view{overflow:hidden}.Yvqh9W_panel,.Yvqh9W_panel *,.Yvqh9W_panel :before,.Yvqh9W_panel :after{box-sizing:border-box}.Yvqh9W_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex;overflow:hidden}.Yvqh9W_panelHeader{flex:none;align-items:baseline;gap:10px;display:flex}.Yvqh9W_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:16px;font-weight:700}.Yvqh9W_panelSubtitle{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}.Yvqh9W_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:10px;flex:none;padding:7px 12px;font-size:12px;line-height:1.5}.Yvqh9W_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.Yvqh9W_banner[data-kind=warn]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.Yvqh9W_studio{flex:1;gap:14px;min-width:0;min-height:0;display:flex}.Yvqh9W_config{flex-direction:column;flex:none;gap:12px;width:300px;min-width:260px;max-width:340px;height:100%;min-height:0;display:flex;overflow:hidden}.Yvqh9W_configScroll{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l2) transparent;flex-direction:column;flex:1;gap:12px;min-height:0;padding-right:2px;display:flex;overflow-y:auto}.Yvqh9W_configScroll::-webkit-scrollbar{width:8px}.Yvqh9W_configScroll::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:999px}.Yvqh9W_canvas{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:12px;flex-direction:column;flex:1;min-width:0;min-height:0;display:flex;position:relative;overflow:hidden}.Yvqh9W_history{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:12px;flex-direction:column;flex:none;width:240px;min-width:200px;max-width:280px;min-height:0;display:flex;overflow:hidden}.Yvqh9W_historyHeader{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;justify-content:space-between;align-items:center;gap:8px;padding:10px 12px;display:flex}.Yvqh9W_historyTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.Yvqh9W_historyClear{font:inherit;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background:0 0;border-radius:999px;padding:2px 8px;font-size:11.5px}.Yvqh9W_historyClear:hover{color:var(--dsw-alias-label-error);border-color:var(--dsw-alias-label-error)}.Yvqh9W_historyList{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l2) transparent;flex-direction:column;flex:1;gap:8px;min-height:0;padding:10px;display:flex;overflow-y:auto}.Yvqh9W_historyList::-webkit-scrollbar{width:8px}.Yvqh9W_historyList::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:999px}.Yvqh9W_historyEmpty{text-align:center;color:var(--dsw-alias-label-tertiary);flex:1;justify-content:center;align-items:center;padding:20px;font-size:12px;line-height:1.6;display:flex}.Yvqh9W_historyItem{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);border-radius:10px;flex-direction:column;flex:none;gap:6px;padding:8px;display:flex}.Yvqh9W_historyItem:hover{border-color:var(--dsw-alias-border-l2)}.Yvqh9W_historyItem[data-active]{border-color:var(--dsw-alias-brand-primary)}.Yvqh9W_historyMain{font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:flex-start;gap:8px;min-width:0;padding:0;display:flex}.Yvqh9W_historyThumb{object-fit:cover;background:var(--dsw-alias-bg-base);border-radius:8px;flex:none;width:52px;height:52px}.Yvqh9W_historyThumbPlaceholder{background:var(--dsw-alias-bg-layer-3);border-radius:8px;flex:none;width:52px;height:52px}.Yvqh9W_historyInfo{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Yvqh9W_historyPrompt{color:var(--dsw-alias-label-primary);-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:12px;line-height:1.4;display:-webkit-box;overflow:hidden}.Yvqh9W_historyMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;overflow:hidden}.Yvqh9W_historyActions{justify-content:flex-end;gap:6px;display:flex}.Yvqh9W_historyAction{font:inherit;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background:0 0;border-radius:999px;padding:2px 8px;font-size:11.5px}.Yvqh9W_historyAction:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Yvqh9W_historyAction[data-danger]:hover{color:var(--dsw-alias-label-error);border-color:var(--dsw-alias-label-error)}.Yvqh9W_card{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.Yvqh9W_modeRow{align-items:center;gap:8px;display:flex}.Yvqh9W_modePill{flex:1;justify-content:center;height:28px;font-size:13px}.Yvqh9W_uploadBox{min-height:128px;color:var(--dsw-alias-label-secondary);border:1.5px dashed var(--dsw-alias-border-l2);cursor:pointer;font:inherit;text-align:center;background:0 0;border-radius:12px;flex-direction:column;justify-content:center;align-items:center;gap:6px;padding:16px;font-size:12.5px;display:flex}.Yvqh9W_uploadBox:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-interactive-bg-hover)}.Yvqh9W_uploadBox:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Yvqh9W_uploadIcon{color:var(--dsw-alias-label-tertiary);display:inline-flex}.Yvqh9W_uploadHint{color:var(--dsw-alias-label-tertiary);font-size:11px}.Yvqh9W_reference{flex-direction:column;gap:8px;display:flex}.Yvqh9W_referenceImage{object-fit:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;width:100%;max-height:176px}.Yvqh9W_referenceActions{gap:8px;display:flex}.Yvqh9W_hiddenFile{display:none}.Yvqh9W_prompt{width:100%;min-height:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);resize:vertical;box-sizing:border-box;border-radius:10px;outline:none;padding:10px 12px;font-family:inherit;font-size:13px;line-height:1.6}.Yvqh9W_prompt:focus-visible{border-color:var(--dsw-alias-brand-primary)}.Yvqh9W_prompt::placeholder{color:var(--dsw-alias-label-tertiary)}.Yvqh9W_promptFooter{justify-content:flex-end;margin-top:-6px;display:flex}.Yvqh9W_promptCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px}.Yvqh9W_paramGroup{flex-direction:column;gap:8px;display:flex}.Yvqh9W_paramLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.Yvqh9W_optionRow{flex-wrap:wrap;gap:6px;display:flex}.Yvqh9W_optionGrid{grid-template-columns:repeat(3,1fr);gap:6px;display:grid}.Yvqh9W_optionPill{justify-content:center}.Yvqh9W_paramHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.45}.Yvqh9W_footer{border-top:1px solid var(--dsw-alias-border-l1);flex-direction:column;flex:none;align-items:stretch;gap:8px;padding:10px 2px 0 0;display:flex}.Yvqh9W_modelWrap{flex-direction:column;gap:5px;min-width:0;display:flex}.Yvqh9W_modelLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.Yvqh9W_modelSelect{width:100%;height:36px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;border-radius:18px;outline:none;padding:0 12px;font-family:inherit;font-size:13px}.Yvqh9W_modelSelect:focus-visible{border-color:var(--dsw-alias-brand-primary)}.Yvqh9W_modelSelect:disabled{opacity:.55}.Yvqh9W_generateButton{width:100%}.Yvqh9W_generateInner{align-items:center;gap:7px;display:inline-flex}.Yvqh9W_canvasState{text-align:center;color:var(--dsw-alias-label-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px;display:flex}.Yvqh9W_canvasStateTitle{color:var(--dsw-alias-label-secondary);font-size:14px;font-weight:600}.Yvqh9W_canvasStateHint{max-width:380px;font-size:12px;line-height:1.6}.Yvqh9W_canvasEmptyIcon{color:var(--dsw-alias-label-tertiary);margin-bottom:4px;display:inline-flex}.Yvqh9W_canvasError{color:var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-label-error);overflow-wrap:anywhere;border-radius:10px;flex:none;margin:14px;padding:10px 14px;font-size:12.5px;line-height:1.6}.Yvqh9W_canvasBody{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l2) transparent;flex-direction:column;flex:1;gap:10px;min-height:0;padding:14px;display:flex;overflow-y:auto}.Yvqh9W_canvasBody::-webkit-scrollbar{width:8px}.Yvqh9W_canvasBody::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:999px}.Yvqh9W_canvasMeta{color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;gap:8px;font-size:12px;display:flex}.Yvqh9W_canvasHistoryTag{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px}.Yvqh9W_grid{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));align-content:start;gap:14px;display:grid}.Yvqh9W_imageCard{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);cursor:zoom-in;border-radius:12px;flex-direction:column;margin:0;display:flex;position:relative;overflow:hidden}.Yvqh9W_imageCard:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Yvqh9W_image{aspect-ratio:1;object-fit:cover;background:var(--dsw-alias-bg-base);width:100%;display:block}.Yvqh9W_imageCaption{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;border-top:1px solid var(--dsw-alias-border-l1);padding:7px 10px;font-size:11px;line-height:1.5;overflow:hidden}.Yvqh9W_download{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-mask-1);border:1px solid var(--dsw-alias-border-l2);opacity:0;backdrop-filter:blur(4px);border-radius:999px;padding:2px 10px;font-size:12px;font-weight:500;line-height:20px;text-decoration:none;transition:opacity .12s;position:absolute;top:8px;right:8px}.Yvqh9W_imageCard:hover .Yvqh9W_download{opacity:1}.Yvqh9W_download:hover{background:var(--dsw-alias-bg-base)}.Yvqh9W_zoomHint{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-mask-1);border:1px solid var(--dsw-alias-border-l2);opacity:0;backdrop-filter:blur(4px);pointer-events:none;border-radius:999px;align-items:center;gap:5px;padding:2px 10px;font-size:12px;font-weight:500;line-height:20px;transition:opacity .12s;display:inline-flex;position:absolute;bottom:8px;left:8px}.Yvqh9W_imageCard:hover .Yvqh9W_zoomHint{opacity:1}.Yvqh9W_spinner,.Yvqh9W_bigSpinner{border:2px solid;border-top-color:#0000;border-radius:50%;flex:none;animation:.8s linear infinite Yvqh9W_dshImageGenSpin;display:inline-block}.Yvqh9W_spinner{width:11px;height:11px}.Yvqh9W_bigSpinner{width:30px;height:30px;color:var(--dsw-alias-state-business-primary);border-width:3px;margin-bottom:6px}.Yvqh9W_lightbox{z-index:1000;backdrop-filter:blur(6px);background:#000000b8;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.Yvqh9W_lightboxClose{color:#fff;cursor:pointer;background:#ffffff24;border:1px solid #ffffff47;border-radius:50%;justify-content:center;align-items:center;width:38px;height:38px;display:inline-flex;position:absolute;top:16px;right:16px}.Yvqh9W_lightboxClose:hover{background:#ffffff42}.Yvqh9W_lightboxNav{color:#fff;cursor:pointer;background:#ffffff24;border:1px solid #ffffff47;border-radius:50%;justify-content:center;align-items:center;width:42px;height:42px;display:inline-flex;position:absolute;top:50%;transform:translateY(-50%)}.Yvqh9W_lightboxNav:hover{background:#ffffff42}.Yvqh9W_lightboxNav[data-dir=prev]{left:16px}.Yvqh9W_lightboxNav[data-dir=next]{right:16px}.Yvqh9W_lightboxFigure{flex-direction:column;gap:10px;max-width:min(1100px,100vw - 160px);max-height:calc(100vh - 48px);margin:0;display:flex}.Yvqh9W_lightboxImage{object-fit:contain;background:#ffffff0a;border-radius:10px;max-width:100%;max-height:calc(100vh - 160px);box-shadow:0 24px 80px #00000080}.Yvqh9W_lightboxCaption{color:#ffffffe6;-webkit-line-clamp:3;-webkit-box-orient:vertical;font-size:12px;line-height:1.6;display:-webkit-box;overflow:hidden}.Yvqh9W_lightboxMeta{justify-content:space-between;align-items:center;gap:12px;display:flex}.Yvqh9W_lightboxIndex{color:#fffc;font-variant-numeric:tabular-nums;font-size:12px}.Yvqh9W_lightboxDownload{color:#fff;background:#ffffff24;border:1px solid #ffffff47;border-radius:999px;padding:4px 14px;font-size:12.5px;font-weight:500;text-decoration:none}.Yvqh9W_lightboxDownload:hover{background:#ffffff42}@keyframes Yvqh9W_dshImageGenSpin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.Yvqh9W_download,.Yvqh9W_spinner,.Yvqh9W_bigSpinner{transition:none;animation-duration:1.5s}}";
349
+ const tagId$1 = "@dickpy/dsh-imagegen/panel.module.css";
350
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
351
+ const tag = document.createElement("style");
352
+ tag.dataset.plugin = "@dickpy/dsh-imagegen";
353
+ tag.dataset.pluginCss = tagId$1;
354
+ tag.textContent = css$1;
355
+ document.head.appendChild(tag);
356
+ }
357
+ var panel_module_css_default = {
358
+ "referenceActions": "Yvqh9W_referenceActions",
359
+ "banner": "Yvqh9W_banner",
360
+ "historyThumb": "Yvqh9W_historyThumb",
361
+ "configScroll": "Yvqh9W_configScroll",
362
+ "reference": "Yvqh9W_reference",
363
+ "card": "Yvqh9W_card",
364
+ "grid": "Yvqh9W_grid",
365
+ "download": "Yvqh9W_download",
366
+ "zoomHint": "Yvqh9W_zoomHint",
367
+ "lightbox": "Yvqh9W_lightbox",
368
+ "optionPill": "Yvqh9W_optionPill",
369
+ "prompt": "Yvqh9W_prompt",
370
+ "lightboxImage": "Yvqh9W_lightboxImage",
371
+ "promptCount": "Yvqh9W_promptCount",
372
+ "optionRow": "Yvqh9W_optionRow",
373
+ "modelWrap": "Yvqh9W_modelWrap",
374
+ "canvasError": "Yvqh9W_canvasError",
375
+ "lightboxFigure": "Yvqh9W_lightboxFigure",
376
+ "dshImageGenSpin": "Yvqh9W_dshImageGenSpin",
377
+ "lightboxDownload": "Yvqh9W_lightboxDownload",
378
+ "referenceImage": "Yvqh9W_referenceImage",
379
+ "historyAction": "Yvqh9W_historyAction",
380
+ "optionGrid": "Yvqh9W_optionGrid",
381
+ "studio": "Yvqh9W_studio",
382
+ "entryLabel": "Yvqh9W_entryLabel",
383
+ "panelHeader": "Yvqh9W_panelHeader",
384
+ "historyHeader": "Yvqh9W_historyHeader",
385
+ "historyThumbPlaceholder": "Yvqh9W_historyThumbPlaceholder",
386
+ "historyInfo": "Yvqh9W_historyInfo",
387
+ "modeRow": "Yvqh9W_modeRow",
388
+ "canvasHistoryTag": "Yvqh9W_canvasHistoryTag",
389
+ "image": "Yvqh9W_image",
390
+ "canvasState": "Yvqh9W_canvasState",
391
+ "panelSubtitle": "Yvqh9W_panelSubtitle",
392
+ "promptFooter": "Yvqh9W_promptFooter",
393
+ "canvasMeta": "Yvqh9W_canvasMeta",
394
+ "historyTitle": "Yvqh9W_historyTitle",
395
+ "canvasEmptyIcon": "Yvqh9W_canvasEmptyIcon",
396
+ "panelTitle": "Yvqh9W_panelTitle",
397
+ "paramLabel": "Yvqh9W_paramLabel",
398
+ "lightboxIndex": "Yvqh9W_lightboxIndex",
399
+ "historyPrompt": "Yvqh9W_historyPrompt",
400
+ "imageCaption": "Yvqh9W_imageCaption",
401
+ "canvasStateTitle": "Yvqh9W_canvasStateTitle",
402
+ "config": "Yvqh9W_config",
403
+ "uploadHint": "Yvqh9W_uploadHint",
404
+ "historyMain": "Yvqh9W_historyMain",
405
+ "historyClear": "Yvqh9W_historyClear",
406
+ "modelSelect": "Yvqh9W_modelSelect",
407
+ "paramGroup": "Yvqh9W_paramGroup",
408
+ "generateButton": "Yvqh9W_generateButton",
409
+ "historyItem": "Yvqh9W_historyItem",
410
+ "modelLabel": "Yvqh9W_modelLabel",
411
+ "historyMeta": "Yvqh9W_historyMeta",
412
+ "bigSpinner": "Yvqh9W_bigSpinner",
413
+ "modePill": "Yvqh9W_modePill",
414
+ "history": "Yvqh9W_history",
415
+ "hiddenFile": "Yvqh9W_hiddenFile",
416
+ "lightboxCaption": "Yvqh9W_lightboxCaption",
417
+ "generateInner": "Yvqh9W_generateInner",
418
+ "imageCard": "Yvqh9W_imageCard",
419
+ "view": "Yvqh9W_view",
420
+ "panel": "Yvqh9W_panel",
421
+ "historyEmpty": "Yvqh9W_historyEmpty",
422
+ "spinner": "Yvqh9W_spinner",
423
+ "paramHint": "Yvqh9W_paramHint",
424
+ "lightboxNav": "Yvqh9W_lightboxNav",
425
+ "canvasStateHint": "Yvqh9W_canvasStateHint",
426
+ "canvas": "Yvqh9W_canvas",
427
+ "uploadBox": "Yvqh9W_uploadBox",
428
+ "entry": "Yvqh9W_entry",
429
+ "canvasBody": "Yvqh9W_canvasBody",
430
+ "footer": "Yvqh9W_footer",
431
+ "entryIcon": "Yvqh9W_entryIcon",
432
+ "historyList": "Yvqh9W_historyList",
433
+ "lightboxClose": "Yvqh9W_lightboxClose",
434
+ "lightboxMeta": "Yvqh9W_lightboxMeta",
435
+ "uploadIcon": "Yvqh9W_uploadIcon",
436
+ "historyActions": "Yvqh9W_historyActions"
437
+ };
438
+ //#endregion
439
+ //#region src/client/ImageGenPanel.tsx
440
+ /**
441
+ * The AI 生图 studio: a three-column layout — left, a card-grouped
442
+ * configuration sidebar (mode tabs, prompt with counter, rounded parameter
443
+ * selectors, model dropdown + generate button); center, the result canvas;
444
+ * right, a persistent generation history column.
445
+ *
446
+ * Controls ride the system UI primitives (@deepseek-ai/dsh-client-ui-primitives,
447
+ * a platform module) so the studio matches the dsh shell look by construction.
448
+ */
449
+ /** The model dropdown offers exactly the plugin's namesake model. */
450
+ const MODELS = ["gpt-image-2"];
451
+ /** All size options for gpt-image-2. */
452
+ const SIZES = [
453
+ "auto",
454
+ "1024x1024",
455
+ "1536x1024",
456
+ "1024x1536",
457
+ "512x512",
458
+ "1792x1024",
459
+ "1024x1792"
460
+ ];
461
+ /** Size option keys in the locale dictionary. */
462
+ const SIZE_KEYS = {
463
+ auto: "size.auto",
464
+ "1024x1024": "size.square",
465
+ "1536x1024": "size.landscape",
466
+ "1024x1536": "size.portrait",
467
+ "512x512": "size.small",
468
+ "1792x1024": "size.wide",
469
+ "1024x1792": "size.tall"
470
+ };
471
+ /** Quality options. */
472
+ const QUALITIES = [
473
+ "auto",
474
+ "low",
475
+ "medium",
476
+ "high"
477
+ ];
478
+ /** Detail options ('' = omit the passthrough). */
479
+ const DETAILS = [
480
+ "",
481
+ "standard",
482
+ "high"
483
+ ];
484
+ const PROMPT_MAX = 2e3;
485
+ const REF_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
486
+ /** Read the current config from the settings scope snapshot. */
487
+ function useConfig(scope) {
488
+ const [value, setValue] = (0, react.useState)(scope.getSnapshot().value);
489
+ (0, react.useEffect)(() => scope.subscribe(() => {
490
+ setValue(scope.getSnapshot().value);
491
+ }), [scope]);
492
+ return value;
493
+ }
494
+ /** Tick a seconds counter while `running`. */
495
+ function useElapsed(running, startedAt) {
496
+ const [elapsed, setElapsed] = (0, react.useState)(0);
497
+ (0, react.useEffect)(() => {
498
+ if (!running || startedAt === null) return;
499
+ const timer = window.setInterval(() => {
500
+ setElapsed(Math.max(1, Math.round((Date.now() - startedAt) / 1e3)));
501
+ }, 1e3);
502
+ return () => window.clearInterval(timer);
503
+ }, [running, startedAt]);
504
+ return elapsed;
505
+ }
506
+ /** Data URL for a generated image. */
507
+ function srcOf(image) {
508
+ return `data:${image.mime};base64,${image.b64}`;
509
+ }
510
+ /** Fetch persisted history image refs and decode them back to in-memory
511
+ * GeneratedImage[] (base64), so the canvas/preview can reuse the same
512
+ * rendering path as a fresh generation. */
513
+ async function historyImagesToGenerated(refs) {
514
+ return Promise.all(refs.map(async (ref) => {
515
+ const response = await fetch(ref.url);
516
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
517
+ const blob = await response.blob();
518
+ const dataUrl = await new Promise((resolve, reject) => {
519
+ const reader = new FileReader();
520
+ reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : "");
521
+ reader.onerror = () => reject(/* @__PURE__ */ new Error("image read failed"));
522
+ reader.readAsDataURL(blob);
523
+ });
524
+ const comma = dataUrl.indexOf(",");
525
+ return {
526
+ b64: comma >= 0 ? dataUrl.slice(comma + 1) : "",
527
+ mime: ref.mime,
528
+ ...ref.revisedPrompt === void 0 ? {} : { revisedPrompt: ref.revisedPrompt }
529
+ };
530
+ }));
531
+ }
532
+ /** Compact, locale-independent timestamp for history entries. */
533
+ function formatTime(timestamp) {
534
+ const d = new Date(timestamp);
535
+ const pad = (n) => String(n).padStart(2, "0");
536
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
537
+ }
538
+ /** Render the studio. */
539
+ function ImageGenPanel(props) {
540
+ const { api, scope } = props;
541
+ const config = useConfig(scope);
542
+ const enabled = config?.enabled ?? true;
543
+ const apiUrl = config?.apiUrl ?? "";
544
+ const configured = apiUrl.trim() !== "";
545
+ const [mode, setMode] = (0, react.useState)("text");
546
+ const [prompt, setPrompt] = (0, react.useState)("");
547
+ const [size, setSize] = (0, react.useState)("auto");
548
+ const [quality, setQuality] = (0, react.useState)("auto");
549
+ const [count, setCount] = (0, react.useState)(1);
550
+ const [detail, setDetail] = (0, react.useState)("");
551
+ const [model, setModel] = (0, react.useState)(MODELS[0]);
552
+ const [refImage, setRefImage] = (0, react.useState)(null);
553
+ const [images, setImages] = (0, react.useState)([]);
554
+ const [error, setError] = (0, react.useState)(null);
555
+ const [generating, setGenerating] = (0, react.useState)(false);
556
+ const [startedAt, setStartedAt] = (0, react.useState)(null);
557
+ const [history, setHistory] = (0, react.useState)([]);
558
+ const [viewingHistoryId, setViewingHistoryId] = (0, react.useState)(null);
559
+ const [preview, setPreview] = (0, react.useState)(null);
560
+ const fileInput = (0, react.useRef)(null);
561
+ const elapsed = useElapsed(generating, startedAt);
562
+ (0, react.useEffect)(() => {
563
+ let disposed = false;
564
+ api.historyList().then((entries) => {
565
+ if (!disposed) setHistory(entries);
566
+ }).catch(() => {});
567
+ return () => {
568
+ disposed = true;
569
+ };
570
+ }, [api]);
571
+ /** Read an uploaded reference image into a data URL. */
572
+ const acceptFile = (file) => {
573
+ if (file === void 0) return;
574
+ if (!file.type.startsWith("image/")) {
575
+ setError(tt("edit.uploadHint"));
576
+ return;
577
+ }
578
+ if (file.size > REF_IMAGE_MAX_BYTES) {
579
+ setError(tt("edit.uploadHint"));
580
+ return;
581
+ }
582
+ const reader = new FileReader();
583
+ reader.onload = () => {
584
+ if (typeof reader.result === "string") setRefImage({
585
+ dataUrl: reader.result,
586
+ name: file.name
587
+ });
588
+ };
589
+ reader.onerror = () => {
590
+ setError(tt("edit.uploadHint"));
591
+ };
592
+ reader.readAsDataURL(file);
593
+ };
594
+ /** Run one generation. */
595
+ const handleGenerate = async () => {
596
+ if (generating) return;
597
+ const promptText = prompt.trim();
598
+ if (promptText === "") {
599
+ setError(tt("prompt.required"));
600
+ return;
601
+ }
602
+ if (mode === "edit" && refImage === null) {
603
+ setError(tt("edit.required"));
604
+ return;
605
+ }
606
+ const request = {
607
+ mode,
608
+ model,
609
+ prompt: promptText,
610
+ size,
611
+ quality,
612
+ n: count,
613
+ detail,
614
+ ...mode === "edit" && refImage !== null ? { image: refImage.dataUrl } : {}
615
+ };
616
+ setGenerating(true);
617
+ setError(null);
618
+ setImages([]);
619
+ setStartedAt(Date.now());
620
+ try {
621
+ const result = await api.generate(request);
622
+ setImages(result.images);
623
+ setViewingHistoryId(null);
624
+ const entry = {
625
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
626
+ createdAt: Date.now(),
627
+ mode,
628
+ model,
629
+ prompt: promptText,
630
+ size,
631
+ quality,
632
+ detail,
633
+ n: count,
634
+ images: result.images,
635
+ ...mode === "edit" && refImage !== null ? { refName: refImage.name } : {}
636
+ };
637
+ try {
638
+ setHistory(await api.historyAppend(entry));
639
+ } catch {}
640
+ } catch (caught) {
641
+ setError(errorMessage(caught));
642
+ } finally {
643
+ setGenerating(false);
644
+ setStartedAt(null);
645
+ }
646
+ };
647
+ /** Open the full-screen image preview at a given index. */
648
+ const openPreview = (previewImages, index) => {
649
+ setPreview({
650
+ images: previewImages,
651
+ index
652
+ });
653
+ };
654
+ /** Step the preview by ±1, wrapping around. */
655
+ const stepPreview = (delta) => {
656
+ setPreview((current) => {
657
+ if (current === null) return null;
658
+ const total = current.images.length;
659
+ return {
660
+ images: current.images,
661
+ index: (current.index + delta + total) % total
662
+ };
663
+ });
664
+ };
665
+ (0, react.useEffect)(() => {
666
+ if (preview === null) return;
667
+ const onKey = (event) => {
668
+ if (event.key === "Escape") setPreview(null);
669
+ else if (event.key === "ArrowLeft") stepPreview(-1);
670
+ else if (event.key === "ArrowRight") stepPreview(1);
671
+ };
672
+ window.addEventListener("keydown", onKey);
673
+ return () => window.removeEventListener("keydown", onKey);
674
+ }, [preview]);
675
+ /** Load a past generation's images into the canvas. */
676
+ const viewHistoryEntry = async (entry) => {
677
+ try {
678
+ setImages(await historyImagesToGenerated(entry.images));
679
+ setError(null);
680
+ setViewingHistoryId(entry.id);
681
+ } catch (caught) {
682
+ setError(errorMessage(caught));
683
+ }
684
+ };
685
+ /** Restore a past generation's parameters (and its images) into the form. */
686
+ const restoreHistoryEntry = async (entry) => {
687
+ try {
688
+ const restored = await historyImagesToGenerated(entry.images);
689
+ setMode(entry.mode);
690
+ setPrompt(entry.prompt);
691
+ setSize(SIZES.includes(entry.size) ? entry.size : "auto");
692
+ setQuality(QUALITIES.includes(entry.quality) ? entry.quality : "auto");
693
+ setDetail(DETAILS.includes(entry.detail) ? entry.detail : "");
694
+ setCount(entry.n >= 1 && entry.n <= 4 ? entry.n : 1);
695
+ setModel(MODELS.includes(entry.model) ? entry.model : MODELS[0]);
696
+ setRefImage(null);
697
+ setImages(restored);
698
+ setError(null);
699
+ setViewingHistoryId(entry.id);
700
+ } catch (caught) {
701
+ setError(errorMessage(caught));
702
+ }
703
+ };
704
+ /** Remove one history entry. */
705
+ const deleteHistoryEntry = async (id) => {
706
+ setHistory(history.filter((entry) => entry.id !== id));
707
+ if (viewingHistoryId === id) setViewingHistoryId(null);
708
+ try {
709
+ setHistory(await api.historyRemove(id));
710
+ } catch {}
711
+ };
712
+ /** Remove all history entries. */
713
+ const clearHistory = async () => {
714
+ setHistory([]);
715
+ setViewingHistoryId(null);
716
+ try {
717
+ setHistory(await api.historyClear());
718
+ } catch {}
719
+ };
720
+ const generateDisabled = generating || !enabled || !configured;
721
+ const viewingEntry = viewingHistoryId === null ? null : history.find((entry) => entry.id === viewingHistoryId) ?? null;
722
+ const previewImage = preview === null ? null : preview.images[preview.index] ?? null;
723
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
724
+ className: panel_module_css_default.panel,
725
+ children: [
726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
727
+ className: panel_module_css_default.panelHeader,
728
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
729
+ className: panel_module_css_default.panelTitle,
730
+ children: tt("panel.title")
731
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
732
+ className: panel_module_css_default.panelSubtitle,
733
+ children: tt("panel.subtitle")
734
+ })]
735
+ }),
736
+ !enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
737
+ className: panel_module_css_default.banner,
738
+ "data-kind": "warn",
739
+ children: tt("config.disabled")
740
+ }) : !configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
741
+ className: panel_module_css_default.banner,
742
+ "data-kind": "warn",
743
+ children: tt("config.missing")
744
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
745
+ className: panel_module_css_default.banner,
746
+ "data-kind": "ok",
747
+ children: tt("config.configured", { url: apiUrl })
748
+ }),
749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
750
+ className: panel_module_css_default.studio,
751
+ children: [
752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
753
+ className: panel_module_css_default.config,
754
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
755
+ className: panel_module_css_default.configScroll,
756
+ children: [
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
758
+ className: panel_module_css_default.card,
759
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
760
+ className: panel_module_css_default.modeRow,
761
+ role: "tablist",
762
+ "aria-label": tt("panel.title"),
763
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
764
+ active: mode === "text",
765
+ onClick: () => {
766
+ setMode("text");
767
+ },
768
+ className: panel_module_css_default.modePill,
769
+ children: tt("mode.text")
770
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
771
+ active: mode === "edit",
772
+ onClick: () => {
773
+ setMode("edit");
774
+ },
775
+ className: panel_module_css_default.modePill,
776
+ children: tt("mode.edit")
777
+ })]
778
+ })
779
+ }),
780
+ mode === "edit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
781
+ className: panel_module_css_default.card,
782
+ children: [refImage === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
783
+ type: "button",
784
+ className: panel_module_css_default.uploadBox,
785
+ onClick: () => {
786
+ fileInput.current?.click();
787
+ },
788
+ onDragOver: (event) => {
789
+ event.preventDefault();
790
+ },
791
+ onDrop: (event) => {
792
+ event.preventDefault();
793
+ acceptFile(event.dataTransfer.files?.[0]);
794
+ },
795
+ children: [
796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
797
+ className: panel_module_css_default.uploadIcon,
798
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
799
+ viewBox: "0 0 16 16",
800
+ width: "18",
801
+ height: "18",
802
+ fill: "none",
803
+ stroke: "currentColor",
804
+ strokeWidth: "1.3",
805
+ strokeLinecap: "round",
806
+ strokeLinejoin: "round",
807
+ "aria-hidden": "true",
808
+ children: [
809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 10.5V3" }),
810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 5.5l3-3 3 3" }),
811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 9v3.5h11V9" })
812
+ ]
813
+ })
814
+ }),
815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt("edit.upload") }),
816
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
817
+ className: panel_module_css_default.uploadHint,
818
+ children: tt("edit.uploadHint")
819
+ })
820
+ ]
821
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
822
+ className: panel_module_css_default.reference,
823
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
824
+ className: panel_module_css_default.referenceImage,
825
+ src: refImage.dataUrl,
826
+ alt: refImage.name
827
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
828
+ className: panel_module_css_default.referenceActions,
829
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
830
+ variant: "outline",
831
+ size: "sm",
832
+ onClick: () => {
833
+ fileInput.current?.click();
834
+ },
835
+ children: tt("edit.change")
836
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
837
+ variant: "outline",
838
+ size: "sm",
839
+ onClick: () => {
840
+ setRefImage(null);
841
+ },
842
+ children: tt("edit.remove")
843
+ })]
844
+ })]
845
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
846
+ ref: fileInput,
847
+ type: "file",
848
+ accept: "image/png,image/jpeg,image/webp,image/gif",
849
+ className: panel_module_css_default.hiddenFile,
850
+ onChange: (event) => {
851
+ acceptFile(event.target.files?.[0]);
852
+ event.target.value = "";
853
+ }
854
+ })]
855
+ }) : null,
856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
857
+ className: panel_module_css_default.card,
858
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
859
+ className: panel_module_css_default.prompt,
860
+ value: prompt,
861
+ maxLength: PROMPT_MAX,
862
+ placeholder: tt("prompt.placeholder"),
863
+ onChange: (event) => {
864
+ setPrompt(event.target.value);
865
+ }
866
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
867
+ className: panel_module_css_default.promptFooter,
868
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
869
+ className: panel_module_css_default.promptCount,
870
+ children: tt("prompt.count", { count: prompt.length })
871
+ })
872
+ })]
873
+ }),
874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
875
+ className: panel_module_css_default.card,
876
+ children: [
877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
878
+ className: panel_module_css_default.paramGroup,
879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
880
+ className: panel_module_css_default.paramLabel,
881
+ children: tt("params.size")
882
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
883
+ className: panel_module_css_default.optionGrid,
884
+ children: SIZES.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
885
+ active: size === option,
886
+ onClick: () => {
887
+ setSize(option);
888
+ },
889
+ className: panel_module_css_default.optionPill,
890
+ children: tt(SIZE_KEYS[option] ?? "size.auto")
891
+ }, option))
892
+ })]
893
+ }),
894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
895
+ className: panel_module_css_default.paramGroup,
896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
897
+ className: panel_module_css_default.paramLabel,
898
+ children: tt("params.quality")
899
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
900
+ className: panel_module_css_default.optionRow,
901
+ children: QUALITIES.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
902
+ active: quality === option,
903
+ onClick: () => {
904
+ setQuality(option);
905
+ },
906
+ className: panel_module_css_default.optionPill,
907
+ children: tt(`quality.${option}`)
908
+ }, option))
909
+ })]
910
+ }),
911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
912
+ className: panel_module_css_default.paramGroup,
913
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
914
+ className: panel_module_css_default.paramLabel,
915
+ children: tt("params.count")
916
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
917
+ className: panel_module_css_default.optionRow,
918
+ children: [
919
+ 1,
920
+ 2,
921
+ 3,
922
+ 4
923
+ ].map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
924
+ active: count === option,
925
+ onClick: () => {
926
+ setCount(option);
927
+ },
928
+ className: panel_module_css_default.optionPill,
929
+ children: tt(`count.${option === 1 ? "one" : option === 2 ? "two" : option === 3 ? "three" : "four"}`)
930
+ }, option))
931
+ })]
932
+ }),
933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
934
+ className: panel_module_css_default.paramGroup,
935
+ children: [
936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
937
+ className: panel_module_css_default.paramLabel,
938
+ children: tt("params.detail")
939
+ }),
940
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
941
+ className: panel_module_css_default.optionRow,
942
+ children: DETAILS.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
943
+ active: detail === option,
944
+ onClick: () => {
945
+ setDetail(option);
946
+ },
947
+ className: panel_module_css_default.optionPill,
948
+ children: tt(option === "" ? "detail.auto" : option === "standard" ? "detail.standard" : "detail.high")
949
+ }, option === "" ? "auto" : option))
950
+ }),
951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
952
+ className: panel_module_css_default.paramHint,
953
+ children: tt("detail.hint")
954
+ })
955
+ ]
956
+ })
957
+ ]
958
+ })
959
+ ]
960
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
961
+ className: panel_module_css_default.footer,
962
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
963
+ className: panel_module_css_default.modelWrap,
964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
965
+ className: panel_module_css_default.modelLabel,
966
+ children: tt("model.label")
967
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
968
+ className: panel_module_css_default.modelSelect,
969
+ value: model,
970
+ disabled: generating,
971
+ onChange: (event) => {
972
+ setModel(event.target.value);
973
+ },
974
+ children: MODELS.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
975
+ value: option,
976
+ children: option
977
+ }, option))
978
+ })]
979
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
980
+ variant: "primary",
981
+ size: "md",
982
+ className: panel_module_css_default.generateButton,
983
+ disabled: generateDisabled,
984
+ onClick: () => {
985
+ handleGenerate();
986
+ },
987
+ children: generating ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
988
+ className: panel_module_css_default.generateInner,
989
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.spinner }), tt("generating")]
990
+ }) : tt("generate")
991
+ })]
992
+ })]
993
+ }),
994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
995
+ className: panel_module_css_default.canvas,
996
+ children: [
997
+ generating ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
998
+ className: panel_module_css_default.canvasState,
999
+ role: "status",
1000
+ children: [
1001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.bigSpinner }),
1002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1003
+ className: panel_module_css_default.canvasStateTitle,
1004
+ children: tt("canvas.generating")
1005
+ }),
1006
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1007
+ className: panel_module_css_default.canvasStateHint,
1008
+ children: tt("canvas.elapsed", { seconds: elapsed })
1009
+ })
1010
+ ]
1011
+ }) : null,
1012
+ !generating && error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1013
+ className: panel_module_css_default.canvasError,
1014
+ role: "alert",
1015
+ children: tt("canvas.error", { error })
1016
+ }) : null,
1017
+ !generating && !error && images.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1018
+ className: panel_module_css_default.canvasState,
1019
+ children: [
1020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1021
+ className: panel_module_css_default.canvasEmptyIcon,
1022
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1023
+ viewBox: "0 0 24 24",
1024
+ width: "34",
1025
+ height: "34",
1026
+ fill: "none",
1027
+ stroke: "currentColor",
1028
+ strokeWidth: "1.2",
1029
+ strokeLinecap: "round",
1030
+ strokeLinejoin: "round",
1031
+ "aria-hidden": "true",
1032
+ children: [
1033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1034
+ x: "3",
1035
+ y: "3",
1036
+ width: "18",
1037
+ height: "18",
1038
+ rx: "3"
1039
+ }),
1040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1041
+ cx: "8.5",
1042
+ cy: "8.5",
1043
+ r: "1.5"
1044
+ }),
1045
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 15l-5-5L5 21" })
1046
+ ]
1047
+ })
1048
+ }),
1049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1050
+ className: panel_module_css_default.canvasStateTitle,
1051
+ children: tt("canvas.emptyTitle")
1052
+ }),
1053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1054
+ className: panel_module_css_default.canvasStateHint,
1055
+ children: tt("canvas.emptyHint")
1056
+ })
1057
+ ]
1058
+ }) : null,
1059
+ !generating && images.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1060
+ className: panel_module_css_default.canvasBody,
1061
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1062
+ className: panel_module_css_default.canvasMeta,
1063
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt("canvas.images", { count: images.length }) }), viewingEntry !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1064
+ className: panel_module_css_default.canvasHistoryTag,
1065
+ children: tt("history.viewing", { time: formatTime(viewingEntry.createdAt) })
1066
+ }) : null]
1067
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1068
+ className: panel_module_css_default.grid,
1069
+ children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
1070
+ className: panel_module_css_default.imageCard,
1071
+ role: "button",
1072
+ tabIndex: 0,
1073
+ title: tt("preview.open"),
1074
+ onClick: () => {
1075
+ openPreview(images, index);
1076
+ },
1077
+ onKeyDown: (event) => {
1078
+ if (event.key === "Enter" || event.key === " ") {
1079
+ event.preventDefault();
1080
+ openPreview(images, index);
1081
+ }
1082
+ },
1083
+ children: [
1084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1085
+ className: panel_module_css_default.image,
1086
+ src: srcOf(image),
1087
+ alt: image.revisedPrompt ?? `${tt("panel.title")} ${index + 1}`
1088
+ }),
1089
+ image.revisedPrompt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
1090
+ className: panel_module_css_default.imageCaption,
1091
+ title: image.revisedPrompt,
1092
+ children: tt("revisedPrompt", { prompt: image.revisedPrompt })
1093
+ }) : null,
1094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1095
+ className: panel_module_css_default.zoomHint,
1096
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1097
+ viewBox: "0 0 16 16",
1098
+ width: "13",
1099
+ height: "13",
1100
+ fill: "none",
1101
+ stroke: "currentColor",
1102
+ strokeWidth: "1.4",
1103
+ strokeLinecap: "round",
1104
+ strokeLinejoin: "round",
1105
+ "aria-hidden": "true",
1106
+ children: [
1107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1108
+ cx: "7",
1109
+ cy: "7",
1110
+ r: "4"
1111
+ }),
1112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13 13l-3.2-3.2" }),
1113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7 5.4v3.2M5.4 7h3.2" })
1114
+ ]
1115
+ }), tt("preview.open")]
1116
+ }),
1117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1118
+ className: panel_module_css_default.download,
1119
+ href: srcOf(image),
1120
+ download: `dsh-image-${index + 1}.${extensionOf(image.mime)}`,
1121
+ onClick: (event) => {
1122
+ event.stopPropagation();
1123
+ },
1124
+ children: tt("download")
1125
+ })
1126
+ ]
1127
+ }, index))
1128
+ })]
1129
+ }) : null
1130
+ ]
1131
+ }),
1132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
1133
+ className: panel_module_css_default.history,
1134
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1135
+ className: panel_module_css_default.historyHeader,
1136
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1137
+ className: panel_module_css_default.historyTitle,
1138
+ children: tt("history.title")
1139
+ }), history.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1140
+ type: "button",
1141
+ className: panel_module_css_default.historyClear,
1142
+ onClick: () => {
1143
+ clearHistory();
1144
+ },
1145
+ children: tt("history.clear")
1146
+ }) : null]
1147
+ }), history.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1148
+ className: panel_module_css_default.historyEmpty,
1149
+ children: tt("history.empty")
1150
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1151
+ className: panel_module_css_default.historyList,
1152
+ children: history.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1153
+ className: panel_module_css_default.historyItem,
1154
+ "data-active": entry.id === viewingHistoryId ? "" : void 0,
1155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1156
+ type: "button",
1157
+ className: panel_module_css_default.historyMain,
1158
+ onClick: () => {
1159
+ viewHistoryEntry(entry);
1160
+ },
1161
+ children: [entry.images.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1162
+ className: panel_module_css_default.historyThumb,
1163
+ src: entry.images[0].url,
1164
+ alt: ""
1165
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.historyThumbPlaceholder }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1166
+ className: panel_module_css_default.historyInfo,
1167
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1168
+ className: panel_module_css_default.historyPrompt,
1169
+ children: entry.prompt
1170
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1171
+ className: panel_module_css_default.historyMeta,
1172
+ children: [
1173
+ tt(`mode.${entry.mode === "edit" ? "edit" : "text"}`),
1174
+ " · ",
1175
+ formatTime(entry.createdAt),
1176
+ " · ",
1177
+ entry.images.length,
1178
+ " ",
1179
+ tt("history.images")
1180
+ ]
1181
+ })]
1182
+ })]
1183
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1184
+ className: panel_module_css_default.historyActions,
1185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1186
+ type: "button",
1187
+ className: panel_module_css_default.historyAction,
1188
+ onClick: () => {
1189
+ restoreHistoryEntry(entry);
1190
+ },
1191
+ children: tt("history.restore")
1192
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1193
+ type: "button",
1194
+ className: panel_module_css_default.historyAction,
1195
+ "data-danger": true,
1196
+ onClick: () => {
1197
+ deleteHistoryEntry(entry.id);
1198
+ },
1199
+ children: tt("history.delete")
1200
+ })]
1201
+ })]
1202
+ }, entry.id))
1203
+ })]
1204
+ })
1205
+ ]
1206
+ }),
1207
+ preview !== null && previewImage !== null ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1208
+ className: panel_module_css_default.lightbox,
1209
+ role: "dialog",
1210
+ "aria-modal": "true",
1211
+ "aria-label": tt("preview.title"),
1212
+ onClick: () => {
1213
+ setPreview(null);
1214
+ },
1215
+ children: [
1216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1217
+ type: "button",
1218
+ className: panel_module_css_default.lightboxClose,
1219
+ "aria-label": tt("preview.close"),
1220
+ onClick: () => {
1221
+ setPreview(null);
1222
+ },
1223
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1224
+ viewBox: "0 0 16 16",
1225
+ width: "18",
1226
+ height: "18",
1227
+ fill: "none",
1228
+ stroke: "currentColor",
1229
+ strokeWidth: "1.6",
1230
+ strokeLinecap: "round",
1231
+ "aria-hidden": "true",
1232
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
1233
+ })
1234
+ }),
1235
+ preview.images.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1236
+ type: "button",
1237
+ className: panel_module_css_default.lightboxNav,
1238
+ "data-dir": "prev",
1239
+ "aria-label": tt("preview.prev"),
1240
+ onClick: (event) => {
1241
+ event.stopPropagation();
1242
+ stepPreview(-1);
1243
+ },
1244
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1245
+ viewBox: "0 0 16 16",
1246
+ width: "20",
1247
+ height: "20",
1248
+ fill: "none",
1249
+ stroke: "currentColor",
1250
+ strokeWidth: "1.8",
1251
+ strokeLinecap: "round",
1252
+ strokeLinejoin: "round",
1253
+ "aria-hidden": "true",
1254
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 3l-5 5 5 5" })
1255
+ })
1256
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1257
+ type: "button",
1258
+ className: panel_module_css_default.lightboxNav,
1259
+ "data-dir": "next",
1260
+ "aria-label": tt("preview.next"),
1261
+ onClick: (event) => {
1262
+ event.stopPropagation();
1263
+ stepPreview(1);
1264
+ },
1265
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1266
+ viewBox: "0 0 16 16",
1267
+ width: "20",
1268
+ height: "20",
1269
+ fill: "none",
1270
+ stroke: "currentColor",
1271
+ strokeWidth: "1.8",
1272
+ strokeLinecap: "round",
1273
+ strokeLinejoin: "round",
1274
+ "aria-hidden": "true",
1275
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 3l5 5-5 5" })
1276
+ })
1277
+ })] }) : null,
1278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
1279
+ className: panel_module_css_default.lightboxFigure,
1280
+ onClick: (event) => {
1281
+ event.stopPropagation();
1282
+ },
1283
+ children: [
1284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1285
+ className: panel_module_css_default.lightboxImage,
1286
+ src: srcOf(previewImage),
1287
+ alt: previewImage.revisedPrompt ?? tt("preview.title")
1288
+ }),
1289
+ previewImage.revisedPrompt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
1290
+ className: panel_module_css_default.lightboxCaption,
1291
+ title: previewImage.revisedPrompt,
1292
+ children: tt("revisedPrompt", { prompt: previewImage.revisedPrompt })
1293
+ }) : null,
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1295
+ className: panel_module_css_default.lightboxMeta,
1296
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1297
+ className: panel_module_css_default.lightboxIndex,
1298
+ children: tt("preview.index", {
1299
+ index: preview.index + 1,
1300
+ total: preview.images.length
1301
+ })
1302
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1303
+ className: panel_module_css_default.lightboxDownload,
1304
+ href: srcOf(previewImage),
1305
+ download: `dsh-image-${preview.index + 1}.${extensionOf(previewImage.mime)}`,
1306
+ children: tt("download")
1307
+ })]
1308
+ })
1309
+ ]
1310
+ })
1311
+ ]
1312
+ }), document.body) : null
1313
+ ]
1314
+ });
1315
+ }
1316
+ /** File extension for a MIME type (download filenames). */
1317
+ function extensionOf(mime) {
1318
+ switch (mime.split(";")[0].trim()) {
1319
+ case "image/jpeg": return "jpg";
1320
+ case "image/webp": return "webp";
1321
+ case "image/gif": return "gif";
1322
+ default: return "png";
1323
+ }
1324
+ }
1325
+ //#endregion
1326
+ //#region src/client/mount.tsx
1327
+ /**
1328
+ * Panel view mounting.
1329
+ *
1330
+ * The `conversation` slot is single-occupant (ui-conversation) and external
1331
+ * plugins cannot declare slots, so the panel takes over the center column at
1332
+ * the DOM level: a container is appended inside the `[data-pane="conversation"]`
1333
+ * grid item (an extra trailing child React never manages), and a stylesheet
1334
+ * rule hides the conversation content while the panel is active. Toggling is
1335
+ * a data attribute on <html> — no React involvement, so the conversation
1336
+ * subtree underneath stays mounted and stateful.
1337
+ */
1338
+ const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"]";
1339
+ const ACTIVE_ATTR = "data-dsh-imagegen-active";
1340
+ /** Sibling panels' activation attributes, removed when this panel opens. */
1341
+ const OTHER_ACTIVE_ATTRS = ["data-dsh-taskboard-active", "data-dsh-ssh-active"];
1342
+ /** Cross-plugin activation event; detail is the activating panel name. */
1343
+ const ACTIVATE_EVENT = "dsh-panel-activate";
1344
+ const PANEL_NAME = "imagegen";
1345
+ /** Find the center column, or undefined while the frame is not mounted. */
1346
+ function conversationColumn() {
1347
+ return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
1348
+ }
1349
+ /**
1350
+ * Mount the panel React tree into the center column and bind its visibility
1351
+ * to the controller's panelOpen state.
1352
+ * @param controller - the panel controller driving the view.
1353
+ * @param api - the image-generation API client the panel operates through.
1354
+ * @param scope - the settings scope (config status banner).
1355
+ * @returns disposer unmounting the tree and restoring the column.
1356
+ */
1357
+ function mountPanel(controller, api, scope) {
1358
+ let root;
1359
+ let container;
1360
+ const ensure = () => {
1361
+ if (container !== void 0) {
1362
+ if (container.isConnected) return;
1363
+ root?.unmount();
1364
+ root = void 0;
1365
+ container.remove();
1366
+ container = void 0;
1367
+ }
1368
+ const column = conversationColumn();
1369
+ if (column === void 0) return;
1370
+ container = document.createElement("div");
1371
+ container.dataset.dshImagegenView = "";
1372
+ container.className = panel_module_css_default.view;
1373
+ column.appendChild(container);
1374
+ root = (0, react_dom_client.createRoot)(container);
1375
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageGenPanel, {
1376
+ api,
1377
+ scope
1378
+ }));
1379
+ };
1380
+ const waitObserver = new MutationObserver(() => {
1381
+ ensure();
1382
+ });
1383
+ waitObserver.observe(document.body, {
1384
+ childList: true,
1385
+ subtree: true
1386
+ });
1387
+ const applyActive = () => {
1388
+ if (controller.getSnapshot().panelOpen) {
1389
+ for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr);
1390
+ document.documentElement.setAttribute(ACTIVE_ATTR, "");
1391
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }));
1392
+ } else document.documentElement.removeAttribute(ACTIVE_ATTR);
1393
+ };
1394
+ const onOtherActivate = (event) => {
1395
+ const detail = event.detail;
1396
+ if ((detail === "ssh" || detail === "taskboard") && controller.getSnapshot().panelOpen) controller.close();
1397
+ };
1398
+ const SIDEBAR_ROW_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
1399
+ const onClickSidebarRow = (event) => {
1400
+ if (!controller.getSnapshot().panelOpen) return;
1401
+ const target = event.target;
1402
+ if (target === null) return;
1403
+ if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close();
1404
+ };
1405
+ document.addEventListener("click", onClickSidebarRow, true);
1406
+ document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
1407
+ const unsubscribe = controller.subscribe(applyActive);
1408
+ applyActive();
1409
+ ensure();
1410
+ return () => {
1411
+ document.removeEventListener("click", onClickSidebarRow, true);
1412
+ document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
1413
+ waitObserver.disconnect();
1414
+ unsubscribe();
1415
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
1416
+ root?.unmount();
1417
+ root = void 0;
1418
+ container?.remove();
1419
+ container = void 0;
1420
+ };
1421
+ }
1422
+ //#endregion
1423
+ //#region src/client/sidebar-entry.ts
1424
+ /** Family entry selectors of sibling plugins (relative placement anchor). */
1425
+ const FAMILY_ENTRY_SELECTOR = "[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-imagegen-entry]";
1426
+ /** Find the sidebar shell root element, or undefined while not yet mounted. */
1427
+ function sidebarRoot() {
1428
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
1429
+ if (column === null) return void 0;
1430
+ return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
1431
+ }
1432
+ /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
1433
+ function newSessionButton(root) {
1434
+ const nested = root.querySelector("button[class*=\"newSession\"]");
1435
+ if (nested !== null) return nested;
1436
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
1437
+ }
1438
+ /** Build the entry row (a detached button; insert once the shell is up). */
1439
+ function createEntry(controller, label, tooltip) {
1440
+ const entry = document.createElement("button");
1441
+ entry.type = "button";
1442
+ entry.dataset.dshImagegenEntry = "";
1443
+ entry.className = panel_module_css_default.entry;
1444
+ entry.setAttribute("aria-label", label);
1445
+ entry.setAttribute("title", tooltip);
1446
+ entry.innerHTML = "<span class=\"" + panel_module_css_default.entryIcon + "\"><svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2.5\" width=\"12\" height=\"11\" rx=\"1.5\"/><circle cx=\"5.6\" cy=\"5.8\" r=\"1\"/><path d=\"M2.5 12.5l3.6-3.4 2.4 2.2 3-3 2 2.4\"/></svg></span><span class=\"" + panel_module_css_default.entryLabel + "\">" + label + "</span>";
1447
+ entry.addEventListener("click", () => {
1448
+ controller.toggle();
1449
+ });
1450
+ return entry;
1451
+ }
1452
+ /** Re-insert the entry after the family block (task board → ssh → imagegen). */
1453
+ function placeEntry(root, entry) {
1454
+ const button = newSessionButton(root);
1455
+ if (button === void 0) return false;
1456
+ if (entry.parentElement !== root) {
1457
+ const row = button.closest("[class*=\"logoRow\"]");
1458
+ const base = row !== null && row.parentElement === root ? row : button;
1459
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(FAMILY_ENTRY_SELECTOR));
1460
+ const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
1461
+ root.insertBefore(entry, anchor);
1462
+ }
1463
+ return true;
1464
+ }
1465
+ /**
1466
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
1467
+ * on later React re-renders.
1468
+ * @param controller - the panel controller the entry toggles.
1469
+ * @param label - the entry label (localized).
1470
+ * @param tooltip - the entry tooltip (localized).
1471
+ * @returns disposer removing the entry and its observers.
1472
+ */
1473
+ function mountSidebarEntry(controller, label, tooltip) {
1474
+ const entry = createEntry(controller, label, tooltip);
1475
+ let root;
1476
+ let placed = false;
1477
+ const tryPlace = () => {
1478
+ if (root !== void 0 && !root.isConnected) {
1479
+ rootObserver.disconnect();
1480
+ root = void 0;
1481
+ placed = false;
1482
+ }
1483
+ if (placed) {
1484
+ if (document.body.contains(entry)) return;
1485
+ rootObserver.disconnect();
1486
+ root = void 0;
1487
+ placed = false;
1488
+ }
1489
+ root ??= sidebarRoot();
1490
+ if (root === void 0) return;
1491
+ placed = placeEntry(root, entry);
1492
+ if (placed) rootObserver.observe(root, {
1493
+ childList: true,
1494
+ subtree: true
1495
+ });
1496
+ };
1497
+ const waitObserver = new MutationObserver(() => {
1498
+ tryPlace();
1499
+ });
1500
+ waitObserver.observe(document.body, {
1501
+ childList: true,
1502
+ subtree: true
1503
+ });
1504
+ const rootObserver = new MutationObserver(() => {
1505
+ if (root === void 0 || !root.isConnected) {
1506
+ placed = false;
1507
+ tryPlace();
1508
+ return;
1509
+ }
1510
+ if (!root.contains(entry)) placed = placeEntry(root, entry);
1511
+ });
1512
+ const syncActive = () => {
1513
+ if (controller.getSnapshot().panelOpen) entry.dataset.active = "true";
1514
+ else delete entry.dataset.active;
1515
+ };
1516
+ const unsubscribe = controller.subscribe(syncActive);
1517
+ syncActive();
1518
+ tryPlace();
1519
+ return () => {
1520
+ waitObserver.disconnect();
1521
+ rootObserver.disconnect();
1522
+ unsubscribe();
1523
+ entry.remove();
1524
+ };
1525
+ }
1526
+ //#endregion
1527
+ //#region src/client/settings-form.ts
1528
+ /**
1529
+ * Staged form model behind the plugin settings card. A card stages what the
1530
+ * user types and writes it only when they save — the settings write is a
1531
+ * durable, revision-fenced document mutation, so staging keeps what is on
1532
+ * screen exactly what a save would store. Self-contained slice of the same
1533
+ * pattern the dsh-web-ui family cards use (this package must not depend on a
1534
+ * sibling UI package).
1535
+ */
1536
+ /** A free-text field. An empty draft clears the field. */
1537
+ function textField(field) {
1538
+ return {
1539
+ field,
1540
+ format: (value) => typeof value === "string" ? value : "",
1541
+ parse: (text) => {
1542
+ const trimmed = text.trim();
1543
+ return trimmed === "" ? { kind: "clear" } : {
1544
+ kind: "set",
1545
+ value: trimmed
1546
+ };
1547
+ }
1548
+ };
1549
+ }
1550
+ /** A boolean field, edited through true/false draft text. */
1551
+ function booleanField(field) {
1552
+ return {
1553
+ field,
1554
+ format: (value) => typeof value === "boolean" ? String(value) : "",
1555
+ parse: (text) => {
1556
+ if (text === "true") return {
1557
+ kind: "set",
1558
+ value: true
1559
+ };
1560
+ if (text === "false") return {
1561
+ kind: "set",
1562
+ value: false
1563
+ };
1564
+ }
1565
+ };
1566
+ }
1567
+ /**
1568
+ * A secret field (role('secret') in the namespace schema). The stored value is
1569
+ * never rendered or returned by the redacted wire view, so:
1570
+ * - an empty draft means "no change" (typing nothing must never clear an
1571
+ * invisible stored key); the dedicated clear action stages an explicit clear;
1572
+ * - a write's outcome is judged by the namespace's secrets sidecar through
1573
+ * the {@link CardForm} `secretSettled` hook, never by the user layer.
1574
+ */
1575
+ function secretField(field) {
1576
+ return {
1577
+ field,
1578
+ secret: true,
1579
+ format: () => "",
1580
+ parse: (text) => {
1581
+ const trimmed = text.trim();
1582
+ if (trimmed === "") return void 0;
1583
+ return {
1584
+ kind: "set",
1585
+ value: trimmed
1586
+ };
1587
+ }
1588
+ };
1589
+ }
1590
+ /**
1591
+ * Stages one card's edits over one settings scope and writes them on save.
1592
+ *
1593
+ * The Host is the only authority on whether a value was accepted — its
1594
+ * validators own the constraints no schema can express — so the outcome is
1595
+ * read back from the section rather than predicted here. A save that did not
1596
+ * land keeps its drafts, so the user can correct them instead of retyping.
1597
+ */
1598
+ var CardForm = class {
1599
+ scope;
1600
+ options;
1601
+ specs;
1602
+ staged = /* @__PURE__ */ new Map();
1603
+ listeners = /* @__PURE__ */ new Set();
1604
+ saving = false;
1605
+ failed = false;
1606
+ /**
1607
+ * @param scope - the bound settings scope for this card's namespace.
1608
+ * @param specs - the fields this card edits.
1609
+ * @param options.secretSettled - for secret fields, whether the namespace
1610
+ * currently holds a stored secret (the redacted view never round-trips the
1611
+ * value, so a write's outcome is read from the secrets sidecar instead).
1612
+ */
1613
+ constructor(scope, specs, options = {}) {
1614
+ this.scope = scope;
1615
+ this.options = options;
1616
+ this.specs = new Map(specs.map((spec) => [spec.field, spec]));
1617
+ scope.subscribe(() => {
1618
+ this.publish();
1619
+ });
1620
+ }
1621
+ /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
1622
+ bind(project) {
1623
+ const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
1624
+ this.listeners.add(() => {
1625
+ store.set(project());
1626
+ });
1627
+ return store;
1628
+ }
1629
+ /** Read the card-level state: what the Host serves, and what a save would do. */
1630
+ shell() {
1631
+ const snapshot = this.scope.getSnapshot();
1632
+ const plan = this.plan();
1633
+ return {
1634
+ available: snapshot.status !== "loading",
1635
+ exposed: snapshot.status === "ready",
1636
+ writable: snapshot.writable,
1637
+ dirty: plan.length > 0,
1638
+ invalid: plan.some((item) => item.run === void 0),
1639
+ saving: this.saving,
1640
+ failed: this.failed
1641
+ };
1642
+ }
1643
+ /** Read one field's state from the effective section and its staged draft. */
1644
+ field(field) {
1645
+ const spec = this.specOf(field);
1646
+ const staged = this.staged.get(field);
1647
+ if (staged === void 0) return {
1648
+ text: spec.format(this.sectionValue(field)),
1649
+ overridden: this.stored(field),
1650
+ invalid: false
1651
+ };
1652
+ const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
1653
+ return {
1654
+ text: staged.text,
1655
+ overridden: write?.kind === "set",
1656
+ invalid: write === void 0 && !(spec.secret === true && staged.text.trim() === "")
1657
+ };
1658
+ }
1659
+ /** The actions the card's slot registration injects. */
1660
+ actions() {
1661
+ return {
1662
+ edit: (field, text) => {
1663
+ this.stage(field, {
1664
+ text,
1665
+ clear: false
1666
+ });
1667
+ },
1668
+ resetField: (field) => {
1669
+ this.stage(field, {
1670
+ text: this.specOf(field).format(this.baseValue(field)),
1671
+ clear: true
1672
+ });
1673
+ },
1674
+ save: () => {
1675
+ this.save();
1676
+ },
1677
+ discard: () => {
1678
+ if (this.staged.size === 0 && !this.failed) return;
1679
+ this.staged.clear();
1680
+ this.failed = false;
1681
+ this.publish();
1682
+ }
1683
+ };
1684
+ }
1685
+ /**
1686
+ * Write every staged edit, then re-seed from what the Host accepted.
1687
+ * @returns settlement after every write and the read-back.
1688
+ */
1689
+ async save() {
1690
+ const plan = this.plan();
1691
+ const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
1692
+ if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
1693
+ this.saving = true;
1694
+ this.failed = false;
1695
+ this.publish();
1696
+ let landed = true;
1697
+ for (const write of writes) landed = await write() && landed;
1698
+ if (landed) this.staged.clear();
1699
+ this.saving = false;
1700
+ this.failed = !landed;
1701
+ this.publish();
1702
+ }
1703
+ /**
1704
+ * Every staged edit a save would write. An entry whose draft is not a value
1705
+ * its field accepts carries no write: the form is still dirty, and the save
1706
+ * refuses rather than dropping the edit. A staged edit that matches the
1707
+ * effective section is not a write at all.
1708
+ */
1709
+ plan() {
1710
+ const plan = [];
1711
+ for (const [field, staged] of this.staged) {
1712
+ const spec = this.specOf(field);
1713
+ if (staged.clear) {
1714
+ if (spec.secret === true ? this.options.secretSettled?.(field) ?? false : this.stored(field)) plan.push({
1715
+ field,
1716
+ run: () => this.clear(field)
1717
+ });
1718
+ continue;
1719
+ }
1720
+ if (staged.text === spec.format(this.sectionValue(field))) continue;
1721
+ const write = spec.parse(staged.text);
1722
+ if (write === void 0) plan.push({
1723
+ field,
1724
+ run: void 0
1725
+ });
1726
+ else if (write.kind === "clear") plan.push({
1727
+ field,
1728
+ run: () => this.clear(field)
1729
+ });
1730
+ else plan.push({
1731
+ field,
1732
+ run: () => this.store(field, write.value)
1733
+ });
1734
+ }
1735
+ return plan;
1736
+ }
1737
+ async clear(field) {
1738
+ await this.scope.unset(field);
1739
+ if (this.specOf(field).secret === true) return !(this.options.secretSettled?.(field) ?? false);
1740
+ return !this.stored(field);
1741
+ }
1742
+ async store(field, value) {
1743
+ await this.scope.set(field, value);
1744
+ if (this.specOf(field).secret === true) return this.options.secretSettled?.(field) ?? true;
1745
+ return this.userLayer()?.[field] === value;
1746
+ }
1747
+ stage(field, edit) {
1748
+ this.staged.set(field, edit);
1749
+ this.failed = false;
1750
+ this.publish();
1751
+ }
1752
+ specOf(field) {
1753
+ const spec = this.specs.get(field);
1754
+ if (spec === void 0) throw new Error(`settings card has no field ${field}`);
1755
+ return spec;
1756
+ }
1757
+ snapshotOf() {
1758
+ return this.scope.getSnapshot();
1759
+ }
1760
+ sectionValue(field) {
1761
+ return this.snapshotOf().value?.[field];
1762
+ }
1763
+ baseValue(field) {
1764
+ return this.snapshotOf().base?.[field];
1765
+ }
1766
+ userLayer() {
1767
+ return this.snapshotOf().user;
1768
+ }
1769
+ stored(field) {
1770
+ const user = this.userLayer();
1771
+ return user !== void 0 && Object.hasOwn(user, field);
1772
+ }
1773
+ publish() {
1774
+ for (const listener of [...this.listeners]) listener();
1775
+ }
1776
+ };
1777
+ //#endregion
1778
+ //#region \0dsh-css:E:\dsh-plugin\src\client\settings-card.module.css.mjs
1779
+ const css = ".i1cc5G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.i1cc5G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.i1cc5G_card:has(.i1cc5G_body){background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.i1cc5G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.i1cc5G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.i1cc5G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.i1cc5G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.i1cc5G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.i1cc5G_chevron,.i1cc5G_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.i1cc5G_chevronOpen{transform:rotate(180deg)}.i1cc5G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.i1cc5G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.i1cc5G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.i1cc5G_field+.i1cc5G_field{border-top:1px solid var(--dsw-alias-border-l2)}.i1cc5G_head{align-items:center;gap:8px;display:flex}.i1cc5G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.i1cc5G_badges{align-items:center;gap:8px;display:inline-flex}.i1cc5G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.i1cc5G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.i1cc5G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.i1cc5G_reset:disabled{cursor:default;opacity:.5}.i1cc5G_input,.i1cc5G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;outline:none;padding:0 12px;font-size:13px;line-height:1.5}.i1cc5G_input:focus-visible,.i1cc5G_select:focus-visible{border-color:var(--dsw-alias-brand-primary)}.i1cc5G_input:disabled,.i1cc5G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.i1cc5G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;outline:none;padding:0 12px;font-size:13px;line-height:1.5}.i1cc5G_hint,.i1cc5G_invalid{margin:0;font-size:12px;line-height:1.5}.i1cc5G_hint{color:var(--dsw-alias-label-tertiary)}.i1cc5G_invalid{color:var(--dsw-alias-label-error)}.i1cc5G_readOnly,.i1cc5G_notExposed{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.i1cc5G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.i1cc5G_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.i1cc5G_discard,.i1cc5G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.i1cc5G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.i1cc5G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.i1cc5G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.i1cc5G_discard:disabled,.i1cc5G_save:disabled{opacity:.4;cursor:default}.i1cc5G_discard:focus-visible,.i1cc5G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}@media (prefers-reduced-motion:reduce){.i1cc5G_card,.i1cc5G_chevron,.i1cc5G_chevronOpen{transition:none}}";
1780
+ const tagId = "@dickpy/dsh-imagegen/settings-card.module.css";
1781
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1782
+ const tag = document.createElement("style");
1783
+ tag.dataset.plugin = "@dickpy/dsh-imagegen";
1784
+ tag.dataset.pluginCss = tagId;
1785
+ tag.textContent = css;
1786
+ document.head.appendChild(tag);
1787
+ }
1788
+ var settings_card_module_css_default = {
1789
+ "discard": "i1cc5G_discard",
1790
+ "readOnly": "i1cc5G_readOnly",
1791
+ "chevron": "i1cc5G_chevron",
1792
+ "save": "i1cc5G_save",
1793
+ "reset": "i1cc5G_reset",
1794
+ "header": "i1cc5G_header",
1795
+ "headText": "i1cc5G_headText",
1796
+ "label": "i1cc5G_label",
1797
+ "select": "i1cc5G_select",
1798
+ "footer": "i1cc5G_footer",
1799
+ "inputInvalid": "i1cc5G_inputInvalid",
1800
+ "badge": "i1cc5G_badge",
1801
+ "input": "i1cc5G_input",
1802
+ "card": "i1cc5G_card",
1803
+ "badges": "i1cc5G_badges",
1804
+ "hint": "i1cc5G_hint",
1805
+ "chevronOpen": "i1cc5G_chevronOpen",
1806
+ "body": "i1cc5G_body",
1807
+ "field": "i1cc5G_field",
1808
+ "pending": "i1cc5G_pending",
1809
+ "notExposed": "i1cc5G_notExposed",
1810
+ "head": "i1cc5G_head",
1811
+ "invalid": "i1cc5G_invalid",
1812
+ "failed": "i1cc5G_failed",
1813
+ "name": "i1cc5G_name",
1814
+ "description": "i1cc5G_description"
1815
+ };
1816
+ //#endregion
1817
+ //#region src/client/SettingsCard.tsx
1818
+ /**
1819
+ * The dsh-imagegen settings card: api_url, api_key (secret, display-only
1820
+ * "set" state), and the plugin switches. Registers into the official
1821
+ * `settings.plugin.item` slot (the Settings → Plugins → Configurable tab),
1822
+ * independent of the dsh-web-ui family group, bound to the plugin's own
1823
+ * bridge settings scope.
1824
+ */
1825
+ /** Bridges the imagegen scope onto the card's staged form. */
1826
+ var ImageGenSettingsCardController = class {
1827
+ scope;
1828
+ form;
1829
+ /** @param scope - the bound bridge scope for the dsh-imagegen namespace. */
1830
+ constructor(scope) {
1831
+ this.scope = scope;
1832
+ this.form = new CardForm(scope, [
1833
+ booleanField("enabled"),
1834
+ booleanField("announceToAgent"),
1835
+ textField("apiUrl"),
1836
+ secretField("apiKey")
1837
+ ], { secretSettled: () => this.scope.getKeySetSnapshot() });
1838
+ }
1839
+ projection() {
1840
+ return {
1841
+ ...this.form.shell(),
1842
+ enabled: this.form.field("enabled"),
1843
+ announceToAgent: this.form.field("announceToAgent"),
1844
+ apiUrl: this.form.field("apiUrl"),
1845
+ apiKey: this.form.field("apiKey")
1846
+ };
1847
+ }
1848
+ /**
1849
+ * Build the face the card's slot registration injects.
1850
+ * @returns the card's snapshot, the key-set flag, and the form actions.
1851
+ */
1852
+ inject() {
1853
+ const cardStore = this.form.bind(() => this.projection());
1854
+ const keySetStore = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.scope.getKeySetSnapshot());
1855
+ this.scope.subscribeKeySet(() => {
1856
+ keySetStore.set(this.scope.getKeySetSnapshot());
1857
+ });
1858
+ return {
1859
+ hooks: {
1860
+ imageGenSettingsCard: cardStore,
1861
+ imageGenKeySet: keySetStore
1862
+ },
1863
+ ...this.form.actions()
1864
+ };
1865
+ }
1866
+ };
1867
+ /**
1868
+ * Render the card.
1869
+ * @param props - locale copy, the card snapshot, and the form actions.
1870
+ * @returns the card, or nothing while the namespace is still loading.
1871
+ */
1872
+ function ImageGenSettingsCard(props) {
1873
+ const { t } = props;
1874
+ const state = props.useImageGenSettingsCard((snapshot) => snapshot);
1875
+ const keySet = props.useImageGenKeySet((snapshot) => snapshot);
1876
+ const [open, setOpen] = (0, react.useState)(false);
1877
+ if (!state.available) return null;
1878
+ const title = t("settings.title");
1879
+ const blocked = !state.dirty || state.invalid || state.saving;
1880
+ const disabled = !state.writable;
1881
+ const fieldProps = {
1882
+ overriddenLabel: t("settings.overridden"),
1883
+ resetLabel: t("settings.reset"),
1884
+ invalidLabel: t("settings.invalidNumber"),
1885
+ disabled
1886
+ };
1887
+ if (!state.exposed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1888
+ className: settings_card_module_css_default.card,
1889
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1890
+ type: "button",
1891
+ className: settings_card_module_css_default.header,
1892
+ "aria-expanded": open,
1893
+ "aria-label": `${t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
1894
+ onClick: () => {
1895
+ setOpen(!open);
1896
+ },
1897
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1898
+ className: settings_card_module_css_default.headText,
1899
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1900
+ className: settings_card_module_css_default.name,
1901
+ children: title
1902
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1903
+ className: settings_card_module_css_default.description,
1904
+ children: t("settings.description")
1905
+ })]
1906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1907
+ className: open ? settings_card_module_css_default.chevronOpen : settings_card_module_css_default.chevron,
1908
+ children: "▾"
1909
+ })]
1910
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1911
+ className: settings_card_module_css_default.body,
1912
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1913
+ className: settings_card_module_css_default.notExposed,
1914
+ role: "status",
1915
+ children: t("settings.notExposed")
1916
+ })
1917
+ }) : null]
1918
+ });
1919
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1920
+ className: settings_card_module_css_default.card,
1921
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1922
+ type: "button",
1923
+ className: settings_card_module_css_default.header,
1924
+ "aria-expanded": open,
1925
+ "aria-label": `${t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
1926
+ onClick: () => {
1927
+ setOpen(!open);
1928
+ },
1929
+ children: [
1930
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1931
+ className: settings_card_module_css_default.headText,
1932
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1933
+ className: settings_card_module_css_default.name,
1934
+ children: title
1935
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1936
+ className: settings_card_module_css_default.description,
1937
+ children: t("settings.description")
1938
+ })]
1939
+ }),
1940
+ state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1941
+ className: settings_card_module_css_default.pending,
1942
+ children: t("settings.unsaved")
1943
+ }) : null,
1944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1945
+ className: open ? settings_card_module_css_default.chevronOpen : settings_card_module_css_default.chevron,
1946
+ children: "▾"
1947
+ })
1948
+ ]
1949
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1950
+ className: settings_card_module_css_default.body,
1951
+ children: [
1952
+ !state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1953
+ className: settings_card_module_css_default.readOnly,
1954
+ role: "status",
1955
+ children: t("settings.readOnly")
1956
+ }) : null,
1957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1958
+ id: "dsh-imagegen-settings-apikey",
1959
+ label: t("settings.apiKey"),
1960
+ hint: keySet ? t("settings.apiKeySet") : t("settings.apiKeyHint"),
1961
+ placeholder: "sk-…",
1962
+ secret: true,
1963
+ ...fieldProps,
1964
+ ...state.apiKey,
1965
+ overridden: false,
1966
+ onEdit: (text) => {
1967
+ props.edit("apiKey", text);
1968
+ },
1969
+ onReset: () => {
1970
+ props.resetField("apiKey");
1971
+ },
1972
+ clearLabel: t("settings.apiKeyClear"),
1973
+ onClear: () => {
1974
+ props.resetField("apiKey");
1975
+ },
1976
+ canClear: keySet
1977
+ }),
1978
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1979
+ id: "dsh-imagegen-settings-apiurl",
1980
+ label: t("settings.apiUrl"),
1981
+ hint: t("settings.apiUrlHint"),
1982
+ placeholder: "https://api.openai.com/v1",
1983
+ ...fieldProps,
1984
+ ...state.apiUrl,
1985
+ onEdit: (text) => {
1986
+ props.edit("apiUrl", text);
1987
+ },
1988
+ onReset: () => {
1989
+ props.resetField("apiUrl");
1990
+ }
1991
+ }),
1992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
1993
+ id: "dsh-imagegen-settings-enabled",
1994
+ label: t("settings.enabled"),
1995
+ hint: t("settings.enabledHint"),
1996
+ inheritLabel: t("settings.inherit"),
1997
+ onLabel: t("settings.on"),
1998
+ offLabel: t("settings.off"),
1999
+ ...fieldProps,
2000
+ ...state.enabled,
2001
+ onEdit: (text) => {
2002
+ props.edit("enabled", text);
2003
+ },
2004
+ onReset: () => {
2005
+ props.resetField("enabled");
2006
+ }
2007
+ }),
2008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
2009
+ id: "dsh-imagegen-settings-announce",
2010
+ label: t("settings.announceToAgent"),
2011
+ hint: t("settings.announceToAgentHint"),
2012
+ inheritLabel: t("settings.inherit"),
2013
+ onLabel: t("settings.on"),
2014
+ offLabel: t("settings.off"),
2015
+ ...fieldProps,
2016
+ ...state.announceToAgent,
2017
+ onEdit: (text) => {
2018
+ props.edit("announceToAgent", text);
2019
+ },
2020
+ onReset: () => {
2021
+ props.resetField("announceToAgent");
2022
+ }
2023
+ }),
2024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2025
+ className: settings_card_module_css_default.footer,
2026
+ children: [
2027
+ state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2028
+ className: settings_card_module_css_default.failed,
2029
+ role: "status",
2030
+ children: t("settings.saveFailed")
2031
+ }) : null,
2032
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2033
+ type: "button",
2034
+ className: settings_card_module_css_default.discard,
2035
+ disabled: !state.dirty || state.saving,
2036
+ onClick: props.discard,
2037
+ children: t("settings.discard")
2038
+ }),
2039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2040
+ type: "button",
2041
+ className: settings_card_module_css_default.save,
2042
+ disabled: blocked,
2043
+ onClick: props.save,
2044
+ children: t(!state.saving ? "settings.save" : "settings.saving")
2045
+ })
2046
+ ]
2047
+ })
2048
+ ]
2049
+ }) : null]
2050
+ });
2051
+ }
2052
+ /** A staged value field; `secret` renders a password control. */
2053
+ function ValueField(props) {
2054
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2055
+ className: settings_card_module_css_default.field,
2056
+ children: [
2057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2058
+ className: settings_card_module_css_default.head,
2059
+ children: [
2060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2061
+ className: settings_card_module_css_default.label,
2062
+ htmlFor: props.id,
2063
+ children: props.label
2064
+ }),
2065
+ props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2066
+ className: settings_card_module_css_default.badges,
2067
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2068
+ className: settings_card_module_css_default.badge,
2069
+ children: props.overriddenLabel
2070
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2071
+ type: "button",
2072
+ className: settings_card_module_css_default.reset,
2073
+ disabled: props.disabled,
2074
+ onClick: props.onReset,
2075
+ children: props.resetLabel
2076
+ })]
2077
+ }) : null,
2078
+ props.secret === true && props.canClear === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2079
+ type: "button",
2080
+ className: settings_card_module_css_default.reset,
2081
+ disabled: props.disabled,
2082
+ onClick: props.onClear,
2083
+ children: props.clearLabel ?? props.resetLabel
2084
+ }) : null
2085
+ ]
2086
+ }),
2087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2088
+ id: props.id,
2089
+ className: props.invalid ? settings_card_module_css_default.inputInvalid : settings_card_module_css_default.input,
2090
+ type: props.secret === true ? "password" : "text",
2091
+ autoComplete: props.secret === true ? "off" : void 0,
2092
+ ...props.invalid ? { "aria-invalid": true } : {},
2093
+ value: props.text,
2094
+ placeholder: props.placeholder ?? "",
2095
+ disabled: props.disabled,
2096
+ onChange: (event) => {
2097
+ props.onEdit(event.target.value);
2098
+ }
2099
+ }),
2100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2101
+ className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
2102
+ children: props.invalid ? props.invalidLabel : props.hint
2103
+ })
2104
+ ]
2105
+ });
2106
+ }
2107
+ /** A staged boolean field: 继承 / 开 / 关. */
2108
+ function BooleanField(props) {
2109
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2110
+ className: settings_card_module_css_default.field,
2111
+ children: [
2112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2113
+ className: settings_card_module_css_default.head,
2114
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2115
+ className: settings_card_module_css_default.label,
2116
+ htmlFor: props.id,
2117
+ children: props.label
2118
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2119
+ className: settings_card_module_css_default.badges,
2120
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2121
+ className: settings_card_module_css_default.badge,
2122
+ children: props.overriddenLabel
2123
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2124
+ type: "button",
2125
+ className: settings_card_module_css_default.reset,
2126
+ disabled: props.disabled,
2127
+ onClick: props.onReset,
2128
+ children: props.resetLabel
2129
+ })]
2130
+ }) : null]
2131
+ }),
2132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2133
+ id: props.id,
2134
+ className: settings_card_module_css_default.select,
2135
+ value: props.text,
2136
+ disabled: props.disabled,
2137
+ onChange: (event) => {
2138
+ props.onEdit(event.target.value);
2139
+ },
2140
+ children: [
2141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2142
+ value: "",
2143
+ children: props.inheritLabel
2144
+ }),
2145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2146
+ value: "true",
2147
+ children: props.onLabel
2148
+ }),
2149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2150
+ value: "false",
2151
+ children: props.offLabel
2152
+ })
2153
+ ]
2154
+ }),
2155
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2156
+ className: settings_card_module_css_default.hint,
2157
+ children: props.hint
2158
+ })
2159
+ ]
2160
+ });
2161
+ }
2162
+ //#endregion
2163
+ //#region src/client/settings-scope.ts
2164
+ /**
2165
+ * Browser-side settings scope for the dsh-imagegen namespace, served by the
2166
+ * plugin's own loopback bridge routes (/api/dsh-imagegen/settings). The
2167
+ * official rc.6 settings scope answers "unavailable" for every third-party
2168
+ * namespace (the host-apiproxy allowlist is hard-coded), so this package
2169
+ * re-serves its namespace through the host settings seam over a same-origin,
2170
+ * loopback-only HTTP pair — the same pattern the dsh-web-ui family bridge
2171
+ * uses, self-contained per plugin.
2172
+ */
2173
+ /** Settings wire face over the bridge routes (fetch-backed). */
2174
+ function createBridgeApi(fetchFn) {
2175
+ const post = async (path, body) => {
2176
+ try {
2177
+ const response = await fetchFn(path, {
2178
+ method: "POST",
2179
+ headers: { "content-type": "application/json" },
2180
+ body: JSON.stringify(body)
2181
+ });
2182
+ if (!response.ok) return { result: {
2183
+ ok: false,
2184
+ code: "internal",
2185
+ message: `bridge HTTP ${response.status}`
2186
+ } };
2187
+ return { result: await response.json() };
2188
+ } catch {
2189
+ return { result: {
2190
+ ok: false,
2191
+ code: "internal",
2192
+ message: "settings bridge unreachable"
2193
+ } };
2194
+ }
2195
+ };
2196
+ return { settings: {
2197
+ describe: async (payload) => post(SETTINGS_API.describe, payload),
2198
+ mutate: async (payload) => post(SETTINGS_API.mutate, payload)
2199
+ } };
2200
+ }
2201
+ /**
2202
+ * A SettingsScope over the bridge face: serialized queue, revision-fenced
2203
+ * writes, recovery read after a refusal. Mirrors the official controller's
2204
+ * ordering but trusts the Host-seam value without re-running the wire-schema
2205
+ * validation — the seam already validated it.
2206
+ */
2207
+ var BridgeScopeController = class {
2208
+ api;
2209
+ spec;
2210
+ store;
2211
+ /** Whether the namespace currently holds a stored secret (e.g. apiKey). */
2212
+ keySet;
2213
+ tail = Promise.resolve();
2214
+ disposed = false;
2215
+ constructor(api, spec) {
2216
+ this.api = api;
2217
+ this.spec = spec;
2218
+ this.store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)({
2219
+ status: "loading",
2220
+ value: void 0,
2221
+ base: void 0,
2222
+ user: void 0,
2223
+ revision: void 0,
2224
+ writable: false,
2225
+ mode: "host"
2226
+ });
2227
+ this.keySet = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(false);
2228
+ }
2229
+ getSnapshot() {
2230
+ return this.store.getSnapshot();
2231
+ }
2232
+ /** Whether a stored secret exists (from the redacted view's secrets list). */
2233
+ getKeySetSnapshot() {
2234
+ return this.keySet.getSnapshot();
2235
+ }
2236
+ /** Observe the secret-set flag. */
2237
+ subscribeKeySet(listener) {
2238
+ return this.keySet.subscribe(listener);
2239
+ }
2240
+ subscribe(listener) {
2241
+ return this.store.subscribe(listener);
2242
+ }
2243
+ /** Queue a bridge refresh. */
2244
+ load() {
2245
+ return this.enqueue(() => this.read());
2246
+ }
2247
+ set(field, value) {
2248
+ return this.enqueue(() => this.write({
2249
+ op: "set",
2250
+ path: [field],
2251
+ value
2252
+ }));
2253
+ }
2254
+ unset(field) {
2255
+ return this.enqueue(() => this.write({
2256
+ op: "unset",
2257
+ path: [field]
2258
+ }));
2259
+ }
2260
+ async dispose() {
2261
+ this.disposed = true;
2262
+ await this.tail;
2263
+ }
2264
+ enqueue(operation) {
2265
+ if (this.disposed) return Promise.resolve();
2266
+ const task = this.tail.then(async () => {
2267
+ if (this.disposed) return;
2268
+ await operation();
2269
+ });
2270
+ this.tail = task.catch(() => {});
2271
+ return task;
2272
+ }
2273
+ async read() {
2274
+ let response;
2275
+ try {
2276
+ response = await this.api.describe({});
2277
+ } catch {
2278
+ if (!this.disposed) this.store.update((draft) => {
2279
+ draft.status = "unavailable";
2280
+ });
2281
+ return;
2282
+ }
2283
+ if (!response.result.ok || this.disposed) {
2284
+ if (!this.disposed) this.store.update((draft) => {
2285
+ draft.status = "unavailable";
2286
+ });
2287
+ return;
2288
+ }
2289
+ const { namespaces, writable } = response.result.value;
2290
+ const view = namespaces?.find((candidate) => candidate.ns === this.spec.namespace);
2291
+ if (view === void 0) {
2292
+ this.store.update((draft) => {
2293
+ draft.status = "unavailable";
2294
+ draft.writable = writable === true;
2295
+ });
2296
+ this.keySet.set(false);
2297
+ return;
2298
+ }
2299
+ this.accept(view, writable);
2300
+ }
2301
+ async write(op) {
2302
+ const revision = this.getSnapshot().revision;
2303
+ let response;
2304
+ try {
2305
+ response = await this.api.mutate({
2306
+ ns: this.spec.namespace,
2307
+ ops: [op],
2308
+ ...revision === void 0 ? {} : { expectedRevision: revision }
2309
+ });
2310
+ } catch {
2311
+ await this.read();
2312
+ return;
2313
+ }
2314
+ if (!response.result.ok || this.disposed) {
2315
+ await this.read();
2316
+ return;
2317
+ }
2318
+ this.accept(response.result.value, void 0);
2319
+ }
2320
+ accept(view, writable) {
2321
+ this.store.update((draft) => {
2322
+ draft.revision = view.revision;
2323
+ draft.base = view.base;
2324
+ draft.user = view.user;
2325
+ if (writable !== void 0) draft.writable = writable;
2326
+ draft.status = "ready";
2327
+ draft.value = view.value;
2328
+ });
2329
+ this.keySet.set(Array.isArray(view.secrets) && view.secrets.some((secret) => secret.set));
2330
+ }
2331
+ };
2332
+ /**
2333
+ * Bind the dsh-imagegen settings scope over the bridge routes and start its
2334
+ * initial read (the caller mounts nothing until the scope settles).
2335
+ * @param fetchFn - the fetch implementation (the global fetch on loopback).
2336
+ * @returns the scope; unavailable when the bridge is unreachable.
2337
+ */
2338
+ function bindImageGenScope(fetchFn = fetch) {
2339
+ const controller = new BridgeScopeController(createBridgeApi(fetchFn).settings, { namespace: "dsh-imagegen" });
2340
+ controller.load();
2341
+ return controller;
2342
+ }
2343
+ //#endregion
2344
+ //#region src/client/index.ts
2345
+ /** Locale namespace this plugin owns. */
2346
+ const NS = "dsh-imagegen";
2347
+ /** Required services (fiber inject waiting — the runtime must be up first). */
2348
+ const inject = [
2349
+ "slots",
2350
+ "locale",
2351
+ "connection"
2352
+ ];
2353
+ /**
2354
+ * Mount the studio, its sidebar entry, and the settings card.
2355
+ * @param ctx - client root context (services: slots, locale, connection).
2356
+ */
2357
+ function apply(ctx) {
2358
+ ctx.effect(() => ctx.locale.register(NS, {
2359
+ zh,
2360
+ en
2361
+ }), "dsh-imagegen: dictionaries");
2362
+ const scope = bindImageGenScope(ctx.get("connection")?.isLoopback === true ? (input, init) => fetch(input, init) : () => {
2363
+ throw new Error("settings bridge is loopback-only");
2364
+ });
2365
+ ctx.effect(() => {
2366
+ const disposers = [ctx.on("connection/reset", () => {
2367
+ scope.load();
2368
+ })];
2369
+ return () => {
2370
+ for (const dispose of disposers) dispose();
2371
+ };
2372
+ }, "dsh-imagegen: settings scope invalidation");
2373
+ const settingsCard = new ImageGenSettingsCardController(scope);
2374
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
2375
+ name: "settings.plugin.item",
2376
+ id: "imagegen",
2377
+ order: 30,
2378
+ locale: NS,
2379
+ inject: () => settingsCard.inject()
2380
+ }, ImageGenSettingsCard));
2381
+ let uiDisposer;
2382
+ const mountUi = () => {
2383
+ if (uiDisposer !== void 0) return;
2384
+ const controller = new ImageGenController();
2385
+ const api = new ImageGenApi();
2386
+ const disposers = [];
2387
+ try {
2388
+ disposers.push(mountSidebarEntry(controller, tt("entry.label"), tt("entry.tooltip")));
2389
+ disposers.push(mountPanel(controller, api, scope));
2390
+ } catch (error) {
2391
+ console.warn("[dsh-imagegen] mount failed:", error);
2392
+ }
2393
+ uiDisposer = () => {
2394
+ for (const dispose of disposers.splice(0)) dispose();
2395
+ uiDisposer = void 0;
2396
+ };
2397
+ };
2398
+ const syncEnabled = () => {
2399
+ const snapshot = scope.getSnapshot();
2400
+ if (snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable") mountUi();
2401
+ else uiDisposer?.();
2402
+ };
2403
+ scope.subscribe(syncEnabled);
2404
+ syncEnabled();
2405
+ }
2406
+ //#endregion
2407
+ exports.apply = apply;
2408
+ exports.inject = inject;
2409
+ return module.exports;
2410
+ }
2411
+ });
2412
+
2413
+ //# sourceMappingURL=client.js.map