Compare commits

...

14 Commits

Author SHA1 Message Date
Bentlybro
dc4f8524c0 fix source issue + speed up polling 2024-06-26 15:13:37 +01:00
Bentlybro
e801e18559 Multinode runs partially working -
Need to do more testing with this, wanted to push for others to try and checkout
2024-06-26 15:13:37 +01:00
Bentlybro
0edc2dfed5 fix node names from being "undefined" after a run 2024-06-26 15:13:36 +01:00
Bentlybro
e8f889e8ba fixed node-regeneration and rebuilding from backend reply 2024-06-26 15:13:36 +01:00
Bently
4f6c3c01ba added package files as they where missing 2024-06-26 15:13:36 +01:00
Bentlybro
4660ac93f4 upload "public" folder 2024-06-26 15:13:36 +01:00
Bentlybro
89bfd5ee68 fix for nodes to be placed
but this does need more work and i need to add error handling/management for when it does not properly load
2024-06-26 15:13:36 +01:00
Bentlybro
16f399edd5 fix position data 2024-06-26 15:13:36 +01:00
Bentlybro
0186ca7120 rebuild graphs on api call + pos and metadata 2024-06-26 15:13:36 +01:00
Bentlybro
59c7136b7b Made data being sent to api dynamically made 2024-06-26 15:13:36 +01:00
Bentlybro
2407945ec4 more updates for api calls 2024-06-26 15:13:36 +01:00
Bentlybro
554bd82d8f Agent gen and execute working for PrintingBlocks
need to make it more dynamic for other nodes too
2024-06-26 15:13:36 +01:00
Bentlybro
937a8e7d6a Improved node properties window 2024-06-26 15:13:36 +01:00
Bentlybro
ce3117e6e1 Initial files 2024-06-26 15:13:36 +01:00
22 changed files with 19769 additions and 0 deletions

View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -0,0 +1,13 @@
# AutoGPT-Builder
for this we have used the [ReactFlow](https://github.com/xyflow/xyflow)
# Getting Started with AutoGPT-Builder
To use make sure you have NPM installed and do
```
npm install reactflow
```
then
```
npm start
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
{
"name": "graph",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.98",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"react": "^18.3.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-modal": "^3.16.1",
"react-scripts": "^5.0.1",
"reactflow": "^11.11.3",
"typescript": "^4.9.5",
"uuid": "^10.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-modal": "^3.16.3",
"@types/uuid": "^9.0.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,12 @@
import React from 'react';
import Flow from './Flow';
const App: React.FC = () => {
return (
<div className="App">
<Flow />
</div>
);
};
export default App;

View File

@@ -0,0 +1,115 @@
import React, { useState, useEffect } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import 'reactflow/dist/style.css';
type Schema = {
properties: { [key: string]: any };
};
const CustomNode: React.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 CustomNode;

View File

@@ -0,0 +1,609 @@
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 './index.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);
useEffect(() => {
fetch('http://192.168.0.215:8000/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('http://192.168.0.215:8000/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(`http://192.168.0.215:8000/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(`http://192.168.0.215:8000/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 style={{ height: '100vh', position: 'relative', backgroundColor: '#121212' }}>
<div style={{ position: 'absolute', top: '20px', left: isSidebarOpen ? '400px' : '20px', zIndex: 10, transition: 'left 0.3s ease' }}>
<button
onClick={toggleSidebar}
style={{
padding: '10px 20px',
fontSize: '16px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Nodes
</button>
<button
onClick={runAgent}
style={{
padding: '10px 20px',
fontSize: '16px',
backgroundColor: 'green',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: '10px',
}}
>
Run
</button>
{agentId && (
<span style={{ marginLeft: '10px', color: '#fff', fontSize: '16px' }}>
Agent ID: {agentId}
</span>
)}
</div>
<div style={{ height: '100%', width: '100%' }}>
<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,120 @@
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-osx-smoothing: grayscale;
background-color: #121212;
color: #e0e0e0;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
button {
background-color: #333;
color: #e0e0e0;
padding: 10px;
border: none;
border-radius: 4px;
}
button:hover {
background-color: #555;
}
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: #fff;
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
}
.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;
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve"
},
"include": [
"src"
]
}