@genfeedai/workflow-ui 0.2.3 → 0.2.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 (45) hide show
  1. package/dist/canvas.d.ts +22 -22
  2. package/dist/canvas.mjs +16 -16
  3. package/dist/{chunk-XPZAHIWY.mjs → chunk-2FUPL67V.mjs} +1592 -1044
  4. package/dist/{chunk-HWVTD2LC.mjs → chunk-53XDE62A.mjs} +818 -623
  5. package/dist/{chunk-PCIWWD37.mjs → chunk-7LV4UAUS.mjs} +19 -19
  6. package/dist/{chunk-7SKSRSS7.mjs → chunk-B4EAAKYF.mjs} +16 -16
  7. package/dist/{chunk-ZJD5WMR3.mjs → chunk-C6MQBJFC.mjs} +45 -13
  8. package/dist/{chunk-7H3WJJYS.mjs → chunk-ESVULCFY.mjs} +12 -6
  9. package/dist/{chunk-GWBGK3KL.mjs → chunk-FWJIAW2E.mjs} +82 -47
  10. package/dist/{chunk-R727OFBR.mjs → chunk-GPYIIWD5.mjs} +404 -350
  11. package/dist/{chunk-OQREHJXK.mjs → chunk-IYFWAJBB.mjs} +208 -203
  12. package/dist/{chunk-N5NJZTK4.mjs → chunk-MGLAKMDP.mjs} +23 -21
  13. package/dist/{chunk-LT3ZJJL6.mjs → chunk-OJWVEEMM.mjs} +497 -399
  14. package/dist/{chunk-ZD2BADZO.mjs → chunk-ORVDYXDP.mjs} +221 -175
  15. package/dist/{chunk-CV4M7CNU.mjs → chunk-QQVHGJ2G.mjs} +149 -142
  16. package/dist/{chunk-6PSJTBNV.mjs → chunk-U4QPE4CY.mjs} +387 -347
  17. package/dist/{chunk-EFXQT23N.mjs → chunk-VVQ4CH77.mjs} +5 -5
  18. package/dist/{chunk-VRN3UWE5.mjs → chunk-XRC3O5GK.mjs} +73 -73
  19. package/dist/{chunk-FT33LFII.mjs → chunk-YUIK4AHM.mjs} +1 -1
  20. package/dist/{chunk-FMJPFB6W.mjs → chunk-ZSITTZ4S.mjs} +630 -569
  21. package/dist/hooks.d.ts +37 -37
  22. package/dist/hooks.mjs +10 -10
  23. package/dist/index.d.ts +26 -11
  24. package/dist/index.mjs +99 -19
  25. package/dist/lib.d.ts +203 -203
  26. package/dist/lib.mjs +228 -198
  27. package/dist/nodes.d.ts +2 -2
  28. package/dist/nodes.mjs +12 -12
  29. package/dist/panels.d.ts +2 -3
  30. package/dist/panels.mjs +3 -3
  31. package/dist/provider.d.ts +2 -2
  32. package/dist/provider.mjs +2 -2
  33. package/dist/stores.d.ts +5 -5
  34. package/dist/stores.mjs +5 -5
  35. package/dist/toolbar.d.ts +42 -24
  36. package/dist/toolbar.mjs +4 -4
  37. package/dist/ui.d.ts +2 -2
  38. package/dist/ui.mjs +2 -2
  39. package/dist/{useCommentNavigation-BakbiiIc.d.ts → useRequiredInputs-ByoIS-fT.d.ts} +160 -160
  40. package/dist/{promptLibraryStore-Dl3Q3cP6.d.ts → workflowStore-Bsz0nd5c.d.ts} +368 -368
  41. package/dist/workflowStore-N2F7WIG3.mjs +2 -0
  42. package/package.json +77 -75
  43. package/src/styles/workflow-ui.css +56 -19
  44. package/dist/workflowStore-UAAKOOIK.mjs +0 -2
  45. package/dist/{types-IEKYuYhu.d.ts → types-CRXJnajq.d.ts} +1 -1
