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