@genfeedai/workflow-ui 0.1.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/dist/canvas.d.mts +27 -0
- package/dist/canvas.d.ts +27 -0
- package/dist/canvas.js +45 -0
- package/dist/canvas.mjs +16 -0
- package/dist/chunk-22PDGHNQ.mjs +737 -0
- package/dist/chunk-3SPPKCWR.js +458 -0
- package/dist/chunk-3YFFDHC5.js +300 -0
- package/dist/chunk-5HJFQVUR.js +61 -0
- package/dist/chunk-5LQ4QBR5.js +2 -0
- package/dist/chunk-6DOEUDD5.js +254 -0
- package/dist/chunk-7SKSRSS7.mjs +57 -0
- package/dist/chunk-AC6TWLRT.mjs +27 -0
- package/dist/chunk-ADWNF7V3.js +120 -0
- package/dist/chunk-BJ3R5R32.mjs +2163 -0
- package/dist/chunk-CETJJ73S.js +1555 -0
- package/dist/chunk-CSUBLSKZ.mjs +1002 -0
- package/dist/chunk-CV4M7CNU.mjs +251 -0
- package/dist/chunk-E323WAZG.mjs +272 -0
- package/dist/chunk-E544XUBL.js +378 -0
- package/dist/chunk-EC2ZIWOK.js +1007 -0
- package/dist/chunk-EFXQT23N.mjs +99 -0
- package/dist/chunk-EMUMKW5C.js +107 -0
- package/dist/chunk-FOMOOERN.js +2 -0
- package/dist/chunk-FT33LFII.mjs +21 -0
- package/dist/chunk-FT64PCUP.mjs +533 -0
- package/dist/chunk-H6LZKSLY.js +5678 -0
- package/dist/chunk-HPQT36RR.js +543 -0
- package/dist/chunk-JLWKW3G5.js +2 -0
- package/dist/chunk-L5TF4EHW.mjs +1 -0
- package/dist/chunk-LAJ34AH2.mjs +374 -0
- package/dist/chunk-LDN7IX4Y.mjs +1 -0
- package/dist/chunk-MLJJBBTB.mjs +1 -0
- package/dist/chunk-NSDLGLAQ.js +2166 -0
- package/dist/chunk-RJ262NXS.js +24 -0
- package/dist/chunk-RXNEDWK2.js +141 -0
- package/dist/chunk-SW7QNEZU.js +744 -0
- package/dist/chunk-UQQUWGHW.mjs +118 -0
- package/dist/chunk-VOGL2WCE.mjs +1542 -0
- package/dist/chunk-VRN3UWE5.mjs +138 -0
- package/dist/chunk-XV5Z5XYR.mjs +5640 -0
- package/dist/chunk-Z7PWFZG5.js +30 -0
- package/dist/chunk-ZJD5WMR3.mjs +418 -0
- package/dist/hooks.d.mts +255 -0
- package/dist/hooks.d.ts +255 -0
- package/dist/hooks.js +56 -0
- package/dist/hooks.mjs +11 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +180 -0
- package/dist/index.mjs +19 -0
- package/dist/lib.d.mts +164 -0
- package/dist/lib.d.ts +164 -0
- package/dist/lib.js +144 -0
- package/dist/lib.mjs +3 -0
- package/dist/nodes.d.mts +128 -0
- package/dist/nodes.d.ts +128 -0
- package/dist/nodes.js +151 -0
- package/dist/nodes.mjs +14 -0
- package/dist/panels.d.mts +22 -0
- package/dist/panels.d.ts +22 -0
- package/dist/panels.js +21 -0
- package/dist/panels.mjs +4 -0
- package/dist/promptLibraryStore-BZnfmEkc.d.ts +464 -0
- package/dist/promptLibraryStore-zqb59nsu.d.mts +464 -0
- package/dist/provider.d.mts +29 -0
- package/dist/provider.d.ts +29 -0
- package/dist/provider.js +17 -0
- package/dist/provider.mjs +4 -0
- package/dist/stores.d.mts +96 -0
- package/dist/stores.d.ts +96 -0
- package/dist/stores.js +113 -0
- package/dist/stores.mjs +43 -0
- package/dist/toolbar.d.mts +73 -0
- package/dist/toolbar.d.ts +73 -0
- package/dist/toolbar.js +34 -0
- package/dist/toolbar.mjs +5 -0
- package/dist/types-ipAnBzAJ.d.mts +46 -0
- package/dist/types-ipAnBzAJ.d.ts +46 -0
- package/dist/ui.d.mts +67 -0
- package/dist/ui.d.ts +67 -0
- package/dist/ui.js +84 -0
- package/dist/ui.mjs +3 -0
- package/dist/workflowStore-4EGKJLYK.mjs +3 -0
- package/dist/workflowStore-KM32FDL7.js +12 -0
- package/package.json +117 -0
- package/src/styles/workflow-ui.css +186 -0
|
@@ -0,0 +1,1542 @@
|
|
|
1
|
+
import { nodeTypes } from './chunk-XV5Z5XYR.mjs';
|
|
2
|
+
import { getMediaFromNode } from './chunk-E323WAZG.mjs';
|
|
3
|
+
import { useCanvasKeyboardShortcuts } from './chunk-UQQUWGHW.mjs';
|
|
4
|
+
import { Button } from './chunk-7SKSRSS7.mjs';
|
|
5
|
+
import { usePromptEditorStore } from './chunk-CV4M7CNU.mjs';
|
|
6
|
+
import { useUIStore, useSettingsStore, useExecutionStore } from './chunk-CSUBLSKZ.mjs';
|
|
7
|
+
import { require_dist, useWorkflowStore } from './chunk-BJ3R5R32.mjs';
|
|
8
|
+
import { __toESM } from './chunk-AC6TWLRT.mjs';
|
|
9
|
+
import { useNodes, ViewportPortal, useViewport, useStore, getBezierPath, BaseEdge, useReactFlow, ReactFlow, SelectionMode, ConnectionMode, Background, BackgroundVariant, Controls, MiniMap } from '@xyflow/react';
|
|
10
|
+
import { Pause, Palette, Lock, Unlock, Trash2, Search, X, Keyboard, PanelLeft, Download, ChevronLeft, ChevronRight, ZoomOut, ZoomIn } from 'lucide-react';
|
|
11
|
+
import { memo, useMemo, useState, useCallback, useEffect, useRef } from 'react';
|
|
12
|
+
import '@xyflow/react/dist/style.css';
|
|
13
|
+
import { clsx } from 'clsx';
|
|
14
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
15
|
+
import Image from 'next/image';
|
|
16
|
+
|
|
17
|
+
// src/canvas/WorkflowCanvas.tsx
|
|
18
|
+
var import_types3 = __toESM(require_dist());
|
|
19
|
+
|
|
20
|
+
// src/types/groups.ts
|
|
21
|
+
var GROUP_COLORS = {
|
|
22
|
+
purple: {
|
|
23
|
+
bg: "bg-purple-500/10",
|
|
24
|
+
border: "border-purple-500/30",
|
|
25
|
+
text: "text-purple-400"
|
|
26
|
+
},
|
|
27
|
+
blue: {
|
|
28
|
+
bg: "bg-blue-500/10",
|
|
29
|
+
border: "border-blue-500/30",
|
|
30
|
+
text: "text-blue-400"
|
|
31
|
+
},
|
|
32
|
+
green: {
|
|
33
|
+
bg: "bg-green-500/10",
|
|
34
|
+
border: "border-green-500/30",
|
|
35
|
+
text: "text-green-400"
|
|
36
|
+
},
|
|
37
|
+
yellow: {
|
|
38
|
+
bg: "bg-yellow-500/10",
|
|
39
|
+
border: "border-yellow-500/30",
|
|
40
|
+
text: "text-yellow-400"
|
|
41
|
+
},
|
|
42
|
+
orange: {
|
|
43
|
+
bg: "bg-orange-500/10",
|
|
44
|
+
border: "border-orange-500/30",
|
|
45
|
+
text: "text-orange-400"
|
|
46
|
+
},
|
|
47
|
+
red: {
|
|
48
|
+
bg: "bg-red-500/10",
|
|
49
|
+
border: "border-red-500/30",
|
|
50
|
+
text: "text-red-400"
|
|
51
|
+
},
|
|
52
|
+
pink: {
|
|
53
|
+
bg: "bg-pink-500/10",
|
|
54
|
+
border: "border-pink-500/30",
|
|
55
|
+
text: "text-pink-400"
|
|
56
|
+
},
|
|
57
|
+
gray: {
|
|
58
|
+
bg: "bg-gray-500/10",
|
|
59
|
+
border: "border-gray-500/30",
|
|
60
|
+
text: "text-gray-400"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var DEFAULT_GROUP_COLORS = [
|
|
64
|
+
"purple",
|
|
65
|
+
"blue",
|
|
66
|
+
"green",
|
|
67
|
+
"yellow",
|
|
68
|
+
"orange",
|
|
69
|
+
"red",
|
|
70
|
+
"pink",
|
|
71
|
+
"gray"
|
|
72
|
+
];
|
|
73
|
+
var HEADER_HEIGHT = 32;
|
|
74
|
+
function calculateGroupBounds(nodeIds, nodeMap) {
|
|
75
|
+
if (nodeIds.length === 0) return null;
|
|
76
|
+
let minX = Infinity;
|
|
77
|
+
let minY = Infinity;
|
|
78
|
+
let maxX = -Infinity;
|
|
79
|
+
let maxY = -Infinity;
|
|
80
|
+
let foundAny = false;
|
|
81
|
+
for (const nodeId of nodeIds) {
|
|
82
|
+
const node = nodeMap.get(nodeId);
|
|
83
|
+
if (!node) continue;
|
|
84
|
+
foundAny = true;
|
|
85
|
+
const width = node.measured?.width ?? 200;
|
|
86
|
+
const height = node.measured?.height ?? 100;
|
|
87
|
+
minX = Math.min(minX, node.position.x);
|
|
88
|
+
minY = Math.min(minY, node.position.y);
|
|
89
|
+
maxX = Math.max(maxX, node.position.x + width);
|
|
90
|
+
maxY = Math.max(maxY, node.position.y + height);
|
|
91
|
+
}
|
|
92
|
+
if (!foundAny) return null;
|
|
93
|
+
const padding = 24;
|
|
94
|
+
return {
|
|
95
|
+
x: minX - padding,
|
|
96
|
+
y: minY - padding - HEADER_HEIGHT,
|
|
97
|
+
width: maxX - minX + padding * 2,
|
|
98
|
+
height: maxY - minY + padding * 2 + HEADER_HEIGHT
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function GroupBackground({ group, bounds }) {
|
|
102
|
+
const colors = GROUP_COLORS[group.color ?? "purple"];
|
|
103
|
+
return /* @__PURE__ */ jsx(
|
|
104
|
+
"div",
|
|
105
|
+
{
|
|
106
|
+
className: clsx(
|
|
107
|
+
"absolute rounded-lg border-2 border-dashed",
|
|
108
|
+
colors.bg,
|
|
109
|
+
colors.border,
|
|
110
|
+
group.isLocked && "opacity-60"
|
|
111
|
+
),
|
|
112
|
+
style: {
|
|
113
|
+
left: bounds.x,
|
|
114
|
+
top: bounds.y,
|
|
115
|
+
width: bounds.width,
|
|
116
|
+
height: bounds.height
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
function GroupControls({ group, bounds, nodeMap, zoom }) {
|
|
122
|
+
const { setNodes } = useReactFlow();
|
|
123
|
+
const { toggleGroupLock, deleteGroup, setGroupColor, setDirty, renameGroup } = useWorkflowStore();
|
|
124
|
+
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
125
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
126
|
+
const [editName, setEditName] = useState(group.name);
|
|
127
|
+
const inputRef = useRef(null);
|
|
128
|
+
const colorPickerRef = useRef(null);
|
|
129
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
130
|
+
const dragStartRef = useRef(null);
|
|
131
|
+
const nodeStartPositionsRef = useRef(/* @__PURE__ */ new Map());
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!isEditing) {
|
|
134
|
+
setEditName(group.name);
|
|
135
|
+
}
|
|
136
|
+
}, [group.name, isEditing]);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (isEditing && inputRef.current) {
|
|
139
|
+
inputRef.current.focus();
|
|
140
|
+
inputRef.current.select();
|
|
141
|
+
}
|
|
142
|
+
}, [isEditing]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const handleClickOutside = (e) => {
|
|
145
|
+
if (colorPickerRef.current && !colorPickerRef.current.contains(e.target)) {
|
|
146
|
+
setShowColorPicker(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
if (showColorPicker) {
|
|
150
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
151
|
+
}
|
|
152
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
153
|
+
}, [showColorPicker]);
|
|
154
|
+
const handleNameSubmit = useCallback(() => {
|
|
155
|
+
if (editName.trim() && editName !== group.name) {
|
|
156
|
+
renameGroup(group.id, editName.trim());
|
|
157
|
+
} else {
|
|
158
|
+
setEditName(group.name);
|
|
159
|
+
}
|
|
160
|
+
setIsEditing(false);
|
|
161
|
+
}, [editName, group.name, group.id, renameGroup]);
|
|
162
|
+
const handleKeyDown = useCallback(
|
|
163
|
+
(e) => {
|
|
164
|
+
if (e.key === "Enter") {
|
|
165
|
+
handleNameSubmit();
|
|
166
|
+
} else if (e.key === "Escape") {
|
|
167
|
+
setEditName(group.name);
|
|
168
|
+
setIsEditing(false);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
[handleNameSubmit, group.name]
|
|
172
|
+
);
|
|
173
|
+
const handleMouseDown = useCallback(
|
|
174
|
+
(e) => {
|
|
175
|
+
if (group.isLocked) return;
|
|
176
|
+
if (e.target.closest("button") || e.target.closest("input")) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
e.stopPropagation();
|
|
181
|
+
setIsDragging(true);
|
|
182
|
+
dragStartRef.current = { x: e.clientX, y: e.clientY };
|
|
183
|
+
const positions = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const nodeId of group.nodeIds) {
|
|
185
|
+
const node = nodeMap.get(nodeId);
|
|
186
|
+
if (node) {
|
|
187
|
+
positions.set(nodeId, { x: node.position.x, y: node.position.y });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
nodeStartPositionsRef.current = positions;
|
|
191
|
+
},
|
|
192
|
+
[group.isLocked, group.nodeIds, nodeMap]
|
|
193
|
+
);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!isDragging) return;
|
|
196
|
+
const handleMouseMove = (e) => {
|
|
197
|
+
if (!dragStartRef.current) return;
|
|
198
|
+
const deltaX = (e.clientX - dragStartRef.current.x) / zoom;
|
|
199
|
+
const deltaY = (e.clientY - dragStartRef.current.y) / zoom;
|
|
200
|
+
setNodes(
|
|
201
|
+
(currentNodes) => currentNodes.map((node) => {
|
|
202
|
+
const startPos = nodeStartPositionsRef.current.get(node.id);
|
|
203
|
+
if (!startPos) return node;
|
|
204
|
+
return {
|
|
205
|
+
...node,
|
|
206
|
+
position: {
|
|
207
|
+
x: startPos.x + deltaX,
|
|
208
|
+
y: startPos.y + deltaY
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
const handleMouseUp = () => {
|
|
215
|
+
setDirty(true);
|
|
216
|
+
setIsDragging(false);
|
|
217
|
+
dragStartRef.current = null;
|
|
218
|
+
};
|
|
219
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
220
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
221
|
+
return () => {
|
|
222
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
223
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
224
|
+
};
|
|
225
|
+
}, [isDragging, zoom, setNodes, setDirty]);
|
|
226
|
+
const colors = GROUP_COLORS[group.color ?? "purple"];
|
|
227
|
+
const handleColorSelect = (color) => {
|
|
228
|
+
setGroupColor(group.id, color);
|
|
229
|
+
setShowColorPicker(false);
|
|
230
|
+
};
|
|
231
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
232
|
+
/* @__PURE__ */ jsxs(
|
|
233
|
+
"div",
|
|
234
|
+
{
|
|
235
|
+
onMouseDown: handleMouseDown,
|
|
236
|
+
className: clsx(
|
|
237
|
+
"absolute flex items-center justify-between px-3 rounded-t-lg select-none pointer-events-auto",
|
|
238
|
+
colors.bg,
|
|
239
|
+
colors.border,
|
|
240
|
+
"border-2 border-b-0 border-dashed",
|
|
241
|
+
!group.isLocked && "cursor-grab",
|
|
242
|
+
isDragging && "cursor-grabbing",
|
|
243
|
+
group.isLocked && "opacity-60"
|
|
244
|
+
),
|
|
245
|
+
style: {
|
|
246
|
+
left: bounds.x,
|
|
247
|
+
top: bounds.y,
|
|
248
|
+
width: bounds.width,
|
|
249
|
+
height: HEADER_HEIGHT
|
|
250
|
+
},
|
|
251
|
+
children: [
|
|
252
|
+
isEditing ? /* @__PURE__ */ jsx(
|
|
253
|
+
"input",
|
|
254
|
+
{
|
|
255
|
+
ref: inputRef,
|
|
256
|
+
type: "text",
|
|
257
|
+
value: editName,
|
|
258
|
+
onChange: (e) => setEditName(e.target.value),
|
|
259
|
+
onBlur: handleNameSubmit,
|
|
260
|
+
onKeyDown: handleKeyDown,
|
|
261
|
+
className: clsx(
|
|
262
|
+
"flex-1 bg-transparent border-none outline-none text-sm font-medium px-0 py-0",
|
|
263
|
+
colors.text
|
|
264
|
+
),
|
|
265
|
+
style: { minWidth: 0 }
|
|
266
|
+
}
|
|
267
|
+
) : /* @__PURE__ */ jsx(
|
|
268
|
+
"span",
|
|
269
|
+
{
|
|
270
|
+
className: clsx("font-medium truncate cursor-text", colors.text),
|
|
271
|
+
style: { fontSize: 14 },
|
|
272
|
+
onClick: () => setIsEditing(true),
|
|
273
|
+
children: group.name
|
|
274
|
+
}
|
|
275
|
+
),
|
|
276
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", ref: colorPickerRef, children: [
|
|
278
|
+
/* @__PURE__ */ jsx(
|
|
279
|
+
"button",
|
|
280
|
+
{
|
|
281
|
+
onClick: (e) => {
|
|
282
|
+
e.stopPropagation();
|
|
283
|
+
setShowColorPicker(!showColorPicker);
|
|
284
|
+
},
|
|
285
|
+
className: clsx("p-1 rounded hover:bg-white/10 transition-colors", colors.text),
|
|
286
|
+
title: "Change group color",
|
|
287
|
+
children: /* @__PURE__ */ jsx(Palette, { className: "w-4 h-4" })
|
|
288
|
+
}
|
|
289
|
+
),
|
|
290
|
+
showColorPicker && /* @__PURE__ */ jsx("div", { className: "absolute top-8 right-0 z-50 bg-card border border-border rounded-lg shadow-lg p-2 flex gap-1 flex-wrap w-[120px]", children: DEFAULT_GROUP_COLORS.map((color) => /* @__PURE__ */ jsx(
|
|
291
|
+
"button",
|
|
292
|
+
{
|
|
293
|
+
onClick: (e) => {
|
|
294
|
+
e.stopPropagation();
|
|
295
|
+
handleColorSelect(color);
|
|
296
|
+
},
|
|
297
|
+
className: clsx(
|
|
298
|
+
"w-6 h-6 rounded-md border-2 transition-transform hover:scale-110",
|
|
299
|
+
GROUP_COLORS[color].bg,
|
|
300
|
+
color === group.color ? "border-white" : "border-transparent"
|
|
301
|
+
),
|
|
302
|
+
title: color
|
|
303
|
+
},
|
|
304
|
+
color
|
|
305
|
+
)) })
|
|
306
|
+
] }),
|
|
307
|
+
/* @__PURE__ */ jsx(
|
|
308
|
+
"button",
|
|
309
|
+
{
|
|
310
|
+
onClick: (e) => {
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
toggleGroupLock(group.id);
|
|
313
|
+
},
|
|
314
|
+
className: clsx("p-1 rounded hover:bg-white/10 transition-colors", colors.text),
|
|
315
|
+
title: group.isLocked ? "Unlock group" : "Lock group",
|
|
316
|
+
children: group.isLocked ? /* @__PURE__ */ jsx(Lock, { className: "w-4 h-4" }) : /* @__PURE__ */ jsx(Unlock, { className: "w-4 h-4" })
|
|
317
|
+
}
|
|
318
|
+
),
|
|
319
|
+
/* @__PURE__ */ jsx(
|
|
320
|
+
"button",
|
|
321
|
+
{
|
|
322
|
+
onClick: (e) => {
|
|
323
|
+
e.stopPropagation();
|
|
324
|
+
deleteGroup(group.id);
|
|
325
|
+
},
|
|
326
|
+
className: clsx("p-1 rounded hover:bg-white/10 transition-colors", colors.text),
|
|
327
|
+
title: "Delete group",
|
|
328
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "w-4 h-4" })
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
] })
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
),
|
|
335
|
+
group.isLocked && /* @__PURE__ */ jsx(
|
|
336
|
+
"div",
|
|
337
|
+
{
|
|
338
|
+
className: clsx(
|
|
339
|
+
"absolute px-2 py-0.5 rounded text-xs font-medium pointer-events-none",
|
|
340
|
+
colors.bg,
|
|
341
|
+
colors.text
|
|
342
|
+
),
|
|
343
|
+
style: {
|
|
344
|
+
left: bounds.x + 12,
|
|
345
|
+
top: bounds.y + HEADER_HEIGHT + 8
|
|
346
|
+
},
|
|
347
|
+
children: "LOCKED"
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
] });
|
|
351
|
+
}
|
|
352
|
+
function GroupBackgroundsPortalComponent() {
|
|
353
|
+
const { groups } = useWorkflowStore();
|
|
354
|
+
const nodes = useNodes();
|
|
355
|
+
const nodeMap = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
|
356
|
+
const groupBounds = useMemo(() => {
|
|
357
|
+
const result = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const group of groups) {
|
|
359
|
+
const bounds = calculateGroupBounds(group.nodeIds, nodeMap);
|
|
360
|
+
if (bounds) {
|
|
361
|
+
result.set(group.id, bounds);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
365
|
+
}, [groups, nodeMap]);
|
|
366
|
+
if (groups.length === 0) return null;
|
|
367
|
+
return /* @__PURE__ */ jsx(ViewportPortal, { children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: 0, left: 0, zIndex: -1, pointerEvents: "none" }, children: groups.map((group) => {
|
|
368
|
+
const bounds = groupBounds.get(group.id);
|
|
369
|
+
if (!bounds) return null;
|
|
370
|
+
return /* @__PURE__ */ jsx(GroupBackground, { group, bounds }, group.id);
|
|
371
|
+
}) }) });
|
|
372
|
+
}
|
|
373
|
+
function GroupControlsOverlayComponent() {
|
|
374
|
+
const { groups } = useWorkflowStore();
|
|
375
|
+
const nodes = useNodes();
|
|
376
|
+
const { zoom } = useViewport();
|
|
377
|
+
const nodeMap = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
|
378
|
+
const groupBounds = useMemo(() => {
|
|
379
|
+
const result = /* @__PURE__ */ new Map();
|
|
380
|
+
for (const group of groups) {
|
|
381
|
+
const bounds = calculateGroupBounds(group.nodeIds, nodeMap);
|
|
382
|
+
if (bounds) {
|
|
383
|
+
result.set(group.id, bounds);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}, [groups, nodeMap]);
|
|
388
|
+
if (groups.length === 0) return null;
|
|
389
|
+
return /* @__PURE__ */ jsx(ViewportPortal, { children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: 0, left: 0, zIndex: 1e3, pointerEvents: "none" }, children: groups.map((group) => {
|
|
390
|
+
const bounds = groupBounds.get(group.id);
|
|
391
|
+
if (!bounds) return null;
|
|
392
|
+
return /* @__PURE__ */ jsx(
|
|
393
|
+
GroupControls,
|
|
394
|
+
{
|
|
395
|
+
group,
|
|
396
|
+
bounds,
|
|
397
|
+
nodeMap,
|
|
398
|
+
zoom
|
|
399
|
+
},
|
|
400
|
+
group.id
|
|
401
|
+
);
|
|
402
|
+
}) }) });
|
|
403
|
+
}
|
|
404
|
+
var GroupBackgroundsPortal = memo(GroupBackgroundsPortalComponent);
|
|
405
|
+
var GroupControlsOverlay = memo(GroupControlsOverlayComponent);
|
|
406
|
+
function GroupOverlayComponent() {
|
|
407
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
408
|
+
/* @__PURE__ */ jsx(GroupBackgroundsPortal, {}),
|
|
409
|
+
/* @__PURE__ */ jsx(GroupControlsOverlay, {})
|
|
410
|
+
] });
|
|
411
|
+
}
|
|
412
|
+
var GroupOverlay = memo(GroupOverlayComponent);
|
|
413
|
+
var SNAP_THRESHOLD = 5;
|
|
414
|
+
function HelperLinesComponent({ draggingNodeId }) {
|
|
415
|
+
const [lines, setLines] = useState([]);
|
|
416
|
+
const nodes = useWorkflowStore((state) => state.nodes);
|
|
417
|
+
const transform = useStore((state) => state.transform);
|
|
418
|
+
const calculateHelperLines = useCallback(
|
|
419
|
+
(draggingId) => {
|
|
420
|
+
const draggingNode = nodes.find((n) => n.id === draggingId);
|
|
421
|
+
if (!draggingNode) {
|
|
422
|
+
setLines([]);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const otherNodes = nodes.filter((n) => n.id !== draggingId && !n.selected);
|
|
426
|
+
if (otherNodes.length === 0) {
|
|
427
|
+
setLines([]);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const dragWidth = draggingNode.measured?.width ?? draggingNode.width ?? 220;
|
|
431
|
+
const dragHeight = draggingNode.measured?.height ?? draggingNode.height ?? 100;
|
|
432
|
+
const dragLeft = draggingNode.position.x;
|
|
433
|
+
const dragRight = dragLeft + dragWidth;
|
|
434
|
+
const dragTop = draggingNode.position.y;
|
|
435
|
+
const dragBottom = dragTop + dragHeight;
|
|
436
|
+
const dragCenterX = dragLeft + dragWidth / 2;
|
|
437
|
+
const dragCenterY = dragTop + dragHeight / 2;
|
|
438
|
+
const newLines = [];
|
|
439
|
+
for (const node of otherNodes) {
|
|
440
|
+
const nodeWidth = node.measured?.width ?? node.width ?? 220;
|
|
441
|
+
const nodeHeight = node.measured?.height ?? node.height ?? 100;
|
|
442
|
+
const nodeLeft = node.position.x;
|
|
443
|
+
const nodeRight = nodeLeft + nodeWidth;
|
|
444
|
+
const nodeTop = node.position.y;
|
|
445
|
+
const nodeBottom = nodeTop + nodeHeight;
|
|
446
|
+
const nodeCenterX = nodeLeft + nodeWidth / 2;
|
|
447
|
+
const nodeCenterY = nodeTop + nodeHeight / 2;
|
|
448
|
+
const verticalChecks = [
|
|
449
|
+
{ dragPos: dragLeft, nodePos: nodeLeft, label: "left-left" },
|
|
450
|
+
{ dragPos: dragLeft, nodePos: nodeRight, label: "left-right" },
|
|
451
|
+
{ dragPos: dragRight, nodePos: nodeLeft, label: "right-left" },
|
|
452
|
+
{ dragPos: dragRight, nodePos: nodeRight, label: "right-right" },
|
|
453
|
+
{ dragPos: dragCenterX, nodePos: nodeCenterX, label: "center-center-x" }
|
|
454
|
+
];
|
|
455
|
+
for (const check of verticalChecks) {
|
|
456
|
+
if (Math.abs(check.dragPos - check.nodePos) <= SNAP_THRESHOLD) {
|
|
457
|
+
newLines.push({
|
|
458
|
+
type: "vertical",
|
|
459
|
+
position: check.nodePos,
|
|
460
|
+
start: Math.min(dragTop, nodeTop) - 20,
|
|
461
|
+
end: Math.max(dragBottom, nodeBottom) + 20
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const horizontalChecks = [
|
|
466
|
+
{ dragPos: dragTop, nodePos: nodeTop, label: "top-top" },
|
|
467
|
+
{ dragPos: dragTop, nodePos: nodeBottom, label: "top-bottom" },
|
|
468
|
+
{ dragPos: dragBottom, nodePos: nodeTop, label: "bottom-top" },
|
|
469
|
+
{ dragPos: dragBottom, nodePos: nodeBottom, label: "bottom-bottom" },
|
|
470
|
+
{ dragPos: dragCenterY, nodePos: nodeCenterY, label: "center-center-y" }
|
|
471
|
+
];
|
|
472
|
+
for (const check of horizontalChecks) {
|
|
473
|
+
if (Math.abs(check.dragPos - check.nodePos) <= SNAP_THRESHOLD) {
|
|
474
|
+
newLines.push({
|
|
475
|
+
type: "horizontal",
|
|
476
|
+
position: check.nodePos,
|
|
477
|
+
start: Math.min(dragLeft, nodeLeft) - 20,
|
|
478
|
+
end: Math.max(dragRight, nodeRight) + 20
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const uniqueLines = [];
|
|
484
|
+
const seenPositions = /* @__PURE__ */ new Set();
|
|
485
|
+
for (const line of newLines) {
|
|
486
|
+
const key = `${line.type}-${Math.round(line.position)}`;
|
|
487
|
+
if (!seenPositions.has(key)) {
|
|
488
|
+
seenPositions.add(key);
|
|
489
|
+
const existingLine = uniqueLines.find(
|
|
490
|
+
(l) => l.type === line.type && Math.abs(l.position - line.position) < 1
|
|
491
|
+
);
|
|
492
|
+
if (existingLine) {
|
|
493
|
+
existingLine.start = Math.min(existingLine.start, line.start);
|
|
494
|
+
existingLine.end = Math.max(existingLine.end, line.end);
|
|
495
|
+
} else {
|
|
496
|
+
uniqueLines.push(line);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
setLines(uniqueLines);
|
|
501
|
+
},
|
|
502
|
+
[nodes]
|
|
503
|
+
);
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
if (draggingNodeId) {
|
|
506
|
+
calculateHelperLines(draggingNodeId);
|
|
507
|
+
} else {
|
|
508
|
+
setLines([]);
|
|
509
|
+
}
|
|
510
|
+
}, [draggingNodeId, calculateHelperLines]);
|
|
511
|
+
if (lines.length === 0) return null;
|
|
512
|
+
const [tx, ty, zoom] = transform;
|
|
513
|
+
return /* @__PURE__ */ jsx(
|
|
514
|
+
"svg",
|
|
515
|
+
{
|
|
516
|
+
className: "absolute inset-0 pointer-events-none z-[1000]",
|
|
517
|
+
style: { width: "100%", height: "100%", overflow: "visible" },
|
|
518
|
+
children: lines.map((line, index) => {
|
|
519
|
+
if (line.type === "vertical") {
|
|
520
|
+
const x = line.position * zoom + tx;
|
|
521
|
+
const y1 = line.start * zoom + ty;
|
|
522
|
+
const y2 = line.end * zoom + ty;
|
|
523
|
+
return /* @__PURE__ */ jsx(
|
|
524
|
+
"line",
|
|
525
|
+
{
|
|
526
|
+
x1: x,
|
|
527
|
+
y1,
|
|
528
|
+
x2: x,
|
|
529
|
+
y2,
|
|
530
|
+
stroke: "#3b82f6",
|
|
531
|
+
strokeWidth: 1,
|
|
532
|
+
strokeDasharray: "4 2"
|
|
533
|
+
},
|
|
534
|
+
`v-${index}`
|
|
535
|
+
);
|
|
536
|
+
} else {
|
|
537
|
+
const y = line.position * zoom + ty;
|
|
538
|
+
const x1 = line.start * zoom + tx;
|
|
539
|
+
const x2 = line.end * zoom + tx;
|
|
540
|
+
return /* @__PURE__ */ jsx(
|
|
541
|
+
"line",
|
|
542
|
+
{
|
|
543
|
+
x1,
|
|
544
|
+
y1: y,
|
|
545
|
+
x2,
|
|
546
|
+
y2: y,
|
|
547
|
+
stroke: "#3b82f6",
|
|
548
|
+
strokeWidth: 1,
|
|
549
|
+
strokeDasharray: "4 2"
|
|
550
|
+
},
|
|
551
|
+
`h-${index}`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
var HelperLines = memo(HelperLinesComponent);
|
|
559
|
+
|
|
560
|
+
// src/canvas/NodeSearch.tsx
|
|
561
|
+
var import_types = __toESM(require_dist());
|
|
562
|
+
function NodeSearch() {
|
|
563
|
+
const { activeModal, closeModal } = useUIStore();
|
|
564
|
+
const { nodes, setSelectedNodeIds } = useWorkflowStore();
|
|
565
|
+
const reactFlow = useReactFlow();
|
|
566
|
+
const [search, setSearch] = useState("");
|
|
567
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
568
|
+
const listRef = useRef(null);
|
|
569
|
+
const backdropRef = useRef(null);
|
|
570
|
+
const inputRef = useRef(null);
|
|
571
|
+
const isOpen = activeModal === "nodeSearch";
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (isOpen && inputRef.current) {
|
|
574
|
+
inputRef.current.focus();
|
|
575
|
+
}
|
|
576
|
+
}, [isOpen]);
|
|
577
|
+
const filteredNodes = useMemo(() => {
|
|
578
|
+
if (!search.trim()) return nodes;
|
|
579
|
+
const query = search.toLowerCase();
|
|
580
|
+
return nodes.filter((node) => {
|
|
581
|
+
const label = (node.data.label || "").toLowerCase();
|
|
582
|
+
const type = (node.type || "").toLowerCase();
|
|
583
|
+
const comment = (node.data.comment || "").toLowerCase();
|
|
584
|
+
const nodeDefLabel = import_types.NODE_DEFINITIONS[node.type]?.label?.toLowerCase() || "";
|
|
585
|
+
return label.includes(query) || type.includes(query) || comment.includes(query) || nodeDefLabel.includes(query);
|
|
586
|
+
});
|
|
587
|
+
}, [nodes, search]);
|
|
588
|
+
useEffect(() => {
|
|
589
|
+
setSelectedIndex(0);
|
|
590
|
+
}, []);
|
|
591
|
+
useEffect(() => {
|
|
592
|
+
if (listRef.current) {
|
|
593
|
+
const selectedItem = listRef.current.children[selectedIndex];
|
|
594
|
+
selectedItem?.scrollIntoView({ block: "nearest" });
|
|
595
|
+
}
|
|
596
|
+
}, [selectedIndex]);
|
|
597
|
+
const handleSelectNode = useCallback(
|
|
598
|
+
(node) => {
|
|
599
|
+
setSelectedNodeIds([node.id]);
|
|
600
|
+
reactFlow.fitView({
|
|
601
|
+
nodes: [node],
|
|
602
|
+
padding: 0.5,
|
|
603
|
+
duration: 300
|
|
604
|
+
});
|
|
605
|
+
closeModal();
|
|
606
|
+
setSearch("");
|
|
607
|
+
},
|
|
608
|
+
[setSelectedNodeIds, reactFlow, closeModal]
|
|
609
|
+
);
|
|
610
|
+
const handleKeyDown = useCallback(
|
|
611
|
+
(e) => {
|
|
612
|
+
if (e.key === "ArrowDown") {
|
|
613
|
+
e.preventDefault();
|
|
614
|
+
setSelectedIndex((prev) => Math.min(prev + 1, filteredNodes.length - 1));
|
|
615
|
+
} else if (e.key === "ArrowUp") {
|
|
616
|
+
e.preventDefault();
|
|
617
|
+
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
|
618
|
+
} else if (e.key === "Enter" && filteredNodes[selectedIndex]) {
|
|
619
|
+
e.preventDefault();
|
|
620
|
+
handleSelectNode(filteredNodes[selectedIndex]);
|
|
621
|
+
} else if (e.key === "Escape") {
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
handleClose();
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
[filteredNodes, selectedIndex, handleSelectNode]
|
|
627
|
+
);
|
|
628
|
+
const handleClose = () => {
|
|
629
|
+
closeModal();
|
|
630
|
+
setSearch("");
|
|
631
|
+
setSelectedIndex(0);
|
|
632
|
+
};
|
|
633
|
+
const handleBackdropClick = (e) => {
|
|
634
|
+
if (e.target === backdropRef.current) {
|
|
635
|
+
handleClose();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
if (!isOpen) return null;
|
|
639
|
+
return /* @__PURE__ */ jsx(
|
|
640
|
+
"div",
|
|
641
|
+
{
|
|
642
|
+
ref: backdropRef,
|
|
643
|
+
onClick: handleBackdropClick,
|
|
644
|
+
className: "fixed inset-0 z-50 flex items-start justify-center pt-[20vh]",
|
|
645
|
+
style: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
|
646
|
+
children: /* @__PURE__ */ jsxs(
|
|
647
|
+
"div",
|
|
648
|
+
{
|
|
649
|
+
className: "bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-xl w-full max-w-lg",
|
|
650
|
+
role: "dialog",
|
|
651
|
+
"aria-label": "Find Node",
|
|
652
|
+
children: [
|
|
653
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-[var(--border)]", children: [
|
|
654
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
655
|
+
/* @__PURE__ */ jsx(Search, { className: "w-4 h-4 text-[var(--muted-foreground)]" }),
|
|
656
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Find Node" })
|
|
657
|
+
] }),
|
|
658
|
+
/* @__PURE__ */ jsx(
|
|
659
|
+
"button",
|
|
660
|
+
{
|
|
661
|
+
onClick: handleClose,
|
|
662
|
+
className: "p-1 rounded hover:bg-[var(--secondary)] transition-colors",
|
|
663
|
+
children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
|
|
664
|
+
}
|
|
665
|
+
)
|
|
666
|
+
] }),
|
|
667
|
+
/* @__PURE__ */ jsxs("div", { className: "p-4", onKeyDown: handleKeyDown, children: [
|
|
668
|
+
/* @__PURE__ */ jsxs("div", { className: "relative mb-3", children: [
|
|
669
|
+
/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }),
|
|
670
|
+
/* @__PURE__ */ jsx(
|
|
671
|
+
"input",
|
|
672
|
+
{
|
|
673
|
+
ref: inputRef,
|
|
674
|
+
type: "text",
|
|
675
|
+
placeholder: "Search nodes by name, type, or comment...",
|
|
676
|
+
value: search,
|
|
677
|
+
onChange: (e) => setSearch(e.target.value),
|
|
678
|
+
className: "w-full pl-9 pr-3 py-2 text-sm bg-[var(--secondary)] border border-[var(--border)] rounded-md outline-none focus:ring-1 focus:ring-[var(--ring)]",
|
|
679
|
+
autoFocus: true
|
|
680
|
+
}
|
|
681
|
+
)
|
|
682
|
+
] }),
|
|
683
|
+
/* @__PURE__ */ jsx("div", { ref: listRef, className: "max-h-[300px] overflow-y-auto space-y-1", children: filteredNodes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-muted-foreground py-8", children: search ? `No nodes found for "${search}"` : "No nodes in workflow" }) : filteredNodes.map((node, index) => {
|
|
684
|
+
const nodeDef = import_types.NODE_DEFINITIONS[node.type];
|
|
685
|
+
const comment = node.data.comment;
|
|
686
|
+
return /* @__PURE__ */ jsxs(
|
|
687
|
+
"button",
|
|
688
|
+
{
|
|
689
|
+
onClick: () => handleSelectNode(node),
|
|
690
|
+
className: `w-full flex items-center gap-3 p-2 rounded text-left transition-colors ${index === selectedIndex ? "bg-primary/10 border border-primary/30" : "hover:bg-secondary/50 border border-transparent"}`,
|
|
691
|
+
children: [
|
|
692
|
+
/* @__PURE__ */ jsx("div", { className: "flex-shrink-0 w-8 h-8 rounded bg-secondary flex items-center justify-center text-xs font-medium", children: nodeDef?.label?.charAt(0) || "?" }),
|
|
693
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
694
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium truncate text-sm", children: node.data.label }),
|
|
695
|
+
/* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground truncate", children: [
|
|
696
|
+
nodeDef?.label || node.type,
|
|
697
|
+
comment && ` \xB7 ${comment}`
|
|
698
|
+
] })
|
|
699
|
+
] })
|
|
700
|
+
]
|
|
701
|
+
},
|
|
702
|
+
node.id
|
|
703
|
+
);
|
|
704
|
+
}) }),
|
|
705
|
+
filteredNodes.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-3 pt-3 border-t border-border text-xs text-muted-foreground flex gap-4", children: [
|
|
706
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
707
|
+
/* @__PURE__ */ jsx("kbd", { className: "px-1.5 py-0.5 bg-secondary rounded", children: "up/down" }),
|
|
708
|
+
" Navigate"
|
|
709
|
+
] }),
|
|
710
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
711
|
+
/* @__PURE__ */ jsx("kbd", { className: "px-1.5 py-0.5 bg-secondary rounded", children: "Enter" }),
|
|
712
|
+
" Select"
|
|
713
|
+
] }),
|
|
714
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
715
|
+
/* @__PURE__ */ jsx("kbd", { className: "px-1.5 py-0.5 bg-secondary rounded", children: "Esc" }),
|
|
716
|
+
" Close"
|
|
717
|
+
] })
|
|
718
|
+
] })
|
|
719
|
+
] })
|
|
720
|
+
]
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
var SHORTCUTS = [
|
|
727
|
+
// Navigation
|
|
728
|
+
{ keys: "Scroll", description: "Pan canvas", category: "Navigation" },
|
|
729
|
+
{ keys: "Ctrl + Scroll", description: "Zoom in/out", category: "Navigation" },
|
|
730
|
+
{ keys: "F", description: "Fit view to selection (or all)", category: "Navigation" },
|
|
731
|
+
{ keys: "M", description: "Toggle sidebar", category: "Navigation" },
|
|
732
|
+
// Selection
|
|
733
|
+
{ keys: "Click", description: "Select node", category: "Selection" },
|
|
734
|
+
{ keys: "Shift + Click", description: "Add to selection", category: "Selection" },
|
|
735
|
+
{ keys: "Drag", description: "Marquee select", category: "Selection" },
|
|
736
|
+
{ keys: "Ctrl + A", description: "Select all nodes", category: "Selection" },
|
|
737
|
+
{ keys: "Ctrl + F", description: "Search nodes", category: "Selection" },
|
|
738
|
+
// Editing
|
|
739
|
+
{ keys: "Ctrl + Z", description: "Undo", category: "Editing" },
|
|
740
|
+
{ keys: "Ctrl + Shift + Z", description: "Redo", category: "Editing" },
|
|
741
|
+
{ keys: "Ctrl + C", description: "Copy", category: "Editing" },
|
|
742
|
+
{ keys: "Ctrl + X", description: "Cut", category: "Editing" },
|
|
743
|
+
{ keys: "Ctrl + V", description: "Paste", category: "Editing" },
|
|
744
|
+
{ keys: "Ctrl + D", description: "Duplicate", category: "Editing" },
|
|
745
|
+
{ keys: "Delete / Backspace", description: "Delete selected", category: "Editing" },
|
|
746
|
+
// Nodes
|
|
747
|
+
{ keys: "Shift + I", description: "Add Image Gen node", category: "Nodes" },
|
|
748
|
+
{ keys: "Shift + V", description: "Add Video Gen node", category: "Nodes" },
|
|
749
|
+
{ keys: "Shift + P", description: "Add Prompt node", category: "Nodes" },
|
|
750
|
+
{ keys: "Shift + L", description: "Add LLM node", category: "Nodes" },
|
|
751
|
+
// Organization
|
|
752
|
+
{ keys: "L", description: "Toggle lock on selected", category: "Organization" },
|
|
753
|
+
{ keys: "Ctrl + G", description: "Group selected nodes", category: "Organization" },
|
|
754
|
+
{ keys: "Ctrl + Shift + G", description: "Ungroup", category: "Organization" },
|
|
755
|
+
{ keys: "Ctrl + Shift + L", description: "Unlock all nodes", category: "Organization" },
|
|
756
|
+
// Help
|
|
757
|
+
{ keys: "?", description: "Show this help", category: "Help" }
|
|
758
|
+
];
|
|
759
|
+
var CATEGORIES = ["Navigation", "Selection", "Editing", "Nodes", "Organization", "Help"];
|
|
760
|
+
function ShortcutHelpModal() {
|
|
761
|
+
const { activeModal, closeModal } = useUIStore();
|
|
762
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
763
|
+
const backdropRef = useRef(null);
|
|
764
|
+
const inputRef = useRef(null);
|
|
765
|
+
const isOpen = activeModal === "shortcutHelp";
|
|
766
|
+
const filteredShortcuts = useMemo(() => {
|
|
767
|
+
if (!searchQuery.trim()) return SHORTCUTS;
|
|
768
|
+
const query = searchQuery.toLowerCase();
|
|
769
|
+
return SHORTCUTS.filter(
|
|
770
|
+
(shortcut) => shortcut.keys.toLowerCase().includes(query) || shortcut.description.toLowerCase().includes(query) || shortcut.category.toLowerCase().includes(query)
|
|
771
|
+
);
|
|
772
|
+
}, [searchQuery]);
|
|
773
|
+
const groupedShortcuts = useMemo(() => {
|
|
774
|
+
const grouped = {};
|
|
775
|
+
for (const category of CATEGORIES) {
|
|
776
|
+
const items = filteredShortcuts.filter((s) => s.category === category);
|
|
777
|
+
if (items.length > 0) {
|
|
778
|
+
grouped[category] = items;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return grouped;
|
|
782
|
+
}, [filteredShortcuts]);
|
|
783
|
+
const handleClose = () => {
|
|
784
|
+
closeModal();
|
|
785
|
+
setSearchQuery("");
|
|
786
|
+
};
|
|
787
|
+
const handleBackdropClick = (e) => {
|
|
788
|
+
if (e.target === backdropRef.current) {
|
|
789
|
+
handleClose();
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
if (!isOpen) return null;
|
|
793
|
+
return /* @__PURE__ */ jsx(
|
|
794
|
+
"div",
|
|
795
|
+
{
|
|
796
|
+
ref: backdropRef,
|
|
797
|
+
onClick: handleBackdropClick,
|
|
798
|
+
className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
|
|
799
|
+
style: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
|
800
|
+
children: /* @__PURE__ */ jsxs(
|
|
801
|
+
"div",
|
|
802
|
+
{
|
|
803
|
+
className: "bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-xl w-full max-w-2xl",
|
|
804
|
+
role: "dialog",
|
|
805
|
+
"aria-label": "Keyboard Shortcuts",
|
|
806
|
+
children: [
|
|
807
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-[var(--border)]", children: [
|
|
808
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
809
|
+
/* @__PURE__ */ jsx(Keyboard, { className: "w-4 h-4 text-[var(--muted-foreground)]" }),
|
|
810
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Keyboard Shortcuts" })
|
|
811
|
+
] }),
|
|
812
|
+
/* @__PURE__ */ jsx(
|
|
813
|
+
"button",
|
|
814
|
+
{
|
|
815
|
+
onClick: handleClose,
|
|
816
|
+
className: "p-1 rounded hover:bg-[var(--secondary)] transition-colors",
|
|
817
|
+
children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
|
|
818
|
+
}
|
|
819
|
+
)
|
|
820
|
+
] }),
|
|
821
|
+
/* @__PURE__ */ jsxs("div", { className: "p-4", children: [
|
|
822
|
+
/* @__PURE__ */ jsxs("div", { className: "relative mb-4", children: [
|
|
823
|
+
/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }),
|
|
824
|
+
/* @__PURE__ */ jsx(
|
|
825
|
+
"input",
|
|
826
|
+
{
|
|
827
|
+
ref: inputRef,
|
|
828
|
+
type: "text",
|
|
829
|
+
placeholder: "Search shortcuts...",
|
|
830
|
+
value: searchQuery,
|
|
831
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
832
|
+
className: "w-full pl-9 pr-3 py-2 text-sm bg-[var(--secondary)] border border-[var(--border)] rounded-md outline-none focus:ring-1 focus:ring-[var(--ring)]",
|
|
833
|
+
autoFocus: true
|
|
834
|
+
}
|
|
835
|
+
)
|
|
836
|
+
] }),
|
|
837
|
+
/* @__PURE__ */ jsxs("div", { className: "max-h-[60vh] overflow-y-auto space-y-6 pr-2", children: [
|
|
838
|
+
Object.entries(groupedShortcuts).map(([category, shortcuts]) => /* @__PURE__ */ jsxs("div", { children: [
|
|
839
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-muted-foreground mb-2", children: category }),
|
|
840
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1", children: shortcuts.map((shortcut) => /* @__PURE__ */ jsxs(
|
|
841
|
+
"div",
|
|
842
|
+
{
|
|
843
|
+
className: "flex items-center justify-between py-1.5 px-2 rounded hover:bg-secondary/50",
|
|
844
|
+
children: [
|
|
845
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: shortcut.description }),
|
|
846
|
+
/* @__PURE__ */ jsx("kbd", { className: "px-2 py-1 text-xs font-mono bg-secondary rounded border border-border", children: shortcut.keys })
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
shortcut.keys
|
|
850
|
+
)) })
|
|
851
|
+
] }, category)),
|
|
852
|
+
filteredShortcuts.length === 0 && /* @__PURE__ */ jsxs("div", { className: "text-center text-muted-foreground py-8", children: [
|
|
853
|
+
'No shortcuts found for "',
|
|
854
|
+
searchQuery,
|
|
855
|
+
'"'
|
|
856
|
+
] })
|
|
857
|
+
] })
|
|
858
|
+
] })
|
|
859
|
+
]
|
|
860
|
+
}
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/nodes/NodeDetailModal.tsx
|
|
867
|
+
var import_types2 = __toESM(require_dist());
|
|
868
|
+
var PROMPT_NODE_TYPES = ["prompt"];
|
|
869
|
+
function NodeDetailModal() {
|
|
870
|
+
const { activeModal, nodeDetailNodeId, nodeDetailStartIndex, closeNodeDetailModal } = useUIStore();
|
|
871
|
+
const { getNodeById } = useWorkflowStore();
|
|
872
|
+
const { openEditor } = usePromptEditorStore();
|
|
873
|
+
const [zoomLevel, setZoomLevel] = useState(1);
|
|
874
|
+
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
|
|
875
|
+
const [isPanning, setIsPanning] = useState(false);
|
|
876
|
+
const [panStart, setPanStart] = useState({ x: 0, y: 0 });
|
|
877
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
878
|
+
const node = useMemo(() => {
|
|
879
|
+
if (!nodeDetailNodeId) return null;
|
|
880
|
+
return getNodeById(nodeDetailNodeId);
|
|
881
|
+
}, [nodeDetailNodeId, getNodeById]);
|
|
882
|
+
useEffect(() => {
|
|
883
|
+
if (activeModal !== "nodeDetail" || !node) return;
|
|
884
|
+
if (PROMPT_NODE_TYPES.includes(node.type)) {
|
|
885
|
+
const promptData = node.data;
|
|
886
|
+
closeNodeDetailModal();
|
|
887
|
+
openEditor(node.id, promptData.prompt ?? "");
|
|
888
|
+
}
|
|
889
|
+
}, [activeModal, node, closeNodeDetailModal, openEditor]);
|
|
890
|
+
const mediaInfo = useMemo(() => {
|
|
891
|
+
if (!node) return { url: null, type: null };
|
|
892
|
+
return getMediaFromNode(node.type, node.data);
|
|
893
|
+
}, [node]);
|
|
894
|
+
const nodeDef = useMemo(() => {
|
|
895
|
+
if (!node) return null;
|
|
896
|
+
return import_types2.NODE_DEFINITIONS[node.type];
|
|
897
|
+
}, [node]);
|
|
898
|
+
const imageUrls = mediaInfo.urls ?? [];
|
|
899
|
+
const hasMultipleImages = imageUrls.length > 1;
|
|
900
|
+
const displayUrl = hasMultipleImages ? imageUrls[currentIndex] ?? mediaInfo.url : mediaInfo.url;
|
|
901
|
+
const goToPrevious = useCallback(() => {
|
|
902
|
+
setCurrentIndex((prev) => Math.max(prev - 1, 0));
|
|
903
|
+
setZoomLevel(1);
|
|
904
|
+
setPanOffset({ x: 0, y: 0 });
|
|
905
|
+
}, []);
|
|
906
|
+
const goToNext = useCallback(() => {
|
|
907
|
+
setCurrentIndex((prev) => Math.min(prev + 1, imageUrls.length - 1));
|
|
908
|
+
setZoomLevel(1);
|
|
909
|
+
setPanOffset({ x: 0, y: 0 });
|
|
910
|
+
}, [imageUrls.length]);
|
|
911
|
+
useEffect(() => {
|
|
912
|
+
setZoomLevel(1);
|
|
913
|
+
setPanOffset({ x: 0, y: 0 });
|
|
914
|
+
setCurrentIndex(nodeDetailStartIndex);
|
|
915
|
+
}, [nodeDetailNodeId, nodeDetailStartIndex]);
|
|
916
|
+
useEffect(() => {
|
|
917
|
+
const handleKeyDown = (e) => {
|
|
918
|
+
if (activeModal !== "nodeDetail") return;
|
|
919
|
+
if (e.key === "Escape") {
|
|
920
|
+
closeNodeDetailModal();
|
|
921
|
+
}
|
|
922
|
+
if (e.key === "+" || e.key === "=") {
|
|
923
|
+
setZoomLevel((prev) => Math.min(prev + 0.25, 4));
|
|
924
|
+
}
|
|
925
|
+
if (e.key === "-") {
|
|
926
|
+
setZoomLevel((prev) => Math.max(prev - 0.25, 0.25));
|
|
927
|
+
}
|
|
928
|
+
if (e.key === "0") {
|
|
929
|
+
setZoomLevel(1);
|
|
930
|
+
setPanOffset({ x: 0, y: 0 });
|
|
931
|
+
}
|
|
932
|
+
if (e.key === "ArrowLeft") {
|
|
933
|
+
goToPrevious();
|
|
934
|
+
}
|
|
935
|
+
if (e.key === "ArrowRight") {
|
|
936
|
+
goToNext();
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
940
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
941
|
+
}, [activeModal, closeNodeDetailModal, goToPrevious, goToNext]);
|
|
942
|
+
const handleMouseDown = useCallback(
|
|
943
|
+
(e) => {
|
|
944
|
+
if (zoomLevel > 1) {
|
|
945
|
+
setIsPanning(true);
|
|
946
|
+
setPanStart({ x: e.clientX - panOffset.x, y: e.clientY - panOffset.y });
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
[zoomLevel, panOffset]
|
|
950
|
+
);
|
|
951
|
+
const handleMouseMove = useCallback(
|
|
952
|
+
(e) => {
|
|
953
|
+
if (isPanning) {
|
|
954
|
+
setPanOffset({
|
|
955
|
+
x: e.clientX - panStart.x,
|
|
956
|
+
y: e.clientY - panStart.y
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
[isPanning, panStart]
|
|
961
|
+
);
|
|
962
|
+
const handleMouseUp = useCallback(() => {
|
|
963
|
+
setIsPanning(false);
|
|
964
|
+
}, []);
|
|
965
|
+
const handleDownload = useCallback(() => {
|
|
966
|
+
const url = displayUrl ?? mediaInfo.url;
|
|
967
|
+
if (!url) return;
|
|
968
|
+
const link = document.createElement("a");
|
|
969
|
+
link.href = url;
|
|
970
|
+
const suffix = hasMultipleImages ? `_${currentIndex + 1}` : "";
|
|
971
|
+
link.download = `${node?.data.label || "output"}${suffix}.${mediaInfo.type === "video" ? "mp4" : "png"}`;
|
|
972
|
+
document.body.appendChild(link);
|
|
973
|
+
link.click();
|
|
974
|
+
document.body.removeChild(link);
|
|
975
|
+
}, [displayUrl, mediaInfo, node, hasMultipleImages, currentIndex]);
|
|
976
|
+
if (activeModal !== "nodeDetail" || !node || !nodeDef) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
if (PROMPT_NODE_TYPES.includes(node.type)) {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
const nodeData = node.data;
|
|
983
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
984
|
+
/* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 bg-black/80", onClick: closeNodeDetailModal }),
|
|
985
|
+
/* @__PURE__ */ jsxs("div", { className: "fixed inset-4 z-50 flex flex-col bg-card rounded-lg border border-border shadow-xl overflow-hidden", children: [
|
|
986
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-border", children: [
|
|
987
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
988
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-medium text-foreground", children: nodeData.label }),
|
|
989
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground bg-secondary px-2 py-0.5 rounded", children: nodeDef.label })
|
|
990
|
+
] }),
|
|
991
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
992
|
+
displayUrl && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: handleDownload, children: [
|
|
993
|
+
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 mr-1" }),
|
|
994
|
+
"Download"
|
|
995
|
+
] }),
|
|
996
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: closeNodeDetailModal, children: /* @__PURE__ */ jsx(X, { className: "w-5 h-5" }) })
|
|
997
|
+
] })
|
|
998
|
+
] }),
|
|
999
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsxs(
|
|
1000
|
+
"div",
|
|
1001
|
+
{
|
|
1002
|
+
className: "relative w-full h-full flex items-center justify-center bg-background overflow-hidden",
|
|
1003
|
+
onMouseDown: handleMouseDown,
|
|
1004
|
+
onMouseMove: handleMouseMove,
|
|
1005
|
+
onMouseUp: handleMouseUp,
|
|
1006
|
+
onMouseLeave: handleMouseUp,
|
|
1007
|
+
style: { cursor: zoomLevel > 1 ? isPanning ? "grabbing" : "grab" : "default" },
|
|
1008
|
+
children: [
|
|
1009
|
+
displayUrl ? /* @__PURE__ */ jsxs(
|
|
1010
|
+
"div",
|
|
1011
|
+
{
|
|
1012
|
+
className: "transition-transform duration-100",
|
|
1013
|
+
style: {
|
|
1014
|
+
transform: `scale(${zoomLevel}) translate(${panOffset.x / zoomLevel}px, ${panOffset.y / zoomLevel}px)`
|
|
1015
|
+
},
|
|
1016
|
+
children: [
|
|
1017
|
+
mediaInfo.type === "image" && /* @__PURE__ */ jsx(
|
|
1018
|
+
Image,
|
|
1019
|
+
{
|
|
1020
|
+
src: displayUrl,
|
|
1021
|
+
alt: nodeData.label,
|
|
1022
|
+
width: 800,
|
|
1023
|
+
height: 600,
|
|
1024
|
+
className: "max-h-[calc(100vh-200px)] max-w-[calc(100vw-100px)] object-contain rounded-lg",
|
|
1025
|
+
unoptimized: true
|
|
1026
|
+
}
|
|
1027
|
+
),
|
|
1028
|
+
mediaInfo.type === "video" && /* @__PURE__ */ jsx(
|
|
1029
|
+
"video",
|
|
1030
|
+
{
|
|
1031
|
+
src: displayUrl,
|
|
1032
|
+
controls: true,
|
|
1033
|
+
autoPlay: true,
|
|
1034
|
+
loop: true,
|
|
1035
|
+
className: "max-h-[calc(100vh-200px)] max-w-[calc(100vw-100px)] rounded-lg"
|
|
1036
|
+
}
|
|
1037
|
+
)
|
|
1038
|
+
]
|
|
1039
|
+
}
|
|
1040
|
+
) : /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-center", children: [
|
|
1041
|
+
/* @__PURE__ */ jsx("p", { className: "text-lg", children: "No preview available" }),
|
|
1042
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm mt-2", children: "Generate content to see the preview" })
|
|
1043
|
+
] }),
|
|
1044
|
+
hasMultipleImages && currentIndex > 0 && /* @__PURE__ */ jsx(
|
|
1045
|
+
Button,
|
|
1046
|
+
{
|
|
1047
|
+
variant: "ghost",
|
|
1048
|
+
size: "icon-sm",
|
|
1049
|
+
onClick: goToPrevious,
|
|
1050
|
+
className: "absolute left-4 top-1/2 -translate-y-1/2 bg-card/80 hover:bg-card border border-border shadow-md",
|
|
1051
|
+
title: "Previous image (\u2190)",
|
|
1052
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "w-5 h-5" })
|
|
1053
|
+
}
|
|
1054
|
+
),
|
|
1055
|
+
hasMultipleImages && currentIndex < imageUrls.length - 1 && /* @__PURE__ */ jsx(
|
|
1056
|
+
Button,
|
|
1057
|
+
{
|
|
1058
|
+
variant: "ghost",
|
|
1059
|
+
size: "icon-sm",
|
|
1060
|
+
onClick: goToNext,
|
|
1061
|
+
className: "absolute right-4 top-1/2 -translate-y-1/2 bg-card/80 hover:bg-card border border-border shadow-md",
|
|
1062
|
+
title: "Next image (\u2192)",
|
|
1063
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "w-5 h-5" })
|
|
1064
|
+
}
|
|
1065
|
+
),
|
|
1066
|
+
hasMultipleImages && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-4 left-1/2 -translate-x-1/2 bg-card/80 border border-border rounded-full px-3 py-1 text-xs text-muted-foreground shadow-md", children: [
|
|
1067
|
+
currentIndex + 1,
|
|
1068
|
+
" / ",
|
|
1069
|
+
imageUrls.length
|
|
1070
|
+
] }),
|
|
1071
|
+
displayUrl && mediaInfo.type === "image" && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-4 right-4 flex items-center gap-2 bg-card border border-border rounded-lg p-1", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx(
|
|
1073
|
+
Button,
|
|
1074
|
+
{
|
|
1075
|
+
variant: "ghost",
|
|
1076
|
+
size: "icon-sm",
|
|
1077
|
+
onClick: () => setZoomLevel((prev) => Math.max(prev - 0.25, 0.25)),
|
|
1078
|
+
title: "Zoom out (-)",
|
|
1079
|
+
children: /* @__PURE__ */ jsx(ZoomOut, { className: "w-4 h-4" })
|
|
1080
|
+
}
|
|
1081
|
+
),
|
|
1082
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground w-12 text-center", children: [
|
|
1083
|
+
Math.round(zoomLevel * 100),
|
|
1084
|
+
"%"
|
|
1085
|
+
] }),
|
|
1086
|
+
/* @__PURE__ */ jsx(
|
|
1087
|
+
Button,
|
|
1088
|
+
{
|
|
1089
|
+
variant: "ghost",
|
|
1090
|
+
size: "icon-sm",
|
|
1091
|
+
onClick: () => setZoomLevel((prev) => Math.min(prev + 0.25, 4)),
|
|
1092
|
+
title: "Zoom in (+)",
|
|
1093
|
+
children: /* @__PURE__ */ jsx(ZoomIn, { className: "w-4 h-4" })
|
|
1094
|
+
}
|
|
1095
|
+
),
|
|
1096
|
+
/* @__PURE__ */ jsx(
|
|
1097
|
+
Button,
|
|
1098
|
+
{
|
|
1099
|
+
variant: "ghost",
|
|
1100
|
+
size: "sm",
|
|
1101
|
+
onClick: () => {
|
|
1102
|
+
setZoomLevel(1);
|
|
1103
|
+
setPanOffset({ x: 0, y: 0 });
|
|
1104
|
+
},
|
|
1105
|
+
title: "Reset zoom (0)",
|
|
1106
|
+
children: "Reset"
|
|
1107
|
+
}
|
|
1108
|
+
)
|
|
1109
|
+
] })
|
|
1110
|
+
]
|
|
1111
|
+
}
|
|
1112
|
+
) })
|
|
1113
|
+
] })
|
|
1114
|
+
] });
|
|
1115
|
+
}
|
|
1116
|
+
var DEFAULT_NODE_COLOR = "#6b7280";
|
|
1117
|
+
function supportsImageInput(schema) {
|
|
1118
|
+
if (!schema) return true;
|
|
1119
|
+
const properties = schema.properties;
|
|
1120
|
+
if (!properties) return true;
|
|
1121
|
+
return !!(properties.image || properties.image_input || properties.start_image || properties.first_frame_image || properties.reference_images);
|
|
1122
|
+
}
|
|
1123
|
+
function getEdgeDataType(edge, nodeMap) {
|
|
1124
|
+
const sourceNode = nodeMap.get(edge.source);
|
|
1125
|
+
if (!sourceNode) return null;
|
|
1126
|
+
const nodeDef = import_types3.NODE_DEFINITIONS[sourceNode.type];
|
|
1127
|
+
if (!nodeDef) return null;
|
|
1128
|
+
const sourceHandle = nodeDef.outputs.find((h) => h.id === edge.sourceHandle);
|
|
1129
|
+
return sourceHandle?.type ?? null;
|
|
1130
|
+
}
|
|
1131
|
+
function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
|
|
1132
|
+
const {
|
|
1133
|
+
nodes,
|
|
1134
|
+
edges,
|
|
1135
|
+
onNodesChange,
|
|
1136
|
+
onEdgesChange,
|
|
1137
|
+
onConnect,
|
|
1138
|
+
isValidConnection,
|
|
1139
|
+
findCompatibleHandle,
|
|
1140
|
+
addNode,
|
|
1141
|
+
selectedNodeIds,
|
|
1142
|
+
setSelectedNodeIds,
|
|
1143
|
+
toggleNodeLock,
|
|
1144
|
+
createGroup,
|
|
1145
|
+
deleteGroup,
|
|
1146
|
+
unlockAllNodes,
|
|
1147
|
+
groups,
|
|
1148
|
+
getConnectedNodeIds
|
|
1149
|
+
} = useWorkflowStore();
|
|
1150
|
+
const {
|
|
1151
|
+
selectNode,
|
|
1152
|
+
setHighlightedNodeIds,
|
|
1153
|
+
highlightedNodeIds,
|
|
1154
|
+
showPalette,
|
|
1155
|
+
togglePalette,
|
|
1156
|
+
openModal
|
|
1157
|
+
} = useUIStore();
|
|
1158
|
+
const reactFlow = useReactFlow();
|
|
1159
|
+
const { edgeStyle, showMinimap } = useSettingsStore();
|
|
1160
|
+
const isRunning = useExecutionStore((state) => state.isRunning);
|
|
1161
|
+
const currentNodeId = useExecutionStore((state) => state.currentNodeId);
|
|
1162
|
+
const executingNodeIds = useExecutionStore((state) => state.executingNodeIds);
|
|
1163
|
+
const activeNodeExecutions = useExecutionStore((state) => state.activeNodeExecutions);
|
|
1164
|
+
const hasActiveNodeExecutions = useExecutionStore((state) => state.activeNodeExecutions.size > 0);
|
|
1165
|
+
const [isMinimapVisible, setIsMinimapVisible] = useState(false);
|
|
1166
|
+
const [draggingNodeId, setDraggingNodeId] = useState(null);
|
|
1167
|
+
const hideTimeoutRef = useRef(null);
|
|
1168
|
+
const MINIMAP_HIDE_DELAY = 1500;
|
|
1169
|
+
const openShortcutHelp = useCallback(() => openModal("shortcutHelp"), [openModal]);
|
|
1170
|
+
const openNodeSearch = useCallback(() => openModal("nodeSearch"), [openModal]);
|
|
1171
|
+
useCanvasKeyboardShortcuts({
|
|
1172
|
+
selectedNodeIds,
|
|
1173
|
+
groups,
|
|
1174
|
+
nodes,
|
|
1175
|
+
toggleNodeLock,
|
|
1176
|
+
createGroup,
|
|
1177
|
+
deleteGroup,
|
|
1178
|
+
unlockAllNodes,
|
|
1179
|
+
addNode,
|
|
1180
|
+
togglePalette,
|
|
1181
|
+
fitView: reactFlow.fitView,
|
|
1182
|
+
openShortcutHelp,
|
|
1183
|
+
openNodeSearch
|
|
1184
|
+
});
|
|
1185
|
+
useEffect(() => {
|
|
1186
|
+
if (selectedNodeIds.length === 0) {
|
|
1187
|
+
setHighlightedNodeIds([]);
|
|
1188
|
+
} else {
|
|
1189
|
+
const connectedIds = getConnectedNodeIds(selectedNodeIds);
|
|
1190
|
+
setHighlightedNodeIds(connectedIds);
|
|
1191
|
+
}
|
|
1192
|
+
}, [selectedNodeIds, getConnectedNodeIds, setHighlightedNodeIds]);
|
|
1193
|
+
const nodeMap = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
|
1194
|
+
const isEdgeTargetingDisabledInput = useCallback(
|
|
1195
|
+
(edge) => {
|
|
1196
|
+
const targetNode = nodeMap.get(edge.target);
|
|
1197
|
+
if (!targetNode) return false;
|
|
1198
|
+
if (targetNode.type === "imageGen" && edge.targetHandle === "images") {
|
|
1199
|
+
const nodeData = targetNode.data;
|
|
1200
|
+
const hasImageSupport = supportsImageInput(nodeData?.selectedModel?.inputSchema);
|
|
1201
|
+
return !hasImageSupport;
|
|
1202
|
+
}
|
|
1203
|
+
if (targetNode.type === "videoGen") {
|
|
1204
|
+
if (edge.targetHandle === "image" || edge.targetHandle === "lastFrame") {
|
|
1205
|
+
const nodeData = targetNode.data;
|
|
1206
|
+
const hasImageSupport = supportsImageInput(nodeData?.selectedModel?.inputSchema);
|
|
1207
|
+
return !hasImageSupport;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return false;
|
|
1211
|
+
},
|
|
1212
|
+
[nodeMap]
|
|
1213
|
+
);
|
|
1214
|
+
const styledEdges = useMemo(() => {
|
|
1215
|
+
const executionScope = executingNodeIds.length > 0 ? new Set(executingNodeIds) : null;
|
|
1216
|
+
const highlightedSet = highlightedNodeIds.length > 0 ? new Set(highlightedNodeIds) : null;
|
|
1217
|
+
return edges.map((edge) => {
|
|
1218
|
+
const dataType = getEdgeDataType(edge, nodeMap);
|
|
1219
|
+
const typeClass = dataType ? `edge-${dataType}` : "";
|
|
1220
|
+
const isDisabledTarget = isEdgeTargetingDisabledInput(edge);
|
|
1221
|
+
if (!isRunning && hasActiveNodeExecutions) {
|
|
1222
|
+
const isActiveEdge = activeNodeExecutions.has(edge.source) || activeNodeExecutions.has(edge.target);
|
|
1223
|
+
if (isActiveEdge && !isDisabledTarget) {
|
|
1224
|
+
return {
|
|
1225
|
+
...edge,
|
|
1226
|
+
animated: false,
|
|
1227
|
+
className: `${typeClass} executing`.trim()
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (isRunning) {
|
|
1232
|
+
const isInExecutionScope = !executionScope || executionScope.has(edge.source) || executionScope.has(edge.target);
|
|
1233
|
+
const isExecutingEdge = currentNodeId && (edge.source === currentNodeId || edge.target === currentNodeId);
|
|
1234
|
+
if (isDisabledTarget) {
|
|
1235
|
+
return {
|
|
1236
|
+
...edge,
|
|
1237
|
+
animated: false,
|
|
1238
|
+
className: `${typeClass} edge-disabled`.trim()
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
if (!isInExecutionScope) {
|
|
1242
|
+
return {
|
|
1243
|
+
...edge,
|
|
1244
|
+
animated: false,
|
|
1245
|
+
className: typeClass
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
return {
|
|
1249
|
+
...edge,
|
|
1250
|
+
animated: false,
|
|
1251
|
+
className: `${typeClass} ${isExecutingEdge ? "executing" : "active-pipe"}`.trim()
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
if (isDisabledTarget) {
|
|
1255
|
+
return {
|
|
1256
|
+
...edge,
|
|
1257
|
+
className: `${typeClass} edge-disabled`.trim()
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
if (highlightedSet) {
|
|
1261
|
+
const isConnected = highlightedSet.has(edge.source) && highlightedSet.has(edge.target);
|
|
1262
|
+
return {
|
|
1263
|
+
...edge,
|
|
1264
|
+
className: `${typeClass} ${isConnected ? "highlighted" : "dimmed"}`.trim()
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
...edge,
|
|
1269
|
+
className: typeClass
|
|
1270
|
+
};
|
|
1271
|
+
});
|
|
1272
|
+
}, [
|
|
1273
|
+
edges,
|
|
1274
|
+
nodeMap,
|
|
1275
|
+
highlightedNodeIds,
|
|
1276
|
+
isRunning,
|
|
1277
|
+
currentNodeId,
|
|
1278
|
+
executingNodeIds,
|
|
1279
|
+
activeNodeExecutions,
|
|
1280
|
+
hasActiveNodeExecutions,
|
|
1281
|
+
isEdgeTargetingDisabledInput
|
|
1282
|
+
]);
|
|
1283
|
+
const handleNodeClick = useCallback(
|
|
1284
|
+
(_event, node) => {
|
|
1285
|
+
selectNode(node.id);
|
|
1286
|
+
},
|
|
1287
|
+
[selectNode]
|
|
1288
|
+
);
|
|
1289
|
+
const handlePaneClick = useCallback(() => {
|
|
1290
|
+
selectNode(null);
|
|
1291
|
+
setSelectedNodeIds([]);
|
|
1292
|
+
}, [selectNode, setSelectedNodeIds]);
|
|
1293
|
+
const handleSelectionChange = useCallback(
|
|
1294
|
+
({ nodes: selectedNodes }) => {
|
|
1295
|
+
setSelectedNodeIds(selectedNodes.map((n) => n.id));
|
|
1296
|
+
},
|
|
1297
|
+
[setSelectedNodeIds]
|
|
1298
|
+
);
|
|
1299
|
+
const handleNodeContextMenu = useCallback(
|
|
1300
|
+
(event, node) => {
|
|
1301
|
+
event.preventDefault();
|
|
1302
|
+
},
|
|
1303
|
+
[selectedNodeIds]
|
|
1304
|
+
);
|
|
1305
|
+
const handleEdgeContextMenu = useCallback(
|
|
1306
|
+
(event, edge) => {
|
|
1307
|
+
event.preventDefault();
|
|
1308
|
+
},
|
|
1309
|
+
[]
|
|
1310
|
+
);
|
|
1311
|
+
const handlePaneContextMenu = useCallback(
|
|
1312
|
+
(event) => {
|
|
1313
|
+
event.preventDefault();
|
|
1314
|
+
},
|
|
1315
|
+
[]
|
|
1316
|
+
);
|
|
1317
|
+
const handleSelectionContextMenu = useCallback(
|
|
1318
|
+
(event, selectedNodes) => {
|
|
1319
|
+
event.preventDefault();
|
|
1320
|
+
},
|
|
1321
|
+
[]
|
|
1322
|
+
);
|
|
1323
|
+
const handleDrop = useCallback(
|
|
1324
|
+
(event) => {
|
|
1325
|
+
event.preventDefault();
|
|
1326
|
+
const nodeType = event.dataTransfer.getData("nodeType");
|
|
1327
|
+
if (!nodeType) return;
|
|
1328
|
+
const position = reactFlow.screenToFlowPosition({
|
|
1329
|
+
x: event.clientX,
|
|
1330
|
+
y: event.clientY
|
|
1331
|
+
});
|
|
1332
|
+
addNode(nodeType, position);
|
|
1333
|
+
},
|
|
1334
|
+
[addNode, reactFlow]
|
|
1335
|
+
);
|
|
1336
|
+
const handleDragOver = useCallback((event) => {
|
|
1337
|
+
event.preventDefault();
|
|
1338
|
+
event.dataTransfer.dropEffect = "move";
|
|
1339
|
+
}, []);
|
|
1340
|
+
const handleNodeDragStart = useCallback((_event, node) => {
|
|
1341
|
+
setDraggingNodeId(node.id);
|
|
1342
|
+
}, []);
|
|
1343
|
+
const handleNodeDrag = useCallback((_event, node) => {
|
|
1344
|
+
setDraggingNodeId(node.id);
|
|
1345
|
+
}, []);
|
|
1346
|
+
const handleNodeDragStop = useCallback(() => {
|
|
1347
|
+
setDraggingNodeId(null);
|
|
1348
|
+
}, []);
|
|
1349
|
+
const handleConnectEnd = useCallback(
|
|
1350
|
+
(event, connectionState) => {
|
|
1351
|
+
const state = connectionState;
|
|
1352
|
+
const sourceNodeId = state.fromNode?.id;
|
|
1353
|
+
const sourceHandleId = state.fromHandle?.id;
|
|
1354
|
+
if (!sourceNodeId || !sourceHandleId) return;
|
|
1355
|
+
const target = event.target;
|
|
1356
|
+
const nodeElement = target.closest(".react-flow__node");
|
|
1357
|
+
if (!nodeElement) return;
|
|
1358
|
+
const targetNodeId = nodeElement.getAttribute("data-id");
|
|
1359
|
+
if (!targetNodeId || targetNodeId === sourceNodeId) return;
|
|
1360
|
+
const droppedOnHandle = target.closest(".react-flow__handle");
|
|
1361
|
+
if (droppedOnHandle) return;
|
|
1362
|
+
const compatibleHandle = findCompatibleHandle(sourceNodeId, sourceHandleId, targetNodeId);
|
|
1363
|
+
if (!compatibleHandle) return;
|
|
1364
|
+
onConnect({
|
|
1365
|
+
source: sourceNodeId,
|
|
1366
|
+
sourceHandle: sourceHandleId,
|
|
1367
|
+
target: targetNodeId,
|
|
1368
|
+
targetHandle: compatibleHandle
|
|
1369
|
+
});
|
|
1370
|
+
},
|
|
1371
|
+
[findCompatibleHandle, onConnect]
|
|
1372
|
+
);
|
|
1373
|
+
const checkValidConnection = useCallback(
|
|
1374
|
+
(connection) => {
|
|
1375
|
+
const conn = {
|
|
1376
|
+
source: connection.source,
|
|
1377
|
+
target: connection.target,
|
|
1378
|
+
sourceHandle: connection.sourceHandle ?? null,
|
|
1379
|
+
targetHandle: connection.targetHandle ?? null
|
|
1380
|
+
};
|
|
1381
|
+
return isValidConnection(conn);
|
|
1382
|
+
},
|
|
1383
|
+
[isValidConnection]
|
|
1384
|
+
);
|
|
1385
|
+
const handleMoveStart = useCallback(() => {
|
|
1386
|
+
if (!showMinimap) return;
|
|
1387
|
+
if (hideTimeoutRef.current) {
|
|
1388
|
+
clearTimeout(hideTimeoutRef.current);
|
|
1389
|
+
hideTimeoutRef.current = null;
|
|
1390
|
+
}
|
|
1391
|
+
setIsMinimapVisible(true);
|
|
1392
|
+
}, [showMinimap]);
|
|
1393
|
+
const handleMoveEnd = useCallback(() => {
|
|
1394
|
+
if (!showMinimap) return;
|
|
1395
|
+
if (hideTimeoutRef.current) {
|
|
1396
|
+
clearTimeout(hideTimeoutRef.current);
|
|
1397
|
+
}
|
|
1398
|
+
hideTimeoutRef.current = setTimeout(() => {
|
|
1399
|
+
setIsMinimapVisible(false);
|
|
1400
|
+
hideTimeoutRef.current = null;
|
|
1401
|
+
}, MINIMAP_HIDE_DELAY);
|
|
1402
|
+
}, [showMinimap]);
|
|
1403
|
+
useEffect(() => {
|
|
1404
|
+
return () => {
|
|
1405
|
+
if (hideTimeoutRef.current) {
|
|
1406
|
+
clearTimeout(hideTimeoutRef.current);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}, []);
|
|
1410
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-full h-full relative", onDrop: handleDrop, onDragOver: handleDragOver, children: [
|
|
1411
|
+
!showPalette && /* @__PURE__ */ jsx(
|
|
1412
|
+
"button",
|
|
1413
|
+
{
|
|
1414
|
+
onClick: togglePalette,
|
|
1415
|
+
className: "absolute top-3 left-3 z-10 p-1.5 bg-[var(--background)] border border-[var(--border)] rounded-md hover:bg-[var(--secondary)] transition-colors group",
|
|
1416
|
+
title: "Open sidebar (M)",
|
|
1417
|
+
children: /* @__PURE__ */ jsx(PanelLeft, { className: "w-4 h-4 text-[var(--muted-foreground)] group-hover:text-[var(--foreground)]" })
|
|
1418
|
+
}
|
|
1419
|
+
),
|
|
1420
|
+
/* @__PURE__ */ jsxs(
|
|
1421
|
+
ReactFlow,
|
|
1422
|
+
{
|
|
1423
|
+
nodes,
|
|
1424
|
+
edges: styledEdges,
|
|
1425
|
+
onNodesChange,
|
|
1426
|
+
onEdgesChange,
|
|
1427
|
+
onConnect,
|
|
1428
|
+
onConnectEnd: handleConnectEnd,
|
|
1429
|
+
onNodeClick: handleNodeClick,
|
|
1430
|
+
onPaneClick: handlePaneClick,
|
|
1431
|
+
onSelectionChange: handleSelectionChange,
|
|
1432
|
+
onNodeContextMenu: handleNodeContextMenu,
|
|
1433
|
+
onEdgeContextMenu: handleEdgeContextMenu,
|
|
1434
|
+
onPaneContextMenu: handlePaneContextMenu,
|
|
1435
|
+
onSelectionContextMenu: handleSelectionContextMenu,
|
|
1436
|
+
onNodeDragStart: handleNodeDragStart,
|
|
1437
|
+
onNodeDrag: handleNodeDrag,
|
|
1438
|
+
onNodeDragStop: handleNodeDragStop,
|
|
1439
|
+
isValidConnection: checkValidConnection,
|
|
1440
|
+
nodeTypes: nodeTypesProp ?? nodeTypes,
|
|
1441
|
+
fitView: true,
|
|
1442
|
+
snapToGrid: true,
|
|
1443
|
+
snapGrid: [16, 16],
|
|
1444
|
+
connectionMode: ConnectionMode.Loose,
|
|
1445
|
+
selectionMode: SelectionMode.Partial,
|
|
1446
|
+
selectionOnDrag: true,
|
|
1447
|
+
panOnDrag: [1, 2],
|
|
1448
|
+
onMoveStart: handleMoveStart,
|
|
1449
|
+
onMoveEnd: handleMoveEnd,
|
|
1450
|
+
deleteKeyCode: ["Backspace", "Delete"],
|
|
1451
|
+
defaultEdgeOptions: {
|
|
1452
|
+
type: edgeStyle,
|
|
1453
|
+
animated: false
|
|
1454
|
+
},
|
|
1455
|
+
edgesFocusable: true,
|
|
1456
|
+
edgesReconnectable: true,
|
|
1457
|
+
proOptions: { hideAttribution: true },
|
|
1458
|
+
onlyRenderVisibleElements: nodes.length > 50,
|
|
1459
|
+
children: [
|
|
1460
|
+
/* @__PURE__ */ jsx(GroupOverlay, {}),
|
|
1461
|
+
/* @__PURE__ */ jsx(
|
|
1462
|
+
Background,
|
|
1463
|
+
{
|
|
1464
|
+
variant: BackgroundVariant.Dots,
|
|
1465
|
+
gap: 16,
|
|
1466
|
+
size: 1,
|
|
1467
|
+
color: "rgba(255, 255, 255, 0.08)"
|
|
1468
|
+
}
|
|
1469
|
+
),
|
|
1470
|
+
/* @__PURE__ */ jsx(Controls, {}),
|
|
1471
|
+
showMinimap && /* @__PURE__ */ jsx(
|
|
1472
|
+
MiniMap,
|
|
1473
|
+
{
|
|
1474
|
+
nodeStrokeWidth: 0,
|
|
1475
|
+
nodeColor: () => DEFAULT_NODE_COLOR,
|
|
1476
|
+
zoomable: true,
|
|
1477
|
+
pannable: true,
|
|
1478
|
+
maskColor: "rgba(0, 0, 0, 0.8)",
|
|
1479
|
+
className: `!bg-transparent !border-[var(--border)] !rounded-lg transition-opacity duration-300 ${isMinimapVisible ? "opacity-100" : "opacity-0 pointer-events-none"}`
|
|
1480
|
+
}
|
|
1481
|
+
),
|
|
1482
|
+
/* @__PURE__ */ jsx(HelperLines, { draggingNodeId })
|
|
1483
|
+
]
|
|
1484
|
+
}
|
|
1485
|
+
),
|
|
1486
|
+
/* @__PURE__ */ jsx(NodeDetailModal, {}),
|
|
1487
|
+
/* @__PURE__ */ jsx(ShortcutHelpModal, {}),
|
|
1488
|
+
/* @__PURE__ */ jsx(NodeSearch, {})
|
|
1489
|
+
] });
|
|
1490
|
+
}
|
|
1491
|
+
function PauseEdgeComponent({
|
|
1492
|
+
id,
|
|
1493
|
+
sourceX,
|
|
1494
|
+
sourceY,
|
|
1495
|
+
targetX,
|
|
1496
|
+
targetY,
|
|
1497
|
+
sourcePosition,
|
|
1498
|
+
targetPosition,
|
|
1499
|
+
style = {},
|
|
1500
|
+
markerEnd,
|
|
1501
|
+
data
|
|
1502
|
+
}) {
|
|
1503
|
+
const [edgePath, labelX, labelY] = getBezierPath({
|
|
1504
|
+
sourceX,
|
|
1505
|
+
sourceY,
|
|
1506
|
+
sourcePosition,
|
|
1507
|
+
targetX,
|
|
1508
|
+
targetY,
|
|
1509
|
+
targetPosition
|
|
1510
|
+
});
|
|
1511
|
+
const hasPause = data?.hasPause === true;
|
|
1512
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1513
|
+
/* @__PURE__ */ jsx(
|
|
1514
|
+
BaseEdge,
|
|
1515
|
+
{
|
|
1516
|
+
path: edgePath,
|
|
1517
|
+
markerEnd,
|
|
1518
|
+
style: {
|
|
1519
|
+
...style,
|
|
1520
|
+
...hasPause && {
|
|
1521
|
+
strokeDasharray: "5 5",
|
|
1522
|
+
stroke: "#f59e0b"
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
),
|
|
1527
|
+
hasPause && /* @__PURE__ */ jsx(
|
|
1528
|
+
"foreignObject",
|
|
1529
|
+
{
|
|
1530
|
+
width: 20,
|
|
1531
|
+
height: 20,
|
|
1532
|
+
x: labelX - 10,
|
|
1533
|
+
y: labelY - 10,
|
|
1534
|
+
className: "pointer-events-none",
|
|
1535
|
+
children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center w-5 h-5 rounded-full bg-amber-500 text-white", children: /* @__PURE__ */ jsx(Pause, { className: "w-3 h-3" }) })
|
|
1536
|
+
}
|
|
1537
|
+
)
|
|
1538
|
+
] });
|
|
1539
|
+
}
|
|
1540
|
+
var PauseEdge = memo(PauseEdgeComponent);
|
|
1541
|
+
|
|
1542
|
+
export { DEFAULT_GROUP_COLORS, GROUP_COLORS, GroupOverlay, HelperLines, NodeSearch, PauseEdge, ShortcutHelpModal, WorkflowCanvas };
|