dsh-comfyui 0.3.0-beta.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 +21 -0
- package/README.md +160 -0
- package/README.zh.md +160 -0
- package/client/client.js +3089 -0
- package/client/client.js.map +1 -0
- package/cordis.patch.yml +4 -0
- package/lib/analyze.d.ts +61 -0
- package/lib/analyze.js +96 -0
- package/lib/comfyui.d.ts +195 -0
- package/lib/comfyui.js +392 -0
- package/lib/config.d.ts +71 -0
- package/lib/config.js +28 -0
- package/lib/convert.d.ts +35 -0
- package/lib/convert.js +289 -0
- package/lib/host-hint.d.ts +29 -0
- package/lib/host-hint.js +73 -0
- package/lib/http.d.ts +15 -0
- package/lib/http.js +41 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +344 -0
- package/lib/params.d.ts +63 -0
- package/lib/params.js +537 -0
- package/lib/progress.d.ts +27 -0
- package/lib/progress.js +98 -0
- package/lib/proxy.d.ts +13 -0
- package/lib/proxy.js +89 -0
- package/lib/queue.d.ts +50 -0
- package/lib/queue.js +103 -0
- package/lib/routes.d.ts +13 -0
- package/lib/routes.js +800 -0
- package/lib/skill.d.ts +12 -0
- package/lib/skill.js +72 -0
- package/lib/store.d.ts +120 -0
- package/lib/store.js +193 -0
- package/lib/templates.d.ts +36 -0
- package/lib/templates.js +84 -0
- package/lib/tools.d.ts +165 -0
- package/lib/tools.js +493 -0
- package/package.json +80 -0
package/client/client.js
ADDED
|
@@ -0,0 +1,3089 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-comfyui",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
//#region src/client/i18n.ts
|
|
9
|
+
/** zh/en dictionaries for dsh-comfyui UI (settings page, tool card, panel). */
|
|
10
|
+
const zh = {
|
|
11
|
+
settingsTitle: "ComfyUI",
|
|
12
|
+
settingsDesc: "连接你的 ComfyUI 服务器,让 Agent 直接生成与处理图像、视频。",
|
|
13
|
+
baseUrl: "服务器地址",
|
|
14
|
+
baseUrlHint: "ComfyUI 的 HTTP 地址(默认 http://127.0.0.1:8188)。",
|
|
15
|
+
apiKeyEnv: "API Key 环境变量名",
|
|
16
|
+
apiKeyEnvHint: "凭据存储或环境变量中的密钥名(默认 COMFYUI_API_KEY),远程服务通常需要。",
|
|
17
|
+
hasApiKey: "已配置 API Key",
|
|
18
|
+
noApiKey: "未配置 API Key(本地服务通常不需要)",
|
|
19
|
+
save: "保存",
|
|
20
|
+
saved: "已保存",
|
|
21
|
+
saveFailed: "保存失败",
|
|
22
|
+
test: "测试连接",
|
|
23
|
+
testing: "正在测试…",
|
|
24
|
+
testOk: "连接成功 · ComfyUI {version} · {ms} ms",
|
|
25
|
+
testFail: "连接失败:{message}",
|
|
26
|
+
configNotWritable: "设置服务不可用,请通过 cordis.yml 修改配置。",
|
|
27
|
+
mediaHost: "媒体访问地址",
|
|
28
|
+
mediaHostHint: "生成图片/媒体在聊天中使用的完整地址(如 http://192.168.1.5:3080)。留空时自动使用浏览器访问本服务器的地址;内网或反向代理下若图片不显示,请在此填写你访问本页面所用的地址。",
|
|
29
|
+
language: "界面语言",
|
|
30
|
+
languageHint: "切换插件界面语言(中文 / English)。切换后页面会自动刷新。",
|
|
31
|
+
cardGenerating: "ComfyUI 生成中…",
|
|
32
|
+
cardBackground: "后台任务",
|
|
33
|
+
cardBackgroundDone: "已完成",
|
|
34
|
+
cardPrompt: "任务 ID",
|
|
35
|
+
cardElapsed: "耗时",
|
|
36
|
+
cardMedia: "结果",
|
|
37
|
+
cardEmpty: "无媒体输出",
|
|
38
|
+
cardFailed: "执行失败",
|
|
39
|
+
cardInterrupted: "已中断",
|
|
40
|
+
cardCollect: "使用 job_output 收集后台任务结果",
|
|
41
|
+
cardLoadFailed: "加载失败,点下方下载",
|
|
42
|
+
cardDownload: "下载",
|
|
43
|
+
cardWorkflow: "工作流",
|
|
44
|
+
cardMode: "模式",
|
|
45
|
+
cardListed: "已列出 {runs} 个运行工作流,{graphs} 个图工作流",
|
|
46
|
+
panelTitle: "ComfyUI 面板",
|
|
47
|
+
close: "关闭",
|
|
48
|
+
lbPrev: "上一张",
|
|
49
|
+
lbNext: "下一张",
|
|
50
|
+
tabWorkflows: "工作流",
|
|
51
|
+
tabAssets: "资产",
|
|
52
|
+
tabQueue: "队列",
|
|
53
|
+
error: "出错",
|
|
54
|
+
refresh: "刷新",
|
|
55
|
+
ours: "本插件",
|
|
56
|
+
wfAdd: "新建工作流",
|
|
57
|
+
wfName: "名称",
|
|
58
|
+
wfDesc: "概述",
|
|
59
|
+
wfDescPlaceholder: "告诉 Agent 这个工作流做什么",
|
|
60
|
+
wfNameCopyHint: "点击复制工作流名称",
|
|
61
|
+
wfNameCopied: "已复制工作流名称",
|
|
62
|
+
wfParams: "可调参数",
|
|
63
|
+
wfTags: "标签",
|
|
64
|
+
wfTagAll: "全部",
|
|
65
|
+
wfTagAdd: "自定义标签,回车添加",
|
|
66
|
+
wfTagRemove: "点击移除标签",
|
|
67
|
+
wfParamsEmpty: "未定义参数。点击\"识别参数\"自动提取提示词/分辨率/步数/种子,或\"高级参数\"手动指定任意节点输入。",
|
|
68
|
+
wfParamsDrop: "拖拽/点击/粘贴上传",
|
|
69
|
+
wfPasteHint: "悬停此处后按 Ctrl+V 粘贴剪贴板里的图片/文件",
|
|
70
|
+
wfUploadSection: "加载区",
|
|
71
|
+
wfUploadHint: "点击大图选择图像,或粘贴/上传新图像;选中后图生图默认使用该图像",
|
|
72
|
+
wfUploadZone: "拖拽 / 点击 / 粘贴上传到 ComfyUI input 目录",
|
|
73
|
+
wfUploaded: "已上传",
|
|
74
|
+
wfCopyName: "复制文件名",
|
|
75
|
+
wfLoadArea: "加载区",
|
|
76
|
+
wfLoadHint: "点击选择图像(加载区选中的图像将作为图生图的默认源图)",
|
|
77
|
+
wfLoadEmpty: "未选择图像,点击选择",
|
|
78
|
+
wfLoadPicked: "已加载",
|
|
79
|
+
wfTabAll: "全部",
|
|
80
|
+
wfTabImported: "已导入",
|
|
81
|
+
wfTabGenerated: "已生成",
|
|
82
|
+
wfTypeAll: "全部类型",
|
|
83
|
+
wfUploadedToast: "文件已上传",
|
|
84
|
+
wfPickerUpload: "粘贴 / 点击上传",
|
|
85
|
+
wfLoadNoFiles: "没有可用的图像",
|
|
86
|
+
wfParamsUploading: "上传中…",
|
|
87
|
+
uploadKind_image: "图片",
|
|
88
|
+
uploadKind_video: "视频",
|
|
89
|
+
uploadKind_audio: "音频",
|
|
90
|
+
wfMediaClear: "清空",
|
|
91
|
+
wfMediaEmpty: "文件名(留空=移除该参考位)",
|
|
92
|
+
wfRefSlot: "参考",
|
|
93
|
+
wfParamsRecognize: "识别参数",
|
|
94
|
+
wfParamsRecognizing: "识别中…",
|
|
95
|
+
wfParamsRecognizeFailed: "识别失败:无法连接 ComfyUI 或工作流无效",
|
|
96
|
+
wfParamsAdd: "高级参数",
|
|
97
|
+
wfParamAdd: "添加",
|
|
98
|
+
wfParamLabel: "名称",
|
|
99
|
+
wfParamCustomLabel: "自定义名称",
|
|
100
|
+
wfParamCustomHint: "仅用于展示与 Agent 描述,传参键由系统自动生成;留空使用默认",
|
|
101
|
+
wfParamDefault: "默认值",
|
|
102
|
+
wfParamRandom: "随机",
|
|
103
|
+
wfParamDelete: "删除",
|
|
104
|
+
wfParamNode: "节点",
|
|
105
|
+
wfParamInput: "输入",
|
|
106
|
+
wfJson: "工作流 JSON(ComfyUI UI 导出的 API 格式)",
|
|
107
|
+
wfSave: "保存",
|
|
108
|
+
wfSaveRun: "保存并运行",
|
|
109
|
+
wfCancel: "取消",
|
|
110
|
+
wfEdit: "编辑",
|
|
111
|
+
wfRun: "运行",
|
|
112
|
+
wfDelete: "删除",
|
|
113
|
+
wfQueued: "已入队:",
|
|
114
|
+
wfJsonError: "工作流 JSON 解析失败",
|
|
115
|
+
wfUpdated: "更新于",
|
|
116
|
+
wfEmpty: "没有检测到任何工作流,请通过新建、导入或提取来创建执行流程。",
|
|
117
|
+
wfLibrarySection: "API 工作流",
|
|
118
|
+
wfRefresh: "刷新",
|
|
119
|
+
wfImportFile: "导入文件",
|
|
120
|
+
wfComfyuiSection: "ComfyUI 工作流",
|
|
121
|
+
wfComfyuiHint: "面板自动检测 ComfyUI 中保存的工作流。\n注:非 API 流程,无法直接执行,需经过提取转化为 API 工作流,或自行导入 API 工作流即可执行。",
|
|
122
|
+
wfComfyuiEmpty: "ComfyUI 端没有检测到保存的工作流",
|
|
123
|
+
wfExtracted: "已提取",
|
|
124
|
+
wfNotExtracted: "未提取",
|
|
125
|
+
wfView: "查看",
|
|
126
|
+
wfExtract: "提取",
|
|
127
|
+
wfExtractedNotice: "已提取并加入插件库",
|
|
128
|
+
wfExtractTitle: "提取执行流",
|
|
129
|
+
wfExtractModeAll: "整体提取",
|
|
130
|
+
wfExtractModeAllHint: "所有分量合成一个执行流(运行时全部执行)",
|
|
131
|
+
wfExtractModeSplit: "按分量提取",
|
|
132
|
+
wfExtractModeSplitHint: "每个独立流程一个执行流(推荐)",
|
|
133
|
+
wfExtractModeMain: "只提取主流程",
|
|
134
|
+
wfExtractModeMainHint: "只提取最大分量(通常是当前测试区块)",
|
|
135
|
+
wfExtractRun: "提取",
|
|
136
|
+
wfExtractEmpty: "图里没有可提取的分量",
|
|
137
|
+
wfComponents: "分量",
|
|
138
|
+
wfComponentLabel: "分量",
|
|
139
|
+
wfNodes: "节点",
|
|
140
|
+
wfBypassed: "已绕过节点",
|
|
141
|
+
wfIsolated: "悬空节点",
|
|
142
|
+
wfViewBack: "返回",
|
|
143
|
+
wfViewNodes: "节点清单",
|
|
144
|
+
wfViewJson: "JSON 预览",
|
|
145
|
+
wfViewEmpty: "图中没有可执行的节点",
|
|
146
|
+
assetAll: "全部工作流",
|
|
147
|
+
assetBack: "返回",
|
|
148
|
+
assetEmpty: "还没有生成记录。运行工作流后,结果会出现在这里。",
|
|
149
|
+
assetGone: "源文件已被 ComfyUI 清理",
|
|
150
|
+
queueEmpty: "当前没有任务",
|
|
151
|
+
queueError: "无法读取队列(ComfyUI 未运行?)",
|
|
152
|
+
jobFilterLabel: "历史",
|
|
153
|
+
jobFilterAll: "全部",
|
|
154
|
+
jobFilterPending: "待生成",
|
|
155
|
+
jobFilterInProgress: "生成中",
|
|
156
|
+
jobFilterCompleted: "已完成",
|
|
157
|
+
jobFilterFailed: "失败",
|
|
158
|
+
jobFilterCancelled: "已取消",
|
|
159
|
+
jobActive: "进行中",
|
|
160
|
+
jobDelete: "删除",
|
|
161
|
+
jobInterrupt: "中断",
|
|
162
|
+
jobRerun: "重跑",
|
|
163
|
+
jobViewAssets: "查看资产",
|
|
164
|
+
jobMore: "更多操作",
|
|
165
|
+
jobClearQueue: "清空队列",
|
|
166
|
+
jobClearHistory: "清空历史",
|
|
167
|
+
jobFree: "释放内存",
|
|
168
|
+
jobDuration: "耗时",
|
|
169
|
+
jobOurs: "本插件"
|
|
170
|
+
};
|
|
171
|
+
const en = {
|
|
172
|
+
settingsTitle: "ComfyUI",
|
|
173
|
+
settingsDesc: "Connect your ComfyUI server so the agent can generate and process images and videos directly.",
|
|
174
|
+
baseUrl: "Server URL",
|
|
175
|
+
baseUrlHint: "ComfyUI HTTP address (default http://127.0.0.1:8188).",
|
|
176
|
+
apiKeyEnv: "API key env var",
|
|
177
|
+
apiKeyEnvHint: "Credential-store or environment variable name (default COMFYUI_API_KEY); usually needed for remote servers.",
|
|
178
|
+
hasApiKey: "API key configured",
|
|
179
|
+
noApiKey: "No API key configured (usually not needed for local servers)",
|
|
180
|
+
save: "Save",
|
|
181
|
+
saved: "Saved",
|
|
182
|
+
saveFailed: "Save failed",
|
|
183
|
+
test: "Test connection",
|
|
184
|
+
testing: "Testing…",
|
|
185
|
+
testOk: "Connected · ComfyUI {version} · {ms} ms",
|
|
186
|
+
testFail: "Connection failed: {message}",
|
|
187
|
+
configNotWritable: "Settings service unavailable — edit cordis.yml instead.",
|
|
188
|
+
mediaHost: "Media base URL",
|
|
189
|
+
mediaHostHint: "Absolute base URL used for generated media in chat (e.g. http://192.168.1.5:3080). Leave empty to auto-detect the address your browser uses to reach this server; fill it in when images do not load over LAN or a reverse proxy.",
|
|
190
|
+
language: "Language",
|
|
191
|
+
languageHint: "Switch the plugin UI language (中文 / English). The page reloads after switching.",
|
|
192
|
+
cardGenerating: "ComfyUI generating…",
|
|
193
|
+
cardBackground: "Background job",
|
|
194
|
+
cardBackgroundDone: "Completed",
|
|
195
|
+
cardPrompt: "Prompt ID",
|
|
196
|
+
cardElapsed: "Elapsed",
|
|
197
|
+
cardMedia: "Results",
|
|
198
|
+
cardEmpty: "No media outputs",
|
|
199
|
+
cardFailed: "Execution failed",
|
|
200
|
+
cardInterrupted: "Interrupted",
|
|
201
|
+
cardCollect: "Collect the background job result with job_output",
|
|
202
|
+
cardLoadFailed: "failed to load, use the download link below",
|
|
203
|
+
cardDownload: "Download",
|
|
204
|
+
cardWorkflow: "Workflow",
|
|
205
|
+
cardMode: "Mode",
|
|
206
|
+
cardListed: "Listed {runs} runnable workflows, {graphs} graph workflows",
|
|
207
|
+
panelTitle: "ComfyUI Panel",
|
|
208
|
+
close: "Close",
|
|
209
|
+
lbPrev: "Previous image",
|
|
210
|
+
lbNext: "Next image",
|
|
211
|
+
tabWorkflows: "Workflows",
|
|
212
|
+
tabAssets: "Assets",
|
|
213
|
+
tabQueue: "Queue",
|
|
214
|
+
error: "Error",
|
|
215
|
+
refresh: "Refresh",
|
|
216
|
+
ours: "ours",
|
|
217
|
+
wfAdd: "New workflow",
|
|
218
|
+
wfName: "Name",
|
|
219
|
+
wfDesc: "Overview",
|
|
220
|
+
wfDescPlaceholder: "Tells the agent what this workflow does",
|
|
221
|
+
wfNameCopyHint: "Click to copy the workflow name",
|
|
222
|
+
wfNameCopied: "Workflow name copied",
|
|
223
|
+
wfParams: "Parameters",
|
|
224
|
+
wfTags: "Tags",
|
|
225
|
+
wfTagAll: "All",
|
|
226
|
+
wfTagAdd: "Custom tag, press Enter to add",
|
|
227
|
+
wfTagRemove: "Click to remove tag",
|
|
228
|
+
wfParamsEmpty: "No parameters yet. Click \"Detect\" to auto-extract prompt / resolution / steps / seed, or \"Advanced\" to expose any node input manually.",
|
|
229
|
+
wfParamsDrop: "Drop / Click / Paste to upload",
|
|
230
|
+
wfPasteHint: "Hover here and press Ctrl+V to paste an image/file",
|
|
231
|
+
wfUploadSection: "Load area",
|
|
232
|
+
wfUploadHint: "Click the preview to pick an image, or paste/upload a new one; the selection becomes the default source for image-to-image",
|
|
233
|
+
wfUploadZone: "Drag / Click / Paste to upload into the ComfyUI input dir",
|
|
234
|
+
wfUploaded: "Uploaded",
|
|
235
|
+
wfCopyName: "Copy file name",
|
|
236
|
+
wfLoadArea: "Load area",
|
|
237
|
+
wfLoadHint: "Click to pick an image (the selection becomes the default source for image-to-image)",
|
|
238
|
+
wfLoadEmpty: "No image selected — click to pick",
|
|
239
|
+
wfLoadPicked: "Loaded",
|
|
240
|
+
wfTabAll: "All",
|
|
241
|
+
wfTabImported: "Imported",
|
|
242
|
+
wfTabGenerated: "Generated",
|
|
243
|
+
wfTypeAll: "All types",
|
|
244
|
+
wfUploadedToast: "File uploaded",
|
|
245
|
+
wfPickerUpload: "Paste / Click to upload",
|
|
246
|
+
wfLoadNoFiles: "No images available",
|
|
247
|
+
wfParamsUploading: "Uploading…",
|
|
248
|
+
uploadKind_image: "image",
|
|
249
|
+
uploadKind_video: "video",
|
|
250
|
+
uploadKind_audio: "audio",
|
|
251
|
+
wfMediaClear: "Clear",
|
|
252
|
+
wfMediaEmpty: "filename (empty = drop this slot)",
|
|
253
|
+
wfRefSlot: "Ref ",
|
|
254
|
+
wfParamsRecognize: "Detect",
|
|
255
|
+
wfParamsRecognizing: "Detecting…",
|
|
256
|
+
wfParamsRecognizeFailed: "Detection failed: ComfyUI unreachable or invalid workflow",
|
|
257
|
+
wfParamsAdd: "Advanced",
|
|
258
|
+
wfParamAdd: "Add",
|
|
259
|
+
wfParamLabel: "Name",
|
|
260
|
+
wfParamCustomLabel: "Custom name",
|
|
261
|
+
wfParamCustomHint: "Shown to the user and the agent only; the parameter key is generated automatically. Leave empty for the default",
|
|
262
|
+
wfParamDefault: "Default",
|
|
263
|
+
wfParamRandom: "Random",
|
|
264
|
+
wfParamDelete: "Delete",
|
|
265
|
+
wfParamNode: "Node",
|
|
266
|
+
wfParamInput: "Input",
|
|
267
|
+
wfJson: "Workflow JSON (API format exported from the ComfyUI UI)",
|
|
268
|
+
wfSave: "Save",
|
|
269
|
+
wfSaveRun: "Save & Run",
|
|
270
|
+
wfCancel: "Cancel",
|
|
271
|
+
wfEdit: "Edit",
|
|
272
|
+
wfRun: "Run",
|
|
273
|
+
wfDelete: "Delete",
|
|
274
|
+
wfQueued: "Queued: ",
|
|
275
|
+
wfJsonError: "Workflow JSON could not be parsed",
|
|
276
|
+
wfUpdated: "updated",
|
|
277
|
+
wfEmpty: "No workflows detected — create one via New, Import, or Extract.",
|
|
278
|
+
wfLibrarySection: "API workflows",
|
|
279
|
+
wfRefresh: "Refresh",
|
|
280
|
+
wfImportFile: "Import file",
|
|
281
|
+
wfComfyuiSection: "ComfyUI workflows",
|
|
282
|
+
wfComfyuiHint: "Auto-detects workflows saved in ComfyUI.\nNote: these are UI graphs, not directly runnable — extract them into API workflows, or import an API workflow instead.",
|
|
283
|
+
wfComfyuiEmpty: "No workflows detected on the ComfyUI server",
|
|
284
|
+
wfExtracted: "extracted",
|
|
285
|
+
wfNotExtracted: "not extracted",
|
|
286
|
+
wfView: "View",
|
|
287
|
+
wfExtract: "Extract",
|
|
288
|
+
wfExtractedNotice: "extracted into the library",
|
|
289
|
+
wfExtractTitle: "Extract runnable workflows",
|
|
290
|
+
wfExtractModeAll: "All as one",
|
|
291
|
+
wfExtractModeAllHint: "Merge every component into one runnable workflow (everything executes)",
|
|
292
|
+
wfExtractModeSplit: "Per component",
|
|
293
|
+
wfExtractModeSplitHint: "One runnable workflow per independent flow (recommended)",
|
|
294
|
+
wfExtractModeMain: "Main flow only",
|
|
295
|
+
wfExtractModeMainHint: "Only the largest component (usually the flow under test)",
|
|
296
|
+
wfExtractRun: "Extract",
|
|
297
|
+
wfExtractEmpty: "No extractable components in this graph",
|
|
298
|
+
wfComponents: "components",
|
|
299
|
+
wfComponentLabel: "Component",
|
|
300
|
+
wfNodes: "nodes",
|
|
301
|
+
wfBypassed: "bypassed nodes",
|
|
302
|
+
wfIsolated: "dangling nodes",
|
|
303
|
+
wfViewBack: "Back",
|
|
304
|
+
wfViewNodes: "Nodes",
|
|
305
|
+
wfViewJson: "JSON preview",
|
|
306
|
+
wfViewEmpty: "No executable nodes in this graph",
|
|
307
|
+
assetAll: "All workflows",
|
|
308
|
+
assetBack: "Back",
|
|
309
|
+
assetEmpty: "Nothing generated yet. Results appear here after workflows run.",
|
|
310
|
+
assetGone: "Source file evicted by ComfyUI",
|
|
311
|
+
queueEmpty: "No tasks right now",
|
|
312
|
+
queueError: "Cannot read the queue (ComfyUI not running?)",
|
|
313
|
+
jobFilterLabel: "History",
|
|
314
|
+
jobFilterAll: "All",
|
|
315
|
+
jobFilterPending: "Pending",
|
|
316
|
+
jobFilterInProgress: "Running",
|
|
317
|
+
jobFilterCompleted: "Completed",
|
|
318
|
+
jobFilterFailed: "Failed",
|
|
319
|
+
jobFilterCancelled: "Cancelled",
|
|
320
|
+
jobActive: "Active",
|
|
321
|
+
jobDelete: "Delete",
|
|
322
|
+
jobInterrupt: "Interrupt",
|
|
323
|
+
jobRerun: "Rerun",
|
|
324
|
+
jobViewAssets: "View assets",
|
|
325
|
+
jobMore: "More actions",
|
|
326
|
+
jobClearQueue: "Clear queue",
|
|
327
|
+
jobClearHistory: "Clear history",
|
|
328
|
+
jobFree: "Free memory",
|
|
329
|
+
jobDuration: "Took",
|
|
330
|
+
jobOurs: "ours"
|
|
331
|
+
};
|
|
332
|
+
const LANG_KEY = "dsh-comfyui.lang";
|
|
333
|
+
/** Current plugin UI language, read from localStorage (default zh). */
|
|
334
|
+
function getLang() {
|
|
335
|
+
try {
|
|
336
|
+
return localStorage.getItem(LANG_KEY) === "en" ? "en" : "zh";
|
|
337
|
+
} catch {
|
|
338
|
+
return "zh";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** Persist the plugin UI language choice. */
|
|
342
|
+
function setLang(lang) {
|
|
343
|
+
try {
|
|
344
|
+
localStorage.setItem(LANG_KEY, lang);
|
|
345
|
+
} catch {}
|
|
346
|
+
}
|
|
347
|
+
const DICTS = {
|
|
348
|
+
zh,
|
|
349
|
+
en
|
|
350
|
+
};
|
|
351
|
+
/** Build a plugin-local translator for one language. Resolves {name}
|
|
352
|
+
* placeholders from the first rest argument (same contract as the DSH
|
|
353
|
+
* locale bind), falling back to the key itself when untranslated. */
|
|
354
|
+
function makeT(lang) {
|
|
355
|
+
const dict = DICTS[lang];
|
|
356
|
+
return (key, ...rest) => {
|
|
357
|
+
const template = dict[key] ?? key;
|
|
358
|
+
const params = rest[0] ?? {};
|
|
359
|
+
if (params !== null && typeof params === "object") return template.replace(/\{(\w+)\}/g, (match, name) => {
|
|
360
|
+
const value = params[name];
|
|
361
|
+
return value === void 0 ? match : String(value);
|
|
362
|
+
});
|
|
363
|
+
return template;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/client/lightbox.tsx
|
|
368
|
+
/**
|
|
369
|
+
* Shared lightbox: opens a generated image full-screen with prev/next
|
|
370
|
+
* navigation, keyboard support, and a download pill. Used by both the panel
|
|
371
|
+
* (assets/queue previews) and the tool card (result images).
|
|
372
|
+
*/
|
|
373
|
+
/** Full-screen media overlay with prev/next navigation. */
|
|
374
|
+
function Lightbox({ t, images, kinds, index, onClose, onIndex }) {
|
|
375
|
+
const count = images.length;
|
|
376
|
+
(0, react.useEffect)(() => {
|
|
377
|
+
const onKey = (event) => {
|
|
378
|
+
if (event.key === "Escape") onClose();
|
|
379
|
+
else if (event.key === "ArrowLeft") onIndex((index - 1 + count) % count);
|
|
380
|
+
else if (event.key === "ArrowRight") onIndex((index + 1) % count);
|
|
381
|
+
};
|
|
382
|
+
document.addEventListener("keydown", onKey);
|
|
383
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
384
|
+
}, [
|
|
385
|
+
index,
|
|
386
|
+
count,
|
|
387
|
+
onClose,
|
|
388
|
+
onIndex
|
|
389
|
+
]);
|
|
390
|
+
const src = images[index];
|
|
391
|
+
if (src === void 0) return null;
|
|
392
|
+
const kind = kinds?.[index] ?? "image";
|
|
393
|
+
const media = kind === "video" ? (0, react.createElement)("video", {
|
|
394
|
+
className: "dsc-lightbox-media",
|
|
395
|
+
src,
|
|
396
|
+
controls: true,
|
|
397
|
+
autoPlay: true
|
|
398
|
+
}) : kind === "audio" ? (0, react.createElement)("audio", {
|
|
399
|
+
className: "dsc-lightbox-media",
|
|
400
|
+
src,
|
|
401
|
+
controls: true,
|
|
402
|
+
autoPlay: true
|
|
403
|
+
}) : kind === "image" ? (0, react.createElement)("img", {
|
|
404
|
+
className: "dsc-lightbox-img",
|
|
405
|
+
src,
|
|
406
|
+
alt: ""
|
|
407
|
+
}) : (0, react.createElement)("div", { className: "dsc-lightbox-media" }, src);
|
|
408
|
+
return (0, react.createElement)("div", {
|
|
409
|
+
className: "dsc-lightbox",
|
|
410
|
+
onClick: onClose
|
|
411
|
+
}, (0, react.createElement)("div", {
|
|
412
|
+
className: "dsc-lightbox-body",
|
|
413
|
+
onClick: (event) => event.stopPropagation()
|
|
414
|
+
}, (0, react.createElement)("button", {
|
|
415
|
+
className: "dsc-lightbox-close",
|
|
416
|
+
"aria-label": t("close"),
|
|
417
|
+
onClick: (event) => {
|
|
418
|
+
event.stopPropagation();
|
|
419
|
+
onClose();
|
|
420
|
+
}
|
|
421
|
+
}, "✕"), count > 1 ? (0, react.createElement)("button", {
|
|
422
|
+
className: "dsc-lightbox-nav dsc-lightbox-nav--prev",
|
|
423
|
+
"aria-label": t("lbPrev"),
|
|
424
|
+
onClick: (event) => {
|
|
425
|
+
event.stopPropagation();
|
|
426
|
+
onIndex((index - 1 + count) % count);
|
|
427
|
+
}
|
|
428
|
+
}, "‹") : null, media, (0, react.createElement)("div", { className: "dsc-lightbox-meta" }, (0, react.createElement)("span", null, `${index + 1} / ${count}`), (0, react.createElement)("a", {
|
|
429
|
+
className: "dsc-lightbox-download",
|
|
430
|
+
href: src,
|
|
431
|
+
download: "",
|
|
432
|
+
target: "_blank",
|
|
433
|
+
rel: "noreferrer"
|
|
434
|
+
}, t("cardDownload"))), count > 1 ? (0, react.createElement)("button", {
|
|
435
|
+
className: "dsc-lightbox-nav dsc-lightbox-nav--next",
|
|
436
|
+
"aria-label": t("lbNext"),
|
|
437
|
+
onClick: (event) => {
|
|
438
|
+
event.stopPropagation();
|
|
439
|
+
onIndex((index + 1) % count);
|
|
440
|
+
}
|
|
441
|
+
}, "›") : null));
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/client/card.tsx
|
|
445
|
+
/**
|
|
446
|
+
* Tool cards for comfyui_run and comfyui_workflow (tool.call.toolview, keys
|
|
447
|
+
* 'comfyui_run' / 'comfyui_workflow'). A pure function of the frozen tool-call
|
|
448
|
+
* block: running calls show a generating state, settled calls render the
|
|
449
|
+
* presentationMeta payload (media wall with size labels, click-to-zoom
|
|
450
|
+
* lightbox, status) that the host threaded into the session log.
|
|
451
|
+
*/
|
|
452
|
+
/** Discriminate the settled result node from a running call. */
|
|
453
|
+
function isResultNode(block) {
|
|
454
|
+
return block.kind === "tool-result";
|
|
455
|
+
}
|
|
456
|
+
function parseArgs(raw) {
|
|
457
|
+
try {
|
|
458
|
+
const parsed = JSON.parse(raw);
|
|
459
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
460
|
+
} catch {
|
|
461
|
+
return {};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function argsSummary(args) {
|
|
465
|
+
const parts = [];
|
|
466
|
+
if (typeof args.template === "string") parts.push(`template=${args.template}`);
|
|
467
|
+
if (args.mode !== void 0 && args.mode !== "sync") parts.push(`mode=${String(args.mode)}`);
|
|
468
|
+
if (parts.length === 0) parts.push("custom workflow");
|
|
469
|
+
return parts.join(" · ");
|
|
470
|
+
}
|
|
471
|
+
function aspectLabel(width, height) {
|
|
472
|
+
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
|
|
473
|
+
const g = gcd(width, height);
|
|
474
|
+
const rw = width / g;
|
|
475
|
+
const rh = height / g;
|
|
476
|
+
return rw <= 32 && rh <= 32 ? `${rw}:${rh}` : "";
|
|
477
|
+
}
|
|
478
|
+
function MediaItem({ item, t, onOpen }) {
|
|
479
|
+
const [size, setSize] = (0, react.useState)(null);
|
|
480
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
481
|
+
const media = item.kind === "video" ? (0, react.createElement)("video", {
|
|
482
|
+
src: item.url,
|
|
483
|
+
controls: true,
|
|
484
|
+
preload: "metadata"
|
|
485
|
+
}) : item.kind === "audio" ? (0, react.createElement)("audio", {
|
|
486
|
+
src: item.url,
|
|
487
|
+
controls: true,
|
|
488
|
+
preload: "metadata"
|
|
489
|
+
}) : item.kind === "image" ? failed ? (0, react.createElement)("span", { className: "dsc-media-other" }, `${item.filename}(${t("cardLoadFailed")})`) : (0, react.createElement)("img", {
|
|
490
|
+
src: item.url,
|
|
491
|
+
alt: item.filename,
|
|
492
|
+
loading: "lazy",
|
|
493
|
+
className: "dsc-media-img dsc-media-img--clickable",
|
|
494
|
+
onClick: onOpen,
|
|
495
|
+
onLoad: (event) => {
|
|
496
|
+
const width = event.target.naturalWidth;
|
|
497
|
+
const height = event.target.naturalHeight;
|
|
498
|
+
if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) setSize({
|
|
499
|
+
width,
|
|
500
|
+
height
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
onError: () => setFailed(true)
|
|
504
|
+
}) : (0, react.createElement)("span", { className: "dsc-media-other" }, item.filename);
|
|
505
|
+
const ratio = size === null ? null : aspectLabel(size.width, size.height);
|
|
506
|
+
return (0, react.createElement)("div", { className: "dsc-media" }, media, (0, react.createElement)("div", { className: "dsc-media-meta" }, size !== null ? (0, react.createElement)("span", { className: "dsc-media-size" }, `${size.width}×${size.height}${ratio !== "" ? ` · ${ratio}` : ""}`) : null, (0, react.createElement)("a", {
|
|
507
|
+
href: item.url,
|
|
508
|
+
download: item.filename,
|
|
509
|
+
target: "_blank",
|
|
510
|
+
rel: "noreferrer"
|
|
511
|
+
}, t("cardDownload"))));
|
|
512
|
+
}
|
|
513
|
+
function ResultCard({ title, result, t }) {
|
|
514
|
+
const failed = result.status === "interrupted";
|
|
515
|
+
const [lightbox, setLightbox] = (0, react.useState)(null);
|
|
516
|
+
const urls = result.media.map((item) => item.url);
|
|
517
|
+
const kinds = result.media.map((item) => item.kind);
|
|
518
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, title !== "" ? (0, react.createElement)("span", { className: "dsc-badge" }, title) : null, (0, react.createElement)("span", { className: failed ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, failed ? t("cardInterrupted") : result.status), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardPrompt")} ${result.promptId}`), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardElapsed")} ${result.elapsedMs} ms`)), (0, react.createElement)("div", { className: "dsc-meta" }, result.summary), result.media.length > 0 ? (0, react.createElement)("div", { className: "dsc-grid" }, result.media.map((item, index) => (0, react.createElement)(MediaItem, {
|
|
519
|
+
key: `${item.node}-${item.index}`,
|
|
520
|
+
item,
|
|
521
|
+
t,
|
|
522
|
+
onOpen: () => setLightbox(index)
|
|
523
|
+
}))) : (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty")), lightbox !== null ? (0, react.createElement)(Lightbox, {
|
|
524
|
+
t,
|
|
525
|
+
images: urls,
|
|
526
|
+
kinds,
|
|
527
|
+
index: lightbox,
|
|
528
|
+
onClose: () => setLightbox(null),
|
|
529
|
+
onIndex: setLightbox
|
|
530
|
+
}) : null);
|
|
531
|
+
}
|
|
532
|
+
function BackgroundCard({ label, promptId, t }) {
|
|
533
|
+
const [result, setResult] = (0, react.useState)(null);
|
|
534
|
+
const [lightbox, setLightbox] = (0, react.useState)(null);
|
|
535
|
+
(0, react.useEffect)(() => {
|
|
536
|
+
let stopped = false;
|
|
537
|
+
let timer;
|
|
538
|
+
const poll = async () => {
|
|
539
|
+
try {
|
|
540
|
+
const data = await (await fetch(`/comfyui/jobs/media?promptId=${encodeURIComponent(promptId)}`, { headers: { accept: "application/json" } })).json();
|
|
541
|
+
if (stopped) return;
|
|
542
|
+
if (data.ok !== true || data.status === void 0) return;
|
|
543
|
+
if (data.status === "completed" || data.status === "failed") {
|
|
544
|
+
setResult({
|
|
545
|
+
status: data.status,
|
|
546
|
+
media: data.media,
|
|
547
|
+
error: data.error
|
|
548
|
+
});
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
timer = window.setTimeout(() => {
|
|
552
|
+
poll();
|
|
553
|
+
}, 3e3);
|
|
554
|
+
} catch {
|
|
555
|
+
timer = window.setTimeout(() => {
|
|
556
|
+
poll();
|
|
557
|
+
}, 5e3);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
poll();
|
|
561
|
+
return () => {
|
|
562
|
+
stopped = true;
|
|
563
|
+
if (timer !== void 0) window.clearTimeout(timer);
|
|
564
|
+
};
|
|
565
|
+
}, [promptId]);
|
|
566
|
+
if (result !== null && (result.status === "completed" || result.status === "failed")) {
|
|
567
|
+
const media = result.media ?? [];
|
|
568
|
+
const urls = media.map((item) => item.url);
|
|
569
|
+
const kinds = media.map((item) => item.kind);
|
|
570
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: result.status === "failed" ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, result.status === "failed" ? t("cardFailed") : t("cardBackgroundDone")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), result.status === "failed" ? (0, react.createElement)("div", { className: "dsc-meta dsc-job-error" }, result.error ?? t("cardFailed")) : media.length > 0 ? (0, react.createElement)("div", { className: "dsc-grid" }, media.map((item, index) => (0, react.createElement)(MediaItem, {
|
|
571
|
+
key: `${item.node}-${item.index}`,
|
|
572
|
+
item,
|
|
573
|
+
t,
|
|
574
|
+
onOpen: () => setLightbox(index)
|
|
575
|
+
}))) : (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty")), lightbox !== null ? (0, react.createElement)(Lightbox, {
|
|
576
|
+
t,
|
|
577
|
+
images: urls,
|
|
578
|
+
kinds,
|
|
579
|
+
index: lightbox,
|
|
580
|
+
onClose: () => setLightbox(null),
|
|
581
|
+
onIndex: setLightbox
|
|
582
|
+
}) : null);
|
|
583
|
+
}
|
|
584
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardBackground")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), (0, react.createElement)("div", { className: "dsc-meta" }, t("cardCollect")));
|
|
585
|
+
}
|
|
586
|
+
function SettledCard({ block, t }) {
|
|
587
|
+
if (block.isError === true) return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge dsc-badge--err" }, t("cardFailed"))), (0, react.createElement)("div", { className: "dsc-meta" }, block.error?.name ?? "error"));
|
|
588
|
+
const meta = block.meta;
|
|
589
|
+
if (meta === void 0 || typeof meta !== "object") return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("span", { className: "dsc-badge" }, block.call?.name ?? "comfyui"));
|
|
590
|
+
if ("action" in meta) {
|
|
591
|
+
if (meta.action === "run") {
|
|
592
|
+
if (meta.background !== void 0) return (0, react.createElement)(BackgroundCard, {
|
|
593
|
+
label: meta.workflowName ?? "",
|
|
594
|
+
promptId: meta.background.promptId,
|
|
595
|
+
t
|
|
596
|
+
});
|
|
597
|
+
if (meta.result !== void 0) return (0, react.createElement)(ResultCard, {
|
|
598
|
+
title: meta.workflowName ?? "",
|
|
599
|
+
result: meta.result,
|
|
600
|
+
t
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
if (meta.action === "list") {
|
|
604
|
+
const runs = meta.workflows?.length ?? 0;
|
|
605
|
+
const graphs = meta.comfyuiWorkflows?.length ?? 0;
|
|
606
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, "comfyui_workflow")), (0, react.createElement)("div", { className: "dsc-meta" }, t("cardListed", {
|
|
607
|
+
runs,
|
|
608
|
+
graphs
|
|
609
|
+
})));
|
|
610
|
+
}
|
|
611
|
+
if (meta.action === "get") return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, "comfyui_workflow")), (0, react.createElement)("div", { className: "dsc-meta" }, meta.name ?? meta.id ?? "get"));
|
|
612
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("span", { className: "dsc-badge" }, "comfyui_workflow"));
|
|
613
|
+
}
|
|
614
|
+
if (meta.kind === "background") return (0, react.createElement)(BackgroundCard, {
|
|
615
|
+
label: meta.label,
|
|
616
|
+
promptId: meta.promptId,
|
|
617
|
+
t
|
|
618
|
+
});
|
|
619
|
+
return (0, react.createElement)(ResultCard, {
|
|
620
|
+
title: "",
|
|
621
|
+
result: meta,
|
|
622
|
+
t
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/** The card component: picks running vs settled rendering from the block. */
|
|
626
|
+
function ComfyUICard({ t, block }) {
|
|
627
|
+
if (isResultNode(block)) return (0, react.createElement)(SettledCard, {
|
|
628
|
+
block,
|
|
629
|
+
t
|
|
630
|
+
});
|
|
631
|
+
const args = parseArgs(block.argsRaw);
|
|
632
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardGenerating")), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardWorkflow")} ${argsSummary(args)} · ${t("cardMode")} ${String(args.mode ?? "sync")}`)));
|
|
633
|
+
}
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/client/api.ts
|
|
636
|
+
/** Same-origin JSON helpers shared by the settings page and the panel. */
|
|
637
|
+
async function getJson(url) {
|
|
638
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
639
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
640
|
+
return await response.json();
|
|
641
|
+
}
|
|
642
|
+
async function postJson(url, body) {
|
|
643
|
+
const response = await fetch(url, {
|
|
644
|
+
method: "POST",
|
|
645
|
+
headers: { "content-type": "application/json" },
|
|
646
|
+
body: JSON.stringify(body)
|
|
647
|
+
});
|
|
648
|
+
if (!response.ok) {
|
|
649
|
+
const payload = await response.json().catch(() => ({}));
|
|
650
|
+
throw new Error(payload.error ?? `HTTP ${response.status}`);
|
|
651
|
+
}
|
|
652
|
+
return response.json();
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/client/settings.tsx
|
|
656
|
+
/**
|
|
657
|
+
* The ComfyUI settings page (settings.section, id 'comfyui'). Reads the
|
|
658
|
+
* redacted host config over /comfyui/config, persists edits through the same
|
|
659
|
+
* route, and probes the server over /comfyui/test.
|
|
660
|
+
*/
|
|
661
|
+
/** The ComfyUI settings page component. */
|
|
662
|
+
function ComfyUISettings({ t }) {
|
|
663
|
+
const [config, setConfig] = (0, react.useState)(null);
|
|
664
|
+
const [baseUrl, setBaseUrl] = (0, react.useState)("");
|
|
665
|
+
const [apiKeyEnv, setApiKeyEnv] = (0, react.useState)("");
|
|
666
|
+
const [mediaHost, setMediaHost] = (0, react.useState)("");
|
|
667
|
+
const [loadError, setLoadError] = (0, react.useState)(null);
|
|
668
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
669
|
+
const [saveState, setSaveState] = (0, react.useState)("idle");
|
|
670
|
+
const [testing, setTesting] = (0, react.useState)(false);
|
|
671
|
+
const [testResult, setTestResult] = (0, react.useState)(null);
|
|
672
|
+
const [lang, setLangState] = (0, react.useState)(getLang());
|
|
673
|
+
(0, react.useEffect)(() => {
|
|
674
|
+
let cancelled = false;
|
|
675
|
+
getJson("/comfyui/config").then((view) => {
|
|
676
|
+
if (cancelled) return;
|
|
677
|
+
setConfig(view);
|
|
678
|
+
setBaseUrl(view.baseUrl);
|
|
679
|
+
setApiKeyEnv(view.apiKeyEnv);
|
|
680
|
+
setMediaHost(view.mediaHost ?? "");
|
|
681
|
+
}).catch((error) => {
|
|
682
|
+
if (cancelled) return;
|
|
683
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
684
|
+
});
|
|
685
|
+
return () => {
|
|
686
|
+
cancelled = true;
|
|
687
|
+
};
|
|
688
|
+
}, []);
|
|
689
|
+
const save = async () => {
|
|
690
|
+
setSaving(true);
|
|
691
|
+
setSaveState("idle");
|
|
692
|
+
try {
|
|
693
|
+
const payload = await postJson("/comfyui/config", { patch: {
|
|
694
|
+
baseUrl,
|
|
695
|
+
apiKeyEnv,
|
|
696
|
+
mediaHost
|
|
697
|
+
} });
|
|
698
|
+
if (payload.config !== void 0) {
|
|
699
|
+
setConfig(payload.config);
|
|
700
|
+
setBaseUrl(payload.config.baseUrl);
|
|
701
|
+
setApiKeyEnv(payload.config.apiKeyEnv);
|
|
702
|
+
setMediaHost(payload.config.mediaHost ?? "");
|
|
703
|
+
}
|
|
704
|
+
setSaveState("saved");
|
|
705
|
+
} catch (error) {
|
|
706
|
+
setSaveState("failed");
|
|
707
|
+
setTestResult({
|
|
708
|
+
ok: false,
|
|
709
|
+
text: error instanceof Error ? error.message : String(error)
|
|
710
|
+
});
|
|
711
|
+
} finally {
|
|
712
|
+
setSaving(false);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
const test = async () => {
|
|
716
|
+
setTesting(true);
|
|
717
|
+
setTestResult(null);
|
|
718
|
+
try {
|
|
719
|
+
const payload = await postJson("/comfyui/test", {});
|
|
720
|
+
if (payload.ok === true) setTestResult({
|
|
721
|
+
ok: true,
|
|
722
|
+
text: t("testOk", {
|
|
723
|
+
version: payload.version ?? "unknown",
|
|
724
|
+
ms: payload.latencyMs ?? 0
|
|
725
|
+
})
|
|
726
|
+
});
|
|
727
|
+
else setTestResult({
|
|
728
|
+
ok: false,
|
|
729
|
+
text: t("testFail", { message: payload.error ?? "unknown error" })
|
|
730
|
+
});
|
|
731
|
+
} catch (error) {
|
|
732
|
+
setTestResult({
|
|
733
|
+
ok: false,
|
|
734
|
+
text: t("testFail", { message: error instanceof Error ? error.message : String(error) })
|
|
735
|
+
});
|
|
736
|
+
} finally {
|
|
737
|
+
setTesting(false);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
if (loadError !== null) return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-meta" }, `${t("saveFailed")}: ${loadError}`));
|
|
741
|
+
if (config === null) return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-meta" }, "…"));
|
|
742
|
+
return (0, react.createElement)("div", { className: "dsc-form" }, (0, react.createElement)("div", { className: "dsc-meta" }, t("settingsDesc")), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("language")), (0, react.createElement)("select", {
|
|
743
|
+
className: "dsc-input",
|
|
744
|
+
value: lang,
|
|
745
|
+
onChange: (event) => {
|
|
746
|
+
const next = event.target.value === "en" ? "en" : "zh";
|
|
747
|
+
setLangState(next);
|
|
748
|
+
setLang(next);
|
|
749
|
+
window.location.reload();
|
|
750
|
+
}
|
|
751
|
+
}, (0, react.createElement)("option", { value: "zh" }, "中文"), (0, react.createElement)("option", { value: "en" }, "English")), (0, react.createElement)("div", { className: "dsc-hint" }, t("languageHint"))), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("baseUrl")), (0, react.createElement)("input", {
|
|
752
|
+
value: baseUrl,
|
|
753
|
+
placeholder: "http://127.0.0.1:8188",
|
|
754
|
+
onChange: (event) => {
|
|
755
|
+
setBaseUrl(event.target.value);
|
|
756
|
+
setSaveState("idle");
|
|
757
|
+
}
|
|
758
|
+
}), (0, react.createElement)("div", { className: "dsc-hint" }, t("baseUrlHint"))), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("apiKeyEnv")), (0, react.createElement)("input", {
|
|
759
|
+
value: apiKeyEnv,
|
|
760
|
+
placeholder: "COMFYUI_API_KEY",
|
|
761
|
+
onChange: (event) => {
|
|
762
|
+
setApiKeyEnv(event.target.value);
|
|
763
|
+
setSaveState("idle");
|
|
764
|
+
}
|
|
765
|
+
}), (0, react.createElement)("div", { className: "dsc-hint" }, `${t("apiKeyEnvHint")} — ${config.hasApiKey ? t("hasApiKey") : t("noApiKey")}`)), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("mediaHost")), (0, react.createElement)("input", {
|
|
766
|
+
value: mediaHost,
|
|
767
|
+
placeholder: "http://192.168.1.5:3080",
|
|
768
|
+
onChange: (event) => {
|
|
769
|
+
setMediaHost(event.target.value);
|
|
770
|
+
setSaveState("idle");
|
|
771
|
+
}
|
|
772
|
+
}), (0, react.createElement)("div", { className: "dsc-hint" }, t("mediaHostHint"))), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
773
|
+
className: "dsc-btn",
|
|
774
|
+
disabled: saving || !config.writable,
|
|
775
|
+
onClick: () => void save()
|
|
776
|
+
}, t("save")), (0, react.createElement)("button", {
|
|
777
|
+
className: "dsc-btn",
|
|
778
|
+
disabled: testing,
|
|
779
|
+
onClick: () => void test()
|
|
780
|
+
}, testing ? t("testing") : t("test")), saveState === "saved" ? (0, react.createElement)("span", { className: "dsc-note" }, t("saved")) : null, saveState === "failed" ? (0, react.createElement)("span", { className: "dsc-note" }, t("saveFailed")) : null), testResult !== null ? (0, react.createElement)("div", {
|
|
781
|
+
className: testResult.ok ? "dsc-note" : "dsc-note",
|
|
782
|
+
style: testResult.ok ? { color: "#22c55e" } : { color: "#ef4444" }
|
|
783
|
+
}, testResult.text) : null, config.writable ? null : (0, react.createElement)("div", { className: "dsc-hint" }, t("configNotWritable")));
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/client/panel-store.ts
|
|
787
|
+
/**
|
|
788
|
+
* Shared open/close/tab state for the ComfyUI panel. The overlay occupant
|
|
789
|
+
* and the session-header trigger are separate registrations in one bundle,
|
|
790
|
+
* so they coordinate through this module-level store plus useSyncExternalStore.
|
|
791
|
+
*/
|
|
792
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
793
|
+
let open = false;
|
|
794
|
+
let tab = "workflows";
|
|
795
|
+
let assetFilter = "";
|
|
796
|
+
function emit() {
|
|
797
|
+
for (const listener of listeners) listener();
|
|
798
|
+
}
|
|
799
|
+
const panelStore = {
|
|
800
|
+
isOpen: () => open,
|
|
801
|
+
toggle: () => {
|
|
802
|
+
open = !open;
|
|
803
|
+
emit();
|
|
804
|
+
},
|
|
805
|
+
open: () => {
|
|
806
|
+
if (!open) {
|
|
807
|
+
open = true;
|
|
808
|
+
emit();
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
close: () => {
|
|
812
|
+
if (open) {
|
|
813
|
+
open = false;
|
|
814
|
+
emit();
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
getTab: () => tab,
|
|
818
|
+
setTab: (next) => {
|
|
819
|
+
if (tab !== next) {
|
|
820
|
+
tab = next;
|
|
821
|
+
emit();
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
getAssetFilter: () => assetFilter,
|
|
825
|
+
setAssetFilter: (name) => {
|
|
826
|
+
if (assetFilter !== name) {
|
|
827
|
+
assetFilter = name;
|
|
828
|
+
emit();
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
subscribe(listener) {
|
|
832
|
+
listeners.add(listener);
|
|
833
|
+
return () => {
|
|
834
|
+
listeners.delete(listener);
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
/** React hook reading the panel open state. */
|
|
839
|
+
function usePanelOpen() {
|
|
840
|
+
return (0, react.useSyncExternalStore)(panelStore.subscribe, panelStore.isOpen, panelStore.isOpen);
|
|
841
|
+
}
|
|
842
|
+
/** React hook reading the active panel tab. */
|
|
843
|
+
function usePanelTab() {
|
|
844
|
+
return (0, react.useSyncExternalStore)(panelStore.subscribe, panelStore.getTab, panelStore.getTab);
|
|
845
|
+
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/client/trigger.tsx
|
|
848
|
+
/**
|
|
849
|
+
* The session-header trigger (conversation.session.header.actions, id
|
|
850
|
+
* 'comfyui'): a labelled button that toggles the ComfyUI panel.
|
|
851
|
+
*/
|
|
852
|
+
/**
|
|
853
|
+
* ComfyUI brand glyph (mono), inlined from @lobehub/icons-static-svg
|
|
854
|
+
* (lobehub.com/icons). currentColor fill keeps it theme-aware.
|
|
855
|
+
*/
|
|
856
|
+
function ComfyUIIcon() {
|
|
857
|
+
return (0, react.createElement)("svg", {
|
|
858
|
+
viewBox: "0 0 24 24",
|
|
859
|
+
width: "1em",
|
|
860
|
+
height: "1em",
|
|
861
|
+
fill: "currentColor",
|
|
862
|
+
fillRule: "evenodd",
|
|
863
|
+
style: {
|
|
864
|
+
flex: "none",
|
|
865
|
+
lineHeight: 1
|
|
866
|
+
},
|
|
867
|
+
"aria-hidden": true
|
|
868
|
+
}, (0, react.createElement)("path", { d: "M5.485 23.76c-.568 0-1.026-.207-1.325-.598-.307-.402-.387-.964-.22-1.54l.672-2.315a.605.605 0 00-.1-.536.622.622 0 00-.494-.243H2.085c-.568 0-1.026-.207-1.325-.598-.307-.403-.387-.964-.22-1.54l2.31-7.917.255-.87c.343-1.18 1.592-2.14 2.786-2.14h2.313c.276 0 .519-.18.595-.442l.764-2.633C9.906 1.208 11.155.249 12.35.249l4.945-.008h3.62c.568 0 1.027.206 1.325.597.307.402.387.964.22 1.54l-1.035 3.566c-.343 1.178-1.593 2.137-2.787 2.137l-4.956.01H11.37a.618.618 0 00-.594.441l-1.928 6.604a.605.605 0 00.1.537c.118.153.3.243.495.243l3.275-.006h3.61c.568 0 1.026.206 1.325.598.307.402.387.964.22 1.54l-1.036 3.565c-.342 1.179-1.592 2.138-2.786 2.138l-4.957.01h-3.61z" }));
|
|
869
|
+
}
|
|
870
|
+
/** Header-action button toggling the ComfyUI panel. */
|
|
871
|
+
function ComfyUITrigger({ t }) {
|
|
872
|
+
const open = usePanelOpen();
|
|
873
|
+
return (0, react.createElement)("button", {
|
|
874
|
+
className: "dsc-trigger",
|
|
875
|
+
title: t("panelTitle"),
|
|
876
|
+
"aria-label": t("panelTitle"),
|
|
877
|
+
"aria-pressed": open,
|
|
878
|
+
onClick: () => panelStore.toggle()
|
|
879
|
+
}, (0, react.createElement)("span", { className: "dsc-trigger-glyph" }, (0, react.createElement)(ComfyUIIcon)), (0, react.createElement)("span", null, t("panelTitle")));
|
|
880
|
+
}
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/client/panel.tsx
|
|
883
|
+
/**
|
|
884
|
+
* The ComfyUI panel (shell.overlay, id 'comfyui.panel'): a floating panel
|
|
885
|
+
* with three tabs — workflow library management, generated asset preview,
|
|
886
|
+
* and the live ComfyUI queue. Draggable by its header and resizable via the
|
|
887
|
+
* corner handle; geometry persists in localStorage. Renders null while closed
|
|
888
|
+
* (the overlay layer is click-through, so nothing blocks the app underneath).
|
|
889
|
+
*/
|
|
890
|
+
const GEOM_KEY = "dsh-comfyui-panel-geom";
|
|
891
|
+
function loadGeom() {
|
|
892
|
+
try {
|
|
893
|
+
const raw = localStorage.getItem(GEOM_KEY);
|
|
894
|
+
if (raw === null) return {};
|
|
895
|
+
const parsed = JSON.parse(raw);
|
|
896
|
+
const geom = {};
|
|
897
|
+
if (typeof parsed.x === "number") geom.x = parsed.x;
|
|
898
|
+
if (typeof parsed.y === "number") geom.y = parsed.y;
|
|
899
|
+
if (typeof parsed.width === "number" && parsed.width >= 300) geom.width = parsed.width;
|
|
900
|
+
if (typeof parsed.height === "number" && parsed.height >= 200) geom.height = parsed.height;
|
|
901
|
+
return geom;
|
|
902
|
+
} catch {
|
|
903
|
+
return {};
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function saveGeom(geom) {
|
|
907
|
+
try {
|
|
908
|
+
localStorage.setItem(GEOM_KEY, JSON.stringify(geom));
|
|
909
|
+
} catch {}
|
|
910
|
+
}
|
|
911
|
+
/** Preset workflow classification tags (users can add custom ones). Stored as
|
|
912
|
+
* plain text, so the tag identity is the label itself. */
|
|
913
|
+
const PRESET_TAGS = [
|
|
914
|
+
"图生图",
|
|
915
|
+
"文生图",
|
|
916
|
+
"文生视频",
|
|
917
|
+
"图生视频",
|
|
918
|
+
"参考生视频",
|
|
919
|
+
"文生音频",
|
|
920
|
+
"参考生音频"
|
|
921
|
+
];
|
|
922
|
+
const JOB_FILTERS = [
|
|
923
|
+
{
|
|
924
|
+
key: "all",
|
|
925
|
+
label: "jobFilterAll"
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
key: "completed",
|
|
929
|
+
label: "jobFilterCompleted"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
key: "failed",
|
|
933
|
+
label: "jobFilterFailed"
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
key: "cancelled",
|
|
937
|
+
label: "jobFilterCancelled"
|
|
938
|
+
}
|
|
939
|
+
];
|
|
940
|
+
function shortId(promptId) {
|
|
941
|
+
return promptId.length > 10 ? `${promptId.slice(0, 10)}…` : promptId;
|
|
942
|
+
}
|
|
943
|
+
/** Open a job's preview image URL (via the media proxy) when it has one. */
|
|
944
|
+
function previewUrlOf(job) {
|
|
945
|
+
const preview = job.previewOutput;
|
|
946
|
+
if (preview === void 0 || preview === null || preview.filename === void 0 || preview.filename === null) return null;
|
|
947
|
+
return `/comfyui/media?file=${encodeURIComponent(preview.filename)}&subfolder=${encodeURIComponent(preview.subfolder ?? "")}&type=${encodeURIComponent(preview.type ?? "output")}`;
|
|
948
|
+
}
|
|
949
|
+
/** Opens a generated image in a large overlay with prev/next navigation. */
|
|
950
|
+
function newParamId() {
|
|
951
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
952
|
+
return `p${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
953
|
+
}
|
|
954
|
+
/** Parse the draft workflow into a plain object (empty on invalid JSON). */
|
|
955
|
+
function parseDraftWorkflow(workflow) {
|
|
956
|
+
if (typeof workflow !== "string") return workflow;
|
|
957
|
+
try {
|
|
958
|
+
const parsed = JSON.parse(workflow);
|
|
959
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
960
|
+
} catch {
|
|
961
|
+
return {};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Custom dropdown for loader-node file lists: each option shows a small
|
|
966
|
+
* server-side thumbnail (images only; <select> cannot render images).
|
|
967
|
+
*/
|
|
968
|
+
function UploadSelect(props) {
|
|
969
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
970
|
+
const [live, setLive] = (0, react.useState)(null);
|
|
971
|
+
const ref = (0, react.useRef)(null);
|
|
972
|
+
const refreshRef = (0, react.useRef)(props.onRefresh);
|
|
973
|
+
refreshRef.current = props.onRefresh;
|
|
974
|
+
(0, react.useEffect)(() => {
|
|
975
|
+
if (!open) return;
|
|
976
|
+
const onDoc = (event) => {
|
|
977
|
+
if (ref.current !== null && !ref.current.contains(event.target)) setOpen(false);
|
|
978
|
+
};
|
|
979
|
+
document.addEventListener("mousedown", onDoc);
|
|
980
|
+
return () => document.removeEventListener("mousedown", onDoc);
|
|
981
|
+
}, [open]);
|
|
982
|
+
(0, react.useEffect)(() => {
|
|
983
|
+
if (!open) return;
|
|
984
|
+
const fn = refreshRef.current;
|
|
985
|
+
if (fn === void 0) return;
|
|
986
|
+
let cancelled = false;
|
|
987
|
+
fn().then((latest) => {
|
|
988
|
+
if (!cancelled && latest !== void 0) setLive(latest);
|
|
989
|
+
});
|
|
990
|
+
return () => {
|
|
991
|
+
cancelled = true;
|
|
992
|
+
};
|
|
993
|
+
}, [open]);
|
|
994
|
+
const options = live ?? props.options;
|
|
995
|
+
const thumbUrl = (name) => props.kind === "image" ? `/comfyui/media?file=${encodeURIComponent(String(name))}&type=input` : null;
|
|
996
|
+
const current = options.some((option) => String(option) === props.value) ? props.value : String(props.value);
|
|
997
|
+
return (0, react.createElement)("div", {
|
|
998
|
+
ref,
|
|
999
|
+
className: "dsc-upload-select"
|
|
1000
|
+
}, (0, react.createElement)("button", {
|
|
1001
|
+
className: "dsc-upload-select-btn",
|
|
1002
|
+
onClick: () => setOpen(!open),
|
|
1003
|
+
title: String(current)
|
|
1004
|
+
}, thumbUrl(props.value) !== null ? (0, react.createElement)("img", {
|
|
1005
|
+
className: "dsc-upload-select-thumb",
|
|
1006
|
+
src: thumbUrl(props.value) ?? "",
|
|
1007
|
+
alt: "",
|
|
1008
|
+
loading: "lazy"
|
|
1009
|
+
}) : null, (0, react.createElement)("span", { className: "dsc-upload-select-name" }, String(current))), open ? (0, react.createElement)("div", { className: "dsc-upload-select-pop" }, options.map((option) => {
|
|
1010
|
+
const name = String(option);
|
|
1011
|
+
return (0, react.createElement)("div", {
|
|
1012
|
+
key: name,
|
|
1013
|
+
className: `dsc-upload-select-item${name === props.value ? " dsc-upload-select-item--active" : ""}`,
|
|
1014
|
+
onClick: () => {
|
|
1015
|
+
props.onChange(name);
|
|
1016
|
+
setOpen(false);
|
|
1017
|
+
}
|
|
1018
|
+
}, thumbUrl(option) !== null ? (0, react.createElement)("img", {
|
|
1019
|
+
className: "dsc-upload-select-thumb",
|
|
1020
|
+
src: thumbUrl(option) ?? "",
|
|
1021
|
+
alt: "",
|
|
1022
|
+
loading: "lazy"
|
|
1023
|
+
}) : null, (0, react.createElement)("span", { className: "dsc-upload-select-name" }, name));
|
|
1024
|
+
})) : null);
|
|
1025
|
+
}
|
|
1026
|
+
/** Merge a freshly fetched option list with the previous one (latest first,
|
|
1027
|
+
* deduped) so the dropdown never drops options mid-session. */
|
|
1028
|
+
function mergeOptions(prev, latest) {
|
|
1029
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1030
|
+
const out = [];
|
|
1031
|
+
for (const item of [...latest, ...prev]) {
|
|
1032
|
+
const key = String(item);
|
|
1033
|
+
if (!seen.has(key)) {
|
|
1034
|
+
seen.add(key);
|
|
1035
|
+
out.push(item);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return out;
|
|
1039
|
+
}
|
|
1040
|
+
/** Copy text to the clipboard; falls back to a hidden textarea when the
|
|
1041
|
+
* clipboard API is unavailable (non-secure LAN http). */
|
|
1042
|
+
function copyText(text) {
|
|
1043
|
+
if (navigator.clipboard !== void 0) return navigator.clipboard.writeText(text).then(() => true).catch(() => copyViaTextarea(text));
|
|
1044
|
+
return Promise.resolve(copyViaTextarea(text));
|
|
1045
|
+
}
|
|
1046
|
+
function copyViaTextarea(text) {
|
|
1047
|
+
const textarea = document.createElement("textarea");
|
|
1048
|
+
textarea.value = text;
|
|
1049
|
+
textarea.style.position = "fixed";
|
|
1050
|
+
textarea.style.opacity = "0";
|
|
1051
|
+
document.body.appendChild(textarea);
|
|
1052
|
+
textarea.select();
|
|
1053
|
+
let ok = false;
|
|
1054
|
+
try {
|
|
1055
|
+
ok = document.execCommand("copy");
|
|
1056
|
+
} catch {
|
|
1057
|
+
ok = false;
|
|
1058
|
+
}
|
|
1059
|
+
document.body.removeChild(textarea);
|
|
1060
|
+
return ok;
|
|
1061
|
+
}
|
|
1062
|
+
/** Read a file's pixel size in the browser (image only) and report it to the
|
|
1063
|
+
* plugin so the workflow output size can default to the source image. Returns
|
|
1064
|
+
* the size text for display, or undefined when unreadable. */
|
|
1065
|
+
async function recordMediaSize(file, name) {
|
|
1066
|
+
try {
|
|
1067
|
+
const bitmap = await createImageBitmap(file);
|
|
1068
|
+
const width = bitmap.width;
|
|
1069
|
+
const height = bitmap.height;
|
|
1070
|
+
bitmap.close();
|
|
1071
|
+
await fetch("/comfyui/media-size", {
|
|
1072
|
+
method: "POST",
|
|
1073
|
+
headers: { "Content-Type": "application/json" },
|
|
1074
|
+
body: JSON.stringify({
|
|
1075
|
+
name,
|
|
1076
|
+
width,
|
|
1077
|
+
height
|
|
1078
|
+
})
|
|
1079
|
+
}).catch(() => void 0);
|
|
1080
|
+
return `${width}×${height}`;
|
|
1081
|
+
} catch {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/** SHA-256 hex digest (pure JS — crypto.subtle needs a secure context, which a
|
|
1086
|
+
* LAN http origin is not). Uses the standard padding/compression; the input is
|
|
1087
|
+
* any byte array. */
|
|
1088
|
+
function sha256Hex(data) {
|
|
1089
|
+
const K = new Uint32Array([
|
|
1090
|
+
1116352408,
|
|
1091
|
+
1899447441,
|
|
1092
|
+
3049323471,
|
|
1093
|
+
3921009573,
|
|
1094
|
+
961987163,
|
|
1095
|
+
1508970993,
|
|
1096
|
+
2453635748,
|
|
1097
|
+
2870763221,
|
|
1098
|
+
3624381080,
|
|
1099
|
+
310598401,
|
|
1100
|
+
607225278,
|
|
1101
|
+
1426881987,
|
|
1102
|
+
1925078388,
|
|
1103
|
+
2162078206,
|
|
1104
|
+
2614888103,
|
|
1105
|
+
3248222580,
|
|
1106
|
+
3835390401,
|
|
1107
|
+
4022224774,
|
|
1108
|
+
264347078,
|
|
1109
|
+
604807628,
|
|
1110
|
+
770255983,
|
|
1111
|
+
1249150122,
|
|
1112
|
+
1555081692,
|
|
1113
|
+
1996064986,
|
|
1114
|
+
2554220882,
|
|
1115
|
+
2821834349,
|
|
1116
|
+
2952996808,
|
|
1117
|
+
3210313671,
|
|
1118
|
+
3336571891,
|
|
1119
|
+
3584528711,
|
|
1120
|
+
113926993,
|
|
1121
|
+
338241895,
|
|
1122
|
+
666307205,
|
|
1123
|
+
773529912,
|
|
1124
|
+
1294757372,
|
|
1125
|
+
1396182291,
|
|
1126
|
+
1695183700,
|
|
1127
|
+
1986661051,
|
|
1128
|
+
2177026350,
|
|
1129
|
+
2456956037,
|
|
1130
|
+
2730485921,
|
|
1131
|
+
2820302411,
|
|
1132
|
+
3259730800,
|
|
1133
|
+
3345764771,
|
|
1134
|
+
3516065817,
|
|
1135
|
+
3600352804,
|
|
1136
|
+
4094571909,
|
|
1137
|
+
275423344,
|
|
1138
|
+
430227734,
|
|
1139
|
+
506948616,
|
|
1140
|
+
659060556,
|
|
1141
|
+
883997877,
|
|
1142
|
+
958139571,
|
|
1143
|
+
1322822218,
|
|
1144
|
+
1537002063,
|
|
1145
|
+
1747873779,
|
|
1146
|
+
1955562222,
|
|
1147
|
+
2024104815,
|
|
1148
|
+
2227730452,
|
|
1149
|
+
2361852424,
|
|
1150
|
+
2428436474,
|
|
1151
|
+
2756734187,
|
|
1152
|
+
3204031479,
|
|
1153
|
+
3329325298
|
|
1154
|
+
]);
|
|
1155
|
+
const H = new Uint32Array([
|
|
1156
|
+
1779033703,
|
|
1157
|
+
3144134277,
|
|
1158
|
+
1013904242,
|
|
1159
|
+
2773480762,
|
|
1160
|
+
1359893119,
|
|
1161
|
+
2600822924,
|
|
1162
|
+
528734635,
|
|
1163
|
+
1541459225
|
|
1164
|
+
]);
|
|
1165
|
+
const len = data.length;
|
|
1166
|
+
const total = len + 1 + (56 - (len + 1) % 64 + 64) % 64 + 8;
|
|
1167
|
+
const msg = new Uint8Array(total);
|
|
1168
|
+
msg.set(data);
|
|
1169
|
+
msg[len] = 128;
|
|
1170
|
+
const bitLenLo = len * 8 >>> 0;
|
|
1171
|
+
const bitLenHi = Math.floor(len / 536870912);
|
|
1172
|
+
const dv = new DataView(msg.buffer);
|
|
1173
|
+
dv.setUint32(total - 8, bitLenHi);
|
|
1174
|
+
dv.setUint32(total - 4, bitLenLo);
|
|
1175
|
+
const w = /* @__PURE__ */ new Uint32Array(64);
|
|
1176
|
+
for (let offset = 0; offset < total; offset += 64) {
|
|
1177
|
+
for (let i = 0; i < 16; i++) w[i] = dv.getUint32(offset + i * 4);
|
|
1178
|
+
for (let i = 16; i < 64; i++) {
|
|
1179
|
+
const s0 = (w[i - 15] >>> 7 | w[i - 15] << 25) ^ (w[i - 15] >>> 18 | w[i - 15] << 14) ^ w[i - 15] >>> 3;
|
|
1180
|
+
const s1 = (w[i - 2] >>> 17 | w[i - 2] << 15) ^ (w[i - 2] >>> 19 | w[i - 2] << 13) ^ w[i - 2] >>> 10;
|
|
1181
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
1182
|
+
}
|
|
1183
|
+
let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
|
|
1184
|
+
for (let i = 0; i < 64; i++) {
|
|
1185
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
1186
|
+
const ch = e & f ^ ~e & g;
|
|
1187
|
+
const temp1 = h + S1 + ch + K[i] + w[i] >>> 0;
|
|
1188
|
+
const temp2 = ((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
1189
|
+
h = g;
|
|
1190
|
+
g = f;
|
|
1191
|
+
f = e;
|
|
1192
|
+
e = d + temp1 >>> 0;
|
|
1193
|
+
d = c;
|
|
1194
|
+
c = b;
|
|
1195
|
+
b = a;
|
|
1196
|
+
a = temp1 + temp2 >>> 0;
|
|
1197
|
+
}
|
|
1198
|
+
H[0] = H[0] + a >>> 0;
|
|
1199
|
+
H[1] = H[1] + b >>> 0;
|
|
1200
|
+
H[2] = H[2] + c >>> 0;
|
|
1201
|
+
H[3] = H[3] + d >>> 0;
|
|
1202
|
+
H[4] = H[4] + e >>> 0;
|
|
1203
|
+
H[5] = H[5] + f >>> 0;
|
|
1204
|
+
H[6] = H[6] + g >>> 0;
|
|
1205
|
+
H[7] = H[7] + h >>> 0;
|
|
1206
|
+
}
|
|
1207
|
+
let out = "";
|
|
1208
|
+
for (let i = 0; i < 8; i++) out += H[i].toString(16).padStart(8, "0");
|
|
1209
|
+
return out;
|
|
1210
|
+
}
|
|
1211
|
+
/** `image.png` + hash → `image_3f9a2b1c0d.png`: keeps the name readable while
|
|
1212
|
+
* making it content-unique, so re-uploading the same file reuses the name. */
|
|
1213
|
+
function renameWithHash(original, hash) {
|
|
1214
|
+
const short = hash.slice(0, 10);
|
|
1215
|
+
const dot = original.lastIndexOf(".");
|
|
1216
|
+
return `${dot > 0 ? original.slice(0, dot) : original}_${short}${dot > 0 ? original.slice(dot) : ""}`;
|
|
1217
|
+
}
|
|
1218
|
+
/** Upload a file with content-hash naming and dedup: same bytes → same file
|
|
1219
|
+
* name, and (for root uploads) the existing file is reused instead of creating
|
|
1220
|
+
* a duplicate. Returns the file name on the ComfyUI server. */
|
|
1221
|
+
async function uploadDedup(file, subfolder) {
|
|
1222
|
+
const hash = sha256Hex(new Uint8Array(await file.arrayBuffer()));
|
|
1223
|
+
const dedup = subfolder === void 0 || subfolder === "";
|
|
1224
|
+
if (dedup) {
|
|
1225
|
+
const lookup = await fetch("/comfyui/media-lookup", {
|
|
1226
|
+
method: "POST",
|
|
1227
|
+
headers: { "Content-Type": "application/json" },
|
|
1228
|
+
body: JSON.stringify({ hash })
|
|
1229
|
+
}).then((response) => response.json()).catch(() => void 0);
|
|
1230
|
+
if (lookup !== void 0 && lookup.ok === true && lookup.found === true && typeof lookup.name === "string") return lookup.name;
|
|
1231
|
+
}
|
|
1232
|
+
const renamed = new File([file], renameWithHash(file.name, hash), { type: file.type });
|
|
1233
|
+
const form = new FormData();
|
|
1234
|
+
form.append("image", renamed);
|
|
1235
|
+
if (!dedup) form.append("subfolder", subfolder);
|
|
1236
|
+
const data = await (await fetch("/comfyui/upload", {
|
|
1237
|
+
method: "POST",
|
|
1238
|
+
body: form
|
|
1239
|
+
})).json();
|
|
1240
|
+
if (data.ok !== true || typeof data.name !== "string") throw new Error(data.error ?? "upload failed");
|
|
1241
|
+
if (dedup) await fetch("/comfyui/media-hash", {
|
|
1242
|
+
method: "POST",
|
|
1243
|
+
headers: { "Content-Type": "application/json" },
|
|
1244
|
+
body: JSON.stringify({
|
|
1245
|
+
hash,
|
|
1246
|
+
name: data.name
|
|
1247
|
+
})
|
|
1248
|
+
}).catch(() => void 0);
|
|
1249
|
+
return data.name;
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Parameter editor: lists the workflow's adjustable parameters (auto-detected
|
|
1253
|
+
* prompt/size/steps/seed) with editable name/label/default and a random
|
|
1254
|
+
* toggle for number parameters, plus an "advanced" picker that exposes any
|
|
1255
|
+
* node input as a custom parameter.
|
|
1256
|
+
*/
|
|
1257
|
+
function ParameterEditor(props) {
|
|
1258
|
+
const { t, params, workflow, onChange } = props;
|
|
1259
|
+
const [advanced, setAdvanced] = (0, react.useState)(false);
|
|
1260
|
+
const [advNode, setAdvNode] = (0, react.useState)("");
|
|
1261
|
+
const [advInput, setAdvInput] = (0, react.useState)("");
|
|
1262
|
+
const [advLabel, setAdvLabel] = (0, react.useState)("");
|
|
1263
|
+
const [recognizing, setRecognizing] = (0, react.useState)(false);
|
|
1264
|
+
const [recognizeError, setRecognizeError] = (0, react.useState)(null);
|
|
1265
|
+
const [uploading, setUploading] = (0, react.useState)({});
|
|
1266
|
+
const [hovered, setHovered] = (0, react.useState)(null);
|
|
1267
|
+
const hoverRef = (0, react.useRef)(null);
|
|
1268
|
+
const latestRef = (0, react.useRef)({
|
|
1269
|
+
params,
|
|
1270
|
+
uploadFile: async () => void 0
|
|
1271
|
+
});
|
|
1272
|
+
/** Re-fetch a parameter's live option list (ComfyUI input dir files) and
|
|
1273
|
+
* merge it into the stored options, so uploads made anywhere show up in the
|
|
1274
|
+
* dropdown without reloading the workflow editor. */
|
|
1275
|
+
const refreshOptions = async (param) => {
|
|
1276
|
+
const rawNode = workflow[param.nodeId];
|
|
1277
|
+
const classType = typeof rawNode?.class_type === "string" ? rawNode.class_type : "";
|
|
1278
|
+
if (classType === "") return void 0;
|
|
1279
|
+
const result = await postJson("/comfyui/workflows/input-options", {
|
|
1280
|
+
classType,
|
|
1281
|
+
inputKey: param.inputKey
|
|
1282
|
+
});
|
|
1283
|
+
if (result.ok !== true || !Array.isArray(result.options)) return void 0;
|
|
1284
|
+
const latest = result.options;
|
|
1285
|
+
const index = params.findIndex((entry) => entry.id === param.id);
|
|
1286
|
+
if (index !== -1 && params[index]?.options !== void 0) update(index, { options: mergeOptions(params[index].options, latest) });
|
|
1287
|
+
return latest;
|
|
1288
|
+
};
|
|
1289
|
+
/** Upload a dropped file through the plugin proxy into ComfyUI's input dir. */
|
|
1290
|
+
const uploadFile = async (param, file) => {
|
|
1291
|
+
setUploading((prev) => ({
|
|
1292
|
+
...prev,
|
|
1293
|
+
[param.id]: true
|
|
1294
|
+
}));
|
|
1295
|
+
try {
|
|
1296
|
+
const name = await uploadDedup(file, param.subfolder);
|
|
1297
|
+
recordMediaSize(file, name);
|
|
1298
|
+
const index = params.findIndex((entry) => entry.id === param.id);
|
|
1299
|
+
if (index !== -1) update(index, { default: name });
|
|
1300
|
+
refreshOptions(param);
|
|
1301
|
+
} finally {
|
|
1302
|
+
setUploading((prev) => ({
|
|
1303
|
+
...prev,
|
|
1304
|
+
[param.id]: false
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
latestRef.current = {
|
|
1309
|
+
params,
|
|
1310
|
+
uploadFile
|
|
1311
|
+
};
|
|
1312
|
+
/** Global paste: Ctrl+V with the mouse hovering an upload parameter routes
|
|
1313
|
+
* the clipboard file (e.g. a screenshot) to that parameter — same mental
|
|
1314
|
+
* model as pasting an image into a chat input. */
|
|
1315
|
+
(0, react.useEffect)(() => {
|
|
1316
|
+
const onPaste = (event) => {
|
|
1317
|
+
const targetId = hoverRef.current;
|
|
1318
|
+
if (targetId === null) return;
|
|
1319
|
+
const file = event.clipboardData?.files?.[0];
|
|
1320
|
+
if (file === void 0) return;
|
|
1321
|
+
const param = latestRef.current.params.find((entry) => entry.id === targetId);
|
|
1322
|
+
if (param === void 0 || param.upload === void 0) return;
|
|
1323
|
+
event.preventDefault();
|
|
1324
|
+
latestRef.current.uploadFile(param, file);
|
|
1325
|
+
};
|
|
1326
|
+
document.addEventListener("paste", onPaste);
|
|
1327
|
+
return () => document.removeEventListener("paste", onPaste);
|
|
1328
|
+
}, []);
|
|
1329
|
+
/** Auto-generate the internal parameter name from the input key, deduped against existing params. */
|
|
1330
|
+
const genParamName = (base) => {
|
|
1331
|
+
let name = base;
|
|
1332
|
+
let index = 2;
|
|
1333
|
+
while (params.some((param) => param.name === name)) name = `${base}_${index++}`;
|
|
1334
|
+
return name;
|
|
1335
|
+
};
|
|
1336
|
+
const nodeEntries = Object.entries(workflow).map(([id, raw]) => [id, raw]);
|
|
1337
|
+
const advNodeInfo = nodeEntries.find(([id]) => id === advNode)?.[1];
|
|
1338
|
+
const advInputs = advNodeInfo !== void 0 && advNodeInfo.inputs !== void 0 ? Object.entries(advNodeInfo.inputs).filter(([key]) => key !== "upload") : [];
|
|
1339
|
+
const selectedInput = advInputs.find(([key]) => key === advInput);
|
|
1340
|
+
/** Server-side auto-detection: prompt/size/steps/seed + dropdown options (object_info). */
|
|
1341
|
+
const recognize = async () => {
|
|
1342
|
+
if (recognizing) return;
|
|
1343
|
+
setRecognizing(true);
|
|
1344
|
+
setRecognizeError(null);
|
|
1345
|
+
try {
|
|
1346
|
+
const result = await postJson("/comfyui/workflows/recognize", { workflow });
|
|
1347
|
+
if (result.ok === true && Array.isArray(result.parameters)) {
|
|
1348
|
+
onChange(result.parameters);
|
|
1349
|
+
setAdvanced(false);
|
|
1350
|
+
} else setRecognizeError(result.error ?? t("wfParamsRecognizeFailed"));
|
|
1351
|
+
} catch {
|
|
1352
|
+
setRecognizeError(t("wfParamsRecognizeFailed"));
|
|
1353
|
+
} finally {
|
|
1354
|
+
setRecognizing(false);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
const update = (index, patch) => {
|
|
1358
|
+
onChange(params.map((param, i) => i === index ? {
|
|
1359
|
+
...param,
|
|
1360
|
+
...patch
|
|
1361
|
+
} : param));
|
|
1362
|
+
};
|
|
1363
|
+
const addAdvanced = async () => {
|
|
1364
|
+
if (advNode === "" || advInput === "" || selectedInput === void 0 || advNodeInfo === void 0) return;
|
|
1365
|
+
const [, value] = selectedInput;
|
|
1366
|
+
const type = typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : "string";
|
|
1367
|
+
if (!params.some((param) => param.nodeId === advNode && param.inputKey === advInput && param.upload === "media") && typeof value === "string") {
|
|
1368
|
+
let parsed;
|
|
1369
|
+
try {
|
|
1370
|
+
parsed = JSON.parse(value);
|
|
1371
|
+
} catch {
|
|
1372
|
+
parsed = void 0;
|
|
1373
|
+
}
|
|
1374
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((item) => typeof item === "object" && item !== null && typeof item.file === "string")) {
|
|
1375
|
+
const items = parsed;
|
|
1376
|
+
const firstFile = items[0]?.file;
|
|
1377
|
+
const slash = typeof firstFile === "string" ? firstFile.indexOf("/") : -1;
|
|
1378
|
+
const subfolder = typeof firstFile === "string" && slash > 0 ? firstFile.slice(0, slash) : void 0;
|
|
1379
|
+
const added = items.map((item, i) => ({
|
|
1380
|
+
id: newParamId(),
|
|
1381
|
+
name: genParamName(`media_${i + 1}`),
|
|
1382
|
+
label: `${t("wfRefSlot")}${i + 1}`,
|
|
1383
|
+
type: "string",
|
|
1384
|
+
nodeId: advNode,
|
|
1385
|
+
inputKey: advInput,
|
|
1386
|
+
default: typeof item.name === "string" ? item.name : "",
|
|
1387
|
+
upload: "media",
|
|
1388
|
+
subfolder
|
|
1389
|
+
}));
|
|
1390
|
+
onChange([...params, ...added]);
|
|
1391
|
+
setAdvanced(false);
|
|
1392
|
+
setAdvNode("");
|
|
1393
|
+
setAdvInput("");
|
|
1394
|
+
setAdvLabel("");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
let options;
|
|
1399
|
+
let upload;
|
|
1400
|
+
if (typeof advNodeInfo.class_type === "string") try {
|
|
1401
|
+
const result = await postJson("/comfyui/workflows/input-options", {
|
|
1402
|
+
classType: advNodeInfo.class_type,
|
|
1403
|
+
inputKey: advInput
|
|
1404
|
+
});
|
|
1405
|
+
if (result.ok === true && Array.isArray(result.options) && result.options.length > 0) options = result.options;
|
|
1406
|
+
upload = result.upload;
|
|
1407
|
+
} catch {}
|
|
1408
|
+
onChange([...params, {
|
|
1409
|
+
id: newParamId(),
|
|
1410
|
+
name: genParamName(advInput),
|
|
1411
|
+
label: advLabel !== "" ? advLabel : `${advNode} · ${advInput}`,
|
|
1412
|
+
type,
|
|
1413
|
+
nodeId: advNode,
|
|
1414
|
+
inputKey: advInput,
|
|
1415
|
+
default: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : "",
|
|
1416
|
+
options,
|
|
1417
|
+
upload
|
|
1418
|
+
}]);
|
|
1419
|
+
setAdvanced(false);
|
|
1420
|
+
setAdvNode("");
|
|
1421
|
+
setAdvInput("");
|
|
1422
|
+
setAdvLabel("");
|
|
1423
|
+
};
|
|
1424
|
+
/** Update a parameter's default; DynamicCombo parents refresh their child (size) parameter. */
|
|
1425
|
+
const updateDefault = (index, value) => {
|
|
1426
|
+
update(index, { default: value });
|
|
1427
|
+
const param = params[index];
|
|
1428
|
+
if (param === void 0 || typeof value !== "string") return;
|
|
1429
|
+
if (param.options === void 0 || param.options.length === 0) return;
|
|
1430
|
+
const childIndex = params.findIndex((entry) => entry.nodeId === param.nodeId && entry.inputKey.startsWith(`${param.inputKey}.`));
|
|
1431
|
+
if (childIndex === -1) return;
|
|
1432
|
+
const child = params[childIndex];
|
|
1433
|
+
if (child === void 0) return;
|
|
1434
|
+
const rawNode = workflow[child.nodeId];
|
|
1435
|
+
postJson("/comfyui/workflows/input-options", {
|
|
1436
|
+
classType: typeof rawNode?.class_type === "string" ? rawNode.class_type : "",
|
|
1437
|
+
inputKey: param.inputKey,
|
|
1438
|
+
parentValue: value
|
|
1439
|
+
}).then((result) => {
|
|
1440
|
+
const data = result;
|
|
1441
|
+
if (data.ok === true && data.child !== void 0 && childIndex < params.length) update(childIndex, {
|
|
1442
|
+
options: data.child.options,
|
|
1443
|
+
default: data.child.default
|
|
1444
|
+
});
|
|
1445
|
+
}).catch(() => void 0);
|
|
1446
|
+
};
|
|
1447
|
+
return (0, react.createElement)("div", { className: "dsc-params" }, params.length === 0 ? (0, react.createElement)("div", { className: "dsc-hint" }, t("wfParamsEmpty")) : (0, react.createElement)("div", { className: "dsc-list" }, params.map((param, index) => (0, react.createElement)("div", {
|
|
1448
|
+
key: param.id,
|
|
1449
|
+
className: "dsc-param-row"
|
|
1450
|
+
}, (0, react.createElement)("div", { className: "dsc-param-fields" }, (0, react.createElement)("input", {
|
|
1451
|
+
className: "dsc-input dsc-input--sm",
|
|
1452
|
+
value: param.label,
|
|
1453
|
+
placeholder: t("wfParamLabel"),
|
|
1454
|
+
onChange: (event) => update(index, { label: event.target.value })
|
|
1455
|
+
})), param.upload === "media" ? (0, react.createElement)("div", {
|
|
1456
|
+
className: "dsc-param-upload-wrap",
|
|
1457
|
+
onMouseEnter: () => {
|
|
1458
|
+
hoverRef.current = param.id;
|
|
1459
|
+
setHovered(param.id);
|
|
1460
|
+
},
|
|
1461
|
+
onMouseLeave: () => {
|
|
1462
|
+
hoverRef.current = null;
|
|
1463
|
+
setHovered(null);
|
|
1464
|
+
}
|
|
1465
|
+
}, (0, react.createElement)("input", {
|
|
1466
|
+
className: "dsc-input dsc-input--sm dsc-param-default",
|
|
1467
|
+
value: String(param.default),
|
|
1468
|
+
placeholder: t("wfMediaEmpty"),
|
|
1469
|
+
title: `${param.subfolder ?? ""}/`,
|
|
1470
|
+
onChange: (event) => updateDefault(index, event.target.value)
|
|
1471
|
+
}), (0, react.createElement)("label", {
|
|
1472
|
+
className: hovered === param.id ? "dsc-dropzone dsc-dropzone--over" : "dsc-dropzone",
|
|
1473
|
+
title: t("wfPasteHint"),
|
|
1474
|
+
tabIndex: 0,
|
|
1475
|
+
onDragOver: (event) => event.preventDefault(),
|
|
1476
|
+
onDrop: (event) => {
|
|
1477
|
+
event.preventDefault();
|
|
1478
|
+
const file = event.dataTransfer?.files?.[0];
|
|
1479
|
+
if (file !== void 0) uploadFile(param, file);
|
|
1480
|
+
},
|
|
1481
|
+
onPaste: (event) => {
|
|
1482
|
+
const file = event.clipboardData?.files?.[0];
|
|
1483
|
+
if (file !== void 0) uploadFile(param, file);
|
|
1484
|
+
}
|
|
1485
|
+
}, (0, react.createElement)("input", {
|
|
1486
|
+
type: "file",
|
|
1487
|
+
style: { display: "none" },
|
|
1488
|
+
onChange: (event) => {
|
|
1489
|
+
const file = event.target.files?.[0];
|
|
1490
|
+
if (file !== void 0) uploadFile(param, file);
|
|
1491
|
+
event.target.value = "";
|
|
1492
|
+
}
|
|
1493
|
+
}), uploading[param.id] === true ? t("wfParamsUploading") : t("wfParamsDrop")), (0, react.createElement)("button", {
|
|
1494
|
+
className: "dsc-btn",
|
|
1495
|
+
onClick: () => updateDefault(index, "")
|
|
1496
|
+
}, t("wfMediaClear"))) : param.upload !== void 0 ? (0, react.createElement)("div", {
|
|
1497
|
+
className: "dsc-param-upload-wrap",
|
|
1498
|
+
onMouseEnter: () => {
|
|
1499
|
+
hoverRef.current = param.id;
|
|
1500
|
+
setHovered(param.id);
|
|
1501
|
+
},
|
|
1502
|
+
onMouseLeave: () => {
|
|
1503
|
+
hoverRef.current = null;
|
|
1504
|
+
setHovered(null);
|
|
1505
|
+
}
|
|
1506
|
+
}, param.options !== void 0 && param.options.length > 0 ? (0, react.createElement)(UploadSelect, {
|
|
1507
|
+
value: String(param.default),
|
|
1508
|
+
options: param.options,
|
|
1509
|
+
kind: param.upload,
|
|
1510
|
+
onChange: (value) => updateDefault(index, value),
|
|
1511
|
+
onRefresh: () => refreshOptions(param)
|
|
1512
|
+
}) : null, (0, react.createElement)("label", {
|
|
1513
|
+
className: hovered === param.id ? "dsc-dropzone dsc-dropzone--over" : "dsc-dropzone",
|
|
1514
|
+
title: t("wfPasteHint"),
|
|
1515
|
+
tabIndex: 0,
|
|
1516
|
+
onDragOver: (event) => event.preventDefault(),
|
|
1517
|
+
onDrop: (event) => {
|
|
1518
|
+
event.preventDefault();
|
|
1519
|
+
const file = event.dataTransfer?.files?.[0];
|
|
1520
|
+
if (file !== void 0) uploadFile(param, file);
|
|
1521
|
+
},
|
|
1522
|
+
onPaste: (event) => {
|
|
1523
|
+
const file = event.clipboardData?.files?.[0];
|
|
1524
|
+
if (file !== void 0) uploadFile(param, file);
|
|
1525
|
+
}
|
|
1526
|
+
}, (0, react.createElement)("input", {
|
|
1527
|
+
type: "file",
|
|
1528
|
+
style: { display: "none" },
|
|
1529
|
+
onChange: (event) => {
|
|
1530
|
+
const file = event.target.files?.[0];
|
|
1531
|
+
if (file !== void 0) uploadFile(param, file);
|
|
1532
|
+
event.target.value = "";
|
|
1533
|
+
}
|
|
1534
|
+
}), uploading[param.id] === true ? t("wfParamsUploading") : `${t("wfParamsDrop")} ${t(`uploadKind_${param.upload}`)}`)) : param.options !== void 0 && param.options.length > 0 ? (0, react.createElement)("select", {
|
|
1535
|
+
className: "dsc-input dsc-input--sm dsc-param-default",
|
|
1536
|
+
value: String(param.default),
|
|
1537
|
+
onChange: (event) => updateDefault(index, event.target.value)
|
|
1538
|
+
}, param.options.map((option) => (0, react.createElement)("option", {
|
|
1539
|
+
key: String(option),
|
|
1540
|
+
value: String(option)
|
|
1541
|
+
}, String(option)))) : param.type === "string" ? (0, react.createElement)("textarea", {
|
|
1542
|
+
className: "dsc-input dsc-param-default",
|
|
1543
|
+
rows: 2,
|
|
1544
|
+
value: String(param.default),
|
|
1545
|
+
placeholder: t("wfParamDefault"),
|
|
1546
|
+
onChange: (event) => updateDefault(index, event.target.value)
|
|
1547
|
+
}) : (0, react.createElement)("input", {
|
|
1548
|
+
className: "dsc-input dsc-input--sm dsc-param-default",
|
|
1549
|
+
value: String(param.default),
|
|
1550
|
+
placeholder: t("wfParamDefault"),
|
|
1551
|
+
onChange: (event) => {
|
|
1552
|
+
const raw = event.target.value;
|
|
1553
|
+
updateDefault(index, param.type === "number" ? Number(raw) || 0 : raw);
|
|
1554
|
+
}
|
|
1555
|
+
}), (0, react.createElement)("div", { className: "dsc-param-meta" }, (0, react.createElement)("span", { className: "dsc-meta" }, `${param.type} · #${param.nodeId}.${param.inputKey}`), param.type === "number" ? (0, react.createElement)("label", { className: "dsc-param-random" }, (0, react.createElement)("input", {
|
|
1556
|
+
type: "checkbox",
|
|
1557
|
+
checked: param.random === true,
|
|
1558
|
+
onChange: (event) => update(index, { random: event.target.checked })
|
|
1559
|
+
}), (0, react.createElement)("span", null, t("wfParamRandom"))) : null, (0, react.createElement)("button", {
|
|
1560
|
+
className: "dsc-btn",
|
|
1561
|
+
onClick: () => onChange(params.filter((_, i) => i !== index))
|
|
1562
|
+
}, t("wfParamDelete")))))), (0, react.createElement)("div", { className: "dsc-toolbar" }, (0, react.createElement)("button", {
|
|
1563
|
+
className: "dsc-btn",
|
|
1564
|
+
disabled: recognizing,
|
|
1565
|
+
onClick: () => {
|
|
1566
|
+
recognize();
|
|
1567
|
+
}
|
|
1568
|
+
}, recognizing ? t("wfParamsRecognizing") : t("wfParamsRecognize")), (0, react.createElement)("button", {
|
|
1569
|
+
className: "dsc-btn",
|
|
1570
|
+
onClick: () => setAdvanced(!advanced)
|
|
1571
|
+
}, t("wfParamsAdd"))), recognizeError !== null ? (0, react.createElement)("div", { className: "dsc-hint dsc-hint--error" }, recognizeError) : null, advanced ? (0, react.createElement)("div", { className: "dsc-param-advanced" }, (0, react.createElement)("div", { className: "dsc-param-fields" }, (0, react.createElement)("select", {
|
|
1572
|
+
className: "dsc-input dsc-input--sm",
|
|
1573
|
+
value: advNode,
|
|
1574
|
+
onChange: (event) => {
|
|
1575
|
+
setAdvNode(event.target.value);
|
|
1576
|
+
setAdvInput("");
|
|
1577
|
+
}
|
|
1578
|
+
}, (0, react.createElement)("option", { value: "" }, "—"), nodeEntries.map(([id, node]) => (0, react.createElement)("option", {
|
|
1579
|
+
key: id,
|
|
1580
|
+
value: id
|
|
1581
|
+
}, `#${id} ${node.class_type ?? ""}`))), advNodeInfo !== void 0 ? (0, react.createElement)("select", {
|
|
1582
|
+
className: "dsc-input dsc-input--sm",
|
|
1583
|
+
value: advInput,
|
|
1584
|
+
onChange: (event) => setAdvInput(event.target.value)
|
|
1585
|
+
}, (0, react.createElement)("option", { value: "" }, "—"), advInputs.filter(([, value]) => value === null || typeof value !== "object").map(([key, value]) => (0, react.createElement)("option", {
|
|
1586
|
+
key,
|
|
1587
|
+
value: key
|
|
1588
|
+
}, `${key} (${value === null ? "null" : typeof value})`))) : null, (0, react.createElement)("input", {
|
|
1589
|
+
className: "dsc-input dsc-input--sm",
|
|
1590
|
+
value: advLabel,
|
|
1591
|
+
placeholder: t("wfParamCustomLabel"),
|
|
1592
|
+
onChange: (event) => setAdvLabel(event.target.value)
|
|
1593
|
+
}), (0, react.createElement)("button", {
|
|
1594
|
+
className: "dsc-btn",
|
|
1595
|
+
disabled: advNode === "" || advInput === "",
|
|
1596
|
+
onClick: () => {
|
|
1597
|
+
addAdvanced();
|
|
1598
|
+
}
|
|
1599
|
+
}, t("wfParamAdd"))), (0, react.createElement)("div", { className: "dsc-hint" }, t("wfParamCustomHint"))) : null);
|
|
1600
|
+
}
|
|
1601
|
+
function formatTs(ts) {
|
|
1602
|
+
const date = new Date(ts);
|
|
1603
|
+
return Number.isNaN(date.getTime()) ? ts : date.toLocaleString();
|
|
1604
|
+
}
|
|
1605
|
+
function formatBytes(bytes) {
|
|
1606
|
+
if (bytes === void 0) return "";
|
|
1607
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1608
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1609
|
+
return `${(bytes / 1048576).toFixed(2)} MB`;
|
|
1610
|
+
}
|
|
1611
|
+
/** The panel shell: header (drag handle), tabs, and the active tab body. */
|
|
1612
|
+
function ComfyUIPanel({ t }) {
|
|
1613
|
+
const open = usePanelOpen();
|
|
1614
|
+
const tab = usePanelTab();
|
|
1615
|
+
const [geom, setGeom] = (0, react.useState)(loadGeom);
|
|
1616
|
+
const [dragging, setDragging] = (0, react.useState)(null);
|
|
1617
|
+
const [lightbox, setLightbox] = (0, react.useState)(null);
|
|
1618
|
+
const panelRef = (0, react.useRef)(null);
|
|
1619
|
+
const dragRef = (0, react.useRef)(null);
|
|
1620
|
+
const openPreview = (images, kinds, index) => setLightbox({
|
|
1621
|
+
images,
|
|
1622
|
+
kinds,
|
|
1623
|
+
index
|
|
1624
|
+
});
|
|
1625
|
+
(0, react.useEffect)(() => {
|
|
1626
|
+
if (open) return;
|
|
1627
|
+
saveGeom(geom);
|
|
1628
|
+
}, [open]);
|
|
1629
|
+
const startDrag = (event, mode) => {
|
|
1630
|
+
if (event.button !== 0) return;
|
|
1631
|
+
if (mode === "move") {
|
|
1632
|
+
const target = event.target;
|
|
1633
|
+
if (target !== null && target.closest("button, input, textarea, select, a, label")) return;
|
|
1634
|
+
}
|
|
1635
|
+
const rect = panelRef.current?.getBoundingClientRect();
|
|
1636
|
+
if (rect === void 0) return;
|
|
1637
|
+
dragRef.current = {
|
|
1638
|
+
startX: event.clientX,
|
|
1639
|
+
startY: event.clientY,
|
|
1640
|
+
origX: rect.left,
|
|
1641
|
+
origY: rect.top,
|
|
1642
|
+
width: rect.width,
|
|
1643
|
+
height: rect.height
|
|
1644
|
+
};
|
|
1645
|
+
setDragging(mode);
|
|
1646
|
+
const el = event.currentTarget;
|
|
1647
|
+
if (el !== null && typeof el.setPointerCapture === "function") el.setPointerCapture(event.pointerId);
|
|
1648
|
+
};
|
|
1649
|
+
const moveDrag = (event) => {
|
|
1650
|
+
const d = dragRef.current;
|
|
1651
|
+
if (d === null) return;
|
|
1652
|
+
if (dragging === "move") setGeom((prev) => ({
|
|
1653
|
+
...prev,
|
|
1654
|
+
x: Math.round(d.origX + event.clientX - d.startX),
|
|
1655
|
+
y: Math.round(d.origY + event.clientY - d.startY)
|
|
1656
|
+
}));
|
|
1657
|
+
else if (dragging === "resize") {
|
|
1658
|
+
const width = Math.max(300, Math.min(720, d.width + event.clientX - d.startX));
|
|
1659
|
+
const height = Math.max(200, Math.min(Math.max(300, window.innerHeight - 40), d.height + event.clientY - d.startY));
|
|
1660
|
+
setGeom((prev) => ({
|
|
1661
|
+
...prev,
|
|
1662
|
+
width,
|
|
1663
|
+
height
|
|
1664
|
+
}));
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
const endDrag = () => {
|
|
1668
|
+
if (dragRef.current === null) return;
|
|
1669
|
+
dragRef.current = null;
|
|
1670
|
+
setDragging(null);
|
|
1671
|
+
setGeom((prev) => {
|
|
1672
|
+
saveGeom(prev);
|
|
1673
|
+
return prev;
|
|
1674
|
+
});
|
|
1675
|
+
};
|
|
1676
|
+
if (!open) return null;
|
|
1677
|
+
const panelStyle = {};
|
|
1678
|
+
if (geom.x !== void 0 && geom.y !== void 0) {
|
|
1679
|
+
panelStyle.left = `${geom.x}px`;
|
|
1680
|
+
panelStyle.top = `${geom.y}px`;
|
|
1681
|
+
panelStyle.right = "auto";
|
|
1682
|
+
panelStyle.bottom = "auto";
|
|
1683
|
+
}
|
|
1684
|
+
if (geom.width !== void 0) panelStyle.width = `${geom.width}px`;
|
|
1685
|
+
if (geom.height !== void 0) panelStyle.height = `${geom.height}px`;
|
|
1686
|
+
return (0, react.createElement)("div", {
|
|
1687
|
+
ref: panelRef,
|
|
1688
|
+
className: `dsc-panel${dragging !== null ? " dsc-panel--dragging" : ""}`,
|
|
1689
|
+
style: panelStyle
|
|
1690
|
+
}, (0, react.createElement)("div", {
|
|
1691
|
+
className: "dsc-panel-head",
|
|
1692
|
+
onPointerDown: (event) => startDrag(event, "move"),
|
|
1693
|
+
onPointerMove: (event) => moveDrag(event),
|
|
1694
|
+
onPointerUp: () => endDrag(),
|
|
1695
|
+
onPointerCancel: () => endDrag()
|
|
1696
|
+
}, (0, react.createElement)("span", { className: "dsc-panel-title" }, (0, react.createElement)("span", { className: "dsc-trigger-glyph" }, (0, react.createElement)(ComfyUIIcon)), (0, react.createElement)("span", null, t("panelTitle"))), (0, react.createElement)("button", {
|
|
1697
|
+
className: "dsc-panel-close",
|
|
1698
|
+
"aria-label": t("close"),
|
|
1699
|
+
onClick: () => panelStore.close()
|
|
1700
|
+
}, "✕")), (0, react.createElement)("div", { className: "dsc-tabs" }, (0, react.createElement)(TabButton, {
|
|
1701
|
+
t,
|
|
1702
|
+
active: tab === "workflows",
|
|
1703
|
+
label: t("tabWorkflows"),
|
|
1704
|
+
onClick: () => panelStore.setTab("workflows")
|
|
1705
|
+
}), (0, react.createElement)(TabButton, {
|
|
1706
|
+
t,
|
|
1707
|
+
active: tab === "assets",
|
|
1708
|
+
label: t("tabAssets"),
|
|
1709
|
+
onClick: () => panelStore.setTab("assets")
|
|
1710
|
+
}), (0, react.createElement)(TabButton, {
|
|
1711
|
+
t,
|
|
1712
|
+
active: tab === "queue",
|
|
1713
|
+
label: t("tabQueue"),
|
|
1714
|
+
onClick: () => panelStore.setTab("queue")
|
|
1715
|
+
})), (0, react.createElement)("div", { className: "dsc-panel-body" }, tab === "workflows" ? (0, react.createElement)(WorkflowsTab, { t }) : tab === "assets" ? (0, react.createElement)(AssetsTab, {
|
|
1716
|
+
t,
|
|
1717
|
+
onPreview: openPreview
|
|
1718
|
+
}) : (0, react.createElement)(QueueTab, {
|
|
1719
|
+
t,
|
|
1720
|
+
onPreview: openPreview
|
|
1721
|
+
})), lightbox !== null ? (0, react.createElement)(Lightbox, {
|
|
1722
|
+
t,
|
|
1723
|
+
images: lightbox.images,
|
|
1724
|
+
kinds: lightbox.kinds,
|
|
1725
|
+
index: lightbox.index,
|
|
1726
|
+
onClose: () => setLightbox(null),
|
|
1727
|
+
onIndex: (index) => setLightbox({
|
|
1728
|
+
images: lightbox.images,
|
|
1729
|
+
kinds: lightbox.kinds,
|
|
1730
|
+
index
|
|
1731
|
+
})
|
|
1732
|
+
}) : null, (0, react.createElement)("div", {
|
|
1733
|
+
className: "dsc-panel-resize",
|
|
1734
|
+
onPointerDown: (event) => startDrag(event, "resize"),
|
|
1735
|
+
onPointerMove: (event) => moveDrag(event),
|
|
1736
|
+
onPointerUp: () => endDrag(),
|
|
1737
|
+
onPointerCancel: () => endDrag()
|
|
1738
|
+
}));
|
|
1739
|
+
}
|
|
1740
|
+
function TabButton(props) {
|
|
1741
|
+
return (0, react.createElement)("button", {
|
|
1742
|
+
className: props.active ? "dsc-tab dsc-tab--active" : "dsc-tab",
|
|
1743
|
+
onClick: props.onClick
|
|
1744
|
+
}, props.label);
|
|
1745
|
+
}
|
|
1746
|
+
function ErrorNote({ t, message }) {
|
|
1747
|
+
return (0, react.createElement)("div", { className: "dsc-err" }, `${t("error")}: ${message}`);
|
|
1748
|
+
}
|
|
1749
|
+
/** Load area at the bottom of the workflow library, modeled on the ComfyUI
|
|
1750
|
+
* LoadImage node: a big preview of the currently selected image; clicking it
|
|
1751
|
+
* opens a picker (全部/已导入/已生成 tabs + paste-to-upload + masonry grid).
|
|
1752
|
+
* Selecting a file makes it the default source image for image-to-image runs
|
|
1753
|
+
* (the workflow's image parameter defaults to this file name). */
|
|
1754
|
+
function LoadArea(props) {
|
|
1755
|
+
const [current, setCurrent] = (0, react.useState)(null);
|
|
1756
|
+
const [files, setFiles] = (0, react.useState)([]);
|
|
1757
|
+
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
1758
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1759
|
+
const [error, setError] = (0, react.useState)(null);
|
|
1760
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
1761
|
+
const data = await fetch("/comfyui/loadarea").then((response) => response.json()).catch(() => void 0);
|
|
1762
|
+
if (data?.ok === true) {
|
|
1763
|
+
setCurrent(data.current ?? null);
|
|
1764
|
+
setFiles(data.files ?? []);
|
|
1765
|
+
}
|
|
1766
|
+
}, []);
|
|
1767
|
+
(0, react.useEffect)(() => {
|
|
1768
|
+
refresh();
|
|
1769
|
+
}, [refresh]);
|
|
1770
|
+
const upload = async (file) => {
|
|
1771
|
+
setBusy(true);
|
|
1772
|
+
setError(null);
|
|
1773
|
+
try {
|
|
1774
|
+
const name = await uploadDedup(file);
|
|
1775
|
+
const sizeText = await recordMediaSize(file, name);
|
|
1776
|
+
await refresh();
|
|
1777
|
+
props.onNotice(`${props.t("wfUploaded")}:${name}${sizeText !== void 0 ? `(${sizeText})` : ""}`);
|
|
1778
|
+
return name;
|
|
1779
|
+
} catch (cause) {
|
|
1780
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
1781
|
+
throw cause;
|
|
1782
|
+
} finally {
|
|
1783
|
+
setBusy(false);
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
const pick = async (file) => {
|
|
1787
|
+
await fetch("/comfyui/current-image", {
|
|
1788
|
+
method: "POST",
|
|
1789
|
+
headers: { "Content-Type": "application/json" },
|
|
1790
|
+
body: JSON.stringify({
|
|
1791
|
+
name: file.name,
|
|
1792
|
+
kind: file.kind,
|
|
1793
|
+
source: file.source
|
|
1794
|
+
})
|
|
1795
|
+
}).catch(() => void 0);
|
|
1796
|
+
setCurrent(file);
|
|
1797
|
+
setPickerOpen(false);
|
|
1798
|
+
props.onNotice(`${props.t("wfLoadPicked")}:${file.name}`);
|
|
1799
|
+
};
|
|
1800
|
+
return (0, react.createElement)("div", { className: "dsc-loadarea" }, (0, react.createElement)("div", { className: "dsc-loadarea-head" }, (0, react.createElement)("span", { className: "dsc-loadarea-title" }, props.t("wfLoadArea")), (0, react.createElement)("span", { className: "dsc-meta" }, props.t("wfLoadHint"))), (0, react.createElement)("button", {
|
|
1801
|
+
className: "dsc-loadarea-main",
|
|
1802
|
+
onClick: () => setPickerOpen(true),
|
|
1803
|
+
title: props.t("wfLoadHint"),
|
|
1804
|
+
disabled: busy
|
|
1805
|
+
}, current !== null ? (0, react.createElement)("img", {
|
|
1806
|
+
className: "dsc-loadarea-img",
|
|
1807
|
+
src: current.url,
|
|
1808
|
+
alt: "",
|
|
1809
|
+
loading: "lazy"
|
|
1810
|
+
}) : (0, react.createElement)("span", { className: "dsc-loadarea-empty" }, props.t("wfLoadEmpty")), (0, react.createElement)("span", { className: "dsc-loadarea-name" }, current !== null ? `${current.name}${current.width !== void 0 && current.height !== void 0 ? `(${current.width}×${current.height})` : ""}` : "")), pickerOpen ? (0, react.createElement)(LoadPicker, {
|
|
1811
|
+
t: props.t,
|
|
1812
|
+
files,
|
|
1813
|
+
current: current?.name ?? null,
|
|
1814
|
+
onUpload: upload,
|
|
1815
|
+
onPick: pick,
|
|
1816
|
+
onClose: () => setPickerOpen(false)
|
|
1817
|
+
}) : null, error !== null ? (0, react.createElement)("div", { className: "dsc-hint dsc-hint--error" }, error) : null);
|
|
1818
|
+
}
|
|
1819
|
+
/** The load-area picker dialog: tab bar (全部/已导入/已生成) with a
|
|
1820
|
+
* paste/click upload zone on the right, and a masonry image grid below. */
|
|
1821
|
+
function LoadPicker(props) {
|
|
1822
|
+
const [tab, setTab] = (0, react.useState)("all");
|
|
1823
|
+
const [type, setType] = (0, react.useState)("all");
|
|
1824
|
+
const hoverRef = (0, react.useRef)(false);
|
|
1825
|
+
const [uploadFlash, setUploadFlash] = (0, react.useState)(null);
|
|
1826
|
+
const [scrollTo, setScrollTo] = (0, react.useState)(null);
|
|
1827
|
+
const cardRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
1828
|
+
const flashTimer = (0, react.useRef)(void 0);
|
|
1829
|
+
(0, react.useEffect)(() => () => {
|
|
1830
|
+
if (flashTimer.current !== void 0) window.clearTimeout(flashTimer.current);
|
|
1831
|
+
}, []);
|
|
1832
|
+
(0, react.useEffect)(() => {
|
|
1833
|
+
const onPaste = (event) => {
|
|
1834
|
+
if (!hoverRef.current) return;
|
|
1835
|
+
const file = event.clipboardData?.files?.[0];
|
|
1836
|
+
if (file === void 0) return;
|
|
1837
|
+
event.preventDefault();
|
|
1838
|
+
handleUpload(file);
|
|
1839
|
+
};
|
|
1840
|
+
document.addEventListener("paste", onPaste);
|
|
1841
|
+
return () => document.removeEventListener("paste", onPaste);
|
|
1842
|
+
});
|
|
1843
|
+
/** Upload, then flash a confirmation and scroll the grid to the new file. */
|
|
1844
|
+
const handleUpload = async (file) => {
|
|
1845
|
+
try {
|
|
1846
|
+
const name = await props.onUpload(file);
|
|
1847
|
+
setScrollTo(name);
|
|
1848
|
+
setUploadFlash(`${props.t("wfUploadedToast")}:${name}`);
|
|
1849
|
+
if (flashTimer.current !== void 0) window.clearTimeout(flashTimer.current);
|
|
1850
|
+
flashTimer.current = window.setTimeout(() => setUploadFlash(null), 2500);
|
|
1851
|
+
} catch {}
|
|
1852
|
+
};
|
|
1853
|
+
const bySource = tab === "all" ? props.files : props.files.filter((file) => file.source === tab);
|
|
1854
|
+
const filtered = type === "all" ? bySource : bySource.filter((file) => file.kind === type);
|
|
1855
|
+
(0, react.useEffect)(() => {
|
|
1856
|
+
if (scrollTo === null) return;
|
|
1857
|
+
let found;
|
|
1858
|
+
for (const [key, el] of cardRefs.current) if (key.endsWith(`:${scrollTo}`)) {
|
|
1859
|
+
found = el;
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
if (found !== void 0) {
|
|
1863
|
+
found.scrollIntoView({
|
|
1864
|
+
block: "center",
|
|
1865
|
+
behavior: "smooth"
|
|
1866
|
+
});
|
|
1867
|
+
setScrollTo(null);
|
|
1868
|
+
}
|
|
1869
|
+
}, [scrollTo, filtered]);
|
|
1870
|
+
const columns = [
|
|
1871
|
+
[],
|
|
1872
|
+
[],
|
|
1873
|
+
[]
|
|
1874
|
+
];
|
|
1875
|
+
filtered.forEach((file, index) => {
|
|
1876
|
+
columns[index % 3].push(file);
|
|
1877
|
+
});
|
|
1878
|
+
const tabs = [
|
|
1879
|
+
{
|
|
1880
|
+
id: "all",
|
|
1881
|
+
label: props.t("wfTabAll")
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
id: "imported",
|
|
1885
|
+
label: props.t("wfTabImported")
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
id: "generated",
|
|
1889
|
+
label: props.t("wfTabGenerated")
|
|
1890
|
+
}
|
|
1891
|
+
];
|
|
1892
|
+
const typeOptions = [
|
|
1893
|
+
{
|
|
1894
|
+
id: "all",
|
|
1895
|
+
label: props.t("wfTypeAll")
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
id: "image",
|
|
1899
|
+
label: props.t("uploadKind_image")
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
id: "video",
|
|
1903
|
+
label: props.t("uploadKind_video")
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
id: "audio",
|
|
1907
|
+
label: props.t("uploadKind_audio")
|
|
1908
|
+
}
|
|
1909
|
+
];
|
|
1910
|
+
return (0, react.createElement)("div", {
|
|
1911
|
+
className: "dsc-picker-overlay",
|
|
1912
|
+
onClick: props.onClose
|
|
1913
|
+
}, (0, react.createElement)("div", {
|
|
1914
|
+
className: "dsc-picker",
|
|
1915
|
+
onClick: (event) => event.stopPropagation()
|
|
1916
|
+
}, (0, react.createElement)("div", { className: "dsc-picker-bar" }, (0, react.createElement)("div", { className: "dsc-picker-tabs" }, tabs.map((entry) => (0, react.createElement)("button", {
|
|
1917
|
+
key: entry.id,
|
|
1918
|
+
className: `dsc-picker-tab${tab === entry.id ? " dsc-picker-tab--active" : ""}`,
|
|
1919
|
+
onClick: () => setTab(entry.id)
|
|
1920
|
+
}, entry.label))), (0, react.createElement)("label", {
|
|
1921
|
+
className: "dsc-dropzone dsc-picker-upload",
|
|
1922
|
+
title: props.t("wfPasteHint"),
|
|
1923
|
+
onMouseEnter: () => {
|
|
1924
|
+
hoverRef.current = true;
|
|
1925
|
+
},
|
|
1926
|
+
onMouseLeave: () => {
|
|
1927
|
+
hoverRef.current = false;
|
|
1928
|
+
},
|
|
1929
|
+
onDragOver: (event) => event.preventDefault(),
|
|
1930
|
+
onDrop: (event) => {
|
|
1931
|
+
event.preventDefault();
|
|
1932
|
+
const file = event.dataTransfer?.files?.[0];
|
|
1933
|
+
if (file !== void 0) handleUpload(file);
|
|
1934
|
+
}
|
|
1935
|
+
}, (0, react.createElement)("input", {
|
|
1936
|
+
type: "file",
|
|
1937
|
+
style: { display: "none" },
|
|
1938
|
+
onChange: (event) => {
|
|
1939
|
+
const file = event.target.files?.[0];
|
|
1940
|
+
if (file !== void 0) handleUpload(file);
|
|
1941
|
+
event.target.value = "";
|
|
1942
|
+
}
|
|
1943
|
+
}), props.t("wfPickerUpload")), (0, react.createElement)("select", {
|
|
1944
|
+
className: "dsc-picker-type",
|
|
1945
|
+
value: type,
|
|
1946
|
+
onChange: (event) => setType(event.target.value)
|
|
1947
|
+
}, typeOptions.map((option) => (0, react.createElement)("option", {
|
|
1948
|
+
key: option.id,
|
|
1949
|
+
value: option.id
|
|
1950
|
+
}, option.label)))), filtered.length === 0 ? (0, react.createElement)("div", { className: "dsc-picker-empty" }, props.t("wfLoadNoFiles")) : (0, react.createElement)("div", { className: "dsc-picker-grid" }, columns.map((column, columnIndex) => (0, react.createElement)("div", {
|
|
1951
|
+
key: columnIndex,
|
|
1952
|
+
className: "dsc-picker-col"
|
|
1953
|
+
}, column.map((file) => (0, react.createElement)("button", {
|
|
1954
|
+
key: `${file.source}:${file.name}`,
|
|
1955
|
+
className: `dsc-picker-card${file.name === props.current ? " dsc-picker-card--active" : ""}`,
|
|
1956
|
+
onClick: () => {
|
|
1957
|
+
props.onPick(file);
|
|
1958
|
+
},
|
|
1959
|
+
title: file.name,
|
|
1960
|
+
ref: (el) => {
|
|
1961
|
+
if (el !== null) cardRefs.current.set(`${file.source}:${file.name}`, el);
|
|
1962
|
+
else cardRefs.current.delete(`${file.source}:${file.name}`);
|
|
1963
|
+
}
|
|
1964
|
+
}, file.kind === "image" ? (0, react.createElement)("img", {
|
|
1965
|
+
className: "dsc-picker-thumb",
|
|
1966
|
+
src: file.url,
|
|
1967
|
+
alt: "",
|
|
1968
|
+
loading: "lazy"
|
|
1969
|
+
}) : (0, react.createElement)("span", { className: "dsc-picker-thumb dsc-picker-thumb--media" }, file.kind === "video" ? "🎞" : "🎵"), (0, react.createElement)("span", { className: "dsc-picker-name" }, file.name), file.width !== void 0 && file.height !== void 0 ? (0, react.createElement)("span", { className: "dsc-meta" }, `${file.width}×${file.height}`) : null))))), uploadFlash !== null ? (0, react.createElement)("div", { className: "dsc-picker-toast" }, uploadFlash) : null));
|
|
1970
|
+
}
|
|
1971
|
+
/** Tag editor: preset classification chips (图生图 etc.) toggle on/off, plus
|
|
1972
|
+
* free-form custom tags added with Enter and removed by clicking. */
|
|
1973
|
+
function TagEditor(props) {
|
|
1974
|
+
const [custom, setCustom] = (0, react.useState)("");
|
|
1975
|
+
const toggle = (tag) => {
|
|
1976
|
+
props.onChange(props.tags.includes(tag) ? props.tags.filter((entry) => entry !== tag) : [...props.tags, tag]);
|
|
1977
|
+
};
|
|
1978
|
+
const addCustom = () => {
|
|
1979
|
+
const tag = custom.trim();
|
|
1980
|
+
if (tag === "" || props.tags.includes(tag)) return;
|
|
1981
|
+
props.onChange([...props.tags, tag]);
|
|
1982
|
+
setCustom("");
|
|
1983
|
+
};
|
|
1984
|
+
const customTags = props.tags.filter((tag) => !PRESET_TAGS.includes(tag));
|
|
1985
|
+
return (0, react.createElement)("div", { className: "dsc-tag-editor" }, (0, react.createElement)("div", { className: "dsc-tag-row" }, PRESET_TAGS.map((tag) => (0, react.createElement)("button", {
|
|
1986
|
+
key: tag,
|
|
1987
|
+
className: `dsc-tag-chip${props.tags.includes(tag) ? " dsc-tag-chip--active" : ""}`,
|
|
1988
|
+
onClick: () => toggle(tag)
|
|
1989
|
+
}, tag))), customTags.length > 0 ? (0, react.createElement)("div", { className: "dsc-tag-row" }, customTags.map((tag) => (0, react.createElement)("button", {
|
|
1990
|
+
key: tag,
|
|
1991
|
+
className: "dsc-tag-chip dsc-tag-chip--active",
|
|
1992
|
+
title: props.t("wfTagRemove"),
|
|
1993
|
+
onClick: () => toggle(tag)
|
|
1994
|
+
}, `✕ ${tag}`))) : null, (0, react.createElement)("input", {
|
|
1995
|
+
className: "dsc-input dsc-input--sm dsc-tag-input",
|
|
1996
|
+
value: custom,
|
|
1997
|
+
placeholder: props.t("wfTagAdd"),
|
|
1998
|
+
onChange: (event) => setCustom(event.target.value),
|
|
1999
|
+
onKeyDown: (event) => {
|
|
2000
|
+
if (event.key === "Enter") {
|
|
2001
|
+
event.preventDefault();
|
|
2002
|
+
addCustom();
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}));
|
|
2006
|
+
}
|
|
2007
|
+
function WorkflowsTab({ t }) {
|
|
2008
|
+
const [list, setList] = (0, react.useState)(null);
|
|
2009
|
+
const [comfyui, setComfyui] = (0, react.useState)(null);
|
|
2010
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2011
|
+
const [comfyuiError, setComfyuiError] = (0, react.useState)(null);
|
|
2012
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
2013
|
+
const [editing, setEditing] = (0, react.useState)(null);
|
|
2014
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2015
|
+
const [viewing, setViewing] = (0, react.useState)(null);
|
|
2016
|
+
const [extract, setExtract] = (0, react.useState)(null);
|
|
2017
|
+
const [tagFilter, setTagFilter] = (0, react.useState)(null);
|
|
2018
|
+
const load = async () => {
|
|
2019
|
+
try {
|
|
2020
|
+
const data = await getJson("/comfyui/workflows");
|
|
2021
|
+
setList(data.workflows);
|
|
2022
|
+
setError(null);
|
|
2023
|
+
} catch (cause) {
|
|
2024
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2025
|
+
}
|
|
2026
|
+
try {
|
|
2027
|
+
const data = await getJson("/comfyui/comfy-workflows");
|
|
2028
|
+
setComfyui(data.workflows);
|
|
2029
|
+
setComfyuiError(null);
|
|
2030
|
+
} catch (cause) {
|
|
2031
|
+
setComfyuiError(cause instanceof Error ? cause.message : String(cause));
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
(0, react.useEffect)(() => {
|
|
2035
|
+
let cancelled = false;
|
|
2036
|
+
getJson("/comfyui/workflows").then((data) => {
|
|
2037
|
+
if (!cancelled) setList(data.workflows);
|
|
2038
|
+
}).catch((cause) => {
|
|
2039
|
+
if (!cancelled) setError(cause instanceof Error ? cause.message : String(cause));
|
|
2040
|
+
});
|
|
2041
|
+
getJson("/comfyui/comfy-workflows").then((data) => {
|
|
2042
|
+
if (!cancelled) setComfyui(data.workflows);
|
|
2043
|
+
}).catch((cause) => {
|
|
2044
|
+
if (!cancelled) setComfyuiError(cause instanceof Error ? cause.message : String(cause));
|
|
2045
|
+
});
|
|
2046
|
+
return () => {
|
|
2047
|
+
cancelled = true;
|
|
2048
|
+
};
|
|
2049
|
+
}, []);
|
|
2050
|
+
/** Persist the current draft; returns the saved workflow id, or null on failure (error already set). */
|
|
2051
|
+
const persist = async () => {
|
|
2052
|
+
if (editing === null) return null;
|
|
2053
|
+
setBusy(true);
|
|
2054
|
+
setError(null);
|
|
2055
|
+
setNotice(null);
|
|
2056
|
+
let workflow;
|
|
2057
|
+
if (typeof editing.workflow === "string") try {
|
|
2058
|
+
workflow = JSON.parse(editing.workflow);
|
|
2059
|
+
} catch {
|
|
2060
|
+
setError(t("wfJsonError"));
|
|
2061
|
+
setBusy(false);
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
else workflow = editing.workflow;
|
|
2065
|
+
try {
|
|
2066
|
+
const body = {
|
|
2067
|
+
name: String(editing.name ?? ""),
|
|
2068
|
+
description: String(editing.description ?? ""),
|
|
2069
|
+
workflow,
|
|
2070
|
+
parameters: editing.parameters ?? [],
|
|
2071
|
+
tags: editing.tags ?? []
|
|
2072
|
+
};
|
|
2073
|
+
if (editing.id !== void 0) body.id = editing.id;
|
|
2074
|
+
return (await postJson("/comfyui/workflows", body)).workflow?.id ?? (typeof editing.id === "string" ? editing.id : null);
|
|
2075
|
+
} catch (cause) {
|
|
2076
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2077
|
+
return null;
|
|
2078
|
+
} finally {
|
|
2079
|
+
setBusy(false);
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
const save = async () => {
|
|
2083
|
+
if (await persist() === null) return;
|
|
2084
|
+
setEditing(null);
|
|
2085
|
+
await load();
|
|
2086
|
+
};
|
|
2087
|
+
/** Save the draft, then immediately queue it with the saved parameters. */
|
|
2088
|
+
const saveAndRun = async () => {
|
|
2089
|
+
const id = await persist();
|
|
2090
|
+
if (id === null) return;
|
|
2091
|
+
setEditing(null);
|
|
2092
|
+
await load();
|
|
2093
|
+
await run(id);
|
|
2094
|
+
};
|
|
2095
|
+
const run = async (id) => {
|
|
2096
|
+
setBusy(true);
|
|
2097
|
+
setError(null);
|
|
2098
|
+
setNotice(null);
|
|
2099
|
+
try {
|
|
2100
|
+
const result = await postJson("/comfyui/workflows/run", { id });
|
|
2101
|
+
setNotice(`${t("wfQueued")} ${shortId(result.promptId)}`);
|
|
2102
|
+
} catch (cause) {
|
|
2103
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2104
|
+
} finally {
|
|
2105
|
+
setBusy(false);
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
const remove = async (id) => {
|
|
2109
|
+
setBusy(true);
|
|
2110
|
+
setError(null);
|
|
2111
|
+
try {
|
|
2112
|
+
await postJson("/comfyui/workflows/delete", { id });
|
|
2113
|
+
await load();
|
|
2114
|
+
} catch (cause) {
|
|
2115
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2116
|
+
} finally {
|
|
2117
|
+
setBusy(false);
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
const openExtract = (file) => {
|
|
2121
|
+
setError(null);
|
|
2122
|
+
setExtract({
|
|
2123
|
+
file,
|
|
2124
|
+
analysis: null,
|
|
2125
|
+
error: null,
|
|
2126
|
+
mode: "split"
|
|
2127
|
+
});
|
|
2128
|
+
};
|
|
2129
|
+
const runExtract = async (state) => {
|
|
2130
|
+
setBusy(true);
|
|
2131
|
+
try {
|
|
2132
|
+
const data = await postJson("/comfyui/comfy-workflows/extract", {
|
|
2133
|
+
file: state.file,
|
|
2134
|
+
mode: state.mode
|
|
2135
|
+
});
|
|
2136
|
+
if (data.ok !== true) throw new Error(data.error ?? "failed to extract");
|
|
2137
|
+
setNotice(`${state.file} → ${t("wfExtractedNotice")}${data.warnings.length > 0 ? `(${t("error")}: ${data.warnings.join(";")})` : ""}`);
|
|
2138
|
+
setExtract(null);
|
|
2139
|
+
await load();
|
|
2140
|
+
} catch (cause) {
|
|
2141
|
+
setExtract({
|
|
2142
|
+
...state,
|
|
2143
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
2144
|
+
});
|
|
2145
|
+
} finally {
|
|
2146
|
+
setBusy(false);
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
const importFile = (file) => {
|
|
2150
|
+
const reader = new FileReader();
|
|
2151
|
+
reader.onload = () => {
|
|
2152
|
+
try {
|
|
2153
|
+
const parsed = JSON.parse(String(reader.result));
|
|
2154
|
+
setEditing({
|
|
2155
|
+
name: file.name.replace(/\.json$/i, ""),
|
|
2156
|
+
workflow: parsed
|
|
2157
|
+
});
|
|
2158
|
+
setError(null);
|
|
2159
|
+
} catch {
|
|
2160
|
+
setError(t("wfJsonError"));
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
reader.readAsText(file);
|
|
2164
|
+
};
|
|
2165
|
+
const view = async (file) => {
|
|
2166
|
+
setBusy(true);
|
|
2167
|
+
setError(null);
|
|
2168
|
+
try {
|
|
2169
|
+
const data = await getJson(`/comfyui/comfy-workflows?file=${encodeURIComponent(file)}`);
|
|
2170
|
+
if (data.ok !== true || data.workflow === void 0) throw new Error(data.error ?? "failed to read workflow");
|
|
2171
|
+
setViewing({
|
|
2172
|
+
file,
|
|
2173
|
+
graph: data.workflow
|
|
2174
|
+
});
|
|
2175
|
+
} catch (cause) {
|
|
2176
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2177
|
+
} finally {
|
|
2178
|
+
setBusy(false);
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
if (viewing !== null) return (0, react.createElement)(WorkflowView, {
|
|
2182
|
+
t,
|
|
2183
|
+
file: viewing.file,
|
|
2184
|
+
graph: viewing.graph,
|
|
2185
|
+
onBack: () => setViewing(null)
|
|
2186
|
+
});
|
|
2187
|
+
if (extract !== null) return (0, react.createElement)(ExtractDialog, {
|
|
2188
|
+
t,
|
|
2189
|
+
state: extract,
|
|
2190
|
+
onUpdate: setExtract,
|
|
2191
|
+
onRun: (state) => runExtract(state),
|
|
2192
|
+
onClose: () => setExtract(null)
|
|
2193
|
+
});
|
|
2194
|
+
if (error !== null) return (0, react.createElement)("div", null, (0, react.createElement)(ErrorNote, {
|
|
2195
|
+
t,
|
|
2196
|
+
message: error
|
|
2197
|
+
}));
|
|
2198
|
+
if (list === null) return (0, react.createElement)("div", { className: "dsc-meta" }, "…");
|
|
2199
|
+
if (editing !== null) return (0, react.createElement)("div", { className: "dsc-form" }, (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("wfName")), (0, react.createElement)("input", {
|
|
2200
|
+
className: "dsc-input",
|
|
2201
|
+
value: editing.name ?? "",
|
|
2202
|
+
onChange: (event) => setEditing({
|
|
2203
|
+
...editing,
|
|
2204
|
+
name: event.target.value
|
|
2205
|
+
})
|
|
2206
|
+
})), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("wfDesc")), (0, react.createElement)("textarea", {
|
|
2207
|
+
className: "dsc-textarea",
|
|
2208
|
+
placeholder: t("wfDescPlaceholder"),
|
|
2209
|
+
value: editing.description ?? "",
|
|
2210
|
+
onChange: (event) => setEditing({
|
|
2211
|
+
...editing,
|
|
2212
|
+
description: event.target.value
|
|
2213
|
+
})
|
|
2214
|
+
})), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("wfTags")), (0, react.createElement)(TagEditor, {
|
|
2215
|
+
t,
|
|
2216
|
+
tags: editing.tags ?? [],
|
|
2217
|
+
onChange: (tags) => setEditing({
|
|
2218
|
+
...editing,
|
|
2219
|
+
tags
|
|
2220
|
+
})
|
|
2221
|
+
})), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("wfParams")), (0, react.createElement)(ParameterEditor, {
|
|
2222
|
+
t,
|
|
2223
|
+
params: editing.parameters ?? [],
|
|
2224
|
+
workflow: parseDraftWorkflow(editing.workflow),
|
|
2225
|
+
onChange: (parameters) => setEditing({
|
|
2226
|
+
...editing,
|
|
2227
|
+
parameters
|
|
2228
|
+
})
|
|
2229
|
+
})), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("wfJson")), (0, react.createElement)("textarea", {
|
|
2230
|
+
className: "dsc-textarea dsc-textarea--json",
|
|
2231
|
+
value: typeof editing.workflow === "string" ? editing.workflow : JSON.stringify(editing.workflow ?? {}, null, 2),
|
|
2232
|
+
onChange: (event) => setEditing({
|
|
2233
|
+
...editing,
|
|
2234
|
+
workflow: event.target.value
|
|
2235
|
+
})
|
|
2236
|
+
})), notice !== null ? (0, react.createElement)("div", { className: "dsc-ok" }, notice) : null, error !== null ? (0, react.createElement)(ErrorNote, {
|
|
2237
|
+
t,
|
|
2238
|
+
message: error
|
|
2239
|
+
}) : null, (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2240
|
+
className: "dsc-btn",
|
|
2241
|
+
disabled: busy,
|
|
2242
|
+
onClick: () => void save()
|
|
2243
|
+
}, t("wfSave")), (0, react.createElement)("button", {
|
|
2244
|
+
className: "dsc-btn",
|
|
2245
|
+
disabled: busy,
|
|
2246
|
+
onClick: () => void saveAndRun()
|
|
2247
|
+
}, t("wfSaveRun")), (0, react.createElement)("button", {
|
|
2248
|
+
className: "dsc-btn",
|
|
2249
|
+
disabled: busy,
|
|
2250
|
+
onClick: () => {
|
|
2251
|
+
setEditing(null);
|
|
2252
|
+
setError(null);
|
|
2253
|
+
}
|
|
2254
|
+
}, t("wfCancel"))));
|
|
2255
|
+
const comfyuiFold = (0, react.createElement)("details", { className: "dsc-fold" }, (0, react.createElement)("summary", null, `${t("wfComfyuiSection")} (${comfyui?.length ?? 0})`), (0, react.createElement)("div", { className: "dsc-fold-body" }, (0, react.createElement)("div", { className: "dsc-hint" }, t("wfComfyuiHint")), comfyuiError !== null ? (0, react.createElement)(ErrorNote, {
|
|
2256
|
+
t,
|
|
2257
|
+
message: comfyuiError
|
|
2258
|
+
}) : null, comfyui === null ? (0, react.createElement)("div", { className: "dsc-meta" }, "…") : comfyui.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, t("wfComfyuiEmpty")) : (0, react.createElement)("div", { className: "dsc-list" }, comfyui.map((workflow) => (0, react.createElement)("div", {
|
|
2259
|
+
key: workflow.name,
|
|
2260
|
+
className: "dsc-wf dsc-wf--comfyui",
|
|
2261
|
+
title: t("wfNameCopyHint"),
|
|
2262
|
+
onClick: () => {
|
|
2263
|
+
copyText(workflow.name).then((ok) => {
|
|
2264
|
+
if (ok) setNotice(`${t("wfNameCopied")}:${workflow.name}`);
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}, (0, react.createElement)("div", { className: "dsc-wf-col" }, (0, react.createElement)("div", {
|
|
2268
|
+
className: "dsc-wf-name",
|
|
2269
|
+
title: workflow.name
|
|
2270
|
+
}, workflow.name), (0, react.createElement)("div", { className: "dsc-wf-actions" }, (0, react.createElement)("button", {
|
|
2271
|
+
className: "dsc-btn",
|
|
2272
|
+
disabled: busy,
|
|
2273
|
+
onClick: (event) => {
|
|
2274
|
+
event.stopPropagation();
|
|
2275
|
+
view(workflow.name);
|
|
2276
|
+
}
|
|
2277
|
+
}, t("wfView")), (0, react.createElement)("button", {
|
|
2278
|
+
className: "dsc-btn",
|
|
2279
|
+
disabled: busy,
|
|
2280
|
+
onClick: (event) => {
|
|
2281
|
+
event.stopPropagation();
|
|
2282
|
+
openExtract(workflow.name);
|
|
2283
|
+
}
|
|
2284
|
+
}, t("wfExtract"))), (0, react.createElement)("div", { className: "dsc-meta" }, [formatBytes(workflow.size), workflow.modified !== void 0 ? formatTs(new Date(workflow.modified).toISOString()) : ""].filter(Boolean).join(" · "))), (0, react.createElement)("span", { className: workflow.extracted ? "dsc-badge dsc-badge--ok" : "dsc-badge dsc-badge--warn" }, workflow.extracted ? `${t("wfExtracted")} ${workflow.derived.length}` : t("wfNotExtracted")))))));
|
|
2285
|
+
const tagCounts = /* @__PURE__ */ new Map();
|
|
2286
|
+
for (const workflow of list) for (const tag of workflow.tags ?? []) tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
2287
|
+
const visibleTags = [...tagCounts.entries()].sort((a, b) => {
|
|
2288
|
+
const pa = PRESET_TAGS.indexOf(a[0]);
|
|
2289
|
+
const pb = PRESET_TAGS.indexOf(b[0]);
|
|
2290
|
+
if (pa !== -1 && pb !== -1) return pa - pb;
|
|
2291
|
+
if (pa !== -1) return -1;
|
|
2292
|
+
if (pb !== -1) return 1;
|
|
2293
|
+
return b[1] - a[1];
|
|
2294
|
+
}).map(([tag]) => tag);
|
|
2295
|
+
const visibleList = tagFilter === null ? list : list.filter((workflow) => (workflow.tags ?? []).includes(tagFilter));
|
|
2296
|
+
const apiFold = (0, react.createElement)("details", {
|
|
2297
|
+
className: "dsc-fold",
|
|
2298
|
+
open: true
|
|
2299
|
+
}, (0, react.createElement)("summary", null, `${t("wfLibrarySection")} (${visibleList.length})`), (0, react.createElement)("div", { className: "dsc-fold-body" }, visibleTags.length > 0 || tagFilter !== null ? (0, react.createElement)("div", { className: "dsc-tag-filter" }, (0, react.createElement)("button", {
|
|
2300
|
+
className: `dsc-tag-chip${tagFilter === null ? " dsc-tag-chip--active" : ""}`,
|
|
2301
|
+
onClick: () => setTagFilter(null)
|
|
2302
|
+
}, `${t("wfTagAll")} (${list.length})`), visibleTags.map((tag) => (0, react.createElement)("button", {
|
|
2303
|
+
key: tag,
|
|
2304
|
+
className: `dsc-tag-chip${tagFilter === tag ? " dsc-tag-chip--active" : ""}`,
|
|
2305
|
+
onClick: () => setTagFilter(tagFilter === tag ? null : tag)
|
|
2306
|
+
}, `${tag} (${tagCounts.get(tag) ?? 0})`))) : null, visibleList.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, t("wfEmpty")) : (0, react.createElement)("div", { className: "dsc-list" }, visibleList.map((workflow) => (0, react.createElement)("div", {
|
|
2307
|
+
key: workflow.id,
|
|
2308
|
+
className: "dsc-wf",
|
|
2309
|
+
title: t("wfNameCopyHint"),
|
|
2310
|
+
onClick: () => {
|
|
2311
|
+
copyText(workflow.name).then((ok) => {
|
|
2312
|
+
if (ok) setNotice(`${t("wfNameCopied")}:${workflow.name}`);
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}, (0, react.createElement)("div", { className: "dsc-wf-top" }, (0, react.createElement)("div", {
|
|
2316
|
+
className: "dsc-wf-name",
|
|
2317
|
+
title: workflow.name
|
|
2318
|
+
}, workflow.name), (workflow.tags ?? []).length > 0 ? (0, react.createElement)("div", { className: "dsc-wf-tags" }, (workflow.tags ?? []).map((tag) => (0, react.createElement)("span", {
|
|
2319
|
+
key: tag,
|
|
2320
|
+
className: "dsc-tag-chip dsc-tag-chip--mini"
|
|
2321
|
+
}, tag))) : null), workflow.description !== "" ? (0, react.createElement)("div", { className: "dsc-wf-desc" }, workflow.description) : null, (0, react.createElement)("div", { className: "dsc-wf-updated" }, `${t("wfUpdated")} ${formatTs(workflow.updatedAt)}`), (0, react.createElement)("div", { className: "dsc-wf-actions" }, (0, react.createElement)("button", {
|
|
2322
|
+
className: "dsc-btn",
|
|
2323
|
+
disabled: busy,
|
|
2324
|
+
onClick: (event) => {
|
|
2325
|
+
event.stopPropagation();
|
|
2326
|
+
run(workflow.id);
|
|
2327
|
+
}
|
|
2328
|
+
}, t("wfRun")), (0, react.createElement)("button", {
|
|
2329
|
+
className: "dsc-btn",
|
|
2330
|
+
disabled: busy,
|
|
2331
|
+
onClick: (event) => {
|
|
2332
|
+
event.stopPropagation();
|
|
2333
|
+
setEditing({
|
|
2334
|
+
...workflow,
|
|
2335
|
+
workflow: JSON.stringify(workflow.workflow, null, 2),
|
|
2336
|
+
parameters: workflow.parameters ?? [],
|
|
2337
|
+
tags: workflow.tags ?? []
|
|
2338
|
+
});
|
|
2339
|
+
setError(null);
|
|
2340
|
+
}
|
|
2341
|
+
}, t("wfEdit")), (0, react.createElement)("button", {
|
|
2342
|
+
className: "dsc-btn",
|
|
2343
|
+
disabled: busy,
|
|
2344
|
+
onClick: (event) => {
|
|
2345
|
+
event.stopPropagation();
|
|
2346
|
+
remove(workflow.id);
|
|
2347
|
+
}
|
|
2348
|
+
}, t("wfDelete"))))))));
|
|
2349
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "dsc-toolbar" }, (0, react.createElement)("button", {
|
|
2350
|
+
className: "dsc-btn",
|
|
2351
|
+
disabled: busy,
|
|
2352
|
+
onClick: () => {
|
|
2353
|
+
setEditing({
|
|
2354
|
+
workflow: {},
|
|
2355
|
+
parameters: [],
|
|
2356
|
+
tags: []
|
|
2357
|
+
});
|
|
2358
|
+
setError(null);
|
|
2359
|
+
}
|
|
2360
|
+
}, t("wfAdd")), (0, react.createElement)("label", { className: "dsc-btn" }, t("wfImportFile"), (0, react.createElement)("input", {
|
|
2361
|
+
type: "file",
|
|
2362
|
+
accept: ".json,application/json",
|
|
2363
|
+
style: { display: "none" },
|
|
2364
|
+
onChange: (event) => {
|
|
2365
|
+
const file = event.target.files?.[0];
|
|
2366
|
+
if (file !== void 0) importFile(file);
|
|
2367
|
+
event.target.value = "";
|
|
2368
|
+
}
|
|
2369
|
+
})), (0, react.createElement)("button", {
|
|
2370
|
+
className: "dsc-btn",
|
|
2371
|
+
disabled: busy,
|
|
2372
|
+
onClick: () => void load()
|
|
2373
|
+
}, t("wfRefresh"))), notice !== null ? (0, react.createElement)("div", { className: "dsc-ok" }, notice) : null, comfyuiFold, apiFold, (0, react.createElement)(LoadArea, {
|
|
2374
|
+
t,
|
|
2375
|
+
onNotice: setNotice
|
|
2376
|
+
}));
|
|
2377
|
+
}
|
|
2378
|
+
/** Extract-mode chooser: shows the canvas analysis and lets the user pick how
|
|
2379
|
+
* to extract runnable API workflows from a ComfyUI-side graph. */
|
|
2380
|
+
function ExtractDialog(props) {
|
|
2381
|
+
const { t, state, onUpdate, onRun, onClose } = props;
|
|
2382
|
+
(0, react.useEffect)(() => {
|
|
2383
|
+
if (state.analysis === null && state.error === null) {
|
|
2384
|
+
let cancelled = false;
|
|
2385
|
+
getJson(`/comfyui/comfy-workflows/analyze?file=${encodeURIComponent(state.file)}`).then((data) => {
|
|
2386
|
+
if (cancelled) return;
|
|
2387
|
+
if (data.ok !== true || data.analysis === void 0) onUpdate({
|
|
2388
|
+
...state,
|
|
2389
|
+
error: data.error ?? "failed to analyze"
|
|
2390
|
+
});
|
|
2391
|
+
else onUpdate({
|
|
2392
|
+
...state,
|
|
2393
|
+
analysis: data.analysis,
|
|
2394
|
+
mode: data.analysis.mode === "single" ? "all" : "split"
|
|
2395
|
+
});
|
|
2396
|
+
}).catch((cause) => {
|
|
2397
|
+
if (!cancelled) onUpdate({
|
|
2398
|
+
...state,
|
|
2399
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
2400
|
+
});
|
|
2401
|
+
});
|
|
2402
|
+
return () => {
|
|
2403
|
+
cancelled = true;
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
const modes = [
|
|
2408
|
+
{
|
|
2409
|
+
mode: "all",
|
|
2410
|
+
label: t("wfExtractModeAll"),
|
|
2411
|
+
hint: t("wfExtractModeAllHint")
|
|
2412
|
+
},
|
|
2413
|
+
{
|
|
2414
|
+
mode: "split",
|
|
2415
|
+
label: t("wfExtractModeSplit"),
|
|
2416
|
+
hint: t("wfExtractModeSplitHint")
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
mode: "main",
|
|
2420
|
+
label: t("wfExtractModeMain"),
|
|
2421
|
+
hint: t("wfExtractModeMainHint")
|
|
2422
|
+
}
|
|
2423
|
+
];
|
|
2424
|
+
const analysis = state.analysis;
|
|
2425
|
+
return (0, react.createElement)("div", { className: "dsc-dialog" }, (0, react.createElement)("div", { className: "dsc-dialog-head" }, (0, react.createElement)("span", { className: "dsc-wf-name" }, `${t("wfExtractTitle")}: ${state.file}`), (0, react.createElement)("button", {
|
|
2426
|
+
className: "dsc-panel-close",
|
|
2427
|
+
"aria-label": t("close"),
|
|
2428
|
+
onClick: onClose
|
|
2429
|
+
}, "✕")), analysis === null ? (0, react.createElement)("div", { className: "dsc-meta" }, "…") : (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "dsc-hint" }, `${t("wfComponents")}: ${analysis.components.length} · ${t("wfBypassed")}: ${analysis.bypassedCount} · ${t("wfIsolated")}: ${analysis.isolated.length}`), analysis.components.length > 0 ? (0, react.createElement)("ul", { className: "dsc-node-list" }, analysis.components.map((component) => (0, react.createElement)("li", { key: String(component.index) }, `${t("wfComponentLabel")} ${component.index}(${component.size} ${t("wfNodes")})${component.groups.length > 0 ? `[${component.groups.slice(0, 3).join("+")}]` : ""}`))) : (0, react.createElement)("div", { className: "dsc-meta" }, t("wfExtractEmpty")), (0, react.createElement)("div", { className: "dsc-extract-modes" }, modes.map((entry) => (0, react.createElement)("label", {
|
|
2430
|
+
key: entry.mode,
|
|
2431
|
+
className: "dsc-mode"
|
|
2432
|
+
}, (0, react.createElement)("input", {
|
|
2433
|
+
type: "radio",
|
|
2434
|
+
name: "extract-mode",
|
|
2435
|
+
checked: state.mode === entry.mode,
|
|
2436
|
+
onChange: () => onUpdate({
|
|
2437
|
+
...state,
|
|
2438
|
+
mode: entry.mode
|
|
2439
|
+
})
|
|
2440
|
+
}), (0, react.createElement)("span", { className: "dsc-mode-label" }, entry.label), (0, react.createElement)("span", { className: "dsc-hint" }, entry.hint)))), state.error !== null ? (0, react.createElement)(ErrorNote, {
|
|
2441
|
+
t,
|
|
2442
|
+
message: state.error
|
|
2443
|
+
}) : null, (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2444
|
+
className: "dsc-btn",
|
|
2445
|
+
disabled: analysis.components.length === 0,
|
|
2446
|
+
onClick: () => void onRun(state)
|
|
2447
|
+
}, t("wfExtractRun")), (0, react.createElement)("button", {
|
|
2448
|
+
className: "dsc-btn",
|
|
2449
|
+
onClick: onClose
|
|
2450
|
+
}, t("wfCancel")))));
|
|
2451
|
+
}
|
|
2452
|
+
/** Read-only inspection of a ComfyUI-side saved workflow: node list + JSON. */
|
|
2453
|
+
function WorkflowView(props) {
|
|
2454
|
+
const nodes = Array.isArray(props.graph.nodes) ? props.graph.nodes.filter((node) => node.mode !== 4 && typeof node.type === "string" && node.type !== "") : [];
|
|
2455
|
+
const json = JSON.stringify(props.graph, null, 2);
|
|
2456
|
+
return (0, react.createElement)("div", { className: "dsc-view" }, (0, react.createElement)("div", { className: "dsc-view-head" }, (0, react.createElement)("span", { className: "dsc-wf-name" }, props.file), (0, react.createElement)("button", {
|
|
2457
|
+
className: "dsc-btn",
|
|
2458
|
+
onClick: props.onBack
|
|
2459
|
+
}, props.t("wfViewBack"))), (0, react.createElement)("div", { className: "dsc-view-label" }, `${props.t("wfViewNodes")} (${nodes.length})`), nodes.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, props.t("wfViewEmpty")) : (0, react.createElement)("ul", { className: "dsc-node-list" }, nodes.map((node) => (0, react.createElement)("li", { key: String(node.id) }, `${String(node.id)} · ${String(node.type)}`))), (0, react.createElement)("div", { className: "dsc-view-label" }, props.t("wfViewJson")), (0, react.createElement)("textarea", {
|
|
2460
|
+
className: "dsc-textarea dsc-textarea--json dsc-textarea--view",
|
|
2461
|
+
readOnly: true,
|
|
2462
|
+
value: json
|
|
2463
|
+
}));
|
|
2464
|
+
}
|
|
2465
|
+
/** Tab 2: asset preview — thumbnails of everything the plugin generated. */
|
|
2466
|
+
function AssetsTab({ t, onPreview }) {
|
|
2467
|
+
const [assets, setAssets] = (0, react.useState)(null);
|
|
2468
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2469
|
+
const [filter, setFilter] = (0, react.useState)(() => panelStore.getAssetFilter());
|
|
2470
|
+
const load = async () => {
|
|
2471
|
+
try {
|
|
2472
|
+
const data = await getJson("/comfyui/assets");
|
|
2473
|
+
setAssets(data.assets);
|
|
2474
|
+
setError(null);
|
|
2475
|
+
} catch (cause) {
|
|
2476
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
(0, react.useEffect)(() => {
|
|
2480
|
+
let cancelled = false;
|
|
2481
|
+
getJson("/comfyui/assets").then((data) => {
|
|
2482
|
+
if (!cancelled) setAssets(data.assets);
|
|
2483
|
+
}).catch((cause) => {
|
|
2484
|
+
if (!cancelled) setError(cause instanceof Error ? cause.message : String(cause));
|
|
2485
|
+
});
|
|
2486
|
+
return () => {
|
|
2487
|
+
cancelled = true;
|
|
2488
|
+
};
|
|
2489
|
+
}, []);
|
|
2490
|
+
if (error !== null) return (0, react.createElement)("div", null, (0, react.createElement)(ErrorNote, {
|
|
2491
|
+
t,
|
|
2492
|
+
message: error
|
|
2493
|
+
}));
|
|
2494
|
+
if (assets === null) return (0, react.createElement)("div", { className: "dsc-meta" }, "…");
|
|
2495
|
+
const names = [...new Set(assets.map((asset) => asset.workflowName ?? ""))];
|
|
2496
|
+
const visible = filter === "" ? assets : assets.filter((asset) => (asset.workflowName ?? "") === filter);
|
|
2497
|
+
const previewItems = visible.flatMap((asset) => asset.media.map((item) => ({
|
|
2498
|
+
url: item.url,
|
|
2499
|
+
kind: item.kind
|
|
2500
|
+
})));
|
|
2501
|
+
const openAsset = (asset) => {
|
|
2502
|
+
const first = asset.media[0];
|
|
2503
|
+
const index = first !== void 0 ? previewItems.findIndex((item) => item.url === first.url) : 0;
|
|
2504
|
+
onPreview(previewItems.map((item) => item.url), previewItems.map((item) => item.kind), index < 0 ? 0 : index);
|
|
2505
|
+
};
|
|
2506
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "dsc-toolbar" }, (0, react.createElement)("select", {
|
|
2507
|
+
className: "dsc-input dsc-input--inline",
|
|
2508
|
+
value: filter,
|
|
2509
|
+
onChange: (event) => setFilter(event.target.value)
|
|
2510
|
+
}, (0, react.createElement)("option", { value: "" }, t("assetAll")), names.map((name) => (0, react.createElement)("option", {
|
|
2511
|
+
key: name,
|
|
2512
|
+
value: name
|
|
2513
|
+
}, name === "" ? "—" : name))), (0, react.createElement)("button", {
|
|
2514
|
+
className: "dsc-btn",
|
|
2515
|
+
onClick: () => void load()
|
|
2516
|
+
}, t("refresh"))), visible.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, t("assetEmpty")) : (0, react.createElement)("div", { className: "dsc-assets-grid" }, visible.map((asset) => (0, react.createElement)(AssetThumb, {
|
|
2517
|
+
key: asset.promptId,
|
|
2518
|
+
asset,
|
|
2519
|
+
t,
|
|
2520
|
+
onClick: () => openAsset(asset)
|
|
2521
|
+
}))));
|
|
2522
|
+
}
|
|
2523
|
+
function AssetThumb(props) {
|
|
2524
|
+
const first = props.asset.media[0];
|
|
2525
|
+
const [broken, setBroken] = (0, react.useState)(false);
|
|
2526
|
+
if (first === void 0 || broken) return (0, react.createElement)("div", {
|
|
2527
|
+
className: "dsc-asset dsc-asset--empty",
|
|
2528
|
+
onClick: props.onClick
|
|
2529
|
+
}, (0, react.createElement)("div", null, props.t("assetGone")), (0, react.createElement)("div", { className: "dsc-meta" }, props.asset.workflowName ?? "—"));
|
|
2530
|
+
return (0, react.createElement)("div", {
|
|
2531
|
+
className: "dsc-asset",
|
|
2532
|
+
onClick: props.onClick
|
|
2533
|
+
}, first.kind === "video" ? (0, react.createElement)("video", {
|
|
2534
|
+
src: first.url,
|
|
2535
|
+
controls: true,
|
|
2536
|
+
preload: "metadata",
|
|
2537
|
+
onClick: (event) => event.stopPropagation()
|
|
2538
|
+
}) : (0, react.createElement)("img", {
|
|
2539
|
+
src: first.url,
|
|
2540
|
+
alt: first.filename,
|
|
2541
|
+
loading: "lazy",
|
|
2542
|
+
onError: () => setBroken(true)
|
|
2543
|
+
}), (0, react.createElement)("div", { className: "dsc-asset-meta" }, props.asset.workflowName ?? "—"));
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Tab 3: the task center — active tasks (pending / generating) shown as
|
|
2547
|
+
* cards with id, status and progress, plus history filtered by the
|
|
2548
|
+
* completed / failed / cancelled chips. Polled every 3 seconds.
|
|
2549
|
+
*/
|
|
2550
|
+
function QueueTab({ t, onPreview }) {
|
|
2551
|
+
const [filter, setFilter] = (0, react.useState)("all");
|
|
2552
|
+
const [active, setActive] = (0, react.useState)(null);
|
|
2553
|
+
const [history, setHistory] = (0, react.useState)(null);
|
|
2554
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2555
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2556
|
+
const load = async (current) => {
|
|
2557
|
+
try {
|
|
2558
|
+
const historyStatus = current === "all" ? "completed,failed,cancelled" : current;
|
|
2559
|
+
const [activeData, historyData] = await Promise.all([getJson("/comfyui/jobs?status=pending,in_progress&limit=100"), getJson(`/comfyui/jobs?status=${historyStatus}&limit=100`)]);
|
|
2560
|
+
if (activeData.ok !== true) throw new Error(activeData.error ?? t("queueError"));
|
|
2561
|
+
if (historyData.ok !== true) throw new Error(historyData.error ?? t("queueError"));
|
|
2562
|
+
setActive([...activeData.jobs ?? []].sort((a, b) => (a.createTime ?? 0) - (b.createTime ?? 0)));
|
|
2563
|
+
setHistory(historyData.jobs);
|
|
2564
|
+
setError(null);
|
|
2565
|
+
} catch (cause) {
|
|
2566
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
(0, react.useEffect)(() => {
|
|
2570
|
+
load(filter);
|
|
2571
|
+
const timer = setInterval(() => void load(filter), 3e3);
|
|
2572
|
+
return () => clearInterval(timer);
|
|
2573
|
+
}, [filter]);
|
|
2574
|
+
const act = async (payload) => {
|
|
2575
|
+
setBusy(true);
|
|
2576
|
+
try {
|
|
2577
|
+
const data = await postJson("/comfyui/jobs/actions", payload);
|
|
2578
|
+
if (data.ok !== true) throw new Error(data.error ?? "action failed");
|
|
2579
|
+
await load(filter);
|
|
2580
|
+
} catch (cause) {
|
|
2581
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2582
|
+
} finally {
|
|
2583
|
+
setBusy(false);
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
if (error !== null) return (0, react.createElement)("div", null, (0, react.createElement)(ErrorNote, {
|
|
2587
|
+
t,
|
|
2588
|
+
message: error
|
|
2589
|
+
}));
|
|
2590
|
+
if (active === null || history === null) return (0, react.createElement)("div", { className: "dsc-meta" }, "…");
|
|
2591
|
+
const historyPreviewUrls = history.map(previewUrlOf);
|
|
2592
|
+
const previewImages = historyPreviewUrls.filter((url) => url !== null);
|
|
2593
|
+
const previewPrefix = [];
|
|
2594
|
+
{
|
|
2595
|
+
let acc = 0;
|
|
2596
|
+
for (const url of historyPreviewUrls) {
|
|
2597
|
+
previewPrefix.push(acc);
|
|
2598
|
+
if (url !== null) acc += 1;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return (0, react.createElement)("div", { className: "dsc-list" }, (0, react.createElement)("div", { className: "dsc-queue-actions" }, (0, react.createElement)("button", {
|
|
2602
|
+
className: "dsc-btn",
|
|
2603
|
+
onClick: () => void act({ action: "clear" }),
|
|
2604
|
+
disabled: busy
|
|
2605
|
+
}, t("jobClearQueue")), (0, react.createElement)("button", {
|
|
2606
|
+
className: "dsc-btn",
|
|
2607
|
+
onClick: () => void act({ action: "clearHistory" }),
|
|
2608
|
+
disabled: busy
|
|
2609
|
+
}, t("jobClearHistory")), (0, react.createElement)("button", {
|
|
2610
|
+
className: "dsc-btn",
|
|
2611
|
+
onClick: () => void act({
|
|
2612
|
+
action: "free",
|
|
2613
|
+
unloadModels: true,
|
|
2614
|
+
freeMemory: true
|
|
2615
|
+
}),
|
|
2616
|
+
disabled: busy
|
|
2617
|
+
}, t("jobFree"))), (0, react.createElement)("div", { className: "dsc-section" }, (0, react.createElement)("div", { className: "dsc-section-head" }, (0, react.createElement)("span", { className: "dsc-section-title" }, `${t("jobActive")} (${active.length})`)), active.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, t("queueEmpty")) : (0, react.createElement)("div", { className: "dsc-list" }, active.map((job) => (0, react.createElement)(JobRow, {
|
|
2618
|
+
key: job.id,
|
|
2619
|
+
job,
|
|
2620
|
+
t,
|
|
2621
|
+
busy,
|
|
2622
|
+
act
|
|
2623
|
+
})))), (0, react.createElement)("div", { className: "dsc-section" }, (0, react.createElement)("div", { className: "dsc-section-head" }, (0, react.createElement)("span", { className: "dsc-section-title" }, `${t("jobFilterLabel")} (${history.length})`), (0, react.createElement)("div", { className: "dsc-queue-head" }, JOB_FILTERS.map((item) => (0, react.createElement)("button", {
|
|
2624
|
+
key: item.key,
|
|
2625
|
+
className: `dsc-chip${filter === item.key ? " dsc-chip--active" : ""}`,
|
|
2626
|
+
onClick: () => setFilter(item.key),
|
|
2627
|
+
disabled: busy
|
|
2628
|
+
}, t(item.label))))), history.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, t("queueEmpty")) : (0, react.createElement)("div", { className: "dsc-list" }, history.map((job, index) => (0, react.createElement)(JobRow, {
|
|
2629
|
+
key: job.id,
|
|
2630
|
+
job,
|
|
2631
|
+
t,
|
|
2632
|
+
busy,
|
|
2633
|
+
act,
|
|
2634
|
+
onPreview: historyPreviewUrls[index] !== null && previewImages.length > 0 ? () => onPreview(previewImages, previewImages.map(() => "image"), previewPrefix[index]) : void 0
|
|
2635
|
+
})))));
|
|
2636
|
+
}
|
|
2637
|
+
function jobBadge(status) {
|
|
2638
|
+
switch (status) {
|
|
2639
|
+
case "in_progress": return "dsc-badge--info";
|
|
2640
|
+
case "completed": return "dsc-badge--ok";
|
|
2641
|
+
case "failed": return "dsc-badge--danger";
|
|
2642
|
+
default: return "";
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
const STATUS_LABELS = {
|
|
2646
|
+
pending: "jobFilterPending",
|
|
2647
|
+
in_progress: "jobFilterInProgress",
|
|
2648
|
+
completed: "jobFilterCompleted",
|
|
2649
|
+
failed: "jobFilterFailed",
|
|
2650
|
+
cancelled: "jobFilterCancelled"
|
|
2651
|
+
};
|
|
2652
|
+
/**
|
|
2653
|
+
* One task card: a compact row with a status badge, task name + short id,
|
|
2654
|
+
* duration (and error/progress for in-flight tasks), and a ⋯ menu with
|
|
2655
|
+
* per-state actions. Hover highlights the row. Rendered as a component
|
|
2656
|
+
* instance (h(JobRow, ...)) so its hooks stay per-card.
|
|
2657
|
+
*/
|
|
2658
|
+
function JobRow(props) {
|
|
2659
|
+
const { job, t, busy, act, onPreview } = props;
|
|
2660
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
2661
|
+
const menuRef = (0, react.useRef)(null);
|
|
2662
|
+
(0, react.useEffect)(() => {
|
|
2663
|
+
if (!menuOpen) return;
|
|
2664
|
+
const onDoc = (event) => {
|
|
2665
|
+
if (menuRef.current !== null && !menuRef.current.contains(event.target)) setMenuOpen(false);
|
|
2666
|
+
};
|
|
2667
|
+
document.addEventListener("mousedown", onDoc);
|
|
2668
|
+
return () => document.removeEventListener("mousedown", onDoc);
|
|
2669
|
+
}, [menuOpen]);
|
|
2670
|
+
const running = job.status === "in_progress";
|
|
2671
|
+
const terminal = job.status === "completed" || job.status === "failed" || job.status === "cancelled";
|
|
2672
|
+
const pct = job.progress !== null && job.progress !== void 0 && job.progress.max > 0 ? Math.min(100, Math.round(job.progress.value / job.progress.max * 100)) : null;
|
|
2673
|
+
const duration = job.executionStartTime !== null && job.executionEndTime !== null ? Math.max(0, Math.round(((job.executionEndTime ?? 0) - (job.executionStartTime ?? 0)) / 1e3)) : null;
|
|
2674
|
+
const errMsg = job.executionError?.exception_message ?? job.executionError?.message;
|
|
2675
|
+
const name = job.workflowName ?? job.workflowId;
|
|
2676
|
+
const preview = previewUrlOf(job);
|
|
2677
|
+
const close = () => setMenuOpen(false);
|
|
2678
|
+
const statusClass = job.status === "failed" ? " dsc-job-item--failed" : job.status === "cancelled" ? " dsc-job-item--cancelled" : "";
|
|
2679
|
+
return (0, react.createElement)("div", { className: `dsc-job-item${statusClass}` }, preview !== null ? (0, react.createElement)("img", {
|
|
2680
|
+
src: preview,
|
|
2681
|
+
alt: "",
|
|
2682
|
+
loading: "lazy",
|
|
2683
|
+
className: onPreview !== void 0 ? "dsc-job-preview dsc-job-preview--clickable" : "dsc-job-preview",
|
|
2684
|
+
onClick: onPreview
|
|
2685
|
+
}) : null, (0, react.createElement)("div", { className: "dsc-job-main" }, (0, react.createElement)("div", {
|
|
2686
|
+
className: "dsc-job-name",
|
|
2687
|
+
title: name ?? void 0
|
|
2688
|
+
}, name !== null && name !== void 0 && name !== "" ? name : "—"), (0, react.createElement)("div", { className: "dsc-job-mid" }, running && job.progress !== null && job.progress !== void 0 ? (0, react.createElement)("div", { className: "dsc-progress" }, (0, react.createElement)("div", { className: "dsc-progress-track" }, (0, react.createElement)("div", {
|
|
2689
|
+
className: "dsc-progress-fill",
|
|
2690
|
+
style: { width: `${pct ?? 0}%` }
|
|
2691
|
+
})), (0, react.createElement)("span", { className: "dsc-job-progress" }, `${job.progress.value}/${job.progress.max} · ${pct ?? 0}%`)) : null, (0, react.createElement)("div", { className: "dsc-job-mid-right" }, (0, react.createElement)("span", { className: `dsc-badge dsc-badge--status ${jobBadge(job.status)}` }, t(STATUS_LABELS[job.status] ?? job.status)), (0, react.createElement)("div", {
|
|
2692
|
+
ref: menuRef,
|
|
2693
|
+
className: "dsc-job-menu"
|
|
2694
|
+
}, (0, react.createElement)("button", {
|
|
2695
|
+
className: "dsc-job-menu-btn",
|
|
2696
|
+
"aria-label": t("jobMore"),
|
|
2697
|
+
onClick: () => setMenuOpen(!menuOpen)
|
|
2698
|
+
}, "⋯"), menuOpen ? (0, react.createElement)("div", { className: "dsc-job-menu-pop" }, job.status === "pending" ? (0, react.createElement)("button", { onClick: () => {
|
|
2699
|
+
act({
|
|
2700
|
+
action: "delete",
|
|
2701
|
+
ids: [job.id]
|
|
2702
|
+
});
|
|
2703
|
+
close();
|
|
2704
|
+
} }, t("jobDelete")) : null, running ? (0, react.createElement)("button", { onClick: () => {
|
|
2705
|
+
act({
|
|
2706
|
+
action: "interrupt",
|
|
2707
|
+
promptId: job.id
|
|
2708
|
+
});
|
|
2709
|
+
close();
|
|
2710
|
+
} }, t("jobInterrupt")) : null, terminal ? (0, react.createElement)("button", { onClick: () => {
|
|
2711
|
+
act({
|
|
2712
|
+
action: "deleteHistory",
|
|
2713
|
+
ids: [job.id]
|
|
2714
|
+
});
|
|
2715
|
+
close();
|
|
2716
|
+
} }, t("jobDelete")) : null, terminal ? (0, react.createElement)("button", { onClick: () => {
|
|
2717
|
+
act({
|
|
2718
|
+
action: "rerun",
|
|
2719
|
+
jobId: job.id
|
|
2720
|
+
});
|
|
2721
|
+
close();
|
|
2722
|
+
} }, t("jobRerun")) : null, terminal ? (0, react.createElement)("button", { onClick: () => {
|
|
2723
|
+
panelStore.setAssetFilter(name ?? "");
|
|
2724
|
+
panelStore.setTab("assets");
|
|
2725
|
+
close();
|
|
2726
|
+
} }, t("jobViewAssets")) : null) : null))), (0, react.createElement)("div", { className: "dsc-job-bottom" }, (0, react.createElement)("span", { className: "dsc-job-duration" }, `${t("jobDuration")} ${duration ?? 0}s`), errMsg !== void 0 && errMsg !== null ? (0, react.createElement)("span", {
|
|
2727
|
+
className: "dsc-job-error",
|
|
2728
|
+
title: errMsg
|
|
2729
|
+
}, errMsg) : null)));
|
|
2730
|
+
}
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/client/styles.ts
|
|
2733
|
+
/**
|
|
2734
|
+
* dsh-comfyui stylesheet, injected once with plugin ownership. Colors come
|
|
2735
|
+
* from the host theme tokens (--dsw-alias-*), so the UI follows light/dark.
|
|
2736
|
+
*/
|
|
2737
|
+
const CSS = `
|
|
2738
|
+
/* --- shared primitives --- */
|
|
2739
|
+
.dsc-card {
|
|
2740
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
2741
|
+
border-radius: 8px;
|
|
2742
|
+
padding: 10px 12px;
|
|
2743
|
+
margin: 4px 0;
|
|
2744
|
+
font-size: 13px;
|
|
2745
|
+
line-height: 1.5;
|
|
2746
|
+
color: var(--dsw-alias-label-primary);
|
|
2747
|
+
}
|
|
2748
|
+
.dsc-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
|
|
2749
|
+
.dsc-badge {
|
|
2750
|
+
border-radius: 999px; padding: 1px 8px; font-size: 11px;
|
|
2751
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
2752
|
+
color: var(--dsw-alias-label-secondary);
|
|
2753
|
+
}
|
|
2754
|
+
.dsc-badge--ok { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 16%, transparent); }
|
|
2755
|
+
.dsc-badge--err { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent); }
|
|
2756
|
+
.dsc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; margin-top: 6px; }
|
|
2757
|
+
.dsc-media { border-radius: 6px; overflow: hidden; background: var(--dsw-alias-bg-layer-1); }
|
|
2758
|
+
.dsc-media img, .dsc-media video, .dsc-media audio { display: block; width: 100%; max-height: 320px; object-fit: contain; background: #000; }
|
|
2759
|
+
.dsc-media audio { height: 44px; }
|
|
2760
|
+
.dsc-media-img--clickable { cursor: zoom-in; }
|
|
2761
|
+
.dsc-media-other { display: block; padding: 8px 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); word-break: break-all; }
|
|
2762
|
+
.dsc-media-meta { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 3px 6px; }
|
|
2763
|
+
.dsc-media-size { font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
|
|
2764
|
+
.dsc-media-meta a { display: inline; padding: 0; font-size: 11px; color: var(--dsw-alias-label-secondary); text-decoration: none; }
|
|
2765
|
+
.dsc-media-meta a:hover { color: var(--dsw-alias-label-primary); }
|
|
2766
|
+
.dsc-meta { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
2767
|
+
.dsc-err { color: var(--dsw-alias-state-error-primary); font-size: 12px; margin: 4px 0; }
|
|
2768
|
+
.dsc-ok { color: var(--dsw-alias-state-success-primary); font-size: 12px; margin: 4px 0; }
|
|
2769
|
+
.dsc-form { display: flex; flex-direction: column; gap: 12px; max-width: 460px; }
|
|
2770
|
+
.dsc-field label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--dsw-alias-label-primary); }
|
|
2771
|
+
.dsc-input, .dsc-textarea {
|
|
2772
|
+
width: 100%; box-sizing: border-box; padding: 6px 8px; border-radius: 6px;
|
|
2773
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
2774
|
+
background: transparent; color: inherit; font-size: 13px;
|
|
2775
|
+
}
|
|
2776
|
+
select.dsc-input { background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); color-scheme: inherit; }
|
|
2777
|
+
select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); }
|
|
2778
|
+
.dsc-textarea { min-height: 90px; resize: vertical; font-family: ui-monospace, monospace; }
|
|
2779
|
+
.dsc-hint { font-size: 12px; color: var(--dsw-alias-label-secondary); margin-top: 2px; }
|
|
2780
|
+
.dsc-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
2781
|
+
.dsc-btn {
|
|
2782
|
+
border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px;
|
|
2783
|
+
background: transparent; color: inherit; padding: 5px 12px; font-size: 13px; cursor: pointer;
|
|
2784
|
+
}
|
|
2785
|
+
.dsc-btn:disabled { opacity: 0.5; cursor: default; }
|
|
2786
|
+
.dsc-btn--link { display: inline-block; text-decoration: none; }
|
|
2787
|
+
/* --- header action trigger --- */
|
|
2788
|
+
.dsc-trigger {
|
|
2789
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
2790
|
+
border: 1px solid transparent; background: transparent; color: var(--dsw-alias-label-secondary);
|
|
2791
|
+
cursor: pointer; font-size: 13px; padding: 3px 8px; border-radius: 6px;
|
|
2792
|
+
white-space: nowrap;
|
|
2793
|
+
}
|
|
2794
|
+
.dsc-trigger:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-border-l1); }
|
|
2795
|
+
.dsc-trigger[aria-pressed='true'] { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }
|
|
2796
|
+
.dsc-trigger-glyph { font-size: 13px; line-height: 1; }
|
|
2797
|
+
/* --- panel shell: floating window, percentage-anchored to the page header
|
|
2798
|
+
(top) and the composer (bottom), draggable by the header and resizable via
|
|
2799
|
+
the corner handle. Follows the host light/dark theme. --- */
|
|
2800
|
+
.dsc-panel {
|
|
2801
|
+
position: fixed;
|
|
2802
|
+
top: var(--dsc-panel-top, 9%);
|
|
2803
|
+
right: var(--dsc-panel-right, 1.5%);
|
|
2804
|
+
bottom: var(--dsc-panel-bottom, 2%);
|
|
2805
|
+
width: var(--dsc-panel-width, 400px);
|
|
2806
|
+
max-width: min(92vw, 560px); min-width: 300px;
|
|
2807
|
+
z-index: 900;
|
|
2808
|
+
display: flex; flex-direction: column;
|
|
2809
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
2810
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2811
|
+
border-radius: 14px;
|
|
2812
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
2813
|
+
color: var(--dsw-alias-label-primary);
|
|
2814
|
+
overflow: hidden;
|
|
2815
|
+
user-select: none;
|
|
2816
|
+
}
|
|
2817
|
+
.dsc-panel--dragging { cursor: grabbing; }
|
|
2818
|
+
.dsc-panel-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); cursor: grab; touch-action: none; }
|
|
2819
|
+
.dsc-panel--dragging .dsc-panel-head { cursor: grabbing; }
|
|
2820
|
+
.dsc-panel-resize {
|
|
2821
|
+
position: absolute; right: 0; bottom: 0; width: 16px; height: 16px;
|
|
2822
|
+
cursor: nwse-resize; touch-action: none;
|
|
2823
|
+
background: linear-gradient(135deg, transparent 50%, var(--dsw-alias-label-tertiary) 50%);
|
|
2824
|
+
border-bottom-right-radius: 14px;
|
|
2825
|
+
opacity: 0.55;
|
|
2826
|
+
}
|
|
2827
|
+
.dsc-panel-resize:hover { opacity: 1; }
|
|
2828
|
+
.dsc-panel-title { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; font-size: 14px; }
|
|
2829
|
+
.dsc-panel-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 14px; padding: 4px 8px; }
|
|
2830
|
+
.dsc-panel-close:hover { color: var(--dsw-alias-label-primary); }
|
|
2831
|
+
.dsc-tabs { display: flex; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
2832
|
+
.dsc-tab {
|
|
2833
|
+
flex: 1; padding: 8px 4px; background: transparent; border: none;
|
|
2834
|
+
border-bottom: 2px solid transparent; color: var(--dsw-alias-label-secondary);
|
|
2835
|
+
cursor: pointer; font-size: 13px;
|
|
2836
|
+
}
|
|
2837
|
+
.dsc-tab--active { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-brand-primary); }
|
|
2838
|
+
.dsc-panel-body { flex: 1; overflow-y: auto; padding: 10px 12px; }
|
|
2839
|
+
.dsc-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
|
|
2840
|
+
.dsc-list { display: flex; flex-direction: column; gap: 8px; }
|
|
2841
|
+
/* --- workflows tab --- */
|
|
2842
|
+
.dsc-wf { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 8px 10px; background: var(--dsw-alias-bg-layer-1); transition: background 0.12s, border-color 0.12s; cursor: pointer; }
|
|
2843
|
+
.dsc-wf:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l2); }
|
|
2844
|
+
.dsc-wf-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
2845
|
+
.dsc-wf--comfyui { display: flex; align-items: center; gap: 10px; }
|
|
2846
|
+
.dsc-wf-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
2847
|
+
.dsc-wf-name { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2848
|
+
.dsc-wf-desc { color: var(--dsw-alias-label-secondary); font-size: 12px; margin: 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2849
|
+
.dsc-wf-updated { color: var(--dsw-alias-label-tertiary); font-size: 11px; margin-top: 4px; }
|
|
2850
|
+
.dsc-wf-actions { display: flex; gap: 6px; margin-top: 6px; }
|
|
2851
|
+
.dsc-section { margin-top: 14px; }
|
|
2852
|
+
.dsc-section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
|
|
2853
|
+
.dsc-section-title { font-weight: 600; font-size: 13px; }
|
|
2854
|
+
.dsc-fold { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; margin-top: 10px; overflow: hidden; background: var(--dsw-alias-bg-layer-1); }
|
|
2855
|
+
.dsc-fold summary { list-style: none; display: flex; align-items: center; gap: 6px; padding: 8px 10px; cursor: pointer; user-select: none; font-weight: 600; font-size: 13px; }
|
|
2856
|
+
.dsc-fold summary::-webkit-details-marker { display: none; }
|
|
2857
|
+
.dsc-fold summary::before { content: '▸'; font-size: 11px; color: var(--dsw-alias-label-secondary); transition: transform 0.15s; }
|
|
2858
|
+
.dsc-fold[open] summary::before { transform: rotate(90deg); }
|
|
2859
|
+
.dsc-fold summary:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
2860
|
+
.dsc-fold-body { padding: 0 10px 10px; }
|
|
2861
|
+
.dsc-hint { color: var(--dsw-alias-label-secondary); font-size: 11px; margin: 2px 0 6px; white-space: pre-line; }
|
|
2862
|
+
.dsc-badge { font-size: 11px; padding: 1px 8px; border-radius: 999px; }
|
|
2863
|
+
.dsc-badge--ok { color: var(--dsw-alias-state-success-primary); border: 1px solid var(--dsw-alias-state-success-primary); }
|
|
2864
|
+
.dsc-badge--warn { color: var(--dsw-alias-state-warn-primary); border: 1px solid var(--dsw-alias-state-warn-primary); }
|
|
2865
|
+
.dsc-view-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
2866
|
+
.dsc-view-label { font-weight: 600; font-size: 12px; margin: 8px 0 4px; }
|
|
2867
|
+
.dsc-node-list { margin: 0; padding-left: 18px; font-size: 12px; color: var(--dsw-alias-label-secondary); max-height: 180px; overflow-y: auto; }
|
|
2868
|
+
.dsc-node-list li { margin: 2px 0; }
|
|
2869
|
+
.dsc-textarea--view { min-height: 220px; resize: vertical; }
|
|
2870
|
+
.dsc-derived { margin: 6px 0; display: flex; flex-direction: column; gap: 4px; }
|
|
2871
|
+
.dsc-derived-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }
|
|
2872
|
+
.dsc-derived-name { color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2873
|
+
.dsc-dialog { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 10px; }
|
|
2874
|
+
.dsc-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
|
2875
|
+
.dsc-extract-modes { display: flex; flex-direction: column; gap: 8px; margin: 10px 0; }
|
|
2876
|
+
.dsc-mode { display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 8px 10px; cursor: pointer; }
|
|
2877
|
+
.dsc-mode input { accent-color: var(--dsw-alias-brand-primary); }
|
|
2878
|
+
.dsc-mode-label { font-size: 13px; }
|
|
2879
|
+
/* --- assets tab --- */
|
|
2880
|
+
.dsc-assets-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; }
|
|
2881
|
+
.dsc-asset { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; overflow: hidden; cursor: pointer; background: var(--dsw-alias-bg-layer-1); }
|
|
2882
|
+
.dsc-asset img, .dsc-asset video { width: 100%; height: 110px; object-fit: cover; display: block; background: #000; }
|
|
2883
|
+
.dsc-asset--empty {
|
|
2884
|
+
height: 110px; padding: 8px; box-sizing: border-box;
|
|
2885
|
+
display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 4px;
|
|
2886
|
+
font-size: 11px; color: var(--dsw-alias-label-secondary); text-align: center;
|
|
2887
|
+
}
|
|
2888
|
+
.dsc-asset-meta { padding: 3px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2889
|
+
.dsc-asset-detail-media { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
|
|
2890
|
+
.dsc-asset-detail-media img, .dsc-asset-detail-media video { width: 100%; max-height: 320px; object-fit: contain; background: #000; border-radius: 8px; }
|
|
2891
|
+
.dsc-asset-detail-file { display: flex; align-items: center; justify-content: space-between; gap: 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 6px 8px; }
|
|
2892
|
+
.dsc-input--inline { width: auto; }
|
|
2893
|
+
/* --- queue tab --- */
|
|
2894
|
+
.dsc-queue-head { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
2895
|
+
.dsc-queue-item { display: flex; gap: 8px; align-items: center; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 6px 8px; font-size: 12px; flex-wrap: wrap; }
|
|
2896
|
+
.dsc-queue-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; }
|
|
2897
|
+
.dsc-queue-meta { color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
|
2898
|
+
.dsc-queue-tracked { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
|
|
2899
|
+
.dsc-progress { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 90px; }
|
|
2900
|
+
.dsc-progress-track { flex: 1; height: 6px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
|
|
2901
|
+
.dsc-progress-fill { height: 100%; background: #3b82f6; transition: width 0.4s; }
|
|
2902
|
+
.dsc-progress:hover .dsc-progress-fill { background: #2563eb; }
|
|
2903
|
+
.dsc-param-upload-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; }
|
|
2904
|
+
.dsc-param-upload-wrap select { flex: 1; min-width: 0; }
|
|
2905
|
+
.dsc-dropzone { flex: 1; min-width: 120px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 6px; padding: 5px 10px; font-size: 11px; color: var(--dsw-alias-label-secondary); text-align: center; cursor: pointer; transition: border-color 0.12s, color 0.12s; }
|
|
2906
|
+
.dsc-dropzone:hover, .dsc-dropzone--over { border-color: #3b82f6; color: #3b82f6; }
|
|
2907
|
+
.dsc-upload-select { position: relative; flex: 1; min-width: 0; }
|
|
2908
|
+
.dsc-upload-select-btn { display: flex; align-items: center; gap: 6px; width: 100%; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); cursor: pointer; font-size: 12px; text-align: left; }
|
|
2909
|
+
.dsc-upload-select-btn:hover { border-color: var(--dsw-alias-border-l2); }
|
|
2910
|
+
.dsc-upload-select-thumb { width: 36px; height: 24px; object-fit: cover; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); flex: none; }
|
|
2911
|
+
.dsc-upload-select-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
2912
|
+
.dsc-upload-select-pop { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 30; max-height: 220px; overflow-y: auto; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); padding: 4px; }
|
|
2913
|
+
.dsc-upload-select-item { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
2914
|
+
.dsc-upload-select-item:hover { background: var(--dsw-alias-bg-layer-3); }
|
|
2915
|
+
.dsc-upload-dock { margin-top: 14px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
2916
|
+
.dsc-upload-dock-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
2917
|
+
.dsc-upload-dock-title { font-weight: 600; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
2918
|
+
.dsc-upload-dock-zone { min-height: 44px; display: flex; align-items: center; justify-content: center; }
|
|
2919
|
+
.dsc-upload-list { display: flex; flex-direction: column; gap: 4px; }
|
|
2920
|
+
.dsc-upload-item { display: flex; align-items: center; gap: 8px; }
|
|
2921
|
+
.dsc-upload-thumb { width: 40px; height: 28px; object-fit: cover; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); flex: none; }
|
|
2922
|
+
.dsc-upload-thumb--media { display: inline-flex; align-items: center; justify-content: center; font-size: 14px; }
|
|
2923
|
+
.dsc-upload-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
2924
|
+
.dsc-upload-select-item--active { background: var(--dsw-alias-bg-layer-3); }
|
|
2925
|
+
/* --- load area: big preview + picker dialog (ComfyUI LoadImage-like) --- */
|
|
2926
|
+
.dsc-loadarea { margin-top: 14px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
2927
|
+
.dsc-loadarea-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
2928
|
+
.dsc-loadarea-title { font-weight: 600; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
2929
|
+
.dsc-loadarea-main { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 100%; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; padding: 10px; background: var(--dsw-alias-bg-layer-1); cursor: pointer; transition: border-color 0.12s; }
|
|
2930
|
+
.dsc-loadarea-main:hover { border-color: #3b82f6; }
|
|
2931
|
+
.dsc-loadarea-main:disabled { opacity: 0.6; cursor: default; }
|
|
2932
|
+
.dsc-loadarea-img { width: 100%; max-height: 260px; object-fit: contain; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
|
|
2933
|
+
.dsc-loadarea-empty { display: flex; align-items: center; justify-content: center; height: 120px; width: 100%; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-tertiary); font-size: 13px; }
|
|
2934
|
+
.dsc-loadarea-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
2935
|
+
/* picker overlay: dims the page, dialog floats in the center */
|
|
2936
|
+
.dsc-picker-overlay { position: fixed; inset: 0; z-index: 9990; background: rgba(6, 8, 12, 0.6); display: flex; align-items: center; justify-content: center; padding: 24px; }
|
|
2937
|
+
.dsc-picker { position: relative; display: flex; flex-direction: column; width: min(860px, 92vw); height: min(560px, 84vh); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; box-shadow: 0 16px 56px rgba(0, 0, 0, 0.5); overflow: hidden; }
|
|
2938
|
+
.dsc-picker-toast { position: absolute; top: 54px; left: 50%; transform: translateX(-50%); z-index: 5; background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); padding: 6px 14px; border-radius: 8px; font-size: 12px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); pointer-events: none; max-width: 80%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2939
|
+
.dsc-picker-bar { display: flex; align-items: center; gap: 4px; padding: 8px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); flex-wrap: wrap; }
|
|
2940
|
+
.dsc-picker-tab { border: 1px solid transparent; border-radius: 6px; padding: 4px 12px; font-size: 12px; cursor: pointer; background: transparent; color: var(--dsw-alias-label-primary); }
|
|
2941
|
+
.dsc-picker-tab--active { background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
2942
|
+
.dsc-picker-tabs { display: flex; gap: 4px; }
|
|
2943
|
+
.dsc-picker-type { flex: none; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); }
|
|
2944
|
+
.dsc-picker-upload { margin: 0 auto; flex: none; max-width: 200px; min-height: 32px; display: flex; align-items: center; justify-content: center; padding: 0 12px; font-size: 12px; white-space: nowrap; }
|
|
2945
|
+
.dsc-picker-empty { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; color: var(--dsw-alias-label-tertiary); font-size: 13px; }
|
|
2946
|
+
.dsc-picker-grid { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 12px; display: flex; gap: 10px; align-items: flex-start; }
|
|
2947
|
+
.dsc-picker-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
2948
|
+
.dsc-picker-card { display: flex; flex-direction: column; gap: 4px; min-width: 0; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 6px; background: var(--dsw-alias-bg-layer-2); cursor: pointer; text-align: left; }
|
|
2949
|
+
.dsc-picker-card:hover { border-color: #3b82f6; }
|
|
2950
|
+
.dsc-picker-card--active { border-color: var(--dsw-alias-brand-primary); box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }
|
|
2951
|
+
.dsc-picker-thumb { width: 100%; height: auto; object-fit: contain; border-radius: 6px; background: var(--dsw-alias-bg-layer-1); }
|
|
2952
|
+
.dsc-picker-thumb--media { display: flex; align-items: center; justify-content: center; height: 100px; font-size: 22px; }
|
|
2953
|
+
.dsc-picker-name { font-size: 11px; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2954
|
+
/* --- workflow tags: filter bar, chips, editor --- */
|
|
2955
|
+
.dsc-tag-filter { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 0 2px; }
|
|
2956
|
+
.dsc-tag-chip { border: 1px solid var(--dsw-alias-border-l1); border-radius: 999px; padding: 2px 10px; font-size: 12px; cursor: pointer; background: transparent; color: var(--dsw-alias-label-primary); }
|
|
2957
|
+
.dsc-tag-chip--active { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
2958
|
+
.dsc-tag-chip--mini { font-size: 11px; padding: 1px 8px; cursor: default; border: 1px solid var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
|
|
2959
|
+
.dsc-wf-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; }
|
|
2960
|
+
.dsc-wf-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; min-width: 0; }
|
|
2961
|
+
.dsc-wf-top .dsc-wf-tags { margin-top: 0; flex: none; justify-content: flex-end; }
|
|
2962
|
+
.dsc-tag-editor { display: flex; flex-direction: column; gap: 6px; }
|
|
2963
|
+
.dsc-tag-row { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
2964
|
+
.dsc-tag-input { max-width: 260px; }
|
|
2965
|
+
.dsc-chip { border: 1px solid var(--dsw-alias-border-l1); border-radius: 999px; padding: 2px 10px; font-size: 12px; cursor: pointer; background: transparent; color: var(--dsw-alias-label-primary); }
|
|
2966
|
+
.dsc-chip--active { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
2967
|
+
.dsc-badge--info { background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
2968
|
+
.dsc-badge--danger { background: var(--dsw-alias-state-error-primary); color: #fff; }
|
|
2969
|
+
.dsc-queue-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
2970
|
+
.dsc-btn { border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 2px 8px; font-size: 12px; cursor: pointer; background: transparent; color: var(--dsw-alias-label-primary); }
|
|
2971
|
+
.dsc-btn--sm { padding: 1px 6px; font-size: 11px; }
|
|
2972
|
+
.dsc-btn:disabled { opacity: 0.5; cursor: default; }
|
|
2973
|
+
.dsc-job-item { display: flex; align-items: center; gap: 10px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 7px 10px; background: var(--dsw-alias-bg-layer-1); transition: background 0.12s, border-color 0.12s; }
|
|
2974
|
+
.dsc-job-item:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l2); }
|
|
2975
|
+
.dsc-job-item--failed { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent); }
|
|
2976
|
+
.dsc-job-item--cancelled { border-color: var(--dsw-alias-border-l2); opacity: 0.85; }
|
|
2977
|
+
.dsc-badge--status { flex: none; font-weight: 600; }
|
|
2978
|
+
.dsc-job-preview { width: 56px; height: 56px; flex: none; border-radius: 6px; object-fit: cover; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); }
|
|
2979
|
+
.dsc-job-preview--clickable { cursor: zoom-in; }
|
|
2980
|
+
/* --- lightbox: floating local panel, no full-screen dim --- */
|
|
2981
|
+
.dsc-lightbox { position: fixed; inset: 0; z-index: 9999; background: transparent; display: flex; align-items: center; justify-content: center; }
|
|
2982
|
+
.dsc-lightbox-body { position: relative; display: flex; flex-direction: column; align-items: center; gap: 10px; width: 90vw; padding: 12px 12px 10px; background: rgba(12, 14, 18, 0.88); border: 1px solid var(--dsw-alias-border-l2); border-radius: 14px; box-shadow: 0 12px 48px rgba(0, 0, 0, 0.45); }
|
|
2983
|
+
.dsc-lightbox-img { width: 100%; height: 80vh; object-fit: contain; border-radius: 8px; }
|
|
2984
|
+
.dsc-lightbox-media { max-width: 100%; max-height: 80vh; border-radius: 8px; }
|
|
2985
|
+
.dsc-lightbox-meta { display: flex; align-items: center; gap: 14px; color: rgba(255, 255, 255, 0.75); font-size: 12px; }
|
|
2986
|
+
.dsc-lightbox-download { border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 999px; padding: 5px 16px; color: #fff; font-size: 12px; text-decoration: none; display: inline-block; }
|
|
2987
|
+
.dsc-lightbox-download:hover { background: rgba(255, 255, 255, 0.12); }
|
|
2988
|
+
.dsc-lightbox-close { position: absolute; top: clamp(4px, 0.6vw, 14px); right: clamp(4px, 0.6vw, 14px); z-index: 3; width: clamp(28px, 2.2vw, 48px); height: clamp(28px, 2.2vw, 48px); border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.35); background: transparent; color: #fff; font-size: clamp(13px, 1.1vw, 22px); line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
2989
|
+
.dsc-lightbox-close:hover { background: rgba(255, 255, 255, 0.12); }
|
|
2990
|
+
.dsc-lightbox-nav { position: absolute; top: 0; bottom: 0; z-index: 2; width: clamp(64px, 7vw, 128px); display: flex; align-items: center; background: transparent; border: none; color: #fff; font-size: clamp(38px, 3.4vw, 84px); line-height: 1; cursor: pointer; opacity: 0.6; padding: 0; }
|
|
2991
|
+
.dsc-lightbox-nav:hover { opacity: 1; color: var(--dsw-alias-brand-primary); background: rgba(255, 255, 255, 0.06); }
|
|
2992
|
+
.dsc-lightbox-nav--prev { left: 0; justify-content: flex-start; padding-left: clamp(14px, 1.8vw, 36px); }
|
|
2993
|
+
.dsc-lightbox-nav--next { right: 0; justify-content: flex-end; padding-right: clamp(14px, 1.8vw, 36px); }
|
|
2994
|
+
.dsc-job-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
2995
|
+
.dsc-job-name { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }
|
|
2996
|
+
.dsc-job-mid { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
|
2997
|
+
.dsc-job-mid-right { display: flex; align-items: center; gap: 6px; flex: none; margin-left: auto; }
|
|
2998
|
+
.dsc-job-bottom { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--dsw-alias-label-secondary); min-width: 0; }
|
|
2999
|
+
.dsc-job-duration { flex: none; }
|
|
3000
|
+
.dsc-job-progress { color: #3b82f6; font-variant-numeric: tabular-nums; }
|
|
3001
|
+
.dsc-job-menu { position: relative; flex: none; }
|
|
3002
|
+
.dsc-job-menu-btn { border: none; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 15px; line-height: 1; padding: 3px 7px; border-radius: 6px; cursor: pointer; }
|
|
3003
|
+
.dsc-job-menu-btn:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }
|
|
3004
|
+
.dsc-job-menu-pop { position: absolute; right: 0; top: calc(100% + 4px); z-index: 950; min-width: 128px; padding: 4px; display: flex; flex-direction: column; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); }
|
|
3005
|
+
.dsc-job-menu-pop button { border: none; background: transparent; color: var(--dsw-alias-label-primary); text-align: left; font-size: 13px; padding: 6px 10px; border-radius: 6px; cursor: pointer; }
|
|
3006
|
+
.dsc-job-menu-pop button:hover { background: var(--dsw-alias-bg-layer-3); }
|
|
3007
|
+
.dsc-job-error { color: var(--dsw-alias-state-error-primary); font-size: 11px; margin-left: auto; flex: 1; min-width: 0; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3008
|
+
/* --- parameter editor --- */
|
|
3009
|
+
.dsc-params { display: flex; flex-direction: column; gap: 8px; }
|
|
3010
|
+
.dsc-param-row { display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 6px 8px; }
|
|
3011
|
+
.dsc-param-fields { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
3012
|
+
.dsc-param-default { min-width: 0; }
|
|
3013
|
+
.dsc-param-meta { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
3014
|
+
.dsc-param-random { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
3015
|
+
.dsc-param-advanced { display: flex; flex-direction: column; gap: 8px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 8px; padding: 8px; }
|
|
3016
|
+
`;
|
|
3017
|
+
/** Inject the stylesheet once (idempotent), owned by this plugin for HMR. */
|
|
3018
|
+
function injectStyles() {
|
|
3019
|
+
if (typeof document === "undefined") return;
|
|
3020
|
+
if (document.getElementById("dsh-comfyui-styles") !== null) return;
|
|
3021
|
+
const style = document.createElement("style");
|
|
3022
|
+
style.id = "dsh-comfyui-styles";
|
|
3023
|
+
style.setAttribute("data-plugin", "dsh-comfyui");
|
|
3024
|
+
style.textContent = CSS;
|
|
3025
|
+
document.head.append(style);
|
|
3026
|
+
}
|
|
3027
|
+
//#endregion
|
|
3028
|
+
//#region src/client/index.ts
|
|
3029
|
+
/**
|
|
3030
|
+
* dsh-comfyui client half: registers the comfyui_run tool card and the
|
|
3031
|
+
* ComfyUI settings page. Registered through slots.inject so contributions
|
|
3032
|
+
* wait on the real slot declarations and unwind with this plugin's fiber.
|
|
3033
|
+
*/
|
|
3034
|
+
const name = "dsh-comfyui";
|
|
3035
|
+
const inject = ["slots"];
|
|
3036
|
+
function apply(ctx) {
|
|
3037
|
+
const t = makeT(getLang());
|
|
3038
|
+
ctx.effect(() => injectStyles(), "dsh-comfyui: styles");
|
|
3039
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
3040
|
+
name: "tool.call.toolview",
|
|
3041
|
+
key: "comfyui_run"
|
|
3042
|
+
}, (props) => (0, react.createElement)(ComfyUICard, {
|
|
3043
|
+
t,
|
|
3044
|
+
...props ?? {}
|
|
3045
|
+
})));
|
|
3046
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
3047
|
+
name: "tool.call.toolview",
|
|
3048
|
+
key: "comfyui_workflow"
|
|
3049
|
+
}, (props) => (0, react.createElement)(ComfyUICard, {
|
|
3050
|
+
t,
|
|
3051
|
+
...props ?? {}
|
|
3052
|
+
})));
|
|
3053
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
3054
|
+
name: "settings.section",
|
|
3055
|
+
id: "comfyui",
|
|
3056
|
+
order: 30,
|
|
3057
|
+
label: () => t("settingsTitle")
|
|
3058
|
+
}, (props) => (0, react.createElement)(ComfyUISettings, {
|
|
3059
|
+
t,
|
|
3060
|
+
...props ?? {}
|
|
3061
|
+
})));
|
|
3062
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
3063
|
+
name: "shell.overlay",
|
|
3064
|
+
id: "comfyui.panel",
|
|
3065
|
+
order: 20,
|
|
3066
|
+
label: () => t("panelTitle")
|
|
3067
|
+
}, (props) => (0, react.createElement)(ComfyUIPanel, {
|
|
3068
|
+
t,
|
|
3069
|
+
...props ?? {}
|
|
3070
|
+
})));
|
|
3071
|
+
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
3072
|
+
name: "conversation.session.header.actions",
|
|
3073
|
+
id: "comfyui",
|
|
3074
|
+
order: 100,
|
|
3075
|
+
label: () => t("panelTitle")
|
|
3076
|
+
}, (props) => (0, react.createElement)(ComfyUITrigger, {
|
|
3077
|
+
t,
|
|
3078
|
+
...props ?? {}
|
|
3079
|
+
})));
|
|
3080
|
+
}
|
|
3081
|
+
//#endregion
|
|
3082
|
+
exports.apply = apply;
|
|
3083
|
+
exports.inject = inject;
|
|
3084
|
+
exports.name = name;
|
|
3085
|
+
return module.exports;
|
|
3086
|
+
}
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
//# sourceMappingURL=client.js.map
|