diff --git a/rnd/autogpt_builder/src/components/CustomNode.tsx b/rnd/autogpt_builder/src/components/CustomNode.tsx index 74cf1c230317..1c3f287f7460 100644 --- a/rnd/autogpt_builder/src/components/CustomNode.tsx +++ b/rnd/autogpt_builder/src/components/CustomNode.tsx @@ -10,11 +10,25 @@ type Schema = { required?: string[]; }; -const CustomNode: FC = ({ data, id }) => { +type CustomNodeData = { + blockType: string; + title: string; + inputSchema: Schema; + outputSchema: Schema; + hardcodedValues: { [key: string]: any }; + setHardcodedValues: (values: { [key: string]: any }) => void; + connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>; + isPropertiesOpen: boolean; + status?: string; + output_data?: any; +}; + +const CustomNode: FC> = ({ data, id }) => { const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false); const [isModalOpen, setIsModalOpen] = useState(false); const [activeKey, setActiveKey] = useState(null); const [modalValue, setModalValue] = useState(''); + const [errors, setErrors] = useState<{ [key: string]: string | null }>({}); useEffect(() => { if (data.output_data || data.status) { @@ -22,6 +36,10 @@ const CustomNode: FC = ({ data, id }) => { } }, [data.output_data, data.status]); + useEffect(() => { + console.log(`Node ${id} data:`, data); + }, [id, data]); + const toggleProperties = () => { setIsPropertiesOpen(!isPropertiesOpen); }; @@ -59,13 +77,49 @@ const CustomNode: FC = ({ data, id }) => { const handleInputChange = (key: string, value: any) => { const newValues = { ...data.hardcodedValues, [key]: value }; + console.log(`Updating hardcoded values for node ${id}:`, newValues); data.setHardcodedValues(newValues); + setErrors((prevErrors) => ({ ...prevErrors, [key]: null })); + }; + + const validateInput = (key: string, value: any, schema: any) => { + switch (schema.type) { + case 'string': + if (schema.enum && !schema.enum.includes(value)) { + return `Invalid value for ${key}`; + } + break; + case 'boolean': + if (typeof value !== 'boolean') { + return `Invalid value for ${key}`; + } + break; + case 'number': + if (typeof value !== 'number') { + return `Invalid value for ${key}`; + } + break; + case 'array': + if (!Array.isArray(value) || value.some((item: any) => typeof item !== 'string')) { + return `Invalid value for ${key}`; + } + if (schema.minItems && value.length < schema.minItems) { + return `${key} requires at least ${schema.minItems} items`; + } + break; + default: + return null; + } + return null; }; const isHandleConnected = (key: string) => { - return data.connections && data.connections.some((conn: string) => { - const [source, target] = conn.split(' -> '); - return target.includes(key) && target.includes(data.title); + return data.connections && data.connections.some((conn: any) => { + if (typeof conn === 'string') { + const [source, target] = conn.split(' -> '); + return target.includes(key) && target.includes(data.title); + } + return conn.target === id && conn.targetHandle === key; }); }; @@ -83,7 +137,43 @@ const CustomNode: FC = ({ data, id }) => { setActiveKey(null); }; + const addArrayItem = (key: string) => { + const currentValues = data.hardcodedValues[key] || []; + handleInputChange(key, [...currentValues, '']); + }; + + const removeArrayItem = (key: string, index: number) => { + const currentValues = data.hardcodedValues[key] || []; + currentValues.splice(index, 1); + handleInputChange(key, [...currentValues]); + }; + + const handleArrayItemChange = (key: string, index: number, value: string) => { + const currentValues = data.hardcodedValues[key] || []; + currentValues[index] = value; + handleInputChange(key, [...currentValues]); + }; + + const addDynamicTextInput = () => { + const dynamicKeyPrefix = 'texts_$_'; + const currentKeys = Object.keys(data.hardcodedValues).filter(key => key.startsWith(dynamicKeyPrefix)); + const nextIndex = currentKeys.length + 1; + const newKey = `${dynamicKeyPrefix}${nextIndex}`; + handleInputChange(newKey, ''); + }; + + const removeDynamicTextInput = (key: string) => { + const newValues = { ...data.hardcodedValues }; + delete newValues[key]; + data.setHardcodedValues(newValues); + }; + + const handleDynamicTextInputChange = (key: string, value: string) => { + handleInputChange(key, value); + }; + const renderInputField = (key: string, schema: any) => { + const error = errors[key]; switch (schema.type) { case 'string': return schema.enum ? ( @@ -99,12 +189,14 @@ const CustomNode: FC = ({ data, id }) => { ))} + {error && {error}} ) : (
handleInputClick(key)}> {data.hardcodedValues[key] || `Enter ${key}`}
+ {error && {error}}
); case 'boolean': @@ -128,9 +220,9 @@ const CustomNode: FC = ({ data, id }) => { /> False + {error && {error}} ); - case 'integer': case 'number': return (
@@ -140,23 +232,31 @@ const CustomNode: FC = ({ data, id }) => { onChange={(e) => handleInputChange(key, parseFloat(e.target.value))} className="number-input" /> + {error && {error}}
); case 'array': - if (schema.items && schema.items.type === 'string' && schema.items.enum) { + if (schema.items && schema.items.type === 'string') { + const arrayValues = data.hardcodedValues[key] || []; return (
- + {arrayValues.map((item: string, index: number) => ( +
+ handleArrayItemChange(key, index, e.target.value)} + className="array-item-input" + /> + +
+ ))} + + {error && {error}}
); } @@ -166,11 +266,65 @@ const CustomNode: FC = ({ data, id }) => { } }; + const renderDynamicTextFields = () => { + const dynamicKeyPrefix = 'texts_$_'; + const dynamicKeys = Object.keys(data.hardcodedValues).filter(key => key.startsWith(dynamicKeyPrefix)); + + return dynamicKeys.map((key, index) => ( +
+
+ + {key} + {!isHandleConnected(key) && ( + <> + handleDynamicTextInputChange(key, e.target.value)} + className="dynamic-text-input" + /> + + + )} +
+
+ )); + }; + + const validateInputs = () => { + const newErrors: { [key: string]: string | null } = {}; + Object.keys(data.inputSchema.properties).forEach((key) => { + const value = data.hardcodedValues[key]; + const schema = data.inputSchema.properties[key]; + const error = validateInput(key, value, schema); + if (error) { + newErrors[key] = error; + } + }); + setErrors(newErrors); + return Object.values(newErrors).every((error) => error === null); + }; + + const handleSubmit = () => { + if (validateInputs()) { + console.log("Valid data:", data.hardcodedValues); + } else { + console.log("Invalid data:", errors); + } + }; + return (
-
{data?.title.replace(/\d+/g, '')}
+
{data.blockType || data.title}
@@ -180,16 +334,36 @@ const CustomNode: FC = ({ data, id }) => { {data.inputSchema && Object.keys(data.inputSchema.properties).map((key) => (
-
- - {key} -
- {!isHandleConnected(key) && renderInputField(key, data.inputSchema.properties[key])} + {key !== 'texts' ? ( +
+
+ + {key} +
+ {!isHandleConnected(key) && renderInputField(key, data.inputSchema.properties[key])} +
+ ) : ( +
+
+ + {key} +
+ {renderDynamicTextFields()} + +
+ )}
))}
@@ -212,6 +386,7 @@ const CustomNode: FC = ({ data, id }) => {

)} + setIsModalOpen(false)} @@ -222,4 +397,4 @@ const CustomNode: FC = ({ data, id }) => { ); }; -export default memo(CustomNode); +export default memo(CustomNode); \ No newline at end of file diff --git a/rnd/autogpt_builder/src/components/Flow.tsx b/rnd/autogpt_builder/src/components/Flow.tsx index aebd99c7f295..f79e07f6ccbb 100644 --- a/rnd/autogpt_builder/src/components/Flow.tsx +++ b/rnd/autogpt_builder/src/components/Flow.tsx @@ -1,6 +1,5 @@ "use client"; - -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; import ReactFlow, { addEdge, applyNodeChanges, @@ -11,26 +10,39 @@ import ReactFlow, { OnEdgesChange, OnConnect, NodeTypes, - EdgeRemoveChange, + Connection, } 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, +type Schema = { + type: string; + properties: { [key: string]: any }; + required?: string[]; +}; + +type CustomNodeData = { + blockType: string; + title: string; + inputSchema: Schema; + outputSchema: Schema; + hardcodedValues: { [key: string]: any }; + setHardcodedValues: (values: { [key: string]: any }) => void; + connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>; + isPropertiesOpen: boolean; + status?: string; + output_data?: any; + block_id: string; }; -interface AvailableNode { +type AvailableNode = { id: string; name: string; description: string; - inputSchema?: { properties: { [key: string]: any }; required?: string[] }; - outputSchema?: { properties: { [key: string]: any } }; -} + inputSchema: Schema; + outputSchema: Schema; +}; interface ExecData { node_id: string; @@ -38,94 +50,96 @@ interface ExecData { output_data: any; } +const Sidebar: React.FC<{isOpen: boolean, availableNodes: AvailableNode[], addNode: (id: string, name: string) => void}> = + ({isOpen, availableNodes, addNode}) => { + const [searchQuery, setSearchQuery] = useState(''); + + if (!isOpen) return null; + + const filteredNodes = availableNodes.filter(node => + node.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( +
+

Nodes

+ setSearchQuery(e.target.value)} + /> + {filteredNodes.map((node) => ( +
+ {node.name} + +
+ ))} +
+ ); +}; + const Flow: React.FC = () => { - const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState(initialEdges); + const [nodes, setNodes] = useState[]>([]); + const [edges, setEdges] = useState([]); const [nodeId, setNodeId] = useState(1); - const [modalIsOpen, setModalIsOpen] = useState(false); - const [selectedNode, setSelectedNode] = useState(null); - const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); - const [variableName, setVariableName] = useState(''); - const [variableValue, setVariableValue] = useState(''); - const [printVariable, setPrintVariable] = useState(''); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); const [availableNodes, setAvailableNodes] = useState([]); - const [loadingStatus, setLoadingStatus] = useState<'loading' | 'failed' | 'loaded'>('loading'); const [agentId, setAgentId] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); - const apiUrl = 'http://localhost:8000' + 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'); - }); + .then(response => response.json()) + .then(data => setAvailableNodes(data)) + .catch(error => console.error('Error fetching available blocks:', error)); }, []); + const nodeTypes: NodeTypes = useMemo(() => ({ custom: CustomNode }), []); + const onNodesChange: OnNodesChange = useCallback( - (changes) => setNodes((nds) => applyNodeChanges(changes, nds).map(node => ({ - ...node, - data: { - ...node.data, - metadata: { - ...node.data.metadata, - position: node.position - } - } - }))), + (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [] ); 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 } }; - }) - ); - } - }, + (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [] ); const onConnect: OnConnect = useCallback( - (connection) => { + (connection: 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.target}`); - return { ...node, data: { ...node.data, connections } }; - } if (node.id === connection.target) { - const connections = node.data.connections || []; - connections.push(`${connection.source} -> ${node.data.title} ${connection.targetHandle}`); - return { ...node, data: { ...node.data, connections } }; + return { + ...node, + data: { + ...node.data, + connections: [ + ...node.data.connections, + { + source: connection.source, + sourceHandle: connection.sourceHandle, + target: connection.target, + targetHandle: connection.targetHandle, + } as { source: string; sourceHandle: string; target: string; targetHandle: string }, + ], + }, + }; } return node; }) @@ -134,127 +148,79 @@ const Flow: React.FC = () => { [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 addNode = (blockId: string, nodeType: string) => { + const nodeSchema = availableNodes.find(node => node.id === blockId); + if (!nodeSchema) { + console.error(`Schema not found for block ID: ${blockId}`); + return; + } - const newNode: Node = { + const newNode: Node = { id: nodeId.toString(), type: 'custom', + position: { x: Math.random() * 400, y: Math.random() * 400 }, data: { - label: label, - title: `${type} ${nodeId}`, - description: `${description}`, - inputSchema: nodeSchema?.inputSchema, - outputSchema: nodeSchema?.outputSchema, - connections: [], - variableName: '', - variableValue: '', - printVariable: '', - setVariableName, - setVariableValue, - setPrintVariable, + blockType: nodeType, + title: `${nodeType} ${nodeId}`, + inputSchema: nodeSchema.inputSchema, + outputSchema: nodeSchema.outputSchema, hardcodedValues: {}, setHardcodedValues: (values: { [key: string]: any }) => { setNodes((nds) => nds.map((node) => - node.id === nodeId.toString() + node.id === newNode.id ? { ...node, data: { ...node.data, hardcodedValues: values } } : node )); }, - block_id: nodeSchema?.id || '', - metadata: { - position // Store position in metadata - } + connections: [], + isPropertiesOpen: false, + block_id: blockId, }, - position, }; + setNodes((nds) => [...nds, newNode]); - setNodeId((id) => id + 1); + setNodeId((prevId) => prevId + 1); }; - const closeModal = () => { - setModalIsOpen(false); - setSelectedNode(null); - }; + const prepareNodeInputData = (node: Node, allNodes: Node[], allEdges: Edge[]) => { + console.log("Preparing input data for node:", node.id, node.data.blockType); - 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 blockSchema = availableNodes.find(n => n.id === node.data.block_id)?.inputSchema; - const filteredNodes = availableNodes.filter(node => node.name.toLowerCase().includes(searchQuery.toLowerCase())); + if (!blockSchema) { + console.error(`Schema not found for block ID: ${node.data.block_id}`); + return {}; + } - 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 } = { ...node.data.hardcodedValues }; - let inputData: { [key: string]: any } = {}; - const inputProperties = nodeSchema.inputSchema.properties; + // Get data from connected nodes + const incomingEdges = allEdges.filter(edge => edge.target === node.id); + incomingEdges.forEach(edge => { + const sourceNode = allNodes.find(n => n.id === edge.source); + if (sourceNode && sourceNode.data.output_data) { + const outputKey = Object.keys(sourceNode.data.output_data)[0]; // Assuming single output + inputData[edge.targetHandle as string] = sourceNode.data.output_data[outputKey]; + } + }); - Object.keys(inputProperties).forEach(prop => { - const inputEdges = allEdges.filter(edge => edge.target === node.id && edge.targetHandle === prop); - if (inputEdges.length > 0) { - const sourceNode = allNodes.find(n => n.id === inputEdges[0].source); - if (sourceNode && sourceNode.data.output_data) { - // Map the output of the source node to the input of the target node - const sourceOutput = sourceNode.data.output_data; - const outputKey = Object.keys(sourceOutput)[0]; // Assume first output key - inputData[prop] = sourceOutput[outputKey]; - } - } else if (node.data.hardcodedValues && node.data.hardcodedValues[prop]) { - inputData[prop] = node.data.hardcodedValues[prop]; + // Filter out any inputs that are not in the block's schema + Object.keys(inputData).forEach(key => { + if (!blockSchema.properties[key]) { + delete inputData[key]; } }); + console.log(`Final prepared input for ${node.data.blockType} (${node.id}):`, inputData); 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 { + console.log("All nodes before formatting:", nodes); + const formattedNodes = nodes.map(node => { + console.log("Formatting node:", node.id, node.data.blockType); const inputDefault = prepareNodeInputData(node, nodes, edges); const inputNodes = edges .filter(edge => edge.target === node.id) @@ -273,20 +239,22 @@ const Flow: React.FC = () => { return { id: node.id, block_id: node.data.block_id, - input_default: inputNodes.length === 0 ? inputDefault : {}, + input_default: inputDefault, input_nodes: inputNodes, output_nodes: outputNodes, - metadata: node.data.metadata + metadata: { position: node.position } }; }); const payload = { - id: '', + id: agentId || '', name: 'Agent Name', description: 'Agent Description', nodes: formattedNodes, }; + console.log("Payload being sent to the API:", JSON.stringify(payload, null, 2)); + const createResponse = await fetch(`${apiUrl}/graphs`, { method: 'POST', headers: { @@ -300,77 +268,19 @@ const Flow: React.FC = () => { } 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); - 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: node.input_nodes.map((input: any) => `${input.node_id}-${input.name} -> ${node.id}`), - 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 newAgentId = createData.id; + setAgentId(newAgentId); - const newEdges = createData.nodes.flatMap((node: any) => { - return node.output_nodes.map((outputNode: { name: string; node_id: string }) => ({ - id: `${node.id}-${outputNode.name}-${outputNode.node_id}`, - source: node.id, - sourceHandle: outputNode.name, - target: outputNode.node_id, - targetHandle: node.input_nodes.find((inputNode: { name: string; node_id: string }) => inputNode.node_id === outputNode.node_id)?.name || '', - })); - }); + console.log('Response from the API:', JSON.stringify(createData, null, 2)); - 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}/graphs/${newAgentId}/execute`, { - const executeResponse = await fetch(`${apiUrl}/graphs/${agentId}/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(nodeInputForExecution), + body: JSON.stringify({}), }); if (!executeResponse.ok) { @@ -378,210 +288,100 @@ const Flow: React.FC = () => { } const executeData = await executeResponse.json(); - const runId = executeData.id; // Correctly capturing runId from executeData - const startPolling = () => { - const endTime = Date.now() + 60000; - - const poll = async () => { - if (Date.now() >= endTime) { - console.log('Polling timeout reached.'); - return; - } + const runId = executeData.id; - try { - const response = await fetch(`${apiUrl}/graphs/${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(); + const pollExecution = async () => { + const response = await fetch(`${apiUrl}/graphs/${newAgentId}/executions/${runId}`); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + data.forEach(updateNodeData); + if (data.every((node: any) => node.status === 'COMPLETED')) { + console.log('All nodes completed execution'); + } else { + setTimeout(pollExecution, 1000); + } }; - startPolling(); + pollExecution(); + } catch (error) { console.error('Error running agent:', error); } }; + const updateNodesWithExecutionData = (executionData: any[]) => { + setNodes((nds) => + nds.map((node) => { + const nodeExecution = executionData.find((exec) => exec.node_id === node.id); + if (nodeExecution) { + return { + ...node, + data: { + ...node.data, + status: nodeExecution.status, + output_data: nodeExecution.output_data, + isPropertiesOpen: true, + }, + }; + } + return node; + }) + ); + }; + + const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); + + 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; + }) + ); + }; + return ( -
-
- - - {agentId && ( - - Agent ID: {agentId} - - )} -
-
- -
- {selectedNode && ( - -

Edit Node

-
{ - e.preventDefault(); - saveNodeData(); - }} - > -
- -
-
-