@@ -1,59 +1,382 @@
1
- import { calculateWorkflowCost, formatCost } from './chunk-N5NJZTK4.mjs';
2
- import { useSettingsStore, useExecutionStore, useUIStore } from './chunk-LT3ZJJL6.mjs';
3
- import { useWorkflowStore } from './chunk-R727OFBR.mjs';
4
- import { X, CloudOff, Loader2, Cloud, Check, ChevronDown, SaveAll, Save, FolderOpen, Bug, LayoutGrid, Undo2, Redo2, Settings, AlertCircle, Minus, Plus, Square, Play, ChevronUp, PlayCircle, RotateCcw, DollarSign, MoreVertical } from 'lucide-react';
5
- import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
6
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
1
+ import { calculateWorkflowCost, formatCost } from './chunk-MGLAKMDP.mjs';
2
+ import { useExecutionStore, useUIStore, useSettingsStore } from './chunk-OJWVEEMM.mjs';
3
+ import { useWorkflowStore } from './chunk-GPYIIWD5.mjs';
4
+ import { Minus, Plus, Square, Play, ChevronUp, PlayCircle, RotateCcw, DollarSign, MoreVertical, X, CloudOff, Loader2, Cloud, Check, ChevronDown, SaveAll, Save, FolderOpen, Bug, LayoutGrid, Undo2, Redo2, HelpCircle, Settings, AlertCircle } from 'lucide-react';
5
+ import { useState, useRef, useMemo, useCallback, useEffect } from 'react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
- function SaveAsDialog({ isOpen, currentName, onSave, onClose }) {
9
- const [name, setName] = useState("");
10
- const inputRef = useRef(null);
11
- useEffect(() => {
12
- if (isOpen) {
13
- setName(`${currentName} (copy)`);
14
- setTimeout(() => inputRef.current?.select(), 0);
15
- }
16
- }, [isOpen, currentName]);
17
- const handleSubmit = useCallback(
18
- (e) => {
19
- e.preventDefault();
20
- const trimmed = name.trim();
21
- if (trimmed) {
22
- onSave(trimmed);
8
+ var MIN_BATCH = 1;
9
+ var MAX_BATCH = 10;
10
+ function BottomBar() {
11
+ const [batchCount, setBatchCount] = useState(1);
12
+ const [currentBatchRun, setCurrentBatchRun] = useState(0);
13
+ const [isBatchRunning, setIsBatchRunning] = useState(false);
14
+ const [dropdownOpen, setDropdownOpen] = useState(false);
15
+ const batchCancelledRef = useRef(false);
16
+ const dropdownRef = useRef(null);
17
+ const isRunning = useExecutionStore((s) => s.isRunning);
18
+ const executeWorkflow = useExecutionStore((s) => s.executeWorkflow);
19
+ const executeSelectedNodes = useExecutionStore((s) => s.executeSelectedNodes);
20
+ const resumeFromFailed = useExecutionStore((s) => s.resumeFromFailed);
21
+ const canResumeFromFailed = useExecutionStore((s) => s.canResumeFromFailed);
22
+ const stopExecution = useExecutionStore((s) => s.stopExecution);
23
+ useExecutionStore((s) => s.lastFailedNodeId);
24
+ const selectedNodeIds = useWorkflowStore((s) => s.selectedNodeIds);
25
+ const nodes = useWorkflowStore((s) => s.nodes);
26
+ const validateWorkflow = useWorkflowStore((s) => s.validateWorkflow);
27
+ const canRunWorkflow = useMemo(() => {
28
+ if (nodes.length === 0) return false;
29
+ const validation = validateWorkflow();
30
+ return validation.isValid;
31
+ }, [nodes, validateWorkflow]);
32
+ const hasSelection = selectedNodeIds.length > 0;
33
+ const showResume = canResumeFromFailed();
34
+ const decrementBatch = useCallback(() => {
35
+ setBatchCount((prev) => Math.max(MIN_BATCH, prev - 1));
36
+ }, []);
37
+ const incrementBatch = useCallback(() => {
38
+ setBatchCount((prev) => Math.min(MAX_BATCH, prev + 1));
39
+ }, []);
40
+ const waitForExecutionEnd = useCallback(() => {
41
+ return new Promise((resolve) => {
42
+ if (!useExecutionStore.getState().isRunning) {
43
+ resolve();
44
+ return;
23
45
  }
24
- },
25
- [name, onSave]
26
- );
27
- const handleKeyDown = useCallback(
28
- (e) => {
29
- if (e.key === "Escape") {
30
- onClose();
46
+ const unsubscribe = useExecutionStore.subscribe((state) => {
47
+ if (!state.isRunning) {
48
+ unsubscribe();
49
+ resolve();
50
+ }
51
+ });
52
+ });
53
+ }, []);
54
+ const runBatch = useCallback(async () => {
55
+ batchCancelledRef.current = false;
56
+ setIsBatchRunning(true);
57
+ const accumulatedImages = /* @__PURE__ */ new Map();
58
+ for (let i = 0; i < batchCount; i++) {
59
+ if (batchCancelledRef.current) break;
60
+ setCurrentBatchRun(i + 1);
61
+ executeWorkflow();
62
+ await new Promise((r) => setTimeout(r, 50));
63
+ if (!useExecutionStore.getState().isRunning) break;
64
+ await waitForExecutionEnd();
65
+ if (useExecutionStore.getState().lastFailedNodeId) break;
66
+ const { nodes: currentNodes, updateNodeData } = useWorkflowStore.getState();
67
+ for (const node of currentNodes) {
68
+ if (node.type !== "imageGen") continue;
69
+ const nodeData = node.data;
70
+ const newImages = nodeData.outputImages;
71
+ if (!newImages?.length) continue;
72
+ const existing = accumulatedImages.get(node.id) || [];
73
+ const merged = [...existing, ...newImages];
74
+ accumulatedImages.set(node.id, merged);
75
+ updateNodeData(node.id, { outputImages: merged });
31
76
  }
32
- },
33
- [onClose]
34
- );
35
- if (!isOpen) return null;
36
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
37
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/60", onClick: onClose }),
38
- /* @__PURE__ */ jsxs(
39
- "div",
40
- {
41
- className: "relative z-10 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-xl",
42
- onKeyDown: handleKeyDown,
43
- children: [
44
- /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
45
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-foreground", children: "Save As" }),
77
+ }
78
+ setIsBatchRunning(false);
79
+ setCurrentBatchRun(0);
80
+ }, [batchCount, executeWorkflow, waitForExecutionEnd]);
81
+ const handlePrimaryClick = useCallback(() => {
82
+ if (isRunning || isBatchRunning) {
83
+ batchCancelledRef.current = true;
84
+ stopExecution();
85
+ return;
86
+ }
87
+ if (batchCount > 1) {
88
+ runBatch();
89
+ } else {
90
+ executeWorkflow();
91
+ }
92
+ }, [
93
+ isRunning,
94
+ isBatchRunning,
95
+ batchCount,
96
+ runBatch,
97
+ executeWorkflow,
98
+ stopExecution
99
+ ]);
100
+ const handleRunSelected = useCallback(() => {
101
+ if (!isRunning && hasSelection) {
102
+ executeSelectedNodes();
103
+ setDropdownOpen(false);
104
+ }
105
+ }, [isRunning, hasSelection, executeSelectedNodes]);
106
+ const handleResume = useCallback(() => {
107
+ if (canResumeFromFailed()) {
108
+ resumeFromFailed();
109
+ setDropdownOpen(false);
110
+ }
111
+ }, [canResumeFromFailed, resumeFromFailed]);
112
+ const isActive = isRunning || isBatchRunning;
113
+ return /* @__PURE__ */ jsx(
114
+ "div",
115
+ {
116
+ className: "fixed bottom-5 left-1/2 z-50 -translate-x-1/2",
117
+ onMouseDown: (e) => e.stopPropagation(),
118
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-md border border-neutral-700/80 bg-neutral-800/95 px-2 py-1 shadow-lg backdrop-blur-sm", children: [
119
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
120
+ /* @__PURE__ */ jsx("span", { className: "mr-0.5 text-[11px] text-neutral-400", children: "Batch" }),
121
+ /* @__PURE__ */ jsx(
122
+ "button",
123
+ {
124
+ onClick: decrementBatch,
125
+ disabled: batchCount <= MIN_BATCH || isActive,
126
+ className: "flex h-5 w-5 items-center justify-center rounded text-neutral-400 transition hover:bg-neutral-700 hover:text-white disabled:opacity-40 disabled:hover:bg-transparent",
127
+ children: /* @__PURE__ */ jsx(Minus, { className: "h-2.5 w-2.5" })
128
+ }
129
+ ),
130
+ /* @__PURE__ */ jsx("span", { className: "w-4 text-center text-xs font-medium tabular-nums text-white", children: batchCount }),
131
+ /* @__PURE__ */ jsx(
132
+ "button",
133
+ {
134
+ onClick: incrementBatch,
135
+ disabled: batchCount >= MAX_BATCH || isActive,
136
+ className: "flex h-5 w-5 items-center justify-center rounded text-neutral-400 transition hover:bg-neutral-700 hover:text-white disabled:opacity-40 disabled:hover:bg-transparent",
137
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-2.5 w-2.5" })
138
+ }
139
+ )
140
+ ] }),
141
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-neutral-600" }),
142
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center", children: [
143
+ /* @__PURE__ */ jsx(
144
+ "button",
145
+ {
146
+ onClick: (e) => {
147
+ e.stopPropagation();
148
+ handlePrimaryClick();
149
+ },
150
+ disabled: !isActive && !canRunWorkflow,
151
+ className: `flex h-7 items-center gap-1.5 rounded-l px-3 text-xs font-medium transition ${isActive ? "bg-red-500/90 text-white hover:bg-red-500" : canRunWorkflow ? "bg-white text-black hover:bg-neutral-200" : "bg-neutral-600 text-neutral-400"} disabled:cursor-not-allowed`,
152
+ children: isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
153
+ /* @__PURE__ */ jsx(Square, { className: "h-3.5 w-3.5" }),
154
+ "Stop"
155
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
156
+ /* @__PURE__ */ jsx(Play, { className: "h-3.5 w-3.5 fill-current" }),
157
+ "Run"
158
+ ] })
159
+ }
160
+ ),
161
+ /* @__PURE__ */ jsx(
162
+ "button",
163
+ {
164
+ onPointerDown: (e) => {
165
+ e.stopPropagation();
166
+ e.preventDefault();
167
+ },
168
+ onClick: (e) => {
169
+ e.stopPropagation();
170
+ setDropdownOpen((prev) => !prev);
171
+ },
172
+ disabled: isActive,
173
+ className: `flex h-7 items-center rounded-r border-l px-1.5 transition ${isActive ? "border-red-400/30 bg-red-500/90 text-white" : canRunWorkflow ? "border-neutral-300 bg-white text-black hover:bg-neutral-200" : "border-neutral-500 bg-neutral-600 text-neutral-400"} disabled:cursor-not-allowed`,
174
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-3.5 w-3.5" })
175
+ }
176
+ ),
177
+ dropdownOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
46
178
  /* @__PURE__ */ jsx(
47
- "button",
179
+ "div",
48
180
  {
49
- onClick: onClose,
50
- className: "flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
51
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
181
+ className: "fixed inset-0 z-40",
182
+ onClick: () => setDropdownOpen(false)
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsxs(
186
+ "div",
187
+ {
188
+ ref: dropdownRef,
189
+ onClick: (e) => e.stopPropagation(),
190
+ onPointerDown: (e) => e.stopPropagation(),
191
+ className: "absolute bottom-full left-0 z-50 mb-1.5 min-w-[180px] rounded-md border border-neutral-700 bg-neutral-800 py-0.5 shadow-xl",
192
+ children: [
193
+ /* @__PURE__ */ jsxs(
194
+ "button",
195
+ {
196
+ onClick: () => {
197
+ executeWorkflow();
198
+ setDropdownOpen(false);
199
+ },
200
+ disabled: !canRunWorkflow,
201
+ className: "flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left text-xs text-neutral-200 transition hover:bg-neutral-700 disabled:text-neutral-500 disabled:hover:bg-transparent",
202
+ children: [
203
+ /* @__PURE__ */ jsx(Play, { className: "h-3 w-3" }),
204
+ "Run Workflow"
205
+ ]
206
+ }
207
+ ),
208
+ /* @__PURE__ */ jsxs(
209
+ "button",
210
+ {
211
+ onClick: handleRunSelected,
212
+ disabled: !hasSelection || isRunning,
213
+ className: "flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left text-xs text-neutral-200 transition hover:bg-neutral-700 disabled:text-neutral-500 disabled:hover:bg-transparent",
214
+ children: [
215
+ /* @__PURE__ */ jsx(PlayCircle, { className: "h-3 w-3" }),
216
+ "Run Selected (",
217
+ selectedNodeIds.length,
218
+ ")"
219
+ ]
220
+ }
221
+ ),
222
+ showResume && /* @__PURE__ */ jsxs(Fragment, { children: [
223
+ /* @__PURE__ */ jsx("div", { className: "mx-2 my-0.5 h-px bg-neutral-700" }),
224
+ /* @__PURE__ */ jsxs(
225
+ "button",
226
+ {
227
+ onClick: handleResume,
228
+ className: "flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left text-xs text-neutral-200 transition hover:bg-neutral-700",
229
+ children: [
230
+ /* @__PURE__ */ jsx(RotateCcw, { className: "h-3 w-3" }),
231
+ "Resume from Failed"
232
+ ]
233
+ }
234
+ )
235
+ ] })
236
+ ]
52
237
  }
53
238
  )
54
- ] }),
55
- /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, children: [
56
- /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
239
+ ] })
240
+ ] }),
241
+ isBatchRunning && /* @__PURE__ */ jsxs(Fragment, { children: [
242
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-neutral-600" }),
243
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] tabular-nums text-neutral-400", children: [
244
+ currentBatchRun,
245
+ "/",
246
+ batchCount
247
+ ] })
248
+ ] })
249
+ ] })
250
+ }
251
+ );
252
+ }
253
+ function CostIndicator() {
254
+ const nodes = useWorkflowStore((state) => state.nodes);
255
+ const isRunning = useExecutionStore((state) => state.isRunning);
256
+ const actualCost = useExecutionStore((state) => state.actualCost);
257
+ const { openModal } = useUIStore();
258
+ const breakdown = useMemo(() => calculateWorkflowCost(nodes), [nodes]);
259
+ const displayCost = isRunning && actualCost > 0 ? actualCost : breakdown.total;
260
+ if (breakdown.items.length === 0) return null;
261
+ return /* @__PURE__ */ jsxs(
262
+ "button",
263
+ {
264
+ onClick: () => openModal("cost"),
265
+ title: "View cost breakdown",
266
+ className: "flex items-center gap-1.5 rounded-md border border-[var(--border)] px-2 py-1 text-sm text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
267
+ children: [
268
+ /* @__PURE__ */ jsx(DollarSign, { className: "h-3.5 w-3.5" }),
269
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: formatCost(displayCost) }),
270
+ isRunning && actualCost > 0 && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" })
271
+ ]
272
+ }
273
+ );
274
+ }
275
+ function OverflowMenu({ items }) {
276
+ const [isOpen, setIsOpen] = useState(false);
277
+ const menuRef = useRef(null);
278
+ useEffect(() => {
279
+ if (!isOpen) return;
280
+ const handleClickOutside = (event) => {
281
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
282
+ setIsOpen(false);
283
+ }
284
+ };
285
+ document.addEventListener("mousedown", handleClickOutside);
286
+ return () => document.removeEventListener("mousedown", handleClickOutside);
287
+ }, [isOpen]);
288
+ useEffect(() => {
289
+ if (!isOpen) return;
290
+ const handleKeyDown = (event) => {
291
+ if (event.key === "Escape") {
292
+ setIsOpen(false);
293
+ }
294
+ };
295
+ document.addEventListener("keydown", handleKeyDown);
296
+ return () => document.removeEventListener("keydown", handleKeyDown);
297
+ }, [isOpen]);
298
+ return /* @__PURE__ */ jsxs("div", { ref: menuRef, className: "relative", children: [
299
+ /* @__PURE__ */ jsx(
300
+ "button",
301
+ {
302
+ onClick: () => setIsOpen(!isOpen),
303
+ title: "More options",
304
+ className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
305
+ children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" })
306
+ }
307
+ ),
308
+ isOpen && /* @__PURE__ */ jsx("div", { className: "absolute right-0 top-full z-50 mt-1 min-w-[180px] rounded-lg border border-border bg-card py-1 shadow-lg", children: items.map((item) => /* @__PURE__ */ jsxs(
309
+ "button",
310
+ {
311
+ onClick: () => {
312
+ item.onClick?.();
313
+ setIsOpen(false);
314
+ },
315
+ className: "flex w-full items-center gap-2.5 px-3 py-2 text-sm text-foreground transition hover:bg-secondary",
316
+ children: [
317
+ /* @__PURE__ */ jsx("span", { className: "h-4 w-4 shrink-0", children: item.icon }),
318
+ /* @__PURE__ */ jsx("span", { children: item.label }),
319
+ item.external && /* @__PURE__ */ jsx("span", { className: "ml-auto text-xs text-muted-foreground", children: "\u2197" })
320
+ ]
321
+ },
322
+ item.id
323
+ )) })
324
+ ] });
325
+ }
326
+ function SaveAsDialog({
327
+ isOpen,
328
+ currentName,
329
+ onSave,
330
+ onClose
331
+ }) {
332
+ const [name, setName] = useState("");
333
+ const inputRef = useRef(null);
334
+ useEffect(() => {
335
+ if (isOpen) {
336
+ setName(`${currentName} (copy)`);
337
+ setTimeout(() => inputRef.current?.select(), 0);
338
+ }
339
+ }, [isOpen, currentName]);
340
+ const handleSubmit = useCallback(
341
+ (e) => {
342
+ e.preventDefault();
343
+ const trimmed = name.trim();
344
+ if (trimmed) {
345
+ onSave(trimmed);
346
+ }
347
+ },
348
+ [name, onSave]
349
+ );
350
+ const handleKeyDown = useCallback(
351
+ (e) => {
352
+ if (e.key === "Escape") {
353
+ onClose();
354
+ }
355
+ },
356
+ [onClose]
357
+ );
358
+ if (!isOpen) return null;
359
+ return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
360
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/60", onClick: onClose }),
361
+ /* @__PURE__ */ jsxs(
362
+ "div",
363
+ {
364
+ className: "relative z-10 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-xl",
365
+ onKeyDown: handleKeyDown,
366
+ children: [
367
+ /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
368
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-foreground", children: "Save As" }),
369
+ /* @__PURE__ */ jsx(
370
+ "button",
371
+ {
372
+ onClick: onClose,
373
+ className: "flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
374
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
375
+ }
376
+ )
377
+ ] }),
378
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, children: [
379
+ /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
57
380
  /* @__PURE__ */ jsx(
58
381
  "label",
59
382
  {
@@ -102,18 +425,25 @@ function SaveAsDialog({ isOpen, currentName, onSave, onClose }) {
102
425
  )
103
426
  ] });
104
427
  }
