@marktoflow/gui 2.0.0-alpha.1 → 2.0.0-alpha.12
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/.marktoflow/state/workflow-state.db +0 -0
- package/.marktoflow/state/workflow-state.db-shm +0 -0
- package/.marktoflow/state/workflow-state.db-wal +0 -0
- package/.turbo/turbo-build.log +24 -8
- package/.turbo/turbo-test.log +29 -13
- package/README.md +49 -3
- 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/index.js +93 -33
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/ai.js +38 -1
- package/dist/server/routes/ai.js.map +1 -1
- package/dist/server/routes/execute.js +23 -22
- package/dist/server/routes/execute.js.map +1 -1
- package/dist/server/routes/executions.js +125 -0
- package/dist/server/routes/executions.js.map +1 -0
- package/dist/server/{server/routes → routes}/tools.js +406 -0
- package/dist/server/{server/routes → routes}/tools.js.map +1 -1
- package/dist/server/routes/workflows.js +41 -5
- package/dist/server/routes/workflows.js.map +1 -1
- package/dist/server/services/AIService.js +55 -202
- package/dist/server/services/AIService.js.map +1 -1
- package/dist/server/services/FileWatcher.js +0 -2
- package/dist/server/services/FileWatcher.js.map +1 -1
- package/dist/server/services/WorkflowService.js +199 -16
- package/dist/server/services/WorkflowService.js.map +1 -1
- package/dist/server/services/agents/codex-provider.js +270 -0
- package/dist/server/services/agents/codex-provider.js.map +1 -0
- package/dist/server/{server/services → services}/agents/prompts.js +27 -0
- package/dist/server/services/agents/prompts.js.map +1 -0
- package/dist/server/{server/services → services}/agents/registry.js +20 -0
- package/dist/server/services/agents/registry.js.map +1 -0
- package/dist/server/websocket/index.js +12 -0
- package/dist/server/websocket/index.js.map +1 -1
- package/marktoflow-gui-2.0.0-alpha.12.tgz +0 -0
- package/package.json +19 -5
- package/scripts/flatten-dist.js +69 -0
- package/src/client/components/Canvas/Canvas.tsx +27 -7
- package/src/client/components/Canvas/ExecutionOverlay.tsx +120 -32
- package/src/client/components/Canvas/ForEachNode.tsx +152 -0
- package/src/client/components/Canvas/IfElseNode.tsx +141 -0
- package/src/client/components/Canvas/NodeContextMenu.tsx +8 -4
- package/src/client/components/Canvas/ParallelNode.tsx +157 -0
- package/src/client/components/Canvas/SwitchNode.tsx +185 -0
- package/src/client/components/Canvas/Toolbar.tsx +59 -21
- package/src/client/components/Canvas/TransformNode.tsx +194 -0
- package/src/client/components/Canvas/TryCatchNode.tsx +164 -0
- package/src/client/components/Canvas/WhileNode.tsx +161 -0
- package/src/client/components/Canvas/index.ts +24 -0
- 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/serviceIcons.tsx +33 -0
- package/src/client/utils/workflowToGraph.ts +245 -21
- package/src/server/index.ts +25 -2
- package/src/server/routes/executions.ts +136 -0
- package/src/server/routes/tools.ts +406 -0
- package/src/server/routes/workflows.ts +42 -1
- package/src/server/services/WorkflowService.ts +176 -16
- 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/src/server/websocket/index.ts +13 -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 +308 -0
- package/tests/unit/IfElseNode.test.tsx +235 -0
- package/tests/unit/ParallelNode.test.tsx +344 -0
- package/tests/unit/SwitchNode.test.tsx +327 -0
- package/tests/unit/TransformNode.test.tsx +386 -0
- package/tests/unit/TryCatchNode.test.tsx +243 -0
- package/tests/unit/WhileNode.test.tsx +230 -0
- package/tests/unit/agentStore.test.ts +218 -0
- package/tests/unit/codexProvider.test.ts +399 -0
- package/tests/unit/executionStore.test.ts +40 -0
- package/tests/unit/platform.test.ts +118 -0
- package/tests/unit/serviceIcons.test.ts +197 -0
- package/tests/unit/workflowToGraph.test.ts +22 -0
- 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/server/index.js +0 -95
- package/dist/server/server/index.js.map +0 -1
- package/dist/server/server/routes/ai.js +0 -87
- package/dist/server/server/routes/ai.js.map +0 -1
- package/dist/server/server/routes/execute.js +0 -63
- package/dist/server/server/routes/execute.js.map +0 -1
- package/dist/server/server/routes/workflows.js +0 -99
- package/dist/server/server/routes/workflows.js.map +0 -1
- package/dist/server/server/services/AIService.js +0 -69
- package/dist/server/server/services/AIService.js.map +0 -1
- package/dist/server/server/services/FileWatcher.js +0 -60
- package/dist/server/server/services/FileWatcher.js.map +0 -1
- package/dist/server/server/services/WorkflowService.js +0 -363
- package/dist/server/server/services/WorkflowService.js.map +0 -1
- package/dist/server/server/services/agents/prompts.js.map +0 -1
- package/dist/server/server/services/agents/registry.js.map +0 -1
- package/dist/server/server/websocket/index.js +0 -85
- package/dist/server/server/websocket/index.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/FileWatcher.d.ts +0 -10
- package/dist/server/services/FileWatcher.d.ts.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/websocket/index.d.ts +0 -10
- package/dist/server/websocket/index.d.ts.map +0 -1
- /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/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/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
|
@@ -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 };
|
package/src/server/index.ts
CHANGED
|
@@ -5,11 +5,13 @@ import cors from 'cors';
|
|
|
5
5
|
import { createServer, type Server } from 'http';
|
|
6
6
|
import { Server as SocketIOServer } from 'socket.io';
|
|
7
7
|
import { join } from 'path';
|
|
8
|
-
import { existsSync } from 'fs';
|
|
8
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
9
|
+
import { StateStore } from '@marktoflow/core';
|
|
9
10
|
import { workflowRoutes } from './routes/workflows.js';
|
|
10
11
|
import { aiRoutes } from './routes/ai.js';
|
|
11
12
|
import { executeRoutes } from './routes/execute.js';
|
|
12
13
|
import { toolsRoutes } from './routes/tools.js';
|
|
14
|
+
import { executionRoutes } from './routes/executions.js';
|
|
13
15
|
import { setupWebSocket } from './websocket/index.js';
|
|
14
16
|
import { FileWatcher } from './services/FileWatcher.js';
|
|
15
17
|
|
|
@@ -21,6 +23,17 @@ export interface ServerOptions {
|
|
|
21
23
|
|
|
22
24
|
let httpServer: Server | null = null;
|
|
23
25
|
let fileWatcher: FileWatcher | null = null;
|
|
26
|
+
let stateStore: StateStore | null = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the StateStore instance
|
|
30
|
+
*/
|
|
31
|
+
export function getStateStore(): StateStore {
|
|
32
|
+
if (!stateStore) {
|
|
33
|
+
throw new Error('StateStore not initialized. Call startServer() first.');
|
|
34
|
+
}
|
|
35
|
+
return stateStore;
|
|
36
|
+
}
|
|
24
37
|
|
|
25
38
|
/**
|
|
26
39
|
* Start the GUI server programmatically
|
|
@@ -30,6 +43,11 @@ export async function startServer(options: ServerOptions = {}): Promise<Server>
|
|
|
30
43
|
const WORKFLOW_DIR = options.workflowDir || process.env.WORKFLOW_DIR || process.cwd();
|
|
31
44
|
const STATIC_DIR = options.staticDir || process.env.STATIC_DIR;
|
|
32
45
|
|
|
46
|
+
// Initialize StateStore
|
|
47
|
+
const stateDir = join(WORKFLOW_DIR, '.marktoflow', 'state');
|
|
48
|
+
mkdirSync(stateDir, { recursive: true });
|
|
49
|
+
stateStore = new StateStore(join(stateDir, 'workflow-state.db'));
|
|
50
|
+
|
|
33
51
|
const app = express();
|
|
34
52
|
httpServer = createServer(app);
|
|
35
53
|
const io = new SocketIOServer(httpServer, {
|
|
@@ -47,11 +65,12 @@ export async function startServer(options: ServerOptions = {}): Promise<Server>
|
|
|
47
65
|
app.use('/api/workflows', workflowRoutes);
|
|
48
66
|
app.use('/api/ai', aiRoutes);
|
|
49
67
|
app.use('/api/execute', executeRoutes);
|
|
68
|
+
app.use('/api/executions', executionRoutes);
|
|
50
69
|
app.use('/api/tools', toolsRoutes);
|
|
51
70
|
|
|
52
71
|
// Health check
|
|
53
72
|
app.get('/api/health', (_req, res) => {
|
|
54
|
-
res.json({ status: 'ok', version: '2.0.0-alpha.
|
|
73
|
+
res.json({ status: 'ok', version: '2.0.0-alpha.5' });
|
|
55
74
|
});
|
|
56
75
|
|
|
57
76
|
// Serve static files if static dir is provided
|
|
@@ -94,6 +113,10 @@ export function stopServer(): void {
|
|
|
94
113
|
fileWatcher.stop();
|
|
95
114
|
fileWatcher = null;
|
|
96
115
|
}
|
|
116
|
+
if (stateStore) {
|
|
117
|
+
stateStore.close();
|
|
118
|
+
stateStore = null;
|
|
119
|
+
}
|
|
97
120
|
if (httpServer) {
|
|
98
121
|
httpServer.close();
|
|
99
122
|
httpServer = null;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API routes for execution history
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Router, type Request, type Response } from 'express';
|
|
6
|
+
import { getStateStore } from '../index.js';
|
|
7
|
+
|
|
8
|
+
export const executionRoutes = Router();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /api/executions
|
|
12
|
+
* List all executions with optional filtering
|
|
13
|
+
*/
|
|
14
|
+
executionRoutes.get('/', (req: Request, res: Response) => {
|
|
15
|
+
try {
|
|
16
|
+
const stateStore = getStateStore();
|
|
17
|
+
const { workflowId, status, limit, offset } = req.query;
|
|
18
|
+
|
|
19
|
+
// Helper to extract string from query param
|
|
20
|
+
const getString = (val: unknown): string | undefined => {
|
|
21
|
+
if (typeof val === 'string') return val;
|
|
22
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') return val[0];
|
|
23
|
+
return undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const workflowIdStr = getString(workflowId);
|
|
27
|
+
const statusStr = getString(status);
|
|
28
|
+
const limitNum = parseInt(getString(limit) || '50', 10);
|
|
29
|
+
const offsetNum = getString(offset) ? parseInt(getString(offset)!, 10) : undefined;
|
|
30
|
+
|
|
31
|
+
const executions = stateStore.listExecutions({
|
|
32
|
+
workflowId: workflowIdStr,
|
|
33
|
+
status: statusStr as any,
|
|
34
|
+
limit: limitNum,
|
|
35
|
+
offset: offsetNum,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
res.json(executions);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error listing executions:', error);
|
|
41
|
+
res.status(500).json({
|
|
42
|
+
error: 'Failed to list executions',
|
|
43
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* GET /api/executions/:runId
|
|
50
|
+
* Get details for a specific execution
|
|
51
|
+
*/
|
|
52
|
+
executionRoutes.get('/:runId', (req: Request, res: Response) => {
|
|
53
|
+
try {
|
|
54
|
+
const stateStore = getStateStore();
|
|
55
|
+
const runId = Array.isArray(req.params.runId) ? req.params.runId[0] : req.params.runId;
|
|
56
|
+
|
|
57
|
+
const execution = stateStore.getExecution(runId);
|
|
58
|
+
if (!execution) {
|
|
59
|
+
res.status(404).json({ error: 'Execution not found' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
res.json(execution);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error getting execution:', error);
|
|
66
|
+
res.status(500).json({
|
|
67
|
+
error: 'Failed to get execution',
|
|
68
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* GET /api/executions/:runId/checkpoints
|
|
75
|
+
* Get checkpoints for a specific execution
|
|
76
|
+
*/
|
|
77
|
+
executionRoutes.get('/:runId/checkpoints', (req: Request, res: Response) => {
|
|
78
|
+
try {
|
|
79
|
+
const stateStore = getStateStore();
|
|
80
|
+
const runId = Array.isArray(req.params.runId) ? req.params.runId[0] : req.params.runId;
|
|
81
|
+
|
|
82
|
+
const checkpoints = stateStore.getCheckpoints(runId);
|
|
83
|
+
res.json(checkpoints);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Error getting checkpoints:', error);
|
|
86
|
+
res.status(500).json({
|
|
87
|
+
error: 'Failed to get checkpoints',
|
|
88
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* GET /api/executions/:runId/stats
|
|
95
|
+
* Get execution statistics
|
|
96
|
+
*/
|
|
97
|
+
executionRoutes.get('/:runId/stats', (req: Request, res: Response) => {
|
|
98
|
+
try {
|
|
99
|
+
const stateStore = getStateStore();
|
|
100
|
+
const runId = Array.isArray(req.params.runId) ? req.params.runId[0] : req.params.runId;
|
|
101
|
+
|
|
102
|
+
const execution = stateStore.getExecution(runId);
|
|
103
|
+
if (!execution) {
|
|
104
|
+
res.status(404).json({ error: 'Execution not found' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const checkpoints = stateStore.getCheckpoints(runId);
|
|
109
|
+
const completedSteps = checkpoints.filter((c) => c.status === 'completed').length;
|
|
110
|
+
const failedSteps = checkpoints.filter((c) => c.status === 'failed').length;
|
|
111
|
+
|
|
112
|
+
const stats = {
|
|
113
|
+
runId,
|
|
114
|
+
workflowId: execution.workflowId,
|
|
115
|
+
status: execution.status,
|
|
116
|
+
totalSteps: execution.totalSteps,
|
|
117
|
+
completedSteps,
|
|
118
|
+
failedSteps,
|
|
119
|
+
currentStep: execution.currentStep,
|
|
120
|
+
duration:
|
|
121
|
+
execution.completedAt && execution.startedAt
|
|
122
|
+
? execution.completedAt.getTime() - execution.startedAt.getTime()
|
|
123
|
+
: null,
|
|
124
|
+
startedAt: execution.startedAt,
|
|
125
|
+
completedAt: execution.completedAt,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
res.json(stats);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Error getting execution stats:', error);
|
|
131
|
+
res.status(500).json({
|
|
132
|
+
error: 'Failed to get execution stats',
|
|
133
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|