@genfeedai/workflow-ui 0.1.3 → 0.1.4

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.
Files changed (84) hide show
  1. package/dist/canvas.d.mts +16 -2
  2. package/dist/canvas.mjs +10 -8
  3. package/dist/chunk-6PSJTBNV.mjs +638 -0
  4. package/dist/chunk-7H3WJJYS.mjs +52 -0
  5. package/dist/{chunk-HCXI63ME.mjs → chunk-AUQGOJOQ.mjs} +27 -4
  6. package/dist/{chunk-AOTUCJMA.mjs → chunk-GWBGK3KL.mjs} +2 -2
  7. package/dist/chunk-JTPADIUO.mjs +130 -0
  8. package/dist/{chunk-SQK4JDYY.mjs → chunk-LT3ZJJL6.mjs} +9 -2
  9. package/dist/{chunk-7P2JWDC7.mjs → chunk-O5II6BOJ.mjs} +1198 -254
  10. package/dist/{chunk-AUZR6REQ.mjs → chunk-OQREHJXK.mjs} +1 -1
  11. package/dist/chunk-OY7BRSGG.mjs +60 -0
  12. package/dist/{chunk-E3YBVMYZ.mjs → chunk-PANZDSP6.mjs} +274 -305
  13. package/dist/chunk-PCIWWD37.mjs +90 -0
  14. package/dist/{chunk-RIGVIEYB.mjs → chunk-R727OFBR.mjs} +11 -1
  15. package/dist/chunk-ZD2BADZO.mjs +1294 -0
  16. package/dist/contextMenuStore-DMg0hJQ1.d.mts +22 -0
  17. package/dist/hooks.d.mts +53 -244
  18. package/dist/hooks.mjs +6 -6
  19. package/dist/index.d.mts +11 -7
  20. package/dist/index.mjs +13 -11
  21. package/dist/lib.d.mts +250 -4
  22. package/dist/lib.mjs +562 -2
  23. package/dist/nodes.d.mts +3 -1
  24. package/dist/nodes.mjs +6 -6
  25. package/dist/panels.mjs +3 -4
  26. package/dist/{promptLibraryStore-zqb59nsu.d.mts → promptLibraryStore-Bgw5LzvD.d.mts} +33 -5
  27. package/dist/provider.d.mts +2 -2
  28. package/dist/provider.mjs +0 -1
  29. package/dist/stores.d.mts +4 -3
  30. package/dist/stores.mjs +3 -40
  31. package/dist/toolbar.d.mts +3 -1
  32. package/dist/toolbar.mjs +5 -4
  33. package/dist/{types-ipAnBzAJ.d.mts → types-CF6DPx0P.d.mts} +8 -3
  34. package/dist/ui.d.mts +1 -1
  35. package/dist/ui.mjs +0 -1
  36. package/dist/{hooks.d.ts → useCommentNavigation-NzJjkaj2.d.mts} +15 -2
  37. package/dist/workflowStore-UAAKOOIK.mjs +2 -0
  38. package/package.json +30 -24
  39. package/dist/canvas.d.ts +0 -27
  40. package/dist/canvas.js +0 -45
  41. package/dist/chunk-3SPPKCWR.js +0 -458
  42. package/dist/chunk-3TMV3K34.js +0 -534
  43. package/dist/chunk-3YFFDHC5.js +0 -300
  44. package/dist/chunk-4MZ62VMF.js +0 -37
  45. package/dist/chunk-5HJFQVUR.js +0 -61
  46. package/dist/chunk-5LQ4QBR5.js +0 -2
  47. package/dist/chunk-6DOEUDD5.js +0 -254
  48. package/dist/chunk-AXFOCPPP.js +0 -998
  49. package/dist/chunk-BMFRA6GK.js +0 -1546
  50. package/dist/chunk-E323WAZG.mjs +0 -272
  51. package/dist/chunk-ECD5J2BA.js +0 -6022
  52. package/dist/chunk-EMGXUNBL.js +0 -120
  53. package/dist/chunk-EMUMKW5C.js +0 -107
  54. package/dist/chunk-FOMOOERN.js +0 -2
  55. package/dist/chunk-IASLG6IA.mjs +0 -118
  56. package/dist/chunk-IHF35QZD.js +0 -1095
  57. package/dist/chunk-JLWKW3G5.js +0 -2
  58. package/dist/chunk-KDIWRSYV.js +0 -375
  59. package/dist/chunk-L5TF4EHW.mjs +0 -1
  60. package/dist/chunk-RJ262NXS.js +0 -24
  61. package/dist/chunk-RXNEDWK2.js +0 -141
  62. package/dist/chunk-SEV2DWKF.js +0 -744
  63. package/dist/chunk-ZJWP5KGZ.mjs +0 -33
  64. package/dist/hooks.js +0 -56
  65. package/dist/index.d.ts +0 -29
  66. package/dist/index.js +0 -180
  67. package/dist/lib.d.ts +0 -164
  68. package/dist/lib.js +0 -144
  69. package/dist/nodes.d.ts +0 -128
  70. package/dist/nodes.js +0 -151
  71. package/dist/panels.d.ts +0 -22
  72. package/dist/panels.js +0 -21
  73. package/dist/promptLibraryStore-BZnfmEkc.d.ts +0 -464
  74. package/dist/provider.d.ts +0 -29
  75. package/dist/provider.js +0 -17
  76. package/dist/stores.d.ts +0 -96
  77. package/dist/stores.js +0 -113
  78. package/dist/toolbar.d.ts +0 -73
  79. package/dist/toolbar.js +0 -34
  80. package/dist/types-ipAnBzAJ.d.ts +0 -46
  81. package/dist/ui.d.ts +0 -67
  82. package/dist/ui.js +0 -84
  83. package/dist/workflowStore-7SDJC4UR.mjs +0 -3
  84. package/dist/workflowStore-LNJQ5RZG.js +0 -12
