@marktoflow/gui 2.0.0-alpha.1 → 2.0.0-alpha.2
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/.turbo/turbo-build.log +7 -7
- package/README.md +38 -2
- package/dist/client/assets/index-C90Y_aBX.js +678 -0
- package/dist/client/assets/index-C90Y_aBX.js.map +1 -0
- package/dist/client/assets/index-CRWeQ3NN.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/index.js +1 -1
- package/dist/server/server/routes/tools.js +406 -0
- package/dist/server/server/routes/tools.js.map +1 -1
- package/dist/server/server/services/agents/codex-provider.js +270 -0
- package/dist/server/server/services/agents/codex-provider.js.map +1 -0
- package/dist/server/server/services/agents/prompts.js +27 -0
- package/dist/server/server/services/agents/prompts.js.map +1 -1
- package/dist/server/server/services/agents/registry.js +20 -0
- package/dist/server/server/services/agents/registry.js.map +1 -1
- package/package.json +4 -4
- package/src/client/components/Canvas/Canvas.tsx +24 -6
- package/src/client/components/Canvas/ForEachNode.tsx +128 -0
- package/src/client/components/Canvas/IfElseNode.tsx +126 -0
- package/src/client/components/Canvas/ParallelNode.tsx +140 -0
- package/src/client/components/Canvas/SwitchNode.tsx +164 -0
- package/src/client/components/Canvas/TransformNode.tsx +185 -0
- package/src/client/components/Canvas/TryCatchNode.tsx +164 -0
- package/src/client/components/Canvas/WhileNode.tsx +129 -0
- package/src/client/components/Canvas/index.ts +24 -0
- package/src/client/utils/serviceIcons.tsx +33 -0
- package/src/server/index.ts +1 -1
- package/src/server/routes/tools.ts +406 -0
- package/src/server/services/agents/codex-provider.ts +398 -0
- package/src/server/services/agents/prompts.ts +27 -0
- package/src/server/services/agents/registry.ts +21 -0
- package/tailwind.config.ts +1 -1
- package/tests/integration/api.test.ts +203 -1
- package/tests/integration/testApp.ts +1 -1
- package/tests/setup.ts +35 -0
- package/tests/unit/ForEachNode.test.tsx +218 -0
- package/tests/unit/IfElseNode.test.tsx +188 -0
- package/tests/unit/ParallelNode.test.tsx +264 -0
- package/tests/unit/SwitchNode.test.tsx +252 -0
- package/tests/unit/TransformNode.test.tsx +386 -0
- package/tests/unit/TryCatchNode.test.tsx +243 -0
- package/tests/unit/WhileNode.test.tsx +226 -0
- package/tests/unit/codexProvider.test.ts +399 -0
- package/tests/unit/serviceIcons.test.ts +197 -0
- package/.turbo/turbo-test.log +0 -22
- package/dist/client/assets/index-DwTI8opO.js +0 -608
- package/dist/client/assets/index-DwTI8opO.js.map +0 -1
- package/dist/client/assets/index-RoEdL6gO.css +0 -1
- package/dist/server/index.d.ts +0 -3
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -56
- package/dist/server/index.js.map +0 -1
- package/dist/server/routes/ai.js +0 -50
- package/dist/server/routes/ai.js.map +0 -1
- package/dist/server/routes/execute.js +0 -62
- package/dist/server/routes/execute.js.map +0 -1
- package/dist/server/routes/workflows.js +0 -99
- package/dist/server/routes/workflows.js.map +0 -1
- package/dist/server/services/AIService.d.ts +0 -30
- package/dist/server/services/AIService.d.ts.map +0 -1
- package/dist/server/services/AIService.js +0 -216
- package/dist/server/services/AIService.js.map +0 -1
- package/dist/server/services/FileWatcher.d.ts +0 -10
- package/dist/server/services/FileWatcher.d.ts.map +0 -1
- package/dist/server/services/FileWatcher.js +0 -62
- package/dist/server/services/FileWatcher.js.map +0 -1
- package/dist/server/services/WorkflowService.d.ts +0 -54
- package/dist/server/services/WorkflowService.d.ts.map +0 -1
- package/dist/server/services/WorkflowService.js +0 -323
- package/dist/server/services/WorkflowService.js.map +0 -1
- package/dist/server/websocket/index.d.ts +0 -10
- package/dist/server/websocket/index.d.ts.map +0 -1
- package/dist/server/websocket/index.js +0 -85
- package/dist/server/websocket/index.js.map +0 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
|
|
3
|
+
import { Repeat, CheckCircle, XCircle, Clock } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface ForEachNodeData extends Record<string, unknown> {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
items: string;
|
|
9
|
+
itemVariable?: string;
|
|
10
|
+
status?: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
11
|
+
currentIteration?: number;
|
|
12
|
+
totalIterations?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ForEachNodeType = Node<ForEachNodeData, 'for_each'>;
|
|
16
|
+
|
|
17
|
+
function ForEachNodeComponent({ data, selected }: NodeProps<ForEachNodeType>) {
|
|
18
|
+
const statusConfig: Record<
|
|
19
|
+
NonNullable<ForEachNodeData['status']>,
|
|
20
|
+
{ icon: typeof Clock; color: string; bgColor: string; animate?: boolean }
|
|
21
|
+
> = {
|
|
22
|
+
pending: { icon: Clock, color: 'text-gray-400', bgColor: 'bg-gray-400/10' },
|
|
23
|
+
running: {
|
|
24
|
+
icon: Repeat,
|
|
25
|
+
color: 'text-orange-400',
|
|
26
|
+
bgColor: 'bg-orange-400/10',
|
|
27
|
+
animate: true,
|
|
28
|
+
},
|
|
29
|
+
completed: {
|
|
30
|
+
icon: CheckCircle,
|
|
31
|
+
color: 'text-success',
|
|
32
|
+
bgColor: 'bg-success/10',
|
|
33
|
+
},
|
|
34
|
+
failed: { icon: XCircle, color: 'text-error', bgColor: 'bg-error/10' },
|
|
35
|
+
skipped: {
|
|
36
|
+
icon: XCircle,
|
|
37
|
+
color: 'text-gray-500',
|
|
38
|
+
bgColor: 'bg-gray-500/10',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const status = data.status || 'pending';
|
|
43
|
+
const config = statusConfig[status];
|
|
44
|
+
const StatusIcon = config.icon;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={`control-flow-node for-each-node p-0 ${selected ? 'selected' : ''} ${status === 'running' ? 'running' : ''}`}
|
|
49
|
+
style={{
|
|
50
|
+
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{/* Input handle */}
|
|
54
|
+
<Handle
|
|
55
|
+
type="target"
|
|
56
|
+
position={Position.Top}
|
|
57
|
+
className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
{/* Node header */}
|
|
61
|
+
<div className="flex items-center gap-3 p-3 border-b border-white/20">
|
|
62
|
+
<div className="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
|
|
63
|
+
<Repeat className="w-5 h-5 text-white" />
|
|
64
|
+
</div>
|
|
65
|
+
<div className="flex-1 min-w-0">
|
|
66
|
+
<div className="text-sm font-medium text-white">
|
|
67
|
+
{data.name || 'For Each'}
|
|
68
|
+
</div>
|
|
69
|
+
<div className="text-xs text-white/70">Loop</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div
|
|
72
|
+
className={`w-6 h-6 rounded-full ${config.bgColor} flex items-center justify-center`}
|
|
73
|
+
>
|
|
74
|
+
<StatusIcon
|
|
75
|
+
className={`w-4 h-4 ${config.color} ${config.animate ? 'animate-pulse' : ''}`}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Node body */}
|
|
81
|
+
<div className="p-3 bg-white/10">
|
|
82
|
+
<div className="text-xs text-white/90 mb-2">
|
|
83
|
+
<span className="text-white/60">Items:</span>{' '}
|
|
84
|
+
<span className="font-mono">{data.items || 'Not set'}</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="text-xs text-white/90 mb-3">
|
|
87
|
+
<span className="text-white/60">Variable:</span>{' '}
|
|
88
|
+
<span className="font-mono">{data.itemVariable || 'item'}</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Iteration progress */}
|
|
92
|
+
{data.totalIterations !== undefined && (
|
|
93
|
+
<div className="mt-2 p-2 bg-white/5 rounded">
|
|
94
|
+
<div className="flex items-center justify-between mb-1">
|
|
95
|
+
<span className="text-xs text-white/70">Progress</span>
|
|
96
|
+
<span className="text-xs text-white font-medium">
|
|
97
|
+
{data.currentIteration || 0} / {data.totalIterations}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div className="w-full bg-white/10 rounded-full h-1.5">
|
|
101
|
+
<div
|
|
102
|
+
className="bg-orange-400 h-1.5 rounded-full transition-all"
|
|
103
|
+
style={{
|
|
104
|
+
width: `${((data.currentIteration || 0) / data.totalIterations) * 100}%`,
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{/* Loop metadata */}
|
|
112
|
+
<div className="mt-3 text-xs text-white/50 flex items-center gap-2">
|
|
113
|
+
<span>ℹ️</span>
|
|
114
|
+
<span>Access: loop.index, loop.first, loop.last</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Output handle */}
|
|
119
|
+
<Handle
|
|
120
|
+
type="source"
|
|
121
|
+
position={Position.Bottom}
|
|
122
|
+
className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const ForEachNode = memo(ForEachNodeComponent);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
|
|
3
|
+
import { GitBranch, CheckCircle, XCircle, Clock } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface IfElseNodeData extends Record<string, unknown> {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
condition: string;
|
|
9
|
+
status?: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
10
|
+
activeBranch?: 'then' | 'else' | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type IfElseNodeType = Node<IfElseNodeData, 'if'>;
|
|
14
|
+
|
|
15
|
+
function IfElseNodeComponent({ data, selected }: NodeProps<IfElseNodeType>) {
|
|
16
|
+
const statusConfig: Record<
|
|
17
|
+
NonNullable<IfElseNodeData['status']>,
|
|
18
|
+
{ icon: typeof Clock; color: string; bgColor: string; animate?: boolean }
|
|
19
|
+
> = {
|
|
20
|
+
pending: { icon: Clock, color: 'text-gray-400', bgColor: 'bg-gray-400/10' },
|
|
21
|
+
running: {
|
|
22
|
+
icon: GitBranch,
|
|
23
|
+
color: 'text-blue-400',
|
|
24
|
+
bgColor: 'bg-blue-400/10',
|
|
25
|
+
animate: true,
|
|
26
|
+
},
|
|
27
|
+
completed: {
|
|
28
|
+
icon: CheckCircle,
|
|
29
|
+
color: 'text-success',
|
|
30
|
+
bgColor: 'bg-success/10',
|
|
31
|
+
},
|
|
32
|
+
failed: { icon: XCircle, color: 'text-error', bgColor: 'bg-error/10' },
|
|
33
|
+
skipped: {
|
|
34
|
+
icon: XCircle,
|
|
35
|
+
color: 'text-gray-500',
|
|
36
|
+
bgColor: 'bg-gray-500/10',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const status = data.status || 'pending';
|
|
41
|
+
const config = statusConfig[status];
|
|
42
|
+
const StatusIcon = config.icon;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={`control-flow-node if-else-node p-0 ${selected ? 'selected' : ''} ${status === 'running' ? 'running' : ''}`}
|
|
47
|
+
style={{
|
|
48
|
+
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{/* Input handle */}
|
|
52
|
+
<Handle
|
|
53
|
+
type="target"
|
|
54
|
+
position={Position.Top}
|
|
55
|
+
className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
{/* Node header */}
|
|
59
|
+
<div className="flex items-center gap-3 p-3 border-b border-white/20">
|
|
60
|
+
<div className="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
|
|
61
|
+
<GitBranch className="w-5 h-5 text-white" />
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex-1 min-w-0">
|
|
64
|
+
<div className="text-sm font-medium text-white">
|
|
65
|
+
{data.name || 'If/Else'}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="text-xs text-white/70">Conditional</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div
|
|
70
|
+
className={`w-6 h-6 rounded-full ${config.bgColor} flex items-center justify-center`}
|
|
71
|
+
>
|
|
72
|
+
<StatusIcon
|
|
73
|
+
className={`w-4 h-4 ${config.color} ${config.animate ? 'animate-pulse' : ''}`}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Node body */}
|
|
79
|
+
<div className="p-3 bg-white/10">
|
|
80
|
+
<div className="text-xs text-white/90 font-mono mb-3">
|
|
81
|
+
{data.condition || 'No condition set'}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Branch outputs */}
|
|
85
|
+
<div className="grid grid-cols-2 gap-2">
|
|
86
|
+
<div
|
|
87
|
+
className={`text-center p-2 rounded text-xs font-medium transition-colors ${
|
|
88
|
+
data.activeBranch === 'then'
|
|
89
|
+
? 'bg-green-500/30 text-green-200'
|
|
90
|
+
: 'bg-white/5 text-white/60'
|
|
91
|
+
}`}
|
|
92
|
+
>
|
|
93
|
+
✓ Then
|
|
94
|
+
</div>
|
|
95
|
+
<div
|
|
96
|
+
className={`text-center p-2 rounded text-xs font-medium transition-colors ${
|
|
97
|
+
data.activeBranch === 'else'
|
|
98
|
+
? 'bg-red-500/30 text-red-200'
|
|
99
|
+
: 'bg-white/5 text-white/60'
|
|
100
|
+
}`}
|
|
101
|
+
>
|
|
102
|
+
✗ Else
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Output handles */}
|
|
108
|
+
<Handle
|
|
109
|
+
type="source"
|
|
110
|
+
position={Position.Bottom}
|
|
111
|
+
id="then"
|
|
112
|
+
style={{ left: '33%' }}
|
|
113
|
+
className="!w-3 !h-3 !bg-green-500 !border-2 !border-node-bg"
|
|
114
|
+
/>
|
|
115
|
+
<Handle
|
|
116
|
+
type="source"
|
|
117
|
+
position={Position.Bottom}
|
|
118
|
+
id="else"
|
|
119
|
+
style={{ left: '67%' }}
|
|
120
|
+
className="!w-3 !h-3 !bg-red-500 !border-2 !border-node-bg"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const IfElseNode = memo(IfElseNodeComponent);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
|
|
3
|
+
import { Layers, CheckCircle, XCircle, Clock } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface ParallelNodeData extends Record<string, unknown> {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
branches: Array<{ id: string; name?: string }>;
|
|
9
|
+
maxConcurrent?: number;
|
|
10
|
+
onError?: 'stop' | 'continue';
|
|
11
|
+
status?: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
12
|
+
activeBranches?: string[];
|
|
13
|
+
completedBranches?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ParallelNodeType = Node<ParallelNodeData, 'parallel'>;
|
|
17
|
+
|
|
18
|
+
function ParallelNodeComponent({ data, selected }: NodeProps<ParallelNodeType>) {
|
|
19
|
+
const statusConfig: Record<
|
|
20
|
+
NonNullable<ParallelNodeData['status']>,
|
|
21
|
+
{ icon: typeof Clock; color: string; bgColor: string; animate?: boolean }
|
|
22
|
+
> = {
|
|
23
|
+
pending: { icon: Clock, color: 'text-gray-400', bgColor: 'bg-gray-400/10' },
|
|
24
|
+
running: {
|
|
25
|
+
icon: Layers,
|
|
26
|
+
color: 'text-green-400',
|
|
27
|
+
bgColor: 'bg-green-400/10',
|
|
28
|
+
animate: true,
|
|
29
|
+
},
|
|
30
|
+
completed: {
|
|
31
|
+
icon: CheckCircle,
|
|
32
|
+
color: 'text-success',
|
|
33
|
+
bgColor: 'bg-success/10',
|
|
34
|
+
},
|
|
35
|
+
failed: { icon: XCircle, color: 'text-error', bgColor: 'bg-error/10' },
|
|
36
|
+
skipped: {
|
|
37
|
+
icon: XCircle,
|
|
38
|
+
color: 'text-gray-500',
|
|
39
|
+
bgColor: 'bg-gray-500/10',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const status = data.status || 'pending';
|
|
44
|
+
const config = statusConfig[status];
|
|
45
|
+
const StatusIcon = config.icon;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={`control-flow-node parallel-node p-0 ${selected ? 'selected' : ''} ${status === 'running' ? 'running' : ''}`}
|
|
50
|
+
style={{
|
|
51
|
+
background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{/* Input handle */}
|
|
55
|
+
<Handle
|
|
56
|
+
type="target"
|
|
57
|
+
position={Position.Top}
|
|
58
|
+
className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
{/* Node header */}
|
|
62
|
+
<div className="flex items-center gap-3 p-3 border-b border-white/20">
|
|
63
|
+
<div className="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
|
|
64
|
+
<Layers className="w-5 h-5 text-white" />
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex-1 min-w-0">
|
|
67
|
+
<div className="text-sm font-medium text-white">
|
|
68
|
+
{data.name || 'Parallel'}
|
|
69
|
+
</div>
|
|
70
|
+
<div className="text-xs text-white/70">Concurrent Execution</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div
|
|
73
|
+
className={`w-6 h-6 rounded-full ${config.bgColor} flex items-center justify-center`}
|
|
74
|
+
>
|
|
75
|
+
<StatusIcon
|
|
76
|
+
className={`w-4 h-4 ${config.color} ${config.animate ? 'animate-pulse' : ''}`}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Node body */}
|
|
82
|
+
<div className="p-3 bg-white/10">
|
|
83
|
+
<div className="text-xs text-white/90 mb-3">
|
|
84
|
+
<span className="text-white/60">Branches:</span>{' '}
|
|
85
|
+
<span className="font-medium">{data.branches?.length || 0}</span>
|
|
86
|
+
{data.maxConcurrent && (
|
|
87
|
+
<>
|
|
88
|
+
{' '}
|
|
89
|
+
<span className="text-white/60">• Max Concurrent:</span>{' '}
|
|
90
|
+
<span className="font-medium">{data.maxConcurrent}</span>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Branch indicators */}
|
|
96
|
+
<div className="flex flex-wrap gap-2 mb-3">
|
|
97
|
+
{data.branches?.slice(0, 6).map((branch) => {
|
|
98
|
+
const isActive = data.activeBranches?.includes(branch.id);
|
|
99
|
+
const isCompleted = data.completedBranches?.includes(branch.id);
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
key={branch.id}
|
|
103
|
+
className={`px-2 py-1 rounded text-xs font-medium transition-colors ${
|
|
104
|
+
isCompleted
|
|
105
|
+
? 'bg-green-500/30 text-green-200'
|
|
106
|
+
: isActive
|
|
107
|
+
? 'bg-blue-500/30 text-blue-200 animate-pulse'
|
|
108
|
+
: 'bg-white/10 text-white/60'
|
|
109
|
+
}`}
|
|
110
|
+
title={branch.name || branch.id}
|
|
111
|
+
>
|
|
112
|
+
{branch.name || `B${branch.id.slice(-2)}`}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
{data.branches && data.branches.length > 6 && (
|
|
117
|
+
<div className="px-2 py-1 rounded text-xs font-medium bg-white/10 text-white/60">
|
|
118
|
+
+{data.branches.length - 6}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Error handling */}
|
|
124
|
+
<div className="text-xs text-white/50 flex items-center gap-1">
|
|
125
|
+
<span>On Error:</span>
|
|
126
|
+
<span className="font-medium">{data.onError || 'stop'}</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Output handle */}
|
|
131
|
+
<Handle
|
|
132
|
+
type="source"
|
|
133
|
+
position={Position.Bottom}
|
|
134
|
+
className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const ParallelNode = memo(ParallelNodeComponent);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
|
|
3
|
+
import { GitFork, CheckCircle, XCircle, Clock } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface SwitchNodeData extends Record<string, unknown> {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
expression: string;
|
|
9
|
+
cases: Record<string, unknown>;
|
|
10
|
+
hasDefault?: boolean;
|
|
11
|
+
status?: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
12
|
+
activeCase?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SwitchNodeType = Node<SwitchNodeData, 'switch'>;
|
|
16
|
+
|
|
17
|
+
function SwitchNodeComponent({ data, selected }: NodeProps<SwitchNodeType>) {
|
|
18
|
+
const statusConfig: Record<
|
|
19
|
+
NonNullable<SwitchNodeData['status']>,
|
|
20
|
+
{ icon: typeof Clock; color: string; bgColor: string; animate?: boolean }
|
|
21
|
+
> = {
|
|
22
|
+
pending: { icon: Clock, color: 'text-gray-400', bgColor: 'bg-gray-400/10' },
|
|
23
|
+
running: {
|
|
24
|
+
icon: GitFork,
|
|
25
|
+
color: 'text-purple-400',
|
|
26
|
+
bgColor: 'bg-purple-400/10',
|
|
27
|
+
animate: true,
|
|
28
|
+
},
|
|
29
|
+
completed: {
|
|
30
|
+
icon: CheckCircle,
|
|
31
|
+
color: 'text-success',
|
|
32
|
+
bgColor: 'bg-success/10',
|
|
33
|
+
},
|
|
34
|
+
failed: { icon: XCircle, color: 'text-error', bgColor: 'bg-error/10' },
|
|
35
|
+
skipped: {
|
|
36
|
+
icon: XCircle,
|
|
37
|
+
color: 'text-gray-500',
|
|
38
|
+
bgColor: 'bg-gray-500/10',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const status = data.status || 'pending';
|
|
43
|
+
const config = statusConfig[status];
|
|
44
|
+
const StatusIcon = config.icon;
|
|
45
|
+
|
|
46
|
+
const caseKeys = Object.keys(data.cases || {});
|
|
47
|
+
const displayCases = caseKeys.slice(0, 4); // Show up to 4 cases
|
|
48
|
+
const hasMore = caseKeys.length > 4;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={`control-flow-node switch-node p-0 ${selected ? 'selected' : ''} ${status === 'running' ? 'running' : ''}`}
|
|
53
|
+
style={{
|
|
54
|
+
background: 'linear-gradient(135deg, #a855f7 0%, #ec4899 100%)',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{/* Input handle */}
|
|
58
|
+
<Handle
|
|
59
|
+
type="target"
|
|
60
|
+
position={Position.Top}
|
|
61
|
+
className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
{/* Node header */}
|
|
65
|
+
<div className="flex items-center gap-3 p-3 border-b border-white/20">
|
|
66
|
+
<div className="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
|
|
67
|
+
<GitFork className="w-5 h-5 text-white" />
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex-1 min-w-0">
|
|
70
|
+
<div className="text-sm font-medium text-white">
|
|
71
|
+
{data.name || 'Switch'}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="text-xs text-white/70">Multi-Branch Router</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div
|
|
76
|
+
className={`w-6 h-6 rounded-full ${config.bgColor} flex items-center justify-center`}
|
|
77
|
+
>
|
|
78
|
+
<StatusIcon
|
|
79
|
+
className={`w-4 h-4 ${config.color} ${config.animate ? 'animate-pulse' : ''}`}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Node body */}
|
|
85
|
+
<div className="p-3 bg-white/10">
|
|
86
|
+
<div className="text-xs text-white/90 mb-3">
|
|
87
|
+
<span className="text-white/60">Expression:</span>{' '}
|
|
88
|
+
<span className="font-mono">{data.expression || 'Not set'}</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Case list */}
|
|
92
|
+
<div className="space-y-2">
|
|
93
|
+
<div className="text-xs text-white/70 font-medium mb-1">Cases:</div>
|
|
94
|
+
{displayCases.map((caseKey, index) => {
|
|
95
|
+
const isActive = data.activeCase === caseKey;
|
|
96
|
+
const handlePosition = ((index + 1) / (displayCases.length + (data.hasDefault ? 2 : 1))) * 100;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div key={caseKey} className="relative">
|
|
100
|
+
<div
|
|
101
|
+
className={`text-xs px-2 py-1.5 rounded font-medium transition-colors ${
|
|
102
|
+
isActive
|
|
103
|
+
? 'bg-purple-500/30 text-purple-200 ring-1 ring-purple-400/50'
|
|
104
|
+
: 'bg-white/5 text-white/70'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{caseKey}
|
|
108
|
+
</div>
|
|
109
|
+
{/* Output handle for this case */}
|
|
110
|
+
<Handle
|
|
111
|
+
type="source"
|
|
112
|
+
position={Position.Bottom}
|
|
113
|
+
id={`case-${caseKey}`}
|
|
114
|
+
style={{ left: `${handlePosition}%` }}
|
|
115
|
+
className="!w-2.5 !h-2.5 !bg-purple-400 !border-2 !border-node-bg"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
|
|
121
|
+
{hasMore && (
|
|
122
|
+
<div className="text-xs px-2 py-1 rounded bg-white/5 text-white/60 text-center">
|
|
123
|
+
+{caseKeys.length - 4} more cases
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{/* Default case */}
|
|
128
|
+
{data.hasDefault && (
|
|
129
|
+
<div className="relative">
|
|
130
|
+
<div
|
|
131
|
+
className={`text-xs px-2 py-1.5 rounded font-medium transition-colors ${
|
|
132
|
+
data.activeCase === 'default'
|
|
133
|
+
? 'bg-gray-500/30 text-gray-200 ring-1 ring-gray-400/50'
|
|
134
|
+
: 'bg-white/5 text-white/70'
|
|
135
|
+
}`}
|
|
136
|
+
>
|
|
137
|
+
default
|
|
138
|
+
</div>
|
|
139
|
+
{/* Output handle for default */}
|
|
140
|
+
<Handle
|
|
141
|
+
type="source"
|
|
142
|
+
position={Position.Bottom}
|
|
143
|
+
id="case-default"
|
|
144
|
+
style={{ left: '95%' }}
|
|
145
|
+
className="!w-2.5 !h-2.5 !bg-gray-400 !border-2 !border-node-bg"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Case count */}
|
|
152
|
+
<div className="mt-3 text-xs text-white/50 flex items-center gap-2">
|
|
153
|
+
<span>ℹ️</span>
|
|
154
|
+
<span>
|
|
155
|
+
{caseKeys.length} case{caseKeys.length !== 1 ? 's' : ''}
|
|
156
|
+
{data.hasDefault ? ' + default' : ''}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const SwitchNode = memo(SwitchNodeComponent);
|