mirror of
https://github.com/run-llama/flow-maker.git
synced 2026-07-21 02:15:45 -04:00
Add agent flow UI
Implement a drag-and-drop interface using xyflow/react for creating llamaindex-powered agents. Include initial elements: Start, Stop, User Input, Prompt LLM, Prompt Agent, Agent Tool, Splitter, Collector, and Decision. Allow connections between elements.
This commit is contained in:
Generated
+291
-3
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"@xyflow/react": "^12.8.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { memo } from 'react';
|
||||
import { Card } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
Play,
|
||||
Square,
|
||||
MessageCircle,
|
||||
Brain,
|
||||
Bot,
|
||||
Wrench,
|
||||
Split,
|
||||
Merge,
|
||||
GitBranch
|
||||
} from 'lucide-react';
|
||||
|
||||
interface NodeTemplate {
|
||||
type: string;
|
||||
label: string;
|
||||
icon: React.ComponentType<any>;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const nodeTemplates: NodeTemplate[] = [
|
||||
{
|
||||
type: 'start',
|
||||
label: 'Start',
|
||||
icon: Play,
|
||||
description: 'Entry point for the agent flow'
|
||||
},
|
||||
{
|
||||
type: 'stop',
|
||||
label: 'Stop',
|
||||
icon: Square,
|
||||
description: 'End point for the agent flow'
|
||||
},
|
||||
{
|
||||
type: 'userInput',
|
||||
label: 'User Input',
|
||||
icon: MessageCircle,
|
||||
description: 'Capture user input or prompts'
|
||||
},
|
||||
{
|
||||
type: 'promptLLM',
|
||||
label: 'Prompt LLM',
|
||||
icon: Brain,
|
||||
description: 'Send prompts to language models'
|
||||
},
|
||||
{
|
||||
type: 'promptAgent',
|
||||
label: 'Prompt Agent',
|
||||
icon: Bot,
|
||||
description: 'Configure and prompt AI agents'
|
||||
},
|
||||
{
|
||||
type: 'agentTool',
|
||||
label: 'Agent Tool',
|
||||
icon: Wrench,
|
||||
description: 'Tools and functions for agents'
|
||||
},
|
||||
{
|
||||
type: 'splitter',
|
||||
label: 'Splitter',
|
||||
icon: Split,
|
||||
description: 'Split flow into multiple paths'
|
||||
},
|
||||
{
|
||||
type: 'collector',
|
||||
label: 'Collector',
|
||||
icon: Merge,
|
||||
description: 'Merge multiple flows together'
|
||||
},
|
||||
{
|
||||
type: 'decision',
|
||||
label: 'Decision',
|
||||
icon: GitBranch,
|
||||
description: 'Conditional branching logic'
|
||||
}
|
||||
];
|
||||
|
||||
interface AgentBuilderSidebarProps {
|
||||
onAddNode: (type: string) => void;
|
||||
}
|
||||
|
||||
const AgentBuilderSidebar = memo(({ onAddNode }: AgentBuilderSidebarProps) => {
|
||||
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||
event.dataTransfer.setData('application/reactflow', nodeType);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-card border-r border-border p-4 overflow-y-auto">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold text-foreground mb-2">Agent Builder</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Drag and drop components to build your LlamaIndex agent flow
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3">Components</h3>
|
||||
{nodeTemplates.map((template) => {
|
||||
const IconComponent = template.icon;
|
||||
return (
|
||||
<Card
|
||||
key={template.type}
|
||||
className="p-3 cursor-grab active:cursor-grabbing border-border hover:border-primary/50 transition-colors"
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, template.type)}
|
||||
onClick={() => onAddNode(template.type)}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<IconComponent className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-foreground truncate">
|
||||
{template.label}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{template.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-4 bg-muted rounded-lg">
|
||||
<h4 className="text-sm font-medium text-foreground mb-2">Quick Actions</h4>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
onClick={() => onAddNode('start')}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Add Start Node
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
onClick={() => onAddNode('userInput')}
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
Add User Input
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
AgentBuilderSidebar.displayName = 'AgentBuilderSidebar';
|
||||
|
||||
export default AgentBuilderSidebar;
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
addEdge,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
MarkerType,
|
||||
useReactFlow,
|
||||
ReactFlowProvider
|
||||
} from '@xyflow/react';
|
||||
|
||||
// Import custom nodes
|
||||
import StartNode from './nodes/StartNode';
|
||||
import StopNode from './nodes/StopNode';
|
||||
import UserInputNode from './nodes/UserInputNode';
|
||||
import PromptLLMNode from './nodes/PromptLLMNode';
|
||||
import PromptAgentNode from './nodes/PromptAgentNode';
|
||||
import AgentToolNode from './nodes/AgentToolNode';
|
||||
import SplitterNode from './nodes/SplitterNode';
|
||||
import CollectorNode from './nodes/CollectorNode';
|
||||
import DecisionNode from './nodes/DecisionNode';
|
||||
import AgentBuilderSidebar from './AgentBuilderSidebar';
|
||||
|
||||
// Node types mapping
|
||||
const nodeTypes = {
|
||||
start: StartNode,
|
||||
stop: StopNode,
|
||||
userInput: UserInputNode,
|
||||
promptLLM: PromptLLMNode,
|
||||
promptAgent: PromptAgentNode,
|
||||
agentTool: AgentToolNode,
|
||||
splitter: SplitterNode,
|
||||
collector: CollectorNode,
|
||||
decision: DecisionNode,
|
||||
};
|
||||
|
||||
// Initial nodes and edges
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'start',
|
||||
position: { x: 100, y: 200 },
|
||||
data: { label: 'Start' }
|
||||
}
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
let nodeId = 2;
|
||||
|
||||
const AgentFlowInner = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => {
|
||||
const newEdge: Edge = {
|
||||
...params,
|
||||
id: `edge-${params.source}-${params.target}`,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}
|
||||
};
|
||||
setEdges((eds) => addEdge(newEdge, eds));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const type = event.dataTransfer.getData('application/reactflow');
|
||||
if (typeof type === 'undefined' || !type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
const newNode: Node = {
|
||||
id: `${nodeId++}`,
|
||||
type,
|
||||
position,
|
||||
data: {
|
||||
label: type.charAt(0).toUpperCase() + type.slice(1).replace(/([A-Z])/g, ' $1').trim()
|
||||
}
|
||||
};
|
||||
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
},
|
||||
[screenToFlowPosition, setNodes]
|
||||
);
|
||||
|
||||
const onAddNode = useCallback((type: string) => {
|
||||
const position = { x: Math.random() * 400 + 200, y: Math.random() * 400 + 200 };
|
||||
const newNode: Node = {
|
||||
id: `${nodeId++}`,
|
||||
type,
|
||||
position,
|
||||
data: {
|
||||
label: type.charAt(0).toUpperCase() + type.slice(1).replace(/([A-Z])/g, ' $1').trim()
|
||||
}
|
||||
};
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}, [setNodes]);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex">
|
||||
<AgentBuilderSidebar onAddNode={onAddNode} />
|
||||
|
||||
<div className="flex-1" ref={reactFlowWrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
className="bg-flow-bg"
|
||||
defaultEdgeOptions={{
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Background color="hsl(var(--border))" gap={20} />
|
||||
<Controls />
|
||||
<MiniMap
|
||||
zoomable
|
||||
pannable
|
||||
className="bg-card border border-border"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AgentFlow = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<AgentFlowInner />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentFlow;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Wrench } from 'lucide-react';
|
||||
|
||||
interface AgentToolNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
toolType?: string;
|
||||
config?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const AgentToolNode = memo(({ data, selected }: AgentToolNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-tool ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white p-4 min-w-[160px]">
|
||||
<Wrench className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium mb-2">{data.label || 'Agent Tool'}</span>
|
||||
<div className="w-full space-y-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={data.toolType || 'Tool type'}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={data.config || 'Configuration'}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
AgentToolNode.displayName = 'AgentToolNode';
|
||||
|
||||
export default AgentToolNode;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Merge } from 'lucide-react';
|
||||
|
||||
interface CollectorNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
mergeType?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const CollectorNode = memo(({ data, selected }: CollectorNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-collector ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white p-4 min-w-[140px]" style={{ transform: 'skewX(20deg)' }}>
|
||||
<Merge className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium mb-2">{data.label || 'Collector'}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={data.mergeType || 'Merge logic'}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} id="input1" style={{ top: '30%' }} />
|
||||
<Handle type="target" position={Position.Left} id="input2" style={{ top: '70%' }} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CollectorNode.displayName = 'CollectorNode';
|
||||
|
||||
export default CollectorNode;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { GitBranch } from 'lucide-react';
|
||||
|
||||
interface DecisionNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
condition?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const DecisionNode = memo(({ data, selected }: DecisionNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-decision ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white text-center">
|
||||
<GitBranch className="w-4 h-4 mb-1" />
|
||||
<span className="text-xs font-medium">{data.label || 'Decision'}</span>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} style={{ transform: 'rotate(-45deg)', left: '-6px', top: '50%' }} />
|
||||
<Handle type="source" position={Position.Top} id="true" style={{ transform: 'rotate(-45deg)', left: '50%', top: '-6px' }} />
|
||||
<Handle type="source" position={Position.Bottom} id="false" style={{ transform: 'rotate(-45deg)', left: '50%', bottom: '-6px' }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
DecisionNode.displayName = 'DecisionNode';
|
||||
|
||||
export default DecisionNode;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Bot } from 'lucide-react';
|
||||
|
||||
interface PromptAgentNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
agentType?: string;
|
||||
systemPrompt?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const PromptAgentNode = memo(({ data, selected }: PromptAgentNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-agent ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white p-4 min-w-[180px]">
|
||||
<Bot className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium mb-2">{data.label || 'Prompt Agent'}</span>
|
||||
<div className="w-full space-y-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={data.agentType || 'Agent type'}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<textarea
|
||||
placeholder={data.systemPrompt || 'System prompt...'}
|
||||
rows={2}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60 resize-none"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PromptAgentNode.displayName = 'PromptAgentNode';
|
||||
|
||||
export default PromptAgentNode;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Brain } from 'lucide-react';
|
||||
|
||||
interface PromptLLMNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const PromptLLMNode = memo(({ data, selected }: PromptLLMNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-llm ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white p-4 min-w-[180px]">
|
||||
<Brain className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium mb-2">{data.label || 'Prompt LLM'}</span>
|
||||
<div className="w-full space-y-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={data.model || 'Model (e.g., gpt-4)'}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Temperature"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
defaultValue={data.temperature || 0.7}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PromptLLMNode.displayName = 'PromptLLMNode';
|
||||
|
||||
export default PromptLLMNode;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Split } from 'lucide-react';
|
||||
|
||||
interface SplitterNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
splitType?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const SplitterNode = memo(({ data, selected }: SplitterNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-splitter ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white p-4 min-w-[140px]" style={{ transform: 'skewX(20deg)' }}>
|
||||
<Split className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium mb-2">{data.label || 'Splitter'}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={data.splitType || 'Split logic'}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} id="output1" style={{ top: '30%' }} />
|
||||
<Handle type="source" position={Position.Right} id="output2" style={{ top: '70%' }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SplitterNode.displayName = 'SplitterNode';
|
||||
|
||||
export default SplitterNode;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Play } from 'lucide-react';
|
||||
|
||||
interface StartNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const StartNode = memo(({ data, selected }: StartNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-start ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white">
|
||||
<Play className="w-6 h-6 mb-1" fill="currentColor" />
|
||||
<span className="text-xs font-medium">{data.label || 'Start'}</span>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
StartNode.displayName = 'StartNode';
|
||||
|
||||
export default StartNode;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Square } from 'lucide-react';
|
||||
|
||||
interface StopNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const StopNode = memo(({ data, selected }: StopNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-stop ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white">
|
||||
<Square className="w-6 h-6 mb-1" fill="currentColor" />
|
||||
<span className="text-xs font-medium">{data.label || 'Stop'}</span>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
StopNode.displayName = 'StopNode';
|
||||
|
||||
export default StopNode;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { MessageCircle } from 'lucide-react';
|
||||
|
||||
interface UserInputNodeProps {
|
||||
data: {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const UserInputNode = memo(({ data, selected }: UserInputNodeProps) => {
|
||||
return (
|
||||
<div className={`agent-node node-input ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-white p-4 min-w-[160px]">
|
||||
<MessageCircle className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium mb-2">{data.label || 'User Input'}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={data.placeholder || 'Enter prompt...'}
|
||||
className="w-full px-2 py-1 text-xs bg-white/20 border border-white/30 rounded text-white placeholder-white/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
UserInputNode.displayName = 'UserInputNode';
|
||||
|
||||
export default UserInputNode;
|
||||
+228
-49
@@ -2,41 +2,70 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Import xyflow styles */
|
||||
@import '@xyflow/react/dist/style.css';
|
||||
|
||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
||||
All colors MUST be HSL.
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
/* Core colors */
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
/* Agent Builder Brand Colors */
|
||||
--primary: 262 83% 58%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--primary-glow: 262 100% 75%;
|
||||
|
||||
--secondary: 240 5.9% 10%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 240 5.9% 10%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 240 5.9% 10%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--border: 240 5.9% 10%;
|
||||
--input: 240 5.9% 10%;
|
||||
--ring: 262 83% 58%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
/* Flow-specific colors */
|
||||
--flow-bg: 240 10% 3.9%;
|
||||
--node-start: 142 76% 36%;
|
||||
--node-stop: 0 84% 60%;
|
||||
--node-input: 217 91% 60%;
|
||||
--node-llm: 262 83% 58%;
|
||||
--node-agent: 290 95% 70%;
|
||||
--node-tool: 47 96% 53%;
|
||||
--node-splitter: 199 89% 48%;
|
||||
--node-collector: 30 100% 50%;
|
||||
--node-decision: 39 85% 59%;
|
||||
|
||||
/* Gradients */
|
||||
--gradient-primary: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-glow)));
|
||||
--gradient-node: linear-gradient(135deg, hsl(var(--card)), hsl(var(--muted)));
|
||||
|
||||
/* Shadows */
|
||||
--shadow-node: 0 10px 30px -10px hsl(var(--primary) / 0.3);
|
||||
--shadow-glow: 0 0 40px hsl(var(--primary-glow) / 0.4);
|
||||
|
||||
/* Animations */
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
--radius: 0.75rem;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
|
||||
@@ -55,34 +84,25 @@ All colors MUST be HSL.
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
.light {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 262 83% 58%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 262 83% 58%;
|
||||
--flow-bg: 0 0% 100%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
@@ -100,6 +120,165 @@ All colors MUST be HSL.
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
/* Flow Canvas Styles */
|
||||
.react-flow {
|
||||
background: hsl(var(--flow-bg));
|
||||
}
|
||||
|
||||
.react-flow__background {
|
||||
background-color: hsl(var(--flow-bg));
|
||||
}
|
||||
|
||||
.react-flow__background .react-flow__background-pattern {
|
||||
stroke: hsl(var(--border));
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Custom Node Styles */
|
||||
.agent-node {
|
||||
border-radius: var(--radius);
|
||||
border: 2px solid transparent;
|
||||
background: linear-gradient(135deg, hsl(var(--card)), hsl(var(--muted)));
|
||||
box-shadow: var(--shadow-node);
|
||||
transition: var(--transition-smooth);
|
||||
backdrop-filter: blur(10px);
|
||||
min-width: 120px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agent-node:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.agent-node.selected {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
/* Node Type Specific Styles */
|
||||
.node-start, .node-stop {
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.node-start {
|
||||
background: linear-gradient(135deg, hsl(var(--node-start)), hsl(var(--node-start) / 0.8));
|
||||
border-color: hsl(var(--node-start));
|
||||
}
|
||||
|
||||
.node-stop {
|
||||
background: linear-gradient(135deg, hsl(var(--node-stop)), hsl(var(--node-stop) / 0.8));
|
||||
border-color: hsl(var(--node-stop));
|
||||
}
|
||||
|
||||
.node-input {
|
||||
background: linear-gradient(135deg, hsl(var(--node-input)), hsl(var(--node-input) / 0.8));
|
||||
border-color: hsl(var(--node-input));
|
||||
}
|
||||
|
||||
.node-llm {
|
||||
background: linear-gradient(135deg, hsl(var(--node-llm)), hsl(var(--node-llm) / 0.8));
|
||||
border-color: hsl(var(--node-llm));
|
||||
}
|
||||
|
||||
.node-agent {
|
||||
background: linear-gradient(135deg, hsl(var(--node-agent)), hsl(var(--node-agent) / 0.8));
|
||||
border-color: hsl(var(--node-agent));
|
||||
}
|
||||
|
||||
.node-tool {
|
||||
background: linear-gradient(135deg, hsl(var(--node-tool)), hsl(var(--node-tool) / 0.8));
|
||||
border-color: hsl(var(--node-tool));
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.node-splitter, .node-collector {
|
||||
background: linear-gradient(135deg, hsl(var(--node-splitter)), hsl(var(--node-splitter) / 0.8));
|
||||
border-color: hsl(var(--node-splitter));
|
||||
transform: skewX(-20deg);
|
||||
}
|
||||
|
||||
.node-collector {
|
||||
background: linear-gradient(135deg, hsl(var(--node-collector)), hsl(var(--node-collector) / 0.8));
|
||||
border-color: hsl(var(--node-collector));
|
||||
}
|
||||
|
||||
.node-decision {
|
||||
background: linear-gradient(135deg, hsl(var(--node-decision)), hsl(var(--node-decision) / 0.8));
|
||||
border-color: hsl(var(--node-decision));
|
||||
transform: rotate(45deg);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.node-decision .node-content {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
/* Handle Styles */
|
||||
.react-flow__handle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid hsl(var(--primary));
|
||||
background: hsl(var(--background));
|
||||
border-radius: 50%;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.react-flow__handle:hover {
|
||||
transform: scale(1.3);
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.react-flow__handle.connectionindicator {
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* Edge Styles */
|
||||
.react-flow__edge-path {
|
||||
stroke: hsl(var(--primary));
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.react-flow__edge.selected .react-flow__edge-path {
|
||||
stroke: hsl(var(--primary-glow));
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* Control Styles */
|
||||
.react-flow__controls {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.react-flow__controls-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: hsl(var(--foreground));
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
.react-flow__controls-button:hover {
|
||||
background: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
}
|
||||
|
||||
/* Minimap Styles */
|
||||
.react-flow__minimap {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
+3
-6
@@ -1,12 +1,9 @@
|
||||
// Update this page (the content is just a fallback if you fail to update the page)
|
||||
import AgentFlow from "@/components/AgentFlow";
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
|
||||
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
|
||||
</div>
|
||||
<div className="h-screen w-full">
|
||||
<AgentFlow />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-1
@@ -26,7 +26,8 @@ export default {
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
glow: 'hsl(var(--primary-glow))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
@@ -52,6 +53,16 @@ export default {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
'flow-bg': 'hsl(var(--flow-bg))',
|
||||
'node-start': 'hsl(var(--node-start))',
|
||||
'node-stop': 'hsl(var(--node-stop))',
|
||||
'node-input': 'hsl(var(--node-input))',
|
||||
'node-llm': 'hsl(var(--node-llm))',
|
||||
'node-agent': 'hsl(var(--node-agent))',
|
||||
'node-tool': 'hsl(var(--node-tool))',
|
||||
'node-splitter': 'hsl(var(--node-splitter))',
|
||||
'node-collector': 'hsl(var(--node-collector))',
|
||||
'node-decision': 'hsl(var(--node-decision))',
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
|
||||
Reference in New Issue
Block a user