@genfeedai/workflow-ui 0.1.2 → 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 +32 -26
  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
@@ -1,18 +1,456 @@
1
- import { nodeTypes } from './chunk-E3YBVMYZ.mjs';
2
- import { getMediaFromNode } from './chunk-E323WAZG.mjs';
3
- import { useCanvasKeyboardShortcuts } from './chunk-IASLG6IA.mjs';
4
- import { Button } from './chunk-7SKSRSS7.mjs';
5
- import { usePromptEditorStore } from './chunk-CV4M7CNU.mjs';
6
- import { useUIStore, useSettingsStore, useExecutionStore } from './chunk-SQK4JDYY.mjs';
7
- import { useWorkflowStore } from './chunk-RIGVIEYB.mjs';
8
- import { useNodes, ViewportPortal, useViewport, useStore, getBezierPath, BaseEdge, useReactFlow, ReactFlow, SelectionMode, ConnectionMode, Background, BackgroundVariant, Controls, MiniMap } from '@xyflow/react';
9
- import { Pause, Palette, Lock, Unlock, Trash2, Search, X, Keyboard, PanelLeft, Download, ChevronLeft, ChevronRight, ZoomOut, ZoomIn } from 'lucide-react';
10
- import { memo, useMemo, useState, useCallback, useEffect, useRef } from 'react';
1
+ import { nodeTypes, NodeDetailModal } from './chunk-PANZDSP6.mjs';
2
+ import { calculateWorkflowCost, formatCost } from './chunk-JTPADIUO.mjs';
3
+ import { useContextMenu, useCanvasKeyboardShortcuts, ContextMenu } from './chunk-ZD2BADZO.mjs';
4
+ import { useUIStore, useSettingsStore, useExecutionStore } from './chunk-LT3ZJJL6.mjs';
5
+ import { useWorkflowStore, getHandleType } from './chunk-R727OFBR.mjs';
6
+ import { useReactFlow, getBezierPath, BaseEdge, useNodes, ViewportPortal, useViewport, useStore, ReactFlow, SelectionMode, ConnectionMode, Background, BackgroundVariant, Controls, MiniMap } from '@xyflow/react';
7
+ import { Plus, Search, Pause, Play, Trash2, AlignHorizontalSpaceAround, AlignVerticalSpaceAround, Grid3X3, Ungroup, Group, Palette, Lock, Unlock, X, Keyboard, PanelLeft, XCircle, AlertTriangle, CheckCircle, Info, Check, Copy, DollarSign } from 'lucide-react';
8
+ import { memo, useState, useRef, useMemo, useEffect, useCallback } from 'react';
11
9
  import '@xyflow/react/dist/style.css';
