mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
(rnd) Add support for multiple, dynamic inputs (#7296)
multi dynamic inputs
This commit is contained in:
@@ -10,11 +10,25 @@ type Schema = {
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ 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<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [activeKey, setActiveKey] = useState<string | null>(null);
|
||||
const [modalValue, setModalValue] = useState<string>('');
|
||||
const [errors, setErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (data.output_data || data.status) {
|
||||
@@ -22,6 +36,10 @@ const CustomNode: FC<NodeProps> = ({ 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<NodeProps> = ({ 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<NodeProps> = ({ 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<NodeProps> = ({ data, id }) => {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
) : (
|
||||
<div key={key} className="input-container">
|
||||
<div className="clickable-input" onClick={() => handleInputClick(key)}>
|
||||
{data.hardcodedValues[key] || `Enter ${key}`}
|
||||
</div>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'boolean':
|
||||
@@ -128,9 +220,9 @@ const CustomNode: FC<NodeProps> = ({ data, id }) => {
|
||||
/>
|
||||
False
|
||||
</label>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return (
|
||||
<div key={key} className="input-container">
|
||||
@@ -140,23 +232,31 @@ const CustomNode: FC<NodeProps> = ({ data, id }) => {
|
||||
onChange={(e) => handleInputChange(key, parseFloat(e.target.value))}
|
||||
className="number-input"
|
||||
/>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
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 (
|
||||
<div key={key} className="input-container">
|
||||
<select
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="select-input"
|
||||
>
|
||||
{schema.items.enum.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{arrayValues.map((item: string, index: number) => (
|
||||
<div key={`${key}-${index}`} className="array-item-container">
|
||||
<input
|
||||
type="text"
|
||||
value={item}
|
||||
onChange={(e) => handleArrayItemChange(key, index, e.target.value)}
|
||||
className="array-item-input"
|
||||
/>
|
||||
<button onClick={() => removeArrayItem(key, index)} className="array-item-remove">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => addArrayItem(key)} className="array-item-add">
|
||||
Add Item
|
||||
</button>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -166,11 +266,65 @@ const CustomNode: FC<NodeProps> = ({ data, id }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderDynamicTextFields = () => {
|
||||
const dynamicKeyPrefix = 'texts_$_';
|
||||
const dynamicKeys = Object.keys(data.hardcodedValues).filter(key => key.startsWith(dynamicKeyPrefix));
|
||||
|
||||
return dynamicKeys.map((key, index) => (
|
||||
<div key={key} className="input-container">
|
||||
<div className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span className="handle-label">{key}</span>
|
||||
{!isHandleConnected(key) && (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={data.hardcodedValues[key]}
|
||||
onChange={(e) => handleDynamicTextInputChange(key, e.target.value)}
|
||||
className="dynamic-text-input"
|
||||
/>
|
||||
<button onClick={() => removeDynamicTextInput(key)} className="array-item-remove">
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="custom-node">
|
||||
<div className="node-header">
|
||||
<div className="node-title">{data?.title.replace(/\d+/g, '')}</div>
|
||||
<div className="node-title">{data.blockType || data.title}</div>
|
||||
<button onClick={toggleProperties} className="toggle-button">
|
||||
☰
|
||||
</button>
|
||||
@@ -180,16 +334,36 @@ const CustomNode: FC<NodeProps> = ({ data, id }) => {
|
||||
{data.inputSchema &&
|
||||
Object.keys(data.inputSchema.properties).map((key) => (
|
||||
<div key={key}>
|
||||
<div className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span className="handle-label">{key}</span>
|
||||
</div>
|
||||
{!isHandleConnected(key) && renderInputField(key, data.inputSchema.properties[key])}
|
||||
{key !== 'texts' ? (
|
||||
<div>
|
||||
<div className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span className="handle-label">{key}</span>
|
||||
</div>
|
||||
{!isHandleConnected(key) && renderInputField(key, data.inputSchema.properties[key])}
|
||||
</div>
|
||||
) : (
|
||||
<div key={key} className="input-container">
|
||||
<div className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span className="handle-label">{key}</span>
|
||||
</div>
|
||||
{renderDynamicTextFields()}
|
||||
<button onClick={addDynamicTextInput} className="array-item-add">
|
||||
Add Text Input
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -212,6 +386,7 @@ const CustomNode: FC<NodeProps> = ({ data, id }) => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<button onClick={handleSubmit}>Submit</button>
|
||||
<ModalComponent
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
@@ -222,4 +397,4 @@ const CustomNode: FC<NodeProps> = ({ data, id }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
export default memo(CustomNode);
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
interface AvailableNode {
|
||||
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;
|
||||
};
|
||||
|
||||
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 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 Sidebar: React.FC<{isOpen: boolean, availableNodes: AvailableNode[], addNode: (id: string, name: string) => void}> =
|
||||
({isOpen, availableNodes, addNode}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const apiUrl = 'http://localhost:8000'
|
||||
if (!isOpen) return null;
|
||||
|
||||
const filteredNodes = availableNodes.filter(node =>
|
||||
node.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '250px',
|
||||
backgroundColor: '#333',
|
||||
padding: '20px',
|
||||
zIndex: 4,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<h3 style={{color: '#fff'}}>Nodes</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search nodes..."
|
||||
style={{width: '100%', marginBottom: '10px', padding: '5px'}}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{filteredNodes.map((node) => (
|
||||
<div key={node.id} style={{marginBottom: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||
<span style={{color: '#fff'}}>{node.name}</span>
|
||||
<button onClick={() => addNode(node.id, node.name)}>Add</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Flow: React.FC = () => {
|
||||
const [nodes, setNodes] = useState<Node<CustomNodeData>[]>([]);
|
||||
const [edges, setEdges] = useState<Edge[]>([]);
|
||||
const [nodeId, setNodeId] = useState<number>(1);
|
||||
const [availableNodes, setAvailableNodes] = useState<AvailableNode[]>([]);
|
||||
const [agentId, setAgentId] = useState<string | null>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
|
||||
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<CustomNodeData> = {
|
||||
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<CustomNodeData>, allNodes: Node<CustomNodeData>[], 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 blockSchema = availableNodes.find(n => n.id === node.data.block_id)?.inputSchema;
|
||||
|
||||
if (!blockSchema) {
|
||||
console.error(`Schema not found for block ID: ${node.data.block_id}`);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
};
|
||||
let inputData: { [key: string]: any } = { ...node.data.hardcodedValues };
|
||||
|
||||
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;
|
||||
|
||||
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];
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
|
||||
return inputData;
|
||||
};
|
||||
// Filter out any inputs that are not in the block's schema
|
||||
Object.keys(inputData).forEach(key => {
|
||||
if (!blockSchema.properties[key]) {
|
||||
delete inputData[key];
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
})
|
||||
);
|
||||
console.log(`Final prepared input for ${node.data.blockType} (${node.id}):`, inputData);
|
||||
return inputData;
|
||||
};
|
||||
|
||||
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 executeResponse = await fetch(`${apiUrl}/graphs/${newAgentId}/execute`, {
|
||||
|
||||
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/${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 runId = executeData.id;
|
||||
|
||||
const poll = async () => {
|
||||
if (Date.now() >= endTime) {
|
||||
console.log('Polling timeout reached.');
|
||||
return;
|
||||
}
|
||||
|
||||
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 (
|
||||
<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 style={{ height: '100vh', width: '100%' }}>
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: isSidebarOpen ? '260px' : '10px',
|
||||
top: '10px',
|
||||
zIndex: 5,
|
||||
transition: 'left 0.3s'
|
||||
}}
|
||||
>
|
||||
{isSidebarOpen ? 'Hide Sidebar' : 'Show Sidebar'}
|
||||
</button>
|
||||
<Sidebar isOpen={isSidebarOpen} availableNodes={availableNodes} addNode={addNode} />
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
>
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={runAgent}>Run Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
</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;
|
||||
export default Flow;
|
||||
0
rnd/autogpt_builder/src/components/custominput.css
Normal file
0
rnd/autogpt_builder/src/components/custominput.css
Normal file
@@ -1,25 +1,22 @@
|
||||
.custom-node {
|
||||
padding: 20px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 12px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 20px;
|
||||
background: #333;
|
||||
color: #e0e0e0;
|
||||
width: 260px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
font-family: 'Arial', sans-serif;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
@@ -27,16 +24,20 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #e0e0e0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-section, .output-section {
|
||||
.input-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.handle-label {
|
||||
@@ -53,91 +54,86 @@
|
||||
}
|
||||
|
||||
.input-container {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.clickable-input {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
background: #2a2a2a;
|
||||
background: #444;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.clickable-input:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.select-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
background: #2a2a2a;
|
||||
background: #444;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.radio-label input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
background: #2a2a2a;
|
||||
background: #444;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.array-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.array-item-input {
|
||||
flex-grow: 1;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
background: #444;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.array-item-remove {
|
||||
background: #d9534f;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.array-item-add {
|
||||
background: #5bc0de;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.node-properties {
|
||||
margin-top: 20px;
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background: #444;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.node-properties h4 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.node-properties p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.custom-node {
|
||||
width: 90%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.clickable-input {
|
||||
padding: 8px;
|
||||
}
|
||||
.error-message {
|
||||
color: #d9534f;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user