@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.
Files changed (74) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/README.md +38 -2
  3. package/dist/client/assets/index-C90Y_aBX.js +678 -0
  4. package/dist/client/assets/index-C90Y_aBX.js.map +1 -0
  5. package/dist/client/assets/index-CRWeQ3NN.css +1 -0
  6. package/dist/client/index.html +2 -2
  7. package/dist/server/server/index.js +1 -1
  8. package/dist/server/server/routes/tools.js +406 -0
  9. package/dist/server/server/routes/tools.js.map +1 -1
  10. package/dist/server/server/services/agents/codex-provider.js +270 -0
  11. package/dist/server/server/services/agents/codex-provider.js.map +1 -0
  12. package/dist/server/server/services/agents/prompts.js +27 -0
  13. package/dist/server/server/services/agents/prompts.js.map +1 -1
  14. package/dist/server/server/services/agents/registry.js +20 -0
  15. package/dist/server/server/services/agents/registry.js.map +1 -1
  16. package/package.json +4 -4
  17. package/src/client/components/Canvas/Canvas.tsx +24 -6
  18. package/src/client/components/Canvas/ForEachNode.tsx +128 -0
  19. package/src/client/components/Canvas/IfElseNode.tsx +126 -0
  20. package/src/client/components/Canvas/ParallelNode.tsx +140 -0
  21. package/src/client/components/Canvas/SwitchNode.tsx +164 -0
  22. package/src/client/components/Canvas/TransformNode.tsx +185 -0
  23. package/src/client/components/Canvas/TryCatchNode.tsx +164 -0
  24. package/src/client/components/Canvas/WhileNode.tsx +129 -0
  25. package/src/client/components/Canvas/index.ts +24 -0
  26. package/src/client/utils/serviceIcons.tsx +33 -0
  27. package/src/server/index.ts +1 -1
  28. package/src/server/routes/tools.ts +406 -0
  29. package/src/server/services/agents/codex-provider.ts +398 -0
  30. package/src/server/services/agents/prompts.ts +27 -0
  31. package/src/server/services/agents/registry.ts +21 -0
  32. package/tailwind.config.ts +1 -1
  33. package/tests/integration/api.test.ts +203 -1
  34. package/tests/integration/testApp.ts +1 -1
  35. package/tests/setup.ts +35 -0
  36. package/tests/unit/ForEachNode.test.tsx +218 -0
  37. package/tests/unit/IfElseNode.test.tsx +188 -0
  38. package/tests/unit/ParallelNode.test.tsx +264 -0
  39. package/tests/unit/SwitchNode.test.tsx +252 -0
  40. package/tests/unit/TransformNode.test.tsx +386 -0
  41. package/tests/unit/TryCatchNode.test.tsx +243 -0
  42. package/tests/unit/WhileNode.test.tsx +226 -0
  43. package/tests/unit/codexProvider.test.ts +399 -0
  44. package/tests/unit/serviceIcons.test.ts +197 -0
  45. package/.turbo/turbo-test.log +0 -22
  46. package/dist/client/assets/index-DwTI8opO.js +0 -608
  47. package/dist/client/assets/index-DwTI8opO.js.map +0 -1
  48. package/dist/client/assets/index-RoEdL6gO.css +0 -1
  49. package/dist/server/index.d.ts +0 -3
  50. package/dist/server/index.d.ts.map +0 -1
  51. package/dist/server/index.js +0 -56
  52. package/dist/server/index.js.map +0 -1
  53. package/dist/server/routes/ai.js +0 -50
  54. package/dist/server/routes/ai.js.map +0 -1
  55. package/dist/server/routes/execute.js +0 -62
  56. package/dist/server/routes/execute.js.map +0 -1
  57. package/dist/server/routes/workflows.js +0 -99
  58. package/dist/server/routes/workflows.js.map +0 -1
  59. package/dist/server/services/AIService.d.ts +0 -30
  60. package/dist/server/services/AIService.d.ts.map +0 -1
  61. package/dist/server/services/AIService.js +0 -216
  62. package/dist/server/services/AIService.js.map +0 -1
  63. package/dist/server/services/FileWatcher.d.ts +0 -10
  64. package/dist/server/services/FileWatcher.d.ts.map +0 -1
  65. package/dist/server/services/FileWatcher.js +0 -62
  66. package/dist/server/services/FileWatcher.js.map +0 -1
  67. package/dist/server/services/WorkflowService.d.ts +0 -54
  68. package/dist/server/services/WorkflowService.d.ts.map +0 -1
  69. package/dist/server/services/WorkflowService.js +0 -323
  70. package/dist/server/services/WorkflowService.js.map +0 -1
  71. package/dist/server/websocket/index.d.ts +0 -10
  72. package/dist/server/websocket/index.d.ts.map +0 -1
  73. package/dist/server/websocket/index.js +0 -85
  74. 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);