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:
gpt-engineer-app[bot]
2025-07-14 22:26:54 +00:00
parent 202f943161
commit 42e8154b7d
16 changed files with 1183 additions and 59 deletions
+291 -3
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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",
+158
View File
@@ -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;
+172
View File
@@ -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;
+43
View File
@@ -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;
+35
View File
@@ -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;
+29
View File
@@ -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;
+43
View File
@@ -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;
+47
View File
@@ -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;
+35
View File
@@ -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;
+26
View File
@@ -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;
+26
View File
@@ -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;
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))',