Skip to main content

Workflow Designer

Interactive Drag-and-Drop Agent Workflow Builder

Document ID: E5-WORKFLOW-DESIGNER
Version: 1.0
Category: P5 - Interactive Learning
Format: React JSX Component


Component Overview

The Workflow Designer provides a visual drag-and-drop interface for building agentic workflows. Users can connect paradigm nodes, configure agents, and export workflow definitions for implementation.


JSX Component

import React, { useState, useCallback } from 'react';
import {
Plus, Trash2, Download, Upload, Play, Settings,
ArrowRight, Database, Brain, Target, Shield,
GitBranch, Users, Zap, CheckCircle, AlertCircle,
ChevronDown, ChevronRight, X, Save, Copy
} from 'lucide-react';

const WorkflowDesigner = () => {
const [nodes, setNodes] = useState([
{ id: 'start', type: 'start', x: 50, y: 200, label: 'Start' },
{ id: 'end', type: 'end', x: 750, y: 200, label: 'End' }
]);
const [connections, setConnections] = useState([]);
const [selectedNode, setSelectedNode] = useState(null);
const [draggingNode, setDraggingNode] = useState(null);
const [connectingFrom, setConnectingFrom] = useState(null);
const [showNodePalette, setShowNodePalette] = useState(true);
const [workflowName, setWorkflowName] = useState('New Workflow');

const nodeTypes = {
start: { label: 'Start', icon: Play, color: 'green', connectable: { in: false, out: true } },
end: { label: 'End', icon: CheckCircle, color: 'green', connectable: { in: true, out: false } },
lsr: { label: 'LSR Agent', icon: Brain, color: 'purple', paradigm: 'LSR', connectable: { in: true, out: true } },
gs: { label: 'GS Agent', icon: Database, color: 'emerald', paradigm: 'GS', connectable: { in: true, out: true } },
ep: { label: 'EP Agent', icon: Target, color: 'amber', paradigm: 'EP', connectable: { in: true, out: true } },
ve: { label: 'VE Agent', icon: Shield, color: 'blue', paradigm: 'VE', connectable: { in: true, out: true } },
condition: { label: 'Condition', icon: GitBranch, color: 'gray', connectable: { in: true, out: true } },
parallel: { label: 'Parallel', icon: Users, color: 'indigo', connectable: { in: true, out: true } },
tool: { label: 'Tool Call', icon: Zap, color: 'pink', connectable: { in: true, out: true } }
};

const paletteItems = [
{ type: 'lsr', category: 'Agents' },
{ type: 'gs', category: 'Agents' },
{ type: 'ep', category: 'Agents' },
{ type: 've', category: 'Agents' },
{ type: 'condition', category: 'Control' },
{ type: 'parallel', category: 'Control' },
{ type: 'tool', category: 'Actions' }
];

const getColorClass = (color, variant = 'bg') => {
const colors = {
green: { bg: 'bg-green-500', border: 'border-green-500', light: 'bg-green-100' },
purple: { bg: 'bg-purple-500', border: 'border-purple-500', light: 'bg-purple-100' },
emerald: { bg: 'bg-emerald-500', border: 'border-emerald-500', light: 'bg-emerald-100' },
amber: { bg: 'bg-amber-500', border: 'border-amber-500', light: 'bg-amber-100' },
blue: { bg: 'bg-blue-500', border: 'border-blue-500', light: 'bg-blue-100' },
gray: { bg: 'bg-gray-500', border: 'border-gray-500', light: 'bg-gray-100' },
indigo: { bg: 'bg-indigo-500', border: 'border-indigo-500', light: 'bg-indigo-100' },
pink: { bg: 'bg-pink-500', border: 'border-pink-500', light: 'bg-pink-100' }
};
return colors[color]?.[variant] || colors.gray[variant];
};

const addNode = (type, x = 300, y = 200) => {
const nodeType = nodeTypes[type];
const newNode = {
id: `${type}_${Date.now()}`,
type,
x,
y,
label: nodeType.label,
config: getDefaultConfig(type)
};
setNodes([...nodes, newNode]);
};

const getDefaultConfig = (type) => {
switch (type) {
case 'lsr':
return { temperature: 0.7, maxTokens: 2048, systemPrompt: '' };
case 'gs':
return { sources: [], minConfidence: 0.7, maxResults: 5 };
case 'ep':
return { maxIterations: 5, reflexionEnabled: true };
case 've':
return { protocol: '', auditLevel: 'standard' };
case 'condition':
return { expression: '', trueLabel: 'Yes', falseLabel: 'No' };
case 'parallel':
return { waitForAll: true };
case 'tool':
return { toolName: '', parameters: {} };
default:
return {};
}
};

const deleteNode = (nodeId) => {
if (nodeId === 'start' || nodeId === 'end') return;
setNodes(nodes.filter(n => n.id !== nodeId));
setConnections(connections.filter(c => c.from !== nodeId && c.to !== nodeId));
if (selectedNode === nodeId) setSelectedNode(null);
};

const handleNodeDrag = (e, nodeId) => {
if (draggingNode === nodeId) {
const rect = e.currentTarget.parentElement.getBoundingClientRect();
const x = e.clientX - rect.left - 60;
const y = e.clientY - rect.top - 25;

setNodes(nodes.map(n =>
n.id === nodeId ? { ...n, x: Math.max(0, x), y: Math.max(0, y) } : n
));
}
};

const startConnection = (nodeId) => {
const node = nodes.find(n => n.id === nodeId);
if (nodeTypes[node.type].connectable.out) {
setConnectingFrom(nodeId);
}
};

const endConnection = (nodeId) => {
if (connectingFrom && connectingFrom !== nodeId) {
const targetNode = nodes.find(n => n.id === nodeId);
if (nodeTypes[targetNode.type].connectable.in) {
const existingConnection = connections.find(
c => c.from === connectingFrom && c.to === nodeId
);
if (!existingConnection) {
setConnections([...connections, {
id: `conn_${Date.now()}`,
from: connectingFrom,
to: nodeId,
label: ''
}]);
}
}
}
setConnectingFrom(null);
};

const deleteConnection = (connId) => {
setConnections(connections.filter(c => c.id !== connId));
};

const updateNodeConfig = (nodeId, key, value) => {
setNodes(nodes.map(n =>
n.id === nodeId ? { ...n, config: { ...n.config, [key]: value } } : n
));
};

const exportWorkflow = () => {
const workflow = {
name: workflowName,
version: '1.0',
nodes: nodes.map(n => ({
id: n.id,
type: n.type,
label: n.label,
config: n.config,
position: { x: n.x, y: n.y }
})),
connections: connections.map(c => ({
from: c.from,
to: c.to,
label: c.label
})),
exportedAt: new Date().toISOString()
};

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 = `${workflowName.toLowerCase().replace(/\s+/g, '-')}.json`;
a.click();
};

const renderConnection = (conn) => {
const fromNode = nodes.find(n => n.id === conn.from);
const toNode = nodes.find(n => n.id === conn.to);
if (!fromNode || !toNode) return null;

const x1 = fromNode.x + 120;
const y1 = fromNode.y + 25;
const x2 = toNode.x;
const y2 = toNode.y + 25;

const midX = (x1 + x2) / 2;
const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;

return (
<g key={conn.id} className="group">
<path
d={path}
fill="none"
stroke="#6B7280"
strokeWidth="2"
className="cursor-pointer hover:stroke-blue-500"
onClick={() => deleteConnection(conn.id)}
/>
<circle
cx={midX}
cy={(y1 + y2) / 2}
r="8"
fill="white"
stroke="#6B7280"
strokeWidth="2"
className="opacity-0 group-hover:opacity-100 cursor-pointer"
onClick={() => deleteConnection(conn.id)}
/>
<text
x={midX}
y={(y1 + y2) / 2 + 4}
textAnchor="middle"
className="text-xs fill-red-500 opacity-0 group-hover:opacity-100 pointer-events-none"
>
×
</text>
</g>
);
};

const selectedNodeData = nodes.find(n => n.id === selectedNode);

return (
<div className="h-screen flex flex-col bg-gray-100">
{/* Header */}
<div className="bg-white border-b px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<input
type="text"
value={workflowName}
onChange={(e) => setWorkflowName(e.target.value)}
className="text-xl font-bold bg-transparent border-b border-transparent hover:border-gray-300 focus:border-blue-500 focus:outline-none px-1"
/>
<span className="text-sm text-gray-500">{nodes.length - 2} nodes</span>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowNodePalette(!showNodePalette)}
className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
>
<Plus className="w-4 h-4" />
Add Node
</button>
<button
onClick={exportWorkflow}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
<Download className="w-4 h-4" />
Export
</button>
</div>
</div>

<div className="flex-1 flex overflow-hidden">
{/* Node Palette */}
{showNodePalette && (
<div className="w-64 bg-white border-r p-4 overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium text-gray-700">Node Palette</h3>
<button
onClick={() => setShowNodePalette(false)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4 text-gray-400" />
</button>
</div>

{['Agents', 'Control', 'Actions'].map(category => (
<div key={category} className="mb-4">
<h4 className="text-xs uppercase text-gray-400 mb-2">{category}</h4>
<div className="space-y-2">
{paletteItems
.filter(item => item.category === category)
.map(item => {
const nodeType = nodeTypes[item.type];
const Icon = nodeType.icon;
return (
<button
key={item.type}
onClick={() => addNode(item.type)}
className={`w-full flex items-center gap-3 p-3 rounded-lg border-2 border-dashed hover:border-solid transition-all ${getColorClass(nodeType.color, 'border')} ${getColorClass(nodeType.color, 'light')}`}
>
<Icon className={`w-5 h-5 ${getColorClass(nodeType.color, 'bg').replace('bg-', 'text-')}`} />
<span className="text-sm font-medium text-gray-700">{nodeType.label}</span>
</button>
);
})}
</div>
</div>
))}

<div className="mt-6 p-3 bg-blue-50 rounded-lg text-sm text-blue-700">
<p className="font-medium mb-1">💡 Tips</p>
<ul className="text-xs space-y-1 text-blue-600">
<li>• Drag nodes to position them</li>
<li>• Click output port to start connection</li>
<li>• Click node to configure</li>
<li>• Hover connection to delete</li>
</ul>
</div>
</div>
)}

{/* Canvas */}
<div
className="flex-1 relative overflow-auto"
onMouseMove={(e) => draggingNode && handleNodeDrag(e, draggingNode)}
onMouseUp={() => setDraggingNode(null)}
onMouseLeave={() => { setDraggingNode(null); setConnectingFrom(null); }}
>
<svg className="absolute inset-0 w-full h-full pointer-events-none">
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="1" fill="#E5E7EB" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />

{/* Connections */}
<g className="pointer-events-auto">
{connections.map(renderConnection)}
</g>
</svg>

{/* Nodes */}
{nodes.map(node => {
const nodeType = nodeTypes[node.type];
const Icon = nodeType.icon;
const isSelected = selectedNode === node.id;

return (
<div
key={node.id}
className={`absolute cursor-move select-none transition-shadow ${
isSelected ? 'z-10' : ''
}`}
style={{ left: node.x, top: node.y }}
onMouseDown={(e) => {
e.stopPropagation();
setDraggingNode(node.id);
setSelectedNode(node.id);
}}
>
<div className={`
flex items-center gap-2 px-4 py-2 rounded-lg border-2 bg-white shadow-sm
${isSelected ? 'ring-2 ring-blue-500 shadow-lg' : 'hover:shadow-md'}
${getColorClass(nodeType.color, 'border')}
`}>
{/* Input Port */}
{nodeType.connectable.in && (
<div
className={`absolute -left-2 top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 bg-white cursor-pointer hover:scale-125 transition-transform ${
connectingFrom ? 'ring-2 ring-blue-400' : ''
} ${getColorClass(nodeType.color, 'border')}`}
onClick={(e) => { e.stopPropagation(); endConnection(node.id); }}
/>
)}

<Icon className={`w-5 h-5 ${getColorClass(nodeType.color, 'bg').replace('bg-', 'text-')}`} />
<span className="font-medium text-gray-700 whitespace-nowrap">{node.label}</span>

{/* Delete Button */}
{node.type !== 'start' && node.type !== 'end' && isSelected && (
<button
onClick={(e) => { e.stopPropagation(); deleteNode(node.id); }}
className="ml-2 p-1 hover:bg-red-100 rounded text-red-500"
>
<Trash2 className="w-3 h-3" />
</button>
)}

{/* Output Port */}
{nodeType.connectable.out && (
<div
className={`absolute -right-2 top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 bg-white cursor-pointer hover:scale-125 transition-transform ${
connectingFrom === node.id ? 'ring-2 ring-blue-400 scale-125' : ''
} ${getColorClass(nodeType.color, 'border')}`}
onClick={(e) => { e.stopPropagation(); startConnection(node.id); }}
/>
)}
</div>
</div>
);
})}
</div>

{/* Properties Panel */}
{selectedNode && selectedNodeData && (
<div className="w-80 bg-white border-l p-4 overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium text-gray-700">Properties</h3>
<button
onClick={() => setSelectedNode(null)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4 text-gray-400" />
</button>
</div>

<div className="space-y-4">
{/* Node Label */}
<div>
<label className="block text-sm text-gray-500 mb-1">Label</label>
<input
type="text"
value={selectedNodeData.label}
onChange={(e) => setNodes(nodes.map(n =>
n.id === selectedNode ? { ...n, label: e.target.value } : n
))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>

{/* Type-specific config */}
{selectedNodeData.type === 'lsr' && (
<>
<div>
<label className="block text-sm text-gray-500 mb-1">Temperature</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={selectedNodeData.config.temperature}
onChange={(e) => updateNodeConfig(selectedNode, 'temperature', parseFloat(e.target.value))}
className="w-full"
/>
<span className="text-sm text-gray-400">{selectedNodeData.config.temperature}</span>
</div>
<div>
<label className="block text-sm text-gray-500 mb-1">System Prompt</label>
<textarea
value={selectedNodeData.config.systemPrompt}
onChange={(e) => updateNodeConfig(selectedNode, 'systemPrompt', e.target.value)}
className="w-full px-3 py-2 border rounded-lg h-24"
placeholder="Enter system prompt..."
/>
</div>
</>
)}

{selectedNodeData.type === 'gs' && (
<>
<div>
<label className="block text-sm text-gray-500 mb-1">Min Confidence</label>
<input
type="range"
min="0.5"
max="0.95"
step="0.05"
value={selectedNodeData.config.minConfidence}
onChange={(e) => updateNodeConfig(selectedNode, 'minConfidence', parseFloat(e.target.value))}
className="w-full"
/>
<span className="text-sm text-gray-400">{selectedNodeData.config.minConfidence}</span>
</div>
<div>
<label className="block text-sm text-gray-500 mb-1">Max Results</label>
<input
type="number"
value={selectedNodeData.config.maxResults}
onChange={(e) => updateNodeConfig(selectedNode, 'maxResults', parseInt(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</>
)}

{selectedNodeData.type === 'ep' && (
<>
<div>
<label className="block text-sm text-gray-500 mb-1">Max Iterations</label>
<input
type="number"
value={selectedNodeData.config.maxIterations}
onChange={(e) => updateNodeConfig(selectedNode, 'maxIterations', parseInt(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedNodeData.config.reflexionEnabled}
onChange={(e) => updateNodeConfig(selectedNode, 'reflexionEnabled', e.target.checked)}
className="rounded"
/>
<label className="text-sm text-gray-500">Enable Reflexion</label>
</div>
</>
)}

{selectedNodeData.type === 've' && (
<>
<div>
<label className="block text-sm text-gray-500 mb-1">Protocol</label>
<input
type="text"
value={selectedNodeData.config.protocol}
onChange={(e) => updateNodeConfig(selectedNode, 'protocol', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
placeholder="Protocol name..."
/>
</div>
<div>
<label className="block text-sm text-gray-500 mb-1">Audit Level</label>
<select
value={selectedNodeData.config.auditLevel}
onChange={(e) => updateNodeConfig(selectedNode, 'auditLevel', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="minimal">Minimal</option>
<option value="standard">Standard</option>
<option value="full">Full</option>
</select>
</div>
</>
)}

{selectedNodeData.type === 'condition' && (
<div>
<label className="block text-sm text-gray-500 mb-1">Condition Expression</label>
<textarea
value={selectedNodeData.config.expression}
onChange={(e) => updateNodeConfig(selectedNode, 'expression', e.target.value)}
className="w-full px-3 py-2 border rounded-lg h-24 font-mono text-sm"
placeholder="e.g., result.confidence > 0.8"
/>
</div>
)}

{selectedNodeData.type === 'tool' && (
<div>
<label className="block text-sm text-gray-500 mb-1">Tool Name</label>
<select
value={selectedNodeData.config.toolName}
onChange={(e) => updateNodeConfig(selectedNode, 'toolName', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="">Select tool...</option>
<option value="web_search">Web Search</option>
<option value="knowledge_search">Knowledge Search</option>
<option value="database_query">Database Query</option>
<option value="api_call">API Call</option>
<option value="email_send">Email Send</option>
</select>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
};

export default WorkflowDesigner;

Features

  • Drag-and-Drop Canvas: Visual workflow building with node positioning
  • Node Types: Agent paradigms (LSR, GS, EP, VE), control flow (condition, parallel), actions (tool calls)
  • Connection System: Click ports to create connections between nodes
  • Properties Panel: Configure node-specific settings
  • Export: Download workflow as JSON for implementation
  • Visual Feedback: Color-coded nodes, selected state, connection highlights

Exported Workflow Format

{
"name": "Customer Support Workflow",
"version": "1.0",
"nodes": [
{
"id": "gs_1",
"type": "gs",
"label": "Knowledge Search",
"config": {
"sources": ["knowledge_base"],
"minConfidence": 0.7,
"maxResults": 5
},
"position": { "x": 200, "y": 150 }
}
],
"connections": [
{ "from": "start", "to": "gs_1" },
{ "from": "gs_1", "to": "end" }
],
"exportedAt": "2025-01-15T10:30:00Z"
}

Component maintained by CODITECT Education Team. Feedback: education@coditect.com