@@ -0,0 +1,1294 @@
1
+ import { selectNodes, selectEdges, selectRemoveNode, selectDuplicateNode, selectAddNode, useContextMenuStore, selectToggleNodeLock, selectCreateGroup, selectWorkflowId, selectSetSelectedNodeIds, selectUpdateNodeData } from './chunk-PCIWWD37.mjs';
2
+ import { useSettingsStore } from './chunk-LT3ZJJL6.mjs';
3
+ import { useWorkflowStore } from './chunk-R727OFBR.mjs';
4
+ import { useWorkflowUIConfig } from './chunk-FT33LFII.mjs';
5
+ import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
6
+ import { nanoid } from 'nanoid';
7
+ import { useReactFlow } from '@xyflow/react';
8
+ import Dagre from '@dagrejs/dagre';
9
+ import { getNodesByCategory, NODE_DEFINITIONS } from '@genfeedai/types';
10
+ import { ChevronRight, Group, Copy, Lock, LockOpen, AlignHorizontalJustifyCenter, AlignVerticalJustifyCenter, Trash2, GitBranch, ArrowLeftFromLine, ArrowRightToLine, CheckCircle, Subtitles, Pencil, Grid3X3, Maximize, Crop, Film, Scissors, Layers, Wand2, Maximize2, Navigation, AudioLines, Mic, Brain, Video, Sparkles, FileVideo, Volume2, FileText, MessageSquare, Image, Monitor, Clipboard, LayoutGrid, Palette } from 'lucide-react';
11
+ import { jsx, jsxs } from 'react/jsx-runtime';
12
+
13
+ function useCanvasKeyboardShortcuts({
14
+ selectedNodeIds,
15
+ groups,
16
+ nodes,
17
+ toggleNodeLock,
18
+ createGroup,
19
+ deleteGroup,
20
+ unlockAllNodes,
21
+ addNode,
22
+ togglePalette,
23
+ fitView,
24
+ openShortcutHelp,
25
+ openNodeSearch,
26
+ deleteSelectedElements
27
+ }) {
28
+ useEffect(() => {
29
+ const handleKeyDown = (e) => {
30
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target?.contentEditable === "true" || e.target?.closest?.(
31
+ '[role="textbox"], [role="combobox"], [role="searchbox"]'
32
+ )) {
33
+ return;
34
+ }
35
+ const isMod = e.ctrlKey || e.metaKey;
36
+ if (e.key === "Delete" || e.key === "Backspace") {
37
+ e.preventDefault();
38
+ deleteSelectedElements();
39
+ return;
40
+ }
41
+ if (e.key === "m" && !isMod && !e.shiftKey) {
42
+ e.preventDefault();
43
+ togglePalette();
44
+ }
45
+ if (e.key === "f" && !isMod && !e.shiftKey) {
46
+ e.preventDefault();
47
+ if (selectedNodeIds.length > 0) {
48
+ const selectedNodes = nodes.filter((n) => selectedNodeIds.includes(n.id));
49
+ fitView({ nodes: selectedNodes, padding: 0.3, duration: 200 });
50
+ } else {
51
+ fitView({ padding: 0.2, duration: 200 });
52
+ }
53
+ }
54
+ if (e.key === "?" && e.shiftKey && !isMod) {
55
+ e.preventDefault();
56
+ openShortcutHelp();
57
+ }
58
+ if (e.key === "f" && isMod && !e.shiftKey) {
59
+ e.preventDefault();
60
+ openNodeSearch();
61
+ }
62
+ if (e.key === "l" && !isMod && !e.shiftKey) {
63
+ e.preventDefault();
64
+ for (const nodeId of selectedNodeIds) {
65
+ toggleNodeLock(nodeId);
66
+ }
67
+ }
68
+ if (e.key === "g" && isMod && !e.shiftKey) {
69
+ e.preventDefault();
70
+ if (selectedNodeIds.length > 1) {
71
+ createGroup(selectedNodeIds);
72
+ }
73
+ }
74
+ if (e.key === "g" && isMod && e.shiftKey) {
75
+ e.preventDefault();
76
+ for (const nodeId of selectedNodeIds) {
77
+ const group = groups.find((g) => g.nodeIds.includes(nodeId));
78
+ if (group) {
79
+ deleteGroup(group.id);
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ if (e.key === "l" && isMod && e.shiftKey) {
85
+ e.preventDefault();
86
+ unlockAllNodes();
87
+ }
88
+ if (e.key === "I" && e.shiftKey && !isMod) {
89
+ e.preventDefault();
90
+ const position = { x: window.innerWidth / 2 - 150, y: window.innerHeight / 2 - 150 };
91
+ addNode("imageGen", position);
92
+ }
93
+ if (e.key === "V" && e.shiftKey && !isMod) {
94
+ e.preventDefault();
95
+ const position = { x: window.innerWidth / 2 - 150, y: window.innerHeight / 2 - 150 };
96
+ addNode("videoGen", position);
97
+ }
98
+ if (e.key === "P" && e.shiftKey && !isMod) {
99
+ e.preventDefault();
100
+ const position = { x: window.innerWidth / 2 - 150, y: window.innerHeight / 2 - 150 };
101
+ addNode("prompt", position);
102
+ }
103
+ if ((e.key === "L" || e.key === "l") && e.shiftKey && !isMod) {
104
+ e.preventDefault();
105
+ const position = { x: window.innerWidth / 2 - 150, y: window.innerHeight / 2 - 150 };
106
+ addNode("llm", position);
107
+ }
108
+ if (e.key === "z" && isMod && !e.shiftKey) {
109
+ e.preventDefault();
110
+ useWorkflowStore.temporal.getState().undo();
111
+ }
112
+ if (e.key === "z" && isMod && e.shiftKey) {
113
+ e.preventDefault();
114
+ useWorkflowStore.temporal.getState().redo();
115
+ }
116
+ };
117
+ window.addEventListener("keydown", handleKeyDown);
118
+ return () => window.removeEventListener("keydown", handleKeyDown);
119
+ }, [
120
+ selectedNodeIds,
121
+ toggleNodeLock,
122
+ createGroup,
123
+ deleteGroup,
124
+ unlockAllNodes,
125
+ groups,
126
+ nodes,
127
+ addNode,
128
+ togglePalette,
129
+ fitView,
130
+ openShortcutHelp,
131
+ openNodeSearch,
132
+ deleteSelectedElements
133
+ ]);
134
+ }
135
+ function createPastePayload(clipboard, offsetX, offsetY) {
136
+ const { nodes: clipboardNodes, edges: clipboardEdges } = clipboard;
137
+ const idMap = /* @__PURE__ */ new Map();
138
+ for (const node of clipboardNodes) {
139
+ idMap.set(node.id, nanoid(8));
140
+ }
141
+ const minX = Math.min(...clipboardNodes.map((n) => n.position.x));
142
+ const minY = Math.min(...clipboardNodes.map((n) => n.position.y));
143
+ const newNodes = clipboardNodes.map((node) => ({
144
+ ...node,
145
+ id: idMap.get(node.id),
146
+ position: {
147
+ x: node.position.x - minX + offsetX,
148
+ y: node.position.y - minY + offsetY
149
+ },
150
+ selected: true,
151
+ data: {
152
+ ...node.data,
153
+ status: "idle",
154
+ jobId: null,
155
+ error: void 0
156
+ }
157
+ }));
158
+ const newEdges = clipboardEdges.map((edge) => ({
159
+ ...edge,
160
+ id: nanoid(8),
161
+ source: idMap.get(edge.source),
162
+ target: idMap.get(edge.target)
163
+ }));
164
+ return { nodes: newNodes, edges: newEdges };
165
+ }
166
+ function useNodeActions() {
167
+ const nodes = useWorkflowStore(selectNodes);
168
+ const edges = useWorkflowStore(selectEdges);
169
+ const removeNode = useWorkflowStore(selectRemoveNode);
170
+ const duplicateNode = useWorkflowStore(selectDuplicateNode);
171
+ const [clipboard, setClipboard] = useState(null);
172
+ const deleteNode = useCallback(
173
+ (nodeId) => {
174
+ removeNode(nodeId);
175
+ },
176
+ [removeNode]
177
+ );
178
+ const duplicate = useCallback(
179
+ (nodeId) => {
180
+ return duplicateNode(nodeId);
181
+ },
182
+ [duplicateNode]
183
+ );
184
+ const copyNode = useCallback(
185
+ (nodeId) => {
186
+ const node = nodes.find((n) => n.id === nodeId);
187
+ if (node) {
188
+ setClipboard({ nodes: [node], edges: [], isCut: false });
189
+ }
190
+ },
191
+ [nodes]
192
+ );
193
+ const copyMultipleNodes = useCallback(
194
+ (nodeIds) => {
195
+ const nodeSet = new Set(nodeIds);
196
+ const nodesToCopy = nodes.filter((n) => nodeSet.has(n.id));
197
+ const edgesToCopy = edges.filter((e) => nodeSet.has(e.source) && nodeSet.has(e.target));
198
+ if (nodesToCopy.length > 0) {
199
+ setClipboard({ nodes: nodesToCopy, edges: edgesToCopy, isCut: false });
200
+ }
201
+ },
202
+ [nodes, edges]
203
+ );
204
+ const cutNode = useCallback(
205
+ (nodeId) => {
206
+ const node = nodes.find((n) => n.id === nodeId);
207
+ if (node) {
208
+ setClipboard({ nodes: [node], edges: [], isCut: true });
209
+ removeNode(nodeId);
210
+ }
211
+ },
212
+ [nodes, removeNode]
213
+ );
214
+ const cutMultipleNodes = useCallback(
215
+ (nodeIds) => {
216
+ const nodeSet = new Set(nodeIds);
217
+ const nodesToCut = nodes.filter((n) => nodeSet.has(n.id));
218
+ const edgesToCut = edges.filter((e) => nodeSet.has(e.source) && nodeSet.has(e.target));
219
+ if (nodesToCut.length > 0) {
220
+ setClipboard({ nodes: nodesToCut, edges: edgesToCut, isCut: true });
221
+ for (const nodeId of nodeIds) {
222
+ removeNode(nodeId);
223
+ }
224
+ }
225
+ },
226
+ [nodes, edges, removeNode]
227
+ );
228
+ const deleteMultipleNodes = useCallback(
229
+ (nodeIds) => {
230
+ for (const nodeId of nodeIds) {
231
+ removeNode(nodeId);
232
+ }
233
+ },
234
+ [removeNode]
235
+ );
236
+ const duplicateMultipleNodes = useCallback(
237
+ (nodeIds) => {
238
+ const newIds = [];
239
+ for (const nodeId of nodeIds) {
240
+ const newId = duplicateNode(nodeId);
241
+ if (newId) {
242
+ newIds.push(newId);
243
+ }
244
+ }
245
+ return newIds;
246
+ },
247
+ [duplicateNode]
248
+ );
249
+ const pasteNodes = useCallback(
250
+ (offsetX, offsetY) => {
251
+ if (!clipboard || clipboard.nodes.length === 0) return null;
252
+ const payload = createPastePayload(clipboard, offsetX, offsetY);
253
+ if (clipboard.isCut) {
254
+ setClipboard(null);
255
+ }
256
+ return {
257
+ nodeIds: payload.nodes.map((n) => n.id),
258
+ edgeIds: payload.edges.map((e) => e.id)
259
+ };
260
+ },
261
+ [clipboard]
262
+ );
263
+ const getPasteData = useCallback(
264
+ (offsetX, offsetY) => {
265
+ if (!clipboard || clipboard.nodes.length === 0) return null;
266
+ const payload = createPastePayload(clipboard, offsetX, offsetY);
267
+ if (clipboard.isCut) {
268
+ setClipboard(null);
269
+ }
270
+ return payload;
271
+ },
272
+ [clipboard]
273
+ );
274
+ return {
275
+ clipboard,
276
+ deleteNode,
277
+ duplicate,
278
+ copyNode,
279
+ copyMultipleNodes,
280
+ cutNode,
281
+ cutMultipleNodes,
282
+ deleteMultipleNodes,
283
+ duplicateMultipleNodes,
284
+ pasteNodes,
285
+ getPasteData
286
+ };
287
+ }
288
+ var DEFAULT_NODE_WIDTH = 280;
289
+ var DEFAULT_NODE_HEIGHT = 200;
290
+ var MIN_GAP = 40;
291
+ function getNodeRect(node) {
292
+ return {
293
+ x: node.position.x,
294
+ y: node.position.y,
295
+ width: node.measured?.width ?? DEFAULT_NODE_WIDTH,
296
+ height: node.measured?.height ?? DEFAULT_NODE_HEIGHT
297
+ };
298
+ }
299
+ function getInputHandleIndex(nodeType, handleId) {
300
+ const nodeDef = NODE_DEFINITIONS[nodeType];
301
+ if (!nodeDef) return 0;
302
+ const index = nodeDef.inputs.findIndex((h) => h.id === handleId);
303
+ return index === -1 ? 0 : index;
304
+ }
305
+ function rectsOverlap(a, b, gap) {
306
+ return a.x < b.x + b.width + gap && a.x + a.width + gap > b.x && a.y < b.y + b.height + gap && a.y + a.height + gap > b.y;
307
+ }
308
+ function getOverlap(a, b, gap) {
309
+ const aCenterX = a.x + a.width / 2;
310
+ const aCenterY = a.y + a.height / 2;
311
+ const bCenterX = b.x + b.width / 2;
312
+ const bCenterY = b.y + b.height / 2;
313
+ const dx = bCenterX - aCenterX;
314
+ const dy = bCenterY - aCenterY;
315
+ const minDistX = (a.width + b.width) / 2 + gap;
316
+ const minDistY = (a.height + b.height) / 2 + gap;
317
+ const overlapX = minDistX - Math.abs(dx);
318
+ const overlapY = minDistY - Math.abs(dy);
319
+ return { x: overlapX, y: overlapY };
320
+ }
321
+ function reorderByHandlePosition(nodes, edges) {
322
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
323
+ const edgesByTarget = /* @__PURE__ */ new Map();
324
+ for (const edge of edges) {
325
+ const existing = edgesByTarget.get(edge.target) ?? [];
326
+ existing.push(edge);
327
+ edgesByTarget.set(edge.target, existing);
328
+ }
329
+ for (const [targetId, targetEdges] of edgesByTarget) {
330
+ if (targetEdges.length < 2) continue;
331
+ const targetNode = nodeMap.get(targetId);
332
+ if (!targetNode) continue;
333
+ const sourceInfos = [];
334
+ for (const edge of targetEdges) {
335
+ const sourceNode = nodeMap.get(edge.source);
336
+ if (!sourceNode) continue;
337
+ const handleIndex = getInputHandleIndex(targetNode.type, edge.targetHandle ?? "");
338
+ sourceInfos.push({
339
+ nodeId: edge.source,
340
+ handleIndex,
341
+ currentY: sourceNode.position.y
342
+ });
343
+ }
344
+ if (sourceInfos.length < 2) continue;
345
+ sourceInfos.sort((a, b) => a.handleIndex - b.handleIndex);
346
+ const sortedYPositions = sourceInfos.map((s) => s.currentY).sort((a, b) => a - b);
347
+ for (let i = 0; i < sourceInfos.length; i++) {
348
+ const node = nodeMap.get(sourceInfos[i].nodeId);
349
+ if (node) {
350
+ node.position.y = sortedYPositions[i];
351
+ }
352
+ }
353
+ }
354
+ return nodes;
355
+ }
356
+ function resolveCollisions(nodes) {
357
+ const maxIterations = 100;
358
+ let iteration = 0;
359
+ let hasCollision = true;
360
+ while (hasCollision && iteration < maxIterations) {
361
+ hasCollision = false;
362
+ iteration++;
363
+ for (let i = 0; i < nodes.length; i++) {
364
+ for (let j = i + 1; j < nodes.length; j++) {
365
+ const nodeA = nodes[i];
366
+ const nodeB = nodes[j];
367
+ const rectA = getNodeRect(nodeA);
368
+ const rectB = getNodeRect(nodeB);
369
+ if (rectsOverlap(rectA, rectB, MIN_GAP)) {
370
+ hasCollision = true;
371
+ const overlap = getOverlap(rectA, rectB, MIN_GAP);
372
+ const aCenterX = rectA.x + rectA.width / 2;
373
+ const aCenterY = rectA.y + rectA.height / 2;
374
+ const bCenterX = rectB.x + rectB.width / 2;
375
+ const bCenterY = rectB.y + rectB.height / 2;
376
+ if (overlap.x < overlap.y && overlap.x > 0) {
377
+ const pushX = overlap.x / 2;
378
+ if (bCenterX >= aCenterX) {
379
+ nodeA.position.x -= pushX;
380
+ nodeB.position.x += pushX;
381
+ } else {
382
+ nodeA.position.x += pushX;
383
+ nodeB.position.x -= pushX;
384
+ }
385
+ } else if (overlap.y > 0) {
386
+ const pushY = overlap.y / 2;
387
+ if (bCenterY >= aCenterY) {
388
+ nodeA.position.y -= pushY;
389
+ nodeB.position.y += pushY;
390
+ } else {
391
+ nodeA.position.y += pushY;
392
+ nodeB.position.y -= pushY;
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ return nodes;
400
+ }
401
+ function getLayoutedNodes(nodes, edges, options = {}) {
402
+ if (nodes.length === 0) return nodes;
403
+ const { direction = "LR", nodeSpacing = 50, rankSpacing = 120 } = options;
404
+ const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
405
+ graph.setGraph({
406
+ rankdir: direction,
407
+ nodesep: nodeSpacing,
408
+ ranksep: rankSpacing,
409
+ marginx: 50,
410
+ marginy: 50,
411
+ ranker: "network-simplex",
412
+ acyclicer: "greedy"
413
+ });
414
+ for (const node of nodes) {
415
+ const width = node.measured?.width ?? DEFAULT_NODE_WIDTH;
416
+ const height = node.measured?.height ?? DEFAULT_NODE_HEIGHT;
417
+ graph.setNode(node.id, { width, height });
418
+ }
419
+ for (const edge of edges) {
420
+ graph.setEdge(edge.source, edge.target);
421
+ }
422
+ Dagre.layout(graph);
423
+ const layoutedNodes = nodes.map((node) => {
424
+ const nodeWithPosition = graph.node(node.id);
425
+ const width = node.measured?.width ?? DEFAULT_NODE_WIDTH;
426
+ const height = node.measured?.height ?? DEFAULT_NODE_HEIGHT;
427
+ return {
428
+ ...node,
429
+ position: {
430
+ x: nodeWithPosition.x - width / 2,
431
+ y: nodeWithPosition.y - height / 2
432
+ }
433
+ };
434
+ });
435
+ reorderByHandlePosition(layoutedNodes, edges);
436
+ return resolveCollisions(layoutedNodes);
437
+ }
438
+
439
+ // src/hooks/usePaneActions.ts
440
+ function usePaneActions() {
441
+ const addNode = useWorkflowStore(selectAddNode);
442
+ const reactFlow = useReactFlow();
443
+ const addNodeAtPosition = useCallback(
444
+ (type, screenX, screenY) => {
445
+ const position = reactFlow.screenToFlowPosition({ x: screenX, y: screenY });
446
+ addNode(type, position);
447
+ },
448
+ [addNode, reactFlow]
449
+ );
450
+ const selectAll = useCallback(() => {
451
+ reactFlow.setNodes(
452
+ (nds) => nds.map((node) => ({
453
+ ...node,
454
+ selected: true
455
+ }))
456
+ );
457
+ }, [reactFlow]);
458
+ const fitView = useCallback(() => {
459
+ reactFlow.fitView({ padding: 0.2 });
460
+ }, [reactFlow]);
461
+ const autoLayout = useCallback(
462
+ (direction = "LR") => {
463
+ const edgeStyle = useSettingsStore.getState().edgeStyle;
464
+ const currentNodes = reactFlow.getNodes();
465
+ const currentEdges = reactFlow.getEdges();
466
+ const layoutedNodes = getLayoutedNodes(currentNodes, currentEdges, { direction });
467
+ reactFlow.setNodes(layoutedNodes);
468
+ reactFlow.setEdges(
469
+ (eds) => eds.map((edge) => ({
470
+ ...edge,
471
+ type: edgeStyle
472
+ }))
473
+ );
474
+ setTimeout(() => {
475
+ reactFlow.fitView({ padding: 0.2 });
476
+ }, 50);
477
+ },
478
+ [reactFlow]
479
+ );
480
+ return {
481
+ addNodeAtPosition,
482
+ selectAll,
483
+ fitView,
484
+ autoLayout
485
+ };
486
+ }
487
+ function getEdgeMenuItems({ edgeId, onDelete }) {
488
+ return [
489
+ {
490
+ id: "delete",
491
+ label: "Delete Connection",
492
+ icon: /* @__PURE__ */ jsx(Trash2, { className: "w-4 h-4" }),
493
+ danger: true,
494
+ onClick: () => onDelete(edgeId)
495
+ }
496
+ ];
497
+ }
498
+ function ContextMenuSeparator() {
499
+ return /* @__PURE__ */ jsx("div", { className: "h-px bg-[var(--border)] my-1" });
500
+ }
501
+ function ContextMenuItem({
502
+ label,
503
+ icon,
504
+ shortcut,
505
+ disabled,
506
+ danger,
507
+ submenu,
508
+ onClick,
509
+ onClose,
510
+ isSelected
511
+ }) {
512
+ const [showSubmenu, setShowSubmenu] = useState(false);
513
+ const itemRef = useRef(null);
514
+ const timeoutRef = useRef(null);
515
+ const handleMouseEnter = useCallback(() => {
516
+ if (submenu && !disabled) {
517
+ if (timeoutRef.current) {
518
+ clearTimeout(timeoutRef.current);
519
+ }
520
+ setShowSubmenu(true);
521
+ }
522
+ }, [submenu, disabled]);
523
+ const handleMouseLeave = useCallback(() => {
524
+ if (submenu) {
525
+ timeoutRef.current = setTimeout(() => {
526
+ setShowSubmenu(false);
527
+ }, 100);
528
+ }
529
+ }, [submenu]);
530
+ const handleSubmenuClick = useCallback(
531
+ (item) => {
532
+ if (!item.disabled && item.onClick) {
533
+ item.onClick();
534
+ onClose?.();
535
+ }
536
+ },
537
+ [onClose]
538
+ );
539
+ const hasSubmenu = submenu && submenu.length > 0;
540
+ return /* @__PURE__ */ jsxs(
541
+ "div",
542
+ {
543
+ ref: itemRef,
544
+ className: "relative",
545
+ onMouseEnter: handleMouseEnter,
546
+ onMouseLeave: handleMouseLeave,
547
+ children: [
548
+ /* @__PURE__ */ jsxs(
549
+ "button",
550
+ {
551
+ onClick: hasSubmenu ? void 0 : onClick,
552
+ disabled,
553
+ className: `
554
+ w-full flex items-center gap-3 px-3 py-2 text-left text-sm rounded-md
555
+ transition-colors outline-none
556
+ ${isSelected || showSubmenu ? "bg-[var(--secondary)]" : ""}
557
+ ${disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-[var(--secondary)] cursor-pointer"}
558
+ ${danger && !disabled ? "text-red-400 hover:text-red-300" : "text-[var(--foreground)]"}
559
+ `,
560
+ children: [
561
+ icon && /* @__PURE__ */ jsx(
562
+ "span",
563
+ {
564
+ className: `w-4 h-4 flex items-center justify-center ${danger ? "text-red-400" : "text-[var(--muted-foreground)]"}`,
565
+ children: icon
566
+ }
567
+ ),
568
+ label && /* @__PURE__ */ jsx("span", { className: "flex-1", children: label }),
569
+ shortcut && !hasSubmenu && /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--muted-foreground)] ml-4", children: shortcut }),
570
+ hasSubmenu && /* @__PURE__ */ jsx(ChevronRight, { className: "w-4 h-4 text-[var(--muted-foreground)]" })
571
+ ]
572
+ }
573
+ ),
574
+ hasSubmenu && showSubmenu && /* @__PURE__ */ jsx(
575
+ "div",
576
+ {
577
+ className: "absolute left-full top-0 ml-1 min-w-[200px] py-1 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg backdrop-blur-sm z-50",
578
+ onMouseEnter: handleMouseEnter,
579
+ onMouseLeave: handleMouseLeave,
580
+ children: submenu.map((item) => {
581
+ if (item.separator) {
582
+ return /* @__PURE__ */ jsx(ContextMenuSeparator, {}, item.id);
583
+ }
584
+ return /* @__PURE__ */ jsxs(
585
+ "button",
586
+ {
587
+ onClick: () => handleSubmenuClick(item),
588
+ disabled: item.disabled,
589
+ className: `
590
+ w-full flex items-center gap-3 px-3 py-2 text-left text-sm rounded-md
591
+ transition-colors outline-none
592
+ ${item.disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-[var(--secondary)] cursor-pointer"}
593
+ ${item.danger && !item.disabled ? "text-red-400 hover:text-red-300" : "text-[var(--foreground)]"}
594
+ `,
595
+ children: [
596
+ item.icon && /* @__PURE__ */ jsx(
597
+ "span",
598
+ {
599
+ className: `w-4 h-4 flex items-center justify-center ${item.danger ? "text-red-400" : "text-[var(--muted-foreground)]"}`,
600
+ children: item.icon
601
+ }
602
+ ),
603
+ item.label && /* @__PURE__ */ jsx("span", { className: "flex-1", children: item.label }),
604
+ item.shortcut && /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--muted-foreground)] ml-4", children: item.shortcut })
605
+ ]
606
+ },
607
+ item.id
608
+ );
609
+ })
610
+ }
611
+ )
612
+ ]
613
+ }
614
+ );
615
+ }
616
+ function createSeparator(id) {
617
+ return { id, separator: true };
618
+ }
619
+ function ContextMenu({ x, y, items, onClose }) {
620
+ const menuRef = useRef(null);
621
+ const [selectedIndex, setSelectedIndex] = useState(-1);
622
+ const [position, setPosition] = useState({ x, y });
623
+ useEffect(() => {
624
+ if (menuRef.current) {
625
+ const rect = menuRef.current.getBoundingClientRect();
626
+ const padding = 8;
627
+ let newX = x;
628
+ let newY = y;
629
+ if (x + rect.width + padding > window.innerWidth) {
630
+ newX = x - rect.width;
631
+ }
632
+ if (y + rect.height + padding > window.innerHeight) {
633
+ newY = y - rect.height;
634
+ }
635
+ newX = Math.max(padding, newX);
636
+ newY = Math.max(padding, newY);
637
+ setPosition({ x: newX, y: newY });
638
+ }
639
+ }, [x, y]);
640
+ useEffect(() => {
641
+ const handleClickOutside = (event) => {
642
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
643
+ onClose();
644
+ }
645
+ };
646
+ const timeoutId = setTimeout(() => {
647
+ document.addEventListener("mousedown", handleClickOutside);
648
+ }, 0);
649
+ return () => {
650
+ clearTimeout(timeoutId);
651
+ document.removeEventListener("mousedown", handleClickOutside);
652
+ };
653
+ }, [onClose]);
654
+ useEffect(() => {
655
+ const handleKeyDown = (event) => {
656
+ const selectableIndices = items.map((item, index) => !item.separator && !item.disabled ? index : -1).filter((index) => index !== -1);
657
+ switch (event.key) {
658
+ case "Escape":
659
+ event.preventDefault();
660
+ onClose();
661
+ break;
662
+ case "ArrowDown":
663
+ event.preventDefault();
664
+ if (selectableIndices.length > 0) {
665
+ const currentSelectableIndex = selectableIndices.indexOf(selectedIndex);
666
+ const nextIndex = currentSelectableIndex === -1 ? 0 : (currentSelectableIndex + 1) % selectableIndices.length;
667
+ setSelectedIndex(selectableIndices[nextIndex]);
668
+ }
669
+ break;
670
+ case "ArrowUp":
671
+ event.preventDefault();
672
+ if (selectableIndices.length > 0) {
673
+ const currentSelectableIndex = selectableIndices.indexOf(selectedIndex);
674
+ const prevIndex = currentSelectableIndex === -1 ? selectableIndices.length - 1 : (currentSelectableIndex - 1 + selectableIndices.length) % selectableIndices.length;
675
+ setSelectedIndex(selectableIndices[prevIndex]);
676
+ }
677
+ break;
678
+ case "Enter":
679
+ event.preventDefault();
680
+ if (selectedIndex >= 0 && selectedIndex < items.length) {
681
+ const item = items[selectedIndex];
682
+ if (!item.separator && !item.disabled && item.onClick) {
683
+ item.onClick();
684
+ onClose();
685
+ }
686
+ }
687
+ break;
688
+ }
689
+ };
690
+ document.addEventListener("keydown", handleKeyDown);
691
+ return () => document.removeEventListener("keydown", handleKeyDown);
692
+ }, [items, selectedIndex, onClose]);
693
+ const handleItemClick = useCallback(
694
+ (item) => {
695
+ if (!item.disabled && item.onClick) {
696
+ item.onClick();
697
+ onClose();
698
+ }
699
+ },
700
+ [onClose]
701
+ );
702
+ return /* @__PURE__ */ jsx(
703
+ "div",
704
+ {
705
+ ref: menuRef,
706
+ className: "fixed z-50 min-w-[200px] py-1 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg backdrop-blur-sm",
707
+ style: { left: position.x, top: position.y },
708
+ children: items.map((item, index) => {
709
+ if (item.separator) {
710
+ return /* @__PURE__ */ jsx(ContextMenuSeparator, {}, item.id);
711
+ }
712
+ return /* @__PURE__ */ jsx(
713
+ ContextMenuItem,
714
+ {
715
+ id: item.id,
716
+ label: item.label,
717
+ icon: item.icon,
718
+ shortcut: item.shortcut,
719
+ disabled: item.disabled,
720
+ danger: item.danger,
721
+ submenu: item.submenu,
722
+ onClick: () => handleItemClick(item),
723
+ onClose,
724
+ isSelected: index === selectedIndex
725
+ },
726
+ item.id
727
+ );
728
+ })
729
+ }
730
+ );
731
+ }
732
+ var NODE_COLOR_VALUES = {
733
+ none: null,
734
+ purple: "#a855f7",
735
+ blue: "#3b82f6",
736
+ green: "#22c55e",
737
+ yellow: "#eab308",
738
+ orange: "#f97316",
739
+ red: "#ef4444",
740
+ pink: "#ec4899",
741
+ gray: "#6b7280"
742
+ };
743
+ var NODE_COLOR_LABELS = {
744
+ none: "Default",
745
+ purple: "Purple",
746
+ blue: "Blue",
747
+ green: "Green",
748
+ yellow: "Yellow",
749
+ orange: "Orange",
750
+ red: "Red",
751
+ pink: "Pink",
752
+ gray: "Gray"
753
+ };
754
+ var NODE_COLORS = ["none", "purple", "blue", "green", "yellow", "orange", "red", "pink", "gray"];
755
+ function getNodeMenuItems({
756
+ nodeId,
757
+ isLocked,
758
+ hasMediaOutput,
759
+ currentColor,
760
+ onDuplicate,
761
+ onLock,
762
+ onUnlock,
763
+ onCut,
764
+ onCopy,
765
+ onDelete,
766
+ onSetAsThumbnail,
767
+ onSetColor
768
+ }) {
769
+ const items = [
770
+ {
771
+ id: "duplicate",
772
+ label: "Duplicate",
773
+ icon: /* @__PURE__ */ jsx(Copy, { className: "w-4 h-4" }),
774
+ shortcut: "\u2318D",
775
+ onClick: () => onDuplicate(nodeId)
776
+ }
777
+ ];
778
+ if (hasMediaOutput && onSetAsThumbnail) {
779
+ items.push({
780
+ id: "setThumbnail",
781
+ label: "Set as Thumbnail",
782
+ icon: /* @__PURE__ */ jsx(Image, { className: "w-4 h-4" }),
783
+ onClick: () => onSetAsThumbnail(nodeId)
784
+ });
785
+ }
786
+ if (onSetColor) {
787
+ const colorSubmenu = NODE_COLORS.map((color) => {
788
+ const colorValue = NODE_COLOR_VALUES[color];
789
+ const isSelected = colorValue === currentColor || color === "none" && !currentColor;
790
+ return {
791
+ id: `color-${color}`,
792
+ label: `${isSelected ? "\u2713 " : ""}${NODE_COLOR_LABELS[color]}`,
793
+ icon: /* @__PURE__ */ jsx(
794
+ "div",
795
+ {
796
+ className: "w-4 h-4 rounded-sm border border-border",
797
+ style: { backgroundColor: colorValue || "transparent" }
798
+ }
799
+ ),
800
+ onClick: () => onSetColor(nodeId, colorValue)
801
+ };
802
+ });
803
+ items.push({
804
+ id: "setColor",
805
+ label: "Set Color",
806
+ icon: /* @__PURE__ */ jsx(Palette, { className: "w-4 h-4" }),
807
+ submenu: colorSubmenu
808
+ });
809
+ }
810
+ items.push(createSeparator("separator-1"));
811
+ items.push(
812
+ isLocked ? {
813
+ id: "unlock",
814
+ label: "Unlock Node",
815
+ icon: /* @__PURE__ */ jsx(LockOpen, { className: "w-4 h-4" }),
816
+ shortcut: "L",
817
+ onClick: () => onUnlock(nodeId)
818
+ } : {
819
+ id: "lock",
820
+ label: "Lock Node",
821
+ icon: /* @__PURE__ */ jsx(Lock, { className: "w-4 h-4" }),
822
+ shortcut: "L",
823
+ onClick: () => onLock(nodeId)
824
+ }
825
+ );
826
+ items.push(createSeparator("separator-2"));
827
+ items.push(
828
+ {
829
+ id: "cut",
830
+ label: "Cut",
831
+ icon: /* @__PURE__ */ jsx(Scissors, { className: "w-4 h-4" }),
832
+ shortcut: "\u2318X",
833
+ onClick: () => onCut(nodeId)
834
+ },
835
+ {
836
+ id: "copy",
837
+ label: "Copy",
838
+ icon: /* @__PURE__ */ jsx(Copy, { className: "w-4 h-4" }),
839
+ shortcut: "\u2318C",
840
+ onClick: () => onCopy(nodeId)
841
+ }
842
+ );
843
+ items.push(createSeparator("separator-3"));
844
+ items.push({
845
+ id: "delete",
846
+ label: "Delete",
847
+ icon: /* @__PURE__ */ jsx(Trash2, { className: "w-4 h-4" }),
848
+ shortcut: "\u232B",
849
+ danger: true,
850
+ onClick: () => onDelete(nodeId)
851
+ });
852
+ return items;
853
+ }
854
+ var NODE_ICONS = {
855
+ Image: Image,
856
+ MessageSquare,
857
+ FileText,
858
+ Volume2,
859
+ FileVideo,
860
+ Sparkles,
861
+ Video,
862
+ Brain,
863
+ Mic,
864
+ AudioLines,
865
+ Navigation,
866
+ Maximize2,
867
+ Wand2,
868
+ Layers,
869
+ Scissors: Scissors,
870
+ Film,
871
+ Crop,
872
+ Maximize,
873
+ Grid3X3,
874
+ Pencil,
875
+ Subtitles,
876
+ CheckCircle,
877
+ ArrowRightToLine,
878
+ ArrowLeftFromLine,
879
+ GitBranch
880
+ };
881
+ var CATEGORY_LABELS = {
882
+ input: "Input",
883
+ ai: "AI Generation",
884
+ processing: "Processing",
885
+ output: "Output",
886
+ composition: "Composition"
887
+ };
888
+ var CATEGORY_ICONS = {
889
+ input: Image,
890
+ ai: Sparkles,
891
+ processing: Wand2,
892
+ output: Monitor,
893
+ composition: GitBranch
894
+ };
895
+ function getPaneMenuItems({
896
+ screenX,
897
+ screenY,
898
+ hasClipboard,
899
+ onAddNode,
900
+ onPaste,
901
+ onSelectAll,
902
+ onFitView,
903
+ onAutoLayout
904
+ }) {
905
+ const nodesByCategory = getNodesByCategory();
906
+ const categories = ["input", "ai", "processing", "output", "composition"].filter((cat) => nodesByCategory[cat].length > 0);
907
+ const addNodeItems = categories.map((category) => {
908
+ const CategoryIcon = CATEGORY_ICONS[category];
909
+ const nodes = nodesByCategory[category];
910
+ return {
911
+ id: `add-${category}`,
912
+ label: CATEGORY_LABELS[category],
913
+ icon: /* @__PURE__ */ jsx(CategoryIcon, { className: "w-4 h-4" }),
914
+ submenu: nodes.map((node) => {
915
+ const NodeIcon = NODE_ICONS[node.icon] ?? Sparkles;
916
+ return {
917
+ id: `add-${node.type}`,
918
+ label: node.label,
919
+ icon: /* @__PURE__ */ jsx(NodeIcon, { className: "w-4 h-4" }),
920
+ onClick: () => onAddNode(node.type, screenX, screenY)
921
+ };
922
+ })
923
+ };
924
+ });
925
+ return [
926
+ ...addNodeItems,
927
+ createSeparator("separator-1"),
928
+ {
929
+ id: "paste",
930
+ label: "Paste",
931
+ icon: /* @__PURE__ */ jsx(Clipboard, { className: "w-4 h-4" }),
932
+ shortcut: "\u2318V",
933
+ disabled: !hasClipboard,
934
+ onClick: onPaste
935
+ },
936
+ createSeparator("separator-2"),
937
+ {
938
+ id: "select-all",
939
+ label: "Select All",
940
+ shortcut: "\u2318A",
941
+ onClick: onSelectAll
942
+ },
943
+ {
944
+ id: "fit-view",
945
+ label: "Fit View",
946
+ icon: /* @__PURE__ */ jsx(Maximize, { className: "w-4 h-4" }),
947
+ shortcut: "F",
948
+ onClick: onFitView
949
+ },
950
+ {
951
+ id: "auto-layout",
952
+ label: "Auto-layout Nodes",
953
+ icon: /* @__PURE__ */ jsx(LayoutGrid, { className: "w-4 h-4" }),
954
+ shortcut: "L",
955
+ onClick: onAutoLayout
956
+ }
957
+ ];
958
+ }
959
+ function getSelectionMenuItems({
960
+ nodeIds,
961
+ onGroup,
962
+ onDuplicateAll,
963
+ onLockAll,
964
+ onUnlockAll,
965
+ onAlignHorizontal,
966
+ onAlignVertical,
967
+ onDeleteAll
968
+ }) {
969
+ const count = nodeIds.length;
970
+ return [
971
+ {
972
+ id: "group",
973
+ label: "Create Group",
974
+ icon: /* @__PURE__ */ jsx(Group, { className: "w-4 h-4" }),
975
+ shortcut: "\u2318G",
976
+ onClick: () => onGroup(nodeIds)
977
+ },
978
+ {
979
+ id: "duplicate-all",
980
+ label: `Duplicate ${count} Nodes`,
981
+ icon: /* @__PURE__ */ jsx(Copy, { className: "w-4 h-4" }),
982
+ shortcut: "\u2318D",
983
+ onClick: () => onDuplicateAll(nodeIds)
984
+ },
985
+ createSeparator("separator-1"),
986
+ {
987
+ id: "lock-all",
988
+ label: "Lock All",
989
+ icon: /* @__PURE__ */ jsx(Lock, { className: "w-4 h-4" }),
990
+ shortcut: "L",
991
+ onClick: () => onLockAll(nodeIds)
992
+ },
993
+ {
994
+ id: "unlock-all",
995
+ label: "Unlock All",
996
+ icon: /* @__PURE__ */ jsx(LockOpen, { className: "w-4 h-4" }),
997
+ onClick: () => onUnlockAll(nodeIds)
998
+ },
999
+ createSeparator("separator-2"),
1000
+ {
1001
+ id: "align-horizontal",
1002
+ label: "Align Horizontally",
1003
+ icon: /* @__PURE__ */ jsx(AlignHorizontalJustifyCenter, { className: "w-4 h-4" }),
1004
+ onClick: () => onAlignHorizontal(nodeIds)
1005
+ },
1006
+ {
1007
+ id: "align-vertical",
1008
+ label: "Align Vertically",
1009
+ icon: /* @__PURE__ */ jsx(AlignVerticalJustifyCenter, { className: "w-4 h-4" }),
1010
+ onClick: () => onAlignVertical(nodeIds)
1011
+ },
1012
+ createSeparator("separator-3"),
1013
+ {
1014
+ id: "delete-all",
1015
+ label: `Delete ${count} Nodes`,
1016
+ icon: /* @__PURE__ */ jsx(Trash2, { className: "w-4 h-4" }),
1017
+ shortcut: "\u232B",
1018
+ danger: true,
1019
+ onClick: () => onDeleteAll(nodeIds)
1020
+ }
1021
+ ];
1022
+ }
1023
+
1024
+ // src/hooks/useContextMenu.ts
1025
+ function useContextMenu() {
1026
+ const {
1027
+ isOpen,
1028
+ position,
1029
+ menuType,
1030
+ targetId,
1031
+ targetIds,
1032
+ openNodeMenu,
1033
+ openEdgeMenu,
1034
+ openPaneMenu,
1035
+ openSelectionMenu,
1036
+ close
1037
+ } = useContextMenuStore();
1038
+ const { workflowsApi } = useWorkflowUIConfig();
1039
+ const nodes = useWorkflowStore(selectNodes);
1040
+ const removeEdge = useWorkflowStore((state) => state.removeEdge);
1041
+ const toggleNodeLock = useWorkflowStore(selectToggleNodeLock);
1042
+ const createGroup = useWorkflowStore(selectCreateGroup);
1043
+ const workflowId = useWorkflowStore(selectWorkflowId);
1044
+ const addNodesAndEdges = useWorkflowStore((state) => state.addNodesAndEdges);
1045
+ const setSelectedNodeIds = useWorkflowStore(selectSetSelectedNodeIds);
1046
+ const updateNodeData = useWorkflowStore(selectUpdateNodeData);
1047
+ const {
1048
+ clipboard,
1049
+ deleteNode,
1050
+ duplicate,
1051
+ copyNode,
1052
+ cutNode,
1053
+ deleteMultipleNodes,
1054
+ duplicateMultipleNodes,
1055
+ getPasteData
1056
+ } = useNodeActions();
1057
+ const { addNodeAtPosition, selectAll, fitView, autoLayout } = usePaneActions();
1058
+ const reactFlow = useReactFlow();
1059
+ const stableHandlersRef = useRef({
1060
+ duplicate,
1061
+ copyNode,
1062
+ cutNode,
1063
+ deleteNode,
1064
+ deleteMultipleNodes,
1065
+ duplicateMultipleNodes,
1066
+ removeEdge,
1067
+ addNodeAtPosition,
1068
+ selectAll,
1069
+ fitView,
1070
+ autoLayout
1071
+ });
1072
+ stableHandlersRef.current = {
1073
+ duplicate,
1074
+ copyNode,
1075
+ cutNode,
1076
+ deleteNode,
1077
+ deleteMultipleNodes,
1078
+ duplicateMultipleNodes,
1079
+ removeEdge,
1080
+ addNodeAtPosition,
1081
+ selectAll,
1082
+ fitView,
1083
+ autoLayout
1084
+ };
1085
+ const lockNode = useCallback(
1086
+ (nodeId) => {
1087
+ const node = nodes.find((n) => n.id === nodeId);
1088
+ if (node && !node.data.locked) {
1089
+ toggleNodeLock(nodeId);
1090
+ }
1091
+ },
1092
+ [nodes, toggleNodeLock]
1093
+ );
1094
+ const unlockNode = useCallback(
1095
+ (nodeId) => {
1096
+ const node = nodes.find((n) => n.id === nodeId);
1097
+ if (node?.data.locked) {
1098
+ toggleNodeLock(nodeId);
1099
+ }
1100
+ },
1101
+ [nodes, toggleNodeLock]
1102
+ );
1103
+ const groupNodes = useCallback(
1104
+ (nodeIds) => {
1105
+ if (nodeIds.length > 1) {
1106
+ createGroup(nodeIds);
1107
+ }
1108
+ },
1109
+ [createGroup]
1110
+ );
1111
+ const lockAllNodes = useCallback(
1112
+ (nodeIds) => {
1113
+ for (const nodeId of nodeIds) {
1114
+ const node = nodes.find((n) => n.id === nodeId);
1115
+ if (node && !node.data.locked) {
1116
+ toggleNodeLock(nodeId);
1117
+ }
1118
+ }
1119
+ },
1120
+ [nodes, toggleNodeLock]
1121
+ );
1122
+ const unlockAllNodes = useCallback(
1123
+ (nodeIds) => {
1124
+ for (const nodeId of nodeIds) {
1125
+ const node = nodes.find((n) => n.id === nodeId);
1126
+ if (node?.data.locked) {
1127
+ toggleNodeLock(nodeId);
1128
+ }
1129
+ }
1130
+ },
1131
+ [nodes, toggleNodeLock]
1132
+ );
1133
+ const alignNodesHorizontally = useCallback(
1134
+ (nodeIds) => {
1135
+ if (nodeIds.length < 2) return;
1136
+ const selectedNodes = nodes.filter((n) => nodeIds.includes(n.id));
1137
+ if (selectedNodes.length < 2) return;
1138
+ const avgY = selectedNodes.reduce((sum, n) => sum + n.position.y, 0) / selectedNodes.length;
1139
+ reactFlow.setNodes(
1140
+ (nds) => nds.map(
1141
+ (node) => nodeIds.includes(node.id) ? { ...node, position: { ...node.position, y: avgY } } : node
1142
+ )
1143
+ );
1144
+ },
1145
+ [nodes, reactFlow]
1146
+ );
1147
+ const alignNodesVertically = useCallback(
1148
+ (nodeIds) => {
1149
+ if (nodeIds.length < 2) return;
1150
+ const selectedNodes = nodes.filter((n) => nodeIds.includes(n.id));
1151
+ if (selectedNodes.length < 2) return;
1152
+ const avgX = selectedNodes.reduce((sum, n) => sum + n.position.x, 0) / selectedNodes.length;
1153
+ reactFlow.setNodes(
1154
+ (nds) => nds.map(
1155
+ (node) => nodeIds.includes(node.id) ? { ...node, position: { ...node.position, x: avgX } } : node
1156
+ )
1157
+ );
1158
+ },
1159
+ [nodes, reactFlow]
1160
+ );
1161
+ const pasteNodes = useCallback(() => {
1162
+ if (!clipboard) return;
1163
+ const flowPosition = reactFlow.screenToFlowPosition({
1164
+ x: position.x,
1165
+ y: position.y
1166
+ });
1167
+ const pasteData = getPasteData(flowPosition.x, flowPosition.y);
1168
+ if (!pasteData) return;
1169
+ addNodesAndEdges(pasteData.nodes, pasteData.edges);
1170
+ setSelectedNodeIds(pasteData.nodes.map((n) => n.id));
1171
+ }, [clipboard, position, reactFlow, getPasteData, addNodesAndEdges, setSelectedNodeIds]);
1172
+ const setNodeColor = useCallback(
1173
+ (nodeId, color) => {
1174
+ updateNodeData(nodeId, { color: color || void 0 });
1175
+ },
1176
+ [updateNodeData]
1177
+ );
1178
+ const setAsThumbnail = useCallback(
1179
+ async (nodeId) => {
1180
+ if (!workflowId || !workflowsApi) return;
1181
+ const node = nodes.find((n) => n.id === nodeId);
1182
+ if (!node) return;
1183
+ const data = node.data;
1184
+ const thumbnailUrl = data.outputVideo || data.outputImage;
1185
+ if (!thumbnailUrl) return;
1186
+ try {
1187
+ await workflowsApi.setThumbnail(workflowId, thumbnailUrl, nodeId);
1188
+ } catch {
1189
+ }
1190
+ },
1191
+ [workflowId, workflowsApi, nodes]
1192
+ );
1193
+ const hasMediaOutput = useCallback(
1194
+ (nodeId) => {
1195
+ const node = nodes.find((n) => n.id === nodeId);
1196
+ if (!node) return false;
1197
+ const data = node.data;
1198
+ return Boolean(data.outputVideo || data.outputImage);
1199
+ },
1200
+ [nodes]
1201
+ );
1202
+ const getMenuItems = useCallback(() => {
1203
+ if (!menuType) return [];
1204
+ const handlers = stableHandlersRef.current;
1205
+ switch (menuType) {
1206
+ case "node": {
1207
+ if (!targetId) return [];
1208
+ const node = nodes.find((n) => n.id === targetId);
1209
+ const isLocked = Boolean(node?.data.locked);
1210
+ const nodeHasMediaOutput = hasMediaOutput(targetId);
1211
+ const currentColor = node?.data?.color;
1212
+ return getNodeMenuItems({
1213
+ nodeId: targetId,
1214
+ isLocked,
1215
+ hasMediaOutput: nodeHasMediaOutput,
1216
+ currentColor,
1217
+ onDuplicate: handlers.duplicate,
1218
+ onLock: lockNode,
1219
+ onUnlock: unlockNode,
1220
+ onCut: handlers.cutNode,
1221
+ onCopy: handlers.copyNode,
1222
+ onDelete: handlers.deleteNode,
1223
+ onSetAsThumbnail: workflowId && workflowsApi ? setAsThumbnail : void 0,
1224
+ onSetColor: setNodeColor
1225
+ });
1226
+ }
1227
+ case "edge":
1228
+ if (!targetId) return [];
1229
+ return getEdgeMenuItems({
1230
+ edgeId: targetId,
1231
+ onDelete: handlers.removeEdge
1232
+ });
1233
+ case "pane":
1234
+ return getPaneMenuItems({
1235
+ screenX: position.x,
1236
+ screenY: position.y,
1237
+ hasClipboard: !!clipboard,
1238
+ onAddNode: handlers.addNodeAtPosition,
1239
+ onPaste: pasteNodes,
1240
+ onSelectAll: handlers.selectAll,
1241
+ onFitView: handlers.fitView,
1242
+ onAutoLayout: () => handlers.autoLayout("LR")
1243
+ });
1244
+ case "selection":
1245
+ if (!targetIds || targetIds.length === 0) return [];
1246
+ return getSelectionMenuItems({
1247
+ nodeIds: targetIds,
1248
+ onGroup: groupNodes,
1249
+ onDuplicateAll: handlers.duplicateMultipleNodes,
1250
+ onLockAll: lockAllNodes,
1251
+ onUnlockAll: unlockAllNodes,
1252
+ onAlignHorizontal: alignNodesHorizontally,
1253
+ onAlignVertical: alignNodesVertically,
1254
+ onDeleteAll: handlers.deleteMultipleNodes
1255
+ });
1256
+ default:
1257
+ return [];
1258
+ }
1259
+ }, [
1260
+ menuType,
1261
+ targetId,
1262
+ targetIds,
1263
+ nodes,
1264
+ position,
1265
+ clipboard,
1266
+ lockNode,
1267
+ unlockNode,
1268
+ pasteNodes,
1269
+ groupNodes,
1270
+ lockAllNodes,
1271
+ unlockAllNodes,
1272
+ alignNodesHorizontally,
1273
+ alignNodesVertically,
1274
+ hasMediaOutput,
1275
+ setAsThumbnail,
1276
+ setNodeColor,
1277
+ workflowId,
1278
+ workflowsApi
1279
+ ]);
1280
+ const menuItems = useMemo(() => getMenuItems(), [getMenuItems]);
1281
+ return {
1282
+ isOpen,
1283
+ position,
1284
+ menuType,
1285
+ menuItems,
1286
+ openNodeMenu,
1287
+ openEdgeMenu,
1288
+ openPaneMenu,
1289
+ openSelectionMenu,
1290
+ close
1291
+ };
1292
+ }
1293
+
1294
+ export { ContextMenu, useCanvasKeyboardShortcuts, useContextMenu, useNodeActions, usePaneActions };