@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,414 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
useNodesState,
|
|
4
|
+
useEdgesState,
|
|
5
|
+
addEdge,
|
|
6
|
+
type OnNodesChange,
|
|
7
|
+
type OnEdgesChange,
|
|
8
|
+
type OnConnect,
|
|
9
|
+
type Connection,
|
|
10
|
+
} from '@xyflow/react'
|
|
11
|
+
import type { RuntypeClient } from '@runtypelabs/sdk'
|
|
12
|
+
import type {
|
|
13
|
+
FlowStep,
|
|
14
|
+
RuntypeNode,
|
|
15
|
+
RuntypeEdge,
|
|
16
|
+
SavedFlow,
|
|
17
|
+
UseRuntypeFlowReturn,
|
|
18
|
+
} from '../types'
|
|
19
|
+
import {
|
|
20
|
+
flowStepsToNodes,
|
|
21
|
+
nodesToFlowSteps,
|
|
22
|
+
createEdgesFromNodes,
|
|
23
|
+
createDefaultStep,
|
|
24
|
+
} from '../utils/adapter'
|
|
25
|
+
import { autoLayout } from '../utils/layout'
|
|
26
|
+
import type { FlowStepType } from '@travrse/shared'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Hook Options
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export interface UseRuntypeFlowOptions {
|
|
33
|
+
/** Runtype API client instance */
|
|
34
|
+
client: RuntypeClient
|
|
35
|
+
/** Initial flow ID to load */
|
|
36
|
+
flowId?: string
|
|
37
|
+
/** Initial flow name */
|
|
38
|
+
initialName?: string
|
|
39
|
+
/** Initial flow description */
|
|
40
|
+
initialDescription?: string
|
|
41
|
+
/** Initial steps */
|
|
42
|
+
initialSteps?: FlowStep[]
|
|
43
|
+
/** Callback when flow changes */
|
|
44
|
+
onChange?: (nodes: RuntypeNode[], edges: RuntypeEdge[]) => void
|
|
45
|
+
/** Auto-layout nodes on load */
|
|
46
|
+
autoLayoutOnLoad?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Main Hook
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
export function useRuntypeFlow(options: UseRuntypeFlowOptions): UseRuntypeFlowReturn {
|
|
54
|
+
const {
|
|
55
|
+
client,
|
|
56
|
+
flowId: initialFlowId,
|
|
57
|
+
initialName = '',
|
|
58
|
+
initialDescription = '',
|
|
59
|
+
initialSteps = [],
|
|
60
|
+
onChange,
|
|
61
|
+
autoLayoutOnLoad = true,
|
|
62
|
+
} = options
|
|
63
|
+
|
|
64
|
+
// State
|
|
65
|
+
const [flowId, setFlowId] = useState<string | null>(initialFlowId || null)
|
|
66
|
+
const [flowName, setFlowName] = useState(initialName)
|
|
67
|
+
const [flowDescription, setFlowDescription] = useState(initialDescription)
|
|
68
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
69
|
+
const [isSaving, setIsSaving] = useState(false)
|
|
70
|
+
const [error, setError] = useState<Error | null>(null)
|
|
71
|
+
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
|
72
|
+
|
|
73
|
+
// React Flow state
|
|
74
|
+
const [nodes, setNodes, onNodesChange] = useNodesState<RuntypeNode>([])
|
|
75
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState<RuntypeEdge>([])
|
|
76
|
+
|
|
77
|
+
// Track last saved state for change detection
|
|
78
|
+
const lastSavedState = useRef<string>('')
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Handlers
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
const handleStepChange = useCallback(
|
|
85
|
+
(stepId: string, updates: Partial<FlowStep>) => {
|
|
86
|
+
setNodes((nds) =>
|
|
87
|
+
nds.map((node) => {
|
|
88
|
+
if (node.id === stepId) {
|
|
89
|
+
return {
|
|
90
|
+
...node,
|
|
91
|
+
data: {
|
|
92
|
+
...node.data,
|
|
93
|
+
step: {
|
|
94
|
+
...node.data.step,
|
|
95
|
+
...updates,
|
|
96
|
+
config: updates.config
|
|
97
|
+
? { ...node.data.step.config, ...updates.config }
|
|
98
|
+
: node.data.step.config,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return node
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
setHasUnsavedChanges(true)
|
|
107
|
+
},
|
|
108
|
+
[setNodes]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const handleStepDelete = useCallback(
|
|
112
|
+
(stepId: string) => {
|
|
113
|
+
setNodes((nds) => nds.filter((node) => node.id !== stepId))
|
|
114
|
+
setEdges((eds) =>
|
|
115
|
+
eds.filter((edge) => edge.source !== stepId && edge.target !== stepId)
|
|
116
|
+
)
|
|
117
|
+
setHasUnsavedChanges(true)
|
|
118
|
+
},
|
|
119
|
+
[setNodes, setEdges]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const handleConnect: OnConnect = useCallback(
|
|
123
|
+
(connection: Connection) => {
|
|
124
|
+
setEdges((eds) => addEdge({ ...connection, type: 'smoothstep' }, eds))
|
|
125
|
+
setHasUnsavedChanges(true)
|
|
126
|
+
},
|
|
127
|
+
[setEdges]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// API Operations
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load a flow by ID
|
|
136
|
+
*/
|
|
137
|
+
const loadFlow = useCallback(
|
|
138
|
+
async (id: string) => {
|
|
139
|
+
setIsLoading(true)
|
|
140
|
+
setError(null)
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const flow = await client.flows.get(id)
|
|
144
|
+
|
|
145
|
+
// Get flow steps
|
|
146
|
+
const stepsResponse = await client.flowSteps.getByFlow(id)
|
|
147
|
+
const steps: FlowStep[] = Array.isArray(stepsResponse) ? stepsResponse : []
|
|
148
|
+
|
|
149
|
+
// Convert to React Flow nodes
|
|
150
|
+
let newNodes = flowStepsToNodes(steps, {
|
|
151
|
+
onChange: handleStepChange,
|
|
152
|
+
onDelete: handleStepDelete,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Auto-layout if enabled
|
|
156
|
+
if (autoLayoutOnLoad) {
|
|
157
|
+
const newEdges = createEdgesFromNodes(newNodes)
|
|
158
|
+
newNodes = autoLayout(newNodes, newEdges)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const newEdges = createEdgesFromNodes(newNodes)
|
|
162
|
+
|
|
163
|
+
setFlowId(id)
|
|
164
|
+
setFlowName(flow.name || '')
|
|
165
|
+
setFlowDescription(flow.description || '')
|
|
166
|
+
setNodes(newNodes)
|
|
167
|
+
setEdges(newEdges)
|
|
168
|
+
setHasUnsavedChanges(false)
|
|
169
|
+
|
|
170
|
+
// Store last saved state
|
|
171
|
+
lastSavedState.current = JSON.stringify(nodesToFlowSteps(newNodes))
|
|
172
|
+
} catch (err) {
|
|
173
|
+
setError(err instanceof Error ? err : new Error('Failed to load flow'))
|
|
174
|
+
throw err
|
|
175
|
+
} finally {
|
|
176
|
+
setIsLoading(false)
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[client, handleStepChange, handleStepDelete, autoLayoutOnLoad, setNodes, setEdges]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Save the current flow
|
|
184
|
+
*/
|
|
185
|
+
const saveFlow = useCallback(async (): Promise<SavedFlow> => {
|
|
186
|
+
if (!flowId) {
|
|
187
|
+
throw new Error('No flow ID. Use createFlow() to create a new flow first.')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setIsSaving(true)
|
|
191
|
+
setError(null)
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const steps = nodesToFlowSteps(nodes)
|
|
195
|
+
|
|
196
|
+
// Update flow metadata
|
|
197
|
+
await client.flows.update(flowId, {
|
|
198
|
+
name: flowName,
|
|
199
|
+
description: flowDescription,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Update steps - delete existing and create new
|
|
203
|
+
const existingSteps = await client.flowSteps.getByFlow(flowId)
|
|
204
|
+
for (const step of existingSteps) {
|
|
205
|
+
await client.flowSteps.delete(step.id)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create new steps
|
|
209
|
+
for (const step of steps) {
|
|
210
|
+
await client.flowSteps.create({
|
|
211
|
+
flowId,
|
|
212
|
+
type: step.type,
|
|
213
|
+
name: step.name,
|
|
214
|
+
order: step.order,
|
|
215
|
+
enabled: step.enabled,
|
|
216
|
+
config: step.config,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setHasUnsavedChanges(false)
|
|
221
|
+
lastSavedState.current = JSON.stringify(steps)
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
id: flowId,
|
|
225
|
+
name: flowName,
|
|
226
|
+
description: flowDescription,
|
|
227
|
+
steps,
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
setError(err instanceof Error ? err : new Error('Failed to save flow'))
|
|
231
|
+
throw err
|
|
232
|
+
} finally {
|
|
233
|
+
setIsSaving(false)
|
|
234
|
+
}
|
|
235
|
+
}, [flowId, flowName, flowDescription, nodes, client])
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a new flow
|
|
239
|
+
*/
|
|
240
|
+
const createFlow = useCallback(
|
|
241
|
+
async (name: string, description?: string): Promise<SavedFlow> => {
|
|
242
|
+
setIsSaving(true)
|
|
243
|
+
setError(null)
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const flow = await client.flows.create({
|
|
247
|
+
name,
|
|
248
|
+
description,
|
|
249
|
+
prompts: [],
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
setFlowId(flow.id)
|
|
253
|
+
setFlowName(name)
|
|
254
|
+
setFlowDescription(description || '')
|
|
255
|
+
setNodes([])
|
|
256
|
+
setEdges([])
|
|
257
|
+
setHasUnsavedChanges(false)
|
|
258
|
+
lastSavedState.current = '[]'
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
id: flow.id,
|
|
262
|
+
name,
|
|
263
|
+
description,
|
|
264
|
+
steps: [],
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
setError(err instanceof Error ? err : new Error('Failed to create flow'))
|
|
268
|
+
throw err
|
|
269
|
+
} finally {
|
|
270
|
+
setIsSaving(false)
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
[client, setNodes, setEdges]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Delete a step
|
|
278
|
+
*/
|
|
279
|
+
const deleteStep = useCallback(
|
|
280
|
+
(stepId: string) => {
|
|
281
|
+
handleStepDelete(stepId)
|
|
282
|
+
},
|
|
283
|
+
[handleStepDelete]
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Update a step's configuration
|
|
288
|
+
*/
|
|
289
|
+
const updateStep = useCallback(
|
|
290
|
+
(stepId: string, updates: Partial<FlowStep>) => {
|
|
291
|
+
handleStepChange(stepId, updates)
|
|
292
|
+
},
|
|
293
|
+
[handleStepChange]
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Add a new step
|
|
298
|
+
*/
|
|
299
|
+
const addStep = useCallback(
|
|
300
|
+
(type: FlowStepType, position?: { x: number; y: number }) => {
|
|
301
|
+
const newStep = createDefaultStep(type, nodes.length)
|
|
302
|
+
|
|
303
|
+
const newNode: RuntypeNode = {
|
|
304
|
+
id: newStep.id,
|
|
305
|
+
type: newStep.type,
|
|
306
|
+
position: position || { x: 400, y: nodes.length * 230 + 50 },
|
|
307
|
+
data: {
|
|
308
|
+
step: newStep,
|
|
309
|
+
label: newStep.name,
|
|
310
|
+
onChange: handleStepChange,
|
|
311
|
+
onDelete: handleStepDelete,
|
|
312
|
+
},
|
|
313
|
+
draggable: true,
|
|
314
|
+
selectable: true,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
setNodes((nds) => [...nds, newNode])
|
|
318
|
+
|
|
319
|
+
// Add edge from previous node if exists
|
|
320
|
+
if (nodes.length > 0) {
|
|
321
|
+
const lastNode = nodes[nodes.length - 1]
|
|
322
|
+
setEdges((eds) => [
|
|
323
|
+
...eds,
|
|
324
|
+
{
|
|
325
|
+
id: `edge-${lastNode.id}-${newNode.id}`,
|
|
326
|
+
source: lastNode.id,
|
|
327
|
+
target: newNode.id,
|
|
328
|
+
type: 'smoothstep',
|
|
329
|
+
},
|
|
330
|
+
])
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
setHasUnsavedChanges(true)
|
|
334
|
+
},
|
|
335
|
+
[nodes, setNodes, setEdges, handleStepChange, handleStepDelete]
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
// ============================================================================
|
|
339
|
+
// Effects
|
|
340
|
+
// ============================================================================
|
|
341
|
+
|
|
342
|
+
// Load initial flow if flowId is provided
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (initialFlowId) {
|
|
345
|
+
loadFlow(initialFlowId)
|
|
346
|
+
} else if (initialSteps.length > 0) {
|
|
347
|
+
// Initialize with provided steps
|
|
348
|
+
let newNodes = flowStepsToNodes(initialSteps, {
|
|
349
|
+
onChange: handleStepChange,
|
|
350
|
+
onDelete: handleStepDelete,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
if (autoLayoutOnLoad) {
|
|
354
|
+
const newEdges = createEdgesFromNodes(newNodes)
|
|
355
|
+
newNodes = autoLayout(newNodes, newEdges)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const newEdges = createEdgesFromNodes(newNodes)
|
|
359
|
+
|
|
360
|
+
setNodes(newNodes)
|
|
361
|
+
setEdges(newEdges)
|
|
362
|
+
}
|
|
363
|
+
}, []) // Only run on mount
|
|
364
|
+
|
|
365
|
+
// Notify onChange when nodes/edges change
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
onChange?.(nodes, edges)
|
|
368
|
+
}, [nodes, edges, onChange])
|
|
369
|
+
|
|
370
|
+
// Detect unsaved changes
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
const currentState = JSON.stringify(nodesToFlowSteps(nodes))
|
|
373
|
+
if (lastSavedState.current && currentState !== lastSavedState.current) {
|
|
374
|
+
setHasUnsavedChanges(true)
|
|
375
|
+
}
|
|
376
|
+
}, [nodes])
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// Return
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
// React Flow state
|
|
384
|
+
nodes,
|
|
385
|
+
edges,
|
|
386
|
+
onNodesChange: onNodesChange as unknown as (changes: unknown) => void,
|
|
387
|
+
onEdgesChange: onEdgesChange as unknown as (changes: unknown) => void,
|
|
388
|
+
onConnect: handleConnect as unknown as (connection: unknown) => void,
|
|
389
|
+
|
|
390
|
+
// Flow metadata
|
|
391
|
+
flowName,
|
|
392
|
+
flowDescription,
|
|
393
|
+
flowId,
|
|
394
|
+
setFlowName,
|
|
395
|
+
setFlowDescription,
|
|
396
|
+
|
|
397
|
+
// API operations
|
|
398
|
+
loadFlow,
|
|
399
|
+
saveFlow,
|
|
400
|
+
createFlow,
|
|
401
|
+
deleteStep,
|
|
402
|
+
updateStep,
|
|
403
|
+
addStep,
|
|
404
|
+
|
|
405
|
+
// Status
|
|
406
|
+
isLoading,
|
|
407
|
+
isSaving,
|
|
408
|
+
error,
|
|
409
|
+
hasUnsavedChanges,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export default useRuntypeFlow
|
|
414
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export * from './types'
|
|
3
|
+
|
|
4
|
+
// Components
|
|
5
|
+
export { RuntypeFlowEditor } from './components/RuntypeFlowEditor'
|
|
6
|
+
export { BaseNode } from './components/nodes/BaseNode'
|
|
7
|
+
export { PromptNode } from './components/nodes/PromptNode'
|
|
8
|
+
export { FetchUrlNode } from './components/nodes/FetchUrlNode'
|
|
9
|
+
export { CodeNode } from './components/nodes/CodeNode'
|
|
10
|
+
export { ConditionalNode } from './components/nodes/ConditionalNode'
|
|
11
|
+
export { SendEmailNode } from './components/nodes/SendEmailNode'
|
|
12
|
+
|
|
13
|
+
// Hooks
|
|
14
|
+
export { useRuntypeFlow } from './hooks/useRuntypeFlow'
|
|
15
|
+
export { useFlowValidation } from './hooks/useFlowValidation'
|
|
16
|
+
|
|
17
|
+
// Utilities
|
|
18
|
+
export {
|
|
19
|
+
flowStepsToNodes,
|
|
20
|
+
nodesToFlowSteps,
|
|
21
|
+
createEdgesFromNodes,
|
|
22
|
+
createDefaultStep,
|
|
23
|
+
getDefaultStepName,
|
|
24
|
+
generateStepId,
|
|
25
|
+
cloneStep,
|
|
26
|
+
} from './utils/adapter'
|
|
27
|
+
export { autoLayout, centerNodes, snapToGrid, getNodesBoundingBox } from './utils/layout'
|
|
28
|
+
|