dsh-comfyui 0.4.0 → 0.5.1
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 +3 -1
- package/README.md +3 -1
- package/client/client.js +644 -17
- package/client/client.js.map +1 -1
- package/lib/analyze.d.ts +3 -1
- package/lib/analyze.js +2 -1
- package/lib/convert.d.ts +0 -9
- package/lib/convert.js +15 -7
- package/lib/graph.d.ts +15 -0
- package/lib/graph.js +48 -0
- package/lib/index.js +9 -2
- 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 +3 -2
package/client/client.js
CHANGED
|
@@ -61,6 +61,10 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
error: "出错",
|
|
62
62
|
refresh: "刷新",
|
|
63
63
|
ours: "本插件",
|
|
64
|
+
connChecking: "正在检测 ComfyUI 连接…",
|
|
65
|
+
connOk: "连接正常 · ComfyUI {version} · {ms} ms",
|
|
66
|
+
connFailTitle: "无法连接 ComfyUI",
|
|
67
|
+
connFailBody: "后端未能连上 ComfyUI({message})。请先启动本机 ComfyUI,确保后端能检测到其端口;若使用远程服务器,请到设置页检查服务器地址并点「测试连接」。",
|
|
64
68
|
wfAdd: "新建工作流",
|
|
65
69
|
wfName: "名称",
|
|
66
70
|
wfDesc: "概述",
|
|
@@ -131,7 +135,34 @@ window.__ModuleLoader__.load({
|
|
|
131
135
|
wfEmpty: "没有检测到任何工作流,请通过新建、导入或提取来创建执行流程。",
|
|
132
136
|
wfLibrarySection: "API 工作流",
|
|
133
137
|
wfRefresh: "刷新",
|
|
134
|
-
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: "完成",
|
|
135
166
|
wfComfyuiSection: "ComfyUI 工作流",
|
|
136
167
|
wfComfyuiHint: "面板自动检测 ComfyUI 中保存的工作流。\n注:非 API 流程,无法直接执行,需经过提取转化为 API 工作流,或自行导入 API 工作流即可执行。",
|
|
137
168
|
wfComfyuiEmpty: "ComfyUI 端没有检测到保存的工作流",
|
|
@@ -293,6 +324,10 @@ window.__ModuleLoader__.load({
|
|
|
293
324
|
error: "Error",
|
|
294
325
|
refresh: "Refresh",
|
|
295
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\".",
|
|
296
331
|
wfAdd: "New workflow",
|
|
297
332
|
wfName: "Name",
|
|
298
333
|
wfDesc: "Overview",
|
|
@@ -363,7 +398,34 @@ window.__ModuleLoader__.load({
|
|
|
363
398
|
wfEmpty: "No workflows detected — create one via New, Import, or Extract.",
|
|
364
399
|
wfLibrarySection: "API workflows",
|
|
365
400
|
wfRefresh: "Refresh",
|
|
366
|
-
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",
|
|
367
429
|
wfComfyuiSection: "ComfyUI workflows",
|
|
368
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.",
|
|
369
431
|
wfComfyuiEmpty: "No workflows detected on the ComfyUI server",
|
|
@@ -997,49 +1059,49 @@ window.__ModuleLoader__.load({
|
|
|
997
1059
|
* and the session-header trigger are separate registrations in one bundle,
|
|
998
1060
|
* so they coordinate through this module-level store plus useSyncExternalStore.
|
|
999
1061
|
*/
|
|
1000
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
1062
|
+
const listeners$1 = /* @__PURE__ */ new Set();
|
|
1001
1063
|
let open = false;
|
|
1002
1064
|
let tab = "workflows";
|
|
1003
1065
|
let assetFilter = "";
|
|
1004
|
-
function emit() {
|
|
1005
|
-
for (const listener of listeners) listener();
|
|
1066
|
+
function emit$1() {
|
|
1067
|
+
for (const listener of listeners$1) listener();
|
|
1006
1068
|
}
|
|
1007
1069
|
const panelStore = {
|
|
1008
1070
|
isOpen: () => open,
|
|
1009
1071
|
toggle: () => {
|
|
1010
1072
|
open = !open;
|
|
1011
|
-
emit();
|
|
1073
|
+
emit$1();
|
|
1012
1074
|
},
|
|
1013
1075
|
open: () => {
|
|
1014
1076
|
if (!open) {
|
|
1015
1077
|
open = true;
|
|
1016
|
-
emit();
|
|
1078
|
+
emit$1();
|
|
1017
1079
|
}
|
|
1018
1080
|
},
|
|
1019
1081
|
close: () => {
|
|
1020
1082
|
if (open) {
|
|
1021
1083
|
open = false;
|
|
1022
|
-
emit();
|
|
1084
|
+
emit$1();
|
|
1023
1085
|
}
|
|
1024
1086
|
},
|
|
1025
1087
|
getTab: () => tab,
|
|
1026
1088
|
setTab: (next) => {
|
|
1027
1089
|
if (tab !== next) {
|
|
1028
1090
|
tab = next;
|
|
1029
|
-
emit();
|
|
1091
|
+
emit$1();
|
|
1030
1092
|
}
|
|
1031
1093
|
},
|
|
1032
1094
|
getAssetFilter: () => assetFilter,
|
|
1033
1095
|
setAssetFilter: (name) => {
|
|
1034
1096
|
if (assetFilter !== name) {
|
|
1035
1097
|
assetFilter = name;
|
|
1036
|
-
emit();
|
|
1098
|
+
emit$1();
|
|
1037
1099
|
}
|
|
1038
1100
|
},
|
|
1039
1101
|
subscribe(listener) {
|
|
1040
|
-
listeners.add(listener);
|
|
1102
|
+
listeners$1.add(listener);
|
|
1041
1103
|
return () => {
|
|
1042
|
-
listeners.delete(listener);
|
|
1104
|
+
listeners$1.delete(listener);
|
|
1043
1105
|
};
|
|
1044
1106
|
}
|
|
1045
1107
|
};
|
|
@@ -1052,10 +1114,132 @@ window.__ModuleLoader__.load({
|
|
|
1052
1114
|
return (0, react.useSyncExternalStore)(panelStore.subscribe, panelStore.getTab, panelStore.getTab);
|
|
1053
1115
|
}
|
|
1054
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
|
|
1055
1238
|
//#region src/client/trigger.tsx
|
|
1056
1239
|
/**
|
|
1057
1240
|
* The session-header trigger (conversation.session.header.actions, id
|
|
1058
|
-
* '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.
|
|
1059
1243
|
*/
|
|
1060
1244
|
/**
|
|
1061
1245
|
* ComfyUI brand glyph (mono), inlined from @lobehub/icons-static-svg
|
|
@@ -1075,16 +1259,22 @@ window.__ModuleLoader__.load({
|
|
|
1075
1259
|
"aria-hidden": true
|
|
1076
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" }));
|
|
1077
1261
|
}
|
|
1078
|
-
/** 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. */
|
|
1079
1265
|
function ComfyUITrigger({ t }) {
|
|
1080
1266
|
const open = usePanelOpen();
|
|
1081
|
-
return (0, react.createElement)("button", {
|
|
1267
|
+
return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("button", {
|
|
1082
1268
|
className: "dsc-trigger",
|
|
1083
1269
|
title: t("panelTitle"),
|
|
1084
1270
|
"aria-label": t("panelTitle"),
|
|
1085
1271
|
"aria-pressed": open,
|
|
1086
|
-
onClick: () =>
|
|
1087
|
-
|
|
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 }));
|
|
1088
1278
|
}
|
|
1089
1279
|
//#endregion
|
|
1090
1280
|
//#region src/client/panel.tsx
|
|
@@ -2971,6 +3161,8 @@ window.__ModuleLoader__.load({
|
|
|
2971
3161
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2972
3162
|
const [viewing, setViewing] = (0, react.useState)(null);
|
|
2973
3163
|
const [extract, setExtract] = (0, react.useState)(null);
|
|
3164
|
+
/** Open transfer dialog: 'export' | 'import', null = closed. */
|
|
3165
|
+
const [transfer, setTransfer] = (0, react.useState)(null);
|
|
2974
3166
|
const [tagFilter, setTagFilter] = (0, react.useState)(null);
|
|
2975
3167
|
const [skillFor, setSkillFor] = (0, react.useState)(null);
|
|
2976
3168
|
/** A delete waiting on the user's answer about the workflow's skill pack. */
|
|
@@ -3174,6 +3366,25 @@ window.__ModuleLoader__.load({
|
|
|
3174
3366
|
onRun: (state) => runExtract(state),
|
|
3175
3367
|
onClose: () => setExtract(null)
|
|
3176
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
|
+
});
|
|
3177
3388
|
if (error !== null) return (0, react.createElement)("div", null, (0, react.createElement)(ErrorNote, {
|
|
3178
3389
|
t,
|
|
3179
3390
|
message: error
|
|
@@ -3382,6 +3593,20 @@ window.__ModuleLoader__.load({
|
|
|
3382
3593
|
event.target.value = "";
|
|
3383
3594
|
}
|
|
3384
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", {
|
|
3385
3610
|
className: "dsc-btn",
|
|
3386
3611
|
disabled: busy,
|
|
3387
3612
|
onClick: () => void load()
|
|
@@ -3403,6 +3628,10 @@ window.__ModuleLoader__.load({
|
|
|
3403
3628
|
...state,
|
|
3404
3629
|
error: data.error ?? "failed to analyze"
|
|
3405
3630
|
});
|
|
3631
|
+
else if (data.analysis.ok !== true) onUpdate({
|
|
3632
|
+
...state,
|
|
3633
|
+
error: data.analysis.error
|
|
3634
|
+
});
|
|
3406
3635
|
else onUpdate({
|
|
3407
3636
|
...state,
|
|
3408
3637
|
analysis: data.analysis,
|
|
@@ -3464,6 +3693,348 @@ window.__ModuleLoader__.load({
|
|
|
3464
3693
|
onClick: onClose
|
|
3465
3694
|
}, t("wfCancel")))));
|
|
3466
3695
|
}
|
|
3696
|
+
/** Shared checkbox row for the transfer dialogs: name, badges, description. */
|
|
3697
|
+
function TransferItem(props) {
|
|
3698
|
+
return (0, react.createElement)("label", { className: "dsc-transfer-item" }, (0, react.createElement)("input", {
|
|
3699
|
+
type: "checkbox",
|
|
3700
|
+
checked: props.checked,
|
|
3701
|
+
onChange: props.onToggle
|
|
3702
|
+
}), (0, react.createElement)("span", {
|
|
3703
|
+
className: "dsc-transfer-item-name",
|
|
3704
|
+
title: props.name
|
|
3705
|
+
}, 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", {
|
|
3706
|
+
className: "dsc-transfer-item-desc",
|
|
3707
|
+
title: props.description
|
|
3708
|
+
}, props.description) : null, props.warnings.length > 0 ? (0, react.createElement)("span", {
|
|
3709
|
+
className: "dsc-transfer-item-desc dsc-danger-text",
|
|
3710
|
+
title: props.warnings.join(";")
|
|
3711
|
+
}, props.warnings.join(";")) : null);
|
|
3712
|
+
}
|
|
3713
|
+
/** While a transfer dialog is open, swallow drag events aimed at the host
|
|
3714
|
+
* page. The conversation shell attaches dropped files to the chat with
|
|
3715
|
+
* document-level bubble listeners, so dragging a preset toward the dialog
|
|
3716
|
+
* also dropped it into the conversation. Window-capture listeners run before
|
|
3717
|
+
* any document-bubble handler and shield everything OUTSIDE the dialog;
|
|
3718
|
+
* events landing inside the dialog pass through — the dialog box itself must
|
|
3719
|
+
* stopPropagation() them at React's dispatch root, or they bubble on to the
|
|
3720
|
+
* document and the shell attaches the file anyway. */
|
|
3721
|
+
function DragGuard() {
|
|
3722
|
+
(0, react.useEffect)(() => {
|
|
3723
|
+
const guard = (event) => {
|
|
3724
|
+
if (event.target instanceof Element && event.target.closest(".dsc-transfer") !== null) return;
|
|
3725
|
+
event.preventDefault();
|
|
3726
|
+
event.stopPropagation();
|
|
3727
|
+
};
|
|
3728
|
+
for (const type of [
|
|
3729
|
+
"dragenter",
|
|
3730
|
+
"dragover",
|
|
3731
|
+
"drop"
|
|
3732
|
+
]) window.addEventListener(type, guard, true);
|
|
3733
|
+
return () => {
|
|
3734
|
+
for (const type of [
|
|
3735
|
+
"dragenter",
|
|
3736
|
+
"dragover",
|
|
3737
|
+
"drop"
|
|
3738
|
+
]) window.removeEventListener(type, guard, true);
|
|
3739
|
+
};
|
|
3740
|
+
}, []);
|
|
3741
|
+
return null;
|
|
3742
|
+
}
|
|
3743
|
+
/** Export dialog: pick library workflows (checkboxes + select-all, all
|
|
3744
|
+
* pre-selected) and download one preset package. Packaging happens host-side;
|
|
3745
|
+
* the dialog lists, selects, and submits a native form download. */
|
|
3746
|
+
function ExportDialog(props) {
|
|
3747
|
+
const { t, workflows } = props;
|
|
3748
|
+
const [selected, setSelected] = (0, react.useState)(() => new Set(workflows.map((workflow) => workflow.id)));
|
|
3749
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3750
|
+
const toggle = (id) => {
|
|
3751
|
+
setSelected((prev) => {
|
|
3752
|
+
const next = new Set(prev);
|
|
3753
|
+
if (next.has(id)) next.delete(id);
|
|
3754
|
+
else next.add(id);
|
|
3755
|
+
return next;
|
|
3756
|
+
});
|
|
3757
|
+
};
|
|
3758
|
+
const allSelected = workflows.length > 0 && selected.size === workflows.length;
|
|
3759
|
+
const toggleAll = () => {
|
|
3760
|
+
setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(workflows.map((workflow) => workflow.id)));
|
|
3761
|
+
};
|
|
3762
|
+
/** Download natively: a hidden form GETs `/comfyui/workflows/export/<file
|
|
3763
|
+
* name>?ids=…` and the server answers with `content-disposition:
|
|
3764
|
+
* attachment`, so the browser — not JS blob plumbing (fetch → blob →
|
|
3765
|
+
* objectURL → revoke, which one environment turned into a 0-byte file) —
|
|
3766
|
+
* owns the transfer. The page never navigates because the response is an
|
|
3767
|
+
* attachment.
|
|
3768
|
+
*
|
|
3769
|
+
* GET with the name in the path is deliberate: this setup's download
|
|
3770
|
+
* manager mangles POST downloads (renames them, or re-fetches the URL on
|
|
3771
|
+
* its own), which is where the phantom "skills sometimes missing" zips
|
|
3772
|
+
* came from. A GET is the one verb every manager handles correctly — and
|
|
3773
|
+
* since the name rides in the URL, saved name == receipt name even under
|
|
3774
|
+
* interception, and a manager's own re-fetch still gets a genuine export
|
|
3775
|
+
* of the same ids (the route is a pure read).
|
|
3776
|
+
*
|
|
3777
|
+
* The form response is invisible to JS, so the notice is fed from
|
|
3778
|
+
* `export/last` — the server's record of what it ACTUALLY packaged — not
|
|
3779
|
+
* from the checkboxes. */
|
|
3780
|
+
const exportSelected = () => {
|
|
3781
|
+
if (busy) return;
|
|
3782
|
+
setBusy(true);
|
|
3783
|
+
(async () => {
|
|
3784
|
+
let previousAt = null;
|
|
3785
|
+
try {
|
|
3786
|
+
previousAt = (await getJson("/comfyui/workflows/export/last")).last?.at ?? null;
|
|
3787
|
+
} catch {}
|
|
3788
|
+
const now = /* @__PURE__ */ new Date();
|
|
3789
|
+
const p = (n, width = 2) => String(n).padStart(width, "0");
|
|
3790
|
+
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`;
|
|
3791
|
+
const form = document.createElement("form");
|
|
3792
|
+
form.method = "GET";
|
|
3793
|
+
form.action = `/comfyui/workflows/export/${filename}`;
|
|
3794
|
+
form.style.display = "none";
|
|
3795
|
+
for (const id of selected) {
|
|
3796
|
+
const input = document.createElement("input");
|
|
3797
|
+
input.type = "hidden";
|
|
3798
|
+
input.name = "ids";
|
|
3799
|
+
input.value = id;
|
|
3800
|
+
form.append(input);
|
|
3801
|
+
}
|
|
3802
|
+
document.body.append(form);
|
|
3803
|
+
form.submit();
|
|
3804
|
+
form.remove();
|
|
3805
|
+
let last = null;
|
|
3806
|
+
for (let attempt = 0; attempt < 10 && last === null; attempt++) {
|
|
3807
|
+
await new Promise((resolve) => window.setTimeout(resolve, 350));
|
|
3808
|
+
try {
|
|
3809
|
+
const payload = await getJson("/comfyui/workflows/export/last");
|
|
3810
|
+
if (payload.last !== null && payload.last.at !== previousAt) last = payload.last;
|
|
3811
|
+
} catch {}
|
|
3812
|
+
}
|
|
3813
|
+
const names = (last?.names ?? selectedNames()).join("、");
|
|
3814
|
+
let notice = t("transferExportDone", {
|
|
3815
|
+
n: last?.count ?? selected.size,
|
|
3816
|
+
names
|
|
3817
|
+
});
|
|
3818
|
+
if (last !== null) notice += " " + t("transferExportFile", {
|
|
3819
|
+
file: last.filename,
|
|
3820
|
+
size: Math.max(1, Math.round(last.size / 1024))
|
|
3821
|
+
});
|
|
3822
|
+
if (last !== null && last.warnings.length > 0) notice += " " + t("transferExportWarnings", { warnings: last.warnings.join(";") });
|
|
3823
|
+
props.onDone(notice);
|
|
3824
|
+
})();
|
|
3825
|
+
};
|
|
3826
|
+
/** Names behind the current selection, dialog order, with pack-bearing
|
|
3827
|
+
* entries annotated. Shown next to the submit button so "what will be
|
|
3828
|
+
* packaged — and will it carry skills" is readable at the decision point:
|
|
3829
|
+
* a selection that happens to exclude the one pack-bearing workflow looks
|
|
3830
|
+
* exactly like "skills export unreliably" from the zip alone. */
|
|
3831
|
+
const selectedNames = () => workflows.filter((workflow) => selected.has(workflow.id)).map((workflow) => workflow.skillDir !== void 0 ? `${workflow.name} (${t("skillBadge")})` : workflow.name);
|
|
3832
|
+
return (0, react.createElement)("div", {
|
|
3833
|
+
className: "dsc-picker-overlay",
|
|
3834
|
+
onClick: props.onClose
|
|
3835
|
+
}, (0, react.createElement)(DragGuard), (0, react.createElement)("div", {
|
|
3836
|
+
className: "dsc-transfer",
|
|
3837
|
+
onClick: (event) => event.stopPropagation(),
|
|
3838
|
+
onDragEnter: (event) => {
|
|
3839
|
+
event.preventDefault();
|
|
3840
|
+
event.stopPropagation();
|
|
3841
|
+
},
|
|
3842
|
+
onDragOver: (event) => {
|
|
3843
|
+
event.preventDefault();
|
|
3844
|
+
event.stopPropagation();
|
|
3845
|
+
},
|
|
3846
|
+
onDrop: (event) => {
|
|
3847
|
+
event.preventDefault();
|
|
3848
|
+
event.stopPropagation();
|
|
3849
|
+
}
|
|
3850
|
+
}, (0, react.createElement)("div", { className: "dsc-transfer-head" }, (0, react.createElement)("span", { className: "dsc-transfer-title" }, t("transferExportTitle")), (0, react.createElement)("button", {
|
|
3851
|
+
className: "dsc-panel-close",
|
|
3852
|
+
"aria-label": t("close"),
|
|
3853
|
+
onClick: props.onClose
|
|
3854
|
+
}, "✕")), 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", {
|
|
3855
|
+
total: workflows.length,
|
|
3856
|
+
selected: selected.size
|
|
3857
|
+
})), (0, react.createElement)("button", {
|
|
3858
|
+
className: "dsc-btn dsc-btn--sm",
|
|
3859
|
+
onClick: toggleAll
|
|
3860
|
+
}, allSelected ? t("transferSelectNone") : t("transferSelectAll"))), (0, react.createElement)("div", { className: "dsc-transfer-list" }, workflows.map((workflow) => (0, react.createElement)(TransferItem, {
|
|
3861
|
+
key: workflow.id,
|
|
3862
|
+
name: workflow.name,
|
|
3863
|
+
description: workflow.description,
|
|
3864
|
+
paramBadge: (workflow.parameters ?? []).length > 0 ? t("transferParamBadge", { n: (workflow.parameters ?? []).length }) : null,
|
|
3865
|
+
skillBadge: workflow.skillDir !== void 0 ? t("skillBadge") : null,
|
|
3866
|
+
warnings: [],
|
|
3867
|
+
checked: selected.has(workflow.id),
|
|
3868
|
+
onToggle: () => toggle(workflow.id)
|
|
3869
|
+
})))), selected.size > 0 ? (0, react.createElement)("div", { className: "dsc-transfer-note" }, (0, react.createElement)("span", {
|
|
3870
|
+
className: "dsc-hint",
|
|
3871
|
+
title: selectedNames().join("、")
|
|
3872
|
+
}, t("transferSelectedList", { names: selectedNames().join("、") }))) : null, (0, react.createElement)("div", { className: "dsc-transfer-actions" }, workflows.length === 0 ? null : (0, react.createElement)("button", {
|
|
3873
|
+
className: "dsc-btn dsc-btn--primary",
|
|
3874
|
+
disabled: busy || selected.size === 0,
|
|
3875
|
+
onClick: () => exportSelected()
|
|
3876
|
+
}, busy ? t("transferExporting") : t("transferExportRun", { n: selected.size })), (0, react.createElement)("button", {
|
|
3877
|
+
className: "dsc-btn",
|
|
3878
|
+
disabled: busy,
|
|
3879
|
+
onClick: props.onClose
|
|
3880
|
+
}, t("cancel")))));
|
|
3881
|
+
}
|
|
3882
|
+
/** Import dialog: choose a preset package (file picker or drag-drop), see
|
|
3883
|
+
* what it offers, then import the selection as NEW workflows. The file is
|
|
3884
|
+
* POSTed twice by design — analyze and apply parse the same bytes, so the
|
|
3885
|
+
* host needs no temp-file staging or cleanup. */
|
|
3886
|
+
function ImportDialog(props) {
|
|
3887
|
+
const { t } = props;
|
|
3888
|
+
const [file, setFile] = (0, react.useState)(null);
|
|
3889
|
+
const [analysis, setAnalysis] = (0, react.useState)(null);
|
|
3890
|
+
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3891
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3892
|
+
const [error, setError] = (0, react.useState)(null);
|
|
3893
|
+
const [results, setResults] = (0, react.useState)(null);
|
|
3894
|
+
const [imported, setImported] = (0, react.useState)(0);
|
|
3895
|
+
const [dragOver, setDragOver] = (0, react.useState)(false);
|
|
3896
|
+
/** Phase 1: upload the package for analysis (no server-side writes). */
|
|
3897
|
+
const analyze = async (picked) => {
|
|
3898
|
+
setBusy(true);
|
|
3899
|
+
setError(null);
|
|
3900
|
+
try {
|
|
3901
|
+
const data = await postRaw("/comfyui/workflows/import/analyze", await picked.arrayBuffer());
|
|
3902
|
+
if (data.ok !== true || data.analysis === void 0) throw new Error(data.error ?? "failed to analyze");
|
|
3903
|
+
setFile(picked);
|
|
3904
|
+
setAnalysis(data.analysis);
|
|
3905
|
+
setSelected(new Set(data.analysis.workflows.map((entry) => entry.index)));
|
|
3906
|
+
} catch (cause) {
|
|
3907
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
3908
|
+
} finally {
|
|
3909
|
+
setBusy(false);
|
|
3910
|
+
}
|
|
3911
|
+
};
|
|
3912
|
+
const toggle = (index) => {
|
|
3913
|
+
setSelected((prev) => {
|
|
3914
|
+
const next = new Set(prev);
|
|
3915
|
+
if (next.has(index)) next.delete(index);
|
|
3916
|
+
else next.add(index);
|
|
3917
|
+
return next;
|
|
3918
|
+
});
|
|
3919
|
+
};
|
|
3920
|
+
const allSelected = analysis !== null && analysis.workflows.length > 0 && selected.size === analysis.workflows.length;
|
|
3921
|
+
const toggleAll = () => {
|
|
3922
|
+
if (analysis === null) return;
|
|
3923
|
+
setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(analysis.workflows.map((entry) => entry.index)));
|
|
3924
|
+
};
|
|
3925
|
+
/** Phase 2: re-upload the same bytes with the selection as manifest indexes. */
|
|
3926
|
+
const importSelected = async () => {
|
|
3927
|
+
if (file === null) return;
|
|
3928
|
+
setBusy(true);
|
|
3929
|
+
setError(null);
|
|
3930
|
+
try {
|
|
3931
|
+
const bytes = await file.arrayBuffer();
|
|
3932
|
+
const data = await postRaw(`/comfyui/workflows/import/apply?select=${[...selected].join(",")}`, bytes);
|
|
3933
|
+
if (data.ok !== true || data.results === void 0) throw new Error(data.error ?? "failed to import");
|
|
3934
|
+
setResults(data.results);
|
|
3935
|
+
setImported(data.imported ?? 0);
|
|
3936
|
+
} catch (cause) {
|
|
3937
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
3938
|
+
} finally {
|
|
3939
|
+
setBusy(false);
|
|
3940
|
+
}
|
|
3941
|
+
};
|
|
3942
|
+
const finish = () => {
|
|
3943
|
+
if (imported > 0) props.onDone(t("transferImportDone", { n: imported }));
|
|
3944
|
+
else props.onClose();
|
|
3945
|
+
};
|
|
3946
|
+
return (0, react.createElement)("div", {
|
|
3947
|
+
className: "dsc-picker-overlay",
|
|
3948
|
+
onClick: props.onClose
|
|
3949
|
+
}, (0, react.createElement)(DragGuard), (0, react.createElement)("div", {
|
|
3950
|
+
className: "dsc-transfer",
|
|
3951
|
+
onClick: (event) => event.stopPropagation(),
|
|
3952
|
+
onDragEnter: (event) => {
|
|
3953
|
+
event.preventDefault();
|
|
3954
|
+
event.stopPropagation();
|
|
3955
|
+
},
|
|
3956
|
+
onDragOver: (event) => {
|
|
3957
|
+
event.preventDefault();
|
|
3958
|
+
event.stopPropagation();
|
|
3959
|
+
},
|
|
3960
|
+
onDrop: (event) => {
|
|
3961
|
+
event.preventDefault();
|
|
3962
|
+
event.stopPropagation();
|
|
3963
|
+
}
|
|
3964
|
+
}, (0, react.createElement)("div", { className: "dsc-transfer-head" }, (0, react.createElement)("span", { className: "dsc-transfer-title" }, t("transferImportTitle")), (0, react.createElement)("button", {
|
|
3965
|
+
className: "dsc-panel-close",
|
|
3966
|
+
"aria-label": t("close"),
|
|
3967
|
+
onClick: props.onClose
|
|
3968
|
+
}, "✕")), (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", {
|
|
3969
|
+
key: outcome.index,
|
|
3970
|
+
className: `dsc-transfer-item dsc-transfer-item--static${outcome.ok ? " dsc-transfer-item--ok" : " dsc-transfer-item--fail"}`
|
|
3971
|
+
}, (0, react.createElement)("span", {
|
|
3972
|
+
className: "dsc-transfer-item-name",
|
|
3973
|
+
title: outcome.name
|
|
3974
|
+
}, outcome.ok ? t("transferImportedItem", {
|
|
3975
|
+
src: outcome.name,
|
|
3976
|
+
dst: outcome.newName ?? ""
|
|
3977
|
+
}) : t("transferImportFailedItem", {
|
|
3978
|
+
src: outcome.name,
|
|
3979
|
+
reason: outcome.error ?? ""
|
|
3980
|
+
})), outcome.warnings.length > 0 ? (0, react.createElement)("span", {
|
|
3981
|
+
className: "dsc-transfer-item-desc",
|
|
3982
|
+
title: outcome.warnings.join(";")
|
|
3983
|
+
}, outcome.warnings.join(";")) : null))) : analysis === null ? (0, react.createElement)("label", {
|
|
3984
|
+
className: `dsc-transfer-drop${dragOver ? " dsc-transfer-drop--over" : ""}`,
|
|
3985
|
+
onDragOver: (event) => {
|
|
3986
|
+
event.preventDefault();
|
|
3987
|
+
setDragOver(true);
|
|
3988
|
+
},
|
|
3989
|
+
onDragLeave: () => setDragOver(false),
|
|
3990
|
+
onDrop: (event) => {
|
|
3991
|
+
event.preventDefault();
|
|
3992
|
+
setDragOver(false);
|
|
3993
|
+
const dropped = event.dataTransfer?.files?.[0];
|
|
3994
|
+
if (dropped !== void 0) analyze(dropped);
|
|
3995
|
+
}
|
|
3996
|
+
}, (0, react.createElement)("input", {
|
|
3997
|
+
type: "file",
|
|
3998
|
+
accept: ".zip,application/zip,application/x-zip-compressed",
|
|
3999
|
+
style: { display: "none" },
|
|
4000
|
+
onChange: (event) => {
|
|
4001
|
+
const picked = event.target.files?.[0];
|
|
4002
|
+
if (picked !== void 0) analyze(picked);
|
|
4003
|
+
event.target.value = "";
|
|
4004
|
+
}
|
|
4005
|
+
}), 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", {
|
|
4006
|
+
n: analysis.workflows.length,
|
|
4007
|
+
date: formatTs(analysis.exportedAt)
|
|
4008
|
+
})), (0, react.createElement)("button", {
|
|
4009
|
+
className: "dsc-btn dsc-btn--sm",
|
|
4010
|
+
onClick: toggleAll
|
|
4011
|
+
}, allSelected ? t("transferSelectNone") : t("transferSelectAll"))), (0, react.createElement)("div", { className: "dsc-transfer-list" }, analysis.workflows.map((entry) => (0, react.createElement)(TransferItem, {
|
|
4012
|
+
key: entry.index,
|
|
4013
|
+
name: entry.name,
|
|
4014
|
+
description: entry.description,
|
|
4015
|
+
paramBadge: entry.paramCount > 0 ? t("transferParamBadge", { n: entry.paramCount }) : null,
|
|
4016
|
+
skillBadge: entry.skill !== null ? entry.skill.required ? t("transferSkillRequired", { n: entry.skill.fileCount }) : t("transferSkillBadge", { n: entry.skill.fileCount }) : null,
|
|
4017
|
+
warnings: entry.warnings,
|
|
4018
|
+
checked: selected.has(entry.index),
|
|
4019
|
+
onToggle: () => toggle(entry.index)
|
|
4020
|
+
})))), error !== null ? (0, react.createElement)(ErrorNote, {
|
|
4021
|
+
t,
|
|
4022
|
+
message: error
|
|
4023
|
+
}) : 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", {
|
|
4024
|
+
className: "dsc-btn dsc-btn--primary",
|
|
4025
|
+
onClick: finish
|
|
4026
|
+
}, t("transferFinish")) : analysis !== null ? (0, react.createElement)("button", {
|
|
4027
|
+
className: "dsc-btn dsc-btn--primary",
|
|
4028
|
+
disabled: busy || selected.size === 0,
|
|
4029
|
+
onClick: () => {
|
|
4030
|
+
importSelected();
|
|
4031
|
+
}
|
|
4032
|
+
}, busy ? t("transferImporting") : t("transferImportRun", { n: selected.size })) : null, (0, react.createElement)("button", {
|
|
4033
|
+
className: "dsc-btn",
|
|
4034
|
+
disabled: busy,
|
|
4035
|
+
onClick: results !== null ? finish : props.onClose
|
|
4036
|
+
}, t("cancel")))));
|
|
4037
|
+
}
|
|
3467
4038
|
/** Read-only inspection of a ComfyUI-side saved workflow: node list + JSON. */
|
|
3468
4039
|
function WorkflowView(props) {
|
|
3469
4040
|
const nodes = Array.isArray(props.graph.nodes) ? props.graph.nodes.filter((node) => node.mode !== 4 && typeof node.type === "string" && node.type !== "") : [];
|
|
@@ -3990,6 +4561,40 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
|
|
|
3990
4561
|
.dsc-trigger:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-border-l1); }
|
|
3991
4562
|
.dsc-trigger[aria-pressed='true'] { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }
|
|
3992
4563
|
.dsc-trigger-glyph { font-size: 13px; line-height: 1; }
|
|
4564
|
+
/* --- connection reminder toast: fixed at the top of the page, mounted by the
|
|
4565
|
+
header trigger when the backend probe cannot reach ComfyUI. The probing
|
|
4566
|
+
state fades in after a short delay so a fast successful probe never paints;
|
|
4567
|
+
the fail state is sticky until dismissed or a later probe recovers. --- */
|
|
4568
|
+
.dsc-conn-toast {
|
|
4569
|
+
position: fixed; top: 14px; left: 50%;
|
|
4570
|
+
z-index: 980;
|
|
4571
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
4572
|
+
max-width: min(560px, 92vw);
|
|
4573
|
+
padding: 10px 12px;
|
|
4574
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
4575
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
4576
|
+
border-radius: 10px;
|
|
4577
|
+
box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3);
|
|
4578
|
+
color: var(--dsw-alias-label-primary);
|
|
4579
|
+
font-size: 12px; line-height: 1.5;
|
|
4580
|
+
animation: dsc-conn-toast-in 0.16s ease-out both;
|
|
4581
|
+
}
|
|
4582
|
+
.dsc-conn-toast--probing { animation-delay: 0.45s; }
|
|
4583
|
+
.dsc-conn-toast--ok { border-color: var(--dsw-alias-state-success-primary); }
|
|
4584
|
+
.dsc-conn-toast--fail { border-color: var(--dsw-alias-state-error-primary); }
|
|
4585
|
+
.dsc-conn-toast-dot { flex: none; width: 8px; height: 8px; margin-top: 5px; border-radius: 50%; }
|
|
4586
|
+
.dsc-conn-toast--probing .dsc-conn-toast-dot { background: var(--dsw-alias-state-warn-primary); animation: dsc-conn-dot 1s ease-in-out infinite; }
|
|
4587
|
+
.dsc-conn-toast--ok .dsc-conn-toast-dot { background: var(--dsw-alias-state-success-primary); }
|
|
4588
|
+
.dsc-conn-toast--fail .dsc-conn-toast-dot { background: var(--dsw-alias-state-error-primary); }
|
|
4589
|
+
.dsc-conn-toast-body { min-width: 0; }
|
|
4590
|
+
.dsc-conn-toast-title { font-weight: 600; }
|
|
4591
|
+
.dsc-conn-toast--fail .dsc-conn-toast-title { color: var(--dsw-alias-state-error-primary); }
|
|
4592
|
+
.dsc-conn-toast--ok .dsc-conn-toast-title { color: var(--dsw-alias-state-success-primary); }
|
|
4593
|
+
.dsc-conn-toast-text { color: var(--dsw-alias-label-secondary); margin-top: 2px; overflow-wrap: anywhere; }
|
|
4594
|
+
.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; }
|
|
4595
|
+
.dsc-conn-toast-close:hover { color: var(--dsw-alias-label-primary); }
|
|
4596
|
+
@keyframes dsc-conn-toast-in { from { opacity: 0; transform: translate(-50%, -6px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
|
4597
|
+
@keyframes dsc-conn-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
3993
4598
|
/* --- panel shell: floating window, percentage-anchored to the page header
|
|
3994
4599
|
(top) and the composer (bottom), draggable by the header and resizable via
|
|
3995
4600
|
the corner handle. Follows the host light/dark theme. --- */
|
|
@@ -4295,6 +4900,28 @@ video.dsc-picker-player { max-height: 180px; }
|
|
|
4295
4900
|
.dsc-param-random { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
4296
4901
|
.dsc-param-bool { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dsw-alias-label-primary); cursor: pointer; }
|
|
4297
4902
|
.dsc-param-advanced { display: flex; flex-direction: column; gap: 8px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 8px; padding: 8px; }
|
|
4903
|
+
/* --- workflow preset transfer dialogs (export / import) --- */
|
|
4904
|
+
.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; }
|
|
4905
|
+
.dsc-transfer-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
4906
|
+
.dsc-transfer-title { font-weight: 600; font-size: 13px; color: var(--dsw-alias-label-primary); }
|
|
4907
|
+
.dsc-transfer-body { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; overflow-y: auto; min-height: 0; }
|
|
4908
|
+
.dsc-transfer-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
4909
|
+
.dsc-transfer-list { display: flex; flex-direction: column; gap: 4px; }
|
|
4910
|
+
.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); }
|
|
4911
|
+
.dsc-transfer-item:hover { border-color: var(--dsw-alias-brand-primary); }
|
|
4912
|
+
.dsc-transfer-item--static, .dsc-transfer-item--static:hover { cursor: default; border-color: var(--dsw-alias-border-l1); }
|
|
4913
|
+
.dsc-transfer-item--ok { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 45%, transparent); }
|
|
4914
|
+
.dsc-transfer-item--fail { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); }
|
|
4915
|
+
.dsc-transfer-item-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 240px; }
|
|
4916
|
+
.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; }
|
|
4917
|
+
.dsc-transfer-actions { display: flex; gap: 8px; justify-content: flex-end; padding: 10px 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
4918
|
+
.dsc-transfer-note { padding: 6px 12px 0; }
|
|
4919
|
+
.dsc-transfer-note .dsc-hint { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
4920
|
+
.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; }
|
|
4921
|
+
.dsc-transfer-drop:hover, .dsc-transfer-drop--over { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
4922
|
+
.dsc-transfer-empty { padding: 40px 12px; text-align: center; color: var(--dsw-alias-label-tertiary); font-size: 13px; }
|
|
4923
|
+
.dsc-btn--primary { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
4924
|
+
.dsc-btn--primary:disabled { opacity: 0.5; cursor: default; }
|
|
4298
4925
|
`;
|
|
4299
4926
|
/** Inject the stylesheet once (idempotent), owned by this plugin for HMR. */
|
|
4300
4927
|
function injectStyles() {
|