@genfeedai/workflow-ui 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/canvas.js +12 -13
  2. package/dist/canvas.mjs +6 -7
  3. package/dist/{chunk-FT64PCUP.mjs → chunk-3PMLER6D.mjs} +6 -15
  4. package/dist/{chunk-CSUBLSKZ.mjs → chunk-73V6SUEO.mjs} +27 -36
  5. package/dist/{chunk-HPQT36RR.js → chunk-DICI5FSP.js} +18 -27
  6. package/dist/{chunk-EC2ZIWOK.js → chunk-DUZLPHRC.js} +36 -45
  7. package/dist/{chunk-ADWNF7V3.js → chunk-EMGXUNBL.js} +3 -3
  8. package/dist/{chunk-VOGL2WCE.mjs → chunk-EYL6J4MW.mjs} +9 -18
  9. package/dist/{chunk-H6LZKSLY.js → chunk-H3JOHAS5.js} +113 -148
  10. package/dist/{chunk-E544XUBL.js → chunk-HWY6IBWT.js} +8 -11
  11. package/dist/{chunk-22PDGHNQ.mjs → chunk-I3GNO2SR.mjs} +2 -2
  12. package/dist/{chunk-UQQUWGHW.mjs → chunk-IASLG6IA.mjs} +1 -1
  13. package/dist/chunk-IHF35QZD.js +1095 -0
  14. package/dist/{chunk-SW7QNEZU.js → chunk-OZNYKFMX.js} +30 -30
  15. package/dist/{chunk-XV5Z5XYR.mjs → chunk-PFHFGSM5.mjs} +18 -53
  16. package/dist/{chunk-CETJJ73S.js → chunk-PVWLMJGZ.js} +28 -37
  17. package/dist/chunk-RIGVIEYB.mjs +1093 -0
  18. package/dist/{chunk-LAJ34AH2.mjs → chunk-XOC5ETTX.mjs} +4 -7
  19. package/dist/hooks.js +14 -15
  20. package/dist/hooks.mjs +4 -5
  21. package/dist/index.js +41 -42
  22. package/dist/index.mjs +8 -9
  23. package/dist/lib.js +0 -1
  24. package/dist/lib.mjs +0 -1
  25. package/dist/nodes.js +37 -38
  26. package/dist/nodes.mjs +4 -5
  27. package/dist/panels.js +6 -7
  28. package/dist/panels.mjs +3 -4
  29. package/dist/provider.js +0 -1
  30. package/dist/provider.mjs +0 -1
  31. package/dist/stores.js +7 -8
  32. package/dist/stores.mjs +2 -3
  33. package/dist/toolbar.js +9 -10
  34. package/dist/toolbar.mjs +3 -4
  35. package/dist/ui.js +0 -1
  36. package/dist/ui.mjs +0 -1
  37. package/dist/workflowStore-32ZH5REM.js +11 -0
  38. package/dist/workflowStore-ZCSVIZQF.mjs +2 -0
  39. package/package.json +3 -3
  40. package/dist/chunk-AC6TWLRT.mjs +0 -27
  41. package/dist/chunk-BJ3R5R32.mjs +0 -2163
  42. package/dist/chunk-NSDLGLAQ.js +0 -2166
  43. package/dist/chunk-Z7PWFZG5.js +0 -30
  44. package/dist/workflowStore-4EGKJLYK.mjs +0 -3
  45. package/dist/workflowStore-KM32FDL7.js +0 -12
