@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.
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import '@xyflow/react/dist/style.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
11
+
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src"]
20
+ }
21
+
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 3100,
8
+ },
9
+ resolve: {
10
+ dedupe: ['react', 'react-dom'],
11
+ },
12
+ })
13
+
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@runtypelabs/react-flow",
3
+ "version": "0.1.0",
4
+ "description": "React Flow adapter for building visual flow editors with Runtype",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "sideEffects": [
16
+ "*.css"
17
+ ],
18
+ "dependencies": {
19
+ "@travrse/shared": "1.0.0",
20
+ "@runtypelabs/sdk": "0.1.1"
21
+ },
22
+ "peerDependencies": {
23
+ "@xyflow/react": "^12.0.0",
24
+ "react": "^18.0.0",
25
+ "react-dom": "^18.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "18.3.18",
29
+ "@types/react-dom": "18.3.5",
30
+ "@xyflow/react": "^12.0.0",
31
+ "react": "18.3.1",
32
+ "react-dom": "18.3.1",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.3.3"
35
+ },
36
+ "keywords": [
37
+ "runtype",
38
+ "react-flow",
39
+ "flow-editor",
40
+ "visual-programming",
41
+ "workflow",
42
+ "ai",
43
+ "automation"
44
+ ],
45
+ "author": "Runtype Labs",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/runtypelabs/core",
50
+ "directory": "packages/react-flow"
51
+ },
52
+ "homepage": "https://runtype.com",
53
+ "bugs": {
54
+ "url": "https://github.com/runtypelabs/core/issues"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "dev": "tsup --watch",
62
+ "clean": "rm -rf dist",
63
+ "typecheck": "tsc --noEmit --skipLibCheck"
64
+ }
65
+ }
@@ -0,0 +1,528 @@
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
+ import {
3
+ ReactFlow,
4
+ Controls,
5
+ Background,
6
+ MiniMap,
7
+ Panel,
8
+ type NodeTypes,
9
+ BackgroundVariant,
10
+ ConnectionLineType,
11
+ } from '@xyflow/react'
12
+
13
+ import type {
14
+ RuntypeFlowEditorProps,
15
+ RuntypeNode,
16
+ FlowStep,
17
+ SupportedNodeType,
18
+ } from '../types'
19
+ import { useRuntypeFlow } from '../hooks/useRuntypeFlow'
20
+ import { useFlowValidation } from '../hooks/useFlowValidation'
21
+ import { PromptNode } from './nodes/PromptNode'
22
+ import { FetchUrlNode } from './nodes/FetchUrlNode'
23
+ import { CodeNode } from './nodes/CodeNode'
24
+ import { ConditionalNode } from './nodes/ConditionalNode'
25
+ import { SendEmailNode } from './nodes/SendEmailNode'
26
+ import { BaseNode, BrainIcon, NODE_HEADER_COLORS } from './nodes/BaseNode'
27
+
28
+ // ============================================================================
29
+ // Styles
30
+ // ============================================================================
31
+
32
+ const styles = {
33
+ container: {
34
+ width: '100%',
35
+ height: '100%',
36
+ position: 'relative' as const,
37
+ },
38
+ toolbar: {
39
+ display: 'flex',
40
+ alignItems: 'center',
41
+ gap: '8px',
42
+ padding: '8px 12px',
43
+ backgroundColor: '#ffffff',
44
+ borderRadius: '8px',
45
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
46
+ border: '1px solid #e5e7eb',
47
+ },
48
+ toolbarButton: {
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ gap: '6px',
52
+ padding: '8px 12px',
53
+ fontSize: '12px',
54
+ fontWeight: 500,
55
+ backgroundColor: '#f9fafb',
56
+ border: '1px solid #e5e7eb',
57
+ borderRadius: '6px',
58
+ cursor: 'pointer',
59
+ color: '#374151',
60
+ transition: 'all 0.15s ease',
61
+ },
62
+ toolbarButtonPrimary: {
63
+ backgroundColor: '#6366f1',
64
+ borderColor: '#6366f1',
65
+ color: '#ffffff',
66
+ },
67
+ toolbarDivider: {
68
+ width: '1px',
69
+ height: '24px',
70
+ backgroundColor: '#e5e7eb',
71
+ margin: '0 4px',
72
+ },
73
+ statusPanel: {
74
+ display: 'flex',
75
+ alignItems: 'center',
76
+ gap: '8px',
77
+ padding: '8px 12px',
78
+ backgroundColor: '#ffffff',
79
+ borderRadius: '8px',
80
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
81
+ border: '1px solid #e5e7eb',
82
+ fontSize: '12px',
83
+ color: '#6b7280',
84
+ },
85
+ statusBadge: (isValid: boolean) => ({
86
+ display: 'inline-flex',
87
+ alignItems: 'center',
88
+ padding: '2px 8px',
89
+ borderRadius: '12px',
90
+ fontSize: '10px',
91
+ fontWeight: 600,
92
+ backgroundColor: isValid ? '#d1fae5' : '#fee2e2',
93
+ color: isValid ? '#059669' : '#dc2626',
94
+ }),
95
+ addStepMenu: {
96
+ position: 'absolute' as const,
97
+ top: '100%',
98
+ left: 0,
99
+ marginTop: '4px',
100
+ backgroundColor: '#ffffff',
101
+ borderRadius: '8px',
102
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
103
+ border: '1px solid #e5e7eb',
104
+ padding: '4px',
105
+ zIndex: 1000,
106
+ minWidth: '200px',
107
+ },
108
+ addStepMenuItem: {
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ gap: '8px',
112
+ padding: '8px 12px',
113
+ fontSize: '12px',
114
+ color: '#374151',
115
+ backgroundColor: 'transparent',
116
+ border: 'none',
117
+ borderRadius: '4px',
118
+ cursor: 'pointer',
119
+ width: '100%',
120
+ textAlign: 'left' as const,
121
+ transition: 'background-color 0.15s ease',
122
+ },
123
+ flowNameInput: {
124
+ padding: '6px 10px',
125
+ fontSize: '14px',
126
+ fontWeight: 600,
127
+ border: '1px solid transparent',
128
+ borderRadius: '4px',
129
+ backgroundColor: 'transparent',
130
+ color: '#1f2937',
131
+ outline: 'none',
132
+ transition: 'border-color 0.15s ease, background-color 0.15s ease',
133
+ minWidth: '200px',
134
+ },
135
+ unsavedBadge: {
136
+ display: 'inline-flex',
137
+ alignItems: 'center',
138
+ padding: '2px 6px',
139
+ borderRadius: '4px',
140
+ fontSize: '10px',
141
+ fontWeight: 500,
142
+ backgroundColor: '#fef3c7',
143
+ color: '#d97706',
144
+ },
145
+ }
146
+
147
+ // ============================================================================
148
+ // Node Types Configuration
149
+ // ============================================================================
150
+
151
+ const nodeTypes: NodeTypes = {
152
+ prompt: PromptNode,
153
+ 'fetch-url': FetchUrlNode,
154
+ 'transform-data': CodeNode,
155
+ conditional: ConditionalNode,
156
+ 'send-email': SendEmailNode,
157
+ // Default node for unsupported types
158
+ default: DefaultNode,
159
+ }
160
+
161
+ // Default node for unsupported step types
162
+ function DefaultNode(props: { data: { step: FlowStep; label: string }; selected?: boolean; id: string }) {
163
+ const { data, selected, id } = props
164
+
165
+ return (
166
+ <BaseNode
167
+ data={data as any}
168
+ selected={selected}
169
+ id={id}
170
+ typeLabel={data.step.type}
171
+ icon={<BrainIcon />}
172
+ headerColor={NODE_HEADER_COLORS.default}
173
+ >
174
+ <div style={{ fontSize: '12px', color: '#6b7280' }}>
175
+ This step type is not yet fully supported in the visual editor.
176
+ </div>
177
+ </BaseNode>
178
+ )
179
+ }
180
+
181
+ // ============================================================================
182
+ // Step Type Options
183
+ // ============================================================================
184
+
185
+ const STEP_TYPE_OPTIONS: Array<{
186
+ type: SupportedNodeType
187
+ label: string
188
+ icon: React.ReactNode
189
+ color: string
190
+ }> = [
191
+ {
192
+ type: 'prompt',
193
+ label: 'AI Prompt',
194
+ icon: <PromptIcon />,
195
+ color: NODE_HEADER_COLORS.prompt,
196
+ },
197
+ {
198
+ type: 'fetch-url',
199
+ label: 'Fetch URL',
200
+ icon: <GlobeIcon />,
201
+ color: NODE_HEADER_COLORS['fetch-url'],
202
+ },
203
+ {
204
+ type: 'transform-data',
205
+ label: 'Run Code',
206
+ icon: <CodeIcon />,
207
+ color: NODE_HEADER_COLORS['transform-data'],
208
+ },
209
+ {
210
+ type: 'conditional',
211
+ label: 'Conditional',
212
+ icon: <BranchIcon />,
213
+ color: NODE_HEADER_COLORS.conditional,
214
+ },
215
+ {
216
+ type: 'send-email',
217
+ label: 'Send Email',
218
+ icon: <MailIcon />,
219
+ color: NODE_HEADER_COLORS['send-email'],
220
+ },
221
+ ]
222
+
223
+ // ============================================================================
224
+ // Main Editor Component
225
+ // ============================================================================
226
+
227
+ export function RuntypeFlowEditor({
228
+ client,
229
+ flowId,
230
+ initialName = 'Untitled Flow',
231
+ initialDescription = '',
232
+ initialSteps = [],
233
+ onSave,
234
+ onChange,
235
+ onStepSelect,
236
+ showToolbar = true,
237
+ readOnly = false,
238
+ className,
239
+ }: RuntypeFlowEditorProps) {
240
+ const [showAddMenu, setShowAddMenu] = useState(false)
241
+ const [selectedNode, setSelectedNode] = useState<RuntypeNode | null>(null)
242
+
243
+ // Initialize flow hook
244
+ const {
245
+ nodes,
246
+ edges,
247
+ onNodesChange,
248
+ onEdgesChange,
249
+ onConnect,
250
+ flowName,
251
+ setFlowName,
252
+ saveFlow,
253
+ addStep,
254
+ isLoading,
255
+ isSaving,
256
+ error,
257
+ hasUnsavedChanges,
258
+ } = useRuntypeFlow({
259
+ client,
260
+ flowId,
261
+ initialName,
262
+ initialDescription,
263
+ initialSteps,
264
+ onChange,
265
+ autoLayoutOnLoad: true,
266
+ })
267
+
268
+ // Validation
269
+ const { result: validationResult } = useFlowValidation({ nodes })
270
+
271
+ // Handlers
272
+ const handleSave = useCallback(async () => {
273
+ try {
274
+ const savedFlow = await saveFlow()
275
+ onSave?.(savedFlow)
276
+ } catch (err) {
277
+ console.error('Failed to save flow:', err)
278
+ }
279
+ }, [saveFlow, onSave])
280
+
281
+ const handleAddStep = useCallback(
282
+ (type: SupportedNodeType) => {
283
+ addStep(type)
284
+ setShowAddMenu(false)
285
+ },
286
+ [addStep]
287
+ )
288
+
289
+ const handleNodeClick = useCallback(
290
+ (_: React.MouseEvent, node: RuntypeNode) => {
291
+ setSelectedNode(node)
292
+ onStepSelect?.(node.data.step)
293
+ },
294
+ [onStepSelect]
295
+ )
296
+
297
+ const handlePaneClick = useCallback(() => {
298
+ setSelectedNode(null)
299
+ onStepSelect?.(null)
300
+ }, [onStepSelect])
301
+
302
+ // Minimap node color
303
+ const minimapNodeColor = useCallback((node: RuntypeNode) => {
304
+ return NODE_HEADER_COLORS[node.data.step.type] || NODE_HEADER_COLORS.default
305
+ }, [])
306
+
307
+ return (
308
+ <div style={styles.container} className={className}>
309
+ <ReactFlow
310
+ nodes={nodes}
311
+ edges={edges}
312
+ onNodesChange={readOnly ? undefined : onNodesChange}
313
+ onEdgesChange={readOnly ? undefined : onEdgesChange}
314
+ onConnect={readOnly ? undefined : onConnect}
315
+ onNodeClick={handleNodeClick}
316
+ onPaneClick={handlePaneClick}
317
+ nodeTypes={nodeTypes}
318
+ fitView
319
+ fitViewOptions={{ padding: 0.2 }}
320
+ snapToGrid
321
+ snapGrid={[20, 20]}
322
+ connectionLineType={ConnectionLineType.SmoothStep}
323
+ defaultEdgeOptions={{
324
+ type: 'smoothstep',
325
+ animated: false,
326
+ }}
327
+ proOptions={{ hideAttribution: true }}
328
+ >
329
+ <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#d1d5db" />
330
+ <Controls showInteractive={!readOnly} />
331
+ <MiniMap
332
+ nodeColor={minimapNodeColor}
333
+ nodeStrokeWidth={3}
334
+ zoomable
335
+ pannable
336
+ style={{
337
+ backgroundColor: '#f9fafb',
338
+ border: '1px solid #e5e7eb',
339
+ borderRadius: '8px',
340
+ }}
341
+ />
342
+
343
+ {/* Top Toolbar */}
344
+ {showToolbar && (
345
+ <Panel position="top-left">
346
+ <div style={styles.toolbar}>
347
+ {/* Flow Name */}
348
+ <input
349
+ style={styles.flowNameInput}
350
+ value={flowName}
351
+ onChange={(e) => setFlowName(e.target.value)}
352
+ placeholder="Flow name..."
353
+ disabled={readOnly}
354
+ onFocus={(e) => {
355
+ e.target.style.borderColor = '#6366f1'
356
+ e.target.style.backgroundColor = '#ffffff'
357
+ }}
358
+ onBlur={(e) => {
359
+ e.target.style.borderColor = 'transparent'
360
+ e.target.style.backgroundColor = 'transparent'
361
+ }}
362
+ />
363
+
364
+ {hasUnsavedChanges && (
365
+ <span style={styles.unsavedBadge}>Unsaved</span>
366
+ )}
367
+
368
+ <div style={styles.toolbarDivider} />
369
+
370
+ {/* Add Step Button */}
371
+ {!readOnly && (
372
+ <div style={{ position: 'relative' }}>
373
+ <button
374
+ style={styles.toolbarButton}
375
+ onClick={() => setShowAddMenu(!showAddMenu)}
376
+ onMouseEnter={(e) => {
377
+ e.currentTarget.style.backgroundColor = '#f3f4f6'
378
+ e.currentTarget.style.borderColor = '#d1d5db'
379
+ }}
380
+ onMouseLeave={(e) => {
381
+ e.currentTarget.style.backgroundColor = '#f9fafb'
382
+ e.currentTarget.style.borderColor = '#e5e7eb'
383
+ }}
384
+ >
385
+ <PlusIcon /> Add Step
386
+ </button>
387
+
388
+ {showAddMenu && (
389
+ <div style={styles.addStepMenu}>
390
+ {STEP_TYPE_OPTIONS.map((option) => (
391
+ <button
392
+ key={option.type}
393
+ style={styles.addStepMenuItem}
394
+ onClick={() => handleAddStep(option.type)}
395
+ onMouseEnter={(e) => {
396
+ e.currentTarget.style.backgroundColor = option.color
397
+ }}
398
+ onMouseLeave={(e) => {
399
+ e.currentTarget.style.backgroundColor = 'transparent'
400
+ }}
401
+ >
402
+ {option.icon}
403
+ {option.label}
404
+ </button>
405
+ ))}
406
+ </div>
407
+ )}
408
+ </div>
409
+ )}
410
+
411
+ {/* Save Button */}
412
+ {!readOnly && flowId && (
413
+ <button
414
+ style={{ ...styles.toolbarButton, ...styles.toolbarButtonPrimary }}
415
+ onClick={handleSave}
416
+ disabled={isSaving || !hasUnsavedChanges}
417
+ onMouseEnter={(e) => {
418
+ if (!isSaving && hasUnsavedChanges) {
419
+ e.currentTarget.style.backgroundColor = '#4f46e5'
420
+ }
421
+ }}
422
+ onMouseLeave={(e) => {
423
+ e.currentTarget.style.backgroundColor = '#6366f1'
424
+ }}
425
+ >
426
+ {isSaving ? 'Saving...' : 'Save'}
427
+ </button>
428
+ )}
429
+ </div>
430
+ </Panel>
431
+ )}
432
+
433
+ {/* Status Panel */}
434
+ <Panel position="top-right">
435
+ <div style={styles.statusPanel}>
436
+ <span style={styles.statusBadge(validationResult.isValid)}>
437
+ {validationResult.isValid ? 'Valid' : `${validationResult.errors.length} Error(s)`}
438
+ </span>
439
+ <span>{nodes.length} steps</span>
440
+ {isLoading && <span>Loading...</span>}
441
+ {error && (
442
+ <span style={{ color: '#dc2626' }}>Error: {error.message}</span>
443
+ )}
444
+ </div>
445
+ </Panel>
446
+ </ReactFlow>
447
+
448
+ {/* Click outside to close menu */}
449
+ {showAddMenu && (
450
+ <div
451
+ style={{
452
+ position: 'fixed',
453
+ top: 0,
454
+ left: 0,
455
+ right: 0,
456
+ bottom: 0,
457
+ zIndex: 999,
458
+ }}
459
+ onClick={() => setShowAddMenu(false)}
460
+ />
461
+ )}
462
+ </div>
463
+ )
464
+ }
465
+
466
+ // ============================================================================
467
+ // Icon Components
468
+ // ============================================================================
469
+
470
+ function PlusIcon() {
471
+ return (
472
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
473
+ <line x1="12" y1="5" x2="12" y2="19" />
474
+ <line x1="5" y1="12" x2="19" y2="12" />
475
+ </svg>
476
+ )
477
+ }
478
+
479
+ function PromptIcon() {
480
+ return (
481
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
482
+ <path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
483
+ <path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
484
+ </svg>
485
+ )
486
+ }
487
+
488
+ function GlobeIcon() {
489
+ return (
490
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
491
+ <circle cx="12" cy="12" r="10" />
492
+ <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
493
+ <path d="M2 12h20" />
494
+ </svg>
495
+ )
496
+ }
497
+
498
+ function CodeIcon() {
499
+ return (
500
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
501
+ <polyline points="16 18 22 12 16 6" />
502
+ <polyline points="8 6 2 12 8 18" />
503
+ </svg>
504
+ )
505
+ }
506
+
507
+ function BranchIcon() {
508
+ return (
509
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
510
+ <line x1="6" y1="3" x2="6" y2="15" />
511
+ <circle cx="18" cy="6" r="3" />
512
+ <circle cx="6" cy="18" r="3" />
513
+ <path d="M18 9a9 9 0 0 1-9 9" />
514
+ </svg>
515
+ )
516
+ }
517
+
518
+ function MailIcon() {
519
+ return (
520
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
521
+ <rect width="20" height="16" x="2" y="4" rx="2" />
522
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
523
+ </svg>
524
+ )
525
+ }
526
+
527
+ export default RuntypeFlowEditor
528
+