12
- import { NODE_DEFINITIONS } from '@genfeedai/types';
13
- import { clsx } from 'clsx';
10
+ import { CONNECTION_RULES, getNodesByCategory, NODE_DEFINITIONS } from '@genfeedai/types';
14
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
- import Image from 'next/image';
12
+ import { clsx } from 'clsx';
13
+ import { createPortal } from 'react-dom';
14
+
15
+ var CATEGORY_LABELS = {
16
+ input: "Input",
17
+ ai: "AI",
18
+ processing: "Processing",
19
+ output: "Output",
20
+ composition: "Composition"
21
+ };
22
+ var CATEGORY_ORDER = ["input", "ai", "processing", "output", "composition"];
23
+ function ConnectionDropMenuComponent() {
24
+ const { connectionDropMenu, closeConnectionDropMenu } = useUIStore();
25
+ const { addNode, findCompatibleHandle, onConnect } = useWorkflowStore();
26
+ const reactFlow = useReactFlow();
27
+ const [search, setSearch] = useState("");
28
+ const [selectedIndex, setSelectedIndex] = useState(0);
29
+ const inputRef = useRef(null);
30
+ const listRef = useRef(null);
31
+ const backdropRef = useRef(null);
32
+ const isOpen = connectionDropMenu !== null;
33
+ const compatibleNodes = useMemo(() => {
34
+ if (!connectionDropMenu) return [];
35
+ const sourceType = connectionDropMenu.sourceHandleType;
36
+ const compatibleTargetTypes = CONNECTION_RULES[sourceType] ?? [];
37
+ const nodesByCategory = getNodesByCategory();
38
+ const result = [];
39
+ for (const category of CATEGORY_ORDER) {
40
+ const defs = nodesByCategory[category] ?? [];
41
+ for (const def of defs) {
42
+ const hasCompatibleInput = def.inputs.some(
43
+ (input) => compatibleTargetTypes.includes(input.type)
44
+ );
45
+ if (hasCompatibleInput) {
46
+ result.push({
47
+ type: def.type,
48
+ label: def.label,
49
+ category
50
+ });
51
+ }
52
+ }
53
+ }
54
+ return result;
55
+ }, [connectionDropMenu]);
56
+ const filteredNodes = useMemo(() => {
57
+ if (!search.trim()) return compatibleNodes;
58
+ const query = search.toLowerCase();
59
+ return compatibleNodes.filter(
60
+ (n) => n.label.toLowerCase().includes(query) || n.type.toLowerCase().includes(query)
61
+ );
62
+ }, [compatibleNodes, search]);
63
+ const groupedNodes = useMemo(() => {
64
+ const grouped = {};
65
+ for (const node of filteredNodes) {
66
+ if (!grouped[node.category]) grouped[node.category] = [];
67
+ grouped[node.category].push(node);
68
+ }
69
+ return grouped;
70
+ }, [filteredNodes]);
71
+ useEffect(() => {
72
+ if (isOpen) {
73
+ setSearch("");
74
+ setSelectedIndex(0);
75
+ setTimeout(() => inputRef.current?.focus(), 0);
76
+ }
77
+ }, [isOpen]);
78
+ useEffect(() => {
79
+ setSelectedIndex(0);
80
+ }, [search]);
81
+ useEffect(() => {
82
+ if (listRef.current) {
83
+ const items = listRef.current.querySelectorAll("[data-node-item]");
84
+ const selected = items[selectedIndex];
85
+ selected?.scrollIntoView({ block: "nearest" });
86
+ }
87
+ }, [selectedIndex]);
88
+ const handleSelect = useCallback(
89
+ (nodeType) => {
90
+ if (!connectionDropMenu) return;
91
+ const position = reactFlow.screenToFlowPosition({
92
+ x: connectionDropMenu.screenPosition.x,
93
+ y: connectionDropMenu.screenPosition.y
94
+ });
95
+ const newNodeId = addNode(nodeType, position);
96
+ const compatibleHandle = findCompatibleHandle(
97
+ connectionDropMenu.sourceNodeId,
98
+ connectionDropMenu.sourceHandleId,
99
+ newNodeId
100
+ );
101
+ if (compatibleHandle) {
102
+ onConnect({
103
+ source: connectionDropMenu.sourceNodeId,
104
+ sourceHandle: connectionDropMenu.sourceHandleId,
105
+ target: newNodeId,
106
+ targetHandle: compatibleHandle
107
+ });
108
+ }
109
+ closeConnectionDropMenu();
110
+ },
111
+ [connectionDropMenu, reactFlow, addNode, findCompatibleHandle, onConnect, closeConnectionDropMenu]
112
+ );
113
+ const handleKeyDown = useCallback(
114
+ (e) => {
115
+ if (e.key === "ArrowDown") {
116
+ e.preventDefault();
117
+ setSelectedIndex((prev) => Math.min(prev + 1, filteredNodes.length - 1));
118
+ } else if (e.key === "ArrowUp") {
119
+ e.preventDefault();
120
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
121
+ } else if (e.key === "Enter" && filteredNodes[selectedIndex]) {
122
+ e.preventDefault();
123
+ handleSelect(filteredNodes[selectedIndex].type);
124
+ } else if (e.key === "Escape") {
125
+ e.preventDefault();
126
+ closeConnectionDropMenu();
127
+ }
128
+ },
129
+ [filteredNodes, selectedIndex, handleSelect, closeConnectionDropMenu]
130
+ );
131
+ const handleBackdropClick = (e) => {
132
+ if (e.target === backdropRef.current) {
133
+ closeConnectionDropMenu();
134
+ }
135
+ };
136
+ if (!isOpen || !connectionDropMenu) return null;
137
+ const menuStyle = {
138
+ position: "fixed",
139
+ left: connectionDropMenu.screenPosition.x,
140
+ top: connectionDropMenu.screenPosition.y,
141
+ zIndex: 50
142
+ };
143
+ let flatIndex = 0;
144
+ return /* @__PURE__ */ jsx("div", { ref: backdropRef, className: "fixed inset-0 z-40", onClick: handleBackdropClick, children: /* @__PURE__ */ jsxs(
145
+ "div",
146
+ {
147
+ style: menuStyle,
148
+ className: "bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-xl w-64 max-h-80 flex flex-col",
149
+ role: "dialog",
150
+ "aria-label": "Add Node",
151
+ children: [
152
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-[var(--border)]", children: [
153
+ /* @__PURE__ */ jsx(Plus, { className: "w-3.5 h-3.5 text-[var(--muted-foreground)]" }),
154
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: "Add Connected Node" })
155
+ ] }),
156
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-2", onKeyDown: handleKeyDown, children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
157
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--muted-foreground)]" }),
158
+ /* @__PURE__ */ jsx(
159
+ "input",
160
+ {
161
+ ref: inputRef,
162
+ type: "text",
163
+ placeholder: "Search compatible nodes...",
164
+ value: search,
165
+ onChange: (e) => setSearch(e.target.value),
166
+ className: "w-full pl-7 pr-2 py-1.5 text-xs bg-[var(--secondary)] border border-[var(--border)] rounded-md outline-none focus:ring-1 focus:ring-[var(--ring)]"
167
+ }
168
+ )
169
+ ] }) }),
170
+ /* @__PURE__ */ jsx(
171
+ "div",
172
+ {
173
+ ref: listRef,
174
+ className: "flex-1 overflow-y-auto px-1.5 pb-1.5",
175
+ onKeyDown: handleKeyDown,
176
+ children: filteredNodes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-[var(--muted-foreground)] text-xs py-4", children: "No compatible nodes found" }) : Object.entries(groupedNodes).map(([category, nodes]) => {
177
+ if (!nodes || nodes.length === 0) return null;
178
+ return /* @__PURE__ */ jsxs("div", { className: "mb-1", children: [
179
+ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-semibold text-[var(--muted-foreground)] uppercase tracking-wider px-2 py-1", children: CATEGORY_LABELS[category] }),
180
+ nodes.map((node) => {
181
+ const currentIndex = flatIndex++;
182
+ return /* @__PURE__ */ jsx(
183
+ "button",
184
+ {
185
+ "data-node-item": true,
186
+ onClick: () => handleSelect(node.type),
187
+ className: `w-full text-left px-2 py-1.5 rounded text-xs transition-colors ${currentIndex === selectedIndex ? "bg-[var(--primary)]/10 text-[var(--foreground)]" : "text-[var(--foreground)] hover:bg-[var(--secondary)]"}`,
188
+ children: node.label
189
+ },
190
+ node.type
191
+ );
192
+ })
193
+ ] }, category);
194
+ })
195
+ }
196
+ )
197
+ ]
198
+ }
199
+ ) });
200
+ }
201
+ var ConnectionDropMenu = memo(ConnectionDropMenuComponent);
202
+ var DATA_TYPE_COLORS = {
203
+ image: ["#3b82f6", "#60a5fa"],
204
+ // blue
205
+ video: ["#8b5cf6", "#a78bfa"],
206
+ // purple
207
+ text: ["#22c55e", "#4ade80"],
208
+ // green
209
+ audio: ["#f97316", "#fb923c"],
210
+ // orange
211
+ number: ["#6b7280", "#9ca3af"]
212
+ // gray
213
+ };
214
+ var DEFAULT_COLORS = ["#6b7280", "#9ca3af"];
215
+ function EditableEdgeComponent({
216
+ id,
217
+ sourceX,
218
+ sourceY,
219
+ targetX,
220
+ targetY,
221
+ sourcePosition,
222
+ targetPosition,
223
+ style = {},
224
+ markerEnd,
225
+ data,
226
+ selected
227
+ }) {
228
+ const [edgePath, labelX, labelY] = getBezierPath({
229
+ sourceX,
230
+ sourceY,
231
+ sourcePosition,
232
+ targetX,
233
+ targetY,
234
+ targetPosition
235
+ });
236
+ const hasPause = data?.hasPause === true;
237
+ const dataType = data?.dataType ?? null;
238
+ const [colorStart, colorEnd] = dataType ? DATA_TYPE_COLORS[dataType] ?? DEFAULT_COLORS : DEFAULT_COLORS;
239
+ const gradientId = `edge-gradient-${id}`;
240
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
241
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gradientId, x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [
242
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: colorStart }),
243
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: colorEnd })
244
+ ] }) }),
245
+ /* @__PURE__ */ jsx(
246
+ "path",
247
+ {
248
+ d: edgePath,
249
+ fill: "none",
250
+ stroke: "transparent",
251
+ strokeWidth: 20,
252
+ className: "react-flow__edge-interaction"
253
+ }
254
+ ),
255
+ /* @__PURE__ */ jsx(
256
+ BaseEdge,
257
+ {
258
+ path: edgePath,
259
+ markerEnd,
260
+ style: {
261
+ ...style,
262
+ stroke: `url(#${gradientId})`,
263
+ strokeWidth: selected ? 3 : 2,
264
+ ...hasPause && {
265
+ strokeDasharray: "8 4"
266
+ }
267
+ }
268
+ }
269
+ ),
270
+ hasPause && /* @__PURE__ */ jsx(
271
+ "foreignObject",
272
+ {
273
+ width: 20,
274
+ height: 20,
275
+ x: labelX - 10,
276
+ y: labelY - 10,
277
+ className: "pointer-events-none",
278
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center w-5 h-5 rounded-full bg-amber-500 text-white shadow-sm", children: /* @__PURE__ */ jsx(Pause, { className: "w-3 h-3" }) })
279
+ }
280
+ ),
281
+ selected && !hasPause && /* @__PURE__ */ jsx(
282
+ "foreignObject",
283
+ {
284
+ width: 8,
285
+ height: 8,
286
+ x: labelX - 4,
287
+ y: labelY - 4,
288
+ className: "pointer-events-none",
289
+ children: /* @__PURE__ */ jsx(
290
+ "div",
291
+ {
292
+ className: "w-2 h-2 rounded-full",
293
+ style: { backgroundColor: colorStart }
294
+ }
295
+ )
296
+ }
297
+ )
298
+ ] });
299
+ }
300
+ var EditableEdge = memo(EditableEdgeComponent);
301
+ function EdgeToolbarComponent() {
302
+ const { selectedEdgeId, selectEdge } = useUIStore();
303
+ const { edges, toggleEdgePause, removeEdge } = useWorkflowStore();
304
+ const reactFlow = useReactFlow();
305
+ const selectedEdge = useMemo(
306
+ () => selectedEdgeId ? edges.find((e) => e.id === selectedEdgeId) : null,
307
+ [selectedEdgeId, edges]
308
+ );
309
+ const position = useMemo(() => {
310
+ if (!selectedEdge) return null;
311
+ const sourceNode = reactFlow.getNode(selectedEdge.source);
312
+ const targetNode = reactFlow.getNode(selectedEdge.target);
313
+ if (!sourceNode || !targetNode) return null;
314
+ const midX = (sourceNode.position.x + (sourceNode.measured?.width ?? 280) / 2 + (targetNode.position.x + (targetNode.measured?.width ?? 280) / 2)) / 2;
315
+ const midY = (sourceNode.position.y + (sourceNode.measured?.height ?? 200) / 2 + (targetNode.position.y + (targetNode.measured?.height ?? 200) / 2)) / 2;
316
+ return reactFlow.flowToScreenPosition({ x: midX, y: midY });
317
+ }, [selectedEdge, reactFlow]);
318
+ if (!selectedEdge || !position) return null;
319
+ const hasPause = selectedEdge.data?.hasPause === true;
320
+ const handleTogglePause = (e) => {
321
+ e.stopPropagation();
322
+ toggleEdgePause(selectedEdge.id);
323
+ };
324
+ const handleDelete = (e) => {
325
+ e.stopPropagation();
326
+ removeEdge(selectedEdge.id);
327
+ selectEdge(null);
328
+ };
329
+ return /* @__PURE__ */ jsxs(
330
+ "div",
331
+ {
332
+ className: "fixed z-30 flex items-center gap-1 bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-lg px-1.5 py-1",
333
+ style: {
334
+ left: position.x,
335
+ top: position.y - 40,
336
+ transform: "translateX(-50%)"
337
+ },
338
+ onMouseDown: (e) => e.stopPropagation(),
339
+ children: [
340
+ /* @__PURE__ */ jsx(
341
+ "button",
342
+ {
343
+ onClick: handleTogglePause,
344
+ title: hasPause ? "Resume edge" : "Pause edge",
345
+ className: "flex h-7 w-7 items-center justify-center rounded text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
346
+ children: hasPause ? /* @__PURE__ */ jsx(Play, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Pause, { className: "h-3.5 w-3.5" })
347
+ }
348
+ ),
349
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-[var(--border)]" }),
350
+ /* @__PURE__ */ jsx(
351
+ "button",
352
+ {
353
+ onClick: handleDelete,
354
+ title: "Delete edge",
355
+ className: "flex h-7 w-7 items-center justify-center rounded text-[var(--muted-foreground)] transition hover:bg-red-500/10 hover:text-red-500",
356
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
357
+ }
358
+ )
359
+ ]
360
+ }
361
+ );
362
+ }
363
+ var EdgeToolbar = memo(EdgeToolbarComponent);
364
+ var REFERENCE_COLOR = "#52525b";
365
+ function ReferenceEdge({
366
+ id,
367
+ sourceX,
368
+ sourceY,
369
+ targetX,
370
+ targetY,
371
+ sourcePosition,
372
+ targetPosition,
373
+ style,
374
+ markerEnd,
375
+ source,
376
+ target
377
+ }) {
378
+ const nodes = useWorkflowStore((state) => state.nodes);
379
+ const isConnectedToSelection = useMemo(() => {
380
+ const selectedNodes = nodes.filter((n) => n.selected);
381
+ if (selectedNodes.length === 0) return false;
382
+ return selectedNodes.some((n) => n.id === source || n.id === target);
383
+ }, [nodes, source, target]);
384
+ const [edgePath] = useMemo(() => {
385
+ return getBezierPath({
386
+ sourceX,
387
+ sourceY,
388
+ sourcePosition,
389
+ targetX,
390
+ targetY,
391
+ targetPosition,
392
+ curvature: 0.25
393
+ });
394
+ }, [sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition]);
395
+ const gradientId = useMemo(() => {
396
+ const selectionKey = isConnectedToSelection ? "active" : "dimmed";
397
+ return `reference-gradient-${selectionKey}-${id}`;
398
+ }, [isConnectedToSelection, id]);
399
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
400
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gradientId, x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [
401
+ /* @__PURE__ */ jsx(
402
+ "stop",
403
+ {
404
+ offset: "0%",
405
+ stopColor: REFERENCE_COLOR,
406
+ stopOpacity: isConnectedToSelection ? 1 : 0.25
407
+ }
408
+ ),
409
+ /* @__PURE__ */ jsx(
410
+ "stop",
411
+ {
412
+ offset: "50%",
413
+ stopColor: REFERENCE_COLOR,
414
+ stopOpacity: isConnectedToSelection ? 0.55 : 0.1
415
+ }
416
+ ),
417
+ /* @__PURE__ */ jsx(
418
+ "stop",
419
+ {
420
+ offset: "100%",
421
+ stopColor: REFERENCE_COLOR,
422
+ stopOpacity: isConnectedToSelection ? 1 : 0.25
423
+ }
424
+ )
425
+ ] }) }),
426
+ /* @__PURE__ */ jsx(
427
+ BaseEdge,
428
+ {
429
+ id,
430
+ path: edgePath,
431
+ markerEnd,
432
+ style: {
433
+ ...style,
434
+ stroke: `url(#${gradientId})`,
435
+ strokeWidth: 2,
436
+ strokeDasharray: "6 4",
437
+ strokeLinecap: "round",
438
+ strokeLinejoin: "round"
439
+ }
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsx(
443
+ "path",
444
+ {
445
+ d: edgePath,
446
+ fill: "none",
447
+ strokeWidth: 10,
448
+ stroke: "transparent",
449
+ className: "react-flow__edge-interaction"
450
+ }
451
+ )
452
+ ] });
453
+ }
16
454
 
