Add reactflow component in AutoGPT builder (#7270)
* Getting started with nextjs * fix linting * remove gitignore for package.json * pulling in reactflow components * updating css * use environment variables * clean up css / ui a lil * Fixed nodes/run button animation so they are always visible --------- Co-authored-by: Bentlybro <tomnoon9@gmail.com>rushi/add-storybook
parent
dd960f9306
commit
cdc658695f
|
@ -0,0 +1 @@
|
|||
AGPT_SERVER_URL=http://localhost:8000
|
|
@ -9,18 +9,21 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.4",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.2.4"
|
||||
"react-modal": "^3.16.1",
|
||||
"reactflow": "^11.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.4"
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,97 +1,40 @@
|
|||
import Image from "next/image";
|
||||
import Flow from '../components/Flow';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by adding a
|
||||
<code className="font-mono font-bold">node</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://news.agpt.co/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/autogpt.svg"
|
||||
alt="AutoGPT Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by adding a
|
||||
<code className="font-mono font-bold">node</code>
|
||||
</p>
|
||||
<div
|
||||
className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://news.agpt.co/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/autogpt.svg"
|
||||
alt="AutoGPT Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
|
||||
<Image
|
||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||
src="/autogpt.svg"
|
||||
alt="AutoGPT Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-32 w-full flex place-items-center max-w-5xl grid grid-cols-1 md:grid-cols-3 gap-4 text-center lg:mb-0 lg:text-left">
|
||||
<a
|
||||
href="https://docs.agpt.co/"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Docs{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Find all the information about AutoGPT's features.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
|
||||
<a
|
||||
href=""
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Runner{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Run your agent!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href=""
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Deploy{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
|
||||
Deploy your agent!
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<div className="w-full flex justify-center mt-10">
|
||||
<div className="flow-container w-full h-full">
|
||||
<Flow/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
import React, { useState, useEffect, FC, memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
type Schema = {
|
||||
properties: { [key: string]: any };
|
||||
};
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ data }) => {
|
||||
const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false);
|
||||
|
||||
// Automatically open properties when output_data or status is updated
|
||||
useEffect(() => {
|
||||
if (data.output_data || data.status) {
|
||||
setIsPropertiesOpen(true);
|
||||
}
|
||||
}, [data.output_data, data.status]);
|
||||
|
||||
const toggleProperties = () => {
|
||||
setIsPropertiesOpen(!isPropertiesOpen);
|
||||
};
|
||||
|
||||
const generateHandles = (schema: Schema, type: 'source' | 'target') => {
|
||||
if (!schema?.properties) return null;
|
||||
const keys = Object.keys(schema.properties);
|
||||
return keys.map((key) => (
|
||||
<div key={key} style={{ display: 'flex', alignItems: 'center', position: 'relative', marginBottom: '5px' }}>
|
||||
{type === 'target' && (
|
||||
<>
|
||||
<Handle
|
||||
type={type}
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span style={{ color: '#e0e0e0', marginLeft: '10px' }}>{key}</span>
|
||||
</>
|
||||
)}
|
||||
{type === 'source' && (
|
||||
<>
|
||||
<span style={{ color: '#e0e0e0', marginRight: '10px' }}>{key}</span>
|
||||
<Handle
|
||||
type={type}
|
||||
position={Position.Right}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
const handleInputChange = (key: string, value: any) => {
|
||||
const newValues = { ...data.hardcodedValues, [key]: value };
|
||||
data.setHardcodedValues(newValues);
|
||||
};
|
||||
|
||||
const isHandleConnected = (key: string) => {
|
||||
return data.connections.some((conn: string) => {
|
||||
const [, target] = conn.split(' -> ');
|
||||
return target.includes(key) && target.includes(data.title);
|
||||
});
|
||||
};
|
||||
|
||||
const hasDisconnectedHandle = (key: string) => {
|
||||
return !isHandleConnected(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', border: '2px solid #fff', borderRadius: '20px', background: '#333', color: '#e0e0e0', width: '250px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>
|
||||
{data?.title.replace(/\d+/g, '')}
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleProperties}
|
||||
style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: '#e0e0e0' }}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', gap: '20px' }}>
|
||||
<div>
|
||||
{data.inputSchema && generateHandles(data.inputSchema, 'target')}
|
||||
{data.inputSchema && Object.keys(data.inputSchema.properties).map(key => (
|
||||
hasDisconnectedHandle(key) && (
|
||||
<div key={key} style={{ marginBottom: '5px' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Enter ${key}`}
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
style={{ width: '100%', padding: '5px', borderRadius: '4px', border: '1px solid #555', background: '#444', color: '#e0e0e0' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{data.outputSchema && generateHandles(data.outputSchema, 'source')}
|
||||
</div>
|
||||
</div>
|
||||
{isPropertiesOpen && (
|
||||
<div style={{ marginTop: '10px', background: '#444', padding: '10px', borderRadius: '10px' }}>
|
||||
<h4>Node Output</h4>
|
||||
<p><strong>Status:</strong> {typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}</p>
|
||||
<p><strong>Output Data:</strong> {typeof data.output_data === 'object' ? JSON.stringify(data.output_data) : data.output_data || 'N/A'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
|
@ -0,0 +1,590 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
Node,
|
||||
Edge,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
OnConnect,
|
||||
NodeTypes,
|
||||
EdgeRemoveChange,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import Modal from 'react-modal';
|
||||
import CustomNode from './CustomNode';
|
||||
import './flow.css';
|
||||
|
||||
const initialNodes: Node[] = [];
|
||||
const initialEdges: Edge[] = [];
|
||||
const nodeTypes: NodeTypes = {
|
||||
custom: CustomNode,
|
||||
};
|
||||
|
||||
interface AvailableNode {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema?: { properties: { [key: string]: any }; required?: string[] };
|
||||
outputSchema?: { properties: { [key: string]: any } };
|
||||
}
|
||||
|
||||
interface ExecData {
|
||||
node_id: string;
|
||||
status: string;
|
||||
output_data: any;
|
||||
}
|
||||
|
||||
const Flow: React.FC = () => {
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
const [nodeId, setNodeId] = useState<number>(1);
|
||||
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [description, setDescription] = useState<string>('');
|
||||
const [variableName, setVariableName] = useState<string>('');
|
||||
const [variableValue, setVariableValue] = useState<string>('');
|
||||
const [printVariable, setPrintVariable] = useState<string>('');
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [availableNodes, setAvailableNodes] = useState<AvailableNode[]>([]);
|
||||
const [loadingStatus, setLoadingStatus] = useState<'loading' | 'failed' | 'loaded'>('loading');
|
||||
const [agentId, setAgentId] = useState<string | null>(null);
|
||||
|
||||
const apiUrl = 'http://localhost:8000'
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${apiUrl}/blocks`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
setAvailableNodes(data.map((node: AvailableNode) => ({
|
||||
...node,
|
||||
description: typeof node.description === 'object' ? JSON.stringify(node.description) : node.description,
|
||||
})));
|
||||
setLoadingStatus('loaded');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching nodes:', error);
|
||||
setLoadingStatus('failed');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds).map(node => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
metadata: {
|
||||
...node.data.metadata,
|
||||
position: node.position
|
||||
}
|
||||
}
|
||||
}))),
|
||||
[]
|
||||
);
|
||||
|
||||
const onEdgesChange: OnEdgesChange = useCallback(
|
||||
(changes) => {
|
||||
const removedEdges = changes.filter((change): change is EdgeRemoveChange => change.type === 'remove');
|
||||
setEdges((eds) => applyEdgeChanges(changes, eds));
|
||||
|
||||
if (removedEdges.length > 0) {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
const updatedConnections = node.data.connections.filter(
|
||||
(conn: string) =>
|
||||
!removedEdges.some((edge) => edge.id && conn.includes(edge.id))
|
||||
);
|
||||
return { ...node, data: { ...node.data, connections: updatedConnections } };
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === connection.source) {
|
||||
const connections = node.data.connections || [];
|
||||
connections.push(`${node.data.title} ${connection.sourceHandle} -> ${connection.targetHandle}`);
|
||||
return { ...node, data: { ...node.data, connections } };
|
||||
}
|
||||
if (node.id === connection.target) {
|
||||
const connections = node.data.connections || [];
|
||||
connections.push(`${connection.sourceHandle} -> ${node.data.title} ${connection.targetHandle}`);
|
||||
return { ...node, data: { ...node.data, connections } };
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setEdges, setNodes]
|
||||
);
|
||||
|
||||
const addNode = (type: string, label: string, description: string) => {
|
||||
const nodeSchema = availableNodes.find(node => node.name === label);
|
||||
const position = { x: Math.random() * 400, y: Math.random() * 400 };
|
||||
|
||||
const newNode: Node = {
|
||||
id: nodeId.toString(),
|
||||
type: 'custom',
|
||||
data: {
|
||||
label: label,
|
||||
title: `${type} ${nodeId}`,
|
||||
description: `${description}`,
|
||||
inputSchema: nodeSchema?.inputSchema,
|
||||
outputSchema: nodeSchema?.outputSchema,
|
||||
connections: [],
|
||||
variableName: '',
|
||||
variableValue: '',
|
||||
printVariable: '',
|
||||
setVariableName,
|
||||
setVariableValue,
|
||||
setPrintVariable,
|
||||
hardcodedValues: {},
|
||||
setHardcodedValues: (values: { [key: string]: any }) => {
|
||||
setNodes((nds) => nds.map((node) =>
|
||||
node.id === nodeId.toString()
|
||||
? { ...node, data: { ...node.data, hardcodedValues: values } }
|
||||
: node
|
||||
));
|
||||
},
|
||||
block_id: nodeSchema?.id || '',
|
||||
metadata: {
|
||||
position // Store position in metadata
|
||||
}
|
||||
},
|
||||
position,
|
||||
};
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
setNodeId((id) => id + 1);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalIsOpen(false);
|
||||
setSelectedNode(null);
|
||||
};
|
||||
|
||||
const saveNodeData = () => {
|
||||
if (selectedNode) {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === selectedNode.id
|
||||
? {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
title,
|
||||
description,
|
||||
label: title,
|
||||
variableName,
|
||||
variableValue: typeof variableValue === 'object' ? JSON.stringify(variableValue) : variableValue,
|
||||
printVariable: typeof printVariable === 'object' ? JSON.stringify(printVariable) : printVariable,
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
);
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
};
|
||||
|
||||
const filteredNodes = availableNodes.filter(node => node.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
|
||||
const prepareNodeInputData = (node: Node, allNodes: Node[], allEdges: Edge[]) => {
|
||||
const nodeSchema = availableNodes.find(n => n.id === node.data.block_id);
|
||||
if (!nodeSchema || !nodeSchema.inputSchema) return {};
|
||||
|
||||
let inputData: { [key: string]: any } = {};
|
||||
const inputProperties = nodeSchema.inputSchema.properties;
|
||||
const requiredProperties = nodeSchema.inputSchema.required || [];
|
||||
|
||||
// Initialize inputData with default values for all required properties
|
||||
requiredProperties.forEach(prop => {
|
||||
inputData[prop] = node.data.hardcodedValues[prop] || '';
|
||||
});
|
||||
|
||||
Object.keys(inputProperties).forEach(prop => {
|
||||
const inputEdge = allEdges.find(edge => edge.target === node.id && edge.targetHandle === prop);
|
||||
if (inputEdge) {
|
||||
const sourceNode = allNodes.find(n => n.id === inputEdge.source);
|
||||
inputData[prop] = sourceNode?.data.output_data || sourceNode?.data.hardcodedValues[prop] || '';
|
||||
} else if (node.data.hardcodedValues && node.data.hardcodedValues[prop]) {
|
||||
inputData[prop] = node.data.hardcodedValues[prop];
|
||||
}
|
||||
});
|
||||
|
||||
return inputData;
|
||||
};
|
||||
|
||||
const updateNodeData = (execData: ExecData) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === execData.node_id) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
status: execData.status,
|
||||
output_data: execData.output_data,
|
||||
isPropertiesOpen: true, // Open the properties
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const runAgent = async () => {
|
||||
try {
|
||||
const formattedNodes = nodes.map(node => ({
|
||||
id: node.id,
|
||||
block_id: node.data.block_id,
|
||||
input_default: prepareNodeInputData(node, nodes, edges),
|
||||
input_nodes: edges.filter(edge => edge.target === node.id).reduce((acc, edge) => {
|
||||
if (edge.targetHandle) {
|
||||
acc[edge.targetHandle] = edge.source;
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [key: string]: string }),
|
||||
output_nodes: edges.filter(edge => edge.source === node.id).reduce((acc, edge) => {
|
||||
if (edge.sourceHandle) {
|
||||
acc[edge.sourceHandle] = edge.target;
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [key: string]: string }),
|
||||
metadata: node.data.metadata,
|
||||
connections: node.data.connections // Ensure connections are preserved
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
id: '',
|
||||
name: 'Agent Name',
|
||||
description: 'Agent Description',
|
||||
nodes: formattedNodes,
|
||||
};
|
||||
|
||||
const createResponse = await fetch(`${apiUrl}/agents`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!createResponse.ok) {
|
||||
throw new Error(`HTTP error! Status: ${createResponse.status}`);
|
||||
}
|
||||
|
||||
const createData = await createResponse.json();
|
||||
const agentId = createData.id;
|
||||
setAgentId(agentId);
|
||||
|
||||
const responseNodes = createData.nodes.map((node: any) => {
|
||||
const block = availableNodes.find(n => n.id === node.block_id);
|
||||
const connections = edges.filter(edge => edge.source === node.id || edge.target === node.id).map(edge => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle
|
||||
}));
|
||||
return {
|
||||
id: node.id,
|
||||
type: 'custom',
|
||||
position: node.metadata.position,
|
||||
data: {
|
||||
label: block?.name || 'Unknown',
|
||||
title: `${block?.name || 'Unknown'}`,
|
||||
description: `${block?.description || ''}`,
|
||||
inputSchema: block?.inputSchema,
|
||||
outputSchema: block?.outputSchema,
|
||||
connections: connections.map(c => `${c.source}-${c.sourceHandle} -> ${c.target}-${c.targetHandle}`),
|
||||
variableName: '',
|
||||
variableValue: '',
|
||||
printVariable: '',
|
||||
setVariableName,
|
||||
setVariableValue,
|
||||
setPrintVariable,
|
||||
hardcodedValues: node.input_default,
|
||||
setHardcodedValues: (values: { [key: string]: any }) => {
|
||||
setNodes((nds) => nds.map((n) =>
|
||||
n.id === node.id
|
||||
? { ...n, data: { ...n.data, hardcodedValues: values } }
|
||||
: n
|
||||
));
|
||||
},
|
||||
block_id: node.block_id,
|
||||
metadata: node.metadata
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const newEdges = createData.nodes.flatMap((node: any) => {
|
||||
return Object.entries(node.output_nodes).map(([sourceHandle, targetNodeId]) => ({
|
||||
id: `${node.id}-${sourceHandle}-${targetNodeId}`,
|
||||
source: node.id,
|
||||
sourceHandle: sourceHandle,
|
||||
target: targetNodeId,
|
||||
targetHandle: Object.keys(node.input_nodes).find(key => node.input_nodes[key] === targetNodeId) || '',
|
||||
}));
|
||||
});
|
||||
|
||||
setNodes(responseNodes);
|
||||
setEdges(newEdges);
|
||||
|
||||
const initialNodeInput = nodes.reduce((acc, node) => {
|
||||
acc[node.id] = prepareNodeInputData(node, nodes, edges);
|
||||
return acc;
|
||||
}, {} as { [key: string]: any });
|
||||
|
||||
const nodeInputForExecution = Object.keys(initialNodeInput).reduce((acc, key) => {
|
||||
const blockId = nodes.find(node => node.id === key)?.data.block_id;
|
||||
const nodeSchema = availableNodes.find(n => n.id === blockId);
|
||||
if (nodeSchema && nodeSchema.inputSchema) {
|
||||
Object.keys(nodeSchema.inputSchema.properties).forEach(prop => {
|
||||
acc[prop] = initialNodeInput[key][prop];
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [key: string]: any });
|
||||
|
||||
const executeResponse = await fetch(`${apiUrl}/agents/${agentId}/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(nodeInputForExecution),
|
||||
});
|
||||
|
||||
if (!executeResponse.ok) {
|
||||
throw new Error(`HTTP error! Status: ${executeResponse.status}`);
|
||||
}
|
||||
|
||||
const executeData = await executeResponse.json();
|
||||
const runId = executeData.run_id;
|
||||
|
||||
const startPolling = () => {
|
||||
const endTime = Date.now() + 60000;
|
||||
|
||||
const poll = async () => {
|
||||
if (Date.now() >= endTime) {
|
||||
console.log('Polling timeout reached.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/agents/${agentId}/executions/${runId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
data.forEach(updateNodeData);
|
||||
|
||||
const allCompleted = data.every((exec: any) => exec.status === 'COMPLETED');
|
||||
if (allCompleted) {
|
||||
console.log('All nodes are completed.');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(poll, 100);
|
||||
} catch (error) {
|
||||
console.error('Error during polling:', error);
|
||||
setTimeout(poll, 100);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
};
|
||||
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
console.error('Error running agent:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flow-container">
|
||||
<div className={`flow-controls ${isSidebarOpen ? 'open' : ''}`}>
|
||||
<button className="nodes-button" onClick={toggleSidebar}>
|
||||
Nodes
|
||||
</button>
|
||||
<button className="run-button" onClick={runAgent}>
|
||||
Run
|
||||
</button>
|
||||
{agentId && (
|
||||
<span style={{ marginLeft: '10px', color: '#fff', fontSize: '16px' }}>
|
||||
Agent ID: {agentId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flow-wrapper">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
/>
|
||||
</div>
|
||||
{selectedNode && (
|
||||
<Modal isOpen={modalIsOpen} onRequestClose={closeModal} contentLabel="Node Info" className="modal" overlayClassName="overlay">
|
||||
<h2>Edit Node</h2>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveNodeData();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label>
|
||||
Title:
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Description:
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{selectedNode.data.title.includes('Variable') && (
|
||||
<>
|
||||
<div>
|
||||
<label>
|
||||
Variable Name:
|
||||
<input
|
||||
type="text"
|
||||
value={variableName}
|
||||
onChange={(e) => setVariableName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Variable Value:
|
||||
<input
|
||||
type="text"
|
||||
value={variableValue}
|
||||
onChange={(e) => setVariableValue(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{selectedNode.data.title.includes('Print') && (
|
||||
<>
|
||||
<div>
|
||||
<label>
|
||||
Variable to Print:
|
||||
<input
|
||||
type="text"
|
||||
value={printVariable}
|
||||
onChange={(e) => setPrintVariable(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button type="submit">Save</button>
|
||||
<button type="button" onClick={closeModal}>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
)}
|
||||
<div className={`sidebar ${isSidebarOpen ? 'open' : ''}`}>
|
||||
<h3 style={{ margin: '0 0 10px 0' }}>Nodes</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search nodes..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
padding: '10px',
|
||||
fontSize: '16px',
|
||||
backgroundColor: '#333',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #555',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '10px',
|
||||
width: 'calc(100% - 22px)',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{loadingStatus === 'loading' && <p>Loading...</p>}
|
||||
{loadingStatus === 'failed' && <p>Failed To Load Nodes</p>}
|
||||
{loadingStatus === 'loaded' && filteredNodes.map(node => (
|
||||
<div key={node.id} style={sidebarNodeRowStyle}>
|
||||
<div>
|
||||
<strong>{node.name}</strong>
|
||||
<p>{node.description}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => addNode(node.name, node.name, node.description)}
|
||||
style={sidebarButtonStyle}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const sidebarNodeRowStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#444',
|
||||
padding: '10px',
|
||||
borderRadius: '4px',
|
||||
};
|
||||
|
||||
const sidebarButtonStyle = {
|
||||
padding: '10px 20px',
|
||||
fontSize: '16px',
|
||||
backgroundColor: '#007bff',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
export default Flow;
|
|
@ -0,0 +1,150 @@
|
|||
/* flow.css or index.css */
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #444;
|
||||
color: #e0e0e0;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
width: calc(100% - 18px);
|
||||
box-sizing: border-box;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
input::placeholder, textarea::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
margin-right: -50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #333;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.modal form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal form div {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -600px;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
transition: left 0.3s ease;
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sidebar h3 {
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.sidebarNodeRowStyle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #444;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.sidebarNodeRowStyle.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.flow-container {
|
||||
width: 100%;
|
||||
height: 600px; /* Adjust this height as needed */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-controls {
|
||||
position: absolute;
|
||||
left: -80px;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.flow-controls.open {
|
||||
transform: translateX(350px);
|
||||
}
|
Loading…
Reference in New Issue