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