105
- function SaveIndicator() {
106
- const isDirty = useWorkflowStore((state) => state.isDirty);
107
- const isSaving = useWorkflowStore((state) => state.isSaving);
428
+ function SaveIndicator({
429
+ isDirty: isDirtyProp,
430
+ isSaving: isSavingProp,
431
+ variant = "default"
432
+ }) {
433
+ const storeIsDirty = useWorkflowStore((state) => state.isDirty);
434
+ const storeIsSaving = useWorkflowStore((state) => state.isSaving);
108
435
  const autoSaveEnabled = useSettingsStore((state) => state.autoSaveEnabled);
109
436
  const toggleAutoSave = useSettingsStore((state) => state.toggleAutoSave);
437
+ const isDirty = isDirtyProp ?? storeIsDirty;
438
+ const isSaving = isSavingProp ?? storeIsSaving;
439
+ const isPill = variant === "pill";
110
440
  if (!autoSaveEnabled) {
111
441
  return /* @__PURE__ */ jsxs(
112
442
  "button",
113
443
  {
114
444
  onClick: toggleAutoSave,
115
445
  title: "Click to enable auto-save",
116
- className: "flex items-center gap-1.5 text-muted-foreground text-xs hover:text-foreground transition-colors",
446
+ className: "flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground",
117
447
  children: [
118
448
  /* @__PURE__ */ jsx(CloudOff, { className: "h-3.5 w-3.5" }),
119
449
  /* @__PURE__ */ jsx("span", { children: "Auto-save off" })
@@ -122,23 +452,35 @@ function SaveIndicator() {
122
452
  );
123
453
  }
124
454
  if (isSaving) {
125
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-blue-500 text-xs", children: [
126
- /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
127
- /* @__PURE__ */ jsx("span", { children: "Saving..." })
128
- ] });
455
+ return /* @__PURE__ */ jsxs(
456
+ "div",
457
+ {
458
+ className: isPill ? "flex items-center gap-1.5 rounded-full border border-blue-500/20 bg-blue-500/10 px-2.5 py-1 text-xs text-blue-400" : "flex items-center gap-1.5 text-xs text-blue-500",
459
+ children: [
460
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
461
+ /* @__PURE__ */ jsx("span", { children: "Saving..." })
462
+ ]
463
+ }
464
+ );
129
465
  }
130
466
  if (isDirty) {
131
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground text-xs", children: [
132
- /* @__PURE__ */ jsx(Cloud, { className: "h-3.5 w-3.5" }),
133
- /* @__PURE__ */ jsx("span", { children: "Unsaved" })
134
- ] });
467
+ return /* @__PURE__ */ jsxs(
468
+ "div",
469
+ {
470
+ className: isPill ? "flex items-center gap-1.5 rounded-full border border-border bg-secondary/40 px-2.5 py-1 text-xs text-muted-foreground" : "flex items-center gap-1.5 text-xs text-muted-foreground",
471
+ children: [
472
+ /* @__PURE__ */ jsx(Cloud, { className: "h-3.5 w-3.5" }),
473
+ /* @__PURE__ */ jsx("span", { children: "Unsaved" })
474
+ ]
475
+ }
476
+ );
135
477
  }
