llama1's picture
Upload 781 files
5da4770 verified
'use client';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import {
ReactFlow,
Background,
Edge,
Node,
ProOptions,
ReactFlowProvider,
useNodesState,
useEdgesState,
addEdge,
Connection,
Controls,
Panel,
BackgroundVariant,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import './index.css';
import useLayout from './hooks/use-layout';
import nodeTypes from './node-types';
import edgeTypes from './edge-types';
import { ConditionalStep } from '@/components/agents/workflows/conditional-workflow-builder';
import { uuid } from './utils';
import { convertWorkflowToReactFlow, convertReactFlowToWorkflow } from './utils/conversion';
import { Button } from '@/components/ui/button';
import { Plus, GitBranch } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { toast } from 'sonner';
const proOptions: ProOptions = { account: 'paid-pro', hideAttribution: true };
interface WorkflowBuilderProps {
steps: ConditionalStep[];
onStepsChange: (steps: ConditionalStep[]) => void;
agentTools?: {
agentpress_tools: Array<{ name: string; description: string; icon?: string; enabled: boolean }>;
mcp_tools: Array<{ name: string; description: string; icon?: string; server?: string }>;
};
isLoadingTools?: boolean;
}
function WorkflowBuilderInner({
steps,
onStepsChange,
agentTools,
isLoadingTools
}: WorkflowBuilderProps) {
useLayout();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onNodesChangeDebug = useCallback((changes: any) => {
onNodesChange(changes);
}, [onNodesChange]);
const onEdgesChangeDebug = useCallback((changes: any) => {
onEdgesChange(changes);
}, [onEdgesChange]);
const [isInternalUpdate, setIsInternalUpdate] = useState(false);
const [selectedNode, setSelectedNode] = useState<string | null>(null);
const lastConvertedSteps = useRef<string>('');
useEffect(() => {
if (!isInternalUpdate && nodes.length > 0) {
const convertedSteps = convertReactFlowToWorkflow(nodes, edges);
const convertedStepsStr = JSON.stringify(convertedSteps);
if (convertedStepsStr !== lastConvertedSteps.current) {
lastConvertedSteps.current = convertedStepsStr;
onStepsChange(convertedSteps);
}
}
}, [nodes, edges, isInternalUpdate, onStepsChange]);
useEffect(() => {
setIsInternalUpdate(true);
if (steps.length > 0) {
const { nodes: convertedNodes, edges: convertedEdges } = convertWorkflowToReactFlow(steps);
const nodesWithTools = convertedNodes.map(node => ({
...node,
data: {
...node.data,
onDelete: handleNodeDelete,
agentTools,
isLoadingTools,
}
}));
setNodes(nodesWithTools);
setEdges(convertedEdges);
} else if (nodes.length === 0) {
const defaultNodes: Node[] = [
{
id: '1',
data: {
name: 'Start',
description: 'Click to add steps or use the Add Node button',
onDelete: handleNodeDelete,
agentTools,
isLoadingTools,
},
position: { x: 0, y: 0 },
type: 'step',
},
];
setNodes(defaultNodes);
}
setTimeout(() => {
setIsInternalUpdate(false);
}, 100);
}, []);
const onConnect = useCallback(
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const handleNodeDelete = useCallback((nodeId: string) => {
setNodes((nds) => nds.filter((node) => node.id !== nodeId));
setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
}, [setNodes, setEdges]);
const handleNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
setSelectedNode(node.id);
}, []);
const handlePaneClick = useCallback(() => {
setSelectedNode(null);
}, []);
const addNewStep = useCallback(() => {
console.log('=== ADDING NEW STEP ===');
const newNodeId = uuid();
const existingNodes = nodes;
let position = { x: 0, y: 0 };
let sourceNode = null;
if (selectedNode) {
sourceNode = nodes.find(n => n.id === selectedNode);
if (sourceNode) {
position = { x: sourceNode.position.x, y: sourceNode.position.y + 150 };
}
} else if (existingNodes.length > 0) {
const bottomNode = existingNodes.reduce((prev, current) =>
prev.position.y > current.position.y ? prev : current
);
sourceNode = bottomNode;
position = { x: bottomNode.position.x, y: bottomNode.position.y + 150 };
}
const newNode: Node = {
id: newNodeId,
type: 'step',
position,
data: {
name: 'New Step',
description: '',
hasIssues: true,
onDelete: handleNodeDelete,
agentTools,
isLoadingTools,
},
};
setNodes((nds) => {
const newNodes = [...nds, newNode];
return newNodes;
});
if (sourceNode) {
const newEdge = {
id: `${sourceNode.id}->${newNodeId}`,
source: sourceNode.id,
target: newNodeId,
type: 'workflow',
};
setEdges((eds) => {
const existingEdge = eds.find(e => e.id === newEdge.id);
if (existingEdge) {
return eds;
}
const newEdges = [...eds, newEdge];
return newEdges;
});
}
}, [nodes, selectedNode, handleNodeDelete, setNodes, setEdges, agentTools, isLoadingTools]);
const addConditionBranch = useCallback((type: 'if' | 'if-else' | 'if-elseif-else') => {
if (!selectedNode) {
toast.error('Please select a node first to add conditions');
return;
}
const sourceNode = nodes.find(n => n.id === selectedNode);
if (!sourceNode) return;
const conditions: Array<{ type: 'if' | 'elseif' | 'else'; id: string }> = [];
if (type === 'if') {
conditions.push({ type: 'if', id: uuid() });
} else if (type === 'if-else') {
conditions.push({ type: 'if', id: uuid() });
conditions.push({ type: 'else', id: uuid() });
} else {
conditions.push({ type: 'if', id: uuid() });
conditions.push({ type: 'elseif', id: uuid() });
conditions.push({ type: 'else', id: uuid() });
}
const newNodes: Node[] = [];
const newEdges: Edge[] = [];
const xSpacing = 300;
const startX = sourceNode.position.x - ((conditions.length - 1) * xSpacing / 2);
conditions.forEach((condition, index) => {
const conditionNode: Node = {
id: condition.id,
type: 'condition',
position: {
x: startX + index * xSpacing,
y: sourceNode.position.y + 150,
},
data: {
conditionType: condition.type,
expression: condition.type !== 'else' ? '' : undefined,
onDelete: handleNodeDelete,
},
};
newNodes.push(conditionNode);
const conditionEdge = {
id: `${sourceNode.id}->${condition.id}`,
source: sourceNode.id,
target: condition.id,
type: 'workflow',
label: condition.type === 'if' ? 'if' :
condition.type === 'elseif' ? 'else if' : 'else',
labelStyle: { fill: '#666', fontSize: 12 },
labelBgStyle: { fill: '#fff' },
};
if (!newEdges.find(e => e.id === conditionEdge.id)) {
newEdges.push(conditionEdge);
}
});
setNodes((nds) => [...nds, ...newNodes]);
setEdges((eds) => {
const combinedEdges = [...eds, ...newEdges];
const uniqueEdges = combinedEdges.filter((edge, index, self) =>
index === self.findIndex(e => e.id === edge.id)
);
return uniqueEdges;
});
}, [selectedNode, nodes, handleNodeDelete, setNodes, setEdges]);
return (
<div className="h-full w-full border rounded-none border-none bg-muted/10 react-flow-container">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChangeDebug}
onEdgesChange={onEdgesChangeDebug}
onConnect={onConnect}
onNodeClick={handleNodeClick}
onPaneClick={handlePaneClick}
proOptions={proOptions}
fitView
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitViewOptions={{ padding: 0.2 }}
minZoom={0.5}
maxZoom={1.5}
nodesDraggable={false}
nodesConnectable={false}
zoomOnDoubleClick={false}
deleteKeyCode={null}
>
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
<Controls />
<Panel position="top-left" className="react-flow__panel">
<div className="workflow-panel-actions">
<Button
variant="outline"
size="sm"
onClick={addNewStep}
>
<Plus className="h-4 w-4" />
Add Step
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<GitBranch className="h-4 w-4" />
Add Condition
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => addConditionBranch('if')}>
<span className="text-xs font-mono mr-2">if</span>
Single condition
</DropdownMenuItem>
<DropdownMenuItem onClick={() => addConditionBranch('if-else')}>
<span className="text-xs font-mono mr-2">if-else</span>
Two branches
</DropdownMenuItem>
<DropdownMenuItem onClick={() => addConditionBranch('if-elseif-else')}>
<span className="text-xs font-mono mr-2">if-elif-else</span>
Three branches
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{selectedNode && (
<div className="workflow-panel-selection">
Node selected: {nodes.find(n => n.id === selectedNode)?.data.name || selectedNode}
</div>
)}
</Panel>
</ReactFlow>
</div>
);
}
export function WorkflowBuilder(props: WorkflowBuilderProps) {
return (
<ReactFlowProvider>
<WorkflowBuilderInner {...props} />
</ReactFlowProvider>
);
}