@marktoflow/gui 2.0.0-alpha.4 → 2.0.0-alpha.5
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 +24 -8
- package/README.md +11 -1
- package/dist/client/assets/index-CM44OayM.js +704 -0
- package/dist/client/assets/index-CM44OayM.js.map +1 -0
- package/dist/client/assets/index-Dru63gi6.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/{server/index.js → index.js} +22 -1
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/executions.js +125 -0
- package/dist/server/routes/executions.js.map +1 -0
- package/dist/server/{server/routes → routes}/workflows.js +37 -1
- package/dist/server/routes/workflows.js.map +1 -0
- package/dist/server/{server/services → services}/WorkflowService.js +158 -15
- package/dist/server/services/WorkflowService.js.map +1 -0
- package/dist/server/{server/websocket → websocket}/index.js +12 -0
- package/dist/server/{server/websocket → websocket}/index.js.map +1 -1
- package/marktoflow-gui-2.0.0-alpha.5.tgz +0 -0
- package/package.json +19 -5
- package/scripts/flatten-dist.js +69 -0
- package/src/client/components/Canvas/Canvas.tsx +3 -1
- package/src/client/components/Canvas/ExecutionOverlay.tsx +120 -32
- package/src/client/components/Canvas/ForEachNode.tsx +27 -3
- package/src/client/components/Canvas/IfElseNode.tsx +22 -7
- package/src/client/components/Canvas/NodeContextMenu.tsx +8 -4
- package/src/client/components/Canvas/ParallelNode.tsx +25 -8
- package/src/client/components/Canvas/SwitchNode.tsx +41 -20
- package/src/client/components/Canvas/Toolbar.tsx +59 -21
- package/src/client/components/Canvas/TransformNode.tsx +9 -0
- package/src/client/components/Canvas/WhileNode.tsx +35 -3
- package/src/client/components/Debug/VariableInspector.tsx +148 -0
- package/src/client/components/Prompt/PromptInput.tsx +3 -1
- package/src/client/components/Settings/ProviderSwitcher.tsx +228 -0
- package/src/client/components/Sidebar/ImportDialog.tsx +257 -0
- package/src/client/components/Sidebar/Sidebar.tsx +21 -2
- package/src/client/components/common/KeyboardShortcuts.tsx +8 -2
- package/src/client/stores/agentStore.ts +109 -0
- package/src/client/stores/executionStore.ts +64 -2
- package/src/client/stores/workflowStore.ts +10 -2
- package/src/client/styles/globals.css +106 -0
- package/src/client/utils/platform.ts +46 -0
- package/src/client/utils/workflowToGraph.ts +245 -21
- package/src/server/index.ts +24 -1
- package/src/server/routes/executions.ts +136 -0
- package/src/server/routes/workflows.ts +42 -1
- package/src/server/services/WorkflowService.ts +176 -16
- package/src/server/websocket/index.ts +13 -0
- package/tests/unit/ForEachNode.test.tsx +96 -6
- package/tests/unit/IfElseNode.test.tsx +47 -0
- package/tests/unit/ParallelNode.test.tsx +80 -0
- package/tests/unit/SwitchNode.test.tsx +75 -0
- package/tests/unit/WhileNode.test.tsx +12 -8
- package/tests/unit/agentStore.test.ts +218 -0
- package/tests/unit/executionStore.test.ts +40 -0
- package/tests/unit/platform.test.ts +118 -0
- package/tests/unit/workflowToGraph.test.ts +22 -0
- package/dist/client/assets/index-C90Y_aBX.js +0 -678
- package/dist/client/assets/index-C90Y_aBX.js.map +0 -1
- package/dist/client/assets/index-CRWeQ3NN.css +0 -1
- package/dist/server/server/index.js.map +0 -1
- package/dist/server/server/routes/workflows.js.map +0 -1
- package/dist/server/server/services/WorkflowService.js.map +0 -1
- /package/dist/server/{server/routes → routes}/ai.js +0 -0
- /package/dist/server/{server/routes → routes}/ai.js.map +0 -0
- /package/dist/server/{server/routes → routes}/execute.js +0 -0
- /package/dist/server/{server/routes → routes}/execute.js.map +0 -0
- /package/dist/server/{server/routes → routes}/tools.js +0 -0
- /package/dist/server/{server/routes → routes}/tools.js.map +0 -0
- /package/dist/server/{server/services → services}/AIService.js +0 -0
- /package/dist/server/{server/services → services}/AIService.js.map +0 -0
- /package/dist/server/{server/services → services}/FileWatcher.js +0 -0
- /package/dist/server/{server/services → services}/FileWatcher.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/claude-code-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/claude-code-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/claude-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/claude-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/codex-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/codex-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/copilot-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/copilot-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/demo-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/demo-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/index.js +0 -0
- /package/dist/server/{server/services → services}/agents/index.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/ollama-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/ollama-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/prompts.js +0 -0
- /package/dist/server/{server/services → services}/agents/prompts.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/registry.js +0 -0
- /package/dist/server/{server/services → services}/agents/registry.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/types.js +0 -0
- /package/dist/server/{server/services → services}/agents/types.js.map +0 -0
- /package/dist/{server/shared → shared}/constants.js +0 -0
- /package/dist/{server/shared → shared}/constants.js.map +0 -0
- /package/dist/{server/shared → shared}/types.js +0 -0
- /package/dist/{server/shared → shared}/types.js.map +0 -0
|
@@ -8,6 +8,7 @@ export interface ExecutionStepResult {
|
|
|
8
8
|
startTime?: string;
|
|
9
9
|
endTime?: string;
|
|
10
10
|
duration?: number;
|
|
11
|
+
inputs?: Record<string, unknown>;
|
|
11
12
|
output?: unknown;
|
|
12
13
|
outputVariable?: string;
|
|
13
14
|
error?: string;
|
|
@@ -42,13 +43,14 @@ interface ExecutionState {
|
|
|
42
43
|
currentRunId: string | null;
|
|
43
44
|
isExecuting: boolean;
|
|
44
45
|
isPaused: boolean;
|
|
46
|
+
isLoadingHistory: boolean;
|
|
45
47
|
|
|
46
48
|
// Debug mode state
|
|
47
49
|
debug: DebugState;
|
|
48
50
|
|
|
49
51
|
// Existing methods
|
|
50
52
|
startExecution: (workflowId: string, workflowName: string, inputs?: Record<string, unknown>) => string;
|
|
51
|
-
updateStepStatus: (runId: string, stepId: string, status: StepStatus, output?: unknown, error?: string, outputVariable?: string) => void;
|
|
53
|
+
updateStepStatus: (runId: string, stepId: string, status: StepStatus, output?: unknown, error?: string, outputVariable?: string, inputs?: Record<string, unknown>) => void;
|
|
52
54
|
completeExecution: (runId: string, status: WorkflowStatus, outputs?: Record<string, unknown>) => void;
|
|
53
55
|
addLog: (runId: string, message: string) => void;
|
|
54
56
|
pauseExecution: () => void;
|
|
@@ -57,6 +59,10 @@ interface ExecutionState {
|
|
|
57
59
|
clearHistory: () => void;
|
|
58
60
|
getRun: (runId: string) => ExecutionRun | undefined;
|
|
59
61
|
|
|
62
|
+
// API sync methods
|
|
63
|
+
loadHistory: (workflowId?: string) => Promise<void>;
|
|
64
|
+
syncRunWithBackend: (runId: string) => Promise<void>;
|
|
65
|
+
|
|
60
66
|
// Debug mode methods
|
|
61
67
|
enableDebugMode: () => void;
|
|
62
68
|
disableDebugMode: () => void;
|
|
@@ -87,6 +93,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
|
|
|
87
93
|
currentRunId: null,
|
|
88
94
|
isExecuting: false,
|
|
89
95
|
isPaused: false,
|
|
96
|
+
isLoadingHistory: false,
|
|
90
97
|
debug: initialDebugState,
|
|
91
98
|
|
|
92
99
|
startExecution: (workflowId, workflowName, inputs) => {
|
|
@@ -112,7 +119,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
|
|
|
112
119
|
return runId;
|
|
113
120
|
},
|
|
114
121
|
|
|
115
|
-
updateStepStatus: (runId, stepId, status, output, error, outputVariable) => {
|
|
122
|
+
updateStepStatus: (runId, stepId, status, output, error, outputVariable, inputs) => {
|
|
116
123
|
const { debug } = get();
|
|
117
124
|
|
|
118
125
|
// Check if we need to pause at breakpoint
|
|
@@ -141,6 +148,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
|
|
|
141
148
|
status,
|
|
142
149
|
endTime,
|
|
143
150
|
duration,
|
|
151
|
+
inputs: inputs ?? s.inputs,
|
|
144
152
|
output: output ?? s.output,
|
|
145
153
|
outputVariable: outputVariable ?? s.outputVariable,
|
|
146
154
|
error: error ?? s.error,
|
|
@@ -157,6 +165,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
|
|
|
157
165
|
stepName: stepId,
|
|
158
166
|
status,
|
|
159
167
|
startTime: now,
|
|
168
|
+
inputs,
|
|
160
169
|
output,
|
|
161
170
|
outputVariable,
|
|
162
171
|
error,
|
|
@@ -411,6 +420,59 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
|
|
|
411
420
|
},
|
|
412
421
|
});
|
|
413
422
|
},
|
|
423
|
+
|
|
424
|
+
// API sync methods
|
|
425
|
+
loadHistory: async (workflowId) => {
|
|
426
|
+
set({ isLoadingHistory: true });
|
|
427
|
+
try {
|
|
428
|
+
const url = workflowId
|
|
429
|
+
? `/api/executions?workflowId=${encodeURIComponent(workflowId)}&limit=50`
|
|
430
|
+
: '/api/executions?limit=50';
|
|
431
|
+
|
|
432
|
+
const response = await fetch(url);
|
|
433
|
+
if (!response.ok) {
|
|
434
|
+
throw new Error('Failed to load execution history');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const executions = await response.json();
|
|
438
|
+
|
|
439
|
+
// Convert backend ExecutionRecord format to ExecutionRun format
|
|
440
|
+
const runs: ExecutionRun[] = executions.map((exec: any) => ({
|
|
441
|
+
id: exec.runId,
|
|
442
|
+
workflowId: exec.workflowId,
|
|
443
|
+
workflowName: exec.workflowPath.split('/').pop()?.replace('.md', '') || exec.workflowId,
|
|
444
|
+
status: exec.status,
|
|
445
|
+
startTime: exec.startedAt,
|
|
446
|
+
endTime: exec.completedAt || undefined,
|
|
447
|
+
duration: exec.completedAt
|
|
448
|
+
? new Date(exec.completedAt).getTime() - new Date(exec.startedAt).getTime()
|
|
449
|
+
: undefined,
|
|
450
|
+
steps: [],
|
|
451
|
+
logs: [],
|
|
452
|
+
inputs: exec.inputs || undefined,
|
|
453
|
+
outputs: exec.outputs || undefined,
|
|
454
|
+
}));
|
|
455
|
+
|
|
456
|
+
// Merge with existing runs, avoiding duplicates
|
|
457
|
+
const existingRunIds = new Set(get().runs.map((r) => r.id));
|
|
458
|
+
const newRuns = runs.filter((r) => !existingRunIds.has(r.id));
|
|
459
|
+
|
|
460
|
+
set({
|
|
461
|
+
runs: [...get().runs, ...newRuns].slice(0, 50),
|
|
462
|
+
isLoadingHistory: false,
|
|
463
|
+
});
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error('Error loading execution history:', error);
|
|
466
|
+
set({ isLoadingHistory: false });
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
syncRunWithBackend: async (runId) => {
|
|
471
|
+
// This would be called to persist a run to the backend
|
|
472
|
+
// For now, the backend integration will happen via WebSocket events
|
|
473
|
+
// This is a placeholder for future direct API sync
|
|
474
|
+
console.log('Syncing run with backend:', runId);
|
|
475
|
+
},
|
|
414
476
|
}));
|
|
415
477
|
|
|
416
478
|
// Helper to format duration
|
|
@@ -9,6 +9,7 @@ interface WorkflowState {
|
|
|
9
9
|
error: string | null;
|
|
10
10
|
|
|
11
11
|
loadWorkflows: () => Promise<void>;
|
|
12
|
+
fetchWorkflows: () => Promise<void>; // Alias for compatibility
|
|
12
13
|
selectWorkflow: (path: string) => void;
|
|
13
14
|
loadWorkflow: (path: string) => Promise<void>;
|
|
14
15
|
saveWorkflow: (workflow: Workflow) => Promise<void>;
|
|
@@ -85,8 +86,8 @@ const demoWorkflow: Workflow = {
|
|
|
85
86
|
|
|
86
87
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|
87
88
|
workflows: demoWorkflows,
|
|
88
|
-
selectedWorkflow:
|
|
89
|
-
currentWorkflow:
|
|
89
|
+
selectedWorkflow: null,
|
|
90
|
+
currentWorkflow: null,
|
|
90
91
|
isLoading: false,
|
|
91
92
|
error: null,
|
|
92
93
|
|
|
@@ -96,13 +97,20 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|
|
96
97
|
const response = await fetch('/api/workflows');
|
|
97
98
|
if (!response.ok) throw new Error('Failed to load workflows');
|
|
98
99
|
const data = await response.json();
|
|
100
|
+
console.log('Loaded workflows:', data.workflows.length);
|
|
99
101
|
set({ workflows: data.workflows, isLoading: false });
|
|
100
102
|
} catch (error) {
|
|
103
|
+
console.error('Failed to load workflows, using demo data:', error);
|
|
101
104
|
// Use demo data if API fails
|
|
102
105
|
set({ workflows: demoWorkflows, isLoading: false });
|
|
103
106
|
}
|
|
104
107
|
},
|
|
105
108
|
|
|
109
|
+
// Alias for compatibility with components
|
|
110
|
+
fetchWorkflows: async () => {
|
|
111
|
+
await get().loadWorkflows();
|
|
112
|
+
},
|
|
113
|
+
|
|
106
114
|
selectWorkflow: (path) => {
|
|
107
115
|
set({ selectedWorkflow: path });
|
|
108
116
|
get().loadWorkflow(path);
|
|
@@ -82,6 +82,112 @@
|
|
|
82
82
|
border-color: #d9534f;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/* Control flow node base styles */
|
|
86
|
+
.control-flow-node {
|
|
87
|
+
min-width: 220px;
|
|
88
|
+
border-radius: 12px;
|
|
89
|
+
border: 2px solid transparent;
|
|
90
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
91
|
+
transition: all 0.2s ease;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.control-flow-node:hover {
|
|
96
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
|
|
97
|
+
transform: translateY(-1px);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.control-flow-node.selected {
|
|
101
|
+
box-shadow: 0 0 0 4px rgba(255, 109, 90, 0.3), 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
102
|
+
border-color: rgba(255, 109, 90, 0.5);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.control-flow-node.running {
|
|
106
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.control-flow-node.completed {
|
|
110
|
+
box-shadow: 0 0 0 2px rgba(92, 184, 92, 0.4), 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.control-flow-node.failed {
|
|
114
|
+
box-shadow: 0 0 0 2px rgba(217, 83, 79, 0.4), 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Control flow specific node types */
|
|
118
|
+
.if-else-node {
|
|
119
|
+
position: relative;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.switch-node {
|
|
123
|
+
position: relative;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.for-each-node,
|
|
127
|
+
.while-node {
|
|
128
|
+
position: relative;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.parallel-node {
|
|
132
|
+
position: relative;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.try-catch-node {
|
|
136
|
+
position: relative;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.transform-node {
|
|
140
|
+
position: relative;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Execution overlays for control flow nodes */
|
|
144
|
+
.control-flow-overlay {
|
|
145
|
+
position: absolute;
|
|
146
|
+
top: 0;
|
|
147
|
+
left: 0;
|
|
148
|
+
right: 0;
|
|
149
|
+
bottom: 0;
|
|
150
|
+
background: rgba(0, 0, 0, 0.7);
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
z-index: 10;
|
|
155
|
+
pointer-events: none;
|
|
156
|
+
animation: fadeIn 0.2s ease-out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.control-flow-badge {
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 4px;
|
|
163
|
+
padding: 4px 8px;
|
|
164
|
+
border-radius: 6px;
|
|
165
|
+
font-size: 10px;
|
|
166
|
+
font-weight: 600;
|
|
167
|
+
text-transform: uppercase;
|
|
168
|
+
letter-spacing: 0.5px;
|
|
169
|
+
background: rgba(0, 0, 0, 0.5);
|
|
170
|
+
backdrop-filter: blur(4px);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.control-flow-badge.early-exit {
|
|
174
|
+
background: rgba(255, 152, 0, 0.2);
|
|
175
|
+
border: 1px solid rgba(255, 152, 0, 0.5);
|
|
176
|
+
color: #ff9800;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.control-flow-badge.skipped {
|
|
180
|
+
background: rgba(128, 128, 128, 0.2);
|
|
181
|
+
border: 1px solid rgba(128, 128, 128, 0.5);
|
|
182
|
+
color: #999;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.control-flow-badge.max-concurrent {
|
|
186
|
+
background: rgba(255, 193, 7, 0.2);
|
|
187
|
+
border: 1px solid rgba(255, 193, 7, 0.5);
|
|
188
|
+
color: #ffc107;
|
|
189
|
+
}
|
|
190
|
+
|
|
85
191
|
/* Edge animation */
|
|
86
192
|
.react-flow__edge-path.animated {
|
|
87
193
|
stroke-dasharray: 8;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utilities for cross-platform keyboard shortcuts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect if the user is on a Mac platform
|
|
7
|
+
*/
|
|
8
|
+
export const isMac = (): boolean => {
|
|
9
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return navigator.platform.toUpperCase().includes('MAC');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the appropriate modifier key display for the current platform
|
|
17
|
+
* Returns '⌘' on Mac, 'Ctrl' on Windows/Linux
|
|
18
|
+
*/
|
|
19
|
+
export const getModKey = (): string => {
|
|
20
|
+
return isMac() ? '⌘' : 'Ctrl';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert a shortcut string to display the correct modifier for the platform
|
|
25
|
+
* @param shortcut - The shortcut string (can contain ⌘, Cmd, or Ctrl)
|
|
26
|
+
* @returns The shortcut with the correct platform modifier
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* getShortcutDisplay('⌘+S') // '⌘+S' on Mac, 'Ctrl+S' on Windows
|
|
30
|
+
* getShortcutDisplay('Cmd+Z') // '⌘+Z' on Mac, 'Ctrl+Z' on Windows
|
|
31
|
+
*/
|
|
32
|
+
export const getShortcutDisplay = (shortcut: string): string => {
|
|
33
|
+
const modKey = getModKey();
|
|
34
|
+
|
|
35
|
+
return shortcut
|
|
36
|
+
.replace(/⌘/g, modKey)
|
|
37
|
+
.replace(/Cmd/g, modKey);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if the modifier key is pressed in a keyboard event
|
|
42
|
+
* Checks metaKey on Mac, ctrlKey on Windows/Linux
|
|
43
|
+
*/
|
|
44
|
+
export const isModKeyPressed = (event: KeyboardEvent | React.KeyboardEvent): boolean => {
|
|
45
|
+
return isMac() ? event.metaKey : event.ctrlKey;
|
|
46
|
+
};
|
|
@@ -5,9 +5,15 @@ interface WorkflowStep {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
action?: string;
|
|
7
7
|
workflow?: string;
|
|
8
|
+
type?: 'while' | 'for_each' | 'for' | 'switch' | 'parallel' | 'try' | 'if' | 'map' | 'filter' | 'reduce';
|
|
9
|
+
condition?: string;
|
|
10
|
+
items?: string;
|
|
11
|
+
maxIterations?: number;
|
|
8
12
|
inputs: Record<string, unknown>;
|
|
9
13
|
outputVariable?: string;
|
|
10
14
|
conditions?: string[];
|
|
15
|
+
steps?: WorkflowStep[];
|
|
16
|
+
variables?: Record<string, { initial: unknown }>;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
interface WorkflowTrigger {
|
|
@@ -31,17 +37,119 @@ interface GraphResult {
|
|
|
31
37
|
edges: Edge[];
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Parse control flow constructs from raw markdown content
|
|
42
|
+
* This is a temporary solution until the core parser supports control flow
|
|
43
|
+
*/
|
|
44
|
+
function extractControlFlowFromMarkdown(markdown?: string): WorkflowStep[] {
|
|
45
|
+
if (!markdown) return [];
|
|
46
|
+
|
|
47
|
+
const controlFlowSteps: WorkflowStep[] = [];
|
|
48
|
+
// Match YAML code blocks that contain control flow types
|
|
49
|
+
const codeBlockRegex = /```yaml\s*\n([\s\S]*?)\n```/g;
|
|
50
|
+
let match;
|
|
51
|
+
let stepIndex = 0;
|
|
52
|
+
|
|
53
|
+
while ((match = codeBlockRegex.exec(markdown)) !== null) {
|
|
54
|
+
const yamlContent = match[1];
|
|
55
|
+
|
|
56
|
+
// Check if this is a control flow block
|
|
57
|
+
const typeMatch = yamlContent.match(/^type:\s*(while|for_each|for|switch|parallel|try|if|map|filter|reduce)/m);
|
|
58
|
+
if (typeMatch) {
|
|
59
|
+
const type = typeMatch[1] as WorkflowStep['type'];
|
|
60
|
+
const id = `control-flow-${type}-${stepIndex++}`;
|
|
61
|
+
|
|
62
|
+
// Extract common properties
|
|
63
|
+
const conditionMatch = yamlContent.match(/condition:\s*["'](.+?)["']/);
|
|
64
|
+
const itemsMatch = yamlContent.match(/items:\s*["'](.+?)["']/);
|
|
65
|
+
const maxIterMatch = yamlContent.match(/max_iterations:\s*(\d+)/);
|
|
66
|
+
const expressionMatch = yamlContent.match(/expression:\s*["'](.+?)["']/);
|
|
67
|
+
const itemVarMatch = yamlContent.match(/item_variable:\s*(\w+)/);
|
|
68
|
+
|
|
69
|
+
// Extract switch expression
|
|
70
|
+
const switchExprMatch = yamlContent.match(/expression:\s*["'](.+?)["']/);
|
|
71
|
+
|
|
72
|
+
// Extract variables for while loops
|
|
73
|
+
const variables: Record<string, { initial: unknown }> = {};
|
|
74
|
+
const varsMatch = yamlContent.match(/variables:\s*\n((?: .*\n)*)/);
|
|
75
|
+
if (varsMatch) {
|
|
76
|
+
const varsContent = varsMatch[1];
|
|
77
|
+
const varLines = varsContent.split('\n').filter(Boolean);
|
|
78
|
+
varLines.forEach(line => {
|
|
79
|
+
const varMatch = line.match(/^\s+(\w+):\s*$/);
|
|
80
|
+
if (varMatch) {
|
|
81
|
+
const varName = varMatch[1];
|
|
82
|
+
// Try to find initial value on next line
|
|
83
|
+
const initMatch = varsContent.match(new RegExp(`${varName}:\\s*\\n\\s+initial:\\s*(.+)`));
|
|
84
|
+
if (initMatch) {
|
|
85
|
+
try {
|
|
86
|
+
variables[varName] = { initial: JSON.parse(initMatch[1]) };
|
|
87
|
+
} catch {
|
|
88
|
+
variables[varName] = { initial: initMatch[1].trim() };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build step data based on type
|
|
96
|
+
const step: WorkflowStep = {
|
|
97
|
+
id,
|
|
98
|
+
type,
|
|
99
|
+
name: type === 'map' ? 'Map Transform' :
|
|
100
|
+
type === 'filter' ? 'Filter Transform' :
|
|
101
|
+
type === 'reduce' ? 'Reduce Transform' :
|
|
102
|
+
type === 'switch' ? 'Switch/Case' :
|
|
103
|
+
type === 'parallel' ? 'Parallel Execution' :
|
|
104
|
+
type === 'try' ? 'Try/Catch' :
|
|
105
|
+
type === 'if' ? 'If/Else' :
|
|
106
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} Loop`,
|
|
107
|
+
inputs: {},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Add type-specific properties
|
|
111
|
+
if (conditionMatch) step.condition = conditionMatch[1];
|
|
112
|
+
if (itemsMatch) step.items = itemsMatch[1];
|
|
113
|
+
if (maxIterMatch) step.maxIterations = parseInt(maxIterMatch[1], 10);
|
|
114
|
+
if (Object.keys(variables).length > 0) step.variables = variables;
|
|
115
|
+
|
|
116
|
+
// Transform-specific properties
|
|
117
|
+
if (type === 'map' || type === 'filter' || type === 'reduce') {
|
|
118
|
+
if (itemVarMatch) {
|
|
119
|
+
step.inputs = { ...step.inputs, itemVariable: itemVarMatch[1] };
|
|
120
|
+
}
|
|
121
|
+
if (expressionMatch) {
|
|
122
|
+
step.inputs = { ...step.inputs, expression: expressionMatch[1] };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Switch-specific properties
|
|
127
|
+
if (type === 'switch' && switchExprMatch) {
|
|
128
|
+
step.inputs = { ...step.inputs, expression: switchExprMatch[1] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
controlFlowSteps.push(step);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return controlFlowSteps;
|
|
136
|
+
}
|
|
137
|
+
|
|
34
138
|
/**
|
|
35
139
|
* Converts a marktoflow Workflow to React Flow nodes and edges
|
|
36
140
|
*/
|
|
37
|
-
export function workflowToGraph(workflow: Workflow): GraphResult {
|
|
141
|
+
export function workflowToGraph(workflow: Workflow & { markdown?: string }): GraphResult {
|
|
38
142
|
const nodes: Node[] = [];
|
|
39
143
|
const edges: Edge[] = [];
|
|
40
144
|
|
|
41
|
-
const VERTICAL_SPACING =
|
|
145
|
+
const VERTICAL_SPACING = 180;
|
|
42
146
|
const HORIZONTAL_OFFSET = 250;
|
|
43
147
|
let currentY = 0;
|
|
44
148
|
|
|
149
|
+
// Try to extract control flow from markdown if available
|
|
150
|
+
const controlFlowSteps = extractControlFlowFromMarkdown(workflow.markdown);
|
|
151
|
+
const allSteps = [...workflow.steps, ...controlFlowSteps];
|
|
152
|
+
|
|
45
153
|
// Add trigger node if triggers are defined
|
|
46
154
|
if (workflow.triggers && workflow.triggers.length > 0) {
|
|
47
155
|
const trigger = workflow.triggers[0]; // Primary trigger
|
|
@@ -65,11 +173,11 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
|
|
|
65
173
|
currentY += VERTICAL_SPACING;
|
|
66
174
|
|
|
67
175
|
// Edge from trigger to first step
|
|
68
|
-
if (
|
|
176
|
+
if (allSteps.length > 0) {
|
|
69
177
|
edges.push({
|
|
70
|
-
id: `e-${triggerId}-${
|
|
178
|
+
id: `e-${triggerId}-${allSteps[0].id}`,
|
|
71
179
|
source: triggerId,
|
|
72
|
-
target:
|
|
180
|
+
target: allSteps[0].id,
|
|
73
181
|
type: 'smoothstep',
|
|
74
182
|
animated: false,
|
|
75
183
|
style: { stroke: '#ff6d5a', strokeWidth: 2 },
|
|
@@ -78,30 +186,101 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
|
|
|
78
186
|
}
|
|
79
187
|
|
|
80
188
|
// Create nodes for each step
|
|
81
|
-
|
|
189
|
+
allSteps.forEach((step, index) => {
|
|
82
190
|
const isSubWorkflow = !!step.workflow;
|
|
191
|
+
const isControlFlow = !!step.type && ['while', 'for_each', 'for', 'switch', 'parallel', 'try', 'if', 'map', 'filter', 'reduce'].includes(step.type);
|
|
192
|
+
|
|
193
|
+
let nodeType = 'step';
|
|
194
|
+
if (isSubWorkflow) {
|
|
195
|
+
nodeType = 'subworkflow';
|
|
196
|
+
} else if (isControlFlow) {
|
|
197
|
+
nodeType = step.type!;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Build node data based on type
|
|
201
|
+
const baseData = {
|
|
202
|
+
id: step.id,
|
|
203
|
+
name: step.name,
|
|
204
|
+
action: step.action,
|
|
205
|
+
workflowPath: step.workflow,
|
|
206
|
+
status: 'pending' as const,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Add control-flow specific data
|
|
210
|
+
let nodeData = { ...baseData };
|
|
211
|
+
if (step.type === 'while') {
|
|
212
|
+
nodeData = {
|
|
213
|
+
...baseData,
|
|
214
|
+
condition: step.condition || 'true',
|
|
215
|
+
maxIterations: step.maxIterations || 100,
|
|
216
|
+
variables: step.variables,
|
|
217
|
+
};
|
|
218
|
+
} else if (step.type === 'for_each' || step.type === 'for') {
|
|
219
|
+
nodeData = {
|
|
220
|
+
...baseData,
|
|
221
|
+
items: step.items || '[]',
|
|
222
|
+
itemVariable: step.inputs?.itemVariable as string,
|
|
223
|
+
};
|
|
224
|
+
} else if (step.type === 'switch') {
|
|
225
|
+
nodeData = {
|
|
226
|
+
...baseData,
|
|
227
|
+
expression: step.inputs?.expression as string || step.condition || '',
|
|
228
|
+
cases: {},
|
|
229
|
+
hasDefault: true,
|
|
230
|
+
};
|
|
231
|
+
} else if (step.type === 'parallel') {
|
|
232
|
+
nodeData = {
|
|
233
|
+
...baseData,
|
|
234
|
+
branches: [],
|
|
235
|
+
maxConcurrent: 0,
|
|
236
|
+
};
|
|
237
|
+
} else if (step.type === 'try') {
|
|
238
|
+
nodeData = {
|
|
239
|
+
...baseData,
|
|
240
|
+
hasCatch: true,
|
|
241
|
+
hasFinally: false,
|
|
242
|
+
};
|
|
243
|
+
} else if (step.type === 'if') {
|
|
244
|
+
nodeData = {
|
|
245
|
+
...baseData,
|
|
246
|
+
condition: step.condition || 'true',
|
|
247
|
+
hasElse: true,
|
|
248
|
+
};
|
|
249
|
+
} else if (step.type === 'map' || step.type === 'filter' || step.type === 'reduce') {
|
|
250
|
+
nodeData = {
|
|
251
|
+
...baseData,
|
|
252
|
+
transformType: step.type,
|
|
253
|
+
items: step.items || '[]',
|
|
254
|
+
itemVariable: step.inputs?.itemVariable as string,
|
|
255
|
+
expression: step.inputs?.expression as string,
|
|
256
|
+
condition: step.condition,
|
|
257
|
+
};
|
|
258
|
+
} else {
|
|
259
|
+
// Regular step
|
|
260
|
+
nodeData = {
|
|
261
|
+
...baseData,
|
|
262
|
+
condition: step.condition,
|
|
263
|
+
items: step.items,
|
|
264
|
+
maxIterations: step.maxIterations,
|
|
265
|
+
variables: step.variables,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
83
268
|
|
|
84
269
|
const node: Node = {
|
|
85
270
|
id: step.id,
|
|
86
|
-
type:
|
|
271
|
+
type: nodeType,
|
|
87
272
|
position: {
|
|
88
273
|
x: HORIZONTAL_OFFSET,
|
|
89
274
|
y: currentY + index * VERTICAL_SPACING,
|
|
90
275
|
},
|
|
91
|
-
data:
|
|
92
|
-
id: step.id,
|
|
93
|
-
name: step.name,
|
|
94
|
-
action: step.action,
|
|
95
|
-
workflowPath: step.workflow,
|
|
96
|
-
status: 'pending',
|
|
97
|
-
},
|
|
276
|
+
data: nodeData,
|
|
98
277
|
};
|
|
99
278
|
|
|
100
279
|
nodes.push(node);
|
|
101
280
|
|
|
102
281
|
// Create edge to next step
|
|
103
|
-
if (index <
|
|
104
|
-
const nextStep =
|
|
282
|
+
if (index < allSteps.length - 1) {
|
|
283
|
+
const nextStep = allSteps[index + 1];
|
|
105
284
|
const edge: Edge = {
|
|
106
285
|
id: `e-${step.id}-${nextStep.id}`,
|
|
107
286
|
source: step.id,
|
|
@@ -120,16 +299,61 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
|
|
|
120
299
|
|
|
121
300
|
edges.push(edge);
|
|
122
301
|
}
|
|
302
|
+
|
|
303
|
+
// Add loop-back edge for loops
|
|
304
|
+
if (step.type === 'while' || step.type === 'for_each' || step.type === 'for') {
|
|
305
|
+
const loopColor = step.type === 'while' ? '#fb923c' : '#f093fb';
|
|
306
|
+
edges.push({
|
|
307
|
+
id: `e-${step.id}-loop-back`,
|
|
308
|
+
source: step.id,
|
|
309
|
+
target: step.id,
|
|
310
|
+
sourceHandle: 'loop-back',
|
|
311
|
+
type: 'smoothstep',
|
|
312
|
+
animated: true,
|
|
313
|
+
style: {
|
|
314
|
+
stroke: loopColor,
|
|
315
|
+
strokeWidth: 2,
|
|
316
|
+
strokeDasharray: '5,5',
|
|
317
|
+
},
|
|
318
|
+
label: step.type === 'while' ? 'while true' : 'for each item',
|
|
319
|
+
labelStyle: { fill: loopColor, fontSize: 9 },
|
|
320
|
+
labelBgStyle: { fill: '#1a1a2e', fillOpacity: 0.9 },
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Add iteration indicator for transform operations (map/filter/reduce)
|
|
325
|
+
if (step.type === 'map' || step.type === 'filter' || step.type === 'reduce') {
|
|
326
|
+
const transformColor = '#14b8a6';
|
|
327
|
+
const label = step.type === 'map' ? 'transform each' :
|
|
328
|
+
step.type === 'filter' ? 'test each' :
|
|
329
|
+
'accumulate';
|
|
330
|
+
edges.push({
|
|
331
|
+
id: `e-${step.id}-transform-flow`,
|
|
332
|
+
source: step.id,
|
|
333
|
+
target: step.id,
|
|
334
|
+
sourceHandle: 'loop-back',
|
|
335
|
+
type: 'smoothstep',
|
|
336
|
+
animated: true,
|
|
337
|
+
style: {
|
|
338
|
+
stroke: transformColor,
|
|
339
|
+
strokeWidth: 1.5,
|
|
340
|
+
strokeDasharray: '3,3',
|
|
341
|
+
},
|
|
342
|
+
label,
|
|
343
|
+
labelStyle: { fill: transformColor, fontSize: 8 },
|
|
344
|
+
labelBgStyle: { fill: '#1a1a2e', fillOpacity: 0.9 },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
123
347
|
});
|
|
124
348
|
|
|
125
349
|
// Add output node at the end
|
|
126
|
-
if (
|
|
350
|
+
if (allSteps.length > 0) {
|
|
127
351
|
const outputId = `output-${workflow.metadata.id}`;
|
|
128
|
-
const lastStep =
|
|
129
|
-
const outputY = currentY +
|
|
352
|
+
const lastStep = allSteps[allSteps.length - 1];
|
|
353
|
+
const outputY = currentY + allSteps.length * VERTICAL_SPACING;
|
|
130
354
|
|
|
131
355
|
// Collect all output variables
|
|
132
|
-
const outputVariables =
|
|
356
|
+
const outputVariables = allSteps
|
|
133
357
|
.filter((s) => s.outputVariable)
|
|
134
358
|
.map((s) => s.outputVariable as string);
|
|
135
359
|
|
|
@@ -157,7 +381,7 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
|
|
|
157
381
|
}
|
|
158
382
|
|
|
159
383
|
// Add data flow edges based on variable references
|
|
160
|
-
const variableEdges = findVariableDependencies(
|
|
384
|
+
const variableEdges = findVariableDependencies(allSteps);
|
|
161
385
|
edges.push(...variableEdges);
|
|
162
386
|
|
|
163
387
|
return { nodes, edges };
|