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
Aarushi 2024-06-27 10:14:25 +01:00 committed by GitHub
parent dd960f9306
commit cdc658695f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 896 additions and 94 deletions

View File

@ -0,0 +1 @@
AGPT_SERVER_URL=http://localhost:8000

View File

@ -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"
}
}

View File

@ -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&nbsp;
<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&nbsp;
<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">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">
Find all the information about AutoGPT&apos;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">
-&gt;
</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">
-&gt;
</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>
);
}

View File

@ -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' }}
>
&#9776;
</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);

View File

@ -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;

View File

@ -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);
}