17
455
  // src/types/groups.ts
18
456
  var GROUP_COLORS = {
@@ -856,254 +1294,651 @@ function ShortcutHelpModal() {
856
1294
  }
857
1295
  );
858
1296
  }
859
- var PROMPT_NODE_TYPES = ["prompt"];
860
- function NodeDetailModal() {
861
- const { activeModal, nodeDetailNodeId, nodeDetailStartIndex, closeNodeDetailModal } = useUIStore();
862
- const { getNodeById } = useWorkflowStore();
863
- const { openEditor } = usePromptEditorStore();
864
- const [zoomLevel, setZoomLevel] = useState(1);
865
- const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
866
- const [isPanning, setIsPanning] = useState(false);
867
- const [panStart, setPanStart] = useState({ x: 0, y: 0 });
868
- const [currentIndex, setCurrentIndex] = useState(0);
869
- const node = useMemo(() => {
870
- if (!nodeDetailNodeId) return null;
871
- return getNodeById(nodeDetailNodeId);
872
- }, [nodeDetailNodeId, getNodeById]);
873
- useEffect(() => {
874
- if (activeModal !== "nodeDetail" || !node) return;
875
- if (PROMPT_NODE_TYPES.includes(node.type)) {
876
- const promptData = node.data;
877
- closeNodeDetailModal();
878
- openEditor(node.id, promptData.prompt ?? "");
1297
+ function CostModal() {
1298
+ const { activeModal, closeModal } = useUIStore();
1299
+ const nodes = useWorkflowStore((state) => state.nodes);
1300
+ const actualCost = useExecutionStore((state) => state.actualCost);
1301
+ const backdropRef = useRef(null);
1302
+ const isOpen = activeModal === "cost";
1303
+ const breakdown = useMemo(() => calculateWorkflowCost(nodes), [nodes]);
1304
+ const handleClose = () => {
1305
+ closeModal();
1306
+ };
1307
+ const handleBackdropClick = (e) => {
1308
+ if (e.target === backdropRef.current) {
1309
+ handleClose();
879
1310
  }
880
- }, [activeModal, node, closeNodeDetailModal, openEditor]);
881
- const mediaInfo = useMemo(() => {
882
- if (!node) return { url: null, type: null };
883
- return getMediaFromNode(node.type, node.data);
884
- }, [node]);
885
- const nodeDef = useMemo(() => {
886
- if (!node) return null;
887
- return NODE_DEFINITIONS[node.type];
888
- }, [node]);
889
- const imageUrls = mediaInfo.urls ?? [];
890
- const hasMultipleImages = imageUrls.length > 1;
891
- const displayUrl = hasMultipleImages ? imageUrls[currentIndex] ?? mediaInfo.url : mediaInfo.url;
892
- const goToPrevious = useCallback(() => {
893
- setCurrentIndex((prev) => Math.max(prev - 1, 0));
894
- setZoomLevel(1);
895
- setPanOffset({ x: 0, y: 0 });
896
- }, []);
897
- const goToNext = useCallback(() => {
898
- setCurrentIndex((prev) => Math.min(prev + 1, imageUrls.length - 1));
899
- setZoomLevel(1);
900
- setPanOffset({ x: 0, y: 0 });
901
- }, [imageUrls.length]);
902
- useEffect(() => {
903
- setZoomLevel(1);
904
- setPanOffset({ x: 0, y: 0 });
905
- setCurrentIndex(nodeDetailStartIndex);
906
- }, [nodeDetailNodeId, nodeDetailStartIndex]);
907
- useEffect(() => {
908
- const handleKeyDown = (e) => {
909
- if (activeModal !== "nodeDetail") return;
910
- if (e.key === "Escape") {
911
- closeNodeDetailModal();
912
- }
913
- if (e.key === "+" || e.key === "=") {
914
- setZoomLevel((prev) => Math.min(prev + 0.25, 4));
915
- }
916
- if (e.key === "-") {
917
- setZoomLevel((prev) => Math.max(prev - 0.25, 0.25));
918
- }
919
- if (e.key === "0") {
920
- setZoomLevel(1);
921
- setPanOffset({ x: 0, y: 0 });
922
- }
923
- if (e.key === "ArrowLeft") {
924
- goToPrevious();
925
- }
926
- if (e.key === "ArrowRight") {
927
- goToNext();
928
- }
929
- };
930
- window.addEventListener("keydown", handleKeyDown);
931
- return () => window.removeEventListener("keydown", handleKeyDown);
932
- }, [activeModal, closeNodeDetailModal, goToPrevious, goToNext]);
933
- const handleMouseDown = useCallback(
934
- (e) => {
935
- if (zoomLevel > 1) {
936
- setIsPanning(true);
937
- setPanStart({ x: e.clientX - panOffset.x, y: e.clientY - panOffset.y });
938
- }
939
- },
940
- [zoomLevel, panOffset]
941
- );
942
- const handleMouseMove = useCallback(
943
- (e) => {
944
- if (isPanning) {
945
- setPanOffset({
946
- x: e.clientX - panStart.x,
947
- y: e.clientY - panStart.y
948
- });
949
- }
950
- },
951
- [isPanning, panStart]
952
- );
953
- const handleMouseUp = useCallback(() => {
954
- setIsPanning(false);
955
- }, []);
956
- const handleDownload = useCallback(() => {
957
- const url = displayUrl ?? mediaInfo.url;
958
- if (!url) return;
959
- const link = document.createElement("a");
960
- link.href = url;
961
- const suffix = hasMultipleImages ? `_${currentIndex + 1}` : "";
962
- link.download = `${node?.data.label || "output"}${suffix}.${mediaInfo.type === "video" ? "mp4" : "png"}`;
963
- document.body.appendChild(link);
964
- link.click();
965
- document.body.removeChild(link);
966
- }, [displayUrl, mediaInfo, node, hasMultipleImages, currentIndex]);
967
- if (activeModal !== "nodeDetail" || !node || !nodeDef) {
968
- return null;
969
- }
970
- if (PROMPT_NODE_TYPES.includes(node.type)) {
971
- return null;
972
- }
973
- const nodeData = node.data;
974
- return /* @__PURE__ */ jsxs(Fragment, { children: [
975
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 bg-black/80", onClick: closeNodeDetailModal }),
976
- /* @__PURE__ */ jsxs("div", { className: "fixed inset-4 z-50 flex flex-col bg-card rounded-lg border border-border shadow-xl overflow-hidden", children: [
977
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-border", children: [
978
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
979
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-medium text-foreground", children: nodeData.label }),
980
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground bg-secondary px-2 py-0.5 rounded", children: nodeDef.label })
981
- ] }),
982
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
983
- displayUrl && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: handleDownload, children: [
984
- /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 mr-1" }),
985
- "Download"
986
- ] }),
987
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: closeNodeDetailModal, children: /* @__PURE__ */ jsx(X, { className: "w-5 h-5" }) })
988
- ] })
989
- ] }),
990
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsxs(
1311
+ };
1312
+ const handleKeyDown = (e) => {
1313
+ if (e.key === "Escape") {
1314
+ handleClose();
1315
+ }
1316
+ };
1317
+ if (!isOpen) return null;
1318
+ const hasActualCost = actualCost > 0;
1319
+ const variance = hasActualCost ? actualCost - breakdown.total : 0;
1320
+ return /* @__PURE__ */ jsx(
1321
+ "div",
1322
+ {
1323
+ ref: backdropRef,
1324
+ onClick: handleBackdropClick,
1325
+ onKeyDown: handleKeyDown,
1326
+ className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
1327
+ style: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
1328
+ children: /* @__PURE__ */ jsxs(
991
1329
  "div",
992
1330
  {
993
- className: "relative w-full h-full flex items-center justify-center bg-background overflow-hidden",
994
- onMouseDown: handleMouseDown,
995
- onMouseMove: handleMouseMove,
996
- onMouseUp: handleMouseUp,
997
- onMouseLeave: handleMouseUp,
998
- style: { cursor: zoomLevel > 1 ? isPanning ? "grabbing" : "grab" : "default" },
1331
+ className: "bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-xl w-full max-w-lg",
1332
+ role: "dialog",
1333
+ "aria-label": "Cost Breakdown",
999
1334
  children: [
1000
- displayUrl ? /* @__PURE__ */ jsxs(
1001
- "div",
1002
- {
1003
- className: "transition-transform duration-100",
1004
- style: {
1005
- transform: `scale(${zoomLevel}) translate(${panOffset.x / zoomLevel}px, ${panOffset.y / zoomLevel}px)`
1006
- },
1007
- children: [
1008
- mediaInfo.type === "image" && /* @__PURE__ */ jsx(
1009
- Image,
1010
- {
1011
- src: displayUrl,
1012
- alt: nodeData.label,
1013
- width: 800,
1014
- height: 600,
1015
- className: "max-h-[calc(100vh-200px)] max-w-[calc(100vw-100px)] object-contain rounded-lg",
1016
- unoptimized: true
1017
- }
1018
- ),
1019
- mediaInfo.type === "video" && /* @__PURE__ */ jsx(
1020
- "video",
1021
- {
1022
- src: displayUrl,
1023
- controls: true,
1024
- autoPlay: true,
1025
- loop: true,
1026
- className: "max-h-[calc(100vh-200px)] max-w-[calc(100vw-100px)] rounded-lg"
1027
- }
1028
- )
1029
- ]
1030
- }
1031
- ) : /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-center", children: [
1032
- /* @__PURE__ */ jsx("p", { className: "text-lg", children: "No preview available" }),
1033
- /* @__PURE__ */ jsx("p", { className: "text-sm mt-2", children: "Generate content to see the preview" })
1034
- ] }),
1035
- hasMultipleImages && currentIndex > 0 && /* @__PURE__ */ jsx(
1036
- Button,
1037
- {
1038
- variant: "ghost",
1039
- size: "icon-sm",
1040
- onClick: goToPrevious,
1041
- className: "absolute left-4 top-1/2 -translate-y-1/2 bg-card/80 hover:bg-card border border-border shadow-md",
1042
- title: "Previous image (\u2190)",
1043
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "w-5 h-5" })
1044
- }
1045
- ),
1046
- hasMultipleImages && currentIndex < imageUrls.length - 1 && /* @__PURE__ */ jsx(
1047
- Button,
1048
- {
1049
- variant: "ghost",
1050
- size: "icon-sm",
1051
- onClick: goToNext,
1052
- className: "absolute right-4 top-1/2 -translate-y-1/2 bg-card/80 hover:bg-card border border-border shadow-md",
1053
- title: "Next image (\u2192)",
1054
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "w-5 h-5" })
1055
- }
1056
- ),
1057
- 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: [
1058
- currentIndex + 1,
1059
- " / ",
1060
- imageUrls.length
1061
- ] }),
1062
- 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: [
1335
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-[var(--border)]", children: [
1336
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1337
+ /* @__PURE__ */ jsx(DollarSign, { className: "w-4 h-4 text-[var(--muted-foreground)]" }),
1338
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Cost Breakdown" })
1339
+ ] }),
1063
1340
  /* @__PURE__ */ jsx(
1064
- Button,
1341
+ "button",
1065
1342
  {
1066
- variant: "ghost",
1067
- size: "icon-sm",
1068
- onClick: () => setZoomLevel((prev) => Math.max(prev - 0.25, 0.25)),
1069
- title: "Zoom out (-)",
1070
- children: /* @__PURE__ */ jsx(ZoomOut, { className: "w-4 h-4" })
1343
+ onClick: handleClose,
1344
+ className: "p-1 rounded hover:bg-[var(--secondary)] transition-colors",
1345
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
1071
1346
  }
1072
- ),
1073
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground w-12 text-center", children: [
1074
- Math.round(zoomLevel * 100),
1075
- "%"
1076
- ] }),
1347
+ )
1348
+ ] }),
1349
+ /* @__PURE__ */ jsx("div", { className: "p-4", children: breakdown.nodes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-[var(--muted-foreground)] py-8", children: "No billable nodes in workflow" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1350
+ /* @__PURE__ */ jsx("div", { className: "max-h-[40vh] overflow-y-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
1351
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-left text-xs text-[var(--muted-foreground)] border-b border-[var(--border)]", children: [
1352
+ /* @__PURE__ */ jsx("th", { className: "pb-2 font-medium", children: "Node" }),
1353
+ /* @__PURE__ */ jsx("th", { className: "pb-2 font-medium", children: "Model" }),
1354
+ /* @__PURE__ */ jsx("th", { className: "pb-2 font-medium", children: "Unit" }),
1355
+ /* @__PURE__ */ jsx("th", { className: "pb-2 font-medium text-right", children: "Cost" })
1356
+ ] }) }),
1357
+ /* @__PURE__ */ jsx("tbody", { children: breakdown.nodes.map((estimate) => /* @__PURE__ */ jsxs(
1358
+ "tr",
1359
+ {
1360
+ className: "border-b border-[var(--border)] last:border-0",
1361
+ children: [
1362
+ /* @__PURE__ */ jsx("td", { className: "py-2 truncate max-w-[120px]", title: estimate.nodeLabel, children: estimate.nodeLabel }),
1363
+ /* @__PURE__ */ jsx("td", { className: "py-2 text-[var(--muted-foreground)] font-mono text-xs", children: estimate.model }),
1364
+ /* @__PURE__ */ jsx("td", { className: "py-2 text-[var(--muted-foreground)] text-xs", children: estimate.unit }),
1365
+ /* @__PURE__ */ jsx("td", { className: "py-2 text-right font-mono", children: formatCost(estimate.cost) })
1366
+ ]
1367
+ },
1368
+ estimate.nodeId
1369
+ )) })
1370
+ ] }) }),
1371
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 pt-3 border-t border-[var(--border)] space-y-1.5", children: [
1372
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
1373
+ /* @__PURE__ */ jsx("span", { className: "text-[var(--muted-foreground)]", children: "Estimated Total" }),
1374
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium", children: formatCost(breakdown.total) })
1375
+ ] }),
1376
+ hasActualCost && /* @__PURE__ */ jsxs(Fragment, { children: [
1377
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
1378
+ /* @__PURE__ */ jsx("span", { className: "text-[var(--muted-foreground)]", children: "Actual Cost" }),
1379
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium text-green-500", children: formatCost(actualCost) })
1380
+ ] }),
1381
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
1382
+ /* @__PURE__ */ jsx("span", { className: "text-[var(--muted-foreground)]", children: "Variance" }),
1383
+ /* @__PURE__ */ jsxs(
1384
+ "span",
1385
+ {
1386
+ className: `font-mono text-xs ${variance > 0 ? "text-red-400" : variance < 0 ? "text-green-400" : "text-[var(--muted-foreground)]"}`,
1387
+ children: [
1388
+ variance > 0 ? "+" : "",
1389
+ formatCost(variance)
1390
+ ]
1391
+ }
1392
+ )
1393
+ ] })
1394
+ ] })
1395
+ ] })
1396
+ ] }) })
1397
+ ]
1398
+ }
1399
+ )
1400
+ }
1401
+ );
1402
+ }
1403
+ var EMPTY_HISTORY = [];
1404
+ function formatRelativeTime(timestamp) {
1405
+ const diff = Date.now() - timestamp;
1406
+ const seconds = Math.floor(diff / 1e3);
1407
+ const minutes = Math.floor(seconds / 60);
1408
+ const hours = Math.floor(minutes / 60);
1409
+ if (hours > 0) return `${hours}h ago`;
1410
+ if (minutes > 0) return `${minutes}m ago`;
1411
+ return "Just now";
1412
+ }
1413
+ function calculateFanPosition(index, _total) {
1414
+ const verticalSpacing = 60;
1415
+ const curveStrength = 0.15;
1416
+ const xOffset = index * index * curveStrength;
1417
+ const x = -28 + xOffset;
1418
+ const y = -(index * verticalSpacing + 56);
1419
+ return { x, y };
1420
+ }
1421
+ function FanItem({
1422
+ item,
1423
+ index,
1424
+ total,
1425
+ onDragStart
1426
+ }) {
1427
+ const { x, y } = calculateFanPosition(index);
1428
+ const delay = index * 30;
1429
+ return /* @__PURE__ */ jsx(
1430
+ "button",
1431
+ {
1432
+ draggable: true,
1433
+ onDragStart: (e) => onDragStart(e, item),
1434
+ className: "absolute w-14 h-14 rounded-lg overflow-hidden border-2 border-neutral-600 hover:border-blue-500 shadow-lg cursor-grab active:cursor-grabbing transition-colors duration-150 animate-fan-enter group",
1435
+ style: {
1436
+ "--fan-x": `${x}px`,
1437
+ "--fan-y": `${y}px`,
1438
+ animationDelay: `${delay}ms`,
1439
+ zIndex: 10 - index
1440
+ },
1441
+ title: `${formatRelativeTime(item.timestamp)}
1442
+ ${item.prompt?.substring(0, 50) || ""}...`,
1443
+ children: /* @__PURE__ */ jsx(
1444
+ "img",
1445
+ {
1446
+ src: item.image,
1447
+ alt: `History ${index + 1}`,
1448
+ className: "w-full h-full object-cover pointer-events-none",
1449
+ draggable: false
1450
+ }
1451
+ )
1452
+ }
1453
+ );
1454
+ }
1455
+ function HistorySidebar({
1456
+ history,
1457
+ onClear,
1458
+ onClose,
1459
+ onDragStart,
1460
+ triggerRect
1461
+ }) {
1462
+ const sidebarRef = useRef(null);
1463
+ useEffect(() => {
1464
+ const handleClickOutside = (event) => {
1465
+ if (sidebarRef.current && !sidebarRef.current.contains(event.target)) {
1466
+ onClose();
1467
+ }
1468
+ };
1469
+ document.addEventListener("mousedown", handleClickOutside);
1470
+ return () => document.removeEventListener("mousedown", handleClickOutside);
1471
+ }, [onClose]);
1472
+ useEffect(() => {
1473
+ const handleKeyDown = (e) => {
1474
+ if (e.key === "Escape") onClose();
1475
+ };
1476
+ document.addEventListener("keydown", handleKeyDown);
1477
+ return () => document.removeEventListener("keydown", handleKeyDown);
1478
+ }, [onClose]);
1479
+ const sidebarStyle = {
1480
+ position: "fixed",
1481
+ zIndex: 200
1482
+ };
1483
+ if (triggerRect) {
1484
+ const left = Math.max(16, triggerRect.left - 140);
1485
+ const bottom = window.innerHeight - triggerRect.top + 8;
1486
+ sidebarStyle.left = `${left}px`;
1487
+ sidebarStyle.bottom = `${bottom}px`;
1488
+ } else {
1489
+ sidebarStyle.right = "100px";
1490
+ sidebarStyle.bottom = "100px";
1491
+ }
1492
+ return createPortal(
1493
+ /* @__PURE__ */ jsxs(
1494
+ "div",
1495
+ {
1496
+ ref: sidebarRef,
1497
+ className: "w-80 max-h-[420px] bg-neutral-800 border border-neutral-600 rounded-lg shadow-xl flex flex-col",
1498
+ style: sidebarStyle,
1499
+ children: [
1500
+ /* @__PURE__ */ jsxs("div", { className: "px-4 py-3 border-b border-neutral-700 flex items-center justify-between shrink-0", children: [
1501
+ /* @__PURE__ */ jsxs("span", { className: "text-sm text-neutral-200 font-medium", children: [
1502
+ "All History (",
1503
+ history.length,
1504
+ ")"
1505
+ ] }),
1506
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1077
1507
  /* @__PURE__ */ jsx(
1078
- Button,
1508
+ "button",
1079
1509
  {
1080
- variant: "ghost",
1081
- size: "icon-sm",
1082
- onClick: () => setZoomLevel((prev) => Math.min(prev + 0.25, 4)),
1083
- title: "Zoom in (+)",
1084
- children: /* @__PURE__ */ jsx(ZoomIn, { className: "w-4 h-4" })
1510
+ onClick: onClear,
1511
+ className: "text-[10px] text-neutral-500 hover:text-red-400 transition-colors",
1512
+ title: "Clear all history",
1513
+ children: "Clear All"
1085
1514
  }
1086
1515
  ),
1087
1516
  /* @__PURE__ */ jsx(
1088
- Button,
1517
+ "button",
1089
1518
  {
1090
- variant: "ghost",
1091
- size: "sm",
1092
- onClick: () => {
1093
- setZoomLevel(1);
1094
- setPanOffset({ x: 0, y: 0 });
1095
- },
1096
- title: "Reset zoom (0)",
1097
- children: "Reset"
1519
+ onClick: onClose,
1520
+ className: "w-5 h-5 rounded hover:bg-neutral-700 flex items-center justify-center text-neutral-400 hover:text-white transition-colors",
1521
+ title: "Close",
1522
+ children: /* @__PURE__ */ jsx(
1523
+ "svg",
1524
+ {
1525
+ className: "w-3 h-3",
1526
+ fill: "none",
1527
+ viewBox: "0 0 24 24",
1528
+ stroke: "currentColor",
1529
+ strokeWidth: 2,
1530
+ children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" })
1531
+ }
1532
+ )
1098
1533
  }
1099
1534
  )
1100
1535
  ] })
1101
- ]
1536
+ ] }),
1537
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-2 space-y-1.5", children: history.map((item, index) => /* @__PURE__ */ jsxs(
1538
+ "div",
1539
+ {
1540
+ draggable: true,
1541
+ onDragStart: (e) => onDragStart(e, item),
1542
+ className: "flex gap-3 p-2 rounded-lg hover:bg-neutral-700/50 cursor-grab active:cursor-grabbing group transition-colors",
1543
+ children: [
1544
+ /* @__PURE__ */ jsx("div", { className: "w-14 h-14 rounded overflow-hidden shrink-0 border border-neutral-600 group-hover:border-blue-500 transition-colors", children: /* @__PURE__ */ jsx(
1545
+ "img",
1546
+ {
1547
+ src: item.image,
1548
+ alt: `History ${index + 1}`,
1549
+ className: "w-full h-full object-cover pointer-events-none",
1550
+ draggable: false
1551
+ }
1552
+ ) }),
1553
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col justify-center", children: [
1554
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-neutral-300 truncate", children: item.prompt?.substring(0, 60) || "No prompt" }),
1555
+ /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-neutral-500 mt-0.5", children: [
1556
+ formatRelativeTime(item.timestamp),
1557
+ item.model ? ` \xB7 ${item.model}` : ""
1558
+ ] })
1559
+ ] })
1560
+ ]
1561
+ },
1562
+ item.id
1563
+ )) }),
1564
+ /* @__PURE__ */ jsx("div", { className: "px-4 py-2 border-t border-neutral-700 bg-neutral-900/50 shrink-0", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] text-neutral-500", children: "Drag images to canvas to create nodes" }) })
1565
+ ]
1566
+ }
1567
+ ),
1568
+ document.body
1569
+ );
1570
+ }
1571
+ function GlobalImageHistory() {
1572
+ const [isOpen, setIsOpen] = useState(false);
1573
+ const [showSidebar, setShowSidebar] = useState(false);
1574
+ const drawerRef = useRef(null);
1575
+ const triggerRef = useRef(null);
1576
+ const history = useWorkflowStore((state) => state.globalImageHistory ?? EMPTY_HISTORY);
1577
+ const clearGlobalHistory = useWorkflowStore((state) => state.clearGlobalHistory);
1578
+ const fanItems = history.slice(0, 10);
1579
+ const hasOverflow = history.length > 10;
1580
+ useEffect(() => {
1581
+ const handleClickOutside = (event) => {
1582
+ if (drawerRef.current && !drawerRef.current.contains(event.target)) {
1583
+ setIsOpen(false);
1584
+ }
1585
+ };
1586
+ if (isOpen && !showSidebar) {
1587
+ document.addEventListener("mousedown", handleClickOutside);
1588
+ }
1589
+ return () => document.removeEventListener("mousedown", handleClickOutside);
1590
+ }, [isOpen, showSidebar]);
1591
+ useEffect(() => {
1592
+ const handleKeyDown = (e) => {
1593
+ if (e.key === "Escape") {
1594
+ if (showSidebar) {
1595
+ setShowSidebar(false);
1596
+ } else {
1597
+ setIsOpen(false);
1102
1598
  }
1103
- ) })
1104
- ] })
1599
+ }
1600
+ };
1601
+ if (isOpen || showSidebar) {
1602
+ document.addEventListener("keydown", handleKeyDown);
1603
+ }
1604
+ return () => document.removeEventListener("keydown", handleKeyDown);
1605
+ }, [isOpen, showSidebar]);
1606
+ const handleDragStart = useCallback((e, item) => {
1607
+ e.dataTransfer.setData(
1608
+ "application/history-image",
1609
+ JSON.stringify({
1610
+ image: item.image,
1611
+ prompt: item.prompt,
1612
+ timestamp: item.timestamp
1613
+ })
1614
+ );
1615
+ e.dataTransfer.effectAllowed = "copy";
1616
+ setTimeout(() => {
1617
+ setIsOpen(false);
1618
+ setShowSidebar(false);
1619
+ }, 0);
1620
+ }, []);
1621
+ const handleShowAll = useCallback(() => {
1622
+ setIsOpen(false);
1623
+ setShowSidebar(true);
1624
+ }, []);
1625
+ const handleCloseSidebar = useCallback(() => {
1626
+ setShowSidebar(false);
1627
+ }, []);
1628
+ const handleClear = useCallback(() => {
1629
+ clearGlobalHistory();
1630
+ setIsOpen(false);
1631
+ setShowSidebar(false);
1632
+ }, [clearGlobalHistory]);
1633
+ if (history.length === 0) return null;
1634
+ return /* @__PURE__ */ jsxs("div", { ref: drawerRef, className: "absolute bottom-4 right-64 z-10", children: [
1635
+ /* @__PURE__ */ jsxs(
1636
+ "button",
1637
+ {
1638
+ ref: triggerRef,
1639
+ onClick: () => setIsOpen(!isOpen),
1640
+ className: "relative w-8 h-8 rounded-lg flex items-center justify-center bg-neutral-800 hover:bg-neutral-700 border border-neutral-600 text-neutral-400 hover:text-white shadow-lg transition-colors",
1641
+ title: `${history.length} image${history.length > 1 ? "s" : ""} in history`,
1642
+ children: [
1643
+ /* @__PURE__ */ jsx(
1644
+ "svg",
1645
+ {
1646
+ className: "w-4 h-4",
1647
+ fill: "none",
1648
+ viewBox: "0 0 24 24",
1649
+ stroke: "currentColor",
1650
+ strokeWidth: 2,
1651
+ children: /* @__PURE__ */ jsx(
1652
+ "path",
1653
+ {
1654
+ strokeLinecap: "round",
1655
+ strokeLinejoin: "round",
1656
+ d: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
1657
+ }
1658
+ )
1659
+ }
1660
+ ),
1661
+ /* @__PURE__ */ jsx("span", { className: "absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1 bg-blue-500 rounded-full text-[10px] text-white flex items-center justify-center font-bold", children: history.length > 99 ? "99+" : history.length })
1662
+ ]
1663
+ }
1664
+ ),
1665
+ isOpen && /* @__PURE__ */ jsxs(
1666
+ "div",
1667
+ {
1668
+ className: "absolute bottom-full right-1/2 translate-x-1/2 mb-2",
1669
+ style: { zIndex: 100 },
1670
+ children: [
1671
+ /* @__PURE__ */ jsx("div", { className: "relative w-0 h-0", children: fanItems.map((item, index) => /* @__PURE__ */ jsx(
1672
+ FanItem,
1673
+ {
1674
+ item,
1675
+ index,
1676
+ total: fanItems.length,
1677
+ onDragStart: handleDragStart
1678
+ },
1679
+ item.id
1680
+ )) }),
1681
+ hasOverflow && (() => {
1682
+ const topItemPos = calculateFanPosition(fanItems.length - 1, fanItems.length);
1683
+ return /* @__PURE__ */ jsxs(
1684
+ "button",
1685
+ {
1686
+ onClick: handleShowAll,
1687
+ className: "absolute animate-fan-enter bg-neutral-800 hover:bg-neutral-700 border border-neutral-600 rounded-lg px-2 py-1 text-[10px] text-neutral-300 hover:text-white shadow-lg transition-colors whitespace-nowrap",
1688
+ style: {
1689
+ "--fan-x": `${topItemPos.x}px`,
1690
+ "--fan-y": `${topItemPos.y - 60}px`,
1691
+ animationDelay: `${fanItems.length * 30}ms`
1692
+ },
1693
+ children: [
1694
+ "+",
1695
+ history.length - 10,
1696
+ " more"
1697
+ ]
1698
+ }
1699
+ );
1700
+ })()
1701
+ ]
1702
+ }
1703
+ ),
1704
+ showSidebar && /* @__PURE__ */ jsx(
1705
+ HistorySidebar,
1706
+ {
1707
+ history,
1708
+ onClear: handleClear,
1709
+ onClose: handleCloseSidebar,
1710
+ onDragStart: handleDragStart,
1711
+ triggerRect: triggerRef.current?.getBoundingClientRect() || null
1712
+ }
1713
+ )
1105
1714
  ] });
