The Birth of the Canvas Editor
The heart of XGen is its visual canvas editor. It needs to let users design AI workflows without writing a single line of code, which made an intuitive drag-and-drop node system a hard requirement.
Adopting React Flow
We evaluated several canvas libraries before making a decision.
| Library | Pros | Cons |
|---|---|---|
| React Flow | React-native, custom node support | Learning curve |
| JointJS | Powerful diagramming features | Heavy, licensing |
| Rete.js | Purpose-built for node editors | Small ecosystem |
We ultimately went with React Flow. The deciding factor was the ability to build custom nodes freely as standard React components.
Basic Canvas Structure
import { ReactFlow, Background, Controls, MiniMap } from '@xyflow/react';
const CanvasEditor = () => {
const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useCanvasStore();
return (
<div className="w-full h-full">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={customNodeTypes}
fitView
>
<Background variant="dots" gap={16} />
<Controls />
<MiniMap />
</ReactFlow>
</div>
);
};
Custom Node System
Each AI component is represented as a node. Node types such as ChatOpenAI, VectorStore, and Retriever each have their own distinct UI and configuration options.
// Node type definitions
const customNodeTypes = {
chatOpenAI: ChatOpenAINode,
chatAnthropic: ChatAnthropicNode,
vectorStore: VectorStoreNode,
retriever: RetrieverNode,
agent: AgentNode,
tool: ToolNode,
};
Implementing Drag-and-Drop
We implemented a flow where dragging a node from the sidebar and dropping it onto the canvas creates a new node.
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode = {
id: `${type}_${Date.now()}`,
type,
position,
data: getDefaultNodeData(type),
};
addNode(newNode);
},
[screenToFlowPosition, addNode]
);
Edge Connection Rules
Not every node should be allowed to connect to every other node. We added validation logic to permit connections only when the output type and input type are compatible. This turned out to be more complex than expected, and getting the type system design right was critical.