@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,1744 @@
1
+ import React, { useState, useCallback, useMemo, useRef } from 'react'
2
+ import {
3
+ ReactFlow,
4
+ Controls,
5
+ Background,
6
+ MiniMap,
7
+ Panel,
8
+ useNodesState,
9
+ useEdgesState,
10
+ addEdge,
11
+ BackgroundVariant,
12
+ ConnectionLineType,
13
+ type OnConnect,
14
+ } from '@xyflow/react'
15
+
16
+ // Import node components from the package
17
+ import {
18
+ PromptNode,
19
+ FetchUrlNode,
20
+ CodeNode,
21
+ ConditionalNode,
22
+ SendEmailNode,
23
+ flowStepsToNodes,
24
+ createEdgesFromNodes,
25
+ createDefaultStep,
26
+ nodesToFlowSteps,
27
+ type FlowStep,
28
+ type RuntypeNode,
29
+ } from '@runtypelabs/react-flow'
30
+
31
+ // ============================================================================
32
+ // API Configuration
33
+ // ============================================================================
34
+
35
+ const API_BASE_URL = import.meta.env.VITE_RUNTYPE_API_URL || 'http://localhost:8787/v1'
36
+ const API_KEY = import.meta.env.VITE_RUNTYPE_API_KEY || ''
37
+
38
+ // ============================================================================
39
+ // Flow Browser Types
40
+ // ============================================================================
41
+
42
+ interface Flow {
43
+ id: string
44
+ name: string
45
+ status: string
46
+ created_at: string
47
+ updated_at: string
48
+ last_run_at: string | null
49
+ }
50
+
51
+ interface FlowWithSteps extends Flow {
52
+ steps: FlowStep[]
53
+ }
54
+
55
+ // ============================================================================
56
+ // Flow Execution Types
57
+ // ============================================================================
58
+
59
+ interface StepProgress {
60
+ id: string
61
+ name: string
62
+ index: number
63
+ status: 'pending' | 'running' | 'completed' | 'failed'
64
+ streamingText?: string
65
+ result?: any
66
+ error?: string
67
+ executionTime?: number
68
+ }
69
+
70
+ interface FlowProgress {
71
+ flowId: string
72
+ flowName: string
73
+ totalSteps: number
74
+ currentStepIndex: number
75
+ steps: StepProgress[]
76
+ isComplete: boolean
77
+ isError: boolean
78
+ executionTime?: number
79
+ }
80
+
81
+ // ============================================================================
82
+ // Sample Flow Steps
83
+ // ============================================================================
84
+
85
+ const sampleSteps: FlowStep[] = [
86
+ {
87
+ id: 'step-1',
88
+ type: 'fetch-url',
89
+ name: 'Fetch User Data',
90
+ order: 0,
91
+ enabled: true,
92
+ config: {
93
+ http: {
94
+ url: 'https://api.example.com/users/{{userId}}',
95
+ method: 'GET',
96
+ },
97
+ responseType: 'json',
98
+ outputVariable: 'user_data',
99
+ },
100
+ },
101
+ {
102
+ id: 'step-2',
103
+ type: 'prompt',
104
+ name: 'Analyze User',
105
+ order: 1,
106
+ enabled: true,
107
+ config: {
108
+ mode: 'instruction',
109
+ model: 'gpt-4',
110
+ userPrompt: 'Analyze the following user data and provide insights:\n\n{{user_data}}',
111
+ responseFormat: 'json',
112
+ outputVariable: 'analysis',
113
+ },
114
+ },
115
+ {
116
+ id: 'step-3',
117
+ type: 'conditional',
118
+ name: 'Check Premium Status',
119
+ order: 2,
120
+ enabled: true,
121
+ config: {
122
+ condition: "user_data.subscription === 'premium'",
123
+ trueSteps: [
124
+ {
125
+ id: 'premium-insights',
126
+ type: 'prompt',
127
+ name: 'Generate Premium Insights',
128
+ order: 0,
129
+ enabled: true,
130
+ config: {
131
+ mode: 'instruction',
132
+ model: 'gpt-4',
133
+ userPrompt: 'Generate detailed premium insights for {{user_data.name}} based on their usage patterns:\n\n{{analysis}}',
134
+ responseFormat: 'json',
135
+ outputVariable: 'premium_insights',
136
+ },
137
+ },
138
+ {
139
+ id: 'premium-email',
140
+ type: 'send-email',
141
+ name: 'Send Premium Report',
142
+ order: 1,
143
+ enabled: true,
144
+ config: {
145
+ from: 'premium@messages.runtype.com',
146
+ to: '{{user_data.email}}',
147
+ subject: '🌟 Your Premium Analytics Report',
148
+ html: '<h1>Premium Report for {{user_data.name}}</h1><p>{{premium_insights.summary}}</p><h2>Advanced Metrics</h2><p>{{premium_insights.metrics}}</p>',
149
+ outputVariable: 'premium_email_result',
150
+ },
151
+ },
152
+ ],
153
+ falseSteps: [
154
+ {
155
+ id: 'upgrade-pitch',
156
+ type: 'prompt',
157
+ name: 'Generate Upgrade Pitch',
158
+ order: 0,
159
+ enabled: true,
160
+ config: {
161
+ mode: 'instruction',
162
+ model: 'gpt-3.5-turbo',
163
+ userPrompt: 'Generate a friendly upgrade pitch for {{user_data.name}} highlighting premium benefits. Keep it short and compelling.',
164
+ responseFormat: 'text',
165
+ outputVariable: 'upgrade_pitch',
166
+ },
167
+ },
168
+ {
169
+ id: 'log-activity',
170
+ type: 'fetch-url',
171
+ name: 'Log Free User Activity',
172
+ order: 1,
173
+ enabled: true,
174
+ config: {
175
+ http: {
176
+ url: 'https://api.example.com/analytics/free-users',
177
+ method: 'POST',
178
+ body: JSON.stringify({
179
+ userId: '{{user_data.id}}',
180
+ action: 'viewed_report',
181
+ timestamp: '{{now}}',
182
+ }),
183
+ },
184
+ responseType: 'json',
185
+ outputVariable: 'analytics_result',
186
+ },
187
+ },
188
+ {
189
+ id: 'basic-email',
190
+ type: 'send-email',
191
+ name: 'Send Basic Report + Upsell',
192
+ order: 2,
193
+ enabled: true,
194
+ config: {
195
+ from: 'no-reply@messages.runtype.com',
196
+ to: '{{user_data.email}}',
197
+ subject: 'Your Report is Ready',
198
+ html: '<h1>Hi {{user_data.name}}</h1><p>{{analysis.summary}}</p><hr><p>{{upgrade_pitch}}</p><a href="https://runtype.com/upgrade">Upgrade to Premium →</a>',
199
+ outputVariable: 'basic_email_result',
200
+ },
201
+ },
202
+ ],
203
+ },
204
+ },
205
+ {
206
+ id: 'step-4',
207
+ type: 'send-email',
208
+ name: 'Send Report',
209
+ order: 3,
210
+ enabled: true,
211
+ config: {
212
+ from: 'no-reply@messages.runtype.com',
213
+ to: '{{user_data.email}}',
214
+ subject: 'Your Analysis Report',
215
+ html: '<h1>Analysis</h1><p>{{analysis.summary}}</p>',
216
+ outputVariable: 'email_result',
217
+ },
218
+ },
219
+ ]
220
+
221
+ // ============================================================================
222
+ // Node Types
223
+ // ============================================================================
224
+
225
+ const nodeTypes = {
226
+ prompt: PromptNode,
227
+ 'fetch-url': FetchUrlNode,
228
+ 'transform-data': CodeNode,
229
+ conditional: ConditionalNode,
230
+ 'send-email': SendEmailNode,
231
+ }
232
+
233
+ // ============================================================================
234
+ // Dispatch Config Panel Component
235
+ // ============================================================================
236
+
237
+ interface DispatchPanelProps {
238
+ isOpen: boolean
239
+ onClose: () => void
240
+ steps: FlowStep[]
241
+ flowName: string
242
+ }
243
+
244
+ function DispatchPanel({ isOpen, onClose, steps, flowName }: DispatchPanelProps) {
245
+ const [activeTab, setActiveTab] = useState<'dispatch' | 'steps'>('dispatch')
246
+
247
+ // Generate the dispatch config
248
+ const dispatchConfig = useMemo(() => {
249
+ return {
250
+ record: {
251
+ name: 'example-record',
252
+ type: 'user_analysis',
253
+ metadata: {
254
+ userId: '12345',
255
+ requestedAt: new Date().toISOString(),
256
+ },
257
+ },
258
+ flow: {
259
+ name: flowName || 'Untitled Flow',
260
+ steps: steps.map((step) => ({
261
+ type: step.type,
262
+ name: step.name,
263
+ order: step.order,
264
+ enabled: step.enabled,
265
+ config: step.config,
266
+ })),
267
+ },
268
+ options: {
269
+ streamResponse: true,
270
+ recordMode: 'virtual',
271
+ flowMode: 'virtual',
272
+ storeResults: false,
273
+ },
274
+ }
275
+ }, [steps, flowName])
276
+
277
+ if (!isOpen) return null
278
+
279
+ return (
280
+ <div style={panelOverlayStyle}>
281
+ <div style={panelContainerStyle}>
282
+ {/* Header */}
283
+ <div style={panelHeaderStyle}>
284
+ <h2 style={panelTitleStyle}>Runtype Dispatch Config</h2>
285
+ <button style={closeButtonStyle} onClick={onClose}>
286
+ <CloseIcon />
287
+ </button>
288
+ </div>
289
+
290
+ {/* Tabs */}
291
+ <div style={tabsStyle}>
292
+ <button
293
+ style={activeTab === 'dispatch' ? activeTabStyle : tabStyle}
294
+ onClick={() => setActiveTab('dispatch')}
295
+ >
296
+ Dispatch Request
297
+ </button>
298
+ <button
299
+ style={activeTab === 'steps' ? activeTabStyle : tabStyle}
300
+ onClick={() => setActiveTab('steps')}
301
+ >
302
+ Flow Steps ({steps.length})
303
+ </button>
304
+ </div>
305
+
306
+ {/* Content */}
307
+ <div style={panelContentStyle}>
308
+ {activeTab === 'dispatch' ? (
309
+ <>
310
+ <p style={descriptionStyle}>
311
+ This is the configuration that would be sent to the Runtype API via{' '}
312
+ <code style={codeStyle}>POST /dispatch</code>
313
+ </p>
314
+ <div style={codeBlockStyle}>
315
+ <pre style={preStyle}>{JSON.stringify(dispatchConfig, null, 2)}</pre>
316
+ </div>
317
+ <div style={usageStyle}>
318
+ <h4 style={usageTitleStyle}>Usage with @runtypelabs/sdk</h4>
319
+ <div style={codeBlockStyle}>
320
+ <pre style={preStyle}>{`import { createClient } from '@runtypelabs/sdk'
321
+
322
+ const client = createClient({ apiKey: 'your-api-key' })
323
+
324
+ // Execute the flow
325
+ const response = await client.dispatch.execute(${JSON.stringify(dispatchConfig, null, 2).split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n')})
326
+
327
+ console.log(response)`}</pre>
328
+ </div>
329
+ </div>
330
+ </>
331
+ ) : (
332
+ <>
333
+ <p style={descriptionStyle}>
334
+ Individual step configurations in the flow:
335
+ </p>
336
+ {steps.map((step, index) => (
337
+ <div key={step.id} style={stepCardStyle}>
338
+ <div style={stepHeaderStyle}>
339
+ <span style={stepBadgeStyle(step.type)}>{step.type}</span>
340
+ <span style={stepNameStyle}>{step.name}</span>
341
+ <span style={stepOrderStyle}>#{index + 1}</span>
342
+ </div>
343
+ <div style={codeBlockStyle}>
344
+ <pre style={{ ...preStyle, fontSize: '11px' }}>
345
+ {JSON.stringify(step.config, null, 2)}
346
+ </pre>
347
+ </div>
348
+ </div>
349
+ ))}
350
+ </>
351
+ )}
352
+ </div>
353
+
354
+ {/* Footer */}
355
+ <div style={panelFooterStyle}>
356
+ <button style={copyButtonStyle} onClick={() => {
357
+ navigator.clipboard.writeText(JSON.stringify(dispatchConfig, null, 2))
358
+ }}>
359
+ <CopyIcon /> Copy Config
360
+ </button>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ )
365
+ }
366
+
367
+ // ============================================================================
368
+ // Execution Panel Component
369
+ // ============================================================================
370
+
371
+ interface ExecutionPanelProps {
372
+ isOpen: boolean
373
+ onClose: () => void
374
+ isRunning: boolean
375
+ flowProgress: FlowProgress | null
376
+ onRunFlow: () => void
377
+ }
378
+
379
+ function ExecutionPanel({ isOpen, onClose, isRunning, flowProgress, onRunFlow }: ExecutionPanelProps) {
380
+ if (!isOpen) return null
381
+
382
+ const getStatusIcon = (status: string) => {
383
+ switch (status) {
384
+ case 'running':
385
+ return <SpinnerIcon />
386
+ case 'completed':
387
+ return <CheckIcon />
388
+ case 'failed':
389
+ return <ErrorIcon />
390
+ default:
391
+ return <PendingIcon />
392
+ }
393
+ }
394
+
395
+ const getStatusColor = (status: string) => {
396
+ switch (status) {
397
+ case 'running':
398
+ return '#3b82f6'
399
+ case 'completed':
400
+ return '#22c55e'
401
+ case 'failed':
402
+ return '#ef4444'
403
+ default:
404
+ return '#6b7280'
405
+ }
406
+ }
407
+
408
+ return (
409
+ <div style={panelOverlayStyle}>
410
+ <div style={panelContainerStyle}>
411
+ {/* Header */}
412
+ <div style={panelHeaderStyle}>
413
+ <h2 style={panelTitleStyle}>Flow Execution</h2>
414
+ <button style={closeButtonStyle} onClick={onClose}>
415
+ <CloseIcon />
416
+ </button>
417
+ </div>
418
+
419
+ {/* Content */}
420
+ <div style={panelContentStyle}>
421
+ {!flowProgress && !isRunning && (
422
+ <div style={{ textAlign: 'center', padding: '40px 20px' }}>
423
+ <div style={{ fontSize: '48px', marginBottom: '16px' }}>🚀</div>
424
+ <h3 style={{ fontSize: '18px', fontWeight: 600, color: '#cdd6f4', marginBottom: '8px' }}>
425
+ Ready to Execute
426
+ </h3>
427
+ <p style={{ fontSize: '13px', color: '#a6adc8', marginBottom: '24px' }}>
428
+ Click the button below to run this flow using the Runtype API
429
+ </p>
430
+ <button
431
+ style={runButtonStyle}
432
+ onClick={onRunFlow}
433
+ disabled={isRunning}
434
+ >
435
+ <PlayIcon /> Run Flow
436
+ </button>
437
+ </div>
438
+ )}
439
+
440
+ {(flowProgress || isRunning) && (
441
+ <>
442
+ {/* Progress Header */}
443
+ <div style={progressHeaderStyle}>
444
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
445
+ {isRunning ? <SpinnerIcon /> : flowProgress?.isError ? <ErrorIcon /> : <CheckIcon />}
446
+ <div>
447
+ <div style={{ fontSize: '14px', fontWeight: 600, color: '#cdd6f4' }}>
448
+ {flowProgress?.flowName || 'Executing Flow...'}
449
+ </div>
450
+ <div style={{ fontSize: '12px', color: '#a6adc8' }}>
451
+ {isRunning
452
+ ? `Step ${(flowProgress?.currentStepIndex ?? 0) + 1} of ${flowProgress?.totalSteps || '?'}`
453
+ : flowProgress?.isError
454
+ ? 'Execution failed'
455
+ : `Completed in ${flowProgress?.executionTime || 0}ms`}
456
+ </div>
457
+ </div>
458
+ </div>
459
+ {!isRunning && (
460
+ <button
461
+ style={{ ...runButtonStyle, padding: '8px 16px', fontSize: '12px' }}
462
+ onClick={onRunFlow}
463
+ >
464
+ <PlayIcon /> Run Again
465
+ </button>
466
+ )}
467
+ </div>
468
+
469
+ {/* Steps List */}
470
+ <div style={{ marginTop: '16px' }}>
471
+ {flowProgress?.steps.map((step, index) => (
472
+ <div key={step.id} style={executionStepStyle}>
473
+ <div style={executionStepHeaderStyle}>
474
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
475
+ <span style={{ color: getStatusColor(step.status) }}>
476
+ {getStatusIcon(step.status)}
477
+ </span>
478
+ <span style={{ fontSize: '13px', fontWeight: 500, color: '#cdd6f4' }}>
479
+ {step.name}
480
+ </span>
481
+ </div>
482
+ {step.executionTime && (
483
+ <span style={{ fontSize: '11px', color: '#6c7086' }}>
484
+ {step.executionTime}ms
485
+ </span>
486
+ )}
487
+ </div>
488
+
489
+ {/* Streaming Text Output */}
490
+ {step.streamingText && (
491
+ <div style={streamingOutputStyle}>
492
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
493
+ {step.streamingText}
494
+ </pre>
495
+ </div>
496
+ )}
497
+
498
+ {/* Error */}
499
+ {step.error && (
500
+ <div style={errorOutputStyle}>
501
+ {step.error}
502
+ </div>
503
+ )}
504
+
505
+ {/* Result (collapsed by default) */}
506
+ {step.result && !step.streamingText && (
507
+ <details style={{ marginTop: '8px' }}>
508
+ <summary style={{ fontSize: '11px', color: '#a6adc8', cursor: 'pointer' }}>
509
+ View Result
510
+ </summary>
511
+ <div style={codeBlockStyle}>
512
+ <pre style={{ ...preStyle, fontSize: '10px' }}>
513
+ {typeof step.result === 'string'
514
+ ? step.result
515
+ : JSON.stringify(step.result, null, 2)}
516
+ </pre>
517
+ </div>
518
+ </details>
519
+ )}
520
+ </div>
521
+ ))}
522
+ </div>
523
+ </>
524
+ )}
525
+ </div>
526
+ </div>
527
+ </div>
528
+ )
529
+ }
530
+
531
+ // ============================================================================
532
+ // Flow Browser Panel Component
533
+ // ============================================================================
534
+
535
+ interface FlowBrowserPanelProps {
536
+ isOpen: boolean
537
+ onClose: () => void
538
+ onLoadFlow: (flow: FlowWithSteps) => void
539
+ }
540
+
541
+ function FlowBrowserPanel({ isOpen, onClose, onLoadFlow }: FlowBrowserPanelProps) {
542
+ const [flows, setFlows] = useState<Flow[]>([])
543
+ const [isLoading, setIsLoading] = useState(false)
544
+ const [error, setError] = useState<string | null>(null)
545
+ const [loadingFlowId, setLoadingFlowId] = useState<string | null>(null)
546
+
547
+ // Fetch flows when panel opens
548
+ React.useEffect(() => {
549
+ if (isOpen) {
550
+ fetchFlows()
551
+ }
552
+ }, [isOpen])
553
+
554
+ const fetchFlows = async () => {
555
+ setIsLoading(true)
556
+ setError(null)
557
+ try {
558
+ const response = await fetch(`${API_BASE_URL}/flows`, {
559
+ headers: {
560
+ 'Authorization': `Bearer ${API_KEY}`,
561
+ },
562
+ })
563
+ if (!response.ok) {
564
+ throw new Error(`Failed to fetch flows: ${response.status}`)
565
+ }
566
+ const data = await response.json()
567
+ // API returns { data: [...flows], pagination: {...} } format
568
+ setFlows(data.data || data.flows || data || [])
569
+ } catch (err) {
570
+ setError(err instanceof Error ? err.message : 'Failed to fetch flows')
571
+ } finally {
572
+ setIsLoading(false)
573
+ }
574
+ }
575
+
576
+ const loadFlow = async (flowId: string) => {
577
+ setLoadingFlowId(flowId)
578
+ setError(null)
579
+ try {
580
+ // Fetch flow details
581
+ const flowResponse = await fetch(`${API_BASE_URL}/flows/${flowId}`, {
582
+ headers: {
583
+ 'Authorization': `Bearer ${API_KEY}`,
584
+ },
585
+ })
586
+ if (!flowResponse.ok) {
587
+ throw new Error(`Failed to fetch flow: ${flowResponse.status}`)
588
+ }
589
+ const flowData = await flowResponse.json()
590
+
591
+ // Fetch flow steps
592
+ const stepsResponse = await fetch(`${API_BASE_URL}/flow-steps/flow/${flowId}`, {
593
+ headers: {
594
+ 'Authorization': `Bearer ${API_KEY}`,
595
+ },
596
+ })
597
+ if (!stepsResponse.ok) {
598
+ throw new Error(`Failed to fetch flow steps: ${stepsResponse.status}`)
599
+ }
600
+ const stepsData = await stepsResponse.json()
601
+
602
+ // Convert API response to FlowStep format
603
+ // API returns { data: [...steps], flow_id, flowName }
604
+ const stepsArray = stepsData.data || stepsData.steps || stepsData || []
605
+ const steps: FlowStep[] = stepsArray.map((step: any) => ({
606
+ id: step.id,
607
+ type: step.type,
608
+ name: step.name,
609
+ order: step.order,
610
+ enabled: step.enabled,
611
+ config: step.config || {},
612
+ }))
613
+
614
+ onLoadFlow({
615
+ ...flowData,
616
+ steps,
617
+ })
618
+ onClose()
619
+ } catch (err) {
620
+ setError(err instanceof Error ? err.message : 'Failed to load flow')
621
+ } finally {
622
+ setLoadingFlowId(null)
623
+ }
624
+ }
625
+
626
+ if (!isOpen) return null
627
+
628
+ return (
629
+ <div style={panelOverlayStyle}>
630
+ <div style={{ ...panelContainerStyle, width: '600px' }}>
631
+ {/* Header */}
632
+ <div style={panelHeaderStyle}>
633
+ <h2 style={panelTitleStyle}>Load Flow</h2>
634
+ <button style={closeButtonStyle} onClick={onClose}>
635
+ <CloseIcon />
636
+ </button>
637
+ </div>
638
+
639
+ {/* Content */}
640
+ <div style={panelContentStyle}>
641
+ {isLoading && (
642
+ <div style={{ textAlign: 'center', padding: '40px 20px' }}>
643
+ <SpinnerIcon />
644
+ <p style={{ marginTop: '12px', color: '#a6adc8' }}>Loading flows...</p>
645
+ </div>
646
+ )}
647
+
648
+ {error && (
649
+ <div style={{
650
+ padding: '16px',
651
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
652
+ borderRadius: '8px',
653
+ marginBottom: '16px',
654
+ border: '1px solid rgba(239, 68, 68, 0.3)'
655
+ }}>
656
+ <div style={{ color: '#ef4444', fontSize: '13px', fontWeight: 600, marginBottom: '4px' }}>
657
+ Error
658
+ </div>
659
+ <div style={{ color: '#fca5a5', fontSize: '12px' }}>
660
+ {error}
661
+ </div>
662
+ </div>
663
+ )}
664
+
665
+ {!isLoading && flows.length === 0 && !error && (
666
+ <div style={{ textAlign: 'center', padding: '40px 20px' }}>
667
+ <div style={{ fontSize: '48px', marginBottom: '16px' }}>📁</div>
668
+ <h3 style={{ fontSize: '16px', fontWeight: 600, color: '#cdd6f4', marginBottom: '8px' }}>
669
+ No Flows Found
670
+ </h3>
671
+ <p style={{ fontSize: '13px', color: '#a6adc8' }}>
672
+ Create flows in the Runtype dashboard to see them here.
673
+ </p>
674
+ </div>
675
+ )}
676
+
677
+ {!isLoading && flows.length > 0 && (
678
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
679
+ {flows.map((flow) => (
680
+ <div
681
+ key={flow.id}
682
+ style={{
683
+ padding: '16px',
684
+ backgroundColor: '#181825',
685
+ borderRadius: '8px',
686
+ border: '1px solid #313244',
687
+ cursor: 'pointer',
688
+ transition: 'all 0.15s ease',
689
+ }}
690
+ onClick={() => loadFlow(flow.id)}
691
+ onMouseEnter={(e) => {
692
+ e.currentTarget.style.borderColor = '#89b4fa'
693
+ e.currentTarget.style.backgroundColor = '#1e1e2e'
694
+ }}
695
+ onMouseLeave={(e) => {
696
+ e.currentTarget.style.borderColor = '#313244'
697
+ e.currentTarget.style.backgroundColor = '#181825'
698
+ }}
699
+ >
700
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
701
+ <div style={{ flex: 1 }}>
702
+ <div style={{ fontSize: '14px', fontWeight: 600, color: '#cdd6f4', marginBottom: '4px' }}>
703
+ {flow.name}
704
+ </div>
705
+ <div style={{ fontSize: '11px', color: '#6c7086' }}>
706
+ {flow.status} • Updated: {new Date(flow.updated_at).toLocaleDateString()}
707
+ {flow.last_run_at && ` • Last run: ${new Date(flow.last_run_at).toLocaleDateString()}`}
708
+ </div>
709
+ </div>
710
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
711
+ {loadingFlowId === flow.id ? (
712
+ <SpinnerIcon />
713
+ ) : (
714
+ <div style={{
715
+ padding: '6px 12px',
716
+ backgroundColor: '#89b4fa',
717
+ color: '#1e1e2e',
718
+ borderRadius: '6px',
719
+ fontSize: '12px',
720
+ fontWeight: 600,
721
+ }}>
722
+ Load
723
+ </div>
724
+ )}
725
+ </div>
726
+ </div>
727
+ </div>
728
+ ))}
729
+ </div>
730
+ )}
731
+ </div>
732
+
733
+ {/* Footer */}
734
+ <div style={panelFooterStyle}>
735
+ <button
736
+ style={{ ...buttonStyle, padding: '8px 16px' }}
737
+ onClick={fetchFlows}
738
+ disabled={isLoading}
739
+ >
740
+ <RefreshIcon /> Refresh
741
+ </button>
742
+ </div>
743
+ </div>
744
+ </div>
745
+ )
746
+ }
747
+
748
+ // ============================================================================
749
+ // App Component
750
+ // ============================================================================
751
+
752
+ export default function App() {
753
+ const [steps, setSteps] = useState<FlowStep[]>(sampleSteps)
754
+ const [showDispatchPanel, setShowDispatchPanel] = useState(false)
755
+ const [showExecutionPanel, setShowExecutionPanel] = useState(false)
756
+ const [showFlowBrowser, setShowFlowBrowser] = useState(false)
757
+ const [isRunning, setIsRunning] = useState(false)
758
+ const [flowProgress, setFlowProgress] = useState<FlowProgress | null>(null)
759
+ const [flowName, setFlowName] = useState('User Analysis Flow')
760
+ const [loadedFlowId, setLoadedFlowId] = useState<string | null>(null)
761
+
762
+ // Create stable references for handlers that will update nodes
763
+ const [nodes, setNodes, onNodesChange] = useNodesState<RuntypeNode>([])
764
+ const [edges, setEdges, onEdgesChange] = useEdgesState([])
765
+ const [isInitialized, setIsInitialized] = useState(false)
766
+
767
+ // Handler that updates both steps AND nodes
768
+ const handleStepChangeWithNodes = useCallback((stepId: string, updates: Partial<FlowStep>) => {
769
+ // console.log('[App] handleStepChangeWithNodes:', stepId, updates)
770
+ // Update steps state
771
+ setSteps((prev) =>
772
+ prev.map((step) => {
773
+ if (step.id === stepId) {
774
+ return {
775
+ ...step,
776
+ ...updates,
777
+ config: updates.config ? { ...step.config, ...updates.config } : step.config,
778
+ }
779
+ }
780
+ return step
781
+ })
782
+ )
783
+
784
+ // Update nodes state to reflect the change
785
+ setNodes((nds) =>
786
+ nds.map((node) => {
787
+ if (node.id === stepId) {
788
+ const updatedStep = {
789
+ ...node.data.step,
790
+ ...updates,
791
+ config: updates.config
792
+ ? { ...node.data.step.config, ...updates.config }
793
+ : node.data.step.config,
794
+ }
795
+ return {
796
+ ...node,
797
+ data: {
798
+ ...node.data,
799
+ step: updatedStep,
800
+ label: updatedStep.name,
801
+ },
802
+ }
803
+ }
804
+ return node
805
+ })
806
+ )
807
+ }, [setNodes])
808
+
809
+ // Handler that updates both steps AND nodes for deletion
810
+ const handleStepDeleteWithNodes = useCallback((stepId: string) => {
811
+ setSteps((prev) => prev.filter((step) => step.id !== stepId))
812
+ setNodes((nds) => nds.filter((node) => node.id !== stepId))
813
+ setEdges((eds) => eds.filter((edge) => edge.source !== stepId && edge.target !== stepId))
814
+ }, [setNodes, setEdges])
815
+
816
+ // Initialize nodes on first render
817
+ React.useEffect(() => {
818
+ if (!isInitialized) {
819
+ const initialNodes = flowStepsToNodes(steps, {
820
+ onChange: handleStepChangeWithNodes,
821
+ onDelete: handleStepDeleteWithNodes,
822
+ startPosition: { x: 400, y: 50 },
823
+ })
824
+ const initialEdges = createEdgesFromNodes(initialNodes)
825
+ setNodes(initialNodes)
826
+ setEdges(initialEdges)
827
+ setIsInitialized(true)
828
+ }
829
+ }, [isInitialized, steps, handleStepChangeWithNodes, handleStepDeleteWithNodes, setNodes, setEdges])
830
+
831
+ // Derive current steps from nodes
832
+ const currentSteps = useMemo(() => {
833
+ return nodesToFlowSteps(nodes)
834
+ }, [nodes])
835
+
836
+ // Load flow from API
837
+ const handleLoadFlow = useCallback((flow: FlowWithSteps) => {
838
+ // Update steps state
839
+ setSteps(flow.steps)
840
+ setFlowName(flow.name)
841
+ setLoadedFlowId(flow.id)
842
+
843
+ // Reset initialization so nodes get rebuilt
844
+ setIsInitialized(false)
845
+ }, [])
846
+
847
+ const onConnect: OnConnect = useCallback(
848
+ (connection) => setEdges((eds) => addEdge({ ...connection, type: 'smoothstep' }, eds)),
849
+ [setEdges]
850
+ )
851
+
852
+ // Add a new step
853
+ const addStep = useCallback((type: FlowStep['type']) => {
854
+ const newStep = createDefaultStep(type, nodes.length)
855
+ setSteps((prev) => [...prev, newStep])
856
+
857
+ const newNode: RuntypeNode = {
858
+ id: newStep.id,
859
+ type: newStep.type,
860
+ position: { x: 400, y: nodes.length * 230 + 50 },
861
+ data: {
862
+ step: newStep,
863
+ label: newStep.name,
864
+ onChange: handleStepChangeWithNodes,
865
+ onDelete: handleStepDeleteWithNodes,
866
+ },
867
+ }
868
+
869
+ setNodes((nds) => [...nds, newNode])
870
+
871
+ // Add edge from last node
872
+ if (nodes.length > 0) {
873
+ const lastNode = nodes[nodes.length - 1]
874
+ setEdges((eds) => [
875
+ ...eds,
876
+ {
877
+ id: `edge-${lastNode.id}-${newNode.id}`,
878
+ source: lastNode.id,
879
+ target: newNode.id,
880
+ type: 'smoothstep',
881
+ },
882
+ ])
883
+ }
884
+ }, [nodes, setNodes, setEdges, handleStepChangeWithNodes, handleStepDeleteWithNodes])
885
+
886
+ // Execute flow via Runtype API
887
+ const executeFlow = useCallback(async () => {
888
+ setIsRunning(true)
889
+ setFlowProgress({
890
+ flowId: '',
891
+ flowName: flowName,
892
+ totalSteps: currentSteps.length,
893
+ currentStepIndex: 0,
894
+ steps: [],
895
+ isComplete: false,
896
+ isError: false,
897
+ })
898
+
899
+ try {
900
+ // Build dispatch request
901
+ const dispatchRequest = {
902
+ record: {
903
+ name: 'example-record',
904
+ type: 'user_analysis',
905
+ metadata: {
906
+ userId: '12345',
907
+ requestedAt: new Date().toISOString(),
908
+ },
909
+ },
910
+ flow: {
911
+ name: flowName,
912
+ steps: currentSteps.map((step) => ({
913
+ id: step.id,
914
+ type: step.type,
915
+ name: step.name,
916
+ order: step.order,
917
+ enabled: step.enabled,
918
+ config: step.config,
919
+ })),
920
+ },
921
+ options: {
922
+ stream_response: true,
923
+ record_mode: 'virtual',
924
+ flow_mode: 'virtual',
925
+ store_results: false,
926
+ },
927
+ }
928
+
929
+ const response = await fetch(`${API_BASE_URL}/dispatch`, {
930
+ method: 'POST',
931
+ headers: {
932
+ 'Content-Type': 'application/json',
933
+ 'Authorization': `Bearer ${API_KEY}`,
934
+ },
935
+ body: JSON.stringify(dispatchRequest),
936
+ })
937
+
938
+ if (!response.ok) {
939
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`)
940
+ }
941
+
942
+ const reader = response.body?.getReader()
943
+ const decoder = new TextDecoder()
944
+
945
+ if (!reader) {
946
+ throw new Error('Failed to get response reader')
947
+ }
948
+
949
+ let buffer = ''
950
+
951
+ while (true) {
952
+ const { done, value } = await reader.read()
953
+ if (done) break
954
+
955
+ const chunk = decoder.decode(value, { stream: true })
956
+ buffer += chunk
957
+
958
+ // Parse SSE events
959
+ const lines = buffer.split('\n')
960
+ buffer = lines.pop() || '' // Keep incomplete line in buffer
961
+
962
+ for (const line of lines) {
963
+ if (line.startsWith('data: ')) {
964
+ const eventData = line.slice(6)
965
+ if (eventData === '[DONE]') continue
966
+
967
+ try {
968
+ const data = JSON.parse(eventData)
969
+ handleSSEEvent(data)
970
+ } catch (parseError) {
971
+ console.error('Error parsing SSE data:', parseError, 'Event:', eventData)
972
+ }
973
+ }
974
+ }
975
+ }
976
+
977
+ // Process remaining buffer
978
+ if (buffer.startsWith('data: ')) {
979
+ const eventData = buffer.slice(6)
980
+ if (eventData && eventData !== '[DONE]') {
981
+ try {
982
+ const data = JSON.parse(eventData)
983
+ handleSSEEvent(data)
984
+ } catch (parseError) {
985
+ console.error('Error parsing final SSE data:', parseError)
986
+ }
987
+ }
988
+ }
989
+ } catch (error) {
990
+ console.error('Flow execution error:', error)
991
+ setFlowProgress((prev) => {
992
+ if (!prev) return null
993
+ return {
994
+ ...prev,
995
+ isComplete: true,
996
+ isError: true,
997
+ }
998
+ })
999
+ } finally {
1000
+ setIsRunning(false)
1001
+ }
1002
+ }, [currentSteps, flowName])
1003
+
1004
+ // Handle SSE events from the streaming response
1005
+ const handleSSEEvent = useCallback((data: any) => {
1006
+ switch (data.type) {
1007
+ case 'flow_start':
1008
+ setFlowProgress((prev) => ({
1009
+ ...prev!,
1010
+ flowId: data.flowId || '',
1011
+ totalSteps: data.totalSteps || prev?.totalSteps || 0,
1012
+ }))
1013
+ break
1014
+
1015
+ case 'step_start':
1016
+ setFlowProgress((prev) => {
1017
+ if (!prev) return null
1018
+ const stepIndex = data.index !== undefined ? data.index : prev.steps.length
1019
+ const existingIndex = prev.steps.findIndex(s => s.id === data.id)
1020
+
1021
+ if (existingIndex >= 0) {
1022
+ const newSteps = [...prev.steps]
1023
+ newSteps[existingIndex] = {
1024
+ ...newSteps[existingIndex],
1025
+ index: stepIndex,
1026
+ status: 'running',
1027
+ }
1028
+ return {
1029
+ ...prev,
1030
+ steps: newSteps.sort((a, b) => a.index - b.index),
1031
+ currentStepIndex: Math.max(prev.currentStepIndex, stepIndex),
1032
+ }
1033
+ }
1034
+
1035
+ return {
1036
+ ...prev,
1037
+ steps: [
1038
+ ...prev.steps,
1039
+ {
1040
+ id: data.id,
1041
+ name: data.name || `Step ${stepIndex + 1}`,
1042
+ index: stepIndex,
1043
+ status: 'running',
1044
+ },
1045
+ ].sort((a, b) => a.index - b.index),
1046
+ currentStepIndex: Math.max(prev.currentStepIndex, stepIndex),
1047
+ }
1048
+ })
1049
+ break
1050
+
1051
+ case 'step_chunk':
1052
+ setFlowProgress((prev) => {
1053
+ if (!prev) return null
1054
+ const stepIndex = prev.steps.findIndex(s => s.id === data.id)
1055
+
1056
+ if (stepIndex < 0) {
1057
+ const chunkStepIndex = data.index !== undefined ? data.index : prev.steps.length
1058
+ return {
1059
+ ...prev,
1060
+ steps: [
1061
+ ...prev.steps,
1062
+ {
1063
+ id: data.id,
1064
+ name: data.name || '',
1065
+ index: chunkStepIndex,
1066
+ status: 'running',
1067
+ streamingText: data.text || data.content || '',
1068
+ },
1069
+ ].sort((a, b) => a.index - b.index),
1070
+ }
1071
+ }
1072
+
1073
+ const newSteps = [...prev.steps]
1074
+ newSteps[stepIndex] = {
1075
+ ...newSteps[stepIndex],
1076
+ streamingText: (newSteps[stepIndex].streamingText || '') + (data.text || data.content || ''),
1077
+ }
1078
+ return { ...prev, steps: newSteps }
1079
+ })
1080
+ break
1081
+
1082
+ case 'step_complete':
1083
+ setFlowProgress((prev) => {
1084
+ if (!prev) return null
1085
+ const stepIndex = prev.steps.findIndex(s => s.id === data.id)
1086
+ if (stepIndex >= 0) {
1087
+ const newSteps = [...prev.steps]
1088
+ const step = newSteps[stepIndex]
1089
+
1090
+ let finalText = step.streamingText
1091
+ if (!finalText && data.output?.message) {
1092
+ finalText = data.output.message
1093
+ }
1094
+
1095
+ newSteps[stepIndex] = {
1096
+ ...step,
1097
+ status: 'completed',
1098
+ result: data.result || data.output,
1099
+ streamingText: finalText,
1100
+ executionTime: data.executionTime,
1101
+ }
1102
+
1103
+ return {
1104
+ ...prev,
1105
+ steps: newSteps.sort((a, b) => a.index - b.index),
1106
+ }
1107
+ }
1108
+ return prev
1109
+ })
1110
+ break
1111
+
1112
+ case 'step_error':
1113
+ setFlowProgress((prev) => {
1114
+ if (!prev) return null
1115
+ const stepIndex = prev.steps.findIndex(s => s.id === data.id)
1116
+ if (stepIndex >= 0) {
1117
+ const newSteps = [...prev.steps]
1118
+ newSteps[stepIndex] = {
1119
+ ...newSteps[stepIndex],
1120
+ status: 'failed',
1121
+ error: data.error,
1122
+ executionTime: data.executionTime,
1123
+ }
1124
+ return {
1125
+ ...prev,
1126
+ steps: newSteps.sort((a, b) => a.index - b.index),
1127
+ isError: true,
1128
+ }
1129
+ }
1130
+ return prev
1131
+ })
1132
+ break
1133
+
1134
+ case 'flow_complete':
1135
+ setFlowProgress((prev) => {
1136
+ if (!prev) return null
1137
+ return {
1138
+ ...prev,
1139
+ isComplete: true,
1140
+ isError: !data.success,
1141
+ executionTime: data.executionTime || data.duration,
1142
+ }
1143
+ })
1144
+ setIsRunning(false)
1145
+ break
1146
+
1147
+ default:
1148
+ break
1149
+ }
1150
+ }, [])
1151
+
1152
+ return (
1153
+ <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
1154
+ {/* Header */}
1155
+ <header style={headerStyle}>
1156
+ <div>
1157
+ <h1 style={titleStyle}>Runtype React Flow Example</h1>
1158
+ <p style={subtitleStyle}>Visual flow editor built with @runtypelabs/react-flow</p>
1159
+ </div>
1160
+ <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
1161
+ {loadedFlowId && (
1162
+ <span style={{ fontSize: '12px', color: '#a6adc8', marginRight: '8px' }}>
1163
+ Flow: <strong style={{ color: '#cdd6f4' }}>{flowName}</strong>
1164
+ </span>
1165
+ )}
1166
+ <button
1167
+ style={loadFlowButtonStyle}
1168
+ onClick={() => setShowFlowBrowser(true)}
1169
+ >
1170
+ <FolderIcon /> Load Flow
1171
+ </button>
1172
+ <button
1173
+ style={runFlowButtonStyle}
1174
+ onClick={() => setShowExecutionPanel(true)}
1175
+ >
1176
+ <PlayIcon /> Run Flow
1177
+ </button>
1178
+ <button
1179
+ style={viewConfigButtonStyle}
1180
+ onClick={() => setShowDispatchPanel(true)}
1181
+ >
1182
+ <CodeIcon /> View Dispatch Config
1183
+ </button>
1184
+ </div>
1185
+ </header>
1186
+
1187
+ {/* Flow Editor */}
1188
+ <div style={{ flex: 1 }}>
1189
+ <ReactFlow
1190
+ nodes={nodes}
1191
+ edges={edges}
1192
+ onNodesChange={onNodesChange}
1193
+ onEdgesChange={onEdgesChange}
1194
+ onConnect={onConnect}
1195
+ nodeTypes={nodeTypes}
1196
+ fitView
1197
+ fitViewOptions={{ padding: 0.2 }}
1198
+ snapToGrid
1199
+ snapGrid={[20, 20]}
1200
+ connectionLineType={ConnectionLineType.SmoothStep}
1201
+ defaultEdgeOptions={{
1202
+ type: 'smoothstep',
1203
+ animated: false,
1204
+ }}
1205
+ >
1206
+ <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#d1d5db" />
1207
+ <Controls />
1208
+ <MiniMap
1209
+ nodeStrokeWidth={3}
1210
+ zoomable
1211
+ pannable
1212
+ style={{
1213
+ backgroundColor: '#f9fafb',
1214
+ border: '1px solid #e5e7eb',
1215
+ borderRadius: '8px',
1216
+ }}
1217
+ />
1218
+
1219
+ {/* Toolbar */}
1220
+ <Panel position="top-left">
1221
+ <div style={toolbarStyle}>
1222
+ <span style={toolbarLabelStyle}>Add Step:</span>
1223
+ <button style={buttonStyle} onClick={() => addStep('prompt')}>
1224
+ + Prompt
1225
+ </button>
1226
+ <button style={buttonStyle} onClick={() => addStep('fetch-url')}>
1227
+ + Fetch URL
1228
+ </button>
1229
+ <button style={buttonStyle} onClick={() => addStep('transform-data')}>
1230
+ + Code
1231
+ </button>
1232
+ <button style={buttonStyle} onClick={() => addStep('conditional')}>
1233
+ + Conditional
1234
+ </button>
1235
+ <button style={buttonStyle} onClick={() => addStep('send-email')}>
1236
+ + Email
1237
+ </button>
1238
+ </div>
1239
+ </Panel>
1240
+
1241
+ {/* Info Panel */}
1242
+ <Panel position="top-right">
1243
+ <div style={infoStyle}>
1244
+ <strong>{nodes.length}</strong> steps | <strong>{edges.length}</strong> connections
1245
+ </div>
1246
+ </Panel>
1247
+ </ReactFlow>
1248
+ </div>
1249
+
1250
+ {/* Dispatch Config Panel */}
1251
+ <DispatchPanel
1252
+ isOpen={showDispatchPanel}
1253
+ onClose={() => setShowDispatchPanel(false)}
1254
+ steps={currentSteps}
1255
+ flowName={flowName}
1256
+ />
1257
+
1258
+ {/* Execution Panel */}
1259
+ <ExecutionPanel
1260
+ isOpen={showExecutionPanel}
1261
+ onClose={() => setShowExecutionPanel(false)}
1262
+ isRunning={isRunning}
1263
+ flowProgress={flowProgress}
1264
+ onRunFlow={executeFlow}
1265
+ />
1266
+
1267
+ {/* Flow Browser Panel */}
1268
+ <FlowBrowserPanel
1269
+ isOpen={showFlowBrowser}
1270
+ onClose={() => setShowFlowBrowser(false)}
1271
+ onLoadFlow={handleLoadFlow}
1272
+ />
1273
+ </div>
1274
+ )
1275
+ }
1276
+
1277
+ // ============================================================================
1278
+ // Icons
1279
+ // ============================================================================
1280
+
1281
+ function CloseIcon() {
1282
+ return (
1283
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1284
+ <line x1="18" y1="6" x2="6" y2="18" />
1285
+ <line x1="6" y1="6" x2="18" y2="18" />
1286
+ </svg>
1287
+ )
1288
+ }
1289
+
1290
+ function CopyIcon() {
1291
+ return (
1292
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1293
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
1294
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
1295
+ </svg>
1296
+ )
1297
+ }
1298
+
1299
+ function CodeIcon() {
1300
+ return (
1301
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1302
+ <polyline points="16 18 22 12 16 6" />
1303
+ <polyline points="8 6 2 12 8 18" />
1304
+ </svg>
1305
+ )
1306
+ }
1307
+
1308
+ function PlayIcon() {
1309
+ return (
1310
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1311
+ <polygon points="5 3 19 12 5 21 5 3" />
1312
+ </svg>
1313
+ )
1314
+ }
1315
+
1316
+ function FolderIcon() {
1317
+ return (
1318
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1319
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
1320
+ </svg>
1321
+ )
1322
+ }
1323
+
1324
+ function RefreshIcon() {
1325
+ return (
1326
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1327
+ <polyline points="23 4 23 10 17 10" />
1328
+ <polyline points="1 20 1 14 7 14" />
1329
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
1330
+ </svg>
1331
+ )
1332
+ }
1333
+
1334
+ function SpinnerIcon() {
1335
+ return (
1336
+ <svg
1337
+ width="16"
1338
+ height="16"
1339
+ viewBox="0 0 24 24"
1340
+ fill="none"
1341
+ stroke="currentColor"
1342
+ strokeWidth="2"
1343
+ style={{ animation: 'spin 1s linear infinite' }}
1344
+ >
1345
+ <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
1346
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
1347
+ </svg>
1348
+ )
1349
+ }
1350
+
1351
+ function CheckIcon() {
1352
+ return (
1353
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1354
+ <polyline points="20 6 9 17 4 12" />
1355
+ </svg>
1356
+ )
1357
+ }
1358
+
1359
+ function ErrorIcon() {
1360
+ return (
1361
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1362
+ <circle cx="12" cy="12" r="10" />
1363
+ <line x1="15" y1="9" x2="9" y2="15" />
1364
+ <line x1="9" y1="9" x2="15" y2="15" />
1365
+ </svg>
1366
+ )
1367
+ }
1368
+
1369
+ function PendingIcon() {
1370
+ return (
1371
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1372
+ <circle cx="12" cy="12" r="10" />
1373
+ </svg>
1374
+ )
1375
+ }
1376
+
1377
+ // ============================================================================
1378
+ // Styles
1379
+ // ============================================================================
1380
+
1381
+ const headerStyle: React.CSSProperties = {
1382
+ padding: '16px 24px',
1383
+ backgroundColor: '#1e1e2e',
1384
+ borderBottom: '1px solid #313244',
1385
+ display: 'flex',
1386
+ justifyContent: 'space-between',
1387
+ alignItems: 'center',
1388
+ }
1389
+
1390
+ const titleStyle: React.CSSProperties = {
1391
+ fontSize: '20px',
1392
+ fontWeight: 700,
1393
+ color: '#cdd6f4',
1394
+ marginBottom: '4px',
1395
+ }
1396
+
1397
+ const subtitleStyle: React.CSSProperties = {
1398
+ fontSize: '14px',
1399
+ color: '#a6adc8',
1400
+ }
1401
+
1402
+ const viewConfigButtonStyle: React.CSSProperties = {
1403
+ display: 'flex',
1404
+ alignItems: 'center',
1405
+ gap: '8px',
1406
+ padding: '10px 16px',
1407
+ fontSize: '13px',
1408
+ fontWeight: 600,
1409
+ backgroundColor: '#89b4fa',
1410
+ border: 'none',
1411
+ borderRadius: '8px',
1412
+ cursor: 'pointer',
1413
+ color: '#1e1e2e',
1414
+ transition: 'all 0.15s ease',
1415
+ }
1416
+
1417
+ const runFlowButtonStyle: React.CSSProperties = {
1418
+ display: 'flex',
1419
+ alignItems: 'center',
1420
+ gap: '8px',
1421
+ padding: '10px 16px',
1422
+ fontSize: '13px',
1423
+ fontWeight: 600,
1424
+ backgroundColor: '#a6e3a1',
1425
+ border: 'none',
1426
+ borderRadius: '8px',
1427
+ cursor: 'pointer',
1428
+ color: '#1e1e2e',
1429
+ transition: 'all 0.15s ease',
1430
+ }
1431
+
1432
+ const loadFlowButtonStyle: React.CSSProperties = {
1433
+ display: 'flex',
1434
+ alignItems: 'center',
1435
+ gap: '8px',
1436
+ padding: '10px 16px',
1437
+ fontSize: '13px',
1438
+ fontWeight: 600,
1439
+ backgroundColor: '#cba6f7',
1440
+ border: 'none',
1441
+ borderRadius: '8px',
1442
+ cursor: 'pointer',
1443
+ color: '#1e1e2e',
1444
+ transition: 'all 0.15s ease',
1445
+ }
1446
+
1447
+ const runButtonStyle: React.CSSProperties = {
1448
+ display: 'inline-flex',
1449
+ alignItems: 'center',
1450
+ gap: '8px',
1451
+ padding: '12px 24px',
1452
+ fontSize: '14px',
1453
+ fontWeight: 600,
1454
+ backgroundColor: '#a6e3a1',
1455
+ border: 'none',
1456
+ borderRadius: '8px',
1457
+ cursor: 'pointer',
1458
+ color: '#1e1e2e',
1459
+ transition: 'all 0.15s ease',
1460
+ }
1461
+
1462
+ const progressHeaderStyle: React.CSSProperties = {
1463
+ display: 'flex',
1464
+ alignItems: 'center',
1465
+ justifyContent: 'space-between',
1466
+ padding: '16px',
1467
+ backgroundColor: '#181825',
1468
+ borderRadius: '8px',
1469
+ border: '1px solid #313244',
1470
+ }
1471
+
1472
+ const executionStepStyle: React.CSSProperties = {
1473
+ backgroundColor: '#181825',
1474
+ borderRadius: '8px',
1475
+ border: '1px solid #313244',
1476
+ marginBottom: '8px',
1477
+ overflow: 'hidden',
1478
+ }
1479
+
1480
+ const executionStepHeaderStyle: React.CSSProperties = {
1481
+ display: 'flex',
1482
+ alignItems: 'center',
1483
+ justifyContent: 'space-between',
1484
+ padding: '12px',
1485
+ borderBottom: '1px solid #313244',
1486
+ }
1487
+
1488
+ const streamingOutputStyle: React.CSSProperties = {
1489
+ padding: '12px',
1490
+ backgroundColor: '#11111b',
1491
+ fontSize: '12px',
1492
+ fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
1493
+ color: '#cdd6f4',
1494
+ maxHeight: '200px',
1495
+ overflow: 'auto',
1496
+ }
1497
+
1498
+ const errorOutputStyle: React.CSSProperties = {
1499
+ padding: '12px',
1500
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
1501
+ fontSize: '12px',
1502
+ fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
1503
+ color: '#ef4444',
1504
+ }
1505
+
1506
+ const toolbarStyle: React.CSSProperties = {
1507
+ display: 'flex',
1508
+ alignItems: 'center',
1509
+ gap: '8px',
1510
+ padding: '10px 14px',
1511
+ backgroundColor: '#ffffff',
1512
+ borderRadius: '8px',
1513
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
1514
+ border: '1px solid #e5e7eb',
1515
+ }
1516
+
1517
+ const toolbarLabelStyle: React.CSSProperties = {
1518
+ fontSize: '12px',
1519
+ fontWeight: 600,
1520
+ color: '#6b7280',
1521
+ marginRight: '4px',
1522
+ }
1523
+
1524
+ const buttonStyle: React.CSSProperties = {
1525
+ padding: '6px 12px',
1526
+ fontSize: '12px',
1527
+ fontWeight: 500,
1528
+ backgroundColor: '#f3f4f6',
1529
+ border: '1px solid #e5e7eb',
1530
+ borderRadius: '6px',
1531
+ cursor: 'pointer',
1532
+ color: '#374151',
1533
+ transition: 'all 0.15s ease',
1534
+ }
1535
+
1536
+ const infoStyle: React.CSSProperties = {
1537
+ padding: '8px 14px',
1538
+ backgroundColor: '#ffffff',
1539
+ borderRadius: '8px',
1540
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
1541
+ border: '1px solid #e5e7eb',
1542
+ fontSize: '12px',
1543
+ color: '#6b7280',
1544
+ }
1545
+
1546
+ // Panel Styles
1547
+ const panelOverlayStyle: React.CSSProperties = {
1548
+ position: 'fixed',
1549
+ top: 0,
1550
+ right: 0,
1551
+ bottom: 0,
1552
+ left: 0,
1553
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
1554
+ zIndex: 1000,
1555
+ display: 'flex',
1556
+ justifyContent: 'flex-end',
1557
+ }
1558
+
1559
+ const panelContainerStyle: React.CSSProperties = {
1560
+ width: '560px',
1561
+ maxWidth: '100%',
1562
+ height: '100%',
1563
+ backgroundColor: '#1e1e2e',
1564
+ display: 'flex',
1565
+ flexDirection: 'column',
1566
+ boxShadow: '-4px 0 20px rgba(0, 0, 0, 0.3)',
1567
+ }
1568
+
1569
+ const panelHeaderStyle: React.CSSProperties = {
1570
+ display: 'flex',
1571
+ alignItems: 'center',
1572
+ justifyContent: 'space-between',
1573
+ padding: '16px 20px',
1574
+ borderBottom: '1px solid #313244',
1575
+ }
1576
+
1577
+ const panelTitleStyle: React.CSSProperties = {
1578
+ fontSize: '16px',
1579
+ fontWeight: 700,
1580
+ color: '#cdd6f4',
1581
+ margin: 0,
1582
+ }
1583
+
1584
+ const closeButtonStyle: React.CSSProperties = {
1585
+ display: 'flex',
1586
+ alignItems: 'center',
1587
+ justifyContent: 'center',
1588
+ width: '32px',
1589
+ height: '32px',
1590
+ backgroundColor: 'transparent',
1591
+ border: 'none',
1592
+ borderRadius: '6px',
1593
+ cursor: 'pointer',
1594
+ color: '#a6adc8',
1595
+ transition: 'all 0.15s ease',
1596
+ }
1597
+
1598
+ const tabsStyle: React.CSSProperties = {
1599
+ display: 'flex',
1600
+ gap: '4px',
1601
+ padding: '12px 20px',
1602
+ borderBottom: '1px solid #313244',
1603
+ }
1604
+
1605
+ const tabStyle: React.CSSProperties = {
1606
+ padding: '8px 16px',
1607
+ fontSize: '13px',
1608
+ fontWeight: 500,
1609
+ backgroundColor: 'transparent',
1610
+ border: 'none',
1611
+ borderRadius: '6px',
1612
+ cursor: 'pointer',
1613
+ color: '#a6adc8',
1614
+ transition: 'all 0.15s ease',
1615
+ }
1616
+
1617
+ const activeTabStyle: React.CSSProperties = {
1618
+ ...tabStyle,
1619
+ backgroundColor: '#313244',
1620
+ color: '#cdd6f4',
1621
+ }
1622
+
1623
+ const panelContentStyle: React.CSSProperties = {
1624
+ flex: 1,
1625
+ overflow: 'auto',
1626
+ padding: '20px',
1627
+ }
1628
+
1629
+ const descriptionStyle: React.CSSProperties = {
1630
+ fontSize: '13px',
1631
+ color: '#a6adc8',
1632
+ marginBottom: '16px',
1633
+ lineHeight: 1.5,
1634
+ }
1635
+
1636
+ const codeStyle: React.CSSProperties = {
1637
+ padding: '2px 6px',
1638
+ backgroundColor: '#313244',
1639
+ borderRadius: '4px',
1640
+ fontFamily: 'monospace',
1641
+ fontSize: '12px',
1642
+ color: '#89b4fa',
1643
+ }
1644
+
1645
+ const codeBlockStyle: React.CSSProperties = {
1646
+ backgroundColor: '#11111b',
1647
+ borderRadius: '8px',
1648
+ border: '1px solid #313244',
1649
+ overflow: 'auto',
1650
+ maxHeight: '400px',
1651
+ }
1652
+
1653
+ const preStyle: React.CSSProperties = {
1654
+ margin: 0,
1655
+ padding: '16px',
1656
+ fontSize: '12px',
1657
+ fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
1658
+ color: '#cdd6f4',
1659
+ lineHeight: 1.5,
1660
+ whiteSpace: 'pre-wrap',
1661
+ wordBreak: 'break-word',
1662
+ }
1663
+
1664
+ const usageStyle: React.CSSProperties = {
1665
+ marginTop: '24px',
1666
+ }
1667
+
1668
+ const usageTitleStyle: React.CSSProperties = {
1669
+ fontSize: '14px',
1670
+ fontWeight: 600,
1671
+ color: '#cdd6f4',
1672
+ marginBottom: '12px',
1673
+ }
1674
+
1675
+ const stepCardStyle: React.CSSProperties = {
1676
+ marginBottom: '16px',
1677
+ backgroundColor: '#181825',
1678
+ borderRadius: '8px',
1679
+ border: '1px solid #313244',
1680
+ overflow: 'hidden',
1681
+ }
1682
+
1683
+ const stepHeaderStyle: React.CSSProperties = {
1684
+ display: 'flex',
1685
+ alignItems: 'center',
1686
+ gap: '8px',
1687
+ padding: '10px 12px',
1688
+ borderBottom: '1px solid #313244',
1689
+ backgroundColor: '#1e1e2e',
1690
+ }
1691
+
1692
+ const stepBadgeStyle = (type: string): React.CSSProperties => {
1693
+ const colors: Record<string, { bg: string; text: string }> = {
1694
+ 'prompt': { bg: '#cba6f7', text: '#1e1e2e' },
1695
+ 'fetch-url': { bg: '#89b4fa', text: '#1e1e2e' },
1696
+ 'transform-data': { bg: '#f9e2af', text: '#1e1e2e' },
1697
+ 'conditional': { bg: '#f5c2e7', text: '#1e1e2e' },
1698
+ 'send-email': { bg: '#a6e3a1', text: '#1e1e2e' },
1699
+ }
1700
+ const color = colors[type] || { bg: '#6c7086', text: '#cdd6f4' }
1701
+ return {
1702
+ padding: '2px 8px',
1703
+ borderRadius: '4px',
1704
+ fontSize: '10px',
1705
+ fontWeight: 700,
1706
+ backgroundColor: color.bg,
1707
+ color: color.text,
1708
+ textTransform: 'uppercase',
1709
+ }
1710
+ }
1711
+
1712
+ const stepNameStyle: React.CSSProperties = {
1713
+ flex: 1,
1714
+ fontSize: '13px',
1715
+ fontWeight: 600,
1716
+ color: '#cdd6f4',
1717
+ }
1718
+
1719
+ const stepOrderStyle: React.CSSProperties = {
1720
+ fontSize: '11px',
1721
+ color: '#6c7086',
1722
+ }
1723
+
1724
+ const panelFooterStyle: React.CSSProperties = {
1725
+ display: 'flex',
1726
+ justifyContent: 'flex-end',
1727
+ padding: '16px 20px',
1728
+ borderTop: '1px solid #313244',
1729
+ }
1730
+
1731
+ const copyButtonStyle: React.CSSProperties = {
1732
+ display: 'flex',
1733
+ alignItems: 'center',
1734
+ gap: '6px',
1735
+ padding: '8px 16px',
1736
+ fontSize: '13px',
1737
+ fontWeight: 500,
1738
+ backgroundColor: '#89b4fa',
1739
+ border: 'none',
1740
+ borderRadius: '6px',
1741
+ cursor: 'pointer',
1742
+ color: '#1e1e2e',
1743
+ transition: 'all 0.15s ease',
1744
+ }