136
478
  return /* @__PURE__ */ jsxs(
137
479
  "button",
138
480
  {
139
481
  onClick: toggleAutoSave,
140
482
  title: "Click to disable auto-save",
141
- className: "flex items-center gap-1.5 text-green-500 text-xs hover:text-green-400 transition-colors",
483
+ className: isPill ? "flex items-center gap-1.5 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-2.5 py-1 text-xs text-emerald-400 transition-colors hover:text-emerald-300" : "flex items-center gap-1.5 text-xs text-green-500 transition-colors hover:text-green-400",
142
484
  children: [
143
485
  /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
144
486
  /* @__PURE__ */ jsx("span", { children: "Saved" })
@@ -234,526 +576,245 @@ function Toolbar({
234
576
  fileMenuItemsPrepend,
235
577
  fileMenuItemsAppend,
236
578
  additionalMenus,
237
- logoHref = "/",
238
- logoSrc = "https://cdn.genfeed.ai/assets/branding/logo-white.png",
239
- showSettings = true,
240
- rightContent
241
- }) {
242
- const { exportWorkflow, workflowName } = useWorkflowStore();
243
- const { undo, redo } = useWorkflowStore.temporal.getState();
244
- const [canUndo, setCanUndo] = useState(false);
245
- const [canRedo, setCanRedo] = useState(false);
246
- const [showSaveAsDialog, setShowSaveAsDialog] = useState(false);
247
- const validationErrors = useExecutionStore((state) => state.validationErrors);
248
- const clearValidationErrors = useExecutionStore((state) => state.clearValidationErrors);
249
- const { openModal } = useUIStore();
250
- const debugMode = useSettingsStore((s) => s.debugMode);
251
- const uniqueErrorMessages = useMemo(() => {
252
- if (!validationErrors?.errors.length) return [];
253
- return [...new Set(validationErrors.errors.map((e) => e.message))];
254
- }, [validationErrors]);
255
- useEffect(() => {
256
- const unsubscribe = useWorkflowStore.temporal.subscribe((state) => {
257
- setCanUndo(state.pastStates.length > 0);
258
- setCanRedo(state.futureStates.length > 0);
259
- });
260
- const temporal = useWorkflowStore.temporal.getState();
261
- setCanUndo(temporal.pastStates.length > 0);
262
- setCanRedo(temporal.futureStates.length > 0);
263
- return unsubscribe;
264
- }, []);
265
- const handleExport = useCallback(() => {
266
- const workflow = exportWorkflow();
267
- const blob = new Blob([JSON.stringify(workflow, null, 2)], {
268
- type: "application/json"
269
- });
270
- const url = URL.createObjectURL(blob);
271
- const link = document.createElement("a");
272
- link.href = url;
273
- link.download = `${workflow.name.toLowerCase().replace(/\s+/g, "-")}.json`;
274
- document.body.appendChild(link);
275
- link.click();
276
- document.body.removeChild(link);
277
- URL.revokeObjectURL(url);
278
- }, [exportWorkflow]);
279
- const handleImport = useCallback(() => {
280
- const input = document.createElement("input");
281
- input.type = "file";
282
- input.accept = ".json";
283
- input.onchange = (e) => {
284
- const file = e.target.files?.[0];
285
- if (!file) return;
286
- const reader = new FileReader();
287
- reader.onload = (event) => {
288
- try {
289
- const data = JSON.parse(event.target?.result);
290
- if (!isValidWorkflow(data)) {
291
- console.warn("[Toolbar] Invalid workflow file structure");
292
- return;
293
- }
294
- useWorkflowStore.getState().loadWorkflow(data);
295
- } catch {
296
- console.warn("[Toolbar] Failed to parse workflow file");
297
- }
298
- };
299
- reader.readAsText(file);
300
- };
301
- input.click();
302
- }, []);
303
- const handleSaveAs = useCallback(
304
- (newName) => {
305
- if (onSaveAs) {
306
- onSaveAs(newName);
307
- }
308
- setShowSaveAsDialog(false);
309
- },
310
- [onSaveAs]
311
- );
312
- const fileMenuItems = useMemo(() => {
313
- const items = [];
314
- if (fileMenuItemsPrepend?.length) {
315
- items.push(...fileMenuItemsPrepend);
316
- items.push({ id: "separator-prepend", separator: true });
317
- }
318
- if (onSaveAs) {
319
- items.push({
320
- id: "saveAs",
321
- label: "Save As...",
322
- icon: /* @__PURE__ */ jsx(SaveAll, { className: "h-4 w-4" }),
323
- onClick: () => setShowSaveAsDialog(true)
324
- });
325
- items.push({ id: "separator-saveas", separator: true });
326
- }
327
- items.push(
328
- {
329
- id: "export",
330
- label: "Export Workflow",
331
- icon: /* @__PURE__ */ jsx(Save, { className: "h-4 w-4" }),
332
- onClick: handleExport
333
- },
334
- {
335
- id: "import",
336
- label: "Import Workflow",
337
- icon: /* @__PURE__ */ jsx(FolderOpen, { className: "h-4 w-4" }),
338
- onClick: handleImport
339
- }
340
- );
341
- if (fileMenuItemsAppend?.length) {
342
- items.push({ id: "separator-append", separator: true });
343
- items.push(...fileMenuItemsAppend);
344
- }
345
- return items;
346
- }, [handleExport, handleImport, onSaveAs, fileMenuItemsPrepend, fileMenuItemsAppend]);
347
- return /* @__PURE__ */ jsxs("div", { className: "flex h-14 items-center gap-3 border-b border-border bg-card px-4", children: [
348
- /* @__PURE__ */ jsx(
349
- "a",
350
- {
351
- href: logoHref,
352
- title: "Go to Dashboard",
353
- className: "flex h-6 w-6 items-center justify-center hover:opacity-90 transition",
354
- children: /* @__PURE__ */ jsx(
355
- "img",
356
- {
357
- src: logoSrc,
358
- alt: "Genfeed",
359
- className: "h-6 w-6 object-contain"
360
- }
361
- )
362
- }
363
- ),
364
- /* @__PURE__ */ jsx("div", { className: "h-8 w-px bg-border" }),
365
- /* @__PURE__ */ jsx(ToolbarDropdown, { label: "File", items: fileMenuItems }),
366
- additionalMenus?.map((menu) => /* @__PURE__ */ jsx(ToolbarDropdown, { label: menu.label, items: menu.items }, menu.label)),
367
- /* @__PURE__ */ jsx("div", { className: "h-8 w-px bg-border" }),
368
- debugMode && /* @__PURE__ */ jsxs(
369
- "button",
370
- {
371
- onClick: () => openModal("settings"),
372
- title: "Debug mode active - API calls are mocked",
373
- className: "flex items-center gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1 text-sm text-amber-500 transition hover:bg-amber-500/20",
374
- children: [
375
- /* @__PURE__ */ jsx(Bug, { className: "h-4 w-4" }),
376
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Debug" })
377
- ]
378
- }
379
- ),
380
- onAutoLayout && /* @__PURE__ */ jsx(
381
- "button",
382
- {
383
- onClick: () => onAutoLayout("LR"),
384
- title: "Auto-layout nodes",
385
- className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
386
- children: /* @__PURE__ */ jsx(LayoutGrid, { className: "h-4 w-4" })
387
- }
388
- ),
389
- /* @__PURE__ */ jsx(
390
- "button",
391
- {
392
- onClick: () => undo(),
393
- disabled: !canUndo,
394
- title: "Undo (Ctrl+Z)",
395
- className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent",
396
- children: /* @__PURE__ */ jsx(Undo2, { className: "h-4 w-4" })
397
- }
398
- ),
399
- /* @__PURE__ */ jsx(
400
- "button",
401
- {
402
- onClick: () => redo(),
403
- disabled: !canRedo,
404
- title: "Redo (Ctrl+Shift+Z)",
405
- className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent",
406
- children: /* @__PURE__ */ jsx(Redo2, { className: "h-4 w-4" })
407
- }
408
- ),
409
- /* @__PURE__ */ jsx(SaveIndicator, {}),
410
- /* @__PURE__ */ jsx("div", { className: "flex-1" }),
411
- rightContent,
412
- showSettings && /* @__PURE__ */ jsx(
413
- "button",
414
- {
415
- onClick: () => openModal("settings"),
416
- title: "Settings",
417
- className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
418
- children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
419
- }
420
- ),
421
- uniqueErrorMessages.length > 0 && /* @__PURE__ */ jsx("div", { className: "fixed right-4 top-20 z-50 max-w-sm rounded-lg border border-destructive/30 bg-destructive/10 p-4 shadow-xl", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
422
- /* @__PURE__ */ jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 shrink-0 text-destructive" }),
423
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
424
- /* @__PURE__ */ jsx("h4", { className: "mb-2 text-sm font-medium text-destructive", children: "Cannot run workflow" }),
425
- /* @__PURE__ */ jsxs("ul", { className: "space-y-1", children: [
426
- uniqueErrorMessages.slice(0, 5).map((message) => /* @__PURE__ */ jsx("li", { className: "text-xs text-destructive/80", children: message }, message)),
427
- uniqueErrorMessages.length > 5 && /* @__PURE__ */ jsxs("li", { className: "text-xs text-destructive/60", children: [
428
- "+",
429
- uniqueErrorMessages.length - 5,
430
- " more errors"
431
- ] })
432
- ] })
433
- ] }),
434
- /* @__PURE__ */ jsx(
435
- "button",
436
- {
437
- onClick: clearValidationErrors,
438
- className: "flex h-7 w-7 items-center justify-center rounded-md text-destructive transition hover:bg-destructive/20",
439
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
440
- }
441
- )
442
- ] }) }),
443
- /* @__PURE__ */ jsx(
444
- SaveAsDialog,
445
- {
446
- isOpen: showSaveAsDialog,
447
- currentName: workflowName,
448
- onSave: handleSaveAs,
449
- onClose: () => setShowSaveAsDialog(false)
450
- }
451
- )
452
- ] });
453
- }
454
- var MIN_BATCH = 1;
455
- var MAX_BATCH = 10;
456
- function BottomBar() {
457
- const [batchCount, setBatchCount] = useState(1);
458
- const [currentBatchRun, setCurrentBatchRun] = useState(0);
459
- const [isBatchRunning, setIsBatchRunning] = useState(false);
460
- const [dropdownOpen, setDropdownOpen] = useState(false);
461
- const batchCancelledRef = useRef(false);
462
- const dropdownRef = useRef(null);
463
- const isRunning = useExecutionStore((s) => s.isRunning);
464
- const executeWorkflow = useExecutionStore((s) => s.executeWorkflow);
465
- const executeSelectedNodes = useExecutionStore((s) => s.executeSelectedNodes);
466
- const resumeFromFailed = useExecutionStore((s) => s.resumeFromFailed);
467
- const canResumeFromFailed = useExecutionStore((s) => s.canResumeFromFailed);
468
- const stopExecution = useExecutionStore((s) => s.stopExecution);
469
- useExecutionStore((s) => s.lastFailedNodeId);
470
- const selectedNodeIds = useWorkflowStore((s) => s.selectedNodeIds);
471
- const nodes = useWorkflowStore((s) => s.nodes);
472
- const validateWorkflow = useWorkflowStore((s) => s.validateWorkflow);
473
- const canRunWorkflow = useMemo(() => {
474
- if (nodes.length === 0) return false;
475
- const validation = validateWorkflow();
476
- return validation.isValid;
477
- }, [nodes, validateWorkflow]);
478
- const hasSelection = selectedNodeIds.length > 0;
479
- const showResume = canResumeFromFailed();
480
- const decrementBatch = useCallback(() => {
481
- setBatchCount((prev) => Math.max(MIN_BATCH, prev - 1));
482
- }, []);
483
- const incrementBatch = useCallback(() => {
484
- setBatchCount((prev) => Math.min(MAX_BATCH, prev + 1));
485
- }, []);
486
- const waitForExecutionEnd = useCallback(() => {
487
- return new Promise((resolve) => {
488
- if (!useExecutionStore.getState().isRunning) {
489
- resolve();
490
- return;
491
- }
492
- const unsubscribe = useExecutionStore.subscribe((state) => {
493
- if (!state.isRunning) {
494
- unsubscribe();
495
- resolve();
496
- }
497
- });
498
- });
499
- }, []);
500
- const runBatch = useCallback(async () => {
501
- batchCancelledRef.current = false;
502
- setIsBatchRunning(true);
503
- const accumulatedImages = /* @__PURE__ */ new Map();
504
- for (let i = 0; i < batchCount; i++) {
505
- if (batchCancelledRef.current) break;
506
- setCurrentBatchRun(i + 1);
507
- executeWorkflow();
508
- await new Promise((r) => setTimeout(r, 50));
509
- if (!useExecutionStore.getState().isRunning) break;
510
- await waitForExecutionEnd();
511
- if (useExecutionStore.getState().lastFailedNodeId) break;
512
- const { nodes: currentNodes, updateNodeData } = useWorkflowStore.getState();
513
- for (const node of currentNodes) {
514
- if (node.type !== "imageGen") continue;
515
- const nodeData = node.data;
516
- const newImages = nodeData.outputImages;
517
- if (!newImages?.length) continue;
518
- const existing = accumulatedImages.get(node.id) || [];
519
- const merged = [...existing, ...newImages];
520
- accumulatedImages.set(node.id, merged);
521
- updateNodeData(node.id, { outputImages: merged });
522
- }
523
- }
524
- setIsBatchRunning(false);
525
- setCurrentBatchRun(0);
526
- }, [batchCount, executeWorkflow, waitForExecutionEnd]);
527
- const handlePrimaryClick = useCallback(() => {
528
- if (isRunning || isBatchRunning) {
529
- batchCancelledRef.current = true;
530
- stopExecution();
531
- return;
532
- }
533
- if (batchCount > 1) {
534
- runBatch();
535
- } else {
536
- executeWorkflow();
537
- }
538
- }, [isRunning, isBatchRunning, batchCount, runBatch, executeWorkflow, stopExecution]);
539
- const handleRunSelected = useCallback(() => {
540
- if (!isRunning && hasSelection) {
541
- executeSelectedNodes();
542
- setDropdownOpen(false);
543
- }
544
- }, [isRunning, hasSelection, executeSelectedNodes]);
545
- const handleResume = useCallback(() => {
546
- if (canResumeFromFailed()) {
547
- resumeFromFailed();
548
- setDropdownOpen(false);
549
- }
550
- }, [canResumeFromFailed, resumeFromFailed]);
551
- const isActive = isRunning || isBatchRunning;
552
- return /* @__PURE__ */ jsx(
553
- "div",
554
- {
555
- className: "fixed bottom-5 left-1/2 z-50 -translate-x-1/2",
556
- onMouseDown: (e) => e.stopPropagation(),
557
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-md border border-neutral-700/80 bg-neutral-800/95 px-2 py-1 shadow-lg backdrop-blur-sm", children: [
558
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
559
- /* @__PURE__ */ jsx("span", { className: "mr-0.5 text-[11px] text-neutral-400", children: "Batch" }),
560
- /* @__PURE__ */ jsx(
561
- "button",
562
- {
563
- onClick: decrementBatch,
564
- disabled: batchCount <= MIN_BATCH || isActive,
565
- className: "flex h-5 w-5 items-center justify-center rounded text-neutral-400 transition hover:bg-neutral-700 hover:text-white disabled:opacity-40 disabled:hover:bg-transparent",
566
- children: /* @__PURE__ */ jsx(Minus, { className: "h-2.5 w-2.5" })
567
- }
568
- ),
569
- /* @__PURE__ */ jsx("span", { className: "w-4 text-center text-xs font-medium tabular-nums text-white", children: batchCount }),
570
- /* @__PURE__ */ jsx(
571
- "button",
572
- {
573
- onClick: incrementBatch,
574
- disabled: batchCount >= MAX_BATCH || isActive,
575
- className: "flex h-5 w-5 items-center justify-center rounded text-neutral-400 transition hover:bg-neutral-700 hover:text-white disabled:opacity-40 disabled:hover:bg-transparent",
576
- children: /* @__PURE__ */ jsx(Plus, { className: "h-2.5 w-2.5" })
577
- }
578
- )
579
- ] }),
580
- /* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-neutral-600" }),
581
- /* @__PURE__ */ jsxs("div", { className: "relative flex items-center", children: [
582
- /* @__PURE__ */ jsx(
583
- "button",
584
- {
585
- onClick: (e) => {
586
- e.stopPropagation();
587
- handlePrimaryClick();
588
- },
589
- disabled: !isActive && !canRunWorkflow,
590
- className: `flex h-7 items-center gap-1.5 rounded-l px-3 text-xs font-medium transition ${isActive ? "bg-red-500/90 text-white hover:bg-red-500" : canRunWorkflow ? "bg-white text-black hover:bg-neutral-200" : "bg-neutral-600 text-neutral-400"} disabled:cursor-not-allowed`,
591
- children: isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
592
- /* @__PURE__ */ jsx(Square, { className: "h-3.5 w-3.5" }),
593
- "Stop"
594
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
595
- /* @__PURE__ */ jsx(Play, { className: "h-3.5 w-3.5 fill-current" }),
596
- "Run"
597
- ] })
598
- }
599
- ),
600
- /* @__PURE__ */ jsx(
601
- "button",
602
- {
603
- onPointerDown: (e) => {
604
- e.stopPropagation();
605
- e.preventDefault();
606
- },
607
- onClick: (e) => {
608
- e.stopPropagation();
609
- setDropdownOpen((prev) => !prev);
610
- },
611
- disabled: isActive,
612
- className: `flex h-7 items-center rounded-r border-l px-1.5 transition ${isActive ? "border-red-400/30 bg-red-500/90 text-white" : canRunWorkflow ? "border-neutral-300 bg-white text-black hover:bg-neutral-200" : "border-neutral-500 bg-neutral-600 text-neutral-400"} disabled:cursor-not-allowed`,
613
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-3.5 w-3.5" })
614
- }
615
- ),
616
- dropdownOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
617
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-40", onClick: () => setDropdownOpen(false) }),
618
- /* @__PURE__ */ jsxs(
619
- "div",
620
- {
621
- ref: dropdownRef,
622
- onClick: (e) => e.stopPropagation(),
623
- onPointerDown: (e) => e.stopPropagation(),
624
- className: "absolute bottom-full left-0 z-50 mb-1.5 min-w-[180px] rounded-md border border-neutral-700 bg-neutral-800 py-0.5 shadow-xl",
625
- children: [
626
- /* @__PURE__ */ jsxs(
627
- "button",
628
- {
629
- onClick: () => {
630
- executeWorkflow();
631
- setDropdownOpen(false);
632
- },
633
- disabled: !canRunWorkflow,
634
- className: "flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left text-xs text-neutral-200 transition hover:bg-neutral-700 disabled:text-neutral-500 disabled:hover:bg-transparent",
635
- children: [
636
- /* @__PURE__ */ jsx(Play, { className: "h-3 w-3" }),
637
- "Run Workflow"
638
- ]
639
- }
640
- ),
641
- /* @__PURE__ */ jsxs(
642
- "button",
643
- {
644
- onClick: handleRunSelected,
645
- disabled: !hasSelection || isRunning,
646
- className: "flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left text-xs text-neutral-200 transition hover:bg-neutral-700 disabled:text-neutral-500 disabled:hover:bg-transparent",
647
- children: [
648
- /* @__PURE__ */ jsx(PlayCircle, { className: "h-3 w-3" }),
649
- "Run Selected (",
650
- selectedNodeIds.length,
651
- ")"
652
- ]
653
- }
654
- ),
655
- showResume && /* @__PURE__ */ jsxs(Fragment, { children: [
656
- /* @__PURE__ */ jsx("div", { className: "mx-2 my-0.5 h-px bg-neutral-700" }),
657
- /* @__PURE__ */ jsxs(
658
- "button",
659
- {
660
- onClick: handleResume,
661
- className: "flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left text-xs text-neutral-200 transition hover:bg-neutral-700",
662
- children: [
663
- /* @__PURE__ */ jsx(RotateCcw, { className: "h-3 w-3" }),
664
- "Resume from Failed"
665
- ]
666
- }
667
- )
668
- ] })
669
- ]
670
- }
671
- )
672
- ] })
673
- ] }),
674
- isBatchRunning && /* @__PURE__ */ jsxs(Fragment, { children: [
675
- /* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-neutral-600" }),
676
- /* @__PURE__ */ jsxs("span", { className: "text-[11px] tabular-nums text-neutral-400", children: [
677
- currentBatchRun,
678
- "/",
679
- batchCount
680
- ] })
681
- ] })
682
- ] })
683
- }
579
+ branding,
580
+ leftContent,
581
+ middleContent,
582
+ saveIndicator,
583
+ logoHref = "/",
584
+ logoSrc = "https://cdn.genfeed.ai/assets/branding/logo-white.png",
585
+ showSettings = true,
586
+ showShortcutHelp = false,
587
+ rightContent
588
+ }) {
589
+ const { exportWorkflow, workflowName } = useWorkflowStore();
590
+ const { undo, redo } = useWorkflowStore.temporal.getState();
591
+ const [canUndo, setCanUndo] = useState(false);
592
+ const [canRedo, setCanRedo] = useState(false);
593
+ const [showSaveAsDialog, setShowSaveAsDialog] = useState(false);
594
+ const validationErrors = useExecutionStore((state) => state.validationErrors);
595
+ const clearValidationErrors = useExecutionStore(
596
+ (state) => state.clearValidationErrors
684
597
  );
685
- }
686
- function CostIndicator() {
687
- const nodes = useWorkflowStore((state) => state.nodes);
688
- const isRunning = useExecutionStore((state) => state.isRunning);
689
- const actualCost = useExecutionStore((state) => state.actualCost);
690
598
  const { openModal } = useUIStore();
691
- const breakdown = useMemo(() => calculateWorkflowCost(nodes), [nodes]);
692
- const displayCost = isRunning && actualCost > 0 ? actualCost : breakdown.total;
693
- if (breakdown.items.length === 0) return null;
694
- return /* @__PURE__ */ jsxs(
695
- "button",
696
- {
697
- onClick: () => openModal("cost"),
698
- title: "View cost breakdown",
699
- className: "flex items-center gap-1.5 rounded-md border border-[var(--border)] px-2 py-1 text-sm text-[var(--muted-foreground)] transition hover:bg-[var(--secondary)] hover:text-[var(--foreground)]",
700
- children: [
701
- /* @__PURE__ */ jsx(DollarSign, { className: "h-3.5 w-3.5" }),
702
- /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: formatCost(displayCost) }),
703
- isRunning && actualCost > 0 && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" })
704
- ]
705
- }
706
- );
707
- }
708
- function OverflowMenu({ items }) {
709
- const [isOpen, setIsOpen] = useState(false);
710
- const menuRef = useRef(null);
599
+ const debugMode = useSettingsStore((s) => s.debugMode);
600
+ const uniqueErrorMessages = useMemo(() => {
601
+ if (!validationErrors?.errors.length) return [];
602
+ return [...new Set(validationErrors.errors.map((e) => e.message))];
603
+ }, [validationErrors]);
711
604
  useEffect(() => {
712
- if (!isOpen) return;
713
- const handleClickOutside = (event) => {
714
- if (menuRef.current && !menuRef.current.contains(event.target)) {
715
- setIsOpen(false);
716
- }
605
+ const unsubscribe = useWorkflowStore.temporal.subscribe((state) => {
606
+ setCanUndo(state.pastStates.length > 0);
607
+ setCanRedo(state.futureStates.length > 0);
608
+ });
609
+ const temporal = useWorkflowStore.temporal.getState();
610
+ setCanUndo(temporal.pastStates.length > 0);
611
+ setCanRedo(temporal.futureStates.length > 0);
612
+ return unsubscribe;
613
+ }, []);
614
+ const handleExport = useCallback(() => {
615
+ const workflow = exportWorkflow();
616
+ const blob = new Blob([JSON.stringify(workflow, null, 2)], {
617
+ type: "application/json"
618
+ });
619
+ const url = URL.createObjectURL(blob);
620
+ const link = document.createElement("a");
621
+ link.href = url;
622
+ link.download = `${workflow.name.toLowerCase().replace(/\s+/g, "-")}.json`;
623
+ document.body.appendChild(link);
624
+ link.click();
625
+ document.body.removeChild(link);
626
+ URL.revokeObjectURL(url);
627
+ }, [exportWorkflow]);
628
+ const handleImport = useCallback(() => {
629
+ const input = document.createElement("input");
630
+ input.type = "file";
631
+ input.accept = ".json";
632
+ input.onchange = (e) => {
633
+ const file = e.target.files?.[0];
634
+ if (!file) return;
635
+ const reader = new FileReader();
636
+ reader.onload = (event) => {
637
+ try {
638
+ const data = JSON.parse(event.target?.result);
639
+ if (!isValidWorkflow(data)) {
640
+ console.warn("[Toolbar] Invalid workflow file structure");
641
+ return;
642
+ }
643
+ useWorkflowStore.getState().loadWorkflow(data);
644
+ } catch {
645
+ console.warn("[Toolbar] Failed to parse workflow file");
646
+ }
647
+ };
648
+ reader.readAsText(file);
717
649
  };
718
- document.addEventListener("mousedown", handleClickOutside);
719
- return () => document.removeEventListener("mousedown", handleClickOutside);
720
- }, [isOpen]);
721
- useEffect(() => {
722
- if (!isOpen) return;
723
- const handleKeyDown = (event) => {
724
- if (event.key === "Escape") {
725
- setIsOpen(false);
650
+ input.click();
651
+ }, []);
652
+ const handleSaveAs = useCallback(
653
+ (newName) => {
654
+ if (onSaveAs) {
655
+ onSaveAs(newName);
726
656
  }
727
- };
728
- document.addEventListener("keydown", handleKeyDown);
729
- return () => document.removeEventListener("keydown", handleKeyDown);
730
- }, [isOpen]);
731
- return /* @__PURE__ */ jsxs("div", { ref: menuRef, className: "relative", children: [
657
+ setShowSaveAsDialog(false);
658
+ },
659
+ [onSaveAs]
660
+ );
661
+ const fileMenuItems = useMemo(() => {
662
+ const items = [];
663
+ if (fileMenuItemsPrepend?.length) {
664
+ items.push(...fileMenuItemsPrepend);
665
+ items.push({ id: "separator-prepend", separator: true });
666
+ }
667
+ if (onSaveAs) {
668
+ items.push({
669
+ icon: /* @__PURE__ */ jsx(SaveAll, { className: "h-4 w-4" }),
670
+ id: "saveAs",
671
+ label: "Save As...",
672
+ onClick: () => setShowSaveAsDialog(true)
673
+ });
674
+ items.push({ id: "separator-saveas", separator: true });
675
+ }
676
+ items.push(
677
+ {
678
+ icon: /* @__PURE__ */ jsx(Save, { className: "h-4 w-4" }),
679
+ id: "export",
680
+ label: "Export Workflow",
681
+ onClick: handleExport
682
+ },
683
+ {
684
+ icon: /* @__PURE__ */ jsx(FolderOpen, { className: "h-4 w-4" }),
685
+ id: "import",
686
+ label: "Import Workflow",
687
+ onClick: handleImport
688
+ }
689
+ );
690
+ if (fileMenuItemsAppend?.length) {
691
+ items.push({ id: "separator-append", separator: true });
692
+ items.push(...fileMenuItemsAppend);
693
+ }
694
+ return items;
695
+ }, [
696
+ handleExport,
697
+ handleImport,
698
+ onSaveAs,
699
+ fileMenuItemsPrepend,
700
+ fileMenuItemsAppend
701
+ ]);
702
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-14 items-center gap-3 border-b border-border bg-card px-4", children: [
703
+ branding ?? /* @__PURE__ */ jsx(
704
+ "a",
705
+ {
706
+ href: logoHref,
707
+ title: "Go to Dashboard",
708
+ className: "flex h-6 w-6 items-center justify-center transition hover:opacity-90",
709
+ children: /* @__PURE__ */ jsx("img", { src: logoSrc, alt: "Genfeed", className: "h-6 w-6 object-contain" })
710
+ }
711
+ ),
712
+ leftContent,
713
+ /* @__PURE__ */ jsx("div", { className: "h-8 w-px bg-border" }),
714
+ /* @__PURE__ */ jsx(ToolbarDropdown, { label: "File", items: fileMenuItems }),
715
+ additionalMenus?.map((menu) => /* @__PURE__ */ jsx(
716
+ ToolbarDropdown,
717
+ {
718
+ label: menu.label,
719
+ items: menu.items
720
+ },
721
+ menu.label
722
+ )),
723
+ /* @__PURE__ */ jsx("div", { className: "h-8 w-px bg-border" }),
724
+ debugMode && /* @__PURE__ */ jsxs(
725
+ "button",
726
+ {
727
+ onClick: () => openModal("settings"),
728
+ title: "Debug mode active - API calls are mocked",
729
+ className: "flex items-center gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1 text-sm text-amber-500 transition hover:bg-amber-500/20",
730
+ children: [
731
+ /* @__PURE__ */ jsx(Bug, { className: "h-4 w-4" }),
732
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Debug" })
733
+ ]
734
+ }
735
+ ),
736
+ onAutoLayout && /* @__PURE__ */ jsx(
737
+ "button",
738
+ {
739
+ onClick: () => onAutoLayout("LR"),
740
+ title: "Auto-layout nodes",
741
+ className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
742
+ children: /* @__PURE__ */ jsx(LayoutGrid, { className: "h-4 w-4" })
743
+ }
744
+ ),
732
745
  /* @__PURE__ */ jsx(
733
746
  "button",
734
747
  {
735
- onClick: () => setIsOpen(!isOpen),
736
- title: "More options",
748
+ onClick: () => undo(),
749
+ disabled: !canUndo,
750
+ title: "Undo (Ctrl+Z)",
751
+ className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent",
752
+ children: /* @__PURE__ */ jsx(Undo2, { className: "h-4 w-4" })
753
+ }
754
+ ),
755
+ /* @__PURE__ */ jsx(
756
+ "button",
757
+ {
758
+ onClick: () => redo(),
759
+ disabled: !canRedo,
760
+ title: "Redo (Ctrl+Shift+Z)",
761
+ className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent",
762
+ children: /* @__PURE__ */ jsx(Redo2, { className: "h-4 w-4" })
763
+ }
764
+ ),
765
+ saveIndicator ?? /* @__PURE__ */ jsx(SaveIndicator, {}),
766
+ middleContent,
767
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
768
+ rightContent,
769
+ showShortcutHelp && /* @__PURE__ */ jsx(
770
+ "button",
771
+ {
772
+ onClick: () => openModal("shortcutHelp"),
773
+ title: "Keyboard shortcuts",
737
774
  className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
738
- children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" })
775
+ children: /* @__PURE__ */ jsx(HelpCircle, { className: "h-4 w-4" })
739
776
  }
740
777
  ),
741
- isOpen && /* @__PURE__ */ jsx("div", { className: "absolute right-0 top-full z-50 mt-1 min-w-[180px] rounded-lg border border-border bg-card py-1 shadow-lg", children: items.map((item) => /* @__PURE__ */ jsxs(
778
+ showSettings && /* @__PURE__ */ jsx(
742
779
  "button",
743
780
  {
744
- onClick: () => {
745
- item.onClick?.();
746
- setIsOpen(false);
747
- },
748
- className: "flex w-full items-center gap-2.5 px-3 py-2 text-sm text-foreground transition hover:bg-secondary",
749
- children: [
750
- /* @__PURE__ */ jsx("span", { className: "h-4 w-4 shrink-0", children: item.icon }),
751
- /* @__PURE__ */ jsx("span", { children: item.label }),
752
- item.external && /* @__PURE__ */ jsx("span", { className: "ml-auto text-xs text-muted-foreground", children: "\u2197" })
753
- ]
754
- },
755
- item.id
756
- )) })
781
+ onClick: () => openModal("settings"),
782
+ title: "Settings",
783
+ className: "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground",
784
+ children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
785
+ }
786
+ ),
787
+ uniqueErrorMessages.length > 0 && /* @__PURE__ */ jsx("div", { className: "fixed right-4 top-20 z-50 max-w-sm rounded-lg border border-destructive/30 bg-destructive/10 p-4 shadow-xl", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
788
+ /* @__PURE__ */ jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 shrink-0 text-destructive" }),
789
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
790
+ /* @__PURE__ */ jsx("h4", { className: "mb-2 text-sm font-medium text-destructive", children: "Cannot run workflow" }),
791
+ /* @__PURE__ */ jsxs("ul", { className: "space-y-1", children: [
792
+ uniqueErrorMessages.slice(0, 5).map((message) => /* @__PURE__ */ jsx("li", { className: "text-xs text-destructive/80", children: message }, message)),
793
+ uniqueErrorMessages.length > 5 && /* @__PURE__ */ jsxs("li", { className: "text-xs text-destructive/60", children: [
794
+ "+",
795
+ uniqueErrorMessages.length - 5,
796
+ " more errors"
797
+ ] })
798
+ ] })
799
+ ] }),
800
+ /* @__PURE__ */ jsx(
801
+ "button",
802
+ {
803
+ onClick: clearValidationErrors,
804
+ className: "flex h-7 w-7 items-center justify-center rounded-md text-destructive transition hover:bg-destructive/20",
805
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
806
+ }
807
+ )
808
+ ] }) }),
809
+ /* @__PURE__ */ jsx(
810
+ SaveAsDialog,
811
+ {
812
+ isOpen: showSaveAsDialog,
813
+ currentName: workflowName,
814
+ onSave: handleSaveAs,
815
+ onClose: () => setShowSaveAsDialog(false)
816
+ }
817
+ )
757
818
  ] });
758
819
  }
759
820