@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.
- package/dist/canvas.d.ts +22 -22
- package/dist/canvas.mjs +16 -16
- package/dist/{chunk-XPZAHIWY.mjs → chunk-2FUPL67V.mjs} +1592 -1044
- package/dist/{chunk-HWVTD2LC.mjs → chunk-53XDE62A.mjs} +818 -623
- package/dist/{chunk-PCIWWD37.mjs → chunk-7LV4UAUS.mjs} +19 -19
- package/dist/{chunk-7SKSRSS7.mjs → chunk-B4EAAKYF.mjs} +16 -16
- package/dist/{chunk-ZJD5WMR3.mjs → chunk-C6MQBJFC.mjs} +45 -13
- package/dist/{chunk-7H3WJJYS.mjs → chunk-ESVULCFY.mjs} +12 -6
- package/dist/{chunk-GWBGK3KL.mjs → chunk-FWJIAW2E.mjs} +82 -47
- package/dist/{chunk-R727OFBR.mjs → chunk-GPYIIWD5.mjs} +404 -350
- package/dist/{chunk-OQREHJXK.mjs → chunk-IYFWAJBB.mjs} +208 -203
- package/dist/{chunk-N5NJZTK4.mjs → chunk-MGLAKMDP.mjs} +23 -21
- package/dist/{chunk-LT3ZJJL6.mjs → chunk-OJWVEEMM.mjs} +497 -399
- package/dist/{chunk-ZD2BADZO.mjs → chunk-ORVDYXDP.mjs} +221 -175
- package/dist/{chunk-CV4M7CNU.mjs → chunk-QQVHGJ2G.mjs} +149 -142
- package/dist/{chunk-6PSJTBNV.mjs → chunk-U4QPE4CY.mjs} +387 -347
- package/dist/{chunk-EFXQT23N.mjs → chunk-VVQ4CH77.mjs} +5 -5
- package/dist/{chunk-VRN3UWE5.mjs → chunk-XRC3O5GK.mjs} +73 -73
- package/dist/{chunk-FT33LFII.mjs → chunk-YUIK4AHM.mjs} +1 -1
- package/dist/{chunk-FMJPFB6W.mjs → chunk-ZSITTZ4S.mjs} +630 -569
- package/dist/hooks.d.ts +37 -37
- package/dist/hooks.mjs +10 -10
- package/dist/index.d.ts +26 -11
- package/dist/index.mjs +99 -19
- package/dist/lib.d.ts +203 -203
- package/dist/lib.mjs +228 -198
- package/dist/nodes.d.ts +2 -2
- package/dist/nodes.mjs +12 -12
- package/dist/panels.d.ts +2 -3
- package/dist/panels.mjs +3 -3
- package/dist/provider.d.ts +2 -2
- package/dist/provider.mjs +2 -2
- package/dist/stores.d.ts +5 -5
- package/dist/stores.mjs +5 -5
- package/dist/toolbar.d.ts +42 -24
- package/dist/toolbar.mjs +4 -4
- package/dist/ui.d.ts +2 -2
- package/dist/ui.mjs +2 -2
- package/dist/{useCommentNavigation-BakbiiIc.d.ts → useRequiredInputs-ByoIS-fT.d.ts} +160 -160
- package/dist/{promptLibraryStore-Dl3Q3cP6.d.ts → workflowStore-Bsz0nd5c.d.ts} +368 -368
- package/dist/workflowStore-N2F7WIG3.mjs +2 -0
- package/package.json +77 -75
- package/src/styles/workflow-ui.css +56 -19
- package/dist/workflowStore-UAAKOOIK.mjs +0 -2
- package/dist/{types-IEKYuYhu.d.ts → types-CRXJnajq.d.ts} +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { temporal } from 'zundo';
|
|
2
2
|
import { create } from 'zustand';
|
|
3
3
|
import { NODE_DEFINITIONS, CONNECTION_RULES } from '@genfeedai/types';
|
|
4
|
-
import {
|
|
4
|
+
import { applyNodeChanges, applyEdgeChanges, addEdge } from '@xyflow/react';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
6
|
|
|
7
7
|
// src/stores/workflow/helpers/equality.ts
|
|
@@ -24,7 +24,8 @@ function nodeEquals(a, b) {
|
|
|
24
24
|
if (a === b) return true;
|
|
25
25
|
if (a.id !== b.id) return false;
|
|
26
26
|
if (a.type !== b.type) return false;
|
|
27
|
-
if (a.position.x !== b.position.x || a.position.y !== b.position.y)
|
|
27
|
+
if (a.position.x !== b.position.x || a.position.y !== b.position.y)
|
|
28
|
+
return false;
|
|
28
29
|
if (a.width !== b.width || a.height !== b.height) return false;
|
|
29
30
|
const aData = a.data;
|
|
30
31
|
const bData = b.data;
|
|
@@ -68,34 +69,34 @@ function groupEquals(a, b) {
|
|
|
68
69
|
|
|
69
70
|
// src/stores/workflow/slices/chatSlice.ts
|
|
70
71
|
var createChatSlice = (set, get) => ({
|
|
71
|
-
chatMessages: [],
|
|
72
|
-
isChatOpen: false,
|
|
73
72
|
addChatMessage: (role, content) => {
|
|
74
73
|
set((state) => ({
|
|
75
74
|
chatMessages: [
|
|
76
75
|
...state.chatMessages,
|
|
77
76
|
{
|
|
77
|
+
content,
|
|
78
78
|
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
79
79
|
role,
|
|
80
|
-
content,
|
|
81
80
|
timestamp: Date.now()
|
|
82
81
|
}
|
|
83
82
|
]
|
|
84
83
|
}));
|
|
85
84
|
},
|
|
85
|
+
applyChatEditOperations: (operations) => {
|
|
86
|
+
const state = get();
|
|
87
|
+
state.captureSnapshot();
|
|
88
|
+
return state.applyEditOperations(operations);
|
|
89
|
+
},
|
|
90
|
+
chatMessages: [],
|
|
86
91
|
clearChatMessages: () => {
|
|
87
92
|
set({ chatMessages: [] });
|
|
88
93
|
},
|
|
89
|
-
|
|
90
|
-
set((state) => ({ isChatOpen: !state.isChatOpen }));
|
|
91
|
-
},
|
|
94
|
+
isChatOpen: false,
|
|
92
95
|
setChatOpen: (open) => {
|
|
93
96
|
set({ isChatOpen: open });
|
|
94
97
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
state.captureSnapshot();
|
|
98
|
-
return state.applyEditOperations(operations);
|
|
98
|
+
toggleChat: () => {
|
|
99
|
+
set((state) => ({ isChatOpen: !state.isChatOpen }));
|
|
99
100
|
}
|
|
100
101
|
});
|
|
101
102
|
function generateId() {
|
|
@@ -111,23 +112,48 @@ function getHandleType(nodeType, handleId, direction) {
|
|
|
111
112
|
|
|
112
113
|
// src/stores/workflow/slices/edgeSlice.ts
|
|
113
114
|
var createEdgeSlice = (set, get) => ({
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
115
|
+
findCompatibleHandle: (sourceNodeId, sourceHandleId, targetNodeId) => {
|
|
116
|
+
const { nodes, edges } = get();
|
|
117
|
+
const sourceNode = nodes.find((n) => n.id === sourceNodeId);
|
|
118
|
+
const targetNode = nodes.find((n) => n.id === targetNodeId);
|
|
119
|
+
if (!sourceNode || !targetNode) return null;
|
|
120
|
+
const sourceType = getHandleType(
|
|
121
|
+
sourceNode.type,
|
|
122
|
+
sourceHandleId,
|
|
123
|
+
"source"
|
|
117
124
|
);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
if (!sourceType) return null;
|
|
126
|
+
const targetDef = NODE_DEFINITIONS[targetNode.type];
|
|
127
|
+
if (!targetDef) return null;
|
|
128
|
+
const existingTargetHandles = new Set(
|
|
129
|
+
edges.filter((e) => e.target === targetNodeId).map((e) => e.targetHandle)
|
|
130
|
+
);
|
|
131
|
+
for (const input of targetDef.inputs) {
|
|
132
|
+
const hasExistingConnection = existingTargetHandles.has(input.id);
|
|
133
|
+
if (hasExistingConnection && !input.multiple) continue;
|
|
134
|
+
if (CONNECTION_RULES[sourceType]?.includes(input.type)) {
|
|
135
|
+
return input.id;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
122
139
|
},
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
140
|
+
isValidConnection: (connection) => {
|
|
141
|
+
const { nodes } = get();
|
|
142
|
+
const sourceNode = nodes.find((n) => n.id === connection.source);
|
|
143
|
+
const targetNode = nodes.find((n) => n.id === connection.target);
|
|
144
|
+
if (!sourceNode || !targetNode) return false;
|
|
145
|
+
const sourceType = getHandleType(
|
|
146
|
+
sourceNode.type,
|
|
147
|
+
connection.sourceHandle ?? null,
|
|
148
|
+
"source"
|
|
126
149
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
const targetType = getHandleType(
|
|
151
|
+
targetNode.type,
|
|
152
|
+
connection.targetHandle ?? null,
|
|
153
|
+
"target"
|
|
154
|
+
);
|
|
155
|
+
if (!sourceType || !targetType) return false;
|
|
156
|
+
return CONNECTION_RULES[sourceType]?.includes(targetType) ?? false;
|
|
131
157
|
},
|
|
132
158
|
onConnect: (connection) => {
|
|
133
159
|
const { isValidConnection, propagateOutputsDownstream } = get();
|
|
@@ -147,6 +173,24 @@ var createEdgeSlice = (set, get) => ({
|
|
|
147
173
|
propagateOutputsDownstream(connection.source);
|
|
148
174
|
}
|
|
149
175
|
},
|
|
176
|
+
onEdgesChange: (changes) => {
|
|
177
|
+
const hasMeaningfulChange = changes.some(
|
|
178
|
+
(change) => change.type === "add" || change.type === "remove" || change.type === "replace"
|
|
179
|
+
);
|
|
180
|
+
set((state) => ({
|
|
181
|
+
edges: applyEdgeChanges(changes, state.edges),
|
|
182
|
+
...hasMeaningfulChange && { isDirty: true }
|
|
183
|
+
}));
|
|
184
|
+
},
|
|
185
|
+
onNodesChange: (changes) => {
|
|
186
|
+
const hasMeaningfulChange = changes.some(
|
|
187
|
+
(change) => change.type === "add" || change.type === "remove" || change.type === "replace"
|
|
188
|
+
);
|
|
189
|
+
set((state) => ({
|
|
190
|
+
nodes: applyNodeChanges(changes, state.nodes),
|
|
191
|
+
...hasMeaningfulChange && { isDirty: true }
|
|
192
|
+
}));
|
|
193
|
+
},
|
|
150
194
|
removeEdge: (edgeId) => {
|
|
151
195
|
set((state) => ({
|
|
152
196
|
edges: state.edges.filter((edge) => edge.id !== edgeId),
|
|
@@ -173,45 +217,6 @@ var createEdgeSlice = (set, get) => ({
|
|
|
173
217
|
),
|
|
174
218
|
isDirty: true
|
|
175
219
|
}));
|
|
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
220
|
}
|
|
216
221
|
});
|
|
217
222
|
|
|
@@ -227,17 +232,25 @@ var DEFAULT_GROUP_COLORS = [
|
|
|
227
232
|
"gray"
|
|
228
233
|
];
|
|
229
234
|
var createGroupSlice = (set, get) => ({
|
|
235
|
+
addToGroup: (groupId, nodeIds) => {
|
|
236
|
+
set((state) => ({
|
|
237
|
+
groups: state.groups.map(
|
|
238
|
+
(g) => g.id === groupId ? { ...g, nodeIds: [.../* @__PURE__ */ new Set([...g.nodeIds, ...nodeIds])] } : g
|
|
239
|
+
),
|
|
240
|
+
isDirty: true
|
|
241
|
+
}));
|
|
242
|
+
},
|
|
230
243
|
createGroup: (nodeIds, name) => {
|
|
231
244
|
if (nodeIds.length === 0) return "";
|
|
232
245
|
const groupId = generateId();
|
|
233
246
|
const { groups } = get();
|
|
234
247
|
const colorIndex = groups.length % DEFAULT_GROUP_COLORS.length;
|
|
235
248
|
const newGroup = {
|
|
249
|
+
color: DEFAULT_GROUP_COLORS[colorIndex],
|
|
236
250
|
id: groupId,
|
|
237
|
-
name: name ?? `Group ${groups.length + 1}`,
|
|
238
|
-
nodeIds,
|
|
239
251
|
isLocked: false,
|
|
240
|
-
|
|
252
|
+
name: name ?? `Group ${groups.length + 1}`,
|
|
253
|
+
nodeIds
|
|
241
254
|
};
|
|
242
255
|
set((state) => ({
|
|
243
256
|
groups: [...state.groups, newGroup],
|
|
@@ -251,19 +264,29 @@ var createGroupSlice = (set, get) => ({
|
|
|
251
264
|
isDirty: true
|
|
252
265
|
}));
|
|
253
266
|
},
|
|
254
|
-
|
|
267
|
+
getGroupById: (groupId) => {
|
|
268
|
+
return get().groups.find((g) => g.id === groupId);
|
|
269
|
+
},
|
|
270
|
+
getGroupByNodeId: (nodeId) => {
|
|
271
|
+
return get().groups.find((g) => g.nodeIds.includes(nodeId));
|
|
272
|
+
},
|
|
273
|
+
removeFromGroup: (groupId, nodeIds) => {
|
|
255
274
|
set((state) => ({
|
|
256
275
|
groups: state.groups.map(
|
|
257
|
-
(g) => g.id === groupId ? { ...g, nodeIds:
|
|
276
|
+
(g) => g.id === groupId ? { ...g, nodeIds: g.nodeIds.filter((id) => !nodeIds.includes(id)) } : g
|
|
258
277
|
),
|
|
259
278
|
isDirty: true
|
|
260
279
|
}));
|
|
261
280
|
},
|
|
262
|
-
|
|
281
|
+
renameGroup: (groupId, name) => {
|
|
263
282
|
set((state) => ({
|
|
264
|
-
groups: state.groups.map(
|
|
265
|
-
|
|
266
|
-
|
|
283
|
+
groups: state.groups.map((g) => g.id === groupId ? { ...g, name } : g),
|
|
284
|
+
isDirty: true
|
|
285
|
+
}));
|
|
286
|
+
},
|
|
287
|
+
setGroupColor: (groupId, color) => {
|
|
288
|
+
set((state) => ({
|
|
289
|
+
groups: state.groups.map((g) => g.id === groupId ? { ...g, color } : g),
|
|
267
290
|
isDirty: true
|
|
268
291
|
}));
|
|
269
292
|
},
|
|
@@ -272,7 +295,9 @@ var createGroupSlice = (set, get) => ({
|
|
|
272
295
|
const group = groups.find((g) => g.id === groupId);
|
|
273
296
|
if (!group) return;
|
|
274
297
|
set((state) => ({
|
|
275
|
-
groups: state.groups.map(
|
|
298
|
+
groups: state.groups.map(
|
|
299
|
+
(g) => g.id === groupId ? { ...g, isLocked: !g.isLocked } : g
|
|
300
|
+
),
|
|
276
301
|
isDirty: true
|
|
277
302
|
}));
|
|
278
303
|
if (!group.isLocked) {
|
|
@@ -280,24 +305,6 @@ var createGroupSlice = (set, get) => ({
|
|
|
280
305
|
} else {
|
|
281
306
|
unlockMultipleNodes(group.nodeIds);
|
|
282
307
|
}
|
|
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
308
|
}
|
|
302
309
|
});
|
|
303
310
|
|
|
@@ -316,9 +323,15 @@ function getOutputType(sourceType) {
|
|
|
316
323
|
if (["prompt", "llm", "tweetParser", "transcribe"].includes(sourceType)) {
|
|
317
324
|
return "text";
|
|
318
325
|
}
|
|
319
|
-
if ([
|
|
320
|
-
|
|
321
|
-
|
|
326
|
+
if ([
|
|
327
|
+
"imageGen",
|
|
328
|
+
"image",
|
|
329
|
+
"imageInput",
|
|
330
|
+
"upscale",
|
|
331
|
+
"resize",
|
|
332
|
+
"reframe",
|
|
333
|
+
"imageGridSplit"
|
|
334
|
+
].includes(sourceType)) {
|
|
322
335
|
return "image";
|
|
323
336
|
}
|
|
324
337
|
if ([
|
|
@@ -345,10 +358,10 @@ function mapOutputToInput(output, sourceType, targetType) {
|
|
|
345
358
|
const outputType = getOutputType(sourceType);
|
|
346
359
|
if (targetType === "download") {
|
|
347
360
|
if (outputType === "video") {
|
|
348
|
-
return {
|
|
361
|
+
return { inputImage: null, inputType: "video", inputVideo: output };
|
|
349
362
|
}
|
|
350
363
|
if (outputType === "image") {
|
|
351
|
-
return { inputImage: output,
|
|
364
|
+
return { inputImage: output, inputType: "image", inputVideo: null };
|
|
352
365
|
}
|
|
353
366
|
return null;
|
|
354
367
|
}
|
|
@@ -362,11 +375,16 @@ function mapOutputToInput(output, sourceType, targetType) {
|
|
|
362
375
|
}
|
|
363
376
|
if (outputType === "image") {
|
|
364
377
|
if (["upscale", "reframe"].includes(targetType)) {
|
|
365
|
-
return { inputImage: output,
|
|
378
|
+
return { inputImage: output, inputType: "image", inputVideo: null };
|
|
366
379
|
}
|
|
367
|
-
if ([
|
|
368
|
-
|
|
369
|
-
|
|
380
|
+
if ([
|
|
381
|
+
"videoGen",
|
|
382
|
+
"lipSync",
|
|
383
|
+
"voiceChange",
|
|
384
|
+
"motionControl",
|
|
385
|
+
"resize",
|
|
386
|
+
"animation"
|
|
387
|
+
].includes(targetType)) {
|
|
370
388
|
return { inputImage: output };
|
|
371
389
|
}
|
|
372
390
|
if (targetType === "imageGen") {
|
|
@@ -375,7 +393,7 @@ function mapOutputToInput(output, sourceType, targetType) {
|
|
|
375
393
|
}
|
|
376
394
|
if (outputType === "video") {
|
|
377
395
|
if (["upscale", "reframe"].includes(targetType)) {
|
|
378
|
-
return {
|
|
396
|
+
return { inputImage: null, inputType: "video", inputVideo: output };
|
|
379
397
|
}
|
|
380
398
|
if ([
|
|
381
399
|
"lipSync",
|
|
@@ -407,7 +425,13 @@ function collectGalleryUpdate(sourceData, currentOutput, existingGalleryImages,
|
|
|
407
425
|
}
|
|
408
426
|
if (allImages.length === 0) return null;
|
|
409
427
|
return {
|
|
410
|
-
images: [
|
|
428
|
+
images: [
|
|
429
|
+
.../* @__PURE__ */ new Set([
|
|
430
|
+
...existingGalleryImages,
|
|
431
|
+
...pendingUpdateImages,
|
|
432
|
+
...allImages
|
|
433
|
+
])
|
|
434
|
+
]
|
|
411
435
|
};
|
|
412
436
|
}
|
|
413
437
|
function computeDownstreamUpdates(sourceNodeId, initialOutput, nodes, edges) {
|
|
@@ -443,7 +467,11 @@ function computeDownstreamUpdates(sourceNodeId, initialOutput, nodes, edges) {
|
|
|
443
467
|
}
|
|
444
468
|
continue;
|
|
445
469
|
}
|
|
446
|
-
const inputUpdate = mapOutputToInput(
|
|
470
|
+
const inputUpdate = mapOutputToInput(
|
|
471
|
+
current.output,
|
|
472
|
+
currentNode.type,
|
|
473
|
+
targetNode.type
|
|
474
|
+
);
|
|
447
475
|
if (inputUpdate) {
|
|
448
476
|
const existing = updates.get(edge.target) ?? {};
|
|
449
477
|
updates.set(edge.target, { ...existing, ...inputUpdate });
|
|
@@ -495,50 +523,52 @@ function propagateExistingOutputs(nodes, propagateFn) {
|
|
|
495
523
|
var createLockingSlice = (set, get) => ({
|
|
496
524
|
_setNodeLockState: (predicate, lock) => {
|
|
497
525
|
set((state) => ({
|
|
526
|
+
isDirty: true,
|
|
498
527
|
nodes: state.nodes.map(
|
|
499
528
|
(n) => predicate(n.id) ? {
|
|
500
529
|
...n,
|
|
501
|
-
draggable: !lock,
|
|
502
530
|
data: {
|
|
503
531
|
...n.data,
|
|
504
532
|
isLocked: lock,
|
|
505
533
|
lockTimestamp: lock ? Date.now() : void 0,
|
|
506
534
|
...lock && { cachedOutput: getNodeOutput(n) }
|
|
507
|
-
}
|
|
535
|
+
},
|
|
536
|
+
draggable: !lock
|
|
508
537
|
} : n
|
|
509
|
-
)
|
|
510
|
-
isDirty: true
|
|
538
|
+
)
|
|
511
539
|
}));
|
|
512
540
|
},
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
541
|
+
isNodeLocked: (nodeId) => {
|
|
542
|
+
const { nodes, groups } = get();
|
|
543
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
544
|
+
if (!node) return false;
|
|
545
|
+
if (node.data.isLocked) return true;
|
|
546
|
+
return groups.some(
|
|
547
|
+
(group) => group.isLocked && group.nodeIds.includes(nodeId)
|
|
548
|
+
);
|
|
549
|
+
},
|
|
550
|
+
lockMultipleNodes: (nodeIds) => {
|
|
551
|
+
get()._setNodeLockState((id) => nodeIds.includes(id), true);
|
|
518
552
|
},
|
|
519
553
|
lockNode: (nodeId) => {
|
|
520
554
|
const node = get().getNodeById(nodeId);
|
|
521
555
|
if (!node || node.data.isLocked) return;
|
|
522
556
|
get()._setNodeLockState((id) => id === nodeId, true);
|
|
523
557
|
},
|
|
524
|
-
|
|
525
|
-
get().
|
|
558
|
+
toggleNodeLock: (nodeId) => {
|
|
559
|
+
const node = get().getNodeById(nodeId);
|
|
560
|
+
if (!node) return;
|
|
561
|
+
const shouldLock = !(node.data.isLocked ?? false);
|
|
562
|
+
get()._setNodeLockState((id) => id === nodeId, shouldLock);
|
|
526
563
|
},
|
|
527
|
-
|
|
528
|
-
get()._setNodeLockState((
|
|
564
|
+
unlockAllNodes: () => {
|
|
565
|
+
get()._setNodeLockState(() => true, false);
|
|
529
566
|
},
|
|
530
567
|
unlockMultipleNodes: (nodeIds) => {
|
|
531
568
|
get()._setNodeLockState((id) => nodeIds.includes(id), false);
|
|
532
569
|
},
|
|
533
|
-
|
|
534
|
-
get()._setNodeLockState(() =>
|
|
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));
|
|
570
|
+
unlockNode: (nodeId) => {
|
|
571
|
+
get()._setNodeLockState((id) => id === nodeId, false);
|
|
542
572
|
}
|
|
543
573
|
});
|
|
544
574
|
var createNodeSlice = (set, get) => ({
|
|
@@ -547,28 +577,28 @@ var createNodeSlice = (set, get) => ({
|
|
|
547
577
|
if (!nodeDef) return "";
|
|
548
578
|
const id = generateId();
|
|
549
579
|
const newNode = {
|
|
550
|
-
id,
|
|
551
|
-
type,
|
|
552
|
-
position,
|
|
553
580
|
data: {
|
|
554
581
|
...nodeDef.defaultData,
|
|
555
582
|
label: nodeDef.label,
|
|
556
583
|
status: "idle"
|
|
557
584
|
},
|
|
558
|
-
|
|
585
|
+
id,
|
|
586
|
+
position,
|
|
587
|
+
type,
|
|
588
|
+
...type === "download" && { height: 320, width: 280 }
|
|
559
589
|
};
|
|
560
590
|
set((state) => ({
|
|
561
|
-
|
|
562
|
-
|
|
591
|
+
isDirty: true,
|
|
592
|
+
nodes: [...state.nodes, newNode]
|
|
563
593
|
}));
|
|
564
594
|
return id;
|
|
565
595
|
},
|
|
566
596
|
addNodesAndEdges: (newNodes, newEdges) => {
|
|
567
597
|
if (newNodes.length === 0) return;
|
|
568
598
|
set((state) => ({
|
|
569
|
-
nodes: [...state.nodes, ...newNodes],
|
|
570
599
|
edges: [...state.edges, ...newEdges],
|
|
571
|
-
isDirty: true
|
|
600
|
+
isDirty: true,
|
|
601
|
+
nodes: [...state.nodes, ...newNodes]
|
|
572
602
|
}));
|
|
573
603
|
const { propagateOutputsDownstream } = get();
|
|
574
604
|
const sourceNodeIds = new Set(newEdges.map((e) => e.source));
|
|
@@ -576,49 +606,17 @@ var createNodeSlice = (set, get) => ({
|
|
|
576
606
|
propagateOutputsDownstream(sourceId);
|
|
577
607
|
}
|
|
578
608
|
},
|
|
579
|
-
|
|
580
|
-
const
|
|
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));
|
|
609
|
+
addToGlobalHistory: (item) => {
|
|
610
|
+
const id = `history-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
585
611
|
set((state) => ({
|
|
586
|
-
|
|
587
|
-
|
|
612
|
+
globalImageHistory: [{ ...item, id }, ...state.globalImageHistory].slice(
|
|
613
|
+
0,
|
|
614
|
+
100
|
|
615
|
+
)
|
|
588
616
|
}));
|
|
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
617
|
},
|
|
616
|
-
|
|
617
|
-
set(
|
|
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
|
-
}));
|
|
618
|
+
clearGlobalHistory: () => {
|
|
619
|
+
set({ globalImageHistory: [] });
|
|
622
620
|
},
|
|
623
621
|
duplicateNode: (nodeId) => {
|
|
624
622
|
const { nodes, edges, edgeStyle, propagateOutputsDownstream } = get();
|
|
@@ -627,18 +625,20 @@ var createNodeSlice = (set, get) => ({
|
|
|
627
625
|
const newId = generateId();
|
|
628
626
|
const newNode = {
|
|
629
627
|
...node,
|
|
628
|
+
data: {
|
|
629
|
+
...node.data,
|
|
630
|
+
jobId: null,
|
|
631
|
+
status: "idle"
|
|
632
|
+
},
|
|
630
633
|
id: newId,
|
|
631
634
|
position: {
|
|
632
635
|
x: node.position.x + 50,
|
|
633
636
|
y: node.position.y + 50
|
|
634
|
-
},
|
|
635
|
-
data: {
|
|
636
|
-
...node.data,
|
|
637
|
-
status: "idle",
|
|
638
|
-
jobId: null
|
|
639
637
|
}
|
|
640
638
|
};
|
|
641
|
-
const incomingEdges = edges.filter(
|
|
639
|
+
const incomingEdges = edges.filter(
|
|
640
|
+
(e) => e.target === nodeId && e.source !== nodeId
|
|
641
|
+
);
|
|
642
642
|
const clonedEdges = incomingEdges.map((edge) => ({
|
|
643
643
|
...edge,
|
|
644
644
|
id: generateId(),
|
|
@@ -646,9 +646,9 @@ var createNodeSlice = (set, get) => ({
|
|
|
646
646
|
type: edgeStyle
|
|
647
647
|
}));
|
|
648
648
|
set((state) => ({
|
|
649
|
-
nodes: [...state.nodes, newNode],
|
|
650
649
|
edges: [...state.edges, ...clonedEdges],
|
|
651
|
-
isDirty: true
|
|
650
|
+
isDirty: true,
|
|
651
|
+
nodes: [...state.nodes, newNode]
|
|
652
652
|
}));
|
|
653
653
|
const sourcesNotified = /* @__PURE__ */ new Set();
|
|
654
654
|
for (const edge of incomingEdges) {
|
|
@@ -665,22 +665,66 @@ var createNodeSlice = (set, get) => ({
|
|
|
665
665
|
if (!sourceNode) return;
|
|
666
666
|
const output = outputValue ?? getNodeOutput(sourceNode);
|
|
667
667
|
if (!output) return;
|
|
668
|
-
const updates = computeDownstreamUpdates(
|
|
668
|
+
const updates = computeDownstreamUpdates(
|
|
669
|
+
sourceNodeId,
|
|
670
|
+
output,
|
|
671
|
+
nodes,
|
|
672
|
+
edges
|
|
673
|
+
);
|
|
669
674
|
if (updates.size === 0) return;
|
|
670
675
|
if (!hasStateChanged(updates, nodes)) return;
|
|
671
676
|
set((state) => ({
|
|
672
|
-
|
|
673
|
-
|
|
677
|
+
isDirty: true,
|
|
678
|
+
nodes: applyNodeUpdates(state.nodes, updates)
|
|
674
679
|
}));
|
|
675
680
|
},
|
|
676
|
-
|
|
677
|
-
const id = `history-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
681
|
+
removeNode: (nodeId) => {
|
|
678
682
|
set((state) => ({
|
|
679
|
-
|
|
683
|
+
edges: state.edges.filter(
|
|
684
|
+
(edge) => edge.source !== nodeId && edge.target !== nodeId
|
|
685
|
+
),
|
|
686
|
+
isDirty: true,
|
|
687
|
+
nodes: state.nodes.filter((node) => node.id !== nodeId)
|
|
680
688
|
}));
|
|
681
689
|
},
|
|
682
|
-
|
|
683
|
-
|
|
690
|
+
updateNodeData: (nodeId, data) => {
|
|
691
|
+
const { nodes, propagateOutputsDownstream } = get();
|
|
692
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
693
|
+
const TRANSIENT_KEYS = /* @__PURE__ */ new Set(["status", "progress", "error", "jobId"]);
|
|
694
|
+
const dataKeys = Object.keys(data);
|
|
695
|
+
const hasPersistedChange = dataKeys.some((key) => !TRANSIENT_KEYS.has(key));
|
|
696
|
+
set((state) => ({
|
|
697
|
+
nodes: state.nodes.map(
|
|
698
|
+
(n) => n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
|
|
699
|
+
),
|
|
700
|
+
...hasPersistedChange && { isDirty: true }
|
|
701
|
+
}));
|
|
702
|
+
const inputNodeTypes = [
|
|
703
|
+
"prompt",
|
|
704
|
+
"image",
|
|
705
|
+
"imageInput",
|
|
706
|
+
"video",
|
|
707
|
+
"videoInput",
|
|
708
|
+
"audio",
|
|
709
|
+
"audioInput",
|
|
710
|
+
"tweetParser"
|
|
711
|
+
];
|
|
712
|
+
const hasOutputUpdate = "outputImage" in data || "outputImages" in data || "outputVideo" in data || "outputAudio" in data || "outputText" in data;
|
|
713
|
+
if (node && (inputNodeTypes.includes(node.type) || hasOutputUpdate)) {
|
|
714
|
+
if (hasOutputUpdate) {
|
|
715
|
+
const dataRecord = data;
|
|
716
|
+
if ("outputImages" in dataRecord) {
|
|
717
|
+
propagateOutputsDownstream(nodeId);
|
|
718
|
+
} else {
|
|
719
|
+
const outputValue = dataRecord.outputImage ?? dataRecord.outputVideo ?? dataRecord.outputAudio ?? dataRecord.outputText;
|
|
720
|
+
if (typeof outputValue === "string") {
|
|
721
|
+
propagateOutputsDownstream(nodeId, outputValue);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
propagateOutputsDownstream(nodeId);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
684
728
|
}
|
|
685
729
|
});
|
|
686
730
|
function normalizeEdgeTypes(edges) {
|
|
@@ -703,49 +747,46 @@ function hydrateWorkflowNodes(nodes) {
|
|
|
703
747
|
});
|
|
704
748
|
}
|
|
705
749
|
var createPersistenceSlice = (set, get) => ({
|
|
706
|
-
loadWorkflow: (workflow) => {
|
|
707
|
-
const hydratedNodes = hydrateWorkflowNodes(workflow.nodes);
|
|
708
|
-
set({
|
|
709
|
-
nodes: hydratedNodes,
|
|
710
|
-
edges: normalizeEdgeTypes(workflow.edges),
|
|
711
|
-
edgeStyle: workflow.edgeStyle,
|
|
712
|
-
workflowName: workflow.name,
|
|
713
|
-
workflowId: null,
|
|
714
|
-
isDirty: true,
|
|
715
|
-
groups: workflow.groups ?? [],
|
|
716
|
-
selectedNodeIds: []
|
|
717
|
-
});
|
|
718
|
-
propagateExistingOutputs(hydratedNodes, get().propagateOutputsDownstream);
|
|
719
|
-
set({ isDirty: false });
|
|
720
|
-
},
|
|
721
750
|
clearWorkflow: () => {
|
|
722
751
|
set({
|
|
723
|
-
nodes: [],
|
|
724
752
|
edges: [],
|
|
725
|
-
workflowName: "Untitled Workflow",
|
|
726
|
-
workflowId: null,
|
|
727
|
-
isDirty: false,
|
|
728
753
|
groups: [],
|
|
729
|
-
|
|
754
|
+
isDirty: false,
|
|
755
|
+
nodes: [],
|
|
756
|
+
selectedNodeIds: [],
|
|
757
|
+
workflowId: null,
|
|
758
|
+
workflowName: "Untitled Workflow"
|
|
730
759
|
});
|
|
731
760
|
},
|
|
761
|
+
createNewWorkflow: async () => {
|
|
762
|
+
throw new Error(
|
|
763
|
+
"createNewWorkflow not implemented - consuming app must provide API integration"
|
|
764
|
+
);
|
|
765
|
+
},
|
|
766
|
+
deleteWorkflow: async () => {
|
|
767
|
+
throw new Error(
|
|
768
|
+
"deleteWorkflow not implemented - consuming app must provide API integration"
|
|
769
|
+
);
|
|
770
|
+
},
|
|
771
|
+
duplicateWorkflowApi: async () => {
|
|
772
|
+
throw new Error(
|
|
773
|
+
"duplicateWorkflowApi not implemented - consuming app must provide API integration"
|
|
774
|
+
);
|
|
775
|
+
},
|
|
732
776
|
exportWorkflow: () => {
|
|
733
777
|
const { nodes, edges, edgeStyle, workflowName, groups } = get();
|
|
734
778
|
return {
|
|
735
|
-
|
|
736
|
-
name: workflowName,
|
|
779
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
737
780
|
description: "",
|
|
738
|
-
nodes,
|
|
739
|
-
edges,
|
|
740
781
|
edgeStyle,
|
|
782
|
+
edges,
|
|
741
783
|
groups,
|
|
742
|
-
|
|
743
|
-
|
|
784
|
+
name: workflowName,
|
|
785
|
+
nodes,
|
|
786
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
787
|
+
version: 1
|
|
744
788
|
};
|
|
745
789
|
},
|
|
746
|
-
getNodeById: (id) => {
|
|
747
|
-
return get().nodes.find((node) => node.id === id);
|
|
748
|
-
},
|
|
749
790
|
getConnectedInputs: (nodeId) => {
|
|
750
791
|
const { nodes, edges } = get();
|
|
751
792
|
const inputs = /* @__PURE__ */ new Map();
|
|
@@ -800,25 +841,95 @@ var createPersistenceSlice = (set, get) => ({
|
|
|
800
841
|
}
|
|
801
842
|
return Array.from(connected);
|
|
802
843
|
},
|
|
844
|
+
getNodeById: (id) => {
|
|
845
|
+
return get().nodes.find((node) => node.id === id);
|
|
846
|
+
},
|
|
847
|
+
getNodesWithComments: () => {
|
|
848
|
+
const { nodes } = get();
|
|
849
|
+
return nodes.filter((node) => {
|
|
850
|
+
const data = node.data;
|
|
851
|
+
return data.comment?.trim();
|
|
852
|
+
}).sort((a, b) => {
|
|
853
|
+
if (Math.abs(a.position.y - b.position.y) < 50) {
|
|
854
|
+
return a.position.x - b.position.x;
|
|
855
|
+
}
|
|
856
|
+
return a.position.y - b.position.y;
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
getUnviewedCommentCount: () => {
|
|
860
|
+
const { nodes, viewedCommentIds } = get();
|
|
861
|
+
return nodes.filter((node) => {
|
|
862
|
+
const data = node.data;
|
|
863
|
+
return data.comment?.trim() && !viewedCommentIds.has(node.id);
|
|
864
|
+
}).length;
|
|
865
|
+
},
|
|
866
|
+
listWorkflows: async () => {
|
|
867
|
+
throw new Error(
|
|
868
|
+
"listWorkflows not implemented - consuming app must provide API integration"
|
|
869
|
+
);
|
|
870
|
+
},
|
|
871
|
+
loadWorkflow: (workflow) => {
|
|
872
|
+
const hydratedNodes = hydrateWorkflowNodes(workflow.nodes);
|
|
873
|
+
set({
|
|
874
|
+
edgeStyle: workflow.edgeStyle,
|
|
875
|
+
edges: normalizeEdgeTypes(workflow.edges),
|
|
876
|
+
groups: workflow.groups ?? [],
|
|
877
|
+
isDirty: true,
|
|
878
|
+
nodes: hydratedNodes,
|
|
879
|
+
selectedNodeIds: [],
|
|
880
|
+
workflowId: null,
|
|
881
|
+
workflowName: workflow.name
|
|
882
|
+
});
|
|
883
|
+
propagateExistingOutputs(hydratedNodes, get().propagateOutputsDownstream);
|
|
884
|
+
set({ isDirty: false });
|
|
885
|
+
},
|
|
886
|
+
loadWorkflowById: async () => {
|
|
887
|
+
throw new Error(
|
|
888
|
+
"loadWorkflowById not implemented - consuming app must provide API integration"
|
|
889
|
+
);
|
|
890
|
+
},
|
|
891
|
+
markCommentViewed: (nodeId) => {
|
|
892
|
+
set((state) => {
|
|
893
|
+
const newSet = new Set(state.viewedCommentIds);
|
|
894
|
+
newSet.add(nodeId);
|
|
895
|
+
return { viewedCommentIds: newSet };
|
|
896
|
+
});
|
|
897
|
+
},
|
|
898
|
+
// API operations - stubs that throw by default.
|
|
899
|
+
// Consuming apps override these via the store creator or by extending the slice.
|
|
900
|
+
saveWorkflow: async () => {
|
|
901
|
+
throw new Error(
|
|
902
|
+
"saveWorkflow not implemented - consuming app must provide API integration"
|
|
903
|
+
);
|
|
904
|
+
},
|
|
905
|
+
setDirty: (dirty) => {
|
|
906
|
+
set({ isDirty: dirty });
|
|
907
|
+
},
|
|
908
|
+
setNavigationTarget: (nodeId) => {
|
|
909
|
+
set({ navigationTargetId: nodeId });
|
|
910
|
+
},
|
|
911
|
+
setWorkflowName: (name) => {
|
|
912
|
+
set({ isDirty: true, workflowName: name });
|
|
913
|
+
},
|
|
803
914
|
validateWorkflow: () => {
|
|
804
915
|
const { nodes, edges } = get();
|
|
805
916
|
const errors = [];
|
|
806
917
|
const warnings = [];
|
|
807
918
|
if (nodes.length === 0) {
|
|
808
919
|
errors.push({
|
|
809
|
-
nodeId: "",
|
|
810
920
|
message: "Workflow is empty - add some nodes first",
|
|
921
|
+
nodeId: "",
|
|
811
922
|
severity: "error"
|
|
812
923
|
});
|
|
813
|
-
return { isValid: false,
|
|
924
|
+
return { errors, isValid: false, warnings };
|
|
814
925
|
}
|
|
815
926
|
if (edges.length === 0 && nodes.length > 1) {
|
|
816
927
|
errors.push({
|
|
817
|
-
nodeId: "",
|
|
818
928
|
message: "No connections - connect your nodes together",
|
|
929
|
+
nodeId: "",
|
|
819
930
|
severity: "error"
|
|
820
931
|
});
|
|
821
|
-
return { isValid: false,
|
|
932
|
+
return { errors, isValid: false, warnings };
|
|
822
933
|
}
|
|
823
934
|
const hasNodeOutput = (node) => {
|
|
824
935
|
const data = node.data;
|
|
@@ -841,19 +952,23 @@ var createPersistenceSlice = (set, get) => ({
|
|
|
841
952
|
const incomingEdges = edges.filter((e) => e.target === node.id);
|
|
842
953
|
for (const input of nodeDef.inputs) {
|
|
843
954
|
if (input.required) {
|
|
844
|
-
const connectionEdge = incomingEdges.find(
|
|
955
|
+
const connectionEdge = incomingEdges.find(
|
|
956
|
+
(e) => e.targetHandle === input.id
|
|
957
|
+
);
|
|
845
958
|
if (!connectionEdge) {
|
|
846
959
|
errors.push({
|
|
847
|
-
nodeId: node.id,
|
|
848
960
|
message: `Missing required input: ${input.label}`,
|
|
961
|
+
nodeId: node.id,
|
|
849
962
|
severity: "error"
|
|
850
963
|
});
|
|
851
964
|
} else {
|
|
852
|
-
const sourceNode = nodes.find(
|
|
965
|
+
const sourceNode = nodes.find(
|
|
966
|
+
(n) => n.id === connectionEdge.source
|
|
967
|
+
);
|
|
853
968
|
if (sourceNode && !hasNodeOutput(sourceNode)) {
|
|
854
969
|
errors.push({
|
|
855
|
-
nodeId: sourceNode.id,
|
|
856
970
|
message: `${sourceNode.data.label} is empty`,
|
|
971
|
+
nodeId: sourceNode.id,
|
|
857
972
|
severity: "error"
|
|
858
973
|
});
|
|
859
974
|
}
|
|
@@ -878,8 +993,8 @@ var createPersistenceSlice = (set, get) => ({
|
|
|
878
993
|
for (const node of nodes) {
|
|
879
994
|
if (hasCycle(node.id)) {
|
|
880
995
|
errors.push({
|
|
881
|
-
nodeId: node.id,
|
|
882
996
|
message: "Workflow contains a cycle",
|
|
997
|
+
nodeId: node.id,
|
|
883
998
|
severity: "error"
|
|
884
999
|
});
|
|
885
1000
|
break;
|
|
@@ -890,146 +1005,82 @@ var createPersistenceSlice = (set, get) => ({
|
|
|
890
1005
|
const refData = node.data;
|
|
891
1006
|
if (!refData.referencedWorkflowId) {
|
|
892
1007
|
errors.push({
|
|
893
|
-
nodeId: node.id,
|
|
894
1008
|
message: "Subworkflow node must reference a workflow",
|
|
1009
|
+
nodeId: node.id,
|
|
895
1010
|
severity: "error"
|
|
896
1011
|
});
|
|
897
1012
|
} else if (!refData.cachedInterface) {
|
|
898
1013
|
warnings.push({
|
|
899
|
-
nodeId: node.id,
|
|
900
1014
|
message: "Subworkflow interface not loaded - refresh to update handles",
|
|
1015
|
+
nodeId: node.id,
|
|
901
1016
|
severity: "warning"
|
|
902
1017
|
});
|
|
903
1018
|
}
|
|
904
1019
|
}
|
|
905
1020
|
}
|
|
906
1021
|
return {
|
|
907
|
-
isValid: errors.length === 0,
|
|
908
1022
|
errors,
|
|
1023
|
+
isValid: errors.length === 0,
|
|
909
1024
|
warnings
|
|
910
1025
|
};
|
|
911
|
-
},
|
|
912
|
-
setDirty: (dirty) => {
|
|
913
|
-
set({ isDirty: dirty });
|
|
914
|
-
},
|
|
915
|
-
setWorkflowName: (name) => {
|
|
916
|
-
set({ workflowName: name, isDirty: true });
|
|
917
|
-
},
|
|
918
|
-
// API operations - stubs that throw by default.
|
|
919
|
-
// Consuming apps override these via the store creator or by extending the slice.
|
|
920
|
-
saveWorkflow: async () => {
|
|
921
|
-
throw new Error("saveWorkflow not implemented - consuming app must provide API integration");
|
|
922
|
-
},
|
|
923
|
-
loadWorkflowById: async () => {
|
|
924
|
-
throw new Error(
|
|
925
|
-
"loadWorkflowById not implemented - consuming app must provide API integration"
|
|
926
|
-
);
|
|
927
|
-
},
|
|
928
|
-
listWorkflows: async () => {
|
|
929
|
-
throw new Error("listWorkflows not implemented - consuming app must provide API integration");
|
|
930
|
-
},
|
|
931
|
-
deleteWorkflow: async () => {
|
|
932
|
-
throw new Error("deleteWorkflow not implemented - consuming app must provide API integration");
|
|
933
|
-
},
|
|
934
|
-
duplicateWorkflowApi: async () => {
|
|
935
|
-
throw new Error(
|
|
936
|
-
"duplicateWorkflowApi not implemented - consuming app must provide API integration"
|
|
937
|
-
);
|
|
938
|
-
},
|
|
939
|
-
createNewWorkflow: async () => {
|
|
940
|
-
throw new Error(
|
|
941
|
-
"createNewWorkflow not implemented - consuming app must provide API integration"
|
|
942
|
-
);
|
|
943
|
-
},
|
|
944
|
-
getNodesWithComments: () => {
|
|
945
|
-
const { nodes } = get();
|
|
946
|
-
return nodes.filter((node) => {
|
|
947
|
-
const data = node.data;
|
|
948
|
-
return data.comment?.trim();
|
|
949
|
-
}).sort((a, b) => {
|
|
950
|
-
if (Math.abs(a.position.y - b.position.y) < 50) {
|
|
951
|
-
return a.position.x - b.position.x;
|
|
952
|
-
}
|
|
953
|
-
return a.position.y - b.position.y;
|
|
954
|
-
});
|
|
955
|
-
},
|
|
956
|
-
markCommentViewed: (nodeId) => {
|
|
957
|
-
set((state) => {
|
|
958
|
-
const newSet = new Set(state.viewedCommentIds);
|
|
959
|
-
newSet.add(nodeId);
|
|
960
|
-
return { viewedCommentIds: newSet };
|
|
961
|
-
});
|
|
962
|
-
},
|
|
963
|
-
setNavigationTarget: (nodeId) => {
|
|
964
|
-
set({ navigationTargetId: nodeId });
|
|
965
|
-
},
|
|
966
|
-
getUnviewedCommentCount: () => {
|
|
967
|
-
const { nodes, viewedCommentIds } = get();
|
|
968
|
-
return nodes.filter((node) => {
|
|
969
|
-
const data = node.data;
|
|
970
|
-
return data.comment?.trim() && !viewedCommentIds.has(node.id);
|
|
971
|
-
}).length;
|
|
972
1026
|
}
|
|
973
1027
|
});
|
|
974
1028
|
|
|
975
1029
|
// src/stores/workflow/slices/selectionSlice.ts
|
|
976
1030
|
var createSelectionSlice = (set) => ({
|
|
977
|
-
setSelectedNodeIds: (nodeIds) => {
|
|
978
|
-
set({ selectedNodeIds: nodeIds });
|
|
979
|
-
},
|
|
980
1031
|
addToSelection: (nodeId) => {
|
|
981
1032
|
set((state) => ({
|
|
982
1033
|
selectedNodeIds: state.selectedNodeIds.includes(nodeId) ? state.selectedNodeIds : [...state.selectedNodeIds, nodeId]
|
|
983
1034
|
}));
|
|
984
1035
|
},
|
|
1036
|
+
clearSelection: () => {
|
|
1037
|
+
set({ selectedNodeIds: [] });
|
|
1038
|
+
},
|
|
985
1039
|
removeFromSelection: (nodeId) => {
|
|
986
1040
|
set((state) => ({
|
|
987
1041
|
selectedNodeIds: state.selectedNodeIds.filter((id) => id !== nodeId)
|
|
988
1042
|
}));
|
|
989
1043
|
},
|
|
990
|
-
|
|
991
|
-
set({ selectedNodeIds:
|
|
1044
|
+
setSelectedNodeIds: (nodeIds) => {
|
|
1045
|
+
set({ selectedNodeIds: nodeIds });
|
|
992
1046
|
}
|
|
993
1047
|
});
|
|
994
1048
|
|
|
995
1049
|
// src/stores/workflow/slices/snapshotSlice.ts
|
|
996
1050
|
function defaultApplyEditOperations(_operations, state) {
|
|
997
|
-
return {
|
|
1051
|
+
return { applied: 0, edges: state.edges, nodes: state.nodes, skipped: [] };
|
|
998
1052
|
}
|
|
999
1053
|
var createSnapshotSlice = (set, get) => ({
|
|
1000
|
-
|
|
1001
|
-
|
|
1054
|
+
applyEditOperations: (operations) => {
|
|
1055
|
+
const state = get();
|
|
1056
|
+
const result = defaultApplyEditOperations(operations, {
|
|
1057
|
+
edges: state.edges,
|
|
1058
|
+
nodes: state.nodes
|
|
1059
|
+
});
|
|
1060
|
+
set({
|
|
1061
|
+
edges: result.edges,
|
|
1062
|
+
isDirty: true,
|
|
1063
|
+
nodes: result.nodes
|
|
1064
|
+
});
|
|
1065
|
+
return { applied: result.applied, skipped: result.skipped };
|
|
1066
|
+
},
|
|
1002
1067
|
captureSnapshot: () => {
|
|
1003
1068
|
const state = get();
|
|
1004
1069
|
const snapshot = {
|
|
1005
|
-
|
|
1070
|
+
edgeStyle: state.edgeStyle,
|
|
1006
1071
|
edges: JSON.parse(JSON.stringify(state.edges)),
|
|
1007
1072
|
groups: JSON.parse(JSON.stringify(state.groups)),
|
|
1008
|
-
|
|
1073
|
+
nodes: JSON.parse(JSON.stringify(state.nodes))
|
|
1009
1074
|
};
|
|
1010
1075
|
set({
|
|
1011
|
-
|
|
1012
|
-
|
|
1076
|
+
manualChangeCount: 0,
|
|
1077
|
+
previousWorkflowSnapshot: snapshot
|
|
1013
1078
|
});
|
|
1014
1079
|
},
|
|
1015
|
-
revertToSnapshot: () => {
|
|
1016
|
-
const state = get();
|
|
1017
|
-
if (state.previousWorkflowSnapshot) {
|
|
1018
|
-
set({
|
|
1019
|
-
nodes: state.previousWorkflowSnapshot.nodes,
|
|
1020
|
-
edges: state.previousWorkflowSnapshot.edges,
|
|
1021
|
-
groups: state.previousWorkflowSnapshot.groups,
|
|
1022
|
-
edgeStyle: state.previousWorkflowSnapshot.edgeStyle,
|
|
1023
|
-
previousWorkflowSnapshot: null,
|
|
1024
|
-
manualChangeCount: 0,
|
|
1025
|
-
isDirty: true
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
},
|
|
1029
1080
|
clearSnapshot: () => {
|
|
1030
1081
|
set({
|
|
1031
|
-
|
|
1032
|
-
|
|
1082
|
+
manualChangeCount: 0,
|
|
1083
|
+
previousWorkflowSnapshot: null
|
|
1033
1084
|
});
|
|
1034
1085
|
},
|
|
1035
1086
|
incrementManualChangeCount: () => {
|
|
@@ -1037,44 +1088,47 @@ var createSnapshotSlice = (set, get) => ({
|
|
|
1037
1088
|
const newCount = state.manualChangeCount + 1;
|
|
1038
1089
|
if (newCount >= 3) {
|
|
1039
1090
|
set({
|
|
1040
|
-
|
|
1041
|
-
|
|
1091
|
+
manualChangeCount: 0,
|
|
1092
|
+
previousWorkflowSnapshot: null
|
|
1042
1093
|
});
|
|
1043
1094
|
} else {
|
|
1044
1095
|
set({ manualChangeCount: newCount });
|
|
1045
1096
|
}
|
|
1046
1097
|
},
|
|
1047
|
-
|
|
1098
|
+
manualChangeCount: 0,
|
|
1099
|
+
previousWorkflowSnapshot: null,
|
|
1100
|
+
revertToSnapshot: () => {
|
|
1048
1101
|
const state = get();
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1102
|
+
if (state.previousWorkflowSnapshot) {
|
|
1103
|
+
set({
|
|
1104
|
+
edgeStyle: state.previousWorkflowSnapshot.edgeStyle,
|
|
1105
|
+
edges: state.previousWorkflowSnapshot.edges,
|
|
1106
|
+
groups: state.previousWorkflowSnapshot.groups,
|
|
1107
|
+
isDirty: true,
|
|
1108
|
+
manualChangeCount: 0,
|
|
1109
|
+
nodes: state.previousWorkflowSnapshot.nodes,
|
|
1110
|
+
previousWorkflowSnapshot: null
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1059
1113
|
}
|
|
1060
1114
|
});
|
|
1061
1115
|
|
|
1062
1116
|
// src/stores/workflow/workflowStore.ts
|
|
1063
1117
|
var storeCreator = ((...args) => ({
|
|
1064
|
-
// Initial state
|
|
1065
|
-
nodes: [],
|
|
1066
|
-
edges: [],
|
|
1067
1118
|
edgeStyle: "default",
|
|
1068
|
-
|
|
1069
|
-
|
|
1119
|
+
edges: [],
|
|
1120
|
+
globalImageHistory: [],
|
|
1121
|
+
groups: [],
|
|
1070
1122
|
isDirty: false,
|
|
1071
|
-
isSaving: false,
|
|
1072
1123
|
isLoading: false,
|
|
1073
|
-
|
|
1124
|
+
isSaving: false,
|
|
1125
|
+
navigationTargetId: null,
|
|
1126
|
+
// Initial state
|
|
1127
|
+
nodes: [],
|
|
1074
1128
|
selectedNodeIds: [],
|
|
1075
1129
|
viewedCommentIds: /* @__PURE__ */ new Set(),
|
|
1076
|
-
|
|
1077
|
-
|
|
1130
|
+
workflowId: null,
|
|
1131
|
+
workflowName: "Untitled Workflow",
|
|
1078
1132
|
// Compose slices
|
|
1079
1133
|
...createNodeSlice(...args),
|
|
1080
1134
|
...createEdgeSlice(...args),
|
|
@@ -1087,16 +1141,16 @@ var storeCreator = ((...args) => ({
|
|
|
1087
1141
|
}));
|
|
1088
1142
|
var useWorkflowStore = create()(
|
|
1089
1143
|
temporal(storeCreator, {
|
|
1144
|
+
// Optimized equality check using shallow comparison instead of JSON.stringify
|
|
1145
|
+
equality: temporalStateEquals,
|
|
1146
|
+
// Limit history to prevent memory issues
|
|
1147
|
+
limit: 50,
|
|
1090
1148
|
// Only track meaningful state (not UI flags like isDirty, isSaving, etc.)
|
|
1091
1149
|
partialize: (state) => ({
|
|
1092
|
-
nodes: state.nodes,
|
|
1093
1150
|
edges: state.edges,
|
|
1094
|
-
groups: state.groups
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
limit: 50,
|
|
1098
|
-
// Optimized equality check using shallow comparison instead of JSON.stringify
|
|
1099
|
-
equality: temporalStateEquals
|
|
1151
|
+
groups: state.groups,
|
|
1152
|
+
nodes: state.nodes
|
|
1153
|
+
})
|
|
1100
1154
|
})
|
|
1101
1155
|
);
|
|
1102
1156
|
|