diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 4294bde..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,26 +0,0 @@
-package-lock.json
-
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/Canvas.js b/Canvas.js
new file mode 100644
index 0000000..61b12f5
--- /dev/null
+++ b/Canvas.js
@@ -0,0 +1,182 @@
+// Canvas.js
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import ReactFlow, { MiniMap, Controls, Background, useReactFlow } from 'reactflow';
+import 'reactflow/dist/style.css';
+import Node, { addNode, deleteNode } from './Node';
+import { createEdge, deleteEdge } from './Edge';
+import { createConditionEdge, deleteConditionEdge } from './ConditionEdge';
+import { useGraphManager } from './GraphManager';
+import Panel from './Panel';
+const nodeTypes = { textUpdater: Node };
+
+function Canvas() {
+ const {
+ nodes,
+ setNodes,
+ onNodesChange,
+ edges,
+ setEdges,
+ onEdgesChange,
+ nodeIdCounter,
+ setNodeIdCounter,
+ } = useGraphManager();
+
+ const [contextMenu, setContextMenu] = useState(null);
+ const { screenToFlowPosition } = useReactFlow();
+ const menuBarRef = useRef(null);
+ const [canvasHeight, setCanvasHeight] = useState(window.innerHeight);
+ const [showConfig, setShowConfig] = useState(false);
+ const [showRun, setShowRun] = useState(false);
+
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (menuBarRef.current) {
+ const menuBarHeight = menuBarRef.current.offsetHeight;
+ setCanvasHeight(window.innerHeight - menuBarHeight - 10);
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+ handleResize();
+
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ const handleAddNode = useCallback(() => {
+ const newPosition = screenToFlowPosition({ x: contextMenu.mouseX, y: contextMenu.mouseY });
+ addNode(nodes, setNodes, nodeIdCounter, setNodeIdCounter, newPosition);
+ setContextMenu(null);
+ }, [contextMenu, nodeIdCounter, setNodes, screenToFlowPosition, nodes, setNodeIdCounter]);
+
+ const handleDeleteNode = useCallback(() => {
+ if (contextMenu && contextMenu.nodeId) {
+ deleteNode(nodes, setNodes, edges, setEdges, contextMenu.nodeId);
+ }
+ setContextMenu(null);
+ }, [contextMenu, setNodes, setEdges, nodes, edges]);
+
+ const handleDeleteEdge = useCallback(() => {
+ if (contextMenu && contextMenu.edgeId) {
+ const edge = edges.find((e) => e.id === contextMenu.edgeId);
+ if (edge) {
+ if (edge.sourceHandle === 'true' || edge.sourceHandle === 'false') {
+ deleteConditionEdge(edges, setEdges, contextMenu.edgeId, nodes, setNodes);
+ } else {
+ deleteEdge(edges, setEdges, contextMenu.edgeId, nodes, setNodes);
+ }
+ }
+ }
+ setContextMenu(null);
+ }, [contextMenu, setEdges, edges, nodes, setNodes]);
+
+ const handleNodeContextMenu = useCallback((event, node) => {
+ event.preventDefault();
+ setContextMenu({
+ mouseX: event.clientX,
+ mouseY: event.clientY,
+ nodeId: node.id,
+ isNode: true,
+ isEdge: false,
+ });
+ }, []);
+
+ const handlePaneContextMenu = useCallback((event) => {
+ event.preventDefault();
+ setContextMenu({
+ mouseX: event.clientX,
+ mouseY: event.clientY,
+ isNode: false,
+ isEdge: false,
+ });
+ }, []);
+
+ const handleEdgeContextMenu = useCallback((event, edge) => {
+ event.preventDefault();
+ setContextMenu({
+ mouseX: event.clientX,
+ mouseY: event.clientY,
+ edgeId: edge.id,
+ isNode: false,
+ isEdge: true,
+ });
+ }, []);
+
+ const handleCloseContextMenu = () => {
+ setContextMenu(null);
+ };
+
+ const onConnect = useCallback((params) => {
+ const sourceNode = nodes.find(node => node.id === params.source);
+
+ if (params.sourceHandle === 'true') {
+ if (sourceNode.data.true_next !== null) {
+ alert('True port already has a connection.');
+ return;
+ }
+ createConditionEdge(edges, setEdges, params, nodes, setNodes);
+ } else if (params.sourceHandle === 'false') {
+ if (sourceNode.data.false_next !== null) {
+ alert('False port already has a connection.');
+ return;
+ }
+ createConditionEdge(edges, setEdges, params, nodes, setNodes);
+ } else {
+ createEdge(edges, setEdges, params, nodes, setNodes);
+ }
+ }, [setEdges, edges, nodes, setNodes]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {contextMenu && (
+
+ {contextMenu.isNode ? (
+
+ ) : contextMenu.isEdge ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ );
+}
+
+export default Canvas;
\ No newline at end of file
diff --git a/ConditionEdge.js b/ConditionEdge.js
new file mode 100644
index 0000000..7fb9f60
--- /dev/null
+++ b/ConditionEdge.js
@@ -0,0 +1,62 @@
+// ConditionEdge.js
+
+import { addEdge } from 'reactflow';
+
+export const createConditionEdge = (edges, setEdges, params, nodes, setNodes) => {
+ const { source, target, sourceHandle } = params;
+
+ const newEdge = {
+ ...params,
+ id: `e${source}-${target}`,
+ animated: false,
+ style: { stroke: sourceHandle === 'true' ? 'green' : 'red', strokeWidth: 2 },
+ markerEnd: {
+ type: 'arrowclosed',
+ width: 20,
+ height: 20,
+ },
+ };
+
+ setEdges((eds) => addEdge(newEdge, eds));
+
+ setNodes((nds) =>
+ nds.map((node) => {
+ if (node.id === source) {
+ const newData = {
+ ...node.data,
+ ...(sourceHandle === 'true' ? { true_next: target } : { false_next: target })
+ };
+ return { ...node, data: newData };
+ } else if (node.id === target) {
+ return { ...node, prevs: [...(node.prevs || []), source] };
+ }
+ return node;
+ })
+ );
+
+ return newEdge;
+};
+
+export const deleteConditionEdge = (edges, setEdges, edgeId, nodes, setNodes) => {
+ const edge = edges.find((e) => e.id === edgeId);
+ if (!edge) return;
+
+ setEdges((eds) => eds.filter((edge) => edge.id !== edgeId));
+
+ setNodes((nds) =>
+ nds.map((node) => {
+ if (node.id === edge.source) {
+ const newData = {
+ ...node.data,
+ ...(edge.sourceHandle === 'true'
+ ? { true_next: null }
+ : { false_next: null })
+ };
+ return { ...node, data: newData };
+ } else if (node.id === edge.target) {
+ return { ...node, prevs: node.prevs.filter((id) => id !== edge.source) };
+ }
+ return node;
+ })
+ );
+};
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 922e683..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,18 +0,0 @@
-FROM node:20
-
-# Create and set the working directory
-WORKDIR /app
-
-# Copy all files to the container
-COPY . .
-
-# Install dependencies
-RUN npm install
-
-RUN npm run build
-
-# Expose the port the app runs on
-EXPOSE 3000
-
-# Start the application
-CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "3000"]
\ No newline at end of file
diff --git a/Edge.js b/Edge.js
new file mode 100644
index 0000000..ae80dd1
--- /dev/null
+++ b/Edge.js
@@ -0,0 +1,54 @@
+// Edge.js
+
+import { addEdge } from 'reactflow';
+
+export const createEdge = (edges, setEdges, params, nodes, setNodes) => {
+ const { source, target } = params;
+
+ const newEdge = {
+ ...params,
+ id: `e${source}-${target}`,
+ animated: false,
+ style: { stroke: '#000000', strokeWidth: 2 },
+ markerEnd: {
+ type: 'arrowclosed',
+ width: 20,
+ height: 20,
+ },
+ };
+
+ setEdges((eds) => addEdge(newEdge, eds));
+
+ setNodes((nds) =>
+ nds.map((node) => {
+ if (node.id === source) {
+ const newData = { ...node.data, nexts: [...node.data.nexts, target] };
+ return { ...node, data: newData };
+ } else if (node.id === target) {
+ return { ...node, prevs: [...(node.prevs || []), source] };
+ }
+ return node;
+ })
+ );
+
+ return newEdge;
+};
+
+export const deleteEdge = (edges, setEdges, edgeId, nodes, setNodes) => {
+ const edge = edges.find((e) => e.id === edgeId);
+ if (!edge) return;
+
+ setEdges((eds) => eds.filter((edge) => edge.id !== edgeId));
+
+ setNodes((nds) =>
+ nds.map((node) => {
+ if (node.id === edge.source) {
+ const newData = { ...node.data, nexts: node.data.nexts.filter((id) => id !== edge.target) };
+ return { ...node, data: newData };
+ } else if (node.id === edge.target) {
+ return { ...node, prevs: node.prevs.filter((id) => id !== edge.source) };
+ }
+ return node;
+ })
+ );
+};
diff --git a/FileTransmit.js b/FileTransmit.js
new file mode 100644
index 0000000..bc8bbb9
--- /dev/null
+++ b/FileTransmit.js
@@ -0,0 +1,128 @@
+// FileTransmit.js
+
+import React, { useRef } from 'react';
+import SERVER_URL from '../config';
+import ConfigManager from '../ConfigManager';
+
+function FileTransmit({ onUploadComplete }) {
+ const fileInputRef = useRef();
+
+ // Get the username from ConfigManager
+ const { username } = ConfigManager.getSettings();
+
+ const handleUploadClick = () => {
+ fileInputRef.current.click();
+ };
+
+ const handleFileChange = async (event) => {
+ const files = event.target.files;
+ if (files.length > 0) {
+ const formData = new FormData();
+ for (const file of files) {
+ formData.append('files', file);
+ }
+
+ try {
+ // Use the username in the API path
+ const response = await fetch(`${SERVER_URL}/upload/${encodeURIComponent(username)}`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (response.ok) {
+ alert('Files successfully uploaded');
+ if (onUploadComplete) {
+ onUploadComplete();
+ }
+ } else {
+ const errorData = await response.json();
+ alert('Upload failed: ' + errorData.error);
+ }
+ } catch (error) {
+ alert('Upload failed: ' + error.message);
+ } finally {
+ event.target.value = null;
+ }
+ }
+ };
+
+ const handleDownloadClick = async () => {
+ try {
+ // Send GET request to the backend to download the zip file
+ const response = await fetch(`${SERVER_URL}/download/${encodeURIComponent(username)}`);
+
+ if (response.ok) {
+ // Create a Blob from the response data (the zip file)
+ const blob = await response.blob();
+
+ // Create a download link and simulate a click to trigger the download
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${username}_workspace.zip`; // Set the default file name
+ a.click();
+
+ // Clean up the object URL
+ URL.revokeObjectURL(url);
+ } else {
+ const errorData = await response.json();
+ alert('Download failed: ' + errorData.error);
+ }
+ } catch (error) {
+ alert('Download failed: ' + error.message);
+ }
+ };
+
+ // function to handle cache cleanup with username
+ const handleCleanCacheClick = async () => {
+ try {
+ // Use the username in the API path
+ const response = await fetch(`${SERVER_URL}/clean-cache/${encodeURIComponent(username)}`, {
+ method: 'POST'
+ });
+
+ if (response.ok) {
+ alert('Cache successfully cleaned');
+ } else {
+ const errorData = await response.json();
+ alert('Clean cache failed: ' + errorData.error);
+ }
+ } catch (error) {
+ alert('Clean cache failed: ' + error.message);
+ }
+ };
+
+ // Check if username is valid
+ const isUsernameValid = username && username.length > 0;
+
+ return (
+
+
+
+
+
+
+
+
+ {isUsernameValid ? `User: ${username}` : 'User: undefined'}
+
+
+
+ );
+}
+
+export default FileTransmit;
\ No newline at end of file
diff --git a/GraphApp.css b/GraphApp.css
new file mode 100644
index 0000000..74b5e05
--- /dev/null
+++ b/GraphApp.css
@@ -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);
+ }
+}
diff --git a/GraphApp.js b/GraphApp.js
new file mode 100644
index 0000000..19f6537
--- /dev/null
+++ b/GraphApp.js
@@ -0,0 +1,15 @@
+// GraphApp.js
+
+import React from 'react';
+import { ReactFlowProvider } from 'reactflow';
+import Canvas from './Canvas';
+
+function App() {
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/GraphManager.js b/GraphManager.js
new file mode 100644
index 0000000..b3e280b
--- /dev/null
+++ b/GraphManager.js
@@ -0,0 +1,32 @@
+// GraphManager.js
+import React, { createContext, useContext, useState } from 'react';
+import { useNodesState, useEdgesState} from 'reactflow';
+
+const GraphManagerContext = createContext(null);
+
+export const useGraphManager = () => {
+ return useContext(GraphManagerContext);
+};
+
+export const GraphManagerProvider = ({ children }) => {
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
+ const [nodeIdCounter, setNodeIdCounter] = useState(1);
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+
+ const value = {
+ nodes,
+ setNodes,
+ onNodesChange,
+ edges,
+ setEdges,
+ onEdgesChange,
+ nodeIdCounter,
+ setNodeIdCounter,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/JsonUtils.js b/JsonUtils.js
new file mode 100644
index 0000000..e2dcea3
--- /dev/null
+++ b/JsonUtils.js
@@ -0,0 +1,176 @@
+// JsonUtils.js
+
+import NodeData from './NodeData';
+import { createEdge } from './Edge';
+import { createConditionEdge } from './ConditionEdge';
+
+// Convert nodes to a JSON object format
+export const convertFlowToJson = (nodes, nodeIdCounter) => {
+ const nodesData = nodes.map((node) => {
+ // Create a unique set from nexts, then convert it back to an array
+ const uniqueNexts = Array.from(new Set(node.data.nexts || []));
+
+ const ext = {
+ pos_x: node.position.x,
+ pos_y: node.position.y,
+ width: node.data.width || 200,
+ height: node.data.height || 200,
+ info: node.data.info || '',
+ };
+
+ // Return a new node with the updated unique 'nexts' array
+ const nodeData = NodeData.fromReactFlowNode({
+ ...node,
+ data: {
+ ...node.data,
+ nexts: uniqueNexts,
+ },
+ });
+
+ return {
+ ...nodeData.toDict(),
+ ext,
+ };
+ });
+
+ const flowData = {
+ nodes: nodesData,
+ node_counter: nodeIdCounter,
+ };
+
+ return flowData;
+};
+
+// Save the JSON object to a file
+export const saveJsonToFile = (flowData) => {
+ try {
+ const blob = new Blob([JSON.stringify(flowData, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'flow.json';
+ document.body.appendChild(a); // Append the link to the body
+ a.click(); // Simulate a click on the link
+ document.body.removeChild(a); // Remove the link from the body
+ URL.revokeObjectURL(url); // Revoke the object URL
+ alert('Flow saved!');
+ } catch (error) {
+ console.error('Error saving JSON:', error);
+ alert('Failed to save flow.');
+ }
+};
+
+// Original saveJson function - keeps the original interface
+export const saveJson = async (nodes, nodeIdCounter) => {
+ try {
+ // Convert nodes to JSON
+ const flowData = convertFlowToJson(nodes, nodeIdCounter);
+ // Save the JSON data to a file
+ saveJsonToFile(flowData);
+ } catch (error) {
+ console.error('Error in saveJson:', error);
+ alert('Failed to save flow.');
+ }
+};
+
+// Read and process JSON file
+export const readJsonFile = (event) => {
+ return new Promise((resolve, reject) => {
+ const file = event.target.files[0];
+ if (!file) {
+ reject(new Error('No file selected.'));
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const contents = e.target.result;
+ resolve(JSON.parse(contents));
+ } catch (error) {
+ reject(new Error('Error parsing JSON.'));
+ }
+ };
+ reader.onerror = () => reject(new Error('Error reading file.'));
+ reader.readAsText(file);
+ });
+};
+
+// Process flow data
+export const processFlowData = (flowData, setEdges, setNodes, setNodeIdCounter) => {
+ try {
+ const loadedNodes = (flowData.nodes || []).map((nodeData) => {
+ const node = NodeData.fromDict(nodeData);
+ // Create a new object that includes ext properties and updated width/height
+ return {
+ ...node.toReactFlowNode(),
+ position: { x: nodeData.ext.pos_x, y: nodeData.ext.pos_y },
+ data: {
+ ...node.toReactFlowNode().data,
+ width: nodeData.ext.width,
+ height: nodeData.ext.height,
+ info: nodeData.ext.info,
+ },
+
+ };
+ });
+
+ // First, set the nodes
+ setNodes(loadedNodes);
+
+ // Then, create edges
+ const loadedEdges = [];
+ loadedNodes.forEach((node) => {
+ node.data.nexts.forEach((nextId) => {
+ const newEdge = createEdge(loadedEdges, setEdges, { source: node.id, target: nextId }, loadedNodes, setNodes);
+ if (newEdge) {
+ loadedEdges.push(newEdge);
+ }
+ });
+ if (node.data.true_next) {
+ const newEdge = createConditionEdge(loadedEdges, setEdges, { source: node.id, target: node.data.true_next, sourceHandle: 'true' }, loadedNodes, setNodes);
+ if (newEdge) {
+ loadedEdges.push(newEdge);
+ }
+ }
+ if (node.data.false_next) {
+ const newEdge = createConditionEdge(loadedEdges, setEdges, { source: node.id, target: node.data.false_next, sourceHandle: 'false' }, loadedNodes, setNodes);
+ if (newEdge) {
+ loadedEdges.push(newEdge);
+ }
+ }
+ });
+
+ setEdges(loadedEdges);
+
+ // Set node counter
+ setNodeIdCounter(flowData.node_counter || 1);
+ } catch (error) {
+ console.error('Error processing JSON data:', error);
+ alert('Failed to process JSON data.');
+ }
+};
+
+// Load JSON file and process
+export const loadJson = (setEdges, setNodes, setNodeIdCounter) => {
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = '.json';
+ fileInput.style.display = 'none';
+ document.body.appendChild(fileInput);
+
+ fileInput.addEventListener('change', async (event) => {
+ try {
+ const flowData = await readJsonFile(event);
+ if (flowData) {
+ processFlowData(flowData, setEdges, setNodes, setNodeIdCounter);
+ }
+ } catch (error) {
+ console.error('Error loading JSON:', error);
+ alert('Failed to load flow.');
+ } finally {
+ document.body.removeChild(fileInput); // Clean up
+ }
+ });
+
+ fileInput.click();
+};
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 870a1b4..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2025 HomunMage
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/Node.js b/Node.js
new file mode 100644
index 0000000..d5b6084
--- /dev/null
+++ b/Node.js
@@ -0,0 +1,164 @@
+// Node.js
+
+import { memo, useCallback, useState, useEffect, useRef } from 'react';
+import NodeLayout from './NodeLayout';
+import { useGraphManager } from './GraphManager';
+
+// Helper functions to remove references
+export const removePrevs = (nodes, nodeId) => {
+ const nodeToRemovePrevs = nodes.find(node => node.id === nodeId);
+ if (!nodeToRemovePrevs) return nodes;
+
+ try {
+ nodeToRemovePrevs.prevs.forEach(prevId => {
+ const prevNode = nodes.find(node => node.id === prevId);
+ if (prevNode) {
+ prevNode.data.nexts = prevNode.data.nexts.filter(id => id !== nodeId);
+ if (prevNode.data.true_next === nodeId) prevNode.data.true_next = null;
+ if (prevNode.data.false_next === nodeId) prevNode.data.false_next = null;
+ }
+ });
+ nodeToRemovePrevs.prevs = [];
+ }
+ catch (error) {
+ // workaround if a node have no prevs
+ }
+ return nodes;
+};
+
+export const removeTrueFalse = (nodes, nodeId) => {
+ const nodeToRemoveTrueFalse = nodes.find(node => node.id === nodeId);
+ if (!nodeToRemoveTrueFalse) return nodes;
+
+ const trueNextId = nodeToRemoveTrueFalse.data.true_next;
+ if (trueNextId) {
+ const trueNextNode = nodes.find(node => node.id === trueNextId);
+ if (trueNextNode) {
+ trueNextNode.prevs = trueNextNode.prevs.filter(id => id !== nodeId);
+ }
+ }
+
+ const falseNextId = nodeToRemoveTrueFalse.data.false_next;
+ if (falseNextId) {
+ const falseNextNode = nodes.find(node => node.id === falseNextId);
+ if (falseNextNode) {
+ falseNextNode.prevs = falseNextNode.prevs.filter(id => id !== nodeId);
+ }
+ }
+
+ nodeToRemoveTrueFalse.data.true_next = null;
+ nodeToRemoveTrueFalse.data.false_next = null;
+ return nodes;
+};
+
+export const removeNexts = (nodes, nodeId) => {
+ const nodeToRemoveNexts = nodes.find(node => node.id === nodeId);
+ if (!nodeToRemoveNexts) return nodes;
+
+ nodeToRemoveNexts.data.nexts.forEach(nextId => {
+ const nextNode = nodes.find(node => node.id === nextId);
+ if (nextNode) {
+ nextNode.prevs = nextNode.prevs.filter(id => id !== nodeId);
+ }
+ });
+
+ nodeToRemoveNexts.data.nexts = [];
+ return nodes;
+};
+
+function Node({ data, isConnectable, id, prevs }) {
+ const {
+ setNodes,
+ } = useGraphManager();
+ const [nodeData, setNodeData] = useState(data);
+ const changeBuffer = useRef({});
+
+
+ useEffect(() => {
+ setNodeData(data);
+ }, [data]);
+
+ const handleChange = useCallback((event) => {
+ const name = event.target.name;
+ const value = event.target.value;
+ const isComposingEvent = event.nativeEvent.isComposing;
+
+ if (isComposingEvent) {
+ changeBuffer.current = { ...changeBuffer.current, [name]: value };
+ }
+ else {
+ updateNodeData((prevData) => ({ ...prevData, ...changeBuffer.current, [name]: value }));
+ changeBuffer.current = {};
+ }
+ }, [id, setNodes]);
+
+ const updateNodeData = (updateFn) => {
+ setNodes((nds) => {
+ return nds.map((node) => {
+ if (node.id === id) {
+ return {
+ ...node,
+ data: updateFn(node.data),
+ };
+ }
+ return node;
+ });
+ });
+ };
+
+ const onResize = useCallback((width, height) => {
+ updateNodeData((prevData) => ({ ...prevData, width, height }));
+ }, [id, setNodes]);
+
+ return (
+
+ );
+}
+
+export const addNode = (nodes, setNodes, nodeIdCounter, setNodeIdCounter, newPosition) => {
+ const newNode = {
+ id: nodeIdCounter.toString(),
+ type: 'textUpdater',
+ data: {
+ name: `Node ${nodeIdCounter}`,
+ description: '',
+ type: 'STEP',
+ ext: { info: '' }, // Initialize ext.info for new nodes
+ nexts: [],
+ true_next: null,
+ false_next: null,
+ width: 200,
+ height: 200
+ },
+ position: newPosition,
+ prevs: []
+ };
+ setNodes((nds) => nds.concat(newNode));
+ setNodeIdCounter(nodeIdCounter + 1);
+};
+
+export const deleteNode = (nodes, setNodes, edges, setEdges, nodeId) => {
+ let updatedNodes = [...nodes];
+ const nodeToDelete = updatedNodes.find((node) => node.id === nodeId);
+ if (!nodeToDelete) return;
+
+ // Use helper functions to clean up references
+ updatedNodes = removePrevs(updatedNodes, nodeId);
+ updatedNodes = removeTrueFalse(updatedNodes, nodeId);
+ updatedNodes = removeNexts(updatedNodes, nodeId);
+
+ // Remove the node itself
+ updatedNodes = updatedNodes.filter((node) => node.id !== nodeId);
+ setNodes(updatedNodes);
+
+ // Remove edges connected to this node
+ setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
+};
+
+export default memo(Node);
\ No newline at end of file
diff --git a/NodeData.js b/NodeData.js
new file mode 100644
index 0000000..7bb9f40
--- /dev/null
+++ b/NodeData.js
@@ -0,0 +1,76 @@
+// NodeData.js
+
+class NodeData {
+ constructor({
+ uniq_id = '',
+ nexts = [],
+ type = 'START',
+ name = '',
+ description = '',
+ tool = '',
+ true_next = null,
+ false_next = null,
+ }) {
+ this.uniq_id = uniq_id;
+ this.nexts = nexts;
+ this.type = type;
+ this.name = name;
+ this.description = description;
+ this.tool = tool;
+ this.true_next = true_next;
+ this.false_next = false_next;
+ }
+
+ static fromReactFlowNode(node) {
+ return new NodeData({
+ uniq_id: node.id,
+ nexts: node.data.nexts || [],
+ type: node.data.type || 'STEP',
+ name: node.data.name,
+ description: node.data.description || '',
+ tool: node.data.tool || '',
+ true_next: node.data.true_next || null,
+ false_next: node.data.false_next || null,
+ });
+ }
+
+ static fromDict(data) {
+ return new NodeData(data);
+ }
+
+ toReactFlowNode() {
+ return {
+ id: this.uniq_id,
+ type: 'textUpdater',
+ data: {
+ name: this.name,
+ description: this.description,
+ nexts: this.nexts,
+ type: this.type,
+ tool: this.tool,
+ true_next: this.true_next,
+ false_next: this.false_next,
+ },
+ };
+ }
+
+ toDict() {
+ const {
+ uniq_id, nexts, type, name, description,
+ tool, true_next, false_next,
+ } = this;
+
+ return {
+ uniq_id,
+ nexts,
+ type,
+ name,
+ description,
+ tool,
+ true_next,
+ false_next,
+ };
+ }
+}
+
+export default NodeData;
diff --git a/NodeLayout.js b/NodeLayout.js
new file mode 100644
index 0000000..a2a3505
--- /dev/null
+++ b/NodeLayout.js
@@ -0,0 +1,148 @@
+import React, { useCallback } from 'react';
+import { Handle, Position, NodeResizeControl } from 'reactflow';
+import ResizeIcon from './ResizeIcon';
+
+const handleStyle = {
+ width: 6,
+ height: 6,
+ borderRadius: '50%',
+ background: '#555',
+};
+
+function NodeLayout({ data, isConnectable, handleChange, onResize }) {
+ const handleResize = useCallback(
+ (evt, { width, height }) => {
+ onResize(width, height);
+ },
+ [onResize]
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {data.type !== 'START' && (
+ <>
+ {['STEP', 'CONDITION', 'INFO'].includes(data.type) && (
+
+
+
+
+ )}
+ {data.type === 'STEP' && (
+
+
+
+
+ )}
+ {['STEP', 'TOOL', 'CONDITION', 'INFO'].includes(data.type) && (
+
+
+
+
+ )}
+ {data.type === 'INFO' && (
+
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
+
+export default React.memo(NodeLayout);
\ No newline at end of file
diff --git a/Panel.js b/Panel.js
new file mode 100644
index 0000000..ce257df
--- /dev/null
+++ b/Panel.js
@@ -0,0 +1,61 @@
+// Panel.js
+import React from 'react';
+import { saveJson, loadJson } from './JsonUtils';
+import RunWindow from './RunWindow';
+import FileTransmit from './FileTransmit';
+import ConfigWindow from '../ConfigWindow';
+import { useGraphManager } from './GraphManager';
+
+
+function Panel({ showConfig, setShowConfig, showRun, setShowRun}) {
+ const {
+ nodes,
+ setNodes,
+ edges,
+ setEdges,
+ nodeIdCounter,
+ setNodeIdCounter,
+ } = useGraphManager();
+
+ const handleNew = () => {
+ setNodes([]);
+ setEdges([]);
+ setNodeIdCounter(1);
+ };
+
+ const handleSave = async () => {
+ await saveJson(nodes, nodeIdCounter);
+ };
+
+ const handleLoad = async () => {
+ handleNew();
+ await loadJson(setEdges, setNodes, setNodeIdCounter);
+ };
+
+ const handleRun = () => {
+ setShowRun(true);
+ };
+
+ const handleConfig = () => {
+ setShowConfig(true);
+ };
+
+ const handleUploadComplete = () => {
+ console.log('Upload complete.');
+ };
+
+ return (
+
+ );
+}
+
+export default Panel;
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index b2b7d07..0000000
--- a/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# reactflow-ts
-
-## Dev
-* compile
- * ```npm run tsc```
-* lint
- * ```npm run lint```
-* hold
- * ```npm run dev```
-* vitest
- * ```npm run test```
-
-## Serve
-``` bash
-npm run build
-npm run preview -- --host 0.0.0.0 --port 3000
-```
\ No newline at end of file
diff --git a/ResizeIcon.js b/ResizeIcon.js
new file mode 100644
index 0000000..8a84501
--- /dev/null
+++ b/ResizeIcon.js
@@ -0,0 +1,24 @@
+function ResizeIcon() {
+ return (
+
+ );
+}
+
+export default ResizeIcon;
diff --git a/RunWindow.js b/RunWindow.js
new file mode 100644
index 0000000..22c817a
--- /dev/null
+++ b/RunWindow.js
@@ -0,0 +1,150 @@
+import React, { useState, useEffect, useRef } from 'react';
+import SERVER_URL from '../config';
+import { useGraphManager } from './GraphManager';
+import { convertFlowToJson } from './JsonUtils';
+import ConfigManager from '../ConfigManager';
+
+function RunWindow({ onClose }) {
+ const [responseMessage, setResponseMessage] = useState('');
+ const [isRunning, setIsRunning] = useState(false);
+ const { username, llmModel, apiKey } = ConfigManager.getSettings();
+ const { nodes, nodeIdCounter } = useGraphManager();
+ const isPollingRef = useRef(false)
+
+ const saveGraphData = async () => {
+ try {
+ const flowData = convertFlowToJson(nodes, nodeIdCounter);
+ const response = await fetch(`${SERVER_URL}/save-graph/${encodeURIComponent(username)}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(flowData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to save graph data on the server.');
+ }
+
+ console.log('Graph data successfully saved to server.\n');
+ setResponseMessage(prev => prev + '\nGraph data successfully saved to server.\n');
+ } catch (error) {
+ console.error('Error saving graph data:', error);
+ setResponseMessage(prev => prev + '\nError saving graph data: ' + error.message);
+ throw error;
+ }
+ };
+
+ const handleRun = async () => {
+ if (isRunning) return;
+ setIsRunning(true)
+ setResponseMessage('');
+
+
+ try {
+ await saveGraphData();
+
+ console.log("Attempting to send request to Flask server...");
+ const response = await fetch(`${SERVER_URL}/run/${encodeURIComponent(username)}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ username: username,
+ llm_model: llmModel,
+ api_key: apiKey,
+ }),
+ });
+
+ if (!response.body) {
+ throw new Error('ReadableStream not yet supported in this browser.');
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let done = false;
+
+ while (!done) {
+ const { value, done: streamDone } = await reader.read();
+ done = streamDone;
+ if (value) {
+ const chunk = decoder.decode(value, { stream: !done });
+ console.log("Received chunk:", chunk);
+ try{
+ const parsed = JSON.parse(chunk.replace("data: ", "").trim());
+ if (parsed.status){
+ setIsRunning(false)
+ }
+ }catch(e){
+
+ }
+ setResponseMessage(prev => prev + chunk);
+ }
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ setResponseMessage(prev => prev + '\nError: ' + error.message);
+ alert('Error: ' + error.message);
+ setIsRunning(false);
+
+ } finally {
+ if(isPollingRef.current){
+ setIsRunning(false);
+ }
+
+ }
+ };
+
+
+ useEffect(() => {
+ isPollingRef.current = true;
+ const checkStatus = async () => {
+ try {
+ const response = await fetch(`${SERVER_URL}/status/${encodeURIComponent(username)}`, {
+ method: 'GET',
+ });
+ const status = await response.json();
+ setIsRunning(status.running);
+ } catch (error) {
+ console.error('Error checking status:', error);
+ }
+ };
+
+ const interval = setInterval(checkStatus, 2000);
+
+ return () => {
+ isPollingRef.current = false;
+ clearInterval(interval);
+ };
+ }, []);
+
+ const handleLeave = async () => {
+ onClose();
+ };
+
+ return (
+
+
+
Run Script
+
+
+
+
+
+
+
+ );
+}
+
+export default RunWindow;
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
index 6277d3d..0000000
--- a/eslint.config.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-
-export default tseslint.config(
- { ignores: ['dist'] },
- {
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ['**/*.{ts,tsx}'],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- },
- plugins: {
- 'react-hooks': reactHooks,
- 'react-refresh': reactRefresh,
- },
- rules: {
- ...reactHooks.configs.recommended.rules,
- 'react-refresh/only-export-components': [
- 'warn',
- { allowConstantExport: true },
- ],
- 'indent': ['error', 4],
- // Disable the no-explicit-any rule
- '@typescript-eslint/no-explicit-any': 'off',
- },
- },
-)
\ No newline at end of file
diff --git a/index.html b/index.html
deleted file mode 100644
index 4c6d616..0000000
--- a/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- LangGraph-GUI
-
-
-
-
-
-
diff --git a/package.json b/package.json
deleted file mode 100644
index 8b7fcee..0000000
--- a/package.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "name": "langgraph-gui",
- "private": true,
- "version": "1.3.0",
- "type": "module",
- "scripts": {
- "dev": "NODE_ENV=DEBUG vite",
- "build": "tsc -b && vite build",
- "lint": "eslint --fix .",
- "preview": "vite preview",
- "tsc": "tsc -p tsconfig.app.json",
- "test": "vitest"
- },
- "dependencies": {
- "@redux-devtools/extension": "^3.3.0",
- "@reduxjs/toolkit": "^2.5.0",
- "@xyflow/react": "^12.3.6",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-redux": "^9.2.0",
- "react-router-dom": "^7.1.1",
- "uuid": "^11.0.5"
- },
- "devDependencies": {
- "@eslint/js": "^9.17.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.2.0",
- "@types/node": "^22.10.10",
- "@types/react": "^18.3.18",
- "@types/react-dom": "^18.3.5",
- "@vitejs/plugin-react": "^4.3.4",
- "autoprefixer": "^10.4.20",
- "eslint": "^9.17.0",
- "eslint-plugin-react-hooks": "^5.0.0",
- "eslint-plugin-react-refresh": "^0.4.16",
- "globals": "^15.14.0",
- "jsdom": "^26.0.0",
- "postcss": "^8.4.49",
- "tailwindcss": "^3.4.17",
- "typescript": "~5.6.2",
- "typescript-eslint": "^8.18.2",
- "vite": "^6.0.5",
- "vitest": "^3.0.2"
- }
-}
diff --git a/postcss.config.js b/postcss.config.js
deleted file mode 100644
index 2e7af2b..0000000
--- a/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
diff --git a/setupTests.js b/setupTests.js
new file mode 100644
index 0000000..8f2609b
--- /dev/null
+++ b/setupTests.js
@@ -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';
diff --git a/setupTests.ts b/setupTests.ts
deleted file mode 100644
index 2eee530..0000000
--- a/setupTests.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// setupTests.ts
-
-import "@testing-library/jest-dom"; // important to import this for rendering react components
-
-if (typeof ResizeObserver === 'undefined') {
- global.ResizeObserver = class ResizeObserver {
- observe() {
- // do nothing
- }
- unobserve() {
- // do nothing
- }
- disconnect() {
- // do nothing
- }
- };
-}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 73579a5..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-// App.tsx
-
-import React from 'react';
-import AppRoutes from './routes/AppRoutes';
-
-const App: React.FC = () => {
- return (
-
- );
-};
-
-export default App;
\ No newline at end of file
diff --git a/src/Doc/DocPage.tsx b/src/Doc/DocPage.tsx
deleted file mode 100644
index 6f4bd7d..0000000
--- a/src/Doc/DocPage.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-// Graph/Doc.tsx
-
-import React from 'react';
-
-const DocPage: React.FC = () => {
- return (
-
- );
-};
-
-export default DocPage;
\ No newline at end of file
diff --git a/src/GraphMenu/ConfigWindow.tsx b/src/GraphMenu/ConfigWindow.tsx
deleted file mode 100644
index 769eabd..0000000
--- a/src/GraphMenu/ConfigWindow.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-// GraphMenu/ConfigWindow.tsx
-
-import { useState } from 'react';
-import ConfigManager from '../utils/ConfigManager';
-
-interface ConfigWindowProps {
- onClose: () => void;
-}
-
-function ConfigWindow({ onClose }: ConfigWindowProps) {
- const settings = ConfigManager.getSettings();
-
- const [username] = useState(settings.username);
- const [llmModel, setLlmModel] = useState(settings.llmModel);
- const [apiKey, setAPIKey] = useState(settings.apiKey);
-
- const handleSave = () => {
- ConfigManager.setSettings(llmModel, apiKey);
- onClose();
- };
-
- return (
-
-
-
Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default ConfigWindow;
\ No newline at end of file
diff --git a/src/GraphMenu/FileTransmit.ts b/src/GraphMenu/FileTransmit.ts
deleted file mode 100644
index 7b77a77..0000000
--- a/src/GraphMenu/FileTransmit.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-// GraphMenu/FileTransmit.ts
-
-import ConfigManager from '../utils/ConfigManager';
-
-export const handleUpload = async (files: FileList | null) => {
- const { username } = ConfigManager.getSettings();
-
- const SERVER_URL = import.meta.env.VITE_BACKEND_URL;
-
- if (!files || files.length === 0) {
- alert("No files selected for upload.");
- return;
- }
-
- if (!username) {
- alert("Username is not set. Please configure your settings.");
- return;
- }
-
-
- const formData = new FormData();
- for (const file of files) {
- formData.append('files', file);
- }
-
- try {
- const response = await fetch(`${SERVER_URL}/upload/${encodeURIComponent(username)}`, {
- method: 'POST',
- body: formData,
- });
-
- if (response.ok) {
- alert('Files successfully uploaded');
- } else {
- const errorData = await response.json();
- alert('Upload failed: ' + errorData.error);
- }
- } catch (error: any) {
- alert('Upload failed: ' + error.message);
- }
-};
-
-
-export const handleDownload = async () => {
- const { username } = ConfigManager.getSettings();
-
- const SERVER_URL = import.meta.env.VITE_BACKEND_URL;
-
-
- if (!username) {
- alert("Username is not set. Please configure your settings.");
- return;
- }
- try {
- const response = await fetch(`${SERVER_URL}/download/${encodeURIComponent(username)}`);
-
- if (response.ok) {
- const blob = await response.blob();
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${username}_workspace.zip`;
- a.click();
- URL.revokeObjectURL(url);
- } else {
- const errorData = await response.json();
- alert('Download failed: ' + errorData.error);
- }
- } catch (error: any) {
- alert('Download failed: ' + error.message);
- }
-};
-
-
-export const handleCleanCache = async () => {
-
- const SERVER_URL = import.meta.env.VITE_BACKEND_URL;
-
-
- const { username } = ConfigManager.getSettings();
- if (!username) {
- alert("Username is not set. Please configure your settings.");
- return;
- }
- try {
- const response = await fetch(`${SERVER_URL}/clean-cache/${encodeURIComponent(username)}`, {
- method: 'POST'
- });
-
- if (response.ok) {
- alert('Cache successfully cleaned');
- } else {
- const errorData = await response.json();
- alert('Clean cache failed: ' + errorData.error);
- }
- } catch (error: any) {
- alert('Clean cache failed: ' + error.message);
- }
-};
\ No newline at end of file
diff --git a/src/GraphMenu/MenuLayout.tsx b/src/GraphMenu/MenuLayout.tsx
deleted file mode 100644
index d8b65cb..0000000
--- a/src/GraphMenu/MenuLayout.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// GraphMenu/MenuLayout.tsx
-import React, { useState, useRef } from "react";
-import { Outlet } from 'react-router-dom';
-import MenuToggleButton from './MenuToggleButton';
-import RunWindow from './RunWindow';
-import ConfigWindow from './ConfigWindow'; // Import ConfigWindow
-
-
-const MenuLayout: React.FC = () => {
- const [menuOpen, setMenuOpen] = useState(false);
- const [isRunWindowOpen, setIsRunWindowOpen] = useState(false);
- const [isConfigWindowOpen, setIsConfigWindowOpen] = useState(false); // Config window state
- const menuRef = useRef(null);
-
- const toggleMenu = () => {
- setMenuOpen(!menuOpen);
- };
- const closeMenu = () => {
- setMenuOpen(false);
- };
- const openRunWindow = () => {
- setIsRunWindowOpen(true);
- };
- const closeRunWindow = () => {
- setIsRunWindowOpen(false);
- };
-
- const openConfigWindow = () => { // Open config window
- setIsConfigWindowOpen(true);
- };
-
- const closeConfigWindow = () => { // Close config window
- setIsConfigWindowOpen(false);
- };
-
- return (
-
-
-
-
-
-
- {/* Pass openConfigWindow */}
-
-
-
-
- {isRunWindowOpen &&
}
- {isConfigWindowOpen &&
} {/* Render ConfigWindow */}
-
- );
-};
-
-export default MenuLayout;
\ No newline at end of file
diff --git a/src/GraphMenu/MenuToggleButton.tsx b/src/GraphMenu/MenuToggleButton.tsx
deleted file mode 100644
index 41ea2c6..0000000
--- a/src/GraphMenu/MenuToggleButton.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-// GraphMenu/MenuToggleButton.tsx
-
-import React, { useRef } from 'react';
-import ConfigManager from '../utils/ConfigManager';
-import { handleUpload, handleDownload, handleCleanCache } from './FileTransmit';
-
-
-interface MenuToggleButtonProps {
- openRunWindow: () => void;
- openConfigWindow: () => void;
-}
-
-const MenuToggleButton: React.FC = ({ openRunWindow, openConfigWindow }) => {
- const { username } = ConfigManager.getSettings();
- const fileInputRef = useRef(null);
-
- const handleRunClick = () => {
- console.log('run');
- openRunWindow();
- // Handle run logic here (open the RunWindow)
- };
-
- const handleConfigClick = () => {
- openConfigWindow();
- }
-
- const handleDocumentationClick = () => {
- window.open("https://langgraph-gui.github.io/", "_blank");
- };
-
-
- const UsernameValid = username === 'unknown';
-
-
- return (
-
-
- {`User: ${username}`}
-
-
-
{
- await handleUpload(e.target.files);
- if (fileInputRef.current) {
- fileInputRef.current.value = '';
- }
-
- }}
- />
-
-
-
-
-
-
-
- );
-};
-
-export default MenuToggleButton;
\ No newline at end of file
diff --git a/src/GraphMenu/RunWindow.tsx b/src/GraphMenu/RunWindow.tsx
deleted file mode 100644
index df31cd8..0000000
--- a/src/GraphMenu/RunWindow.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-// GraphMenu/RunWindow.tsx
-
-import { useState, useEffect, useRef } from 'react';
-import { useGraph } from '../Graph/GraphContext';
-import { allSubGraphsToJson } from '../Graph/JsonUtil';
-import ConfigManager from '../utils/ConfigManager';
-
-interface RunWindowProps {
- onClose: () => void;
-}
-
-
-function RunWindow({ onClose }: RunWindowProps) {
- const [responseMessage, setResponseMessage] = useState('');
- const [isRunning, setIsRunning] = useState(false);
- const { username, llmModel, apiKey } = ConfigManager.getSettings();
- const { subGraphs } = useGraph();
- const isPollingRef = useRef(false);
-
- const SERVER_URL = import.meta.env.VITE_BACKEND_URL;
-
- const uploadGraphData = async () => {
- try {
- const flowData = allSubGraphsToJson(subGraphs);
-
- if (!username) {
- throw new Error("Username not available to upload graph data.");
- }
-
- const jsonString = JSON.stringify(flowData, null, 2);
- const blob = new Blob([jsonString], { type: 'application/json' });
- const graphFile = new File([blob], 'graph.json');
-
-
- const formData = new FormData();
- formData.append('files', graphFile);
-
-
- const response = await fetch(`${SERVER_URL}/upload/${encodeURIComponent(username)}`, {
- method: 'POST',
- body: formData,
- });
-
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error('Failed to upload graph data: ' + errorData.error);
- }
-
-
- console.log('Graph data successfully uploaded to server.\n');
- setResponseMessage(prev => prev + '\nGraph data successfully uploaded to server.\n');
-
-
- } catch (error: unknown) {
- let errorMessage = "An unknown error occurred";
- if (error instanceof Error) {
- errorMessage = error.message;
- }
- console.error('Error uploading graph data:', errorMessage);
- setResponseMessage(prev => prev + '\nError uploading graph data: ' + errorMessage);
- throw error;
- }
- };
-
-
-
- const handleRun = async () => {
- if (isRunning) return;
- setIsRunning(true);
- setResponseMessage('');
-
-
- try {
- await uploadGraphData();
- console.log("Attempting to send request to Flask server...");
-
- if (!username) {
- throw new Error("Username not available to run.");
- }
-
- const response = await fetch(`${SERVER_URL}/run/${encodeURIComponent(username)}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- username: username,
- llm_model: llmModel,
- api_key: apiKey,
- }),
- });
-
-
- if (!response.body) {
- throw new Error('ReadableStream not yet supported in this browser.');
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let done = false;
-
-
- while (!done) {
- const { value, done: streamDone } = await reader.read();
- done = streamDone;
- if (value) {
- const chunk = decoder.decode(value, { stream: !done });
- console.log("Received chunk:", chunk);
- try{
- const parsed = JSON.parse(chunk.replace("data: ", "").trim());
- if (parsed.status){
- setIsRunning(false)
- }
- }catch(e){
- console.error("Error parsing JSON:", e);
- }
- setResponseMessage(prev => prev + chunk);
- }
- }
- } catch (error: unknown) {
- let errorMessage = "An unknown error occurred";
- if (error instanceof Error) {
- errorMessage = error.message;
- }
- console.error('Error:', errorMessage);
- setResponseMessage(prev => prev + '\nError: ' + errorMessage);
- alert('Error: ' + errorMessage);
- setIsRunning(false);
- } finally {
- if(isPollingRef.current){
- setIsRunning(false);
- }
- }
- };
-
-
- useEffect(() => {
- isPollingRef.current = true;
- const checkStatus = async () => {
- try {
- if (!username) {
- throw new Error("Username not available to check status.");
- }
-
- const response = await fetch(`${SERVER_URL}/status/${encodeURIComponent(username)}`, {
- method: 'GET',
- });
- const status = await response.json();
- setIsRunning(status.running);
- } catch (error) {
- console.error('Error checking status:', error);
- }
- };
- const interval = setInterval(checkStatus, 2000);
-
-
- return () => {
- isPollingRef.current = false;
- clearInterval(interval);
- };
- }, [username, SERVER_URL]);
-
-
- const handleLeave = async () => {
- onClose();
- };
-
-
- return (
-
-
-
Run Script
-
-
-
-
-
- {/* ADDED TEXT-BLACK HERE */}
-
{responseMessage}
-
-
-
- );
-}
-
-
-export default RunWindow;
\ No newline at end of file
diff --git a/src/graph/CustomEdge.tsx b/src/graph/CustomEdge.tsx
deleted file mode 100644
index 4cf8015..0000000
--- a/src/graph/CustomEdge.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-// Graph/CustomEdge.tsx
-
-import React from 'react';
-import { EdgeProps, getBezierPath, Position } from '@xyflow/react';
-
-interface CustomEdgeProps extends Omit {
- sourcePosition: Position;
- sourceNode: string;
- targetNode: string;
-}
-
-const CustomEdge: React.FC = ({
- id,
- sourceX,
- sourceY,
- targetX,
- targetY,
- sourcePosition,
- targetPosition,
- style,
- sourceNode,
- targetNode
-}) => {
-
- const edgePathArray = getBezierPath({
- sourceX,
- sourceY,
- targetX,
- targetY,
- sourcePosition,
- targetPosition
- });
-
-
- const edgePath = Array.isArray(edgePathArray) ? edgePathArray[0] : "";
-
- let strokeColor = 'gray';
-
- if (sourcePosition === Position.Top) {
- strokeColor = 'green';
- } else if (sourcePosition === Position.Bottom) {
- strokeColor = 'red';
- }
-
- const edgeStyle = {
- ...style,
- strokeWidth: 4,
- stroke: strokeColor,
- };
-
- const markerEndId = `arrowhead-${id}`;
- const markerFillColor = strokeColor;
-
-
-
- return (
- <>
-
-
-
-
-
-
- >
- );
-};
-
-export default React.memo(CustomEdge);
diff --git a/src/graph/CustomNode.tsx b/src/graph/CustomNode.tsx
deleted file mode 100644
index ad68307..0000000
--- a/src/graph/CustomNode.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-// Graph/CustomNode.tsx
-
-import React, { useCallback, useRef, useState, useEffect } from 'react';
-import { Handle, Position, NodeResizeControl } from '@xyflow/react';
-import ResizeIcon from './ResizeIcon';
-import { ReactNodeProps, ReactFlowNodeEXT } from './NodeData';
-
-const handleStyle = {
- borderRadius: '50%',
- background: '#555',
-};
-
-const CustomNode: React.FC = ({ id, width, height, data, onNodeDataChange }) => {
- const [localData, setLocalData] = useState(data);
- const dataRef = useRef(data);
-
- useEffect(() => {
- if (data !== dataRef.current) {
- setLocalData(data)
- dataRef.current = data
- }
- }, [data])
-
- const handleChange = useCallback((evt: React.ChangeEvent) => {
- const { name, value } = evt.target;
- setLocalData(prev => ({ ...prev, [name]: value }))
- }, []);
-
- const handleBlur = useCallback(() => {
- if (localData !== data) {
- onNodeDataChange?.(id, localData);
- }
- }, [id, localData, data, onNodeDataChange]);
-
-
- const generateFieldId = (fieldName: string) => `${id}-${fieldName}`;
-
- return (
-
-
-
-
-
-
-
-
-
-
- {localData.type !== 'START' && (
- <>
- {['STEP', 'CONDITION', 'INFO', 'SUBGRAPH'].includes(localData.type) && (
-
-
-
-
- )}
- {localData.type === 'STEP' && (
-
-
-
-
- )}
- {['STEP', 'TOOL', 'CONDITION', 'INFO'].includes(localData.type) && (
-
-
-
-
- )}
- >
- )}
-
-
-
-
-
- );
-};
-
-export default React.memo(CustomNode);
\ No newline at end of file
diff --git a/src/graph/GraphActions.tsx b/src/graph/GraphActions.tsx
deleted file mode 100644
index 80bdb88..0000000
--- a/src/graph/GraphActions.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-// Graph/GraphActions.tsx
-
-import { useCallback } from 'react';
-import { useGraph } from './GraphContext';
-import { Edge, Connection } from '@xyflow/react';
-
-
-interface ContextMenuProps {
- mouseX: number;
- mouseY: number;
- nodeId: string | null;
- edgeId: string | null;
- type: 'panel' | 'node' | 'edge';
-}
-
-interface AddNodeProps {
- contextMenu: ContextMenuProps | null;
- setContextMenu: React.Dispatch>;
- screenToFlowPosition: ((pos: { x: number; y: number }) => { x: number; y: number }) | null;
-}
-
-export const useGraphActions = () => {
- const { currentGraphName, updateSubGraph, getCurrentGraph } = useGraph();
- const currentGraph = useCallback(() => getCurrentGraph(), [getCurrentGraph]);
-
- const handleAddNode = useCallback(({ contextMenu, setContextMenu, screenToFlowPosition }: AddNodeProps) => {
- if (contextMenu && contextMenu.type === 'panel' && screenToFlowPosition) {
- const newPosition = screenToFlowPosition({ x: contextMenu.mouseX, y: contextMenu.mouseY });
- const newNodeId = String(currentGraph().serial_number);
- const newNode = {
- id: newNodeId,
- type: 'custom',
- position: newPosition,
- width: 150,
- height: 200,
- data: {
- type: "STEP",
- name: `Node ${currentGraph().serial_number}` // Set the default name here
- },
- };
- const updatedNodes = [...currentGraph().nodes, newNode]
- updateSubGraph(currentGraphName, {
- ...currentGraph(),
- nodes: updatedNodes,
- serial_number: currentGraph().serial_number + 1,
- }
- );
- setContextMenu(null);
- }
- }, [currentGraph, updateSubGraph, currentGraphName]);
-
-
- const handleDeleteNode = useCallback((contextMenu: ContextMenuProps | null, setContextMenu: React.Dispatch>) => {
- if (contextMenu && contextMenu.nodeId) {
- const nodeToDeleteId = contextMenu.nodeId;
- let updatedNodes = currentGraph().nodes.filter((node) => node.id !== nodeToDeleteId)
- const updatedEdges = currentGraph().edges.filter((edge) => edge.source !== nodeToDeleteId && edge.target !== nodeToDeleteId);
-
-
- // Update prevs and nexts on deletion
- updatedNodes = updatedNodes.map((node) => {
- const updatedNode = { ...node };
-
- //remove all next ref
- if (Array.isArray(updatedNode.data.nexts) && updatedNode.data.nexts.includes(nodeToDeleteId)) {
- (updatedNode.data.nexts as string[]) = updatedNode.data.nexts.filter(next => next !== nodeToDeleteId)
- }
- if (updatedNode.data.true_next === nodeToDeleteId) {
- updatedNode.data.true_next = null
- }
- if (updatedNode.data.false_next === nodeToDeleteId) {
- updatedNode.data.false_next = null;
- }
-
- //remove all prev ref
- if (Array.isArray(updatedNode.data.prevs) && updatedNode.data.prevs.includes(nodeToDeleteId)) {
- (updatedNode.data.prevs as string[]) = updatedNode.data.prevs.filter(prev => prev !== nodeToDeleteId);
- }
-
- return updatedNode
- })
-
- updateSubGraph(currentGraphName, {
- ...currentGraph(),
- nodes: updatedNodes,
- edges: updatedEdges
- });
- setContextMenu(null);
- }
- }, [updateSubGraph, currentGraph, currentGraphName]);
-
-
- const handleDeleteEdge = useCallback((contextMenu: ContextMenuProps | null, setContextMenu: React.Dispatch>) => {
- if (contextMenu && contextMenu.edgeId) {
- const edgeToDeleteId = contextMenu.edgeId;
- const edgeToDelete = currentGraph().edges.find((edge) => edge.id === edgeToDeleteId);
-
- if (!edgeToDelete) return;
-
- const updatedEdges = currentGraph().edges.filter((edge) => edge.id !== edgeToDeleteId);
- const updatedNodes = currentGraph().nodes.map(node => {
- const updatedNode = { ...node };
-
- if (updatedNode.id === edgeToDelete.source) {
- if (edgeToDelete.sourceHandle === 'true') {
- updatedNode.data.true_next = null;
- } else if (edgeToDelete.sourceHandle === 'false') {
- updatedNode.data.false_next = null;
- } else {
- // general to nexts if it exists
- if (Array.isArray(updatedNode.data.nexts) && updatedNode.data.nexts.includes(edgeToDelete.target)){
- (updatedNode.data.nexts as string[]) = updatedNode.data.nexts.filter(next => next !== edgeToDelete.target)
- }
- }
-
- } else if (updatedNode.id === edgeToDelete.target) {
- if (Array.isArray(updatedNode.data.prevs) && updatedNode.data.prevs.includes(edgeToDelete.source)){
- (updatedNode.data.prevs as string[]) = updatedNode.data.prevs.filter(prev => prev !== edgeToDelete.source)
- }
-
- }
-
- return updatedNode
- })
-
- updateSubGraph(currentGraphName, {
- ...currentGraph(),
- edges: updatedEdges,
- nodes: updatedNodes
- });
- setContextMenu(null);
- }
- }, [currentGraph, currentGraphName, updateSubGraph])
-
- const handleAddEdge = useCallback((connection: Connection) => {
-
- const sourceNode = currentGraph().nodes.find(node => node.id === connection.source);
- const targetNode = currentGraph().nodes.find(node => node.id === connection.target);
-
- if (!sourceNode || !targetNode) return;
-
- // Check for existing connections on true/false handles
- if (connection.sourceHandle === 'true' && sourceNode.data.true_next) {
- alert("This node already has a 'true' connection. Please remove existing edge to create new one.");
- return; // Reject new connection
- }
- if (connection.sourceHandle === 'false' && sourceNode.data.false_next) {
- alert("This node already has a 'false' connection. Please remove existing edge to create new one.");
- return; // Reject new connection
- }
-
-
- const newEdge: Edge = {
- id: `${connection.source}-${connection.target}-${connection.sourceHandle || ""}`,
- source: connection.source,
- target: connection.target,
- sourceHandle: connection.sourceHandle,
- type: "custom",
- data:{
- sourceNode: connection.source,
- targetNode: connection.target
- }
- }
-
- const updatedNodes = currentGraph().nodes.map(node =>{
- const updatedNode = { ...node }
- if (updatedNode.id === connection.source){
- if(connection.sourceHandle === 'true'){
- updatedNode.data.true_next = connection.target
- } else if (connection.sourceHandle === 'false'){
- updatedNode.data.false_next = connection.target
- } else {
- // General connection
- if(!updatedNode.data.nexts) {
- updatedNode.data.nexts = []
- }
-
- if (! (updatedNode.data.nexts as string[]).includes(connection.target)){
- (updatedNode.data.nexts as string[]).push(connection.target);
- }
- }
-
- } else if(updatedNode.id === connection.target){
- if(!updatedNode.data.prevs) {
- updatedNode.data.prevs = []
- }
- if (! (updatedNode.data.prevs as string[]).includes(connection.source)){
- (updatedNode.data.prevs as string[]).push(connection.source)
- }
-
- }
-
- return updatedNode;
- })
-
-
- const updatedEdges = [...currentGraph().edges, newEdge];
- updateSubGraph(currentGraphName, {
- ...currentGraph(),
- edges: updatedEdges,
- nodes: updatedNodes
- });
-
- }, [currentGraph, currentGraphName, updateSubGraph])
-
- const handlePanelContextMenu = useCallback((event: React.MouseEvent, setContextMenu: React.Dispatch>) => {
- event.preventDefault();
- const target = event.target as HTMLElement;
- const nodeElement = target.closest('.react-flow__node') as HTMLElement;
- const edgeElement = target.closest('.react-flow__edge') as HTMLElement;
-
- if (nodeElement) {
- const nodeId = nodeElement.getAttribute("data-id")
- setContextMenu({
- mouseX: event.clientX,
- mouseY: event.clientY,
- nodeId: nodeId,
- edgeId: null,
- type: 'node'
- });
- } else if (edgeElement) {
- const edgeId = edgeElement.getAttribute('data-id');
- setContextMenu({
- mouseX: event.clientX,
- mouseY: event.clientY,
- nodeId: null,
- edgeId: edgeId,
- type: 'edge'
- })
- }
- else {
- setContextMenu({
- mouseX: event.clientX,
- mouseY: event.clientY,
- nodeId: null,
- edgeId: null,
- type: 'panel'
- });
- }
-
- }, []);
-
- return { handleAddNode, handleDeleteNode, handleDeleteEdge, handlePanelContextMenu, handleAddEdge }
-}
\ No newline at end of file
diff --git a/src/graph/GraphApp.css b/src/graph/GraphApp.css
deleted file mode 100644
index 7c6b45e..0000000
--- a/src/graph/GraphApp.css
+++ /dev/null
@@ -1,12 +0,0 @@
-/* GraphApp.css */
-
-.react-flow__controls button svg {
- fill: black;
- stroke: black;
-}
-
-.react-flow__background {
- background-color: #f0f0f0; /* Example: light gray background */
- /* You can also add a background pattern: */
- /* background-image: repeating-linear-gradient(45deg, #e0e0e0, #e0e0e0 10px, #f0f0f0 10px, #f0f0f0 20px); */
-}
\ No newline at end of file
diff --git a/src/graph/GraphApp.tsx b/src/graph/GraphApp.tsx
deleted file mode 100644
index e013de0..0000000
--- a/src/graph/GraphApp.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-// Graph/GraphApp.tsx
-
-import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
-import { ReactFlow, MiniMap, Controls, Background, useReactFlow, ReactFlowProps, NodeChange, EdgeChange, } from '@xyflow/react';
-import '@xyflow/react/dist/style.css';
-import { useGraph } from './GraphContext';
-import GraphPanel from './GraphPanel';
-import './GraphApp.css';
-import CustomNode from './CustomNode';
-import CustomEdge from './CustomEdge';
-import { useGraphActions } from './GraphActions';
-import { Edge as ReactFlowEdge } from '@xyflow/react';
-
-
-const GraphApp: React.FC = () => {
- const { currentGraphName, updateNodeData, handleNodesChange, handleEdgesChange, getCurrentGraph} = useGraph();
- const [contextMenu, setContextMenu] = useState<{mouseX: number, mouseY: number, nodeId: string | null, edgeId:string | null, type: 'panel' | 'node' | 'edge'} | null>(null);
- const [canvasHeight, setCanvasHeight] = useState(window.innerHeight);
- const menuBarRef = useRef(null); //ref for menu bar
- const { screenToFlowPosition } = useReactFlow();
-
- const { handleAddNode, handleDeleteNode, handleDeleteEdge, handlePanelContextMenu, handleAddEdge } = useGraphActions();
-
-
- // Always get the current graph, use initial graph data when current graph is not loaded
- const currentGraph = useMemo(()=> getCurrentGraph(), [getCurrentGraph]);
-
-
- const handleCloseContextMenu = useCallback(() => {
- setContextMenu(null);
- }, []);
-
-
- const handleNodeDataChange = useCallback((nodeId: string, newData: any) => {
- updateNodeData(currentGraphName, nodeId, newData)
- }, [updateNodeData, currentGraphName]);
-
- const handleEdgeClick = useCallback((event: React.MouseEvent, edge: ReactFlowEdge) => {
- event.preventDefault();
- event.stopPropagation();
- console.log("handleEdgeClick", edge)
- }, [])
-
-
- const reactFlowProps = useMemo(() => ({
- onContextMenu: (event: React.MouseEvent)=> handlePanelContextMenu(event, setContextMenu),
- onClick: handleCloseContextMenu,
- onNodesChange: (changes: NodeChange[]) => handleNodesChange(currentGraphName, changes),
- onEdgesChange: (changes: EdgeChange[]) => handleEdgesChange(currentGraphName, changes),
- onEdgeClick: handleEdgeClick,
- onConnect: handleAddEdge,
- edgeTypes: {
- custom: (props) => {
- const {sourceNode, targetNode} = props.data || {}
- return
- },
- },
- }),[handlePanelContextMenu,handleCloseContextMenu, handleNodesChange, handleEdgesChange, handleEdgeClick, handleAddEdge, currentGraphName, setContextMenu])
-
- useEffect(() => {
- const handleResize = () => {
- if (menuBarRef.current) {
- const menuBarHeight = menuBarRef.current.offsetHeight;
- setCanvasHeight(window.innerHeight - menuBarHeight - 10);
- } else {
- setCanvasHeight(window.innerHeight-10);
- }
- };
-
- window.addEventListener('resize', handleResize);
- handleResize();
-
- return () => window.removeEventListener('resize', handleResize);
- }, []);
-
- const nodeTypes = useMemo(() => ({
- custom: (props: any) => ,
- }), [handleNodeDataChange]);
-
-
-
- return (
-
- {/*Assuming you have a div with ref for menuBar*/}
-
-
-
-
-
-
-
-
-
-
- {contextMenu && contextMenu.type === 'panel' && (
-
-
-
-
- )}
- {contextMenu && contextMenu.type === 'node' &&(
-
- {/* */}
-
-
-
- )}
- {contextMenu && contextMenu.type === 'edge' &&(
-
-
-
-
- )}
-
-
- );
-};
-
-export default GraphApp;
\ No newline at end of file
diff --git a/src/graph/GraphContext.test.tsx b/src/graph/GraphContext.test.tsx
deleted file mode 100644
index b3a6119..0000000
--- a/src/graph/GraphContext.test.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-// GraphContext.test.tsx
-import React from 'react';
-import { render, screen, act } from '@testing-library/react';
-import { useGraph, GraphProvider } from './GraphContext';
-import { describe, it, expect } from 'vitest';
-
-const TestComponent: React.FC = () => {
- const graphContext = useGraph();
-
- return (
-
-
-
-
SubGraph Count: {graphContext.subGraphs.length}
-
Has Root Graph: {graphContext.subGraphs.some(graph => graph.graphName === 'root') ? 'true' : 'false'}
-
Has Test Graph: {graphContext.subGraphs.some(graph => graph.graphName === 'test') ? 'true' : 'false'}
-
- );
-};
-
-describe('GraphContext', () => {
- it('should manage subgraphs correctly', async () => {
- render(
-
-
-
- );
-
- const testComponent = screen.getByTestId("test-component");
- expect(testComponent).toBeInTheDocument();
-
- const addRootButton = screen.getByTestId("add-root-button");
- const addSubGraphButton = screen.getByTestId("add-subgraph-button");
-
- // Add root and test subgraph
- await act(async () => {
- addRootButton.click();
- });
- await act(async () => {
- addSubGraphButton.click();
- });
-
- let subGraphCountElement = screen.getByTestId("subgraph-count");
- expect(subGraphCountElement).toHaveTextContent("SubGraph Count: 2");
-
- let hasRootElement = screen.getByTestId("has-root-graph");
- expect(hasRootElement).toHaveTextContent("Has Root Graph: true");
-
- let hasTestElement = screen.getByTestId("has-test-graph");
- expect(hasTestElement).toHaveTextContent("Has Test Graph: true");
-
- // Try adding root again, should not change the count
- await act(async () => {
- addRootButton.click();
- });
- subGraphCountElement = screen.getByTestId("subgraph-count");
- expect(subGraphCountElement).toHaveTextContent("SubGraph Count: 2");
-
- hasRootElement = screen.getByTestId("has-root-graph");
- expect(hasRootElement).toHaveTextContent("Has Root Graph: true");
- hasTestElement = screen.getByTestId("has-test-graph");
- expect(hasTestElement).toHaveTextContent("Has Test Graph: true");
- });
-});
\ No newline at end of file
diff --git a/src/graph/GraphContext.tsx b/src/graph/GraphContext.tsx
deleted file mode 100644
index 204c753..0000000
--- a/src/graph/GraphContext.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-// Graph/GraphContext.tsx
-
-import React, { createContext, useState, useContext, useCallback, ReactNode } from 'react';
-import { Node, Edge, NodeChange, applyNodeChanges, EdgeChange, applyEdgeChanges } from '@xyflow/react';
-
-export interface SubGraph {
- graphName: string;
- nodes: Node[];
- edges: Edge[];
- serial_number: number;
-}
-
-export interface GraphContextType {
- subGraphs: SubGraph[];
- currentGraphName: string;
- setCurrentGraphName: (graphName: string) => void;
- getCurrentGraph: () => SubGraph;
- addSubGraph: (graphName: string) => void;
- updateSubGraph: (graphName: string, updatedGraph: SubGraph) => void;
- removeSubGraph: (graphName: string) => void;
- updateNodeData: (graphName: string, nodeId: string, newData: any) => void;
- handleNodesChange: (graphName: string, changes: NodeChange[]) => void;
- handleEdgesChange: (graphName: string, changes: EdgeChange[]) => void;
-}
-
-const initialGraphData = {
- graphName: "root",
- nodes: [],
- edges: [],
- serial_number: 1,
-};
-
-const GraphContext = createContext(undefined);
-
-export const GraphProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
- const [subGraphs, setSubGraphs] = useState([]);
- const [currentGraphName, setCurrentGraphNameState] = useState("root");
-
- const getCurrentGraph = useCallback(():SubGraph => {
- const currentGraph = subGraphs.find(graph => graph.graphName === currentGraphName);
- return currentGraph || {
- graphName: "root",
- nodes: [],
- edges: [],
- serial_number: 1,
- };
- }, [subGraphs, currentGraphName]);
-
- const addSubGraph = (graphName: string) => {
- setSubGraphs(prevGraphs => {
- const graphIndex = prevGraphs.findIndex(graph => graph.graphName === graphName);
- if(graphIndex === -1){
- const newSubgraphs = [...prevGraphs, {
- graphName,
- nodes: [],
- edges: [],
- serial_number: 1,
- }]
- if(!currentGraphName) setCurrentGraphNameState(graphName);
- return newSubgraphs;
- } else {
- return prevGraphs; // Do nothing, just return the previous state
- }
- });
- };
-
- const updateSubGraph = (graphName: string, updatedGraph: SubGraph) => {
-
- setSubGraphs(prevGraphs => {
- const graphIndex = prevGraphs.findIndex(graph => graph.graphName === graphName);
-
- if (graphIndex === -1) {
- return [...prevGraphs, updatedGraph]
- } else {
-
- const updateGraph = prevGraphs.map((graph, index) => index === graphIndex ? updatedGraph : graph)
- if(currentGraphName === graphName) {
-
- setCurrentGraphNameState(updatedGraph.graphName);
- }
- return updateGraph;
- }
- });
- };
-
- const removeSubGraph = (graphName: string) => {
- setSubGraphs(prevGraphs => prevGraphs.filter(graph => graph.graphName !== graphName));
- if (currentGraphName === graphName) {
- setCurrentGraphNameState("root")
- }
- };
-
-
- const setCurrentGraphName = (graphName: string) => {
- setCurrentGraphNameState(graphName);
- };
-
-
- const updateNodeData = (graphName: string, nodeId: string, newData: any) => {
- setSubGraphs(prevGraphs => {
- return prevGraphs.map(graph => {
- if(graph.graphName === graphName){
- return {
- ...graph,
- nodes: graph.nodes.map(node =>{
- if(node.id === nodeId){
- return {
- ...node,
- data: {...node.data, ...newData}
- }
- }
- return node;
- })
- }
- }
- return graph;
- })
- })
- }
-
- const handleNodesChange = useCallback((graphName: string, changes: NodeChange[]) => {
- setSubGraphs((prevGraphs) => {
- return prevGraphs.map(graph => {
- if(graph.graphName === graphName){
- const updatedNodes = applyNodeChanges(changes, graph.nodes);
- return { ...graph, nodes: updatedNodes };
- }
- return graph;
- })
- })
- }, []);
-
- const handleEdgesChange = useCallback((graphName: string, changes: EdgeChange[]) => {
- setSubGraphs((prevGraphs) => {
- return prevGraphs.map(graph => {
- if(graph.graphName === graphName){
- const updatedEdges = applyEdgeChanges(changes, graph.edges);
- return { ...graph, edges: updatedEdges };
- }
- return graph;
- })
- })
- }, []);
-
- //Initialize root graph if not exist
- React.useEffect(()=>{
- const rootGraphExist = subGraphs.find(graph => graph.graphName === "root")
- if(!rootGraphExist){
- setSubGraphs([{...initialGraphData}]);
- }
- }, [subGraphs])
-
- const value = {
- subGraphs,
- currentGraphName,
- setCurrentGraphName,
- getCurrentGraph,
- addSubGraph,
- updateSubGraph,
- removeSubGraph,
- updateNodeData,
- handleNodesChange,
- handleEdgesChange,
- };
-
- return (
-
- {children}
-
- );
-};
-
-export const useGraph = () => {
- const context = useContext(GraphContext);
- if (!context) {
- throw new Error('useGraph must be used within a GraphProvider');
- }
- return context;
-};
\ No newline at end of file
diff --git a/src/graph/GraphPanel.css b/src/graph/GraphPanel.css
deleted file mode 100644
index 5d7afb7..0000000
--- a/src/graph/GraphPanel.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.dropdown-menu {
- background-color: white;
- border: 1px solid #e2e8f0;
- border-radius: 0.25rem;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
- color: black;
- }
-
- .dropdown-menu button {
- color: black; /* Ensure text is black */
- background-color: white; /* set default background white */
- }
-
- .dropdown-menu button:hover {
- background-color: #f3f4f6; /* Tailwind's gray-100 */
- }
-
- .dropdown-menu select {
- color: black; /* Ensure text is black */
- background-color: white; /* set default background white */
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
- }
-
- .dropdown-menu select option {
- color: black;
- background-color: white;
- }
-
- .dropdown-menu select option:hover {
- background-color: #f3f4f6;
- }
\ No newline at end of file
diff --git a/src/graph/GraphPanel.tsx b/src/graph/GraphPanel.tsx
deleted file mode 100644
index 7c97369..0000000
--- a/src/graph/GraphPanel.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-// Graph/GraphPanel.tsx
-
-import React, { useState, useRef, useEffect } from 'react';
-import { useGraph } from './GraphContext';
-import './GraphPanel.css';
-import { allSubGraphsToJson, subGraphToJson, jsonToSubGraphs, jsonToSubGraph, JsonSubGraph } from './JsonUtil';
-import { saveJsonToFile, loadJsonFromFile } from '../utils/JsonIO';
-import { SubGraph } from './GraphContext';
-
-const GraphPanel: React.FC = () => {
- const { subGraphs, currentGraphName, addSubGraph, removeSubGraph, setCurrentGraphName, updateSubGraph, getCurrentGraph } = useGraph(); // Include getCurrentGraph
- const [isGraphMenuOpen, setIsGraphMenuOpen] = useState(false);
- const [isSubGraphMenuOpen, setIsSubGraphMenuOpen] = useState(false);
- const graphMenuRef = useRef(null);
- const subGraphMenuRef = useRef(null);
-
- const handleAddGraph = () => {
- const newGraphName = prompt("Enter a new graph name:");
- if (newGraphName) {
- addSubGraph(newGraphName);
- }
- closeMenus();
- };
-
- const handleRenameGraph = () => {
- const newGraphName = prompt("Enter a new graph name:");
- if (newGraphName && currentGraphName !== "root") {
- const currentGraph = subGraphs.find(graph => graph.graphName === currentGraphName)
- if(currentGraph){
- updateSubGraph(currentGraphName, {...currentGraph, graphName: newGraphName})
- }
- }
- closeMenus();
- }
-
- const handleRemoveGraph = () => {
- const graphName = prompt("Enter the graph name to delete:");
- if (graphName && graphName !== "root") {
- removeSubGraph(graphName);
- } else if (graphName === "root") {
- alert("cannot delete root");
- }
- closeMenus();
- };
-
- const handleSelectGraph = (graphName: string) => {
- setCurrentGraphName(graphName);
- closeMenus();
- };
-
- const handleNewGraph = () => {
- console.log("New Graph clicked");
- closeMenus();
- };
-
- const handleLoadGraph = async () => {
- try {
- const jsonData = await loadJsonFromFile();
- if(jsonData){
- const loadedSubGraphs: SubGraph[] = jsonToSubGraphs(jsonData);
-
- //Clear subgraphs first
- subGraphs.forEach(graph => {
- if(graph.graphName !== 'root') removeSubGraph(graph.graphName)
- })
- //Then load new subgraphs
- loadedSubGraphs.forEach(subGraph => updateSubGraph(subGraph.graphName,subGraph))
-
- alert('Graph loaded successfully!');
- }
-
- } catch (error) {
- console.error("Error loading graph:", error);
- alert('Failed to load graph: ' + error);
- }
- closeMenus();
- };
-
- const handleSaveGraph = () => {
- const jsonData = allSubGraphsToJson(subGraphs);
- saveJsonToFile("Save.json", jsonData);
- closeMenus();
- };
- // Placeholder functions for SubGraph menu
- const handleLoadSubGraph = async () => {
- try {
- const jsonData = await loadJsonFromFile();
-
- if (jsonData) {
-
- // Make sure jsonData is JsonSubGraph
- if(!jsonData.name || !jsonData.nodes || !jsonData.serial_number){
- throw new Error("Invalid Json Format: must be JsonSubGraph")
- }
-
- const loadedSubGraph: SubGraph = jsonToSubGraph(jsonData as JsonSubGraph);
-
- updateSubGraph(loadedSubGraph.graphName, loadedSubGraph);
-
- alert('Subgraph loaded successfully!');
- }
- } catch (error) {
- console.error("Error loading subgraph:", error);
- alert('Failed to load subgraph: ' + error);
- }
- closeMenus();
- };
- const handleSaveSubGraph = () => {
- const currentGraph = getCurrentGraph();
- const jsonData = subGraphToJson(currentGraph);
- saveJsonToFile(`${currentGraph.graphName}.json`, jsonData);
-
- closeMenus();
- };
-
- const toggleGraphMenu = () => {
- setIsGraphMenuOpen(!isGraphMenuOpen);
- setIsSubGraphMenuOpen(false);
- };
-
- const toggleSubGraphMenu = () => {
- setIsSubGraphMenuOpen(!isSubGraphMenuOpen);
- setIsGraphMenuOpen(false);
- };
- const closeMenus = () => {
- setIsGraphMenuOpen(false);
- setIsSubGraphMenuOpen(false);
- }
- // Close menus when clicking outside
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (graphMenuRef.current && !graphMenuRef.current.contains(event.target as Node)
- && subGraphMenuRef.current && !subGraphMenuRef.current.contains(event.target as Node)
- ) {
- closeMenus();
- }
- };
-
- document.addEventListener('mousedown', handleClickOutside);
-
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [graphMenuRef,subGraphMenuRef]);
-
-
- return (
-
- );
-};
-
-export default GraphPanel;
\ No newline at end of file
diff --git a/src/graph/JsonUtil.fromjson.test.tsx b/src/graph/JsonUtil.fromjson.test.tsx
deleted file mode 100644
index fa11dea..0000000
--- a/src/graph/JsonUtil.fromjson.test.tsx
+++ /dev/null
@@ -1,258 +0,0 @@
-// JsonUtil.fromjson.test.tsx
-
-import { describe, it, expect } from 'vitest';
-import { jsonToSubGraph, jsonToSubGraphs, JsonSubGraph } from './JsonUtil';
-import { SubGraph } from './GraphContext';
-
-describe('JsonUtil from JSON', () => {
- it('should convert a JSON subgraph to a SubGraph object correctly', () => {
- const jsonSubGraph: JsonSubGraph = {
- name: "testGraph",
- nodes: [
- {
- uniq_id: '1',
- type: 'START',
- name: "Start Node",
- description: "This is the start",
- tool: '',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 100, pos_y: 100, width: 200, height: 200, info: null },
- },
- {
- uniq_id: '2',
- type: 'STEP',
- name: "",
- description: "",
- tool: 'someTool',
- nexts: ['3'],
- true_next: '3',
- false_next: null,
- ext: { pos_x: 200, pos_y: 200, width: 250, height: 150, info: null },
- },
- {
- uniq_id: '3',
- type: 'END',
- name: "",
- description: "",
- tool: "",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 300, pos_y: 300, width: 200, height: 200, info: null }
- }
- ],
- serial_number: 3,
- };
-
- const expectedSubGraph: SubGraph = {
- graphName: "testGraph",
- nodes: [
- {
- id: '1',
- type: 'custom',
- position: { x: 100, y: 100 },
- width: 200,
- height: 200,
- data: { type: 'START', name: "Start Node", description: "This is the start", tool: '', nexts: [], true_next: null, false_next: null, info: null , prevs: []}
- },
- {
- id: '2',
- type: 'custom',
- position: { x: 200, y: 200 },
- width: 250,
- height: 150,
- data: { type: 'STEP', name: '', description: '', tool: 'someTool', nexts: ['3'], true_next: '3', false_next: null, info: null , prevs: []}
- },
- {
- id: '3',
- type: 'custom',
- position: { x: 300, y: 300 },
- width: 200,
- height: 200,
- data: { type: 'END', name: '', description: '', tool: '', nexts: [], true_next: null, false_next: null, info: null, prevs: [] }
- }
- ],
- edges: [
- {
- id: '2-3',
- source: '2',
- target: '3',
- type: 'custom',
- data: { sourceNode: '2', targetNode: '3' }
- },
- {
- id: '2-3-true',
- source: '2',
- target: '3',
- type: 'custom',
- sourceHandle: 'true',
- data: { sourceNode: '2', targetNode: '3' }
- },
- ],
- serial_number: 3,
- };
-
-
- const result = jsonToSubGraph(jsonSubGraph);
- console.log("jsonToSubGraph Output:", JSON.stringify(result, null, 2));
- expect(result).toEqual(expectedSubGraph);
- });
-
-
- it('should convert a JSON subgraph without positions and sizes', () => {
- const jsonSubGraph: JsonSubGraph = {
- name: "testGraph",
- nodes: [
- {
- uniq_id: '1',
- type: 'START',
- name: "Start Node",
- description: "This is the start",
- tool: '',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { },
- },
- {
- uniq_id: '2',
- type: 'STEP',
- name: "",
- description: "",
- tool: 'someTool',
- nexts: ['3'],
- true_next: '3',
- false_next: null,
- ext: {},
- },
- ],
- serial_number: 3,
- };
-
- const expectedSubGraph: SubGraph = {
- graphName: "testGraph",
- nodes: [
- {
- id: '1',
- type: 'custom',
- position: { x: 0, y: 0 },
- width: 200,
- height: 200,
- data: { type: 'START', name: "Start Node", description: "This is the start", tool: '', nexts: [], true_next: null, false_next: null, info: null, prevs: []}
- },
- {
- id: '2',
- type: 'custom',
- position: { x: 0, y: 0 },
- width: 200,
- height: 200,
- data: { type: 'STEP', name: '', description: '', tool: 'someTool', nexts: ['3'], true_next: '3', false_next: null, info: null, prevs: []}
- },
- ],
- edges: [
- {
- id: '2-3',
- source: '2',
- target: '3',
- type: 'custom',
- data: { sourceNode: '2', targetNode: '3' }
- },
- {
- id: '2-3-true',
- source: '2',
- target: '3',
- type: 'custom',
- sourceHandle: 'true',
- data: { sourceNode: '2', targetNode: '3' }
- },
- ],
- serial_number: 3,
- };
-
-
- const result = jsonToSubGraph(jsonSubGraph);
- console.log("jsonToSubGraph no position Output:", JSON.stringify(result, null, 2));
- expect(result).toEqual(expectedSubGraph);
- });
-
-
- it('should convert an array of JSON subgraphs to an array of SubGraph objects correctly', () => {
- const jsonSubGraphs: JsonSubGraph[] = [
- {
- name: "graph1",
- nodes: [
- {
- uniq_id: '1',
- type: 'START',
- name: "",
- description: "",
- tool: "",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 100, pos_y: 100, width: 200, height: 200, info: null }
- }
- ],
- serial_number: 1,
- },
- {
- name: "graph2",
- nodes: [
- {
- uniq_id: '2',
- type: 'STEP',
- name: "",
- description: "",
- tool: "test",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 200, pos_y: 200, width: 200, height: 200, info: null },
- }
- ],
- serial_number: 2,
- },
- ];
-
-
- const expectedSubGraphs: SubGraph[] = [
- {
- graphName: "graph1",
- nodes: [
- {
- id: '1',
- type: 'custom',
- position: { x: 100, y: 100 },
- width: 200,
- height: 200,
- data: { type: 'START', name: "", description: "", tool: "", nexts: [], true_next: null, false_next: null, info: null, prevs: [] }
- }
- ],
- edges: [],
- serial_number: 1,
- },
- {
- graphName: "graph2",
- nodes: [
- {
- id: '2',
- type: 'custom',
- position: { x: 200, y: 200 },
- width: 200,
- height: 200,
- data: { type: 'STEP', name: "", description: "", tool: "test", nexts: [], true_next: null, false_next: null, info: null, prevs: [] }
- }
- ],
- edges: [],
- serial_number: 2,
- },
- ];
-
-
- const result = jsonToSubGraphs(jsonSubGraphs);
- console.log("jsonToSubGraphs Output:", JSON.stringify(result, null, 2));
- expect(result).toEqual(expectedSubGraphs);
- });
-});
\ No newline at end of file
diff --git a/src/graph/JsonUtil.tojson.test.tsx b/src/graph/JsonUtil.tojson.test.tsx
deleted file mode 100644
index 675112a..0000000
--- a/src/graph/JsonUtil.tojson.test.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-// JsonUtil.tojson.test.tsx
-
-import { describe, it, expect, vi } from 'vitest';
-import { subGraphToJson, allSubGraphsToJson } from './JsonUtil';
-import { SubGraph } from './GraphContext';
-import { Node } from '@xyflow/react';
-
-describe('JsonUtil', () => {
- it('should convert a subGraph to JSON correctly', () => {
- const mockSubGraph: SubGraph = {
- graphName: "testGraph",
- nodes: [
- {
- id: '1',
- type: 'custom',
- position: { x: 100, y: 100 },
- width: 200,
- height: 200,
- data: { type: "START", name: "Start Node", description: "This is the start" } ,
- } as Node,
- {
- id: '2',
- type: 'custom',
- position: { x: 200, y: 200 },
- width: 250,
- height: 150,
- data: { type: "STEP", tool: "someTool", nexts:['3'], true_next: '3'} ,
- } as Node,
- {
- id: '3',
- type: 'custom',
- position: {x: 300, y: 300},
- data:{ type: "END"} ,
- } as Node,
- ],
- edges: [],
- serial_number: 3,
- };
-
- const expectedJson = {
- name: "testGraph",
- nodes: [
- {
- uniq_id: '1',
- type: 'START',
- name: "Start Node",
- description: "This is the start",
- tool: '',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 100, pos_y: 100, width: 200, height: 200, info: null },
- },
- {
- uniq_id: '2',
- type: 'STEP',
- name: "",
- description: "",
- tool: 'someTool',
- nexts: [ '3' ],
- true_next: '3',
- false_next: null,
- ext: { pos_x: 200, pos_y: 200, width: 250, height: 150, info: null },
- },
- {
- uniq_id: '3',
- type: 'END',
- name: "",
- description: "",
- tool: "",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 300, pos_y: 300, width: 200, height: 200, info: null }
- }
- ],
- serial_number: 3,
- };
-
- const result = subGraphToJson(mockSubGraph);
- console.log("subGraphToJson Output:", JSON.stringify(result, null, 2));
- expect(result).toEqual(expectedJson);
- });
-
-
- it('should handle missing properties and default type for node.data', () => {
- const mockSubGraph: SubGraph = {
- graphName: "testGraph",
- nodes: [
- {
- id: '1',
- type: 'custom',
- position: { x: 100, y: 100 },
- data: { } as any, //missing type
- } as Node,
- {
- id: '2',
- type: 'custom',
- position: { x: 200, y: 200 },
- data: {type:"STEP", tool:"test"} ,
- } as Node
- ],
- edges: [],
- serial_number: 1,
- };
-
-
- const expectedJson = {
- name: "testGraph",
- nodes:[
- {
- uniq_id: '1',
- type: 'STEP',
- name: "",
- description: "",
- tool: "",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 100, pos_y: 100, width: 200, height: 200, info: null }
- },
- {
- uniq_id: '2',
- type: 'STEP',
- name: "",
- description: "",
- tool: "test",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 200, pos_y: 200, width: 200, height: 200, info: null }
- }
- ],
- serial_number: 1
- }
-
- // Mock console.error to check if it's called for invalid node data
- const consoleErrorMock = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- const result = subGraphToJson(mockSubGraph);
- console.log("subGraphToJson Output (missing properties):", JSON.stringify(result, null, 2));
- expect(result).toEqual(expectedJson);
- expect(consoleErrorMock).toHaveBeenCalled();
- consoleErrorMock.mockRestore();
- });
-
- it('should convert all subgraphs to JSON correctly', () => {
- const mockSubGraphs: SubGraph[] = [
- {
- graphName: "graph1",
- nodes: [
- {
- id: '1',
- type: 'custom',
- position: { x: 100, y: 100 },
- data: { type: "START"} ,
- } as Node
- ],
- edges: [],
- serial_number: 1,
- },
- {
- graphName: "graph2",
- nodes: [
- {
- id: '2',
- type: 'custom',
- position: { x: 200, y: 200 },
- data: { type: "STEP", tool:"test"} ,
- } as Node
- ],
- edges: [],
- serial_number: 2,
- },
- ];
- const expectedJson = [
- {
- name: "graph1",
- nodes:[
- {
- uniq_id: '1',
- type: 'START',
- name: "",
- description: "",
- tool: "",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 100, pos_y: 100, width: 200, height: 200, info: null }
- },
- ],
- serial_number: 1,
- },
- {
- name: "graph2",
- nodes: [
- {
- uniq_id: '2',
- type: 'STEP',
- name: "",
- description: "",
- tool: "test",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 200, pos_y: 200, width: 200, height: 200, info: null }
- },
- ],
- serial_number: 2,
- },
- ];
- const result = allSubGraphsToJson(mockSubGraphs);
- console.log("allSubGraphsToJson Output:", JSON.stringify(result, null, 2));
- expect(result).toEqual(expectedJson);
- });
-});
\ No newline at end of file
diff --git a/src/graph/JsonUtil.tsx b/src/graph/JsonUtil.tsx
deleted file mode 100644
index 6cb0409..0000000
--- a/src/graph/JsonUtil.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-// Graph/JsonUtil.tsx
-
-import { ReactToJsonNode, ReactNodeProps, ReactFlowNodeEXT, JsonToReactNode, JsonNodeData } from './NodeData';
-import { SubGraph } from './GraphContext';
-import { Node, Edge } from '@xyflow/react';
-
-// Type guard to check if an object is a ReactFlowNodeEXT
-function isReactFlowNodeEXT(data: any): data is ReactFlowNodeEXT {
- return (
- typeof data === 'object' &&
- data !== null &&
- typeof (data as ReactFlowNodeEXT).type === 'string'
- );
-}
-
-export const subGraphToJson = (subGraph: SubGraph) => {
- const jsonNodes = subGraph.nodes.map(node => {
- let nodeData: ReactFlowNodeEXT;
- if (isReactFlowNodeEXT(node.data)) {
- nodeData = node.data;
- } else {
- // Handle the case where node.data is not a ReactFlowNodeEXT
- console.error("Invalid node data:", node.data);
- nodeData = { type: "STEP" }; // Providing default values to avoid potential errors
- }
- const reactNodeProps: ReactNodeProps = {
- id: node.id,
- width: node.width || 200,
- height: node.height || 200,
- position: node.position,
- data: nodeData,
- };
- return ReactToJsonNode(reactNodeProps);
- });
-
- return {
- name: subGraph.graphName,
- nodes: jsonNodes,
- serial_number: subGraph.serial_number
- }
-};
-
-export const allSubGraphsToJson = (subGraphs: SubGraph[]) => {
- return subGraphs.map(subGraphToJson);
-};
-
-
-export interface JsonSubGraph {
- name: string;
- nodes: JsonNodeData[];
- serial_number: number;
-}
-
-
-export const jsonToSubGraph = (json: JsonSubGraph): SubGraph => {
- const nodes: Node[] = json.nodes.map(nodeJson => {
- const reactNodeProps = JsonToReactNode(nodeJson, { x: nodeJson.ext?.pos_x || 0, y: nodeJson.ext?.pos_y || 0 });
- const { data, ...rest } = reactNodeProps;
- return {
- type: 'custom',
- ...rest,
- data: data as unknown as Record,
- };
- });
-
- const edges: Edge[] = [];
-
- nodes.forEach(node => {
- const nodeData = node.data as any;
-
-
- if (nodeData.nexts && Array.isArray(nodeData.nexts)) {
- nodeData.nexts.forEach((nextId:string) => {
- const newEdge: Edge = {
- id: `${node.id}-${nextId}`,
- source: node.id,
- target: nextId,
- type: 'custom',
- data: {
- sourceNode: node.id,
- targetNode: nextId
- }
- };
- edges.push(newEdge);
- });
- }
-
- if (nodeData.true_next) {
- const newEdge: Edge = {
- id: `${node.id}-${nodeData.true_next}-true`,
- source: node.id,
- target: nodeData.true_next,
- sourceHandle: 'true',
- type: 'custom',
- data: {
- sourceNode: node.id,
- targetNode: nodeData.true_next
- }
- };
- edges.push(newEdge);
- }
- if (nodeData.false_next) {
- const newEdge: Edge = {
- id: `${node.id}-${nodeData.false_next}-false`,
- source: node.id,
- target: nodeData.false_next,
- sourceHandle: 'false',
- type: 'custom',
- data: {
- sourceNode: node.id,
- targetNode: nodeData.false_next
- }
- };
- edges.push(newEdge);
- }
- });
-
-
- return {
- graphName: json.name,
- nodes,
- edges,
- serial_number: json.serial_number,
- };
-};
-
-export const jsonToSubGraphs = (jsonArray: JsonSubGraph[]): SubGraph[] => {
- return jsonArray.map(jsonToSubGraph);
-};
\ No newline at end of file
diff --git a/src/graph/NodeData.fromjson.test.tsx b/src/graph/NodeData.fromjson.test.tsx
deleted file mode 100644
index 195de2a..0000000
--- a/src/graph/NodeData.fromjson.test.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-// NodeData.fromjson.test.tsx
-import { describe, it, expect } from 'vitest';
-import { JsonToReactNode, JsonNodeData } from './NodeData';
-
-describe('JsonToReactNode', () => {
- it('should convert JSON node data to React node props correctly', () => {
- const jsonNodeData: JsonNodeData = {
- uniq_id: '123',
- name: 'Test Node',
- description: 'This is a test node',
- tool: 'testTool',
- nexts: ['456'],
- true_next: '789',
- false_next: null,
- type: 'STEP',
- ext: {
- pos_x: 100,
- pos_y: 200,
- width: 250,
- height: 150,
- info: 'Additional info'
- }
- };
-
- const expectedReactNodeProps = {
- id: '123',
- width: 250,
- height: 150,
- position: { x: 100, y: 200 },
- data: {
- type: 'STEP',
- name: 'Test Node',
- description: 'This is a test node',
- tool: 'testTool',
- nexts: ['456'],
- true_next: '789',
- false_next: null,
- info: 'Additional info',
- prevs: [],
- }
- };
-
- const reactNodeProps = JsonToReactNode(jsonNodeData);
- expect(reactNodeProps).toEqual(expectedReactNodeProps);
- });
-
- it('should handle missing optional fields in JSON data', () => {
- const jsonNodeData: JsonNodeData = {
- uniq_id: '456',
- name: 'Another Node',
- description: '',
- tool: 'anotherTool',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: {}
- };
-
- const expectedReactNodeProps = {
- id: '456',
- width: 200,
- height: 200,
- position: { x: 0, y: 0 },
- data: {
- type: 'STEP',
- name: 'Another Node',
- description: '',
- tool: 'anotherTool',
- nexts: [],
- true_next: null,
- false_next: null,
- info: null,
- prevs: [],
- }
- };
-
- const reactNodeProps = JsonToReactNode(jsonNodeData);
- expect(reactNodeProps).toEqual(expectedReactNodeProps);
- });
-
- it('should use a default type if not provided', () => {
- const jsonNodeData: JsonNodeData = {
- uniq_id: '789',
- name: 'Type Missing Node',
- description: 'This node has no type',
- tool: 'defaultTool',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: {
- pos_x: 50,
- pos_y: 75,
- width: 100,
- height: 100,
- info: null
- }
- };
-
- const expectedReactNodeProps = {
- id: '789',
- width: 100,
- height: 100,
- position: { x: 50, y: 75 },
- data: {
- type: 'STEP',
- name: 'Type Missing Node',
- description: 'This node has no type',
- tool: 'defaultTool',
- nexts: [],
- true_next: null,
- false_next: null,
- info: null,
- prevs: [],
- }
- };
-
- const reactNodeProps = JsonToReactNode(jsonNodeData);
- expect(reactNodeProps).toEqual(expectedReactNodeProps);
- });
-
- it('should handle info as null correctly', () => {
- const jsonNodeData: JsonNodeData = {
- uniq_id: '999',
- name: 'Info Null Node',
- description: 'This node has null info',
- tool: 'testTool',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: {
- pos_x: 100,
- pos_y: 200,
- width: 200,
- height: 100,
- info: null,
- },
- };
-
- const expectedReactNodeProps = {
- id: '999',
- width: 200,
- height: 100,
- position: { x: 100, y: 200 },
- data: {
- type: 'STEP',
- name: 'Info Null Node',
- description: 'This node has null info',
- tool: 'testTool',
- nexts: [],
- true_next: null,
- false_next: null,
- info: null,
- prevs: [],
- },
- };
-
- const reactNodeProps = JsonToReactNode(jsonNodeData);
- expect(reactNodeProps).toEqual(expectedReactNodeProps);
- });
- it('should handle undefined info correctly', () => {
- const jsonNodeData: JsonNodeData = {
- uniq_id: '999',
- name: 'Info Undefined Node',
- description: 'This node has undefined info',
- tool: 'testTool',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: {
- pos_x: 100,
- pos_y: 200,
- width: 200,
- height: 100,
- },
- };
-
- const expectedReactNodeProps = {
- id: '999',
- width: 200,
- height: 100,
- position: { x: 100, y: 200 },
- data: {
- type: 'STEP',
- name: 'Info Undefined Node',
- description: 'This node has undefined info',
- tool: 'testTool',
- nexts: [],
- true_next: null,
- false_next: null,
- info: null,
- prevs: [],
- },
- };
-
- const reactNodeProps = JsonToReactNode(jsonNodeData);
- expect(reactNodeProps).toEqual(expectedReactNodeProps);
- });
-});
\ No newline at end of file
diff --git a/src/graph/NodeData.tojson.test.tsx b/src/graph/NodeData.tojson.test.tsx
deleted file mode 100644
index 35032b5..0000000
--- a/src/graph/NodeData.tojson.test.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-// NodeData.tojson.test.tsx
-
-import React, { ReactNode } from 'react';
-import { render, screen, act } from '@testing-library/react';
-import { useGraph, GraphProvider, GraphContextType, SubGraph } from './GraphContext';
-import { describe, it, expect, vi } from 'vitest';
-import { Node } from '@xyflow/react';
-import { ReactToJsonNode, ReactNodeProps, ReactFlowNodeEXT } from './NodeData';
-
-
-interface TestComponentProps {
- onContextChange?: (context: GraphContextType) => void;
-}
-
-const TestComponent: React.FC = ({ onContextChange }) => {
- const graphContext = useGraph();
-
- React.useEffect(() => {
- if (onContextChange) {
- onContextChange(graphContext);
- }
- }, [graphContext, onContextChange]);
-
- const addTestNodes = () => {
- const testGraph: SubGraph = {
- graphName: "test",
- nodes: [
- {
- id: "1",
- type: "custom",
- position: { x: 100, y: 100 },
- data: { type: "START" } // info is not provided here
- } as Node,
- {
- id: "2",
- type: "custom",
- position: { x: 200, y: 250 },
- data: {
- type: "INFO",
- description: "test for flow",
- } // info is not provided here
- } as Node,
- {
- id: "3",
- type: "custom",
- position: { x: 300, y: 330 },
- data: {
- type: "STEP",
- description: "try use a tool",
- tool: "save_file",
- } // info is not provided here
- } as Node
- ],
- edges: [],
- serial_number: 4
- };
- graphContext.updateSubGraph("test", testGraph);
- };
-
- const convertFirstNodeToJson = () => {
- if (graphContext.subGraphs.length > 0) {
- const testGraph = graphContext.subGraphs.find((graph) => graph.graphName === 'test');
- if (testGraph && testGraph.nodes.length > 0) {
- const jsonNodes = testGraph.nodes.map(node => {
- const nodeData = node.data as { type: string };
- const reactNodeProps: ReactNodeProps = {
- id: node.id,
- width: 200, // Or use a constant
- height: 200, // Or use a constant
- position: node.position,
- data: {
- ...node.data,
- type: nodeData.type,
-
- } as ReactFlowNodeEXT,
- };
- const jsonNode = ReactToJsonNode(reactNodeProps);
- return jsonNode;
- })
- console.log("JSON Nodes:", jsonNodes);
- }
- }
- }
-
- return (
-
-
-
-
-
-
SubGraph Count: {graphContext.subGraphs.length}
-
- Has Root Graph: {graphContext.subGraphs.some(graph => graph.graphName === 'root') ? 'true' : 'false'}
-
-
- Has Test Graph: {graphContext.subGraphs.some(graph => graph.graphName === 'test') ? 'true' : 'false'}
-
-
- Test Graph Nodes: {graphContext.subGraphs.find(graph => graph.graphName === 'test')?.nodes.length || 0}
-
-
- );
-};
-
-interface TestWrapperProps {
- children: ReactNode;
-}
-
-const TestWrapper: React.FC = ({ children }) => {
- return (
- {children}
- );
-};
-
-describe('GraphContext', () => {
- it('should manage subgraphs correctly', async () => {
- // Mock console.log to capture its calls
- const consoleLogMock = vi.spyOn(console, 'log');
-
- const handleContextChange = () => {
- // graphContextValue = context; remove unused variable
- };
-
- render(
-
-
-
- );
-
- const testComponent = screen.getByTestId("test-component");
- expect(testComponent).toBeInTheDocument();
-
- const addRootButton = screen.getByTestId("add-root-button");
- const addSubGraphButton = screen.getByTestId("add-subgraph-button");
- const addNodesButton = screen.getByTestId("add-nodes-button");
- const convertToJsonButton = screen.getByTestId("convert-to-json-button");
-
-
- // Add root and test subgraph
- await act(async () => {
- addRootButton.click();
- });
- await act(async () => {
- addSubGraphButton.click();
- });
-
- let subGraphCountElement = screen.getByTestId("subgraph-count");
- expect(subGraphCountElement).toHaveTextContent("SubGraph Count: 2");
-
- let hasRootElement = screen.getByTestId("has-root-graph");
- expect(hasRootElement).toHaveTextContent("Has Root Graph: true");
-
- let hasTestElement = screen.getByTestId("has-test-graph");
- expect(hasTestElement).toHaveTextContent("Has Test Graph: true");
-
- // Add nodes to test subgraph
- await act(async () => {
- addNodesButton.click();
- });
-
-
-
- const testGraphNodesElement = screen.getByTestId("test-graph-nodes");
- expect(testGraphNodesElement).toHaveTextContent("Test Graph Nodes: 3");
-
- // Convert first node to JSON
- await act(async () => {
- convertToJsonButton.click();
- });
-
- // Check if console.log was called with the correct JSON
- expect(consoleLogMock).toHaveBeenCalled();
- const consoleArgs = consoleLogMock.mock.calls[0];
- expect(consoleArgs[0]).toBe("JSON Nodes:");
- const jsonOutput = consoleArgs[1];
- expect(jsonOutput).toEqual([
- {
- uniq_id: '1',
- type: 'START',
- name: "",
- description: "",
- tool: "",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 100, pos_y: 100, width: 200, height: 200, info: null }
- },
- {
- uniq_id: '2',
- type: 'INFO',
- name: "",
- description: 'test for flow',
- tool: "",
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 200, pos_y: 250, width: 200, height: 200, info: null }
- },
- {
- uniq_id: '3',
- type: 'STEP',
- name: "",
- description: 'try use a tool',
- tool: 'save_file',
- nexts: [],
- true_next: null,
- false_next: null,
- ext: { pos_x: 300, pos_y: 330, width: 200, height: 200, info: null }
- }
- ]);
-
- // Try adding root again, should not change the count
- await act(async () => {
- addRootButton.click();
- });
- subGraphCountElement = screen.getByTestId("subgraph-count");
- expect(subGraphCountElement).toHaveTextContent("SubGraph Count: 2");
-
- hasRootElement = screen.getByTestId("has-root-graph");
- expect(hasRootElement).toHaveTextContent("Has Root Graph: true");
- hasTestElement = screen.getByTestId("has-test-graph");
- expect(hasTestElement).toHaveTextContent("Has Test Graph: true");
- // Restore the original console.log
- consoleLogMock.mockRestore();
- });
-});
\ No newline at end of file
diff --git a/src/graph/NodeData.ts b/src/graph/NodeData.ts
deleted file mode 100644
index 6939839..0000000
--- a/src/graph/NodeData.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-// NodeData.ts
-
-export interface ReactFlowNodeEXT {
- type: string;
- name?: string | undefined;
- description?: string | undefined;
- tool?: string | undefined;
- prevs?: string[];
- nexts?: string[];
- true_next?: string | null | undefined;
- false_next?: string | null | undefined;
- info?: string | null;
-}
-
-export interface ReactNodeProps {
- id: string;
- width: number;
- height: number;
- position: { x: number, y: number }
- data: ReactFlowNodeEXT;
- onNodeDataChange?: (id: string, newData: ReactFlowNodeEXT) => void;
-}
-
-export interface JsonNodeData {
- uniq_id: string;
- name: string;
- description: string;
- nexts: string[];
- type?: string;
- tool: string;
- true_next: string | null;
- false_next: string | null;
- ext: {
- pos_x?: number;
- pos_y?: number;
- width?: number;
- height?: number;
- info?: string | null;
- };
-}
-
-
-export const JsonToReactNode = (jsonData: JsonNodeData, position?: { x: number, y: number }): ReactNodeProps => {
- const { uniq_id, ext, ...rest } = jsonData;
-
- const reactNodeData: ReactFlowNodeEXT = {
- type: rest.type || "STEP",
- name: rest.name,
- description: rest.description,
- tool: rest.tool,
- nexts: rest.nexts || [],
- true_next: rest.true_next,
- false_next: rest.false_next,
- info: ext?.info ?? null,
- prevs: [],
- };
-
- return {
- id: uniq_id,
- width: ext?.width ?? 200,
- height: ext?.height ?? 200,
- position: position ||
- {
- x: ext?.pos_x || 0,
- y: ext?.pos_y || 0
- },
- data: reactNodeData,
- };
-};
-
-export const ReactToJsonNode = (reactNode: ReactNodeProps): JsonNodeData => {
- const { id, data, width, height, position } = reactNode;
- const { type, name, description, tool, nexts, true_next, false_next, info } = data;
-
- const ext: JsonNodeData['ext'] = {
- pos_x: position.x,
- pos_y: position.y,
- width,
- height,
- info: info === undefined ? null : info // Set info to null if undefined
- };
-
- return {
- uniq_id: id,
- type,
- name: name || "",
- description: description || "",
- tool: tool || "",
- nexts: nexts || [],
- true_next: true_next == undefined ? null : true_next,
- false_next: false_next == undefined ? null : false_next,
- ext
- };
-};
\ No newline at end of file
diff --git a/src/graph/ResizeIcon.tsx b/src/graph/ResizeIcon.tsx
deleted file mode 100644
index 41fce7d..0000000
--- a/src/graph/ResizeIcon.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-// ResizeIcon.tsx
-
-function ResizeIcon(): JSX.Element {
- return (
-
- );
-}
-
-export default ResizeIcon;
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index e7d4bb2..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,72 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/src/main.test.tsx b/src/main.test.tsx
deleted file mode 100644
index 01572ed..0000000
--- a/src/main.test.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// src/main.test.tsx
-import { render, screen } from '@testing-library/react';
-import { expect, describe, it } from 'vitest';
-
-import { Provider } from 'react-redux';
-import { store } from "./redux/store";
-import { ReactFlowProvider } from '@xyflow/react';
-import { GraphProvider } from './Graph/GraphContext';
-import { StrictMode } from 'react';
-import App from './App';
-
-describe('Application Rendering', () => {
- it('renders the main application with providers', () => {
- render(
-
-
-
-
-
-
-
-
-
-
-
- );
-
- // Now you can assert that elements from your App or child components are rendered.
- // Example: Replace this with your actual test assertion.
- // This example assumes you have an element with the text "My App" inside App component or child.
- const appElement = screen.getByRole("main");
- expect(appElement).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
deleted file mode 100644
index 9733a89..0000000
--- a/src/main.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-// main.tsx
-
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-
-import { Provider } from 'react-redux';
-import {store} from "./redux/store.ts"
-import { ReactFlowProvider } from '@xyflow/react';
-import { GraphProvider } from './Graph/GraphContext';
-
-
-createRoot(document.getElementById('root')!).render(
-
-
-
-
-
-
-
-
- ,
-)
\ No newline at end of file
diff --git a/src/redux/store.ts b/src/redux/store.ts
deleted file mode 100644
index b1747bb..0000000
--- a/src/redux/store.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-// redux/store.ts
-
-import { configureStore, } from '@reduxjs/toolkit';
-import userInfoReducer from './userInfo.store';
-
-export const store = configureStore(
- {
- reducer: {
- userInfo: userInfoReducer,
- },
- },
-);
-
-// Optional: Attach store to the window object for debugging (use conditionally)
-if (process.env.NODE_ENV === 'DEBUG') {
- (window as any).store = store;
-}
-
-export type RootState = ReturnType;
-export type AppDispatch = typeof store.dispatch;
-export type AppStore = typeof store;
\ No newline at end of file
diff --git a/src/redux/userInfo.store.ts b/src/redux/userInfo.store.ts
deleted file mode 100644
index 4eee535..0000000
--- a/src/redux/userInfo.store.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-// redux/userInfo.store.ts
-
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-
-interface UserInfoState {
- user_id: string;
- llmModel: string;
- apiKey: string;
-}
-
-const initialState: UserInfoState = {
- user_id: localStorage.getItem('user_id') || '',
- llmModel: localStorage.getItem('llmModel') || '',
- apiKey: localStorage.getItem('apiKey') || '',
-};
-
-const userInfoSlice = createSlice({
- name: 'userInfo',
- initialState,
- reducers: {
- setSettings: (state, action: PayloadAction<{ newLlmModel: string; newapiKey: string; newUserId: string }>) => {
- const { newLlmModel, newapiKey, newUserId } = action.payload;
- state.llmModel = newLlmModel;
- state.apiKey = newapiKey;
- state.user_id = newUserId;
-
- localStorage.setItem('llmModel', newLlmModel);
- localStorage.setItem('apiKey', newapiKey);
- localStorage.setItem('user_id', newUserId);
- },
- setUserId: (state, action: PayloadAction) => {
- state.user_id = action.payload;
- localStorage.setItem('user_id', action.payload);
- }
- },
-});
-
-export const { setSettings, setUserId } = userInfoSlice.actions;
-export default userInfoSlice.reducer;
\ No newline at end of file
diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx
deleted file mode 100644
index c7dccbd..0000000
--- a/src/routes/AppRoutes.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// routes/AppRoutes.tsx
-
-import React from 'react';
-import { BrowserRouter, Routes, Route } from 'react-router-dom';
-
-import GraphApp from '../Graph/GraphApp';
-import MenuLayout from '../GraphMenu/MenuLayout';
-import DocPage from '../Doc/DocPage';
-
-// Example Components
-const HomePage = () => Home Page
;
-const NotFoundPage = () => 404 Not Found
;
-
-const AppRoutes: React.FC = () => {
- return (
-
-
- {/* Apply MenuLayout ONLY on the root (/) */}
- }>
- } />
-
-
- {/* Other paths, without MenuLayout */}
- } />
- } />
-
- {/* Catch-all for 404 */}
- } />
-
-
- );
-};
-
-export default AppRoutes;
diff --git a/src/utils/ConfigManager.ts b/src/utils/ConfigManager.ts
deleted file mode 100644
index 7e676ba..0000000
--- a/src/utils/ConfigManager.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-// ConfigManager.ts
-
-interface ConfigSettings {
- username: string;
- llmModel: string;
- apiKey: string;
-}
-
-class ConfigManager {
- private static instance: ConfigManager;
- private llmModel: string = 'gpt';
- private apiKey: string = '';
- private username: string = 'unknown';
-
- constructor() {
- if (ConfigManager.instance) {
- return ConfigManager.instance;
- }
-
- const storedLlmModel = localStorage.getItem('llmModel');
- if(storedLlmModel) {
- this.llmModel = storedLlmModel;
- }
-
- const storedApiKey = localStorage.getItem('apiKey');
- if(storedApiKey) {
- this.apiKey = storedApiKey;
- }
-
- this.fetchUsername(); // Initiate username fetch
-
- ConfigManager.instance = this;
- }
-
- // Method to fetch username from Nginx API
- private async fetchUsername() {
- try {
- const response = await fetch('/api/username', {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
-
- if (response.ok) {
- const data = await response.json() as {username: string};
- this.username = data.username;
- } else {
- console.error('Failed to fetch username:', response.status);
- }
- } catch (error) {
- if(error instanceof Error) {
- console.error('Error fetching username:', error.message);
- } else {
- console.error('Error fetching username:', error)
- }
- }
- }
-
-
- // Method to get the current settings
- getSettings(): ConfigSettings {
- return {
- username: this.username,
- llmModel: this.llmModel,
- apiKey: this.apiKey,
- };
- }
-
- // Method to update settings
- setSettings(newLlmModel: string, newapiKey: string): void {
- this.llmModel = newLlmModel;
- this.apiKey = newapiKey;
-
- localStorage.setItem('llmModel', newLlmModel);
- localStorage.setItem('apiKey', newapiKey);
- }
-}
-
-const instance = new ConfigManager();
-export default instance;
\ No newline at end of file
diff --git a/src/utils/JsonIO.ts b/src/utils/JsonIO.ts
deleted file mode 100644
index 52bf95a..0000000
--- a/src/utils/JsonIO.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-// utils/JsonIO.ts
-
-export const saveJsonToFile = (filename: string, JsonData: any): void => {
- try {
- const blob = new Blob([JSON.stringify(JsonData, null, 2)], {
- type: 'application/json',
- });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- alert('File saving!');
- } catch (error) {
- console.error('Error saving JSON:', error);
- alert('Failed to save file.');
- }
-};
-
-export const loadJsonFromFile = (): Promise => {
- return new Promise((resolve, reject) => {
- const fileInput = document.createElement('input');
- fileInput.type = 'file';
- fileInput.accept = '.json';
- fileInput.style.display = 'none';
- document.body.appendChild(fileInput);
-
- fileInput.addEventListener('change', async (event) => {
- try {
- const file = (event.target as HTMLInputElement).files?.[0];
- if (!file) {
- reject(new Error('No file selected.'));
- return;
- }
- const reader = new FileReader();
- reader.onload = async (e) => {
- try {
- const contents = e.target?.result;
- if (typeof contents === 'string') {
- const parsedData = JSON.parse(contents);
- resolve(parsedData);
- } else {
- reject(new Error('File contents are not a string.'));
- }
-
- } catch (error) {
- reject(new Error('Error parsing JSON.' + error));
- }
- };
- reader.onerror = () => reject(new Error('Error reading file.'));
- reader.readAsText(file);
- } catch (error) {
- console.error("Error during file handling:", error);
- reject(new Error('Error loading JSON:' + error));
- } finally {
- document.body.removeChild(fileInput);
- }
- });
- fileInput.click();
- });
-};
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe..0000000
--- a/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index 680cf18..0000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// tailwind.config.js
-
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: [
- "./src/**/*.{js,jsx,ts,tsx}",
- ],
- theme: {
- extend: {
- colors: {
- primary: '#007bff',
- secondary: '#333',
- lightGray: '#ddd',
- background: '#f9f9f9',
- },
- },
- },
- plugins: [],
-};
\ No newline at end of file
diff --git a/tsconfig.app.json b/tsconfig.app.json
deleted file mode 100644
index 47d9ee1..0000000
--- a/tsconfig.app.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true,
- "exactOptionalPropertyTypes": true,
- "noImplicitOverride": true
- },
- "include": ["src", "setupTests.ts"],
- "exclude": ["node_modules"]
-}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index c452f43..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
-}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
deleted file mode 100644
index db0becc..0000000
--- a/tsconfig.node.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2022",
- "lib": ["ES2023"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/vite.config.ts b/vite.config.ts
deleted file mode 100644
index 5f0e570..0000000
--- a/vite.config.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-///
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-
-type BackendUrlMode = 'development' | 'production';
-
-const backendUrls: Record = {
- development: 'http://localhost:5000', // Docker Compose
- production: 'http://backend.local' // K8s preview
-};
-
-export default defineConfig(({ mode }) => {
- const safeMode = mode as BackendUrlMode;
- const backendUrl = backendUrls[safeMode] || backendUrls.production;
-
- return {
- plugins: [react()],
- server: {
- host: '0.0.0.0',
- port: 3000,
- allowedHosts: [
- 'localhost',
- '127.0.0.1',
- 'yourdomain.com',
- ],
- },
- test: {
- globals: true,
- environment: "jsdom",
- setupFiles: "./setupTests.ts",
- },
- define: {
- 'import.meta.env.VITE_BACKEND_URL': JSON.stringify(backendUrl)
- }
- }
-})
\ No newline at end of file