1106
1715
  }
1716
+ var NODE_GAP = 32;
1717
+ var EST_NODE_WIDTH = 280;
1718
+ var EST_NODE_HEIGHT = 200;
1719
+ function MultiSelectToolbarComponent({ onDownloadAsZip }) {
1720
+ const { nodes, selectedNodeIds, onNodesChange, createGroup, deleteGroup, groups } = useWorkflowStore();
1721
+ const reactFlow = useReactFlow();
1722
+ const selectedNodes = useMemo(
1723
+ () => nodes.filter((n) => selectedNodeIds.includes(n.id)),
1724
+ [nodes, selectedNodeIds]
1725
+ );
1726
+ const selectedGroup = useMemo(() => {
1727
+ if (selectedNodes.length < 2) return null;
1728
+ return groups.find((g) => selectedNodeIds.every((id) => g.nodeIds.includes(id))) ?? null;
1729
+ }, [groups, selectedNodeIds, selectedNodes.length]);
1730
+ const toolbarPosition = useMemo(() => {
1731
+ if (selectedNodes.length < 2) return null;
1732
+ let minX = Infinity;
1733
+ let minY = Infinity;
1734
+ let maxX = -Infinity;
1735
+ for (const node of selectedNodes) {
1736
+ const x = node.position.x;
1737
+ const y = node.position.y;
1738
+ const w = node.measured?.width ?? EST_NODE_WIDTH;
1739
+ if (x < minX) minX = x;
1740
+ if (y < minY) minY = y;
1741
+ if (x + w > maxX) maxX = x + w;
1742
+ }
1743
+ const centerX = (minX + maxX) / 2;
1744
+ const topY = minY - 48;
1745
+ return reactFlow.flowToScreenPosition({ x: centerX, y: topY });
1746
+ }, [selectedNodes, reactFlow]);
1747
+ const stackHorizontal = useCallback(() => {
1748
+ if (selectedNodes.length < 2) return;
1749
+ const sorted = [...selectedNodes].sort((a, b) => a.position.x - b.position.x);
1750
+ const baseY = sorted[0].position.y;
1751
+ const changes = sorted.map((node, i) => ({
1752
+ type: "position",
1753
+ id: node.id,
1754
+ position: {
1755
+ x: sorted[0].position.x + i * (EST_NODE_WIDTH + NODE_GAP),
1756
+ y: baseY
1757
+ }
1758
+ }));
1759
+ onNodesChange(changes);
1760
+ }, [selectedNodes, onNodesChange]);
1761
+ const stackVertical = useCallback(() => {
1762
+ if (selectedNodes.length < 2) return;
1763
+ const sorted = [...selectedNodes].sort((a, b) => a.position.y - b.position.y);
1764
+ const baseX = sorted[0].position.x;
1765
+ const changes = sorted.map((node, i) => ({
1766
+ type: "position",
1767
+ id: node.id,
1768
+ position: {
1769
+ x: baseX,
1770
+ y: sorted[0].position.y + i * (EST_NODE_HEIGHT + NODE_GAP)
1771
+ }
1772
+ }));
1773
+ onNodesChange(changes);
1774
+ }, [selectedNodes, onNodesChange]);
1775
+ const arrangeGrid = useCallback(() => {
1776
+ if (selectedNodes.length < 2) return;
1777
+ const cols = Math.ceil(Math.sqrt(selectedNodes.length));
1778
+ const sorted = [...selectedNodes].sort((a, b) => {
1779
+ const rowDiff = Math.floor(a.position.y / EST_NODE_HEIGHT) - Math.floor(b.position.y / EST_NODE_HEIGHT);
1780
+ if (rowDiff !== 0) return rowDiff;
1781
+ return a.position.x - b.position.x;
1782
+ });
1783
+ const baseX = sorted[0].position.x;
1784
+ const baseY = sorted[0].position.y;
1785
+ const changes = sorted.map((node, i) => ({
1786
+ type: "position",
1787
+ id: node.id,
1788
+ position: {
1789
+ x: baseX + i % cols * (EST_NODE_WIDTH + NODE_GAP),
1790
+ y: baseY + Math.floor(i / cols) * (EST_NODE_HEIGHT + NODE_GAP)
1791
+ }
1792
+ }));
1793
+ onNodesChange(changes);
1794
+ }, [selectedNodes, onNodesChange]);
1795
+ const handleGroup = useCallback(() => {
1796
+ if (selectedNodeIds.length < 2) return;
1797
+ createGroup(selectedNodeIds);
1798
+ }, [selectedNodeIds, createGroup]);
1799
+ const handleUngroup = useCallback(() => {
1800
+ if (!selectedGroup) return;
1801
+ deleteGroup(selectedGroup.id);
1802
+ }, [selectedGroup, deleteGroup]);
1803
+ if (selectedNodes.length < 2 || !toolbarPosition) return null;
1804
+ return /* @__PURE__ */ jsxs(
1805
+ "div",
1806
+ {
1807
+ className: "fixed z-30 flex items-center gap-1 bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-lg px-1.5 py-1",
1808
+ style: {
1809
+ left: toolbarPosition.x,
1810
+ top: toolbarPosition.y,
1811
+ transform: "translateX(-50%)"
1812
+ },
1813
+ onMouseDown: (e) => e.stopPropagation(),
1814
+ children: [
1815
+ /* @__PURE__ */ jsx("span", { className: "px-1.5 text-xs font-medium text-[var(--muted-foreground)]", children: selectedNodeIds.length }),
1816
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-[var(--border)]" }),
1817
+ /* @__PURE__ */ jsx(
1818
+ "button",
1819
+ {
1820
+ onClick: stackHorizontal,
1821
+ title: "Stack horizontal",
1822
+ className: "flex h-7 w-7 items-center justify-center rounded text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
1823
+ children: /* @__PURE__ */ jsx(AlignHorizontalSpaceAround, { className: "h-3.5 w-3.5" })
1824
+ }
1825
+ ),
1826
+ /* @__PURE__ */ jsx(
1827
+ "button",
1828
+ {
1829
+ onClick: stackVertical,
1830
+ title: "Stack vertical",
1831
+ className: "flex h-7 w-7 items-center justify-center rounded text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
1832
+ children: /* @__PURE__ */ jsx(AlignVerticalSpaceAround, { className: "h-3.5 w-3.5" })
1833
+ }
1834
+ ),
1835
+ /* @__PURE__ */ jsx(
1836
+ "button",
1837
+ {
1838
+ onClick: arrangeGrid,
1839
+ title: "Arrange as grid",
1840
+ className: "flex h-7 w-7 items-center justify-center rounded text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
1841
+ children: /* @__PURE__ */ jsx(Grid3X3, { className: "h-3.5 w-3.5" })
1842
+ }
1843
+ ),
1844
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-[var(--border)]" }),
1845
+ selectedGroup ? /* @__PURE__ */ jsx(
1846
+ "button",
1847
+ {
1848
+ onClick: handleUngroup,
1849
+ title: "Ungroup",
1850
+ className: "flex h-7 w-7 items-center justify-center rounded text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
1851
+ children: /* @__PURE__ */ jsx(Ungroup, { className: "h-3.5 w-3.5" })
1852
+ }
1853
+ ) : /* @__PURE__ */ jsx(
1854
+ "button",
1855
+ {
1856
+ onClick: handleGroup,
1857
+ title: "Group",
1858
+ className: "flex h-7 w-7 items-center justify-center rounded text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
1859
+ children: /* @__PURE__ */ jsx(Group, { className: "h-3.5 w-3.5" })
1860
+ }
1861
+ )
1862
+ ]
1863
+ }
1864
+ );
1865
+ }
1866
+ var MultiSelectToolbar = memo(MultiSelectToolbarComponent);
1867
+ var typeStyles = {
1868
+ info: "bg-neutral-800 border-neutral-600 text-neutral-100",
1869
+ success: "bg-green-900 border-green-700 text-green-100",
1870
+ warning: "bg-orange-900 border-orange-600 text-orange-100",
1871
+ error: "bg-red-900 border-red-700 text-red-100"
1872
+ };
1873
+ var typeIcons = {
1874
+ info: Info,
1875
+ success: CheckCircle,
1876
+ warning: AlertTriangle,
1877
+ error: XCircle
1878
+ };
1879
+ function NotificationItem({
1880
+ id,
1881
+ type,
1882
+ title,
1883
+ message
1884
+ }) {
1885
+ const removeNotification = useUIStore((state) => state.removeNotification);
1886
+ const [copied, setCopied] = useState(false);
1887
+ const handleCopy = useCallback(async () => {
1888
+ const textToCopy = message ? `${title}
1889
+
1890
+ ${message}` : title;
1891
+ await navigator.clipboard.writeText(textToCopy);
1892
+ setCopied(true);
1893
+ setTimeout(() => setCopied(false), 2e3);
1894
+ }, [title, message]);
1895
+ const Icon = typeIcons[type];
1896
+ return /* @__PURE__ */ jsx(
1897
+ "div",
1898
+ {
1899
+ className: `flex flex-col rounded-lg border shadow-xl animate-in fade-in slide-in-from-top-4 duration-300 max-w-md ${typeStyles[type]}`,
1900
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
1901
+ /* @__PURE__ */ jsx(Icon, { className: "w-5 h-5 shrink-0" }),
1902
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
1903
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: title }),
1904
+ message && /* @__PURE__ */ jsx("p", { className: "text-xs opacity-80 mt-0.5 break-words", children: message })
1905
+ ] }),
1906
+ /* @__PURE__ */ jsx(
1907
+ "button",
1908
+ {
1909
+ onClick: handleCopy,
1910
+ className: "p-1 rounded hover:bg-white/10 transition-colors shrink-0",
1911
+ title: "Copy message",
1912
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "w-4 h-4" }) : /* @__PURE__ */ jsx(Copy, { className: "w-4 h-4" })
1913
+ }
1914
+ ),
1915
+ /* @__PURE__ */ jsx(
1916
+ "button",
1917
+ {
1918
+ onClick: () => removeNotification(id),
1919
+ className: "p-1 rounded hover:bg-white/10 transition-colors shrink-0",
1920
+ title: "Dismiss",
1921
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
1922
+ }
1923
+ )
1924
+ ] })
1925
+ }
1926
+ );
1927
+ }
1928
+ function NotificationToast() {
1929
+ const notifications = useUIStore((state) => state.notifications);
1930
+ if (notifications.length === 0) return null;
1931
+ return /* @__PURE__ */ jsx("div", { className: "fixed top-6 right-6 z-[200] flex flex-col gap-2", children: notifications.map((notification) => /* @__PURE__ */ jsx(
1932
+ NotificationItem,
1933
+ {
1934
+ id: notification.id,
1935
+ type: notification.type,
1936
+ title: notification.title,
1937
+ message: notification.message
1938
+ },
1939
+ notification.id
1940
+ )) });
1941
+ }
1107
1942
  var DEFAULT_NODE_COLOR = "#6b7280";
