dsh-comfyui 0.3.0-beta.6 → 0.5.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/README.en.md +12 -1
- package/README.md +12 -1
- package/client/client.js +780 -53
- package/client/client.js.map +1 -1
- package/lib/index.js +10 -9
- package/lib/routes.js +181 -0
- package/lib/skill.js +1 -0
- package/lib/skillpack.d.ts +29 -2
- package/lib/skillpack.js +73 -5
- package/lib/transfer.d.ts +158 -0
- package/lib/transfer.js +460 -0
- package/package.json +5 -9
package/client/client.js
CHANGED
|
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
baseUrl: "服务器地址",
|
|
14
14
|
baseUrlHint: "ComfyUI 的 HTTP 地址(默认 http://127.0.0.1:8188)。",
|
|
15
15
|
apiKeyEnv: "API Key 环境变量名",
|
|
16
|
-
apiKeyEnvHint: "
|
|
16
|
+
apiKeyEnvHint: "密钥名:环境变量或凭据存储(默认 COMFYUI_API_KEY)。",
|
|
17
17
|
hasApiKey: "已配置 API Key",
|
|
18
18
|
noApiKey: "未配置 API Key(本地服务通常不需要)",
|
|
19
19
|
save: "保存",
|
|
@@ -25,16 +25,16 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
testFail: "连接失败:{message}",
|
|
26
26
|
configNotWritable: "设置服务不可用,请通过 cordis.yml 修改配置。",
|
|
27
27
|
mediaHost: "媒体访问地址",
|
|
28
|
-
mediaHostHint: "
|
|
28
|
+
mediaHostHint: "媒体在聊天中显示的完整地址(如 http://192.168.1.5:3080)。留空自动检测;图片不显示时填你访问本页的地址。",
|
|
29
29
|
comfyuiDirs: "ComfyUI 目录",
|
|
30
|
-
comfyuiDirsHint: "本机 ComfyUI
|
|
30
|
+
comfyuiDirsHint: "本机 ComfyUI 安装目录(可多个,支持映射/多实例/便携版)。Agent 据此定位 models、工作流、音色库等文件。",
|
|
31
31
|
comfyuiDirAdd: "+ 添加目录",
|
|
32
32
|
comfyuiDirRemove: "移除该目录",
|
|
33
33
|
comfyuiDirPlaceholder: "如 D:\\ComfyUI",
|
|
34
34
|
skillsDir: "技能包目录",
|
|
35
|
-
skillsDirHint: "
|
|
35
|
+
skillsDirHint: "工作流技能包(SKILL.md 与参考文档)目录。留空 = 数据目录下 skills/;填绝对路径可放到别的盘或同步盘。改后需手动搬移已有目录。当前实际使用:{current}",
|
|
36
36
|
language: "界面语言",
|
|
37
|
-
languageHint: "
|
|
37
|
+
languageHint: "界面语言,切换后页面自动刷新。",
|
|
38
38
|
cardGenerating: "ComfyUI 生成中…",
|
|
39
39
|
cardBackground: "后台任务",
|
|
40
40
|
cardBackgroundDone: "已完成",
|
|
@@ -51,6 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
cardMode: "模式",
|
|
52
52
|
cardListed: "已列出 {runs} 个运行工作流,{graphs} 个图工作流",
|
|
53
53
|
panelTitle: "ComfyUI 面板",
|
|
54
|
+
panelReset: "复位位置与大小",
|
|
54
55
|
close: "关闭",
|
|
55
56
|
lbPrev: "上一张",
|
|
56
57
|
lbNext: "下一张",
|
|
@@ -60,6 +61,10 @@ window.__ModuleLoader__.load({
|
|
|
60
61
|
error: "出错",
|
|
61
62
|
refresh: "刷新",
|
|
62
63
|
ours: "本插件",
|
|
64
|
+
connChecking: "正在检测 ComfyUI 连接…",
|
|
65
|
+
connOk: "连接正常 · ComfyUI {version} · {ms} ms",
|
|
66
|
+
connFailTitle: "无法连接 ComfyUI",
|
|
67
|
+
connFailBody: "后端未能连上 ComfyUI({message})。请先启动本机 ComfyUI,确保后端能检测到其端口;若使用远程服务器,请到设置页检查服务器地址并点「测试连接」。",
|
|
63
68
|
wfAdd: "新建工作流",
|
|
64
69
|
wfName: "名称",
|
|
65
70
|
wfDesc: "概述",
|
|
@@ -130,7 +135,34 @@ window.__ModuleLoader__.load({
|
|
|
130
135
|
wfEmpty: "没有检测到任何工作流,请通过新建、导入或提取来创建执行流程。",
|
|
131
136
|
wfLibrarySection: "API 工作流",
|
|
132
137
|
wfRefresh: "刷新",
|
|
133
|
-
wfImportFile: "
|
|
138
|
+
wfImportFile: "导入 JSON",
|
|
139
|
+
exportPresets: "导出预设",
|
|
140
|
+
importPresets: "导入预设",
|
|
141
|
+
transferExportTitle: "导出工作流预设",
|
|
142
|
+
transferImportTitle: "导入工作流预设",
|
|
143
|
+
transferExportHint: "共 {total} 个可导出工作流,已选 {selected} 个。运行参数与技能包会一并导出。",
|
|
144
|
+
transferSelectAll: "全选",
|
|
145
|
+
transferSelectNone: "全不选",
|
|
146
|
+
transferExportRun: "导出 ({n})",
|
|
147
|
+
transferImportRun: "导入 ({n})",
|
|
148
|
+
transferExporting: "正在导出…",
|
|
149
|
+
transferImporting: "正在导入…",
|
|
150
|
+
transferExportDone: "已导出 {n} 个工作流:{names}(浏览器正在下载预设包)",
|
|
151
|
+
transferExportFile: "文件 {file}({size} KB)",
|
|
152
|
+
transferExportWarnings: "导出警告:{warnings}",
|
|
153
|
+
transferSelectedList: "将导出:{names}",
|
|
154
|
+
transferExportEmpty: "没有可导出的工作流",
|
|
155
|
+
transferPickFile: "点击选择或拖入 .zip 预设包",
|
|
156
|
+
transferPickHint: "导入会创建为新工作流,不会覆盖库中已有的工作流",
|
|
157
|
+
transferPackageInfo: "包内共 {n} 个工作流 · 导出于 {date}",
|
|
158
|
+
transferParamBadge: "参数 {n}",
|
|
159
|
+
transferSkillBadge: "技能包 · {n} 个文件",
|
|
160
|
+
transferSkillRequired: "技能包 · {n} 个文件 · 必读",
|
|
161
|
+
transferImportDone: "已导入 {n} 个工作流",
|
|
162
|
+
transferImportedItem: "✓ {src} → {dst}",
|
|
163
|
+
transferImportFailedItem: "✗ {src}:{reason}",
|
|
164
|
+
transferImportNote: "重名工作流自动追加(导入)后缀",
|
|
165
|
+
transferFinish: "完成",
|
|
134
166
|
wfComfyuiSection: "ComfyUI 工作流",
|
|
135
167
|
wfComfyuiHint: "面板自动检测 ComfyUI 中保存的工作流。\n注:非 API 流程,无法直接执行,需经过提取转化为 API 工作流,或自行导入 API 工作流即可执行。",
|
|
136
168
|
wfComfyuiEmpty: "ComfyUI 端没有检测到保存的工作流",
|
|
@@ -174,7 +206,7 @@ window.__ModuleLoader__.load({
|
|
|
174
206
|
skillBadgeRequired: "技能包·必读",
|
|
175
207
|
skillBadgeHint: "该工作流挂了技能包:Agent 运行前会读它。",
|
|
176
208
|
skillTitle: "技能包",
|
|
177
|
-
skillIntro: "
|
|
209
|
+
skillIntro: "点击「启用技能包」后,可为这个工作流添加给 Agent 看的额外 skills 说明:什么时候用、参数怎么填、有哪些坑。",
|
|
178
210
|
skillEnable: "启用技能包",
|
|
179
211
|
skillDetach: "停用(保留文件)",
|
|
180
212
|
skillDestroy: "销毁技能包",
|
|
@@ -182,11 +214,10 @@ window.__ModuleLoader__.load({
|
|
|
182
214
|
skillDestroyConfirmButton: "确认销毁",
|
|
183
215
|
skillDropHint: "可把文件拖到这里导入",
|
|
184
216
|
skillRequired: "运行前必读",
|
|
185
|
-
skillRequiredHint: "勾选后,Agent 未读技能包就调用本工作流会被拒绝并提示先读。",
|
|
186
217
|
skillDir: "目录",
|
|
187
218
|
skillSummary: "摘要",
|
|
188
219
|
skillSummaryPlaceholder: "一句话说明这个工作流适合做什么",
|
|
189
|
-
skillSummaryHint: "
|
|
220
|
+
skillSummaryHint: "Agent 靠它决定要不要读正文,等同 skills 里的 description 字段。",
|
|
190
221
|
skillSave: "保存",
|
|
191
222
|
skillSaved: "已保存",
|
|
192
223
|
skillDirty: "未保存",
|
|
@@ -202,6 +233,9 @@ window.__ModuleLoader__.load({
|
|
|
202
233
|
skillReveal: "📂 打开目录",
|
|
203
234
|
skillRevealHint: "在文件管理器中打开这个技能包的文件夹,方便直接拷贝文件进去。打开的是运行 DSH 那台机器上的目录;若你正从另一台机器访问本页面,请按下方显示的路径自行前往。",
|
|
204
235
|
skillRevealed: "已打开",
|
|
236
|
+
skillRefresh: "刷新",
|
|
237
|
+
skillRefreshHint: "重新读取技能包目录,显示 Agent 写入或文件管理器里新增/替换的文件。",
|
|
238
|
+
skillRefreshed: "已刷新",
|
|
205
239
|
skillNewDirPlaceholder: "目录名,如 templates",
|
|
206
240
|
skillNewDirHint: "技能包可以有任意一层子目录。常用:{presets};也可以自己起名(字母/数字/汉字/下划线/连字符)。",
|
|
207
241
|
skillDirCreated: "已创建目录",
|
|
@@ -242,7 +276,7 @@ window.__ModuleLoader__.load({
|
|
|
242
276
|
baseUrl: "Server URL",
|
|
243
277
|
baseUrlHint: "ComfyUI HTTP address (default http://127.0.0.1:8188).",
|
|
244
278
|
apiKeyEnv: "API key env var",
|
|
245
|
-
apiKeyEnvHint: "
|
|
279
|
+
apiKeyEnvHint: "Name of the key: env var or credential store (default COMFYUI_API_KEY).",
|
|
246
280
|
hasApiKey: "API key configured",
|
|
247
281
|
noApiKey: "No API key configured (usually not needed for local servers)",
|
|
248
282
|
save: "Save",
|
|
@@ -254,16 +288,16 @@ window.__ModuleLoader__.load({
|
|
|
254
288
|
testFail: "Connection failed: {message}",
|
|
255
289
|
configNotWritable: "Settings service unavailable — edit cordis.yml instead.",
|
|
256
290
|
mediaHost: "Media base URL",
|
|
257
|
-
mediaHostHint: "
|
|
291
|
+
mediaHostHint: "Base URL for media shown in chat (e.g. http://192.168.1.5:3080). Auto-detected when empty; set it if images do not load over LAN or a reverse proxy.",
|
|
258
292
|
comfyuiDirs: "ComfyUI directories",
|
|
259
|
-
comfyuiDirsHint: "ComfyUI install
|
|
293
|
+
comfyuiDirsHint: "ComfyUI install roots on this machine (multiple allowed: mapped/multiple/portable installs). The agent uses them to locate models, workflows and voice libraries.",
|
|
260
294
|
comfyuiDirAdd: "+ Add directory",
|
|
261
295
|
comfyuiDirRemove: "Remove this directory",
|
|
262
296
|
comfyuiDirPlaceholder: "e.g. D:\\ComfyUI",
|
|
263
297
|
skillsDir: "Skill pack directory",
|
|
264
|
-
skillsDirHint: "
|
|
298
|
+
skillsDirHint: "Directory for workflow skill packs (SKILL.md + reference files). Empty = skills/ under the plugin data dir; an absolute path may target another drive or a synced folder. Existing packs are not moved automatically. Currently in use: {current}",
|
|
265
299
|
language: "Language",
|
|
266
|
-
languageHint: "
|
|
300
|
+
languageHint: "UI language; the page reloads after switching.",
|
|
267
301
|
cardGenerating: "ComfyUI generating…",
|
|
268
302
|
cardBackground: "Background job",
|
|
269
303
|
cardBackgroundDone: "Completed",
|
|
@@ -280,6 +314,7 @@ window.__ModuleLoader__.load({
|
|
|
280
314
|
cardMode: "Mode",
|
|
281
315
|
cardListed: "Listed {runs} runnable workflows, {graphs} graph workflows",
|
|
282
316
|
panelTitle: "ComfyUI Panel",
|
|
317
|
+
panelReset: "Reset position & size",
|
|
283
318
|
close: "Close",
|
|
284
319
|
lbPrev: "Previous image",
|
|
285
320
|
lbNext: "Next image",
|
|
@@ -289,6 +324,10 @@ window.__ModuleLoader__.load({
|
|
|
289
324
|
error: "Error",
|
|
290
325
|
refresh: "Refresh",
|
|
291
326
|
ours: "ours",
|
|
327
|
+
connChecking: "Checking the ComfyUI connection…",
|
|
328
|
+
connOk: "Connected · ComfyUI {version} · {ms} ms",
|
|
329
|
+
connFailTitle: "Cannot reach ComfyUI",
|
|
330
|
+
connFailBody: "The backend could not reach ComfyUI ({message}). Start the local ComfyUI and make sure the backend can see its port; for a remote server, check the URL in settings and run \"Test connection\".",
|
|
292
331
|
wfAdd: "New workflow",
|
|
293
332
|
wfName: "Name",
|
|
294
333
|
wfDesc: "Overview",
|
|
@@ -359,7 +398,34 @@ window.__ModuleLoader__.load({
|
|
|
359
398
|
wfEmpty: "No workflows detected — create one via New, Import, or Extract.",
|
|
360
399
|
wfLibrarySection: "API workflows",
|
|
361
400
|
wfRefresh: "Refresh",
|
|
362
|
-
wfImportFile: "Import
|
|
401
|
+
wfImportFile: "Import JSON",
|
|
402
|
+
exportPresets: "Export presets",
|
|
403
|
+
importPresets: "Import presets",
|
|
404
|
+
transferExportTitle: "Export workflow presets",
|
|
405
|
+
transferImportTitle: "Import workflow presets",
|
|
406
|
+
transferExportHint: "{total} exportable workflows, {selected} selected. Parameters and skill packs are included.",
|
|
407
|
+
transferSelectAll: "Select all",
|
|
408
|
+
transferSelectNone: "Select none",
|
|
409
|
+
transferExportRun: "Export ({n})",
|
|
410
|
+
transferImportRun: "Import ({n})",
|
|
411
|
+
transferExporting: "Exporting…",
|
|
412
|
+
transferImporting: "Importing…",
|
|
413
|
+
transferExportDone: "Exported {n} workflows: {names} (the browser is downloading the package)",
|
|
414
|
+
transferExportFile: "File {file} ({size} KB)",
|
|
415
|
+
transferExportWarnings: "Export warnings: {warnings}",
|
|
416
|
+
transferSelectedList: "Will export: {names}",
|
|
417
|
+
transferExportEmpty: "No exportable workflows",
|
|
418
|
+
transferPickFile: "Click to choose, or drop a .zip preset package here",
|
|
419
|
+
transferPickHint: "Import creates new workflows — nothing in your library is overwritten",
|
|
420
|
+
transferPackageInfo: "{n} workflows in package · exported {date}",
|
|
421
|
+
transferParamBadge: "{n} params",
|
|
422
|
+
transferSkillBadge: "Skill pack · {n} files",
|
|
423
|
+
transferSkillRequired: "Skill pack · {n} files · required",
|
|
424
|
+
transferImportDone: "Imported {n} workflows",
|
|
425
|
+
transferImportedItem: "✓ {src} → {dst}",
|
|
426
|
+
transferImportFailedItem: "✗ {src}: {reason}",
|
|
427
|
+
transferImportNote: "Name clashes get an \"(imported)\" suffix",
|
|
428
|
+
transferFinish: "Done",
|
|
363
429
|
wfComfyuiSection: "ComfyUI workflows",
|
|
364
430
|
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.",
|
|
365
431
|
wfComfyuiEmpty: "No workflows detected on the ComfyUI server",
|
|
@@ -403,7 +469,7 @@ window.__ModuleLoader__.load({
|
|
|
403
469
|
skillBadgeRequired: "Skill pack (required)",
|
|
404
470
|
skillBadgeHint: "This workflow has a skill pack: the agent reads it before running.",
|
|
405
471
|
skillTitle: "Skill pack",
|
|
406
|
-
skillIntro: "
|
|
472
|
+
skillIntro: "Click \"Create skill pack\" to add extra skill instructions for this workflow: when to use it, how to fill its parameters, and which pitfalls to avoid.",
|
|
407
473
|
skillEnable: "Create skill pack",
|
|
408
474
|
skillDetach: "Detach (keep files)",
|
|
409
475
|
skillDestroy: "Destroy pack",
|
|
@@ -411,11 +477,10 @@ window.__ModuleLoader__.load({
|
|
|
411
477
|
skillDestroyConfirmButton: "Destroy",
|
|
412
478
|
skillDropHint: "Drop files here to import",
|
|
413
479
|
skillRequired: "Required before running",
|
|
414
|
-
skillRequiredHint: "When checked, running this workflow is refused until the agent has loaded the pack.",
|
|
415
480
|
skillDir: "Directory",
|
|
416
481
|
skillSummary: "Summary",
|
|
417
482
|
skillSummaryPlaceholder: "One line on what this workflow is for",
|
|
418
|
-
skillSummaryHint: "
|
|
483
|
+
skillSummaryHint: "The agent uses it to decide whether to read the body — like the description field of a skill.",
|
|
419
484
|
skillSave: "Save",
|
|
420
485
|
skillSaved: "Saved",
|
|
421
486
|
skillDirty: "Unsaved",
|
|
@@ -431,6 +496,9 @@ window.__ModuleLoader__.load({
|
|
|
431
496
|
skillReveal: "📂 Open folder",
|
|
432
497
|
skillRevealHint: "Open this pack folder in the file manager so you can copy files straight in. It opens on the machine running DSH; if you are viewing this page from another machine, use the path shown below instead.",
|
|
433
498
|
skillRevealed: "Opened",
|
|
499
|
+
skillRefresh: "Refresh",
|
|
500
|
+
skillRefreshHint: "Re-read the pack directory to show files added or replaced by the agent or the file manager.",
|
|
501
|
+
skillRefreshed: "Refreshed",
|
|
434
502
|
skillNewDirPlaceholder: "Folder name, e.g. templates",
|
|
435
503
|
skillNewDirHint: "A pack may hold any one-level sub-directory. Common ones: {presets}; custom names work too (letters, digits, CJK, underscore, dash).",
|
|
436
504
|
skillDirCreated: "Folder created",
|
|
@@ -991,49 +1059,49 @@ window.__ModuleLoader__.load({
|
|
|
991
1059
|
* and the session-header trigger are separate registrations in one bundle,
|
|
992
1060
|
* so they coordinate through this module-level store plus useSyncExternalStore.
|
|
993
1061
|
*/
|
|
994
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
1062
|
+
const listeners$1 = /* @__PURE__ */ new Set();
|
|
995
1063
|
let open = false;
|
|
996
1064
|
let tab = "workflows";
|
|
997
1065
|
let assetFilter = "";
|
|
998
|
-
function emit() {
|
|
999
|
-
for (const listener of listeners) listener();
|
|
1066
|
+
function emit$1() {
|
|
1067
|
+
for (const listener of listeners$1) listener();
|
|
1000
1068
|
}
|
|
1001
1069
|
const panelStore = {
|
|
1002
1070
|
isOpen: () => open,
|
|
1003
1071
|
toggle: () => {
|
|
1004
1072
|
open = !open;
|
|
1005
|
-
emit();
|
|
1073
|
+
emit$1();
|
|
1006
1074
|
},
|
|
1007
1075
|
open: () => {
|
|
1008
1076
|
if (!open) {
|
|
1009
1077
|
open = true;
|
|
1010
|
-
emit();
|
|
1078
|
+
emit$1();
|
|
1011
1079
|
}
|
|
1012
1080
|
},
|
|
1013
1081
|
close: () => {
|
|
1014
1082
|
if (open) {
|
|
1015
1083
|
open = false;
|
|
1016
|
-
emit();
|
|
1084
|
+
emit$1();
|
|
1017
1085
|
}
|
|
1018
1086
|
},
|
|
1019
1087
|
getTab: () => tab,
|
|
1020
1088
|
setTab: (next) => {
|
|
1021
1089
|
if (tab !== next) {
|
|
1022
1090
|
tab = next;
|
|
1023
|
-
emit();
|
|
1091
|
+
emit$1();
|
|
1024
1092
|
}
|
|
1025
1093
|
},
|
|
1026
1094
|
getAssetFilter: () => assetFilter,
|
|
1027
1095
|
setAssetFilter: (name) => {
|
|
1028
1096
|
if (assetFilter !== name) {
|
|
1029
1097
|
assetFilter = name;
|
|
1030
|
-
emit();
|
|
1098
|
+
emit$1();
|
|
1031
1099
|
}
|
|
1032
1100
|
},
|
|
1033
1101
|
subscribe(listener) {
|
|
1034
|
-
listeners.add(listener);
|
|
1102
|
+
listeners$1.add(listener);
|
|
1035
1103
|
return () => {
|
|
1036
|
-
listeners.delete(listener);
|
|
1104
|
+
listeners$1.delete(listener);
|
|
1037
1105
|
};
|
|
1038
1106
|
}
|
|
1039
1107
|
};
|
|
@@ -1046,10 +1114,132 @@ window.__ModuleLoader__.load({
|
|
|
1046
1114
|
return (0, react.useSyncExternalStore)(panelStore.subscribe, panelStore.getTab, panelStore.getTab);
|
|
1047
1115
|
}
|
|
1048
1116
|
//#endregion
|
|
1117
|
+
//#region src/client/connection.tsx
|
|
1118
|
+
/**
|
|
1119
|
+
* Connection probe + reminder toast. Opening the panel used to give zero
|
|
1120
|
+
* feedback when ComfyUI was down: the workflows/queue tabs just stayed empty
|
|
1121
|
+
* with no explanation. The header trigger now fires a real backend probe
|
|
1122
|
+
* (POST /comfyui/test → the host connects to the configured ComfyUI server —
|
|
1123
|
+
* local port or remote URL alike); when it fails, the panel closes again —
|
|
1124
|
+
* the trigger must not stay highlighted against a dead backend — and a fixed
|
|
1125
|
+
* toast at the top of the page tells the user to start ComfyUI or fix the
|
|
1126
|
+
* server address. The probe never blocks the panel: it opens immediately,
|
|
1127
|
+
* the toast settles whenever the probe answers.
|
|
1128
|
+
*/
|
|
1129
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1130
|
+
let state = { status: "hidden" };
|
|
1131
|
+
/** Dedupe concurrent probes (rapid trigger clicks). */
|
|
1132
|
+
let inFlight = null;
|
|
1133
|
+
/** Skip re-probing while a "connected" verdict is still fresh. */
|
|
1134
|
+
let lastOkAt = 0;
|
|
1135
|
+
/** The ok toast only shows on fail→ok transitions, never on healthy opens. */
|
|
1136
|
+
let hadFailure = false;
|
|
1137
|
+
let okHideTimer = null;
|
|
1138
|
+
function emit() {
|
|
1139
|
+
for (const listener of listeners) listener();
|
|
1140
|
+
}
|
|
1141
|
+
function setState(next) {
|
|
1142
|
+
state = next;
|
|
1143
|
+
emit();
|
|
1144
|
+
}
|
|
1145
|
+
function clearOkTimer() {
|
|
1146
|
+
if (okHideTimer !== null) {
|
|
1147
|
+
clearTimeout(okHideTimer);
|
|
1148
|
+
okHideTimer = null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/** Close button on the toast; also ends the transient ok feedback early. */
|
|
1152
|
+
function dismissConnectionToast() {
|
|
1153
|
+
clearOkTimer();
|
|
1154
|
+
if (state.status !== "hidden") setState({ status: "hidden" });
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Probe the ComfyUI server through the host and surface the result as toast
|
|
1158
|
+
* state. A success within the last 30s skips the round-trip entirely, so
|
|
1159
|
+
* healthy repeated panel opens do not hammer the server; failures always
|
|
1160
|
+
* re-probe on the next open (the user may have just started ComfyUI).
|
|
1161
|
+
*/
|
|
1162
|
+
function probeConnection() {
|
|
1163
|
+
if (inFlight !== null) return inFlight;
|
|
1164
|
+
if (Date.now() - lastOkAt < 3e4) return Promise.resolve();
|
|
1165
|
+
if (state.status !== "fail") {
|
|
1166
|
+
clearOkTimer();
|
|
1167
|
+
setState({ status: "probing" });
|
|
1168
|
+
}
|
|
1169
|
+
inFlight = (async () => {
|
|
1170
|
+
try {
|
|
1171
|
+
const payload = await postJson("/comfyui/test", {});
|
|
1172
|
+
if (payload.ok === true) {
|
|
1173
|
+
lastOkAt = Date.now();
|
|
1174
|
+
const recovered = hadFailure;
|
|
1175
|
+
hadFailure = false;
|
|
1176
|
+
if (recovered) {
|
|
1177
|
+
setState({
|
|
1178
|
+
status: "ok",
|
|
1179
|
+
version: payload.version,
|
|
1180
|
+
latencyMs: payload.latencyMs
|
|
1181
|
+
});
|
|
1182
|
+
clearOkTimer();
|
|
1183
|
+
okHideTimer = setTimeout(dismissConnectionToast, 4e3);
|
|
1184
|
+
} else setState({ status: "hidden" });
|
|
1185
|
+
} else {
|
|
1186
|
+
hadFailure = true;
|
|
1187
|
+
setState({
|
|
1188
|
+
status: "fail",
|
|
1189
|
+
message: payload.error ?? "unknown error"
|
|
1190
|
+
});
|
|
1191
|
+
panelStore.close();
|
|
1192
|
+
}
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
hadFailure = true;
|
|
1195
|
+
setState({
|
|
1196
|
+
status: "fail",
|
|
1197
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1198
|
+
});
|
|
1199
|
+
panelStore.close();
|
|
1200
|
+
} finally {
|
|
1201
|
+
inFlight = null;
|
|
1202
|
+
}
|
|
1203
|
+
})();
|
|
1204
|
+
return inFlight;
|
|
1205
|
+
}
|
|
1206
|
+
function getConnState() {
|
|
1207
|
+
return state;
|
|
1208
|
+
}
|
|
1209
|
+
function subscribe(listener) {
|
|
1210
|
+
listeners.add(listener);
|
|
1211
|
+
return () => {
|
|
1212
|
+
listeners.delete(listener);
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
/** Fixed top-center status toast; mounted next to the header trigger so it
|
|
1216
|
+
* renders (and can be dismissed) even when the panel is closed again. The
|
|
1217
|
+
* probing variant fades in after a short CSS delay, so a fast successful
|
|
1218
|
+
* probe never paints anything. */
|
|
1219
|
+
function ConnectionToast({ t }) {
|
|
1220
|
+
const conn = (0, react.useSyncExternalStore)(subscribe, getConnState, getConnState);
|
|
1221
|
+
if (conn.status === "hidden") return null;
|
|
1222
|
+
return (0, react.createElement)("div", {
|
|
1223
|
+
className: `dsc-conn-toast dsc-conn-toast--${conn.status}`,
|
|
1224
|
+
role: "status"
|
|
1225
|
+
}, (0, react.createElement)("span", {
|
|
1226
|
+
className: "dsc-conn-toast-dot",
|
|
1227
|
+
"aria-hidden": true
|
|
1228
|
+
}), (0, react.createElement)("div", { className: "dsc-conn-toast-body" }, conn.status === "fail" ? (0, react.createElement)("div", { className: "dsc-conn-toast-body-inner" }, (0, react.createElement)("div", { className: "dsc-conn-toast-title" }, t("connFailTitle")), (0, react.createElement)("div", { className: "dsc-conn-toast-text" }, t("connFailBody", { message: conn.message ?? "" }))) : (0, react.createElement)("div", { className: "dsc-conn-toast-title" }, conn.status === "probing" ? t("connChecking") : t("connOk", {
|
|
1229
|
+
version: conn.version ?? "unknown",
|
|
1230
|
+
ms: conn.latencyMs ?? 0
|
|
1231
|
+
}))), (0, react.createElement)("button", {
|
|
1232
|
+
className: "dsc-conn-toast-close",
|
|
1233
|
+
"aria-label": t("close"),
|
|
1234
|
+
onClick: dismissConnectionToast
|
|
1235
|
+
}, "✕"));
|
|
1236
|
+
}
|
|
1237
|
+
//#endregion
|
|
1049
1238
|
//#region src/client/trigger.tsx
|
|
1050
1239
|
/**
|
|
1051
1240
|
* The session-header trigger (conversation.session.header.actions, id
|
|
1052
|
-
* 'comfyui'): a labelled button that toggles the ComfyUI panel
|
|
1241
|
+
* 'comfyui'): a labelled button that toggles the ComfyUI panel, plus the
|
|
1242
|
+
* connection reminder toast shown when the backend cannot reach ComfyUI.
|
|
1053
1243
|
*/
|
|
1054
1244
|
/**
|
|
1055
1245
|
* ComfyUI brand glyph (mono), inlined from @lobehub/icons-static-svg
|
|
@@ -1069,16 +1259,22 @@ window.__ModuleLoader__.load({
|
|
|
1069
1259
|
"aria-hidden": true
|
|
1070
1260
|
}, (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" }));
|
|
1071
1261
|
}
|
|
1072
|
-
/** Header-action button toggling the ComfyUI panel.
|
|
1262
|
+
/** Header-action button toggling the ComfyUI panel. Opening it also fires a
|
|
1263
|
+
* backend connectivity probe: when ComfyUI is down, a reminder toast explains
|
|
1264
|
+
* why the panel stays empty instead of the click appearing to do nothing. */
|
|
1073
1265
|
function ComfyUITrigger({ t }) {
|
|
1074
1266
|
const open = usePanelOpen();
|
|
1075
|
-
return (0, react.createElement)("button", {
|
|
1267
|
+
return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("button", {
|
|
1076
1268
|
className: "dsc-trigger",
|
|
1077
1269
|
title: t("panelTitle"),
|
|
1078
1270
|
"aria-label": t("panelTitle"),
|
|
1079
1271
|
"aria-pressed": open,
|
|
1080
|
-
onClick: () =>
|
|
1081
|
-
|
|
1272
|
+
onClick: () => {
|
|
1273
|
+
const opening = !panelStore.isOpen();
|
|
1274
|
+
panelStore.toggle();
|
|
1275
|
+
if (opening) probeConnection();
|
|
1276
|
+
}
|
|
1277
|
+
}, (0, react.createElement)("span", { className: "dsc-trigger-glyph" }, (0, react.createElement)(ComfyUIIcon)), (0, react.createElement)("span", null, t("panelTitle"))), (0, react.createElement)(ConnectionToast, { t }));
|
|
1082
1278
|
}
|
|
1083
1279
|
//#endregion
|
|
1084
1280
|
//#region src/client/panel.tsx
|
|
@@ -1110,6 +1306,41 @@ window.__ModuleLoader__.load({
|
|
|
1110
1306
|
localStorage.setItem(GEOM_KEY, JSON.stringify(geom));
|
|
1111
1307
|
} catch {}
|
|
1112
1308
|
}
|
|
1309
|
+
/** Default panel width from styles.ts (`.dsc-panel` width var), used only to
|
|
1310
|
+
* estimate the panel extent while clamping a saved position. */
|
|
1311
|
+
const PANEL_WIDTH_FALLBACK = 400;
|
|
1312
|
+
/** Approx. height of the drag-handle band (`.dsc-panel-head`). */
|
|
1313
|
+
const HEAD_BAND = 48;
|
|
1314
|
+
/** Minimum margin kept between the panel and the viewport edge. */
|
|
1315
|
+
const EDGE = 8;
|
|
1316
|
+
/**
|
|
1317
|
+
* Clamp a top-left corner so the drag-handle band always stays inside the
|
|
1318
|
+
* viewport. The failure this prevents: once the header is pushed past the top
|
|
1319
|
+
* edge (or a saved position from a larger monitor no longer fits), there is no
|
|
1320
|
+
* grab surface left and the panel cannot be moved back at all.
|
|
1321
|
+
*/
|
|
1322
|
+
function clampPos(x, y, width, vw, vh) {
|
|
1323
|
+
const maxX = Math.max(EDGE, vw - width - EDGE);
|
|
1324
|
+
const maxY = Math.max(EDGE, vh - HEAD_BAND - EDGE);
|
|
1325
|
+
return {
|
|
1326
|
+
x: Math.min(Math.max(x, EDGE), maxX),
|
|
1327
|
+
y: Math.min(Math.max(y, EDGE), maxY)
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
/** Snap persisted geometry back within reach of the cursor. Geometry that
|
|
1331
|
+
* never carried an explicit position (the CSS-anchored default) is returned
|
|
1332
|
+
* unchanged; geometry already inside the safe area keeps its object identity
|
|
1333
|
+
* so callers can skip pointless re-renders. */
|
|
1334
|
+
function healGeom(geom, vw, vh) {
|
|
1335
|
+
if (geom.x === void 0 || geom.y === void 0) return geom;
|
|
1336
|
+
const pos = clampPos(geom.x, geom.y, geom.width ?? PANEL_WIDTH_FALLBACK, vw, vh);
|
|
1337
|
+
if (pos.x === geom.x && pos.y === geom.y) return geom;
|
|
1338
|
+
return {
|
|
1339
|
+
...geom,
|
|
1340
|
+
x: pos.x,
|
|
1341
|
+
y: pos.y
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1113
1344
|
/** Fallback suggestions when the config route has not answered yet; the
|
|
1114
1345
|
* authoritative list comes from the host (`presetDirs`). */
|
|
1115
1346
|
const SKILL_FALLBACK_DIRS = [
|
|
@@ -1897,7 +2128,7 @@ window.__ModuleLoader__.load({
|
|
|
1897
2128
|
function ComfyUIPanel({ t }) {
|
|
1898
2129
|
const open = usePanelOpen();
|
|
1899
2130
|
const tab = usePanelTab();
|
|
1900
|
-
const [geom, setGeom] = (0, react.useState)(loadGeom);
|
|
2131
|
+
const [geom, setGeom] = (0, react.useState)(() => healGeom(loadGeom(), window.innerWidth, window.innerHeight));
|
|
1901
2132
|
const [dragging, setDragging] = (0, react.useState)(null);
|
|
1902
2133
|
const [lightbox, setLightbox] = (0, react.useState)(null);
|
|
1903
2134
|
const panelRef = (0, react.useRef)(null);
|
|
@@ -1908,9 +2139,18 @@ window.__ModuleLoader__.load({
|
|
|
1908
2139
|
index
|
|
1909
2140
|
});
|
|
1910
2141
|
(0, react.useEffect)(() => {
|
|
1911
|
-
if (open)
|
|
2142
|
+
if (open) {
|
|
2143
|
+
setGeom((prev) => healGeom(prev, window.innerWidth, window.innerHeight));
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
1912
2146
|
saveGeom(geom);
|
|
1913
2147
|
}, [open]);
|
|
2148
|
+
(0, react.useEffect)(() => {
|
|
2149
|
+
if (!open) return;
|
|
2150
|
+
const onResize = () => setGeom((prev) => healGeom(prev, window.innerWidth, window.innerHeight));
|
|
2151
|
+
window.addEventListener("resize", onResize);
|
|
2152
|
+
return () => window.removeEventListener("resize", onResize);
|
|
2153
|
+
}, [open]);
|
|
1914
2154
|
const startDrag = (event, mode) => {
|
|
1915
2155
|
if (event.button !== 0) return;
|
|
1916
2156
|
if (mode === "move") {
|
|
@@ -1934,12 +2174,14 @@ window.__ModuleLoader__.load({
|
|
|
1934
2174
|
const moveDrag = (event) => {
|
|
1935
2175
|
const d = dragRef.current;
|
|
1936
2176
|
if (d === null) return;
|
|
1937
|
-
if (dragging === "move")
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2177
|
+
if (dragging === "move") {
|
|
2178
|
+
const pos = clampPos(Math.round(d.origX + event.clientX - d.startX), Math.round(d.origY + event.clientY - d.startY), d.width, window.innerWidth, window.innerHeight);
|
|
2179
|
+
setGeom((prev) => ({
|
|
2180
|
+
...prev,
|
|
2181
|
+
x: pos.x,
|
|
2182
|
+
y: pos.y
|
|
2183
|
+
}));
|
|
2184
|
+
} else if (dragging === "resize") {
|
|
1943
2185
|
const width = Math.max(300, Math.min(720, d.width + event.clientX - d.startX));
|
|
1944
2186
|
const height = Math.max(200, Math.min(Math.max(300, window.innerHeight - 40), d.height + event.clientY - d.startY));
|
|
1945
2187
|
setGeom((prev) => ({
|
|
@@ -1978,11 +2220,22 @@ window.__ModuleLoader__.load({
|
|
|
1978
2220
|
onPointerMove: (event) => moveDrag(event),
|
|
1979
2221
|
onPointerUp: () => endDrag(),
|
|
1980
2222
|
onPointerCancel: () => endDrag()
|
|
1981
|
-
}, (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", {
|
|
2223
|
+
}, (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)("span", { className: "dsc-panel-head-actions" }, (0, react.createElement)("button", {
|
|
2224
|
+
className: "dsc-panel-reset",
|
|
2225
|
+
type: "button",
|
|
2226
|
+
title: t("panelReset"),
|
|
2227
|
+
"aria-label": t("panelReset"),
|
|
2228
|
+
onClick: () => {
|
|
2229
|
+
setGeom({});
|
|
2230
|
+
try {
|
|
2231
|
+
localStorage.removeItem(GEOM_KEY);
|
|
2232
|
+
} catch {}
|
|
2233
|
+
}
|
|
2234
|
+
}, "↺"), (0, react.createElement)("button", {
|
|
1982
2235
|
className: "dsc-panel-close",
|
|
1983
2236
|
"aria-label": t("close"),
|
|
1984
2237
|
onClick: () => panelStore.close()
|
|
1985
|
-
}, "✕")), (0, react.createElement)("div", { className: "dsc-tabs" }, (0, react.createElement)(TabButton, {
|
|
2238
|
+
}, "✕"))), (0, react.createElement)("div", { className: "dsc-tabs" }, (0, react.createElement)(TabButton, {
|
|
1986
2239
|
t,
|
|
1987
2240
|
active: tab === "workflows",
|
|
1988
2241
|
label: t("tabWorkflows"),
|
|
@@ -2470,6 +2723,28 @@ window.__ModuleLoader__.load({
|
|
|
2470
2723
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2471
2724
|
}
|
|
2472
2725
|
}, [props.workflow.id, openFile]);
|
|
2726
|
+
/** Re-read the pack listing and — only when nothing is unsaved — the file
|
|
2727
|
+
* currently open, so files added or replaced outside this panel (the agent
|
|
2728
|
+
* writing via comfyui_skill, or the file manager after `打开目录`) show up
|
|
2729
|
+
* without leaving and re-entering the view. */
|
|
2730
|
+
const refreshFiles = async () => {
|
|
2731
|
+
setBusy(true);
|
|
2732
|
+
setNotice(null);
|
|
2733
|
+
try {
|
|
2734
|
+
const data = await getJson(`/comfyui/workflows/skill?id=${encodeURIComponent(props.workflow.id)}`);
|
|
2735
|
+
setEnabled(data.enabled === true);
|
|
2736
|
+
setPack(data.pack ?? null);
|
|
2737
|
+
if (Array.isArray(data.presetDirs) && data.presetDirs.length > 0) setPresetDirs(data.presetDirs);
|
|
2738
|
+
setError(null);
|
|
2739
|
+
props.onChanged();
|
|
2740
|
+
if (data.enabled === true && !dirty) await openFile(active);
|
|
2741
|
+
setNotice(t("skillRefreshed"));
|
|
2742
|
+
} catch (cause) {
|
|
2743
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2744
|
+
} finally {
|
|
2745
|
+
setBusy(false);
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2473
2748
|
(0, react.useEffect)(() => {
|
|
2474
2749
|
refresh();
|
|
2475
2750
|
}, [refresh]);
|
|
@@ -2634,6 +2909,12 @@ window.__ModuleLoader__.load({
|
|
|
2634
2909
|
/** Where a new file may go: the pack's own directories first, then the
|
|
2635
2910
|
* presets it has not used yet. */
|
|
2636
2911
|
const targetDirs = [...packDirs, ...presetDirs.filter((dir) => !packDirs.includes(dir))];
|
|
2912
|
+
/** One row in the pack file list; clicking opens it for editing. */
|
|
2913
|
+
const fileButton = (file) => (0, react.createElement)("button", {
|
|
2914
|
+
key: file.path,
|
|
2915
|
+
className: `dsc-skill-file${file.path === active ? " dsc-skill-file--active" : ""}`,
|
|
2916
|
+
onClick: () => selectFile(file.path)
|
|
2917
|
+
}, (0, react.createElement)("span", { className: "dsc-skill-file-name" }, file.path.includes("/") ? file.path.split("/")[1] : file.path), (0, react.createElement)("span", { className: "dsc-skill-file-size" }, formatBytes(file.size)));
|
|
2637
2918
|
const fileList = (0, react.createElement)("div", {
|
|
2638
2919
|
className: "dsc-skill-files",
|
|
2639
2920
|
onDragOver: (event) => event.preventDefault(),
|
|
@@ -2677,7 +2958,12 @@ window.__ModuleLoader__.load({
|
|
|
2677
2958
|
disabled: busy,
|
|
2678
2959
|
title: t("skillRevealHint"),
|
|
2679
2960
|
onClick: () => void revealDir()
|
|
2680
|
-
}, t("skillReveal"))
|
|
2961
|
+
}, t("skillReveal")), (0, react.createElement)("button", {
|
|
2962
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2963
|
+
disabled: busy,
|
|
2964
|
+
title: t("skillRefreshHint"),
|
|
2965
|
+
onClick: () => void refreshFiles()
|
|
2966
|
+
}, t("skillRefresh"))), creatingDir === null ? null : (0, react.createElement)("div", { className: "dsc-skill-create" }, (0, react.createElement)("input", {
|
|
2681
2967
|
className: "dsc-input dsc-input--sm",
|
|
2682
2968
|
value: creatingDir,
|
|
2683
2969
|
placeholder: t("skillNewDirPlaceholder"),
|
|
@@ -2695,14 +2981,14 @@ window.__ModuleLoader__.load({
|
|
|
2695
2981
|
}, t("skillCreate")), (0, react.createElement)("button", {
|
|
2696
2982
|
className: "dsc-btn dsc-btn--sm",
|
|
2697
2983
|
onClick: () => setCreatingDir(null)
|
|
2698
|
-
}, t("cancel")))), (0, react.createElement)("div", { className: "dsc-skill-drop-hint" }, t("skillDropHint")), buckets.map((bucket) => bucket.items.length === 0 && bucket.label !== "" ? null : (0, react.createElement)("div", {
|
|
2699
|
-
key:
|
|
2984
|
+
}, t("cancel")))), (0, react.createElement)("div", { className: "dsc-skill-drop-hint" }, t("skillDropHint")), buckets.map((bucket) => bucket.items.length === 0 && bucket.label !== "" ? null : bucket.label === "" ? (0, react.createElement)("div", {
|
|
2985
|
+
key: ".",
|
|
2700
2986
|
className: "dsc-skill-bucket"
|
|
2701
|
-
}, bucket.
|
|
2702
|
-
key:
|
|
2703
|
-
className:
|
|
2704
|
-
|
|
2705
|
-
}, (0, react.createElement)("
|
|
2987
|
+
}, bucket.items.map(fileButton)) : (0, react.createElement)("details", {
|
|
2988
|
+
key: bucket.label,
|
|
2989
|
+
className: "dsc-fold dsc-skill-fold",
|
|
2990
|
+
open: true
|
|
2991
|
+
}, (0, react.createElement)("summary", null, `${bucket.label}/`), (0, react.createElement)("div", { className: "dsc-skill-bucket" }, bucket.items.map(fileButton)))), creating === null ? null : (0, react.createElement)("div", { className: "dsc-skill-create" }, (0, react.createElement)("select", {
|
|
2706
2992
|
className: "dsc-input dsc-input--sm",
|
|
2707
2993
|
value: creating.bucket,
|
|
2708
2994
|
onChange: (event) => setCreating({
|
|
@@ -2818,7 +3104,7 @@ window.__ModuleLoader__.load({
|
|
|
2818
3104
|
action: "require",
|
|
2819
3105
|
required: event.target.checked
|
|
2820
3106
|
})
|
|
2821
|
-
}), t("skillRequired"))
|
|
3107
|
+
}), t("skillRequired"))), pack !== null ? (0, react.createElement)("div", {
|
|
2822
3108
|
className: "dsc-skill-dir",
|
|
2823
3109
|
title: pack.dir
|
|
2824
3110
|
}, `${t("skillDir")}: ${pack.dir}`) : null, pendingOpen !== null ? (0, react.createElement)("div", { className: "dsc-dialog" }, (0, react.createElement)("div", { className: "dsc-dialog-head" }, t("skillDirtyPrompt")), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
@@ -2875,6 +3161,8 @@ window.__ModuleLoader__.load({
|
|
|
2875
3161
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2876
3162
|
const [viewing, setViewing] = (0, react.useState)(null);
|
|
2877
3163
|
const [extract, setExtract] = (0, react.useState)(null);
|
|
3164
|
+
/** Open transfer dialog: 'export' | 'import', null = closed. */
|
|
3165
|
+
const [transfer, setTransfer] = (0, react.useState)(null);
|
|
2878
3166
|
const [tagFilter, setTagFilter] = (0, react.useState)(null);
|
|
2879
3167
|
const [skillFor, setSkillFor] = (0, react.useState)(null);
|
|
2880
3168
|
/** A delete waiting on the user's answer about the workflow's skill pack. */
|
|
@@ -3078,6 +3366,25 @@ window.__ModuleLoader__.load({
|
|
|
3078
3366
|
onRun: (state) => runExtract(state),
|
|
3079
3367
|
onClose: () => setExtract(null)
|
|
3080
3368
|
});
|
|
3369
|
+
if (transfer === "export") return (0, react.createElement)(ExportDialog, {
|
|
3370
|
+
t,
|
|
3371
|
+
workflows: list ?? [],
|
|
3372
|
+
onClose: () => setTransfer(null),
|
|
3373
|
+
onDone: (notice) => {
|
|
3374
|
+
setTransfer(null);
|
|
3375
|
+
setNotice(notice);
|
|
3376
|
+
load();
|
|
3377
|
+
}
|
|
3378
|
+
});
|
|
3379
|
+
if (transfer === "import") return (0, react.createElement)(ImportDialog, {
|
|
3380
|
+
t,
|
|
3381
|
+
onClose: () => setTransfer(null),
|
|
3382
|
+
onDone: (notice) => {
|
|
3383
|
+
setTransfer(null);
|
|
3384
|
+
setNotice(notice);
|
|
3385
|
+
load();
|
|
3386
|
+
}
|
|
3387
|
+
});
|
|
3081
3388
|
if (error !== null) return (0, react.createElement)("div", null, (0, react.createElement)(ErrorNote, {
|
|
3082
3389
|
t,
|
|
3083
3390
|
message: error
|
|
@@ -3286,6 +3593,20 @@ window.__ModuleLoader__.load({
|
|
|
3286
3593
|
event.target.value = "";
|
|
3287
3594
|
}
|
|
3288
3595
|
})), (0, react.createElement)("button", {
|
|
3596
|
+
className: "dsc-btn",
|
|
3597
|
+
disabled: busy,
|
|
3598
|
+
onClick: () => {
|
|
3599
|
+
setError(null);
|
|
3600
|
+
setTransfer("export");
|
|
3601
|
+
}
|
|
3602
|
+
}, t("exportPresets")), (0, react.createElement)("button", {
|
|
3603
|
+
className: "dsc-btn",
|
|
3604
|
+
disabled: busy,
|
|
3605
|
+
onClick: () => {
|
|
3606
|
+
setError(null);
|
|
3607
|
+
setTransfer("import");
|
|
3608
|
+
}
|
|
3609
|
+
}, t("importPresets")), (0, react.createElement)("button", {
|
|
3289
3610
|
className: "dsc-btn",
|
|
3290
3611
|
disabled: busy,
|
|
3291
3612
|
onClick: () => void load()
|
|
@@ -3368,6 +3689,348 @@ window.__ModuleLoader__.load({
|
|
|
3368
3689
|
onClick: onClose
|
|
3369
3690
|
}, t("wfCancel")))));
|
|
3370
3691
|
}
|
|
3692
|
+
/** Shared checkbox row for the transfer dialogs: name, badges, description. */
|
|
3693
|
+
function TransferItem(props) {
|
|
3694
|
+
return (0, react.createElement)("label", { className: "dsc-transfer-item" }, (0, react.createElement)("input", {
|
|
3695
|
+
type: "checkbox",
|
|
3696
|
+
checked: props.checked,
|
|
3697
|
+
onChange: props.onToggle
|
|
3698
|
+
}), (0, react.createElement)("span", {
|
|
3699
|
+
className: "dsc-transfer-item-name",
|
|
3700
|
+
title: props.name
|
|
3701
|
+
}, props.name), props.paramBadge !== null ? (0, react.createElement)("span", { className: "dsc-badge" }, props.paramBadge) : null, props.skillBadge !== null ? (0, react.createElement)("span", { className: "dsc-badge dsc-badge--ok" }, props.skillBadge) : null, props.description !== "" ? (0, react.createElement)("span", {
|
|
3702
|
+
className: "dsc-transfer-item-desc",
|
|
3703
|
+
title: props.description
|
|
3704
|
+
}, props.description) : null, props.warnings.length > 0 ? (0, react.createElement)("span", {
|
|
3705
|
+
className: "dsc-transfer-item-desc dsc-danger-text",
|
|
3706
|
+
title: props.warnings.join(";")
|
|
3707
|
+
}, props.warnings.join(";")) : null);
|
|
3708
|
+
}
|
|
3709
|
+
/** While a transfer dialog is open, swallow drag events aimed at the host
|
|
3710
|
+
* page. The conversation shell attaches dropped files to the chat with
|
|
3711
|
+
* document-level bubble listeners, so dragging a preset toward the dialog
|
|
3712
|
+
* also dropped it into the conversation. Window-capture listeners run before
|
|
3713
|
+
* any document-bubble handler and shield everything OUTSIDE the dialog;
|
|
3714
|
+
* events landing inside the dialog pass through — the dialog box itself must
|
|
3715
|
+
* stopPropagation() them at React's dispatch root, or they bubble on to the
|
|
3716
|
+
* document and the shell attaches the file anyway. */
|
|
3717
|
+
function DragGuard() {
|
|
3718
|
+
(0, react.useEffect)(() => {
|
|
3719
|
+
const guard = (event) => {
|
|
3720
|
+
if (event.target instanceof Element && event.target.closest(".dsc-transfer") !== null) return;
|
|
3721
|
+
event.preventDefault();
|
|
3722
|
+
event.stopPropagation();
|
|
3723
|
+
};
|
|
3724
|
+
for (const type of [
|
|
3725
|
+
"dragenter",
|
|
3726
|
+
"dragover",
|
|
3727
|
+
"drop"
|
|
3728
|
+
]) window.addEventListener(type, guard, true);
|
|
3729
|
+
return () => {
|
|
3730
|
+
for (const type of [
|
|
3731
|
+
"dragenter",
|
|
3732
|
+
"dragover",
|
|
3733
|
+
"drop"
|
|
3734
|
+
]) window.removeEventListener(type, guard, true);
|
|
3735
|
+
};
|
|
3736
|
+
}, []);
|
|
3737
|
+
return null;
|
|
3738
|
+
}
|
|
3739
|
+
/** Export dialog: pick library workflows (checkboxes + select-all, all
|
|
3740
|
+
* pre-selected) and download one preset package. Packaging happens host-side;
|
|
3741
|
+
* the dialog lists, selects, and submits a native form download. */
|
|
3742
|
+
function ExportDialog(props) {
|
|
3743
|
+
const { t, workflows } = props;
|
|
3744
|
+
const [selected, setSelected] = (0, react.useState)(() => new Set(workflows.map((workflow) => workflow.id)));
|
|
3745
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3746
|
+
const toggle = (id) => {
|
|
3747
|
+
setSelected((prev) => {
|
|
3748
|
+
const next = new Set(prev);
|
|
3749
|
+
if (next.has(id)) next.delete(id);
|
|
3750
|
+
else next.add(id);
|
|
3751
|
+
return next;
|
|
3752
|
+
});
|
|
3753
|
+
};
|
|
3754
|
+
const allSelected = workflows.length > 0 && selected.size === workflows.length;
|
|
3755
|
+
const toggleAll = () => {
|
|
3756
|
+
setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(workflows.map((workflow) => workflow.id)));
|
|
3757
|
+
};
|
|
3758
|
+
/** Download natively: a hidden form GETs `/comfyui/workflows/export/<file
|
|
3759
|
+
* name>?ids=…` and the server answers with `content-disposition:
|
|
3760
|
+
* attachment`, so the browser — not JS blob plumbing (fetch → blob →
|
|
3761
|
+
* objectURL → revoke, which one environment turned into a 0-byte file) —
|
|
3762
|
+
* owns the transfer. The page never navigates because the response is an
|
|
3763
|
+
* attachment.
|
|
3764
|
+
*
|
|
3765
|
+
* GET with the name in the path is deliberate: this setup's download
|
|
3766
|
+
* manager mangles POST downloads (renames them, or re-fetches the URL on
|
|
3767
|
+
* its own), which is where the phantom "skills sometimes missing" zips
|
|
3768
|
+
* came from. A GET is the one verb every manager handles correctly — and
|
|
3769
|
+
* since the name rides in the URL, saved name == receipt name even under
|
|
3770
|
+
* interception, and a manager's own re-fetch still gets a genuine export
|
|
3771
|
+
* of the same ids (the route is a pure read).
|
|
3772
|
+
*
|
|
3773
|
+
* The form response is invisible to JS, so the notice is fed from
|
|
3774
|
+
* `export/last` — the server's record of what it ACTUALLY packaged — not
|
|
3775
|
+
* from the checkboxes. */
|
|
3776
|
+
const exportSelected = () => {
|
|
3777
|
+
if (busy) return;
|
|
3778
|
+
setBusy(true);
|
|
3779
|
+
(async () => {
|
|
3780
|
+
let previousAt = null;
|
|
3781
|
+
try {
|
|
3782
|
+
previousAt = (await getJson("/comfyui/workflows/export/last")).last?.at ?? null;
|
|
3783
|
+
} catch {}
|
|
3784
|
+
const now = /* @__PURE__ */ new Date();
|
|
3785
|
+
const p = (n, width = 2) => String(n).padStart(width, "0");
|
|
3786
|
+
const filename = `dsh-comfyui-presets-${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}-${p(now.getMilliseconds(), 3)}-${Math.random().toString(36).slice(2, 8)}.zip`;
|
|
3787
|
+
const form = document.createElement("form");
|
|
3788
|
+
form.method = "GET";
|
|
3789
|
+
form.action = `/comfyui/workflows/export/${filename}`;
|
|
3790
|
+
form.style.display = "none";
|
|
3791
|
+
for (const id of selected) {
|
|
3792
|
+
const input = document.createElement("input");
|
|
3793
|
+
input.type = "hidden";
|
|
3794
|
+
input.name = "ids";
|
|
3795
|
+
input.value = id;
|
|
3796
|
+
form.append(input);
|
|
3797
|
+
}
|
|
3798
|
+
document.body.append(form);
|
|
3799
|
+
form.submit();
|
|
3800
|
+
form.remove();
|
|
3801
|
+
let last = null;
|
|
3802
|
+
for (let attempt = 0; attempt < 10 && last === null; attempt++) {
|
|
3803
|
+
await new Promise((resolve) => window.setTimeout(resolve, 350));
|
|
3804
|
+
try {
|
|
3805
|
+
const payload = await getJson("/comfyui/workflows/export/last");
|
|
3806
|
+
if (payload.last !== null && payload.last.at !== previousAt) last = payload.last;
|
|
3807
|
+
} catch {}
|
|
3808
|
+
}
|
|
3809
|
+
const names = (last?.names ?? selectedNames()).join("、");
|
|
3810
|
+
let notice = t("transferExportDone", {
|
|
3811
|
+
n: last?.count ?? selected.size,
|
|
3812
|
+
names
|
|
3813
|
+
});
|
|
3814
|
+
if (last !== null) notice += " " + t("transferExportFile", {
|
|
3815
|
+
file: last.filename,
|
|
3816
|
+
size: Math.max(1, Math.round(last.size / 1024))
|
|
3817
|
+
});
|
|
3818
|
+
if (last !== null && last.warnings.length > 0) notice += " " + t("transferExportWarnings", { warnings: last.warnings.join(";") });
|
|
3819
|
+
props.onDone(notice);
|
|
3820
|
+
})();
|
|
3821
|
+
};
|
|
3822
|
+
/** Names behind the current selection, dialog order, with pack-bearing
|
|
3823
|
+
* entries annotated. Shown next to the submit button so "what will be
|
|
3824
|
+
* packaged — and will it carry skills" is readable at the decision point:
|
|
3825
|
+
* a selection that happens to exclude the one pack-bearing workflow looks
|
|
3826
|
+
* exactly like "skills export unreliably" from the zip alone. */
|
|
3827
|
+
const selectedNames = () => workflows.filter((workflow) => selected.has(workflow.id)).map((workflow) => workflow.skillDir !== void 0 ? `${workflow.name} (${t("skillBadge")})` : workflow.name);
|
|
3828
|
+
return (0, react.createElement)("div", {
|
|
3829
|
+
className: "dsc-picker-overlay",
|
|
3830
|
+
onClick: props.onClose
|
|
3831
|
+
}, (0, react.createElement)(DragGuard), (0, react.createElement)("div", {
|
|
3832
|
+
className: "dsc-transfer",
|
|
3833
|
+
onClick: (event) => event.stopPropagation(),
|
|
3834
|
+
onDragEnter: (event) => {
|
|
3835
|
+
event.preventDefault();
|
|
3836
|
+
event.stopPropagation();
|
|
3837
|
+
},
|
|
3838
|
+
onDragOver: (event) => {
|
|
3839
|
+
event.preventDefault();
|
|
3840
|
+
event.stopPropagation();
|
|
3841
|
+
},
|
|
3842
|
+
onDrop: (event) => {
|
|
3843
|
+
event.preventDefault();
|
|
3844
|
+
event.stopPropagation();
|
|
3845
|
+
}
|
|
3846
|
+
}, (0, react.createElement)("div", { className: "dsc-transfer-head" }, (0, react.createElement)("span", { className: "dsc-transfer-title" }, t("transferExportTitle")), (0, react.createElement)("button", {
|
|
3847
|
+
className: "dsc-panel-close",
|
|
3848
|
+
"aria-label": t("close"),
|
|
3849
|
+
onClick: props.onClose
|
|
3850
|
+
}, "✕")), workflows.length === 0 ? (0, react.createElement)("div", { className: "dsc-transfer-empty" }, t("transferExportEmpty")) : (0, react.createElement)("div", { className: "dsc-transfer-body" }, (0, react.createElement)("div", { className: "dsc-transfer-bar" }, (0, react.createElement)("span", { className: "dsc-hint" }, t("transferExportHint", {
|
|
3851
|
+
total: workflows.length,
|
|
3852
|
+
selected: selected.size
|
|
3853
|
+
})), (0, react.createElement)("button", {
|
|
3854
|
+
className: "dsc-btn dsc-btn--sm",
|
|
3855
|
+
onClick: toggleAll
|
|
3856
|
+
}, allSelected ? t("transferSelectNone") : t("transferSelectAll"))), (0, react.createElement)("div", { className: "dsc-transfer-list" }, workflows.map((workflow) => (0, react.createElement)(TransferItem, {
|
|
3857
|
+
key: workflow.id,
|
|
3858
|
+
name: workflow.name,
|
|
3859
|
+
description: workflow.description,
|
|
3860
|
+
paramBadge: (workflow.parameters ?? []).length > 0 ? t("transferParamBadge", { n: (workflow.parameters ?? []).length }) : null,
|
|
3861
|
+
skillBadge: workflow.skillDir !== void 0 ? t("skillBadge") : null,
|
|
3862
|
+
warnings: [],
|
|
3863
|
+
checked: selected.has(workflow.id),
|
|
3864
|
+
onToggle: () => toggle(workflow.id)
|
|
3865
|
+
})))), selected.size > 0 ? (0, react.createElement)("div", { className: "dsc-transfer-note" }, (0, react.createElement)("span", {
|
|
3866
|
+
className: "dsc-hint",
|
|
3867
|
+
title: selectedNames().join("、")
|
|
3868
|
+
}, t("transferSelectedList", { names: selectedNames().join("、") }))) : null, (0, react.createElement)("div", { className: "dsc-transfer-actions" }, workflows.length === 0 ? null : (0, react.createElement)("button", {
|
|
3869
|
+
className: "dsc-btn dsc-btn--primary",
|
|
3870
|
+
disabled: busy || selected.size === 0,
|
|
3871
|
+
onClick: () => exportSelected()
|
|
3872
|
+
}, busy ? t("transferExporting") : t("transferExportRun", { n: selected.size })), (0, react.createElement)("button", {
|
|
3873
|
+
className: "dsc-btn",
|
|
3874
|
+
disabled: busy,
|
|
3875
|
+
onClick: props.onClose
|
|
3876
|
+
}, t("cancel")))));
|
|
3877
|
+
}
|
|
3878
|
+
/** Import dialog: choose a preset package (file picker or drag-drop), see
|
|
3879
|
+
* what it offers, then import the selection as NEW workflows. The file is
|
|
3880
|
+
* POSTed twice by design — analyze and apply parse the same bytes, so the
|
|
3881
|
+
* host needs no temp-file staging or cleanup. */
|
|
3882
|
+
function ImportDialog(props) {
|
|
3883
|
+
const { t } = props;
|
|
3884
|
+
const [file, setFile] = (0, react.useState)(null);
|
|
3885
|
+
const [analysis, setAnalysis] = (0, react.useState)(null);
|
|
3886
|
+
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3887
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3888
|
+
const [error, setError] = (0, react.useState)(null);
|
|
3889
|
+
const [results, setResults] = (0, react.useState)(null);
|
|
3890
|
+
const [imported, setImported] = (0, react.useState)(0);
|
|
3891
|
+
const [dragOver, setDragOver] = (0, react.useState)(false);
|
|
3892
|
+
/** Phase 1: upload the package for analysis (no server-side writes). */
|
|
3893
|
+
const analyze = async (picked) => {
|
|
3894
|
+
setBusy(true);
|
|
3895
|
+
setError(null);
|
|
3896
|
+
try {
|
|
3897
|
+
const data = await postRaw("/comfyui/workflows/import/analyze", await picked.arrayBuffer());
|
|
3898
|
+
if (data.ok !== true || data.analysis === void 0) throw new Error(data.error ?? "failed to analyze");
|
|
3899
|
+
setFile(picked);
|
|
3900
|
+
setAnalysis(data.analysis);
|
|
3901
|
+
setSelected(new Set(data.analysis.workflows.map((entry) => entry.index)));
|
|
3902
|
+
} catch (cause) {
|
|
3903
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
3904
|
+
} finally {
|
|
3905
|
+
setBusy(false);
|
|
3906
|
+
}
|
|
3907
|
+
};
|
|
3908
|
+
const toggle = (index) => {
|
|
3909
|
+
setSelected((prev) => {
|
|
3910
|
+
const next = new Set(prev);
|
|
3911
|
+
if (next.has(index)) next.delete(index);
|
|
3912
|
+
else next.add(index);
|
|
3913
|
+
return next;
|
|
3914
|
+
});
|
|
3915
|
+
};
|
|
3916
|
+
const allSelected = analysis !== null && analysis.workflows.length > 0 && selected.size === analysis.workflows.length;
|
|
3917
|
+
const toggleAll = () => {
|
|
3918
|
+
if (analysis === null) return;
|
|
3919
|
+
setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(analysis.workflows.map((entry) => entry.index)));
|
|
3920
|
+
};
|
|
3921
|
+
/** Phase 2: re-upload the same bytes with the selection as manifest indexes. */
|
|
3922
|
+
const importSelected = async () => {
|
|
3923
|
+
if (file === null) return;
|
|
3924
|
+
setBusy(true);
|
|
3925
|
+
setError(null);
|
|
3926
|
+
try {
|
|
3927
|
+
const bytes = await file.arrayBuffer();
|
|
3928
|
+
const data = await postRaw(`/comfyui/workflows/import/apply?select=${[...selected].join(",")}`, bytes);
|
|
3929
|
+
if (data.ok !== true || data.results === void 0) throw new Error(data.error ?? "failed to import");
|
|
3930
|
+
setResults(data.results);
|
|
3931
|
+
setImported(data.imported ?? 0);
|
|
3932
|
+
} catch (cause) {
|
|
3933
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
3934
|
+
} finally {
|
|
3935
|
+
setBusy(false);
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
const finish = () => {
|
|
3939
|
+
if (imported > 0) props.onDone(t("transferImportDone", { n: imported }));
|
|
3940
|
+
else props.onClose();
|
|
3941
|
+
};
|
|
3942
|
+
return (0, react.createElement)("div", {
|
|
3943
|
+
className: "dsc-picker-overlay",
|
|
3944
|
+
onClick: props.onClose
|
|
3945
|
+
}, (0, react.createElement)(DragGuard), (0, react.createElement)("div", {
|
|
3946
|
+
className: "dsc-transfer",
|
|
3947
|
+
onClick: (event) => event.stopPropagation(),
|
|
3948
|
+
onDragEnter: (event) => {
|
|
3949
|
+
event.preventDefault();
|
|
3950
|
+
event.stopPropagation();
|
|
3951
|
+
},
|
|
3952
|
+
onDragOver: (event) => {
|
|
3953
|
+
event.preventDefault();
|
|
3954
|
+
event.stopPropagation();
|
|
3955
|
+
},
|
|
3956
|
+
onDrop: (event) => {
|
|
3957
|
+
event.preventDefault();
|
|
3958
|
+
event.stopPropagation();
|
|
3959
|
+
}
|
|
3960
|
+
}, (0, react.createElement)("div", { className: "dsc-transfer-head" }, (0, react.createElement)("span", { className: "dsc-transfer-title" }, t("transferImportTitle")), (0, react.createElement)("button", {
|
|
3961
|
+
className: "dsc-panel-close",
|
|
3962
|
+
"aria-label": t("close"),
|
|
3963
|
+
onClick: props.onClose
|
|
3964
|
+
}, "✕")), (0, react.createElement)("div", { className: "dsc-transfer-body" }, results !== null ? (0, react.createElement)("div", { className: "dsc-transfer-list" }, results.map((outcome) => (0, react.createElement)("div", {
|
|
3965
|
+
key: outcome.index,
|
|
3966
|
+
className: `dsc-transfer-item dsc-transfer-item--static${outcome.ok ? " dsc-transfer-item--ok" : " dsc-transfer-item--fail"}`
|
|
3967
|
+
}, (0, react.createElement)("span", {
|
|
3968
|
+
className: "dsc-transfer-item-name",
|
|
3969
|
+
title: outcome.name
|
|
3970
|
+
}, outcome.ok ? t("transferImportedItem", {
|
|
3971
|
+
src: outcome.name,
|
|
3972
|
+
dst: outcome.newName ?? ""
|
|
3973
|
+
}) : t("transferImportFailedItem", {
|
|
3974
|
+
src: outcome.name,
|
|
3975
|
+
reason: outcome.error ?? ""
|
|
3976
|
+
})), outcome.warnings.length > 0 ? (0, react.createElement)("span", {
|
|
3977
|
+
className: "dsc-transfer-item-desc",
|
|
3978
|
+
title: outcome.warnings.join(";")
|
|
3979
|
+
}, outcome.warnings.join(";")) : null))) : analysis === null ? (0, react.createElement)("label", {
|
|
3980
|
+
className: `dsc-transfer-drop${dragOver ? " dsc-transfer-drop--over" : ""}`,
|
|
3981
|
+
onDragOver: (event) => {
|
|
3982
|
+
event.preventDefault();
|
|
3983
|
+
setDragOver(true);
|
|
3984
|
+
},
|
|
3985
|
+
onDragLeave: () => setDragOver(false),
|
|
3986
|
+
onDrop: (event) => {
|
|
3987
|
+
event.preventDefault();
|
|
3988
|
+
setDragOver(false);
|
|
3989
|
+
const dropped = event.dataTransfer?.files?.[0];
|
|
3990
|
+
if (dropped !== void 0) analyze(dropped);
|
|
3991
|
+
}
|
|
3992
|
+
}, (0, react.createElement)("input", {
|
|
3993
|
+
type: "file",
|
|
3994
|
+
accept: ".zip,application/zip,application/x-zip-compressed",
|
|
3995
|
+
style: { display: "none" },
|
|
3996
|
+
onChange: (event) => {
|
|
3997
|
+
const picked = event.target.files?.[0];
|
|
3998
|
+
if (picked !== void 0) analyze(picked);
|
|
3999
|
+
event.target.value = "";
|
|
4000
|
+
}
|
|
4001
|
+
}), busy ? t("transferImporting") : t("transferPickFile")) : (0, react.createElement)("div", { className: "dsc-transfer-body" }, (0, react.createElement)("div", { className: "dsc-transfer-bar" }, (0, react.createElement)("span", { className: "dsc-hint" }, t("transferPackageInfo", {
|
|
4002
|
+
n: analysis.workflows.length,
|
|
4003
|
+
date: formatTs(analysis.exportedAt)
|
|
4004
|
+
})), (0, react.createElement)("button", {
|
|
4005
|
+
className: "dsc-btn dsc-btn--sm",
|
|
4006
|
+
onClick: toggleAll
|
|
4007
|
+
}, allSelected ? t("transferSelectNone") : t("transferSelectAll"))), (0, react.createElement)("div", { className: "dsc-transfer-list" }, analysis.workflows.map((entry) => (0, react.createElement)(TransferItem, {
|
|
4008
|
+
key: entry.index,
|
|
4009
|
+
name: entry.name,
|
|
4010
|
+
description: entry.description,
|
|
4011
|
+
paramBadge: entry.paramCount > 0 ? t("transferParamBadge", { n: entry.paramCount }) : null,
|
|
4012
|
+
skillBadge: entry.skill !== null ? entry.skill.required ? t("transferSkillRequired", { n: entry.skill.fileCount }) : t("transferSkillBadge", { n: entry.skill.fileCount }) : null,
|
|
4013
|
+
warnings: entry.warnings,
|
|
4014
|
+
checked: selected.has(entry.index),
|
|
4015
|
+
onToggle: () => toggle(entry.index)
|
|
4016
|
+
})))), error !== null ? (0, react.createElement)(ErrorNote, {
|
|
4017
|
+
t,
|
|
4018
|
+
message: error
|
|
4019
|
+
}) : null, analysis !== null && results === null ? (0, react.createElement)("div", { className: "dsc-hint" }, t("transferImportNote")) : null, analysis === null && results === null ? (0, react.createElement)("div", { className: "dsc-hint" }, t("transferPickHint")) : null), (0, react.createElement)("div", { className: "dsc-transfer-actions" }, results !== null ? (0, react.createElement)("button", {
|
|
4020
|
+
className: "dsc-btn dsc-btn--primary",
|
|
4021
|
+
onClick: finish
|
|
4022
|
+
}, t("transferFinish")) : analysis !== null ? (0, react.createElement)("button", {
|
|
4023
|
+
className: "dsc-btn dsc-btn--primary",
|
|
4024
|
+
disabled: busy || selected.size === 0,
|
|
4025
|
+
onClick: () => {
|
|
4026
|
+
importSelected();
|
|
4027
|
+
}
|
|
4028
|
+
}, busy ? t("transferImporting") : t("transferImportRun", { n: selected.size })) : null, (0, react.createElement)("button", {
|
|
4029
|
+
className: "dsc-btn",
|
|
4030
|
+
disabled: busy,
|
|
4031
|
+
onClick: results !== null ? finish : props.onClose
|
|
4032
|
+
}, t("cancel")))));
|
|
4033
|
+
}
|
|
3371
4034
|
/** Read-only inspection of a ComfyUI-side saved workflow: node list + JSON. */
|
|
3372
4035
|
function WorkflowView(props) {
|
|
3373
4036
|
const nodes = Array.isArray(props.graph.nodes) ? props.graph.nodes.filter((node) => node.mode !== 4 && typeof node.type === "string" && node.type !== "") : [];
|
|
@@ -3894,6 +4557,40 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
|
|
|
3894
4557
|
.dsc-trigger:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-border-l1); }
|
|
3895
4558
|
.dsc-trigger[aria-pressed='true'] { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }
|
|
3896
4559
|
.dsc-trigger-glyph { font-size: 13px; line-height: 1; }
|
|
4560
|
+
/* --- connection reminder toast: fixed at the top of the page, mounted by the
|
|
4561
|
+
header trigger when the backend probe cannot reach ComfyUI. The probing
|
|
4562
|
+
state fades in after a short delay so a fast successful probe never paints;
|
|
4563
|
+
the fail state is sticky until dismissed or a later probe recovers. --- */
|
|
4564
|
+
.dsc-conn-toast {
|
|
4565
|
+
position: fixed; top: 14px; left: 50%;
|
|
4566
|
+
z-index: 980;
|
|
4567
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
4568
|
+
max-width: min(560px, 92vw);
|
|
4569
|
+
padding: 10px 12px;
|
|
4570
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
4571
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
4572
|
+
border-radius: 10px;
|
|
4573
|
+
box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3);
|
|
4574
|
+
color: var(--dsw-alias-label-primary);
|
|
4575
|
+
font-size: 12px; line-height: 1.5;
|
|
4576
|
+
animation: dsc-conn-toast-in 0.16s ease-out both;
|
|
4577
|
+
}
|
|
4578
|
+
.dsc-conn-toast--probing { animation-delay: 0.45s; }
|
|
4579
|
+
.dsc-conn-toast--ok { border-color: var(--dsw-alias-state-success-primary); }
|
|
4580
|
+
.dsc-conn-toast--fail { border-color: var(--dsw-alias-state-error-primary); }
|
|
4581
|
+
.dsc-conn-toast-dot { flex: none; width: 8px; height: 8px; margin-top: 5px; border-radius: 50%; }
|
|
4582
|
+
.dsc-conn-toast--probing .dsc-conn-toast-dot { background: var(--dsw-alias-state-warn-primary); animation: dsc-conn-dot 1s ease-in-out infinite; }
|
|
4583
|
+
.dsc-conn-toast--ok .dsc-conn-toast-dot { background: var(--dsw-alias-state-success-primary); }
|
|
4584
|
+
.dsc-conn-toast--fail .dsc-conn-toast-dot { background: var(--dsw-alias-state-error-primary); }
|
|
4585
|
+
.dsc-conn-toast-body { min-width: 0; }
|
|
4586
|
+
.dsc-conn-toast-title { font-weight: 600; }
|
|
4587
|
+
.dsc-conn-toast--fail .dsc-conn-toast-title { color: var(--dsw-alias-state-error-primary); }
|
|
4588
|
+
.dsc-conn-toast--ok .dsc-conn-toast-title { color: var(--dsw-alias-state-success-primary); }
|
|
4589
|
+
.dsc-conn-toast-text { color: var(--dsw-alias-label-secondary); margin-top: 2px; overflow-wrap: anywhere; }
|
|
4590
|
+
.dsc-conn-toast-close { flex: none; border: none; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; font-size: 13px; padding: 2px 4px; line-height: 1; }
|
|
4591
|
+
.dsc-conn-toast-close:hover { color: var(--dsw-alias-label-primary); }
|
|
4592
|
+
@keyframes dsc-conn-toast-in { from { opacity: 0; transform: translate(-50%, -6px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
|
4593
|
+
@keyframes dsc-conn-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
3897
4594
|
/* --- panel shell: floating window, percentage-anchored to the page header
|
|
3898
4595
|
(top) and the composer (bottom), draggable by the header and resizable via
|
|
3899
4596
|
the corner handle. Follows the host light/dark theme. --- */
|
|
@@ -3926,6 +4623,9 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
|
|
|
3926
4623
|
}
|
|
3927
4624
|
.dsc-panel-resize:hover { opacity: 1; }
|
|
3928
4625
|
.dsc-panel-title { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; font-size: 14px; }
|
|
4626
|
+
.dsc-panel-head-actions { display: inline-flex; align-items: center; gap: 2px; }
|
|
4627
|
+
.dsc-panel-reset { border: none; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; font-size: 13px; padding: 4px 6px; line-height: 1; }
|
|
4628
|
+
.dsc-panel-reset:hover { color: var(--dsw-alias-label-primary); }
|
|
3929
4629
|
.dsc-panel-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 14px; padding: 4px 8px; }
|
|
3930
4630
|
.dsc-panel-close:hover { color: var(--dsw-alias-label-primary); }
|
|
3931
4631
|
.dsc-tabs { display: flex; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
@@ -3976,7 +4676,12 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
|
|
|
3976
4676
|
.dsc-skill-files { flex: 0 0 190px; min-width: 160px; display: flex; flex-direction: column; gap: 6px; }
|
|
3977
4677
|
.dsc-skill-main { flex: 1 1 300px; min-width: 260px; display: flex; flex-direction: column; gap: 6px; }
|
|
3978
4678
|
.dsc-skill-bucket { display: flex; flex-direction: column; gap: 2px; }
|
|
3979
|
-
|
|
4679
|
+
/* Foldable pack directories: each folder renders as a <details> whose header
|
|
4680
|
+
stays put while its files collapse, keeping crowded packs scannable. */
|
|
4681
|
+
.dsc-skill-fold { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; margin: 2px 0; }
|
|
4682
|
+
.dsc-skill-fold summary { padding: 5px 10px; font-size: 12px; color: var(--dsw-alias-label-secondary); border-radius: 8px; }
|
|
4683
|
+
.dsc-skill-fold[open] summary { color: var(--dsw-alias-label-primary); border-bottom: 1px solid var(--dsw-alias-border-l1); border-radius: 8px 8px 0 0; }
|
|
4684
|
+
.dsc-skill-fold .dsc-skill-bucket { padding: 4px 6px 6px; }
|
|
3980
4685
|
.dsc-skill-file {
|
|
3981
4686
|
display: flex; align-items: center; justify-content: space-between; gap: 6px;
|
|
3982
4687
|
border: 1px solid transparent; border-radius: 6px; background: transparent;
|
|
@@ -4191,6 +4896,28 @@ video.dsc-picker-player { max-height: 180px; }
|
|
|
4191
4896
|
.dsc-param-random { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
4192
4897
|
.dsc-param-bool { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dsw-alias-label-primary); cursor: pointer; }
|
|
4193
4898
|
.dsc-param-advanced { display: flex; flex-direction: column; gap: 8px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 8px; padding: 8px; }
|
|
4899
|
+
/* --- workflow preset transfer dialogs (export / import) --- */
|
|
4900
|
+
.dsc-transfer { position: relative; display: flex; flex-direction: column; width: min(580px, 92vw); max-height: min(640px, 86vh); 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; }
|
|
4901
|
+
.dsc-transfer-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
4902
|
+
.dsc-transfer-title { font-weight: 600; font-size: 13px; color: var(--dsw-alias-label-primary); }
|
|
4903
|
+
.dsc-transfer-body { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; overflow-y: auto; min-height: 0; }
|
|
4904
|
+
.dsc-transfer-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
4905
|
+
.dsc-transfer-list { display: flex; flex-direction: column; gap: 4px; }
|
|
4906
|
+
.dsc-transfer-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; cursor: pointer; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
4907
|
+
.dsc-transfer-item:hover { border-color: var(--dsw-alias-brand-primary); }
|
|
4908
|
+
.dsc-transfer-item--static, .dsc-transfer-item--static:hover { cursor: default; border-color: var(--dsw-alias-border-l1); }
|
|
4909
|
+
.dsc-transfer-item--ok { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 45%, transparent); }
|
|
4910
|
+
.dsc-transfer-item--fail { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); }
|
|
4911
|
+
.dsc-transfer-item-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 240px; }
|
|
4912
|
+
.dsc-transfer-item-desc { color: var(--dsw-alias-label-tertiary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 40px; text-align: right; }
|
|
4913
|
+
.dsc-transfer-actions { display: flex; gap: 8px; justify-content: flex-end; padding: 10px 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
4914
|
+
.dsc-transfer-note { padding: 6px 12px 0; }
|
|
4915
|
+
.dsc-transfer-note .dsc-hint { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
4916
|
+
.dsc-transfer-drop { display: flex; align-items: center; justify-content: center; min-height: 150px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 10px; color: var(--dsw-alias-label-secondary); font-size: 13px; cursor: pointer; text-align: center; padding: 12px; transition: border-color 0.12s, color 0.12s; }
|
|
4917
|
+
.dsc-transfer-drop:hover, .dsc-transfer-drop--over { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
4918
|
+
.dsc-transfer-empty { padding: 40px 12px; text-align: center; color: var(--dsw-alias-label-tertiary); font-size: 13px; }
|
|
4919
|
+
.dsc-btn--primary { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
4920
|
+
.dsc-btn--primary:disabled { opacity: 0.5; cursor: default; }
|
|
4194
4921
|
`;
|
|
4195
4922
|
/** Inject the stylesheet once (idempotent), owned by this plugin for HMR. */
|
|
4196
4923
|
function injectStyles() {
|