jay-code
Version:
Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability
1,401 lines (1,219 loc) • 45.7 kB
JavaScript
/**
* Advanced Workflow Designer for Claude Flow
* Visual drag-and-drop workflow builder with real-time execution
*/
class WorkflowDesigner {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.canvas = null;
this.ctx = null;
this.nodes = new Map();
this.connections = new Map();
this.selectedNode = null;
this.draggedNode = null;
this.dragOffset = { x: 0, y: 0 };
this.connectionStart = null;
this.executionState = new Map();
this.isExecuting = false;
this.zoom = 1;
this.pan = { x: 0, y: 0 };
this.init();
}
init() {
this.createUI();
this.setupEventListeners();
this.loadTemplates();
this.setupNodePalette();
}
createUI() {
this.container.innerHTML = `
<div class="workflow-designer">
<div class="toolbar">
<div class="toolbar-section">
<button class="btn btn-primary" id="saveWorkflow">
<i class="fas fa-save"></i> Save
</button>
<button class="btn btn-secondary" id="loadWorkflow">
<i class="fas fa-folder-open"></i> Load
</button>
<button class="btn btn-success" id="exportWorkflow">
<i class="fas fa-download"></i> Export
</button>
<button class="btn btn-info" id="importWorkflow">
<i class="fas fa-upload"></i> Import
</button>
<input type="file" id="importFile" accept=".json" style="display: none;">
</div>
<div class="toolbar-section">
<button class="btn btn-warning" id="validateWorkflow">
<i class="fas fa-check-circle"></i> Validate
</button>
<button class="btn btn-success" id="executeWorkflow">
<i class="fas fa-play"></i> Execute
</button>
<button class="btn btn-danger" id="stopWorkflow">
<i class="fas fa-stop"></i> Stop
</button>
</div>
<div class="toolbar-section">
<button class="btn btn-secondary" id="zoomIn">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-secondary" id="zoomOut">
<i class="fas fa-search-minus"></i>
</button>
<button class="btn btn-secondary" id="zoomReset">
<i class="fas fa-expand-arrows-alt"></i>
</button>
<button class="btn btn-secondary" id="clearCanvas">
<i class="fas fa-trash"></i> Clear
</button>
</div>
</div>
<div class="designer-body">
<div class="node-palette">
<div class="palette-header">
<h3>Components</h3>
<div class="palette-search">
<input type="text" placeholder="Search components..." id="paletteSearch">
</div>
</div>
<div class="palette-content">
<div class="palette-category" data-category="input">
<h4>Input</h4>
<div class="palette-items"></div>
</div>
<div class="palette-category" data-category="processing">
<h4>Processing</h4>
<div class="palette-items"></div>
</div>
<div class="palette-category" data-category="output">
<h4>Output</h4>
<div class="palette-items"></div>
</div>
<div class="palette-category" data-category="control">
<h4>Control Flow</h4>
<div class="palette-items"></div>
</div>
<div class="palette-category" data-category="ai">
<h4>AI Operations</h4>
<div class="palette-items"></div>
</div>
</div>
</div>
<div class="canvas-container">
<canvas id="workflowCanvas" width="1200" height="800"></canvas>
<div class="canvas-overlay">
<div class="execution-status" id="executionStatus"></div>
</div>
</div>
<div class="properties-panel">
<div class="panel-header">
<h3>Properties</h3>
</div>
<div class="panel-content" id="propertiesContent">
<div class="no-selection">
<p>Select a node to edit properties</p>
</div>
</div>
</div>
</div>
<div class="bottom-panel">
<div class="panel-tabs">
<button class="tab-button active" data-tab="templates">Templates</button>
<button class="tab-button" data-tab="execution">Execution Log</button>
<button class="tab-button" data-tab="validation">Validation</button>
</div>
<div class="panel-content">
<div class="tab-content active" id="templatesTab">
<div class="template-gallery"></div>
</div>
<div class="tab-content" id="executionTab">
<div class="execution-log"></div>
</div>
<div class="tab-content" id="validationTab">
<div class="validation-results"></div>
</div>
</div>
</div>
</div>
`;
this.canvas = document.getElementById('workflowCanvas');
this.ctx = this.canvas.getContext('2d');
this.setupCanvas();
}
setupCanvas() {
// Set up high-DPI canvas
const rect = this.canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
this.canvas.width = rect.width * dpr;
this.canvas.height = rect.height * dpr;
this.ctx.scale(dpr, dpr);
this.canvas.style.width = rect.width + 'px';
this.canvas.style.height = rect.height + 'px';
this.draw();
}
setupEventListeners() {
// Canvas events
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('wheel', this.handleWheel.bind(this));
this.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this));
// Toolbar events
document.getElementById('saveWorkflow').addEventListener('click', this.saveWorkflow.bind(this));
document.getElementById('loadWorkflow').addEventListener('click', this.loadWorkflow.bind(this));
document
.getElementById('exportWorkflow')
.addEventListener('click', this.exportWorkflow.bind(this));
document
.getElementById('importWorkflow')
.addEventListener('click', this.importWorkflow.bind(this));
document
.getElementById('importFile')
.addEventListener('change', this.handleFileImport.bind(this));
document
.getElementById('validateWorkflow')
.addEventListener('click', this.validateWorkflow.bind(this));
document
.getElementById('executeWorkflow')
.addEventListener('click', this.executeWorkflow.bind(this));
document.getElementById('stopWorkflow').addEventListener('click', this.stopWorkflow.bind(this));
document
.getElementById('zoomIn')
.addEventListener('click', () => this.setZoom(this.zoom * 1.2));
document
.getElementById('zoomOut')
.addEventListener('click', () => this.setZoom(this.zoom / 1.2));
document.getElementById('zoomReset').addEventListener('click', () => this.setZoom(1));
document.getElementById('clearCanvas').addEventListener('click', this.clearCanvas.bind(this));
// Palette search
document
.getElementById('paletteSearch')
.addEventListener('input', this.filterPalette.bind(this));
// Tab switching
document.querySelectorAll('.tab-button').forEach((button) => {
button.addEventListener('click', this.switchTab.bind(this));
});
// Window resize
window.addEventListener('resize', this.handleResize.bind(this));
}
setupNodePalette() {
const nodeTypes = {
input: [
{ type: 'file-input', name: 'File Input', icon: 'fas fa-file-import' },
{ type: 'text-input', name: 'Text Input', icon: 'fas fa-keyboard' },
{ type: 'url-input', name: 'URL Input', icon: 'fas fa-link' },
{ type: 'api-input', name: 'API Input', icon: 'fas fa-cloud-download-alt' },
],
processing: [
{ type: 'transform', name: 'Transform', icon: 'fas fa-exchange-alt' },
{ type: 'filter', name: 'Filter', icon: 'fas fa-filter' },
{ type: 'aggregate', name: 'Aggregate', icon: 'fas fa-layer-group' },
{ type: 'sort', name: 'Sort', icon: 'fas fa-sort' },
],
output: [
{ type: 'file-output', name: 'File Output', icon: 'fas fa-file-export' },
{ type: 'display', name: 'Display', icon: 'fas fa-desktop' },
{ type: 'api-output', name: 'API Output', icon: 'fas fa-cloud-upload-alt' },
{ type: 'notification', name: 'Notification', icon: 'fas fa-bell' },
],
control: [
{ type: 'condition', name: 'Condition', icon: 'fas fa-code-branch' },
{ type: 'loop', name: 'Loop', icon: 'fas fa-redo' },
{ type: 'delay', name: 'Delay', icon: 'fas fa-clock' },
{ type: 'parallel', name: 'Parallel', icon: 'fas fa-stream' },
],
ai: [
{ type: 'ai-analyze', name: 'AI Analyze', icon: 'fas fa-brain' },
{ type: 'ai-generate', name: 'AI Generate', icon: 'fas fa-magic' },
{ type: 'ai-classify', name: 'AI Classify', icon: 'fas fa-tags' },
{ type: 'ai-summarize', name: 'AI Summarize', icon: 'fas fa-compress-alt' },
],
};
Object.entries(nodeTypes).forEach(([category, nodes]) => {
const container = document.querySelector(`[data-category="${category}"] .palette-items`);
nodes.forEach((node) => {
const item = document.createElement('div');
item.className = 'palette-item';
item.draggable = true;
item.dataset.nodeType = node.type;
item.innerHTML = `
<i class="${node.icon}"></i>
<span>${node.name}</span>
`;
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', node.type);
e.dataTransfer.effectAllowed = 'copy';
});
container.appendChild(item);
});
});
// Canvas drop support
this.canvas.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
this.canvas.addEventListener('drop', (e) => {
e.preventDefault();
const nodeType = e.dataTransfer.getData('text/plain');
const rect = this.canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - this.pan.x) / this.zoom;
const y = (e.clientY - rect.top - this.pan.y) / this.zoom;
this.createNode(nodeType, x, y);
});
}
createNode(type, x, y) {
const nodeId = 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const node = {
id: nodeId,
type: type,
x: x,
y: y,
width: 150,
height: 80,
inputs: this.getNodeInputs(type),
outputs: this.getNodeOutputs(type),
properties: this.getNodeProperties(type),
status: 'idle',
};
this.nodes.set(nodeId, node);
this.draw();
this.showProperties(node);
return node;
}
getNodeInputs(type) {
const inputs = {
'file-input': [],
'text-input': [],
'url-input': [],
'api-input': [],
transform: [{ name: 'data', type: 'any' }],
filter: [{ name: 'data', type: 'any' }],
aggregate: [{ name: 'data', type: 'array' }],
sort: [{ name: 'data', type: 'array' }],
'file-output': [{ name: 'data', type: 'any' }],
display: [{ name: 'data', type: 'any' }],
'api-output': [{ name: 'data', type: 'any' }],
notification: [{ name: 'message', type: 'string' }],
condition: [
{ name: 'condition', type: 'boolean' },
{ name: 'data', type: 'any' },
],
loop: [{ name: 'data', type: 'array' }],
delay: [{ name: 'data', type: 'any' }],
parallel: [{ name: 'data', type: 'array' }],
'ai-analyze': [{ name: 'data', type: 'any' }],
'ai-generate': [{ name: 'prompt', type: 'string' }],
'ai-classify': [{ name: 'data', type: 'any' }],
'ai-summarize': [{ name: 'data', type: 'string' }],
};
return inputs[type] || [];
}
getNodeOutputs(type) {
const outputs = {
'file-input': [{ name: 'data', type: 'any' }],
'text-input': [{ name: 'text', type: 'string' }],
'url-input': [{ name: 'data', type: 'any' }],
'api-input': [{ name: 'data', type: 'any' }],
transform: [{ name: 'result', type: 'any' }],
filter: [{ name: 'result', type: 'any' }],
aggregate: [{ name: 'result', type: 'any' }],
sort: [{ name: 'result', type: 'array' }],
'file-output': [],
display: [],
'api-output': [{ name: 'response', type: 'any' }],
notification: [],
condition: [
{ name: 'true', type: 'any' },
{ name: 'false', type: 'any' },
],
loop: [{ name: 'result', type: 'array' }],
delay: [{ name: 'data', type: 'any' }],
parallel: [{ name: 'results', type: 'array' }],
'ai-analyze': [{ name: 'analysis', type: 'object' }],
'ai-generate': [{ name: 'generated', type: 'string' }],
'ai-classify': [{ name: 'categories', type: 'array' }],
'ai-summarize': [{ name: 'summary', type: 'string' }],
};
return outputs[type] || [];
}
getNodeProperties(type) {
const properties = {
'file-input': { path: '', format: 'auto' },
'text-input': { value: '', multiline: false },
'url-input': { url: '', method: 'GET', headers: {} },
'api-input': { endpoint: '', method: 'GET', headers: {}, body: '' },
transform: { expression: '', language: 'javascript' },
filter: { condition: '', language: 'javascript' },
aggregate: { operation: 'sum', field: '' },
sort: { field: '', order: 'asc' },
'file-output': { path: '', format: 'auto' },
display: { format: 'table', title: '' },
'api-output': { endpoint: '', method: 'POST', headers: {} },
notification: { type: 'info', title: '' },
condition: { expression: '', language: 'javascript' },
loop: { type: 'forEach', condition: '' },
delay: { duration: 1000, unit: 'ms' },
parallel: { maxConcurrency: 5 },
'ai-analyze': { model: 'gpt-4', temperature: 0.7 },
'ai-generate': { model: 'gpt-4', temperature: 0.7, maxTokens: 1000 },
'ai-classify': { model: 'gpt-4', categories: [] },
'ai-summarize': { model: 'gpt-4', length: 'medium' },
};
return properties[type] || {};
}
handleMouseDown(e) {
const rect = this.canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - this.pan.x) / this.zoom;
const y = (e.clientY - rect.top - this.pan.y) / this.zoom;
// Check for node selection
const node = this.getNodeAt(x, y);
if (node) {
this.selectedNode = node;
this.draggedNode = node;
this.dragOffset = { x: x - node.x, y: y - node.y };
this.showProperties(node);
} else {
this.selectedNode = null;
this.showProperties(null);
}
// Check for connection point
const connectionPoint = this.getConnectionPointAt(x, y);
if (connectionPoint) {
this.connectionStart = connectionPoint;
}
this.draw();
}
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - this.pan.x) / this.zoom;
const y = (e.clientY - rect.top - this.pan.y) / this.zoom;
if (this.draggedNode) {
this.draggedNode.x = x - this.dragOffset.x;
this.draggedNode.y = y - this.dragOffset.y;
this.draw();
}
if (this.connectionStart) {
this.tempConnection = { x, y };
this.draw();
}
}
handleMouseUp(e) {
const rect = this.canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - this.pan.x) / this.zoom;
const y = (e.clientY - rect.top - this.pan.y) / this.zoom;
if (this.connectionStart) {
const connectionEnd = this.getConnectionPointAt(x, y);
if (connectionEnd && connectionEnd.node !== this.connectionStart.node) {
this.createConnection(this.connectionStart, connectionEnd);
}
this.connectionStart = null;
this.tempConnection = null;
this.draw();
}
this.draggedNode = null;
}
handleWheel(e) {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const wheelDelta = e.deltaY < 0 ? 1.1 : 0.9;
const newZoom = Math.max(0.1, Math.min(3, this.zoom * wheelDelta));
this.pan.x = x - (x - this.pan.x) * (newZoom / this.zoom);
this.pan.y = y - (y - this.pan.y) * (newZoom / this.zoom);
this.zoom = newZoom;
this.draw();
}
handleContextMenu(e) {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - this.pan.x) / this.zoom;
const y = (e.clientY - rect.top - this.pan.y) / this.zoom;
const node = this.getNodeAt(x, y);
if (node) {
this.showContextMenu(e.clientX, e.clientY, node);
}
}
getNodeAt(x, y) {
for (let node of this.nodes.values()) {
if (x >= node.x && x <= node.x + node.width && y >= node.y && y <= node.y + node.height) {
return node;
}
}
return null;
}
getConnectionPointAt(x, y) {
for (let node of this.nodes.values()) {
// Check input points
const inputPoints = this.getInputPoints(node);
for (let i = 0; i < inputPoints.length; i++) {
const point = inputPoints[i];
if (Math.abs(x - point.x) < 8 && Math.abs(y - point.y) < 8) {
return { node, type: 'input', index: i };
}
}
// Check output points
const outputPoints = this.getOutputPoints(node);
for (let i = 0; i < outputPoints.length; i++) {
const point = outputPoints[i];
if (Math.abs(x - point.x) < 8 && Math.abs(y - point.y) < 8) {
return { node, type: 'output', index: i };
}
}
}
return null;
}
getInputPoints(node) {
const points = [];
const inputCount = node.inputs.length;
for (let i = 0; i < inputCount; i++) {
points.push({
x: node.x,
y: node.y + (node.height / (inputCount + 1)) * (i + 1),
});
}
return points;
}
getOutputPoints(node) {
const points = [];
const outputCount = node.outputs.length;
for (let i = 0; i < outputCount; i++) {
points.push({
x: node.x + node.width,
y: node.y + (node.height / (outputCount + 1)) * (i + 1),
});
}
return points;
}
createConnection(start, end) {
if (start.type === 'output' && end.type === 'input') {
const connectionId = `${start.node.id}_${start.index}_${end.node.id}_${end.index}`;
this.connections.set(connectionId, {
id: connectionId,
from: start.node.id,
fromIndex: start.index,
to: end.node.id,
toIndex: end.index,
});
}
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.save();
this.ctx.translate(this.pan.x, this.pan.y);
this.ctx.scale(this.zoom, this.zoom);
// Draw grid
this.drawGrid();
// Draw connections
this.drawConnections();
// Draw temporary connection
if (this.tempConnection && this.connectionStart) {
this.drawTempConnection();
}
// Draw nodes
this.drawNodes();
this.ctx.restore();
}
drawGrid() {
const gridSize = 20;
const canvasWidth = this.canvas.width / this.zoom;
const canvasHeight = this.canvas.height / this.zoom;
this.ctx.strokeStyle = '#e0e0e0';
this.ctx.lineWidth = 0.5;
for (let x = 0; x <= canvasWidth; x += gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, canvasHeight);
this.ctx.stroke();
}
for (let y = 0; y <= canvasHeight; y += gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(canvasWidth, y);
this.ctx.stroke();
}
}
drawNodes() {
for (let node of this.nodes.values()) {
this.drawNode(node);
}
}
drawNode(node) {
const isSelected = this.selectedNode === node;
const isExecuting = node.status === 'executing';
const hasError = node.status === 'error';
// Node body
this.ctx.fillStyle = isSelected ? '#e3f2fd' : '#ffffff';
this.ctx.strokeStyle = isSelected ? '#2196f3' : hasError ? '#f44336' : '#cccccc';
this.ctx.lineWidth = isSelected ? 2 : 1;
this.ctx.fillRect(node.x, node.y, node.width, node.height);
this.ctx.strokeRect(node.x, node.y, node.width, node.height);
// Node title
this.ctx.fillStyle = '#333333';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(this.getNodeTitle(node.type), node.x + node.width / 2, node.y + 20);
// Status indicator
if (isExecuting) {
this.ctx.fillStyle = '#ff9800';
this.ctx.beginPath();
this.ctx.arc(node.x + node.width - 10, node.y + 10, 4, 0, 2 * Math.PI);
this.ctx.fill();
} else if (hasError) {
this.ctx.fillStyle = '#f44336';
this.ctx.beginPath();
this.ctx.arc(node.x + node.width - 10, node.y + 10, 4, 0, 2 * Math.PI);
this.ctx.fill();
}
// Input/Output points
this.drawConnectionPoints(node);
}
drawConnectionPoints(node) {
// Input points
const inputPoints = this.getInputPoints(node);
inputPoints.forEach((point, index) => {
this.ctx.fillStyle = '#4caf50';
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
this.ctx.fill();
// Input label
this.ctx.fillStyle = '#666666';
this.ctx.font = '10px Arial';
this.ctx.textAlign = 'right';
this.ctx.fillText(node.inputs[index].name, point.x - 8, point.y + 3);
});
// Output points
const outputPoints = this.getOutputPoints(node);
outputPoints.forEach((point, index) => {
this.ctx.fillStyle = '#2196f3';
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
this.ctx.fill();
// Output label
this.ctx.fillStyle = '#666666';
this.ctx.font = '10px Arial';
this.ctx.textAlign = 'left';
this.ctx.fillText(node.outputs[index].name, point.x + 8, point.y + 3);
});
}
drawConnections() {
for (let connection of this.connections.values()) {
const fromNode = this.nodes.get(connection.from);
const toNode = this.nodes.get(connection.to);
if (fromNode && toNode) {
const fromPoints = this.getOutputPoints(fromNode);
const toPoints = this.getInputPoints(toNode);
const fromPoint = fromPoints[connection.fromIndex];
const toPoint = toPoints[connection.toIndex];
this.drawConnection(fromPoint, toPoint);
}
}
}
drawConnection(from, to) {
const midX = (from.x + to.x) / 2;
this.ctx.strokeStyle = '#666666';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(from.x, from.y);
this.ctx.bezierCurveTo(midX, from.y, midX, to.y, to.x, to.y);
this.ctx.stroke();
// Arrow
const angle = Math.atan2(to.y - from.y, to.x - from.x);
const arrowLength = 8;
this.ctx.beginPath();
this.ctx.moveTo(to.x, to.y);
this.ctx.lineTo(
to.x - arrowLength * Math.cos(angle - Math.PI / 6),
to.y - arrowLength * Math.sin(angle - Math.PI / 6),
);
this.ctx.moveTo(to.x, to.y);
this.ctx.lineTo(
to.x - arrowLength * Math.cos(angle + Math.PI / 6),
to.y - arrowLength * Math.sin(angle + Math.PI / 6),
);
this.ctx.stroke();
}
drawTempConnection() {
const startPoint =
this.connectionStart.type === 'output'
? this.getOutputPoints(this.connectionStart.node)[this.connectionStart.index]
: this.getInputPoints(this.connectionStart.node)[this.connectionStart.index];
this.ctx.strokeStyle = '#2196f3';
this.ctx.lineWidth = 2;
this.ctx.setLineDash([5, 5]);
this.ctx.beginPath();
this.ctx.moveTo(startPoint.x, startPoint.y);
this.ctx.lineTo(this.tempConnection.x, this.tempConnection.y);
this.ctx.stroke();
this.ctx.setLineDash([]);
}
getNodeTitle(type) {
const titles = {
'file-input': 'File Input',
'text-input': 'Text Input',
'url-input': 'URL Input',
'api-input': 'API Input',
transform: 'Transform',
filter: 'Filter',
aggregate: 'Aggregate',
sort: 'Sort',
'file-output': 'File Output',
display: 'Display',
'api-output': 'API Output',
notification: 'Notification',
condition: 'Condition',
loop: 'Loop',
delay: 'Delay',
parallel: 'Parallel',
'ai-analyze': 'AI Analyze',
'ai-generate': 'AI Generate',
'ai-classify': 'AI Classify',
'ai-summarize': 'AI Summarize',
};
return titles[type] || type;
}
showProperties(node) {
const panel = document.getElementById('propertiesContent');
if (!node) {
panel.innerHTML = '<div class="no-selection"><p>Select a node to edit properties</p></div>';
return;
}
const properties = node.properties;
let html = `
<div class="properties-form">
<h4>${this.getNodeTitle(node.type)}</h4>
`;
Object.entries(properties).forEach(([key, value]) => {
html += `
<div class="property-field">
<label>${this.formatPropertyLabel(key)}</label>
${this.renderPropertyInput(key, value, node.id)}
</div>
`;
});
html += '</div>';
panel.innerHTML = html;
// Attach event listeners
panel.querySelectorAll('input, select, textarea').forEach((input) => {
input.addEventListener('change', (e) => {
const propertyKey = e.target.dataset.property;
node.properties[propertyKey] = e.target.value;
});
});
}
formatPropertyLabel(key) {
return key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1');
}
renderPropertyInput(key, value, nodeId) {
const inputId = `${nodeId}_${key}`;
if (typeof value === 'boolean') {
return `
<input type="checkbox" id="${inputId}" data-property="${key}" ${value ? 'checked' : ''}>
`;
} else if (key === 'method') {
return `
<select id="${inputId}" data-property="${key}">
<option value="GET" ${value === 'GET' ? 'selected' : ''}>GET</option>
<option value="POST" ${value === 'POST' ? 'selected' : ''}>POST</option>
<option value="PUT" ${value === 'PUT' ? 'selected' : ''}>PUT</option>
<option value="DELETE" ${value === 'DELETE' ? 'selected' : ''}>DELETE</option>
</select>
`;
} else if (key.includes('expression') || key.includes('body')) {
return `
<textarea id="${inputId}" data-property="${key}" rows="3">${value}</textarea>
`;
} else {
return `
<input type="text" id="${inputId}" data-property="${key}" value="${value}">
`;
}
}
async validateWorkflow() {
const results = {
errors: [],
warnings: [],
info: [],
};
// Check for isolated nodes
const connectedNodes = new Set();
for (let connection of this.connections.values()) {
connectedNodes.add(connection.from);
connectedNodes.add(connection.to);
}
for (let node of this.nodes.values()) {
if (!connectedNodes.has(node.id) && this.nodes.size > 1) {
results.warnings.push(`Node "${this.getNodeTitle(node.type)}" is not connected`);
}
}
// Check for circular dependencies
if (this.hasCircularDependencies()) {
results.errors.push('Circular dependencies detected in workflow');
}
// Check required properties
for (let node of this.nodes.values()) {
const required = this.getRequiredProperties(node.type);
for (let prop of required) {
if (!node.properties[prop] || node.properties[prop] === '') {
results.errors.push(
`Node "${this.getNodeTitle(node.type)}" missing required property: ${prop}`,
);
}
}
}
this.showValidationResults(results);
return results.errors.length === 0;
}
hasCircularDependencies() {
const visited = new Set();
const visiting = new Set();
const visit = (nodeId) => {
if (visiting.has(nodeId)) return true;
if (visited.has(nodeId)) return false;
visiting.add(nodeId);
for (let connection of this.connections.values()) {
if (connection.from === nodeId) {
if (visit(connection.to)) return true;
}
}
visiting.delete(nodeId);
visited.add(nodeId);
return false;
};
for (let nodeId of this.nodes.keys()) {
if (visit(nodeId)) return true;
}
return false;
}
getRequiredProperties(type) {
const required = {
'file-input': ['path'],
'url-input': ['url'],
'api-input': ['endpoint'],
transform: ['expression'],
filter: ['condition'],
'file-output': ['path'],
'api-output': ['endpoint'],
condition: ['expression'],
};
return required[type] || [];
}
showValidationResults(results) {
const panel = document.getElementById('validationTab');
const content = panel.querySelector('.validation-results') || panel;
let html = '<div class="validation-results">';
if (results.errors.length > 0) {
html += '<div class="validation-section errors">';
html += '<h4><i class="fas fa-exclamation-circle"></i> Errors</h4>';
html += '<ul>';
results.errors.forEach((error) => {
html += `<li class="error">${error}</li>`;
});
html += '</ul></div>';
}
if (results.warnings.length > 0) {
html += '<div class="validation-section warnings">';
html += '<h4><i class="fas fa-exclamation-triangle"></i> Warnings</h4>';
html += '<ul>';
results.warnings.forEach((warning) => {
html += `<li class="warning">${warning}</li>`;
});
html += '</ul></div>';
}
if (results.info.length > 0) {
html += '<div class="validation-section info">';
html += '<h4><i class="fas fa-info-circle"></i> Information</h4>';
html += '<ul>';
results.info.forEach((info) => {
html += `<li class="info">${info}</li>`;
});
html += '</ul></div>';
}
if (results.errors.length === 0 && results.warnings.length === 0) {
html += '<div class="validation-success">';
html += '<i class="fas fa-check-circle"></i> Workflow validation passed';
html += '</div>';
}
html += '</div>';
content.innerHTML = html;
// Switch to validation tab
this.switchTabToValidation();
}
async executeWorkflow() {
if (!(await this.validateWorkflow())) {
alert('Workflow validation failed. Please fix errors before executing.');
return;
}
this.isExecuting = true;
this.executionState.clear();
document.getElementById('executeWorkflow').disabled = true;
document.getElementById('stopWorkflow').disabled = false;
const executionLog = document.querySelector('#executionTab .execution-log');
executionLog.innerHTML = '';
try {
const startNodes = this.getStartNodes();
const executionPromises = startNodes.map((node) => this.executeNode(node));
await Promise.all(executionPromises);
this.logExecution('Workflow execution completed successfully', 'success');
} catch (error) {
this.logExecution(`Workflow execution failed: ${error.message}`, 'error');
} finally {
this.isExecuting = false;
document.getElementById('executeWorkflow').disabled = false;
document.getElementById('stopWorkflow').disabled = true;
this.draw();
}
}
getStartNodes() {
const hasInput = new Set();
for (let connection of this.connections.values()) {
hasInput.add(connection.to);
}
return Array.from(this.nodes.values()).filter((node) => !hasInput.has(node.id));
}
async executeNode(node) {
if (!this.isExecuting) return;
node.status = 'executing';
this.draw();
this.logExecution(`Executing node: ${this.getNodeTitle(node.type)}`, 'info');
try {
const result = await this.processNode(node);
node.status = 'completed';
this.executionState.set(node.id, result);
this.logExecution(`Node completed: ${this.getNodeTitle(node.type)}`, 'success');
// Execute connected nodes
const connectedNodes = this.getConnectedNodes(node.id);
for (let connectedNode of connectedNodes) {
await this.executeNode(connectedNode);
}
} catch (error) {
node.status = 'error';
this.logExecution(`Node failed: ${this.getNodeTitle(node.type)} - ${error.message}`, 'error');
throw error;
}
}
async processNode(node) {
// Simulate node processing
await new Promise((resolve) => setTimeout(resolve, 500 + Math.random() * 1000));
switch (node.type) {
case 'file-input':
return { type: 'file', path: node.properties.path };
case 'text-input':
return { type: 'text', value: node.properties.value };
case 'transform':
return { type: 'transformed', data: 'transformed_data' };
case 'ai-analyze':
return { type: 'analysis', confidence: 0.95, insights: [] };
default:
return { type: 'generic', processed: true };
}
}
getConnectedNodes(nodeId) {
const connected = [];
for (let connection of this.connections.values()) {
if (connection.from === nodeId) {
const node = this.nodes.get(connection.to);
if (node) connected.push(node);
}
}
return connected;
}
logExecution(message, type) {
const log = document.querySelector('#executionTab .execution-log');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.innerHTML = `
<span class="timestamp">${new Date().toLocaleTimeString()}</span>
<span class="message">${message}</span>
`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
stopWorkflow() {
this.isExecuting = false;
// Reset all node statuses
for (let node of this.nodes.values()) {
if (node.status === 'executing') {
node.status = 'idle';
}
}
this.logExecution('Workflow execution stopped by user', 'warning');
this.draw();
}
saveWorkflow() {
const workflow = {
nodes: Array.from(this.nodes.values()),
connections: Array.from(this.connections.values()),
metadata: {
version: '1.0',
created: new Date().toISOString(),
name: prompt('Enter workflow name:') || 'Untitled Workflow',
},
};
const saved = JSON.parse(localStorage.getItem('claudeflow_workflows') || '[]');
saved.push(workflow);
localStorage.setItem('claudeflow_workflows', JSON.stringify(saved));
alert('Workflow saved successfully!');
}
loadWorkflow() {
const saved = JSON.parse(localStorage.getItem('claudeflow_workflows') || '[]');
if (saved.length === 0) {
alert('No saved workflows found');
return;
}
const names = saved.map((w, i) => `${i + 1}. ${w.metadata.name}`);
const choice = prompt(`Select workflow to load:\n${names.join('\n')}`);
if (choice) {
const index = parseInt(choice) - 1;
if (index >= 0 && index < saved.length) {
this.loadWorkflowData(saved[index]);
}
}
}
loadWorkflowData(workflow) {
this.nodes.clear();
this.connections.clear();
workflow.nodes.forEach((nodeData) => {
this.nodes.set(nodeData.id, { ...nodeData });
});
workflow.connections.forEach((connectionData) => {
this.connections.set(connectionData.id, { ...connectionData });
});
this.draw();
}
exportWorkflow() {
const workflow = {
nodes: Array.from(this.nodes.values()),
connections: Array.from(this.connections.values()),
metadata: {
version: '1.0',
exported: new Date().toISOString(),
name: prompt('Enter workflow name:') || 'Exported Workflow',
},
};
const blob = new Blob([JSON.stringify(workflow, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${workflow.metadata.name}.json`;
a.click();
URL.revokeObjectURL(url);
}
importWorkflow() {
document.getElementById('importFile').click();
}
handleFileImport(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const workflow = JSON.parse(event.target.result);
this.loadWorkflowData(workflow);
alert('Workflow imported successfully!');
} catch (error) {
alert('Error importing workflow: ' + error.message);
}
};
reader.readAsText(file);
}
loadTemplates() {
const templates = [
{
name: 'Data Processing Pipeline',
description: 'Process files through transformation and analysis',
nodes: [
{ type: 'file-input', x: 50, y: 100 },
{ type: 'transform', x: 250, y: 100 },
{ type: 'ai-analyze', x: 450, y: 100 },
{ type: 'file-output', x: 650, y: 100 },
],
},
{
name: 'Content Generation',
description: 'Generate and process content using AI',
nodes: [
{ type: 'text-input', x: 50, y: 100 },
{ type: 'ai-generate', x: 250, y: 100 },
{ type: 'ai-summarize', x: 450, y: 100 },
{ type: 'display', x: 650, y: 100 },
],
},
{
name: 'API Integration',
description: 'Fetch, process, and send data via APIs',
nodes: [
{ type: 'api-input', x: 50, y: 100 },
{ type: 'filter', x: 250, y: 100 },
{ type: 'transform', x: 450, y: 100 },
{ type: 'api-output', x: 650, y: 100 },
],
},
];
const gallery = document.querySelector('.template-gallery');
gallery.innerHTML = templates
.map(
(template) => `
<div class="template-card" data-template="${template.name}">
<h4>${template.name}</h4>
<p>${template.description}</p>
<button class="btn btn-primary load-template">Load Template</button>
</div>
`,
)
.join('');
// Add event listeners
gallery.querySelectorAll('.load-template').forEach((btn) => {
btn.addEventListener('click', (e) => {
const templateName = e.target.closest('.template-card').dataset.template;
const template = templates.find((t) => t.name === templateName);
if (template) {
this.loadTemplate(template);
}
});
});
}
loadTemplate(template) {
this.clearCanvas();
const nodeMap = new Map();
// Create nodes
template.nodes.forEach((nodeData, index) => {
const node = this.createNode(nodeData.type, nodeData.x, nodeData.y);
nodeMap.set(index, node);
});
// Create connections (if defined in template)
if (template.connections) {
template.connections.forEach((conn) => {
const fromNode = nodeMap.get(conn.from);
const toNode = nodeMap.get(conn.to);
if (fromNode && toNode) {
this.createConnection(
{ node: fromNode, type: 'output', index: conn.fromIndex || 0 },
{ node: toNode, type: 'input', index: conn.toIndex || 0 },
);
}
});
}
this.draw();
}
filterPalette(e) {
const filter = e.target.value.toLowerCase();
const items = document.querySelectorAll('.palette-item');
items.forEach((item) => {
const text = item.textContent.toLowerCase();
item.style.display = text.includes(filter) ? 'block' : 'none';
});
}
switchTab(e) {
const targetTab = e.target.dataset.tab;
// Update button states
document.querySelectorAll('.tab-button').forEach((btn) => {
btn.classList.remove('active');
});
e.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach((content) => {
content.classList.remove('active');
});
document.getElementById(targetTab + 'Tab').classList.add('active');
}
switchTabToValidation() {
document.querySelectorAll('.tab-button').forEach((btn) => {
btn.classList.remove('active');
});
document.querySelector('[data-tab="validation"]').classList.add('active');
document.querySelectorAll('.tab-content').forEach((content) => {
content.classList.remove('active');
});
document.getElementById('validationTab').classList.add('active');
}
setZoom(newZoom) {
this.zoom = Math.max(0.1, Math.min(3, newZoom));
this.draw();
}
clearCanvas() {
if (confirm('Are you sure you want to clear the canvas?')) {
this.nodes.clear();
this.connections.clear();
this.selectedNode = null;
this.draw();
}
}
showContextMenu(x, y, node) {
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.left = x + 'px';
menu.style.top = y + 'px';
menu.innerHTML = `
<div class="context-menu-item" data-action="duplicate">
<i class="fas fa-copy"></i> Duplicate
</div>
<div class="context-menu-item" data-action="delete">
<i class="fas fa-trash"></i> Delete
</div>
<div class="context-menu-item" data-action="properties">
<i class="fas fa-cog"></i> Properties
</div>
`;
document.body.appendChild(menu);
menu.addEventListener('click', (e) => {
const action = e.target.dataset.action;
switch (action) {
case 'duplicate':
this.duplicateNode(node);
break;
case 'delete':
this.deleteNode(node);
break;
case 'properties':
this.showProperties(node);
break;
}
document.body.removeChild(menu);
});
// Remove menu when clicking elsewhere
setTimeout(() => {
document.addEventListener('click', function removeMenu() {
if (menu.parentNode) {
document.body.removeChild(menu);
}
document.removeEventListener('click', removeMenu);
});
}, 100);
}
duplicateNode(node) {
const newNode = this.createNode(node.type, node.x + 50, node.y + 50);
newNode.properties = { ...node.properties };
this.draw();
}
deleteNode(node) {
// Remove connections
const connectionsToRemove = [];
for (let [id, connection] of this.connections) {
if (connection.from === node.id || connection.to === node.id) {
connectionsToRemove.push(id);
}
}
connectionsToRemove.forEach((id) => this.connections.delete(id));
// Remove node
this.nodes.delete(node.id);
if (this.selectedNode === node) {
this.selectedNode = null;
this.showProperties(null);
}
this.draw();
}
handleResize() {
this.setupCanvas();
}
}
// Initialize the workflow designer when the page loads
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('workflowDesigner')) {
window.workflowDesigner = new WorkflowDesigner('workflowDesigner');
}
});
// Export for external use
window.WorkflowDesigner = WorkflowDesigner;