@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/LICENSE +201 -0
- package/README.md +126 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +2413 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +896 -0
- package/package.json +66 -0
- package/src/client/ImageGenPanel.tsx +687 -0
- package/src/client/SettingsCard.tsx +373 -0
- package/src/client/api.ts +89 -0
- package/src/client/controller.ts +46 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/helpers.ts +33 -0
- package/src/client/index.ts +127 -0
- package/src/client/locales.ts +204 -0
- package/src/client/mount.tsx +119 -0
- package/src/client/panel.module.css +970 -0
- package/src/client/settings-card.module.css +288 -0
- package/src/client/settings-form.ts +324 -0
- package/src/client/settings-scope.ts +227 -0
- package/src/client/sidebar-entry.ts +144 -0
- package/src/engine.ts +284 -0
- package/src/history-store.ts +217 -0
- package/src/index.ts +139 -0
- package/src/protocol.ts +118 -0
- package/src/routes.ts +373 -0
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
|