@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,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
|
+
}
|