@runtypelabs/react-flow 0.1.0
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/README.md +289 -0
- package/example/.env.example +3 -0
- package/example/index.html +25 -0
- package/example/node_modules/.bin/browserslist +21 -0
- package/example/node_modules/.bin/terser +21 -0
- package/example/node_modules/.bin/tsc +21 -0
- package/example/node_modules/.bin/tsserver +21 -0
- package/example/node_modules/.bin/vite +21 -0
- package/example/package.json +26 -0
- package/example/src/App.tsx +1744 -0
- package/example/src/main.tsx +11 -0
- package/example/tsconfig.json +21 -0
- package/example/vite.config.ts +13 -0
- package/package.json +65 -0
- package/src/components/RuntypeFlowEditor.tsx +528 -0
- package/src/components/nodes/BaseNode.tsx +357 -0
- package/src/components/nodes/CodeNode.tsx +252 -0
- package/src/components/nodes/ConditionalNode.tsx +264 -0
- package/src/components/nodes/FetchUrlNode.tsx +299 -0
- package/src/components/nodes/PromptNode.tsx +270 -0
- package/src/components/nodes/SendEmailNode.tsx +311 -0
- package/src/hooks/useFlowValidation.ts +424 -0
- package/src/hooks/useRuntypeFlow.ts +414 -0
- package/src/index.ts +28 -0
- package/src/types/index.ts +332 -0
- package/src/utils/adapter.ts +544 -0
- package/src/utils/layout.ts +284 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import type { FlowStep, RuntypeNode, RuntypeEdge, RuntypeNodeData } from '../types'
|
|
2
|
+
import type { FlowStepType } from '@travrse/shared'
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Constants
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
const NODE_WIDTH = 280
|
|
9
|
+
const NODE_HEIGHT = 200 // Increased for better card visibility
|
|
10
|
+
const NODE_SPACING_X = 350 // Horizontal space between main steps
|
|
11
|
+
const NODE_SPACING_Y = 80 // Vertical space between branch steps
|
|
12
|
+
const BRANCH_OFFSET_X = 350 // Horizontal offset for branches to the right of conditional
|
|
13
|
+
const BRANCH_OFFSET_Y = -80 // Vertical offset for true branch (above center)
|
|
14
|
+
const FALSE_BRANCH_GAP = 100 // Gap between true and false branches
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Flow Steps to React Flow Nodes
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
interface FlowStepsToNodesOptions {
|
|
21
|
+
onChange?: (stepId: string, updates: Partial<FlowStep>) => void
|
|
22
|
+
onDelete?: (stepId: string) => void
|
|
23
|
+
startPosition?: { x: number; y: number }
|
|
24
|
+
/** Internal: prefix for nested step IDs */
|
|
25
|
+
idPrefix?: string
|
|
26
|
+
/** Internal: parent conditional ID */
|
|
27
|
+
parentId?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert Runtype FlowStep array to React Flow Node array
|
|
32
|
+
* Handles nested conditional branches with vertical stacking below conditional
|
|
33
|
+
*/
|
|
34
|
+
export function flowStepsToNodes(
|
|
35
|
+
steps: FlowStep[],
|
|
36
|
+
options?: FlowStepsToNodesOptions
|
|
37
|
+
): RuntypeNode[] {
|
|
38
|
+
const {
|
|
39
|
+
onChange,
|
|
40
|
+
onDelete,
|
|
41
|
+
startPosition = { x: 50, y: 200 },
|
|
42
|
+
idPrefix = '',
|
|
43
|
+
parentId,
|
|
44
|
+
} = options ?? {}
|
|
45
|
+
|
|
46
|
+
// Sort steps by order
|
|
47
|
+
const sortedSteps = [...steps].sort((a, b) => a.order - b.order)
|
|
48
|
+
|
|
49
|
+
const nodes: RuntypeNode[] = []
|
|
50
|
+
let currentX = startPosition.x
|
|
51
|
+
let maxBranchY = startPosition.y // Track the maximum Y extent for branches
|
|
52
|
+
|
|
53
|
+
for (const step of sortedSteps) {
|
|
54
|
+
const nodeId = idPrefix ? `${idPrefix}${step.id}` : step.id
|
|
55
|
+
|
|
56
|
+
const nodeData: RuntypeNodeData = {
|
|
57
|
+
step,
|
|
58
|
+
label: step.name || getDefaultStepName(step.type),
|
|
59
|
+
onChange,
|
|
60
|
+
onDelete,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const node: RuntypeNode = {
|
|
64
|
+
id: nodeId,
|
|
65
|
+
type: step.type,
|
|
66
|
+
position: { x: currentX, y: startPosition.y },
|
|
67
|
+
data: nodeData,
|
|
68
|
+
draggable: true,
|
|
69
|
+
selectable: true,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parentId) {
|
|
73
|
+
node.parentId = parentId
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
nodes.push(node)
|
|
77
|
+
|
|
78
|
+
// DEBUG: Log node positions
|
|
79
|
+
console.log(`[flowStepsToNodes] Placed "${step.name}" (${step.type}) at x=${currentX}`)
|
|
80
|
+
|
|
81
|
+
// Handle conditional branches - position to the right with true above, false below
|
|
82
|
+
if (step.type === 'conditional' && step.config) {
|
|
83
|
+
const config = step.config as { trueSteps?: FlowStep[]; falseSteps?: FlowStep[] }
|
|
84
|
+
const branchX = currentX + BRANCH_OFFSET_X
|
|
85
|
+
|
|
86
|
+
// Calculate branch lengths (horizontal extent)
|
|
87
|
+
const trueBranchLength = config.trueSteps?.length || 0
|
|
88
|
+
const falseBranchLength = config.falseSteps?.length || 0
|
|
89
|
+
const maxBranchLength = Math.max(trueBranchLength, falseBranchLength)
|
|
90
|
+
|
|
91
|
+
// Calculate true branch height (vertical extent for stacking)
|
|
92
|
+
const trueBranchHeight = trueBranchLength * (NODE_HEIGHT + NODE_SPACING_Y)
|
|
93
|
+
|
|
94
|
+
// True branch nodes (positioned to the right, above center)
|
|
95
|
+
if (config.trueSteps && config.trueSteps.length > 0) {
|
|
96
|
+
const trueBranchY = startPosition.y + BRANCH_OFFSET_Y
|
|
97
|
+
const trueBranchNodes = flowStepsToNodes(config.trueSteps, {
|
|
98
|
+
onChange,
|
|
99
|
+
onDelete,
|
|
100
|
+
startPosition: {
|
|
101
|
+
x: branchX,
|
|
102
|
+
y: trueBranchY
|
|
103
|
+
},
|
|
104
|
+
idPrefix: `${nodeId}-true-`,
|
|
105
|
+
parentId: nodeId,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
nodes.push(...trueBranchNodes)
|
|
109
|
+
maxBranchY = Math.max(maxBranchY, trueBranchY + trueBranchHeight)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// False branch nodes (positioned to the right, below center)
|
|
113
|
+
if (config.falseSteps && config.falseSteps.length > 0) {
|
|
114
|
+
// Position false branch below true branch (or at center + gap if no true branch)
|
|
115
|
+
const falseBranchY = trueBranchLength > 0
|
|
116
|
+
? startPosition.y + BRANCH_OFFSET_Y + trueBranchHeight + FALSE_BRANCH_GAP
|
|
117
|
+
: startPosition.y + NODE_HEIGHT + NODE_SPACING_Y
|
|
118
|
+
|
|
119
|
+
const falseBranchNodes = flowStepsToNodes(config.falseSteps, {
|
|
120
|
+
onChange,
|
|
121
|
+
onDelete,
|
|
122
|
+
startPosition: {
|
|
123
|
+
x: branchX,
|
|
124
|
+
y: falseBranchY
|
|
125
|
+
},
|
|
126
|
+
idPrefix: `${nodeId}-false-`,
|
|
127
|
+
parentId: nodeId,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
nodes.push(...falseBranchNodes)
|
|
131
|
+
const falseBranchHeight = config.falseSteps.length * (NODE_HEIGHT + NODE_SPACING_Y)
|
|
132
|
+
maxBranchY = Math.max(maxBranchY, falseBranchY + falseBranchHeight)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Skip past the conditional AND all its branch steps
|
|
136
|
+
// Branches start at branchX, so we need to account for:
|
|
137
|
+
// - The offset from conditional to first branch step (BRANCH_OFFSET_X)
|
|
138
|
+
// - All the branch steps (maxBranchLength * (NODE_WIDTH + NODE_SPACING_X))
|
|
139
|
+
if (maxBranchLength > 0) {
|
|
140
|
+
const advance = BRANCH_OFFSET_X + (maxBranchLength * (NODE_WIDTH + NODE_SPACING_X))
|
|
141
|
+
console.log(`[flowStepsToNodes] Conditional "${step.name}" has ${maxBranchLength} branch steps, advancing currentX by ${advance}`)
|
|
142
|
+
currentX += advance
|
|
143
|
+
console.log(`[flowStepsToNodes] After conditional, currentX = ${currentX}`)
|
|
144
|
+
} else {
|
|
145
|
+
currentX += NODE_WIDTH + NODE_SPACING_X
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Move to the next X position for horizontal layout (non-conditional steps)
|
|
149
|
+
currentX += NODE_WIDTH + NODE_SPACING_X
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return nodes
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// React Flow Nodes to Flow Steps
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convert React Flow Node array back to Runtype FlowStep array
|
|
162
|
+
*/
|
|
163
|
+
export function nodesToFlowSteps(nodes: RuntypeNode[]): FlowStep[] {
|
|
164
|
+
// Filter out branch nodes (handled within conditional steps)
|
|
165
|
+
const topLevelNodes = nodes.filter(n => !n.parentId && !n.id.includes('-true-') && !n.id.includes('-false-'))
|
|
166
|
+
|
|
167
|
+
// Sort by X position to determine order (horizontal layout)
|
|
168
|
+
const sortedNodes = [...topLevelNodes].sort((a, b) => a.position.x - b.position.x)
|
|
169
|
+
|
|
170
|
+
return sortedNodes.map((node, index) => {
|
|
171
|
+
const step = node.data.step
|
|
172
|
+
|
|
173
|
+
// Handle conditional steps - extract nested steps
|
|
174
|
+
if (step.type === 'conditional') {
|
|
175
|
+
const trueSteps = extractBranchSteps(nodes, node.id, 'true')
|
|
176
|
+
const falseSteps = extractBranchSteps(nodes, node.id, 'false')
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...step,
|
|
180
|
+
order: index,
|
|
181
|
+
config: {
|
|
182
|
+
...step.config,
|
|
183
|
+
trueSteps,
|
|
184
|
+
falseSteps,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
...step,
|
|
191
|
+
order: index,
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extract branch steps from conditional node
|
|
198
|
+
*/
|
|
199
|
+
function extractBranchSteps(nodes: RuntypeNode[], parentId: string, branch: 'true' | 'false'): FlowStep[] {
|
|
200
|
+
// Match nodes with the branch prefix pattern
|
|
201
|
+
const branchPrefix = `${parentId}-${branch}-`
|
|
202
|
+
const branchNodes = nodes.filter(n => n.id.startsWith(branchPrefix))
|
|
203
|
+
|
|
204
|
+
// Sort by X position (horizontal layout within branches)
|
|
205
|
+
const sortedBranchNodes = [...branchNodes].sort((a, b) => a.position.x - b.position.x)
|
|
206
|
+
|
|
207
|
+
return sortedBranchNodes.map((node, index) => ({
|
|
208
|
+
...node.data.step,
|
|
209
|
+
order: index,
|
|
210
|
+
}))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// Create Edges from Nodes
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create edges connecting nodes in sequence
|
|
219
|
+
*/
|
|
220
|
+
export function createEdgesFromNodes(nodes: RuntypeNode[]): RuntypeEdge[] {
|
|
221
|
+
const edges: RuntypeEdge[] = []
|
|
222
|
+
|
|
223
|
+
// Helper to check if a node is a branch node
|
|
224
|
+
const isBranchNode = (n: RuntypeNode) => n.parentId || n.id.includes('-true-') || n.id.includes('-false-')
|
|
225
|
+
|
|
226
|
+
// Get top-level nodes sorted by X position (horizontal layout)
|
|
227
|
+
const topLevelNodes = nodes
|
|
228
|
+
.filter(n => !isBranchNode(n))
|
|
229
|
+
.sort((a, b) => a.position.x - b.position.x)
|
|
230
|
+
|
|
231
|
+
// Create sequential edges for top-level nodes
|
|
232
|
+
// Skip conditionals - their branches connect to the next step instead
|
|
233
|
+
for (let i = 0; i < topLevelNodes.length - 1; i++) {
|
|
234
|
+
const sourceNode = topLevelNodes[i]
|
|
235
|
+
const targetNode = topLevelNodes[i + 1]
|
|
236
|
+
|
|
237
|
+
// Skip edge from conditional - branches will connect to next step
|
|
238
|
+
if (sourceNode.data.step.type === 'conditional') {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
edges.push({
|
|
243
|
+
id: `edge-${sourceNode.id}-${targetNode.id}`,
|
|
244
|
+
source: sourceNode.id,
|
|
245
|
+
target: targetNode.id,
|
|
246
|
+
sourceHandle: 'output',
|
|
247
|
+
type: 'smoothstep',
|
|
248
|
+
animated: false,
|
|
249
|
+
data: { stepOrder: i },
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Create edges for conditional branches
|
|
254
|
+
const conditionalNodes = nodes.filter(n => n.data.step.type === 'conditional' && !isBranchNode(n))
|
|
255
|
+
|
|
256
|
+
for (const conditionalNode of conditionalNodes) {
|
|
257
|
+
const conditionalId = conditionalNode.id
|
|
258
|
+
const conditionalIndex = topLevelNodes.findIndex(n => n.id === conditionalId)
|
|
259
|
+
const nextMainStep = conditionalIndex < topLevelNodes.length - 1
|
|
260
|
+
? topLevelNodes[conditionalIndex + 1]
|
|
261
|
+
: null
|
|
262
|
+
|
|
263
|
+
// Find true branch nodes (contain -true- after the conditional ID)
|
|
264
|
+
// Sort by X position since branches flow horizontally
|
|
265
|
+
const trueBranchNodes = nodes
|
|
266
|
+
.filter(n => n.id.startsWith(`${conditionalId}-true-`))
|
|
267
|
+
.sort((a, b) => a.position.x - b.position.x)
|
|
268
|
+
|
|
269
|
+
// Find false branch nodes (contain -false- after the conditional ID)
|
|
270
|
+
// Sort by X position since branches flow horizontally
|
|
271
|
+
const falseBranchNodes = nodes
|
|
272
|
+
.filter(n => n.id.startsWith(`${conditionalId}-false-`))
|
|
273
|
+
.sort((a, b) => a.position.x - b.position.x)
|
|
274
|
+
|
|
275
|
+
// Connect conditional to first true branch node (branch goes right)
|
|
276
|
+
if (trueBranchNodes.length > 0) {
|
|
277
|
+
edges.push({
|
|
278
|
+
id: `edge-${conditionalId}-to-true-branch`,
|
|
279
|
+
source: conditionalId,
|
|
280
|
+
target: trueBranchNodes[0].id,
|
|
281
|
+
sourceHandle: 'true',
|
|
282
|
+
type: 'smoothstep',
|
|
283
|
+
animated: false,
|
|
284
|
+
label: 'True',
|
|
285
|
+
labelStyle: { fill: '#22c55e', fontWeight: 600, fontSize: 11 },
|
|
286
|
+
labelBgStyle: { fill: '#f0fdf4', fillOpacity: 0.9 },
|
|
287
|
+
labelBgPadding: [4, 6] as [number, number],
|
|
288
|
+
labelBgBorderRadius: 4,
|
|
289
|
+
style: { stroke: '#22c55e', strokeWidth: 2 },
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// Connect true branch nodes sequentially (horizontal connections)
|
|
293
|
+
for (let i = 0; i < trueBranchNodes.length - 1; i++) {
|
|
294
|
+
edges.push({
|
|
295
|
+
id: `edge-true-${trueBranchNodes[i].id}-${trueBranchNodes[i + 1].id}`,
|
|
296
|
+
source: trueBranchNodes[i].id,
|
|
297
|
+
target: trueBranchNodes[i + 1].id,
|
|
298
|
+
sourceHandle: 'output',
|
|
299
|
+
type: 'smoothstep',
|
|
300
|
+
animated: false,
|
|
301
|
+
style: { stroke: '#22c55e', strokeWidth: 1.5 },
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Connect last true branch node to next main step (convergence)
|
|
306
|
+
if (nextMainStep) {
|
|
307
|
+
const lastTrueNode = trueBranchNodes[trueBranchNodes.length - 1]
|
|
308
|
+
edges.push({
|
|
309
|
+
id: `edge-true-${lastTrueNode.id}-to-${nextMainStep.id}`,
|
|
310
|
+
source: lastTrueNode.id,
|
|
311
|
+
target: nextMainStep.id,
|
|
312
|
+
sourceHandle: 'output',
|
|
313
|
+
type: 'smoothstep',
|
|
314
|
+
animated: false,
|
|
315
|
+
style: { stroke: '#22c55e', strokeWidth: 1.5 },
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
} else if (nextMainStep) {
|
|
319
|
+
// No true branch steps - connect conditional directly to next step via true handle
|
|
320
|
+
edges.push({
|
|
321
|
+
id: `edge-${conditionalId}-true-to-${nextMainStep.id}`,
|
|
322
|
+
source: conditionalId,
|
|
323
|
+
target: nextMainStep.id,
|
|
324
|
+
sourceHandle: 'true',
|
|
325
|
+
type: 'smoothstep',
|
|
326
|
+
animated: false,
|
|
327
|
+
label: 'True',
|
|
328
|
+
labelStyle: { fill: '#22c55e', fontWeight: 600, fontSize: 11 },
|
|
329
|
+
labelBgStyle: { fill: '#f0fdf4', fillOpacity: 0.9 },
|
|
330
|
+
labelBgPadding: [4, 6] as [number, number],
|
|
331
|
+
labelBgBorderRadius: 4,
|
|
332
|
+
style: { stroke: '#22c55e', strokeWidth: 2 },
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Connect conditional to first false branch node (branch goes right, below true)
|
|
337
|
+
if (falseBranchNodes.length > 0) {
|
|
338
|
+
edges.push({
|
|
339
|
+
id: `edge-${conditionalId}-to-false-branch`,
|
|
340
|
+
source: conditionalId,
|
|
341
|
+
target: falseBranchNodes[0].id,
|
|
342
|
+
sourceHandle: 'false',
|
|
343
|
+
type: 'smoothstep',
|
|
344
|
+
animated: false,
|
|
345
|
+
label: 'False',
|
|
346
|
+
labelStyle: { fill: '#ef4444', fontWeight: 600, fontSize: 11 },
|
|
347
|
+
labelBgStyle: { fill: '#fef2f2', fillOpacity: 0.9 },
|
|
348
|
+
labelBgPadding: [4, 6] as [number, number],
|
|
349
|
+
labelBgBorderRadius: 4,
|
|
350
|
+
style: { stroke: '#ef4444', strokeWidth: 2 },
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// Connect false branch nodes sequentially (horizontal connections)
|
|
354
|
+
for (let i = 0; i < falseBranchNodes.length - 1; i++) {
|
|
355
|
+
edges.push({
|
|
356
|
+
id: `edge-false-${falseBranchNodes[i].id}-${falseBranchNodes[i + 1].id}`,
|
|
357
|
+
source: falseBranchNodes[i].id,
|
|
358
|
+
target: falseBranchNodes[i + 1].id,
|
|
359
|
+
sourceHandle: 'output',
|
|
360
|
+
type: 'smoothstep',
|
|
361
|
+
animated: false,
|
|
362
|
+
style: { stroke: '#ef4444', strokeWidth: 1.5 },
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Connect last false branch node to next main step (convergence)
|
|
367
|
+
if (nextMainStep) {
|
|
368
|
+
const lastFalseNode = falseBranchNodes[falseBranchNodes.length - 1]
|
|
369
|
+
edges.push({
|
|
370
|
+
id: `edge-false-${lastFalseNode.id}-to-${nextMainStep.id}`,
|
|
371
|
+
source: lastFalseNode.id,
|
|
372
|
+
target: nextMainStep.id,
|
|
373
|
+
sourceHandle: 'output',
|
|
374
|
+
type: 'smoothstep',
|
|
375
|
+
animated: false,
|
|
376
|
+
style: { stroke: '#ef4444', strokeWidth: 1.5 },
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
} else if (nextMainStep) {
|
|
380
|
+
// No false branch steps - connect conditional directly to next step via false handle
|
|
381
|
+
edges.push({
|
|
382
|
+
id: `edge-${conditionalId}-false-to-${nextMainStep.id}`,
|
|
383
|
+
source: conditionalId,
|
|
384
|
+
target: nextMainStep.id,
|
|
385
|
+
sourceHandle: 'false',
|
|
386
|
+
type: 'smoothstep',
|
|
387
|
+
animated: false,
|
|
388
|
+
label: 'False',
|
|
389
|
+
labelStyle: { fill: '#ef4444', fontWeight: 600, fontSize: 11 },
|
|
390
|
+
labelBgStyle: { fill: '#fef2f2', fillOpacity: 0.9 },
|
|
391
|
+
labelBgPadding: [4, 6] as [number, number],
|
|
392
|
+
labelBgBorderRadius: 4,
|
|
393
|
+
style: { stroke: '#ef4444', strokeWidth: 2 },
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Handle case where conditional has no branches - connect directly to next step
|
|
398
|
+
if (trueBranchNodes.length === 0 && falseBranchNodes.length === 0 && nextMainStep) {
|
|
399
|
+
edges.push({
|
|
400
|
+
id: `edge-${conditionalId}-to-${nextMainStep.id}`,
|
|
401
|
+
source: conditionalId,
|
|
402
|
+
target: nextMainStep.id,
|
|
403
|
+
sourceHandle: 'output',
|
|
404
|
+
type: 'smoothstep',
|
|
405
|
+
animated: false,
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return edges
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ============================================================================
|
|
414
|
+
// Utility Functions
|
|
415
|
+
// ============================================================================
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get default name for a step type
|
|
419
|
+
*/
|
|
420
|
+
export function getDefaultStepName(type: FlowStepType): string {
|
|
421
|
+
const names: Record<FlowStepType, string> = {
|
|
422
|
+
'prompt': 'AI Prompt',
|
|
423
|
+
'fetch-url': 'Fetch URL',
|
|
424
|
+
'retrieve-record': 'Retrieve Record',
|
|
425
|
+
'fetch-github': 'Fetch GitHub',
|
|
426
|
+
'api-call': 'API Call',
|
|
427
|
+
'transform-data': 'Transform Data',
|
|
428
|
+
'conditional': 'Conditional',
|
|
429
|
+
'set-variable': 'Set Variable',
|
|
430
|
+
'upsert-record': 'Upsert Record',
|
|
431
|
+
'send-email': 'Send Email',
|
|
432
|
+
'send-text': 'Send Text',
|
|
433
|
+
'send-event': 'Send Event',
|
|
434
|
+
'send-stream': 'Send Stream',
|
|
435
|
+
'update-record': 'Update Record',
|
|
436
|
+
'search': 'Search',
|
|
437
|
+
'generate-embedding': 'Generate Embedding',
|
|
438
|
+
'vector-search': 'Vector Search',
|
|
439
|
+
'tool-call': 'Tool Call',
|
|
440
|
+
'wait-until': 'Wait Until',
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return names[type] || type
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Create a new flow step with default configuration
|
|
448
|
+
*/
|
|
449
|
+
export function createDefaultStep(type: FlowStepType, order: number = 0): FlowStep {
|
|
450
|
+
const id = `step-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
451
|
+
|
|
452
|
+
const baseStep = {
|
|
453
|
+
id,
|
|
454
|
+
type,
|
|
455
|
+
name: getDefaultStepName(type),
|
|
456
|
+
order,
|
|
457
|
+
enabled: true,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
switch (type) {
|
|
461
|
+
case 'prompt':
|
|
462
|
+
return {
|
|
463
|
+
...baseStep,
|
|
464
|
+
config: {
|
|
465
|
+
mode: 'instruction',
|
|
466
|
+
model: '',
|
|
467
|
+
userPrompt: '',
|
|
468
|
+
responseFormat: 'text',
|
|
469
|
+
outputVariable: `${type}_result`,
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
case 'fetch-url':
|
|
474
|
+
return {
|
|
475
|
+
...baseStep,
|
|
476
|
+
config: {
|
|
477
|
+
http: {
|
|
478
|
+
url: '',
|
|
479
|
+
method: 'GET',
|
|
480
|
+
},
|
|
481
|
+
responseType: 'json',
|
|
482
|
+
outputVariable: 'fetch_result',
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
case 'transform-data':
|
|
487
|
+
return {
|
|
488
|
+
...baseStep,
|
|
489
|
+
config: {
|
|
490
|
+
script: '// Transform your data here\nreturn { result: input }',
|
|
491
|
+
outputVariable: 'transform_result',
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
case 'conditional':
|
|
496
|
+
return {
|
|
497
|
+
...baseStep,
|
|
498
|
+
config: {
|
|
499
|
+
condition: 'true',
|
|
500
|
+
trueSteps: [],
|
|
501
|
+
falseSteps: [],
|
|
502
|
+
},
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
case 'send-email':
|
|
506
|
+
return {
|
|
507
|
+
...baseStep,
|
|
508
|
+
config: {
|
|
509
|
+
from: 'no-reply@messages.runtype.com',
|
|
510
|
+
to: '',
|
|
511
|
+
subject: '',
|
|
512
|
+
html: '',
|
|
513
|
+
outputVariable: 'email_result',
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
default:
|
|
518
|
+
return {
|
|
519
|
+
...baseStep,
|
|
520
|
+
config: {
|
|
521
|
+
outputVariable: `${type.replace(/-/g, '_')}_result`,
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Generate a unique step ID
|
|
529
|
+
*/
|
|
530
|
+
export function generateStepId(prefix: string = 'step'): string {
|
|
531
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Clone a step with a new ID
|
|
536
|
+
*/
|
|
537
|
+
export function cloneStep(step: FlowStep): FlowStep {
|
|
538
|
+
return {
|
|
539
|
+
...step,
|
|
540
|
+
id: generateStepId(step.type),
|
|
541
|
+
config: JSON.parse(JSON.stringify(step.config)),
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|