@@ -0,0 +1,1093 @@
1
+ import { temporal } from 'zundo';
2
+ import { create } from 'zustand';
3
+ import { NODE_DEFINITIONS, CONNECTION_RULES } from '@genfeedai/types';
4
+ import { addEdge, applyEdgeChanges, applyNodeChanges } from '@xyflow/react';
5
+ import { nanoid } from 'nanoid';
6
+
7
+ // src/stores/workflow/helpers/equality.ts
8
+ function temporalStateEquals(a, b) {
9
+ if (a === b) return true;
10
+ if (!arraysShallowEqual(a.nodes, b.nodes, nodeEquals)) return false;
11
+ if (!arraysShallowEqual(a.edges, b.edges, edgeEquals)) return false;
12
+ if (!arraysShallowEqual(a.groups, b.groups, groupEquals)) return false;
13
+ return true;
14
+ }
15
+ function arraysShallowEqual(a, b, itemEquals) {
16
+ if (a === b) return true;
17
+ if (a.length !== b.length) return false;
18
+ for (let i = 0; i < a.length; i++) {
19
+ if (!itemEquals(a[i], b[i])) return false;
20
+ }
21
+ return true;
22
+ }
23
+ function nodeEquals(a, b) {
24
+ if (a === b) return true;
25
+ if (a.id !== b.id) return false;
26
+ if (a.type !== b.type) return false;
27
+ if (a.position.x !== b.position.x || a.position.y !== b.position.y) return false;
28
+ if (a.width !== b.width || a.height !== b.height) return false;
29
+ const aData = a.data;
30
+ const bData = b.data;
31
+ if (aData.status !== bData.status) return false;
32
+ if (aData.outputImage !== bData.outputImage) return false;
33
+ if (aData.outputVideo !== bData.outputVideo) return false;
34
+ if (aData.outputText !== bData.outputText) return false;
35
+ if (aData.outputAudio !== bData.outputAudio) return false;
36
+ if (aData.prompt !== bData.prompt) return false;
37
+ if (aData.image !== bData.image) return false;
38
+ if (aData.video !== bData.video) return false;
39
+ if (aData.audio !== bData.audio) return false;
40
+ if (aData.inputPrompt !== bData.inputPrompt) return false;
41
+ if (aData.inputImage !== bData.inputImage) return false;
42
+ if (aData.inputVideo !== bData.inputVideo) return false;
43
+ if (aData.inputAudio !== bData.inputAudio) return false;
44
+ if (aData.inputText !== bData.inputText) return false;
45
+ if (aData.model !== bData.model) return false;
46
+ if (aData.schemaParams !== bData.schemaParams) {
47
+ if (JSON.stringify(aData.schemaParams) !== JSON.stringify(bData.schemaParams)) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+ function edgeEquals(a, b) {
54
+ if (a === b) return true;
55
+ return a.id === b.id && a.source === b.source && a.target === b.target && a.sourceHandle === b.sourceHandle && a.targetHandle === b.targetHandle;
56
+ }
57
+ function groupEquals(a, b) {
58
+ if (a === b) return true;
59
+ if (a.id !== b.id) return false;
60
+ if (a.name !== b.name) return false;
61
+ if (a.color !== b.color) return false;
62
+ if (a.nodeIds.length !== b.nodeIds.length) return false;
63
+ for (let i = 0; i < a.nodeIds.length; i++) {
64
+ if (a.nodeIds[i] !== b.nodeIds[i]) return false;
65
+ }
66
+ return true;
67
+ }
68
+
69
+ // src/stores/workflow/slices/chatSlice.ts
70
+ var createChatSlice = (set, get) => ({
71
+ chatMessages: [],
72
+ isChatOpen: false,
73
+ addChatMessage: (role, content) => {
74
+ set((state) => ({
75
+ chatMessages: [
76
+ ...state.chatMessages,
77
+ {
78
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
79
+ role,
80
+ content,
81
+ timestamp: Date.now()
82
+ }
83
+ ]
84
+ }));
85
+ },
86
+ clearChatMessages: () => {
87
+ set({ chatMessages: [] });
88
+ },
89
+ toggleChat: () => {
90
+ set((state) => ({ isChatOpen: !state.isChatOpen }));
91
+ },
92
+ setChatOpen: (open) => {
93
+ set({ isChatOpen: open });
94
+ },
95
+ applyChatEditOperations: (operations) => {
96
+ const state = get();
97
+ state.captureSnapshot();
98
+ return state.applyEditOperations(operations);
99
+ }
100
+ });
101
+ function generateId() {
102
+ return nanoid(8);
103
+ }
104
+ function getHandleType(nodeType, handleId, direction) {
105
+ const nodeDef = NODE_DEFINITIONS[nodeType];
106
+ if (!nodeDef) return null;
107
+ const handles = direction === "source" ? nodeDef.outputs : nodeDef.inputs;
108
+ const handle = handles.find((h) => h.id === handleId);
109
+ return handle?.type ?? null;
110
+ }
111
+
112
+ // src/stores/workflow/slices/edgeSlice.ts
113
+ var createEdgeSlice = (set, get) => ({
114
+ onNodesChange: (changes) => {
115
+ const hasMeaningfulChange = changes.some(
116
+ (change) => change.type === "add" || change.type === "remove" || change.type === "replace"
117
+ );
118
+ set((state) => ({
119
+ nodes: applyNodeChanges(changes, state.nodes),
120
+ ...hasMeaningfulChange && { isDirty: true }
121
+ }));
122
+ },
123
+ onEdgesChange: (changes) => {
124
+ const hasMeaningfulChange = changes.some(
125
+ (change) => change.type === "add" || change.type === "remove" || change.type === "replace"
126
+ );
127
+ set((state) => ({
128
+ edges: applyEdgeChanges(changes, state.edges),
129
+ ...hasMeaningfulChange && { isDirty: true }
130
+ }));
131
+ },
132
+ onConnect: (connection) => {
133
+ const { isValidConnection, propagateOutputsDownstream } = get();
134
+ if (!isValidConnection(connection)) return;
135
+ set((state) => ({
136
+ edges: addEdge(
137
+ {
138
+ ...connection,
139
+ id: generateId(),
140
+ type: state.edgeStyle
141
+ },
142
+ state.edges
143
+ ),
144
+ isDirty: true
145
+ }));
146
+ if (connection.source) {
147
+ propagateOutputsDownstream(connection.source);
148
+ }
149
+ },
150
+ removeEdge: (edgeId) => {
151
+ set((state) => ({
152
+ edges: state.edges.filter((edge) => edge.id !== edgeId),
153
+ isDirty: true
154
+ }));
155
+ },
156
+ setEdgeStyle: (style) => {
157
+ set((state) => ({
158
+ edgeStyle: style,
159
+ edges: state.edges.map((edge) => ({ ...edge, type: style })),
160
+ isDirty: true
161
+ }));
162
+ },
163
+ toggleEdgePause: (edgeId) => {
164
+ set((state) => ({
165
+ edges: state.edges.map(
166
+ (edge) => edge.id === edgeId ? {
167
+ ...edge,
168
+ data: {
169
+ ...edge.data,
170
+ hasPause: !edge.data?.hasPause
171
+ }
172
+ } : edge
173
+ ),
174
+ isDirty: true
175
+ }));
176
+ },
177
+ isValidConnection: (connection) => {
178
+ const { nodes } = get();
179
+ const sourceNode = nodes.find((n) => n.id === connection.source);
180
+ const targetNode = nodes.find((n) => n.id === connection.target);
181
+ if (!sourceNode || !targetNode) return false;
182
+ const sourceType = getHandleType(
183
+ sourceNode.type,
184
+ connection.sourceHandle ?? null,
185
+ "source"
186
+ );
187
+ const targetType = getHandleType(
188
+ targetNode.type,
189
+ connection.targetHandle ?? null,
190
+ "target"
191
+ );
192
+ if (!sourceType || !targetType) return false;
193
+ return CONNECTION_RULES[sourceType]?.includes(targetType) ?? false;
194
+ },
195
+ findCompatibleHandle: (sourceNodeId, sourceHandleId, targetNodeId) => {
196
+ const { nodes, edges } = get();
197
+ const sourceNode = nodes.find((n) => n.id === sourceNodeId);
198
+ const targetNode = nodes.find((n) => n.id === targetNodeId);
199
+ if (!sourceNode || !targetNode) return null;
200
+ const sourceType = getHandleType(sourceNode.type, sourceHandleId, "source");
201
+ if (!sourceType) return null;
202
+ const targetDef = NODE_DEFINITIONS[targetNode.type];
203
+ if (!targetDef) return null;
204
+ const existingTargetHandles = new Set(
205
+ edges.filter((e) => e.target === targetNodeId).map((e) => e.targetHandle)
206
+ );
207
+ for (const input of targetDef.inputs) {
208
+ const hasExistingConnection = existingTargetHandles.has(input.id);
209
+ if (hasExistingConnection && !input.multiple) continue;
210
+ if (CONNECTION_RULES[sourceType]?.includes(input.type)) {
211
+ return input.id;
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+ });
217
+
218
+ // src/stores/workflow/slices/groupSlice.ts
219
+ var DEFAULT_GROUP_COLORS = [
220
+ "purple",
221
+ "blue",
222
+ "green",
223
+ "yellow",
224
+ "orange",
225
+ "red",
226
+ "pink",
227
+ "gray"
228
+ ];
229
+ var createGroupSlice = (set, get) => ({
230
+ createGroup: (nodeIds, name) => {
231
+ if (nodeIds.length === 0) return "";
232
+ const groupId = generateId();
233
+ const { groups } = get();
234
+ const colorIndex = groups.length % DEFAULT_GROUP_COLORS.length;
235
+ const newGroup = {
236
+ id: groupId,
237
+ name: name ?? `Group ${groups.length + 1}`,
238
+ nodeIds,
239
+ isLocked: false,
240
+ color: DEFAULT_GROUP_COLORS[colorIndex]
241
+ };
242
+ set((state) => ({
243
+ groups: [...state.groups, newGroup],
244
+ isDirty: true
245
+ }));
246
+ return groupId;
247
+ },
248
+ deleteGroup: (groupId) => {
249
+ set((state) => ({
250
+ groups: state.groups.filter((g) => g.id !== groupId),
251
+ isDirty: true
252
+ }));
253
+ },
254
+ addToGroup: (groupId, nodeIds) => {
255
+ set((state) => ({
256
+ groups: state.groups.map(
257
+ (g) => g.id === groupId ? { ...g, nodeIds: [.../* @__PURE__ */ new Set([...g.nodeIds, ...nodeIds])] } : g
258
+ ),
259
+ isDirty: true
260
+ }));
261
+ },
262
+ removeFromGroup: (groupId, nodeIds) => {
263
+ set((state) => ({
264
+ groups: state.groups.map(
265
+ (g) => g.id === groupId ? { ...g, nodeIds: g.nodeIds.filter((id) => !nodeIds.includes(id)) } : g
266
+ ),
267
+ isDirty: true
268
+ }));
269
+ },
270
+ toggleGroupLock: (groupId) => {
271
+ const { groups, lockMultipleNodes, unlockMultipleNodes } = get();
272
+ const group = groups.find((g) => g.id === groupId);
273
+ if (!group) return;
274
+ set((state) => ({
275
+ groups: state.groups.map((g) => g.id === groupId ? { ...g, isLocked: !g.isLocked } : g),
276
+ isDirty: true
277
+ }));
278
+ if (!group.isLocked) {
279
+ lockMultipleNodes(group.nodeIds);
280
+ } else {
281
+ unlockMultipleNodes(group.nodeIds);
282
+ }
283
+ },
284
+ renameGroup: (groupId, name) => {
285
+ set((state) => ({
286
+ groups: state.groups.map((g) => g.id === groupId ? { ...g, name } : g),
287
+ isDirty: true
288
+ }));
289
+ },
290
+ setGroupColor: (groupId, color) => {
291
+ set((state) => ({
292
+ groups: state.groups.map((g) => g.id === groupId ? { ...g, color } : g),
293
+ isDirty: true
294
+ }));
295
+ },
296
+ getGroupByNodeId: (nodeId) => {
297
+ return get().groups.find((g) => g.nodeIds.includes(nodeId));
298
+ },
299
+ getGroupById: (groupId) => {
300
+ return get().groups.find((g) => g.id === groupId);
301
+ }
302
+ });
303
+
304
+ // src/stores/workflow/helpers/propagation.ts
305
+ function getNodeOutput(node) {
306
+ const data = node.data;
307
+ const outputImages = data.outputImages;
308
+ if (outputImages?.length) return outputImages[0];
309
+ const output = data.outputImage ?? data.outputVideo ?? data.outputText ?? data.outputAudio ?? data.prompt ?? data.extractedTweet ?? data.image ?? data.video ?? data.audio ?? null;
310
+ if (output === null) return null;
311
+ if (typeof output === "string") return output;
312
+ if (Array.isArray(output) && output.length > 0) return String(output[0]);
313
+ return null;
314
+ }
315
+ function getOutputType(sourceType) {
316
+ if (["prompt", "llm", "tweetParser", "transcribe"].includes(sourceType)) {
317
+ return "text";
318
+ }
319
+ if (["imageGen", "image", "imageInput", "upscale", "resize", "reframe", "imageGridSplit"].includes(
320
+ sourceType
321
+ )) {
322
+ return "image";
323
+ }
324
+ if ([
325
+ "videoGen",
326
+ "video",
327
+ "videoInput",
328
+ "animation",
329
+ "videoStitch",
330
+ "lipSync",
331
+ "voiceChange",
332
+ "motionControl",
333
+ "videoTrim",
334
+ "videoFrameExtract",
335
+ "subtitle"
336
+ ].includes(sourceType)) {
337
+ return "video";
338
+ }
339
+ if (["textToSpeech", "audio", "audioInput"].includes(sourceType)) {
340
+ return "audio";
341
+ }
342
+ return null;
343
+ }
344
+ function mapOutputToInput(output, sourceType, targetType) {
345
+ const outputType = getOutputType(sourceType);
346
+ if (targetType === "download") {
347
+ if (outputType === "video") {
348
+ return { inputVideo: output, inputImage: null, inputType: "video" };
349
+ }
350
+ if (outputType === "image") {
351
+ return { inputImage: output, inputVideo: null, inputType: "image" };
352
+ }
353
+ return null;
354
+ }
355
+ if (outputType === "text") {
356
+ if (["textToSpeech", "subtitle"].includes(targetType)) {
357
+ return { inputText: output };
358
+ }
359
+ if (["imageGen", "videoGen", "llm", "motionControl"].includes(targetType)) {
360
+ return { inputPrompt: output };
361
+ }
362
+ }
363
+ if (outputType === "image") {
364
+ if (["upscale", "reframe"].includes(targetType)) {
365
+ return { inputImage: output, inputVideo: null, inputType: "image" };
366
+ }
367
+ if (["videoGen", "lipSync", "voiceChange", "motionControl", "resize", "animation"].includes(
368
+ targetType
369
+ )) {
370
+ return { inputImage: output };
371
+ }
372
+ if (targetType === "imageGen") {
373
+ return { inputImages: [output] };
374
+ }
375
+ }
376
+ if (outputType === "video") {
377
+ if (["upscale", "reframe"].includes(targetType)) {
378
+ return { inputVideo: output, inputImage: null, inputType: "video" };
379
+ }
380
+ if ([
381
+ "lipSync",
382
+ "voiceChange",
383
+ "resize",
384
+ "videoStitch",
385
+ "videoTrim",
386
+ "videoFrameExtract",
387
+ "subtitle",
388
+ "transcribe"
389
+ ].includes(targetType)) {
390
+ return { inputVideo: output };
391
+ }
392
+ }
393
+ if (outputType === "audio") {
394
+ if (["lipSync", "voiceChange", "transcribe"].includes(targetType)) {
395
+ return { inputAudio: output };
396
+ }
397
+ }
398
+ return null;
399
+ }
400
+ function collectGalleryUpdate(sourceData, currentOutput, existingGalleryImages, pendingUpdateImages) {
401
+ const allImages = [];
402
+ const outputImagesArr = sourceData.outputImages;
403
+ if (outputImagesArr?.length) {
404
+ allImages.push(...outputImagesArr);
405
+ } else if (typeof currentOutput === "string") {
406
+ allImages.push(currentOutput);
407
+ }
408
+ if (allImages.length === 0) return null;
409
+ return {
410
+ images: [.../* @__PURE__ */ new Set([...existingGalleryImages, ...pendingUpdateImages, ...allImages])]
411
+ };
412
+ }
413
+ function computeDownstreamUpdates(sourceNodeId, initialOutput, nodes, edges) {
414
+ const updates = /* @__PURE__ */ new Map();
415
+ const visited = /* @__PURE__ */ new Set();
416
+ const queue = [
417
+ { nodeId: sourceNodeId, output: initialOutput }
418
+ ];
419
+ while (queue.length > 0) {
420
+ const current = queue.shift();
421
+ if (visited.has(current.nodeId)) continue;
422
+ visited.add(current.nodeId);
423
+ const currentNode = nodes.find((n) => n.id === current.nodeId);
424
+ if (!currentNode) continue;
425
+ const downstreamEdges = edges.filter((e) => e.source === current.nodeId);
426
+ for (const edge of downstreamEdges) {
427
+ const targetNode = nodes.find((n) => n.id === edge.target);
428
+ if (!targetNode) continue;
429
+ if (targetNode.type === "outputGallery") {
430
+ const sourceData = currentNode.data;
431
+ const existing = updates.get(edge.target) ?? {};
432
+ const pendingImages = existing.images ?? [];
433
+ const targetData = targetNode.data;
434
+ const galleryExisting = targetData.images ?? [];
435
+ const galleryUpdate = collectGalleryUpdate(
436
+ sourceData,
437
+ current.output,
438
+ galleryExisting,
439
+ pendingImages
440
+ );
441
+ if (galleryUpdate) {
442
+ updates.set(edge.target, { ...existing, ...galleryUpdate });
443
+ }
444
+ continue;
445
+ }
446
+ const inputUpdate = mapOutputToInput(current.output, currentNode.type, targetNode.type);
447
+ if (inputUpdate) {
448
+ const existing = updates.get(edge.target) ?? {};
449
+ updates.set(edge.target, { ...existing, ...inputUpdate });
450
+ const targetOutput = getNodeOutput(targetNode);
451
+ if (targetOutput && !visited.has(edge.target)) {
452
+ queue.push({ nodeId: edge.target, output: targetOutput });
453
+ }
454
+ }
455
+ }
456
+ }
457
+ return updates;
458
+ }
459
+ function hasStateChanged(updates, nodes) {
460
+ for (const [nodeId, update] of updates) {
461
+ const existingNode = nodes.find((n) => n.id === nodeId);
462
+ if (!existingNode) continue;
463
+ const existingData = existingNode.data;
464
+ for (const [key, value] of Object.entries(update)) {
465
+ const prev = existingData[key];
466
+ if (Array.isArray(prev) && Array.isArray(value)) {
467
+ if (prev.length !== value.length || prev.some((v, i) => v !== value[i])) {
468
+ return true;
469
+ }
470
+ } else if (prev !== value) {
471
+ return true;
472
+ }
473
+ }
474
+ }
475
+ return false;
476
+ }
477
+ function applyNodeUpdates(nodes, updates) {
478
+ return nodes.map((n) => {
479
+ const update = updates.get(n.id);
480
+ if (update) {
481
+ return { ...n, data: { ...n.data, ...update } };
482
+ }
483
+ return n;
484
+ });
485
+ }
486
+ function propagateExistingOutputs(nodes, propagateFn) {
487
+ for (const node of nodes) {
488
+ if (getNodeOutput(node) !== null) {
489
+ propagateFn(node.id);
490
+ }
491
+ }
492
+ }
493
+
494
+ // src/stores/workflow/slices/lockingSlice.ts
495
+ var createLockingSlice = (set, get) => ({
496
+ _setNodeLockState: (predicate, lock) => {
497
+ set((state) => ({
498
+ nodes: state.nodes.map(
499
+ (n) => predicate(n.id) ? {
500
+ ...n,
501
+ draggable: !lock,
502
+ data: {
503
+ ...n.data,
504
+ isLocked: lock,
505
+ lockTimestamp: lock ? Date.now() : void 0,
506
+ ...lock && { cachedOutput: getNodeOutput(n) }
507
+ }
508
+ } : n
509
+ ),
510
+ isDirty: true
511
+ }));
512
+ },
513
+ toggleNodeLock: (nodeId) => {
514
+ const node = get().getNodeById(nodeId);
515
+ if (!node) return;
516
+ const shouldLock = !(node.data.isLocked ?? false);
517
+ get()._setNodeLockState((id) => id === nodeId, shouldLock);
518
+ },
519
+ lockNode: (nodeId) => {
520
+ const node = get().getNodeById(nodeId);
521
+ if (!node || node.data.isLocked) return;
522
+ get()._setNodeLockState((id) => id === nodeId, true);
523
+ },
524
+ unlockNode: (nodeId) => {
525
+ get()._setNodeLockState((id) => id === nodeId, false);
526
+ },
527
+ lockMultipleNodes: (nodeIds) => {
528
+ get()._setNodeLockState((id) => nodeIds.includes(id), true);
529
+ },
530
+ unlockMultipleNodes: (nodeIds) => {
531
+ get()._setNodeLockState((id) => nodeIds.includes(id), false);
532
+ },
533
+ unlockAllNodes: () => {
534
+ get()._setNodeLockState(() => true, false);
535
+ },
536
+ isNodeLocked: (nodeId) => {
537
+ const { nodes, groups } = get();
538
+ const node = nodes.find((n) => n.id === nodeId);
539
+ if (!node) return false;
540
+ if (node.data.isLocked) return true;
541
+ return groups.some((group) => group.isLocked && group.nodeIds.includes(nodeId));
542
+ }
543
+ });
544
+ var createNodeSlice = (set, get) => ({
545
+ addNode: (type, position) => {
546
+ const nodeDef = NODE_DEFINITIONS[type];
547
+ if (!nodeDef) return "";
548
+ const id = generateId();
549
+ const newNode = {
550
+ id,
551
+ type,
552
+ position,
553
+ data: {
554
+ ...nodeDef.defaultData,
555
+ label: nodeDef.label,
556
+ status: "idle"
557
+ },
558
+ ...type === "download" && { width: 280, height: 320 }
559
+ };
560
+ set((state) => ({
561
+ nodes: [...state.nodes, newNode],
562
+ isDirty: true
563
+ }));
564
+ return id;
565
+ },
566
+ addNodesAndEdges: (newNodes, newEdges) => {
567
+ if (newNodes.length === 0) return;
568
+ set((state) => ({
569
+ nodes: [...state.nodes, ...newNodes],
570
+ edges: [...state.edges, ...newEdges],
571
+ isDirty: true
572
+ }));
573
+ const { propagateOutputsDownstream } = get();
574
+ const sourceNodeIds = new Set(newEdges.map((e) => e.source));
575
+ for (const sourceId of sourceNodeIds) {
576
+ propagateOutputsDownstream(sourceId);
577
+ }
578
+ },
579
+ updateNodeData: (nodeId, data) => {
580
+ const { nodes, propagateOutputsDownstream } = get();
581
+ const node = nodes.find((n) => n.id === nodeId);
582
+ const TRANSIENT_KEYS = /* @__PURE__ */ new Set(["status", "progress", "error", "jobId"]);
583
+ const dataKeys = Object.keys(data);
584
+ const hasPersistedChange = dataKeys.some((key) => !TRANSIENT_KEYS.has(key));
585
+ set((state) => ({
586
+ nodes: state.nodes.map((n) => n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n),
587
+ ...hasPersistedChange && { isDirty: true }
588
+ }));
589
+ const inputNodeTypes = [
590
+ "prompt",
591
+ "image",
592
+ "imageInput",
593
+ "video",
594
+ "videoInput",
595
+ "audio",
596
+ "audioInput",
597
+ "tweetParser"
598
+ ];
599
+ const hasOutputUpdate = "outputImage" in data || "outputImages" in data || "outputVideo" in data || "outputAudio" in data || "outputText" in data;
600
+ if (node && (inputNodeTypes.includes(node.type) || hasOutputUpdate)) {
601
+ if (hasOutputUpdate) {
602
+ const dataRecord = data;
603
+ if ("outputImages" in dataRecord) {
604
+ propagateOutputsDownstream(nodeId);
605
+ } else {
606
+ const outputValue = dataRecord.outputImage ?? dataRecord.outputVideo ?? dataRecord.outputAudio ?? dataRecord.outputText;
607
+ if (typeof outputValue === "string") {
608
+ propagateOutputsDownstream(nodeId, outputValue);
609
+ }
610
+ }
611
+ } else {
612
+ propagateOutputsDownstream(nodeId);
613
+ }
614
+ }
615
+ },
616
+ removeNode: (nodeId) => {
617
+ set((state) => ({
618
+ nodes: state.nodes.filter((node) => node.id !== nodeId),
619
+ edges: state.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId),
620
+ isDirty: true
621
+ }));
622
+ },
623
+ duplicateNode: (nodeId) => {
624
+ const { nodes, edges, edgeStyle, propagateOutputsDownstream } = get();
625
+ const node = nodes.find((n) => n.id === nodeId);
626
+ if (!node) return null;
627
+ const newId = generateId();
628
+ const newNode = {
629
+ ...node,
630
+ id: newId,
631
+ position: {
632
+ x: node.position.x + 50,
633
+ y: node.position.y + 50
634
+ },
635
+ data: {
636
+ ...node.data,
637
+ status: "idle",
638
+ jobId: null
639
+ }
640
+ };
641
+ const incomingEdges = edges.filter((e) => e.target === nodeId && e.source !== nodeId);
642
+ const clonedEdges = incomingEdges.map((edge) => ({
643
+ ...edge,
644
+ id: generateId(),
645
+ target: newId,
646
+ type: edgeStyle
647
+ }));
648
+ set((state) => ({
649
+ nodes: [...state.nodes, newNode],
650
+ edges: [...state.edges, ...clonedEdges],
651
+ isDirty: true
652
+ }));
653
+ const sourcesNotified = /* @__PURE__ */ new Set();
654
+ for (const edge of incomingEdges) {
655
+ if (!sourcesNotified.has(edge.source)) {
656
+ sourcesNotified.add(edge.source);
657
+ propagateOutputsDownstream(edge.source);
658
+ }
659
+ }
660
+ return newId;
661
+ },
662
+ propagateOutputsDownstream: (sourceNodeId, outputValue) => {
663
+ const { nodes, edges } = get();
664
+ const sourceNode = nodes.find((n) => n.id === sourceNodeId);
665
+ if (!sourceNode) return;
666
+ const output = outputValue ?? getNodeOutput(sourceNode);
667
+ if (!output) return;
668
+ const updates = computeDownstreamUpdates(sourceNodeId, output, nodes, edges);
669
+ if (updates.size === 0) return;
670
+ if (!hasStateChanged(updates, nodes)) return;
671
+ set((state) => ({
672
+ nodes: applyNodeUpdates(state.nodes, updates),
673
+ isDirty: true
674
+ }));
675
+ }
676
+ });
677
+ function normalizeEdgeTypes(edges) {
678
+ return edges.map((edge) => ({
679
+ ...edge,
680
+ type: edge.type === "bezier" ? "default" : edge.type
681
+ }));
682
+ }
683
+ function hydrateWorkflowNodes(nodes) {
684
+ return nodes.map((node) => {
685
+ const nodeDef = NODE_DEFINITIONS[node.type];
686
+ if (!nodeDef) return node;
687
+ return {
688
+ ...node,
689
+ data: {
690
+ ...nodeDef.defaultData,
691
+ ...node.data
692
+ }
693
+ };
694
+ });
695
+ }
696
+ var createPersistenceSlice = (set, get) => ({
697
+ loadWorkflow: (workflow) => {
698
+ const hydratedNodes = hydrateWorkflowNodes(workflow.nodes);
699
+ set({
700
+ nodes: hydratedNodes,
701
+ edges: normalizeEdgeTypes(workflow.edges),
702
+ edgeStyle: workflow.edgeStyle,
703
+ workflowName: workflow.name,
704
+ workflowId: null,
705
+ isDirty: true,
706
+ groups: workflow.groups ?? [],
707
+ selectedNodeIds: []
708
+ });
709
+ propagateExistingOutputs(hydratedNodes, get().propagateOutputsDownstream);
710
+ set({ isDirty: false });
711
+ },
712
+ clearWorkflow: () => {
713
+ set({
714
+ nodes: [],
715
+ edges: [],
716
+ workflowName: "Untitled Workflow",
717
+ workflowId: null,
718
+ isDirty: false,
719
+ groups: [],
720
+ selectedNodeIds: []
721
+ });
722
+ },
723
+ exportWorkflow: () => {
724
+ const { nodes, edges, edgeStyle, workflowName, groups } = get();
725
+ return {
726
+ version: 1,
727
+ name: workflowName,
728
+ description: "",
729
+ nodes,
730
+ edges,
731
+ edgeStyle,
732
+ groups,
733
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
734
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
735
+ };
736
+ },
737
+ getNodeById: (id) => {
738
+ return get().nodes.find((node) => node.id === id);
739
+ },
740
+ getConnectedInputs: (nodeId) => {
741
+ const { nodes, edges } = get();
742
+ const inputs = /* @__PURE__ */ new Map();
743
+ const incomingEdges = edges.filter((edge) => edge.target === nodeId);
744
+ for (const edge of incomingEdges) {
745
+ const sourceNode = nodes.find((n) => n.id === edge.source);
746
+ if (!sourceNode) continue;
747
+ const handleId = edge.targetHandle;
748
+ if (!handleId) continue;
749
+ const sourceData = sourceNode.data;
750
+ let value = null;
751
+ if (edge.sourceHandle === "image") {
752
+ value = sourceData.outputImage ?? sourceData.image ?? null;
753
+ } else if (edge.sourceHandle === "video") {
754
+ value = sourceData.outputVideo ?? sourceData.video ?? null;
755
+ } else if (edge.sourceHandle === "text") {
756
+ value = sourceData.outputText ?? sourceData.prompt ?? null;
757
+ } else if (edge.sourceHandle === "audio") {
758
+ value = sourceData.outputAudio ?? sourceData.audio ?? null;
759
+ }
760
+ if (value) {
761
+ const existing = inputs.get(handleId);
762
+ if (existing) {
763
+ if (Array.isArray(existing)) {
764
+ inputs.set(handleId, [...existing, value]);
765
+ } else {
766
+ inputs.set(handleId, [existing, value]);
767
+ }
768
+ } else {
769
+ inputs.set(handleId, value);
770
+ }
771
+ }
772
+ }
773
+ return inputs;
774
+ },
775
+ getConnectedNodeIds: (nodeIds) => {
776
+ const { edges } = get();
777
+ const connected = new Set(nodeIds);
778
+ const visited = /* @__PURE__ */ new Set();
779
+ const queue = [...nodeIds];
780
+ while (queue.length > 0) {
781
+ const currentId = queue.shift();
782
+ if (visited.has(currentId)) continue;
783
+ visited.add(currentId);
784
+ const upstreamEdges = edges.filter((e) => e.target === currentId);
785
+ for (const edge of upstreamEdges) {
786
+ if (!connected.has(edge.source)) {
787
+ connected.add(edge.source);
788
+ queue.push(edge.source);
789
+ }
790
+ }
791
+ }
792
+ return Array.from(connected);
793
+ },
794
+ validateWorkflow: () => {
795
+ const { nodes, edges } = get();
796
+ const errors = [];
797
+ const warnings = [];
798
+ if (nodes.length === 0) {
799
+ errors.push({
800
+ nodeId: "",
801
+ message: "Workflow is empty - add some nodes first",
802
+ severity: "error"
803
+ });
804
+ return { isValid: false, errors, warnings };
805
+ }
806
+ if (edges.length === 0 && nodes.length > 1) {
807
+ errors.push({
808
+ nodeId: "",
809
+ message: "No connections - connect your nodes together",
810
+ severity: "error"
811
+ });
812
+ return { isValid: false, errors, warnings };
813
+ }
814
+ const hasNodeOutput = (node) => {
815
+ const data = node.data;
816
+ switch (node.type) {
817
+ case "prompt":
818
+ return Boolean(data.prompt?.trim());
819
+ case "imageInput":
820
+ return Boolean(data.image);
821
+ case "videoInput":
822
+ return Boolean(data.video);
823
+ case "audioInput":
824
+ return Boolean(data.audio);
825
+ default:
826
+ return true;
827
+ }
828
+ };
829
+ for (const node of nodes) {
830
+ const nodeDef = NODE_DEFINITIONS[node.type];
831
+ if (!nodeDef) continue;
832
+ const incomingEdges = edges.filter((e) => e.target === node.id);
833
+ for (const input of nodeDef.inputs) {
834
+ if (input.required) {
835
+ const connectionEdge = incomingEdges.find((e) => e.targetHandle === input.id);
836
+ if (!connectionEdge) {
837
+ errors.push({
838
+ nodeId: node.id,
839
+ message: `Missing required input: ${input.label}`,
840
+ severity: "error"
841
+ });
842
+ } else {
843
+ const sourceNode = nodes.find((n) => n.id === connectionEdge.source);
844
+ if (sourceNode && !hasNodeOutput(sourceNode)) {
845
+ errors.push({
846
+ nodeId: sourceNode.id,
847
+ message: `${sourceNode.data.label} is empty`,
848
+ severity: "error"
849
+ });
850
+ }
851
+ }
852
+ }
853
+ }
854
+ }
855
+ const visited = /* @__PURE__ */ new Set();
856
+ const recStack = /* @__PURE__ */ new Set();
857
+ function hasCycle(nodeId) {
858
+ if (recStack.has(nodeId)) return true;
859
+ if (visited.has(nodeId)) return false;
860
+ visited.add(nodeId);
861
+ recStack.add(nodeId);
862
+ const outgoing = edges.filter((e) => e.source === nodeId);
863
+ for (const edge of outgoing) {
864
+ if (hasCycle(edge.target)) return true;
865
+ }
866
+ recStack.delete(nodeId);
867
+ return false;
868
+ }
869
+ for (const node of nodes) {
870
+ if (hasCycle(node.id)) {
871
+ errors.push({
872
+ nodeId: node.id,
873
+ message: "Workflow contains a cycle",
874
+ severity: "error"
875
+ });
876
+ break;
877
+ }
878
+ }
879
+ for (const node of nodes) {
880
+ if (node.type === "workflowRef") {
881
+ const refData = node.data;
882
+ if (!refData.referencedWorkflowId) {
883
+ errors.push({
884
+ nodeId: node.id,
885
+ message: "Subworkflow node must reference a workflow",
886
+ severity: "error"
887
+ });
888
+ } else if (!refData.cachedInterface) {
889
+ warnings.push({
890
+ nodeId: node.id,
891
+ message: "Subworkflow interface not loaded - refresh to update handles",
892
+ severity: "warning"
893
+ });
894
+ }
895
+ }
896
+ }
897
+ return {
898
+ isValid: errors.length === 0,
899
+ errors,
900
+ warnings
901
+ };
902
+ },
903
+ setDirty: (dirty) => {
904
+ set({ isDirty: dirty });
905
+ },
906
+ setWorkflowName: (name) => {
907
+ set({ workflowName: name, isDirty: true });
908
+ },
909
+ // API operations - stubs that throw by default.
910
+ // Consuming apps override these via the store creator or by extending the slice.
911
+ saveWorkflow: async () => {
912
+ throw new Error("saveWorkflow not implemented - consuming app must provide API integration");
913
+ },
914
+ loadWorkflowById: async () => {
915
+ throw new Error(
916
+ "loadWorkflowById not implemented - consuming app must provide API integration"
917
+ );
918
+ },
919
+ listWorkflows: async () => {
920
+ throw new Error("listWorkflows not implemented - consuming app must provide API integration");
921
+ },
922
+ deleteWorkflow: async () => {
923
+ throw new Error("deleteWorkflow not implemented - consuming app must provide API integration");
924
+ },
925
+ duplicateWorkflowApi: async () => {
926
+ throw new Error(
927
+ "duplicateWorkflowApi not implemented - consuming app must provide API integration"
928
+ );
929
+ },
930
+ createNewWorkflow: async () => {
931
+ throw new Error(
932
+ "createNewWorkflow not implemented - consuming app must provide API integration"
933
+ );
934
+ },
935
+ getNodesWithComments: () => {
936
+ const { nodes } = get();
937
+ return nodes.filter((node) => {
938
+ const data = node.data;
939
+ return data.comment?.trim();
940
+ }).sort((a, b) => {
941
+ if (Math.abs(a.position.y - b.position.y) < 50) {
942
+ return a.position.x - b.position.x;
943
+ }
944
+ return a.position.y - b.position.y;
945
+ });
946
+ },
947
+ markCommentViewed: (nodeId) => {
948
+ set((state) => {
949
+ const newSet = new Set(state.viewedCommentIds);
950
+ newSet.add(nodeId);
951
+ return { viewedCommentIds: newSet };
952
+ });
953
+ },
954
+ setNavigationTarget: (nodeId) => {
955
+ set({ navigationTargetId: nodeId });
956
+ },
957
+ getUnviewedCommentCount: () => {
958
+ const { nodes, viewedCommentIds } = get();
959
+ return nodes.filter((node) => {
960
+ const data = node.data;
961
+ return data.comment?.trim() && !viewedCommentIds.has(node.id);
962
+ }).length;
963
+ }
964
+ });
965
+
966
+ // src/stores/workflow/slices/selectionSlice.ts
967
+ var createSelectionSlice = (set) => ({
968
+ setSelectedNodeIds: (nodeIds) => {
969
+ set({ selectedNodeIds: nodeIds });
970
+ },
971
+ addToSelection: (nodeId) => {
972
+ set((state) => ({
973
+ selectedNodeIds: state.selectedNodeIds.includes(nodeId) ? state.selectedNodeIds : [...state.selectedNodeIds, nodeId]
974
+ }));
975
+ },
976
+ removeFromSelection: (nodeId) => {
977
+ set((state) => ({
978
+ selectedNodeIds: state.selectedNodeIds.filter((id) => id !== nodeId)
979
+ }));
980
+ },
981
+ clearSelection: () => {
982
+ set({ selectedNodeIds: [] });
983
+ }
984
+ });
985
+
986
+ // src/stores/workflow/slices/snapshotSlice.ts
987
+ function defaultApplyEditOperations(_operations, state) {
988
+ return { nodes: state.nodes, edges: state.edges, applied: 0, skipped: [] };
989
+ }
990
+ var createSnapshotSlice = (set, get) => ({
991
+ previousWorkflowSnapshot: null,
992
+ manualChangeCount: 0,
993
+ captureSnapshot: () => {
994
+ const state = get();
995
+ const snapshot = {
996
+ nodes: JSON.parse(JSON.stringify(state.nodes)),
997
+ edges: JSON.parse(JSON.stringify(state.edges)),
998
+ groups: JSON.parse(JSON.stringify(state.groups)),
999
+ edgeStyle: state.edgeStyle
1000
+ };
1001
+ set({
1002
+ previousWorkflowSnapshot: snapshot,
1003
+ manualChangeCount: 0
1004
+ });
1005
+ },
1006
+ revertToSnapshot: () => {
1007
+ const state = get();
1008
+ if (state.previousWorkflowSnapshot) {
1009
+ set({
1010
+ nodes: state.previousWorkflowSnapshot.nodes,
1011
+ edges: state.previousWorkflowSnapshot.edges,
1012
+ groups: state.previousWorkflowSnapshot.groups,
1013
+ edgeStyle: state.previousWorkflowSnapshot.edgeStyle,
1014
+ previousWorkflowSnapshot: null,
1015
+ manualChangeCount: 0,
1016
+ isDirty: true
1017
+ });
1018
+ }
1019
+ },
1020
+ clearSnapshot: () => {
1021
+ set({
1022
+ previousWorkflowSnapshot: null,
1023
+ manualChangeCount: 0
1024
+ });
1025
+ },
1026
+ incrementManualChangeCount: () => {
1027
+ const state = get();
1028
+ const newCount = state.manualChangeCount + 1;
1029
+ if (newCount >= 3) {
1030
+ set({
1031
+ previousWorkflowSnapshot: null,
1032
+ manualChangeCount: 0
1033
+ });
1034
+ } else {
1035
+ set({ manualChangeCount: newCount });
1036
+ }
1037
+ },
1038
+ applyEditOperations: (operations) => {
1039
+ const state = get();
1040
+ const result = defaultApplyEditOperations(operations, {
1041
+ nodes: state.nodes,
1042
+ edges: state.edges
1043
+ });
1044
+ set({
1045
+ nodes: result.nodes,
1046
+ edges: result.edges,
1047
+ isDirty: true
1048
+ });
1049
+ return { applied: result.applied, skipped: result.skipped };
1050
+ }
1051
+ });
1052
+
1053
+ // src/stores/workflow/workflowStore.ts
1054
+ var storeCreator = ((...args) => ({
1055
+ // Initial state
1056
+ nodes: [],
1057
+ edges: [],
1058
+ edgeStyle: "default",
1059
+ workflowName: "Untitled Workflow",
1060
+ workflowId: null,
1061
+ isDirty: false,
1062
+ isSaving: false,
1063
+ isLoading: false,
1064
+ groups: [],
1065
+ selectedNodeIds: [],
1066
+ viewedCommentIds: /* @__PURE__ */ new Set(),
1067
+ navigationTargetId: null,
1068
+ // Compose slices
1069
+ ...createNodeSlice(...args),
1070
+ ...createEdgeSlice(...args),
1071
+ ...createLockingSlice(...args),
1072
+ ...createGroupSlice(...args),
1073
+ ...createSelectionSlice(...args),
1074
+ ...createPersistenceSlice(...args),
1075
+ ...createSnapshotSlice(...args),
1076
+ ...createChatSlice(...args)
1077
+ }));
1078
+ var useWorkflowStore = create()(
1079
+ temporal(storeCreator, {
1080
+ // Only track meaningful state (not UI flags like isDirty, isSaving, etc.)
1081
+ partialize: (state) => ({
1082
+ nodes: state.nodes,
1083
+ edges: state.edges,
1084
+ groups: state.groups
1085
+ }),
1086
+ // Limit history to prevent memory issues
1087
+ limit: 50,
1088
+ // Optimized equality check using shallow comparison instead of JSON.stringify
1089
+ equality: temporalStateEquals
1090
+ })
1091
+ );
1092
+
1093
+ export { useWorkflowStore };