1108
1943
  function supportsImageInput(schema) {
1109
1944
  if (!schema) return true;
@@ -1119,7 +1954,12 @@ function getEdgeDataType(edge, nodeMap) {
1119
1954
  const sourceHandle = nodeDef.outputs.find((h) => h.id === edge.sourceHandle);
1120
1955
  return sourceHandle?.type ?? null;
1121
1956
  }
1122
- function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1957
+ var edgeTypesMap = {
1958
+ default: EditableEdge,
1959
+ bezier: EditableEdge,
1960
+ reference: ReferenceEdge
1961
+ };
1962
+ function WorkflowCanvas({ nodeTypes: nodeTypesProp, onDownloadAsZip } = {}) {
1123
1963
  const {
1124
1964
  nodes,
1125
1965
  edges,
@@ -1140,11 +1980,13 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1140
1980
  } = useWorkflowStore();
1141
1981
  const {
1142
1982
  selectNode,
1983
+ selectEdge,
1143
1984
  setHighlightedNodeIds,
1144
1985
  highlightedNodeIds,
1145
1986
  showPalette,
1146
1987
  togglePalette,
1147
- openModal
1988
+ openModal,
1989
+ openConnectionDropMenu
1148
1990
  } = useUIStore();
1149
1991
  const reactFlow = useReactFlow();
1150
1992
  const { edgeStyle, showMinimap } = useSettingsStore();
@@ -1157,8 +1999,28 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1157
1999
  const [draggingNodeId, setDraggingNodeId] = useState(null);
1158
2000
  const hideTimeoutRef = useRef(null);
1159
2001
  const MINIMAP_HIDE_DELAY = 1500;
2002
+ const {
2003
+ isOpen: isContextMenuOpen,
2004
+ position: contextMenuPosition,
2005
+ menuItems: contextMenuItems,
2006
+ openNodeMenu,
2007
+ openEdgeMenu,
2008
+ openPaneMenu,
2009
+ openSelectionMenu,
2010
+ close: closeContextMenu
2011
+ } = useContextMenu();
1160
2012
  const openShortcutHelp = useCallback(() => openModal("shortcutHelp"), [openModal]);
1161
2013
  const openNodeSearch = useCallback(() => openModal("nodeSearch"), [openModal]);
2014
+ const deleteSelectedElements = useCallback(() => {
2015
+ const nodesToDelete = nodes.filter((n) => selectedNodeIds.includes(n.id));
2016
+ const edgesToDelete = edges.filter((e) => e.selected);
2017
+ if (nodesToDelete.length > 0) {
2018
+ onNodesChange(nodesToDelete.map((n) => ({ type: "remove", id: n.id })));
2019
+ }
2020
+ if (edgesToDelete.length > 0) {
2021
+ onEdgesChange(edgesToDelete.map((e) => ({ type: "remove", id: e.id })));
2022
+ }
2023
+ }, [nodes, edges, selectedNodeIds, onNodesChange, onEdgesChange]);
1162
2024
  useCanvasKeyboardShortcuts({
1163
2025
  selectedNodeIds,
1164
2026
  groups,
@@ -1171,7 +2033,8 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1171
2033
  togglePalette,
1172
2034
  fitView: reactFlow.fitView,
1173
2035
  openShortcutHelp,
1174
- openNodeSearch
2036
+ openNodeSearch,
2037
+ deleteSelectedElements
1175
2038
  });
1176
2039
  useEffect(() => {
1177
2040
  if (selectedNodeIds.length === 0) {
@@ -1209,11 +2072,13 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1209
2072
  const dataType = getEdgeDataType(edge, nodeMap);
1210
2073
  const typeClass = dataType ? `edge-${dataType}` : "";
1211
2074
  const isDisabledTarget = isEdgeTargetingDisabledInput(edge);
2075
+ const enrichedData = { ...edge.data, dataType };
1212
2076
  if (!isRunning && hasActiveNodeExecutions) {
1213
2077
  const isActiveEdge = activeNodeExecutions.has(edge.source) || activeNodeExecutions.has(edge.target);
1214
2078
  if (isActiveEdge && !isDisabledTarget) {
1215
2079
  return {
1216
2080
  ...edge,
2081
+ data: enrichedData,
1217
2082
  animated: false,
1218
2083
  className: `${typeClass} executing`.trim()
1219
2084
  };
@@ -1225,6 +2090,7 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1225
2090
  if (isDisabledTarget) {
1226
2091
  return {
1227
2092
  ...edge,
2093
+ data: enrichedData,
1228
2094
  animated: false,
1229
2095
  className: `${typeClass} edge-disabled`.trim()
1230
2096
  };
@@ -1232,12 +2098,14 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1232
2098
  if (!isInExecutionScope) {
1233
2099
  return {
1234
2100
  ...edge,
2101
+ data: enrichedData,
1235
2102
  animated: false,
1236
2103
  className: typeClass
1237
2104
  };
1238
2105
  }
1239
2106
  return {
1240
2107
  ...edge,
2108
+ data: enrichedData,
1241
2109
  animated: false,
1242
2110
  className: `${typeClass} ${isExecutingEdge ? "executing" : "active-pipe"}`.trim()
1243
2111
  };
@@ -1245,6 +2113,7 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1245
2113
  if (isDisabledTarget) {
1246
2114
  return {
1247
2115
  ...edge,
2116
+ data: enrichedData,
1248
2117
  className: `${typeClass} edge-disabled`.trim()
1249
2118
  };
1250
2119
  }
@@ -1252,11 +2121,13 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1252
2121
  const isConnected = highlightedSet.has(edge.source) && highlightedSet.has(edge.target);
1253
2122
  return {
1254
2123
  ...edge,
2124
+ data: enrichedData,
1255
2125
  className: `${typeClass} ${isConnected ? "highlighted" : "dimmed"}`.trim()
1256
2126
  };
1257
2127
  }
1258
2128
  return {
1259
2129
  ...edge,
2130
+ data: enrichedData,
1260
2131
  className: typeClass
1261
2132
  };
1262
2133
  });
@@ -1279,8 +2150,15 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1279
2150
  );
1280
2151
  const handlePaneClick = useCallback(() => {
1281
2152
  selectNode(null);
2153
+ selectEdge(null);
1282
2154
  setSelectedNodeIds([]);
1283
- }, [selectNode, setSelectedNodeIds]);
2155
+ }, [selectNode, selectEdge, setSelectedNodeIds]);
2156
+ const handleEdgeClick = useCallback(
2157
+ (_event, edge) => {
2158
+ selectEdge(edge.id);
2159
+ },
2160
+ [selectEdge]
2161
+ );
1284
2162
  const handleSelectionChange = useCallback(
1285
2163
  ({ nodes: selectedNodes }) => {
1286
2164
  setSelectedNodeIds(selectedNodes.map((n) => n.id));
@@ -1290,30 +2168,56 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1290
2168
  const handleNodeContextMenu = useCallback(
1291
2169
  (event, node) => {
1292
2170
  event.preventDefault();
2171
+ if (selectedNodeIds.length > 1 && selectedNodeIds.includes(node.id)) {
2172
+ openSelectionMenu(selectedNodeIds, event.clientX, event.clientY);
2173
+ } else {
2174
+ openNodeMenu(node.id, event.clientX, event.clientY);
2175
+ }
1293
2176
  },
1294
- [selectedNodeIds]
2177
+ [selectedNodeIds, openNodeMenu, openSelectionMenu]
1295
2178
  );
1296
2179
  const handleEdgeContextMenu = useCallback(
1297
2180
  (event, edge) => {
1298
2181
  event.preventDefault();
2182
+ openEdgeMenu(edge.id, event.clientX, event.clientY);
1299
2183
  },
1300
- []
2184
+ [openEdgeMenu]
1301
2185
  );
1302
2186
  const handlePaneContextMenu = useCallback(
1303
2187
  (event) => {
1304
2188
  event.preventDefault();
2189
+ openPaneMenu(event.clientX, event.clientY);
1305
2190
  },
1306
- []
2191
+ [openPaneMenu]
1307
2192
  );
1308
2193
  const handleSelectionContextMenu = useCallback(
1309
2194
  (event, selectedNodes) => {
1310
2195
  event.preventDefault();
2196
+ const nodeIds = selectedNodes.map((n) => n.id);
2197
+ openSelectionMenu(nodeIds, event.clientX, event.clientY);
1311
2198
  },
1312
- []
2199
+ [openSelectionMenu]
1313
2200
  );
1314
2201
  const handleDrop = useCallback(
1315
2202
  (event) => {
1316
2203
  event.preventDefault();
2204
+ const historyData = event.dataTransfer.getData("application/history-image");
2205
+ if (historyData) {
2206
+ try {
2207
+ const parsed = JSON.parse(historyData);
2208
+ const position2 = reactFlow.screenToFlowPosition({
2209
+ x: event.clientX,
2210
+ y: event.clientY
2211
+ });
2212
+ const nodeId = addNode("imageInput", position2);
2213
+ if (nodeId && parsed.image) {
2214
+ const { updateNodeData } = useWorkflowStore.getState();
2215
+ updateNodeData(nodeId, { outputImage: parsed.image });
2216
+ }
2217
+ } catch {
2218
+ }
2219
+ return;
2220
+ }
1317
2221
  const nodeType = event.dataTransfer.getData("nodeType");
1318
2222
  if (!nodeType) return;
1319
2223
  const position = reactFlow.screenToFlowPosition({
@@ -1345,7 +2249,27 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1345
2249
  if (!sourceNodeId || !sourceHandleId) return;
1346
2250
  const target = event.target;
1347
2251
  const nodeElement = target.closest(".react-flow__node");
1348
- if (!nodeElement) return;
2252
+ if (!nodeElement) {
2253
+ const sourceNode = nodes.find((n) => n.id === sourceNodeId);
2254
+ if (!sourceNode) return;
2255
+ const sourceHandleType = getHandleType(
2256
+ sourceNode.type,
2257
+ sourceHandleId,
2258
+ "source"
2259
+ );
2260
+ if (!sourceHandleType) return;
2261
+ const clientX = "clientX" in event ? event.clientX : event.touches?.[0]?.clientX ?? 0;
2262
+ const clientY = "clientY" in event ? event.clientY : event.touches?.[0]?.clientY ?? 0;
2263
+ const flowPosition = reactFlow.screenToFlowPosition({ x: clientX, y: clientY });
2264
+ openConnectionDropMenu({
2265
+ position: flowPosition,
2266
+ screenPosition: { x: clientX, y: clientY },
2267
+ sourceNodeId,
2268
+ sourceHandleId,
2269
+ sourceHandleType
2270
+ });
2271
+ return;
2272
+ }
1349
2273
  const targetNodeId = nodeElement.getAttribute("data-id");
1350
2274
  if (!targetNodeId || targetNodeId === sourceNodeId) return;
1351
2275
  const droppedOnHandle = target.closest(".react-flow__handle");
@@ -1359,7 +2283,7 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1359
2283
  targetHandle: compatibleHandle
1360
2284
  });
1361
2285
  },
1362
- [findCompatibleHandle, onConnect]
2286
+ [findCompatibleHandle, onConnect, nodes, reactFlow, openConnectionDropMenu]
1363
2287
  );
1364
2288
  const checkValidConnection = useCallback(
1365
2289
  (connection) => {
@@ -1427,11 +2351,16 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1427
2351
  onNodeDragStart: handleNodeDragStart,
1428
2352
  onNodeDrag: handleNodeDrag,
1429
2353
  onNodeDragStop: handleNodeDragStop,
2354
+ onEdgeClick: handleEdgeClick,
1430
2355
  isValidConnection: checkValidConnection,
1431
2356
  nodeTypes: nodeTypesProp ?? nodeTypes,
2357
+ edgeTypes: edgeTypesMap,
1432
2358
  fitView: true,
1433
2359
  snapToGrid: true,
1434
2360
  snapGrid: [16, 16],
2361
+ minZoom: 0.1,
2362
+ maxZoom: 4,
2363
+ nodeDragThreshold: 5,
1435
2364
  connectionMode: ConnectionMode.Loose,
1436
2365
  selectionMode: SelectionMode.Partial,
1437
2366
  selectionOnDrag: true,
@@ -1474,9 +2403,24 @@ function WorkflowCanvas({ nodeTypes: nodeTypesProp } = {}) {
1474
2403
  ]
1475
2404
  }
1476
2405
  ),
2406
+ isContextMenuOpen && /* @__PURE__ */ jsx(
2407
+ ContextMenu,
2408
+ {
2409
+ x: contextMenuPosition.x,
2410
+ y: contextMenuPosition.y,
2411
+ items: contextMenuItems,
2412
+ onClose: closeContextMenu
2413
+ }
2414
+ ),
2415
+ /* @__PURE__ */ jsx(EdgeToolbar, {}),
2416
+ /* @__PURE__ */ jsx(MultiSelectToolbar, { onDownloadAsZip }),
1477
2417
  /* @__PURE__ */ jsx(NodeDetailModal, {}),
1478
2418
  /* @__PURE__ */ jsx(ShortcutHelpModal, {}),
1479
- /* @__PURE__ */ jsx(NodeSearch, {})
2419
+ /* @__PURE__ */ jsx(NodeSearch, {}),
2420
+ /* @__PURE__ */ jsx(ConnectionDropMenu, {}),
2421
+ /* @__PURE__ */ jsx(CostModal, {}),
2422
+ /* @__PURE__ */ jsx(NotificationToast, {}),
2423
+ /* @__PURE__ */ jsx(GlobalImageHistory, {})
1480
2424
  ] });
1481
2425
  }
1482
2426
  function PauseEdgeComponent({
@@ -1530,4 +2474,4 @@ function PauseEdgeComponent({
1530
2474
  }
1531
2475
  var PauseEdge = memo(PauseEdgeComponent);
1532
2476
 
1533
- export { DEFAULT_GROUP_COLORS, GROUP_COLORS, GroupOverlay, HelperLines, NodeSearch, PauseEdge, ShortcutHelpModal, WorkflowCanvas };
2477
+ export { ConnectionDropMenu, DEFAULT_GROUP_COLORS, EdgeToolbar, EditableEdge, GROUP_COLORS, GlobalImageHistory, GroupOverlay, HelperLines, NodeSearch, NotificationToast, PauseEdge, ReferenceEdge, ShortcutHelpModal, WorkflowCanvas };