@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,424 @@
|
|
|
1
|
+
import { useMemo, useCallback } from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
FlowStep,
|
|
4
|
+
RuntypeNode,
|
|
5
|
+
FlowValidationResult,
|
|
6
|
+
ValidationError,
|
|
7
|
+
ValidationWarning,
|
|
8
|
+
PromptStepConfig,
|
|
9
|
+
FetchUrlStepConfig,
|
|
10
|
+
TransformDataStepConfig,
|
|
11
|
+
ConditionalStepConfig,
|
|
12
|
+
SendEmailStepConfig,
|
|
13
|
+
} from '../types'
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Validation Rules
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
interface ValidationRule {
|
|
20
|
+
validate: (step: FlowStep, allSteps: FlowStep[]) => ValidationError[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface WarningRule {
|
|
24
|
+
check: (step: FlowStep, allSteps: FlowStep[]) => ValidationWarning[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Prompt Step Validation
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const promptValidation: ValidationRule = {
|
|
32
|
+
validate: (step) => {
|
|
33
|
+
const errors: ValidationError[] = []
|
|
34
|
+
const config = step.config as PromptStepConfig
|
|
35
|
+
|
|
36
|
+
if (!config.model) {
|
|
37
|
+
errors.push({
|
|
38
|
+
stepId: step.id,
|
|
39
|
+
field: 'model',
|
|
40
|
+
message: 'Model is required',
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!config.userPrompt?.trim()) {
|
|
45
|
+
errors.push({
|
|
46
|
+
stepId: step.id,
|
|
47
|
+
field: 'userPrompt',
|
|
48
|
+
message: 'User prompt is required',
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!config.outputVariable?.trim()) {
|
|
53
|
+
errors.push({
|
|
54
|
+
stepId: step.id,
|
|
55
|
+
field: 'outputVariable',
|
|
56
|
+
message: 'Output variable is required',
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return errors
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Fetch URL Step Validation
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
const fetchUrlValidation: ValidationRule = {
|
|
69
|
+
validate: (step) => {
|
|
70
|
+
const errors: ValidationError[] = []
|
|
71
|
+
const config = step.config as FetchUrlStepConfig
|
|
72
|
+
|
|
73
|
+
if (!config.http?.url?.trim()) {
|
|
74
|
+
errors.push({
|
|
75
|
+
stepId: step.id,
|
|
76
|
+
field: 'http.url',
|
|
77
|
+
message: 'URL is required',
|
|
78
|
+
})
|
|
79
|
+
} else {
|
|
80
|
+
// Validate URL format (allow template variables)
|
|
81
|
+
const url = config.http.url
|
|
82
|
+
if (!url.startsWith('http://') && !url.startsWith('https://') && !url.includes('{{')) {
|
|
83
|
+
errors.push({
|
|
84
|
+
stepId: step.id,
|
|
85
|
+
field: 'http.url',
|
|
86
|
+
message: 'URL must start with http:// or https://',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!config.outputVariable?.trim()) {
|
|
92
|
+
errors.push({
|
|
93
|
+
stepId: step.id,
|
|
94
|
+
field: 'outputVariable',
|
|
95
|
+
message: 'Output variable is required',
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return errors
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Transform Data Step Validation
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
const transformDataValidation: ValidationRule = {
|
|
108
|
+
validate: (step) => {
|
|
109
|
+
const errors: ValidationError[] = []
|
|
110
|
+
const config = step.config as TransformDataStepConfig
|
|
111
|
+
|
|
112
|
+
if (!config.script?.trim()) {
|
|
113
|
+
errors.push({
|
|
114
|
+
stepId: step.id,
|
|
115
|
+
field: 'script',
|
|
116
|
+
message: 'Script is required',
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!config.outputVariable?.trim()) {
|
|
121
|
+
errors.push({
|
|
122
|
+
stepId: step.id,
|
|
123
|
+
field: 'outputVariable',
|
|
124
|
+
message: 'Output variable is required',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return errors
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Conditional Step Validation
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
const conditionalValidation: ValidationRule = {
|
|
137
|
+
validate: (step, allSteps) => {
|
|
138
|
+
const errors: ValidationError[] = []
|
|
139
|
+
const config = step.config as ConditionalStepConfig
|
|
140
|
+
|
|
141
|
+
if (!config.condition?.trim()) {
|
|
142
|
+
errors.push({
|
|
143
|
+
stepId: step.id,
|
|
144
|
+
field: 'condition',
|
|
145
|
+
message: 'Condition is required',
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Validate nested steps
|
|
150
|
+
if (config.trueSteps && config.trueSteps.length > 0) {
|
|
151
|
+
for (const nestedStep of config.trueSteps) {
|
|
152
|
+
const nestedErrors = validateStep(nestedStep, allSteps)
|
|
153
|
+
errors.push(
|
|
154
|
+
...nestedErrors.map((e) => ({
|
|
155
|
+
...e,
|
|
156
|
+
message: `True branch: ${e.message}`,
|
|
157
|
+
}))
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (config.falseSteps && config.falseSteps.length > 0) {
|
|
163
|
+
for (const nestedStep of config.falseSteps) {
|
|
164
|
+
const nestedErrors = validateStep(nestedStep, allSteps)
|
|
165
|
+
errors.push(
|
|
166
|
+
...nestedErrors.map((e) => ({
|
|
167
|
+
...e,
|
|
168
|
+
message: `False branch: ${e.message}`,
|
|
169
|
+
}))
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return errors
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Send Email Step Validation
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
const sendEmailValidation: ValidationRule = {
|
|
183
|
+
validate: (step) => {
|
|
184
|
+
const errors: ValidationError[] = []
|
|
185
|
+
const config = step.config as SendEmailStepConfig
|
|
186
|
+
|
|
187
|
+
if (!config.to?.trim()) {
|
|
188
|
+
errors.push({
|
|
189
|
+
stepId: step.id,
|
|
190
|
+
field: 'to',
|
|
191
|
+
message: 'Recipient (To) is required',
|
|
192
|
+
})
|
|
193
|
+
} else {
|
|
194
|
+
// Validate email format (allow template variables)
|
|
195
|
+
const to = config.to
|
|
196
|
+
if (!to.includes('@') && !to.includes('{{')) {
|
|
197
|
+
errors.push({
|
|
198
|
+
stepId: step.id,
|
|
199
|
+
field: 'to',
|
|
200
|
+
message: 'Invalid email address',
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!config.subject?.trim()) {
|
|
206
|
+
errors.push({
|
|
207
|
+
stepId: step.id,
|
|
208
|
+
field: 'subject',
|
|
209
|
+
message: 'Subject is required',
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!config.html?.trim() && !config.text?.trim()) {
|
|
214
|
+
errors.push({
|
|
215
|
+
stepId: step.id,
|
|
216
|
+
field: 'html',
|
|
217
|
+
message: 'Email content (HTML or text) is required',
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!config.outputVariable?.trim()) {
|
|
222
|
+
errors.push({
|
|
223
|
+
stepId: step.id,
|
|
224
|
+
field: 'outputVariable',
|
|
225
|
+
message: 'Output variable is required',
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return errors
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Warning Rules
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
const outputVariableWarning: WarningRule = {
|
|
238
|
+
check: (step, allSteps) => {
|
|
239
|
+
const warnings: ValidationWarning[] = []
|
|
240
|
+
const config = step.config as { outputVariable?: string }
|
|
241
|
+
|
|
242
|
+
if (config.outputVariable) {
|
|
243
|
+
// Check for duplicate output variables
|
|
244
|
+
const duplicates = allSteps.filter(
|
|
245
|
+
(s) =>
|
|
246
|
+
s.id !== step.id &&
|
|
247
|
+
(s.config as { outputVariable?: string }).outputVariable === config.outputVariable
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (duplicates.length > 0) {
|
|
251
|
+
warnings.push({
|
|
252
|
+
stepId: step.id,
|
|
253
|
+
field: 'outputVariable',
|
|
254
|
+
message: `Output variable "${config.outputVariable}" is also used by another step`,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return warnings
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const emptyBranchWarning: WarningRule = {
|
|
264
|
+
check: (step) => {
|
|
265
|
+
const warnings: ValidationWarning[] = []
|
|
266
|
+
|
|
267
|
+
if (step.type === 'conditional') {
|
|
268
|
+
const config = step.config as ConditionalStepConfig
|
|
269
|
+
|
|
270
|
+
if (!config.trueSteps || config.trueSteps.length === 0) {
|
|
271
|
+
warnings.push({
|
|
272
|
+
stepId: step.id,
|
|
273
|
+
field: 'trueSteps',
|
|
274
|
+
message: 'True branch has no steps',
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!config.falseSteps || config.falseSteps.length === 0) {
|
|
279
|
+
warnings.push({
|
|
280
|
+
stepId: step.id,
|
|
281
|
+
field: 'falseSteps',
|
|
282
|
+
message: 'False branch has no steps',
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return warnings
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Validation Logic
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
const validationRules: Record<string, ValidationRule> = {
|
|
296
|
+
prompt: promptValidation,
|
|
297
|
+
'fetch-url': fetchUrlValidation,
|
|
298
|
+
'transform-data': transformDataValidation,
|
|
299
|
+
conditional: conditionalValidation,
|
|
300
|
+
'send-email': sendEmailValidation,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const warningRules: WarningRule[] = [outputVariableWarning, emptyBranchWarning]
|
|
304
|
+
|
|
305
|
+
function validateStep(step: FlowStep, allSteps: FlowStep[]): ValidationError[] {
|
|
306
|
+
const errors: ValidationError[] = []
|
|
307
|
+
|
|
308
|
+
// Common validation
|
|
309
|
+
if (!step.name?.trim()) {
|
|
310
|
+
errors.push({
|
|
311
|
+
stepId: step.id,
|
|
312
|
+
field: 'name',
|
|
313
|
+
message: 'Step name is required',
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Type-specific validation
|
|
318
|
+
const rule = validationRules[step.type]
|
|
319
|
+
if (rule) {
|
|
320
|
+
errors.push(...rule.validate(step, allSteps))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return errors
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function checkWarnings(step: FlowStep, allSteps: FlowStep[]): ValidationWarning[] {
|
|
327
|
+
const warnings: ValidationWarning[] = []
|
|
328
|
+
|
|
329
|
+
for (const rule of warningRules) {
|
|
330
|
+
warnings.push(...rule.check(step, allSteps))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return warnings
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Hook
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
export interface UseFlowValidationOptions {
|
|
341
|
+
/** Steps to validate */
|
|
342
|
+
steps?: FlowStep[]
|
|
343
|
+
/** Nodes to validate (will extract steps from nodes) */
|
|
344
|
+
nodes?: RuntypeNode[]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface UseFlowValidationReturn {
|
|
348
|
+
/** Validation result */
|
|
349
|
+
result: FlowValidationResult
|
|
350
|
+
/** Validate a specific step */
|
|
351
|
+
validateStep: (step: FlowStep) => ValidationError[]
|
|
352
|
+
/** Check if a specific step is valid */
|
|
353
|
+
isStepValid: (stepId: string) => boolean
|
|
354
|
+
/** Get errors for a specific step */
|
|
355
|
+
getStepErrors: (stepId: string) => ValidationError[]
|
|
356
|
+
/** Get warnings for a specific step */
|
|
357
|
+
getStepWarnings: (stepId: string) => ValidationWarning[]
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function useFlowValidation(options: UseFlowValidationOptions): UseFlowValidationReturn {
|
|
361
|
+
const { steps: providedSteps, nodes } = options
|
|
362
|
+
|
|
363
|
+
// Extract steps from nodes if not provided directly
|
|
364
|
+
const steps = useMemo(() => {
|
|
365
|
+
if (providedSteps) return providedSteps
|
|
366
|
+
if (nodes) return nodes.map((n) => n.data.step)
|
|
367
|
+
return []
|
|
368
|
+
}, [providedSteps, nodes])
|
|
369
|
+
|
|
370
|
+
// Perform validation
|
|
371
|
+
const result = useMemo((): FlowValidationResult => {
|
|
372
|
+
const errors: ValidationError[] = []
|
|
373
|
+
const warnings: ValidationWarning[] = []
|
|
374
|
+
|
|
375
|
+
for (const step of steps) {
|
|
376
|
+
errors.push(...validateStep(step, steps))
|
|
377
|
+
warnings.push(...checkWarnings(step, steps))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
isValid: errors.length === 0,
|
|
382
|
+
errors,
|
|
383
|
+
warnings,
|
|
384
|
+
}
|
|
385
|
+
}, [steps])
|
|
386
|
+
|
|
387
|
+
// Helper functions
|
|
388
|
+
const validateStepFn = useCallback(
|
|
389
|
+
(step: FlowStep) => validateStep(step, steps),
|
|
390
|
+
[steps]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
const isStepValid = useCallback(
|
|
394
|
+
(stepId: string) => {
|
|
395
|
+
return !result.errors.some((e) => e.stepId === stepId)
|
|
396
|
+
},
|
|
397
|
+
[result.errors]
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
const getStepErrors = useCallback(
|
|
401
|
+
(stepId: string) => {
|
|
402
|
+
return result.errors.filter((e) => e.stepId === stepId)
|
|
403
|
+
},
|
|
404
|
+
[result.errors]
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
const getStepWarnings = useCallback(
|
|
408
|
+
(stepId: string) => {
|
|
409
|
+
return result.warnings.filter((w) => w.stepId === stepId)
|
|
410
|
+
},
|
|
411
|
+
[result.warnings]
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
result,
|
|
416
|
+
validateStep: validateStepFn,
|
|
417
|
+
isStepValid,
|
|
418
|
+
getStepErrors,
|
|
419
|
+
getStepWarnings,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export default useFlowValidation
|
|
424
|
+
|