diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4294bde
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+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/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..922e683
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+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/LICENSE b/LICENSE
new file mode 100644
index 0000000..870a1b4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+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/README.md b/README.md
new file mode 100644
index 0000000..b2b7d07
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# 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/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..6277d3d
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,31 @@
+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
new file mode 100644
index 0000000..4c6d616
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ LangGraph-GUI
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8b7fcee
--- /dev/null
+++ b/package.json
@@ -0,0 +1,45 @@
+{
+ "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
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/setupTests.ts b/setupTests.ts
new file mode 100644
index 0000000..2eee530
--- /dev/null
+++ b/setupTests.ts
@@ -0,0 +1,17 @@
+// 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
new file mode 100644
index 0000000..73579a5
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,12 @@
+// 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
new file mode 100644
index 0000000..6f4bd7d
--- /dev/null
+++ b/src/Doc/DocPage.tsx
@@ -0,0 +1,15 @@
+// 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
new file mode 100644
index 0000000..769eabd
--- /dev/null
+++ b/src/GraphMenu/ConfigWindow.tsx
@@ -0,0 +1,78 @@
+// 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
+
+
+ Username:
+
+
+
+
+
+ LLM model:
+ setLlmModel(e.target.value)}
+ className="border border-gray-300 p-2 rounded w-full focus:outline-none text-black bg-white"
+ />
+
+
+
+
+ API Key:
+ setAPIKey(e.target.value)}
+ className="border border-gray-300 p-2 rounded w-full focus:outline-none text-black bg-white"
+ />
+
+
+
+
+ Save
+
+
+ Cancel
+
+
+
+
+ );
+}
+
+export default ConfigWindow;
\ No newline at end of file
diff --git a/src/GraphMenu/FileTransmit.ts b/src/GraphMenu/FileTransmit.ts
new file mode 100644
index 0000000..7b77a77
--- /dev/null
+++ b/src/GraphMenu/FileTransmit.ts
@@ -0,0 +1,99 @@
+// 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
new file mode 100644
index 0000000..d8b65cb
--- /dev/null
+++ b/src/GraphMenu/MenuLayout.tsx
@@ -0,0 +1,66 @@
+// 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
new file mode 100644
index 0000000..41ea2c6
--- /dev/null
+++ b/src/GraphMenu/MenuToggleButton.tsx
@@ -0,0 +1,87 @@
+// 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 = '';
+ }
+
+ }}
+ />
+
+ To Run Graph
+
+
{
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+
+ }} className="bg-blue-500 hover:bg-blue-700 text-white font-semibold px-1 rounded focus:outline-none focus:shadow-outline text-sm">
+ Upload Files to Server
+
+
{
+ await handleDownload();
+ }} className="bg-blue-500 hover:bg-green-700 text-white font-semibold px-1 rounded focus:outline-none focus:shadow-outline text-sm">
+ Get Files from Server
+
+
{
+ await handleCleanCache();
+ }} className="bg-blue-500 hover:bg-yellow-700 text-white font-semibold px-1 rounded focus:outline-none focus:shadow-outline text-sm">
+ Clean Server Cache
+
+
+ Settings
+
+
+ Documentation
+
+
+ );
+};
+
+export default MenuToggleButton;
\ No newline at end of file
diff --git a/src/GraphMenu/RunWindow.tsx b/src/GraphMenu/RunWindow.tsx
new file mode 100644
index 0000000..28e1f88
--- /dev/null
+++ b/src/GraphMenu/RunWindow.tsx
@@ -0,0 +1,198 @@
+// 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], 'workflow.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
+
+
+ Run
+
+
+ Leave
+
+
+
+ {/* ADDED TEXT-BLACK HERE */}
+
{responseMessage}
+
+
+
+ );
+}
+
+
+export default RunWindow;
\ No newline at end of file
diff --git a/CustomEdge.tsx b/src/graph/CustomEdge.tsx
similarity index 100%
rename from CustomEdge.tsx
rename to src/graph/CustomEdge.tsx
diff --git a/CustomNode.tsx b/src/graph/CustomNode.tsx
similarity index 100%
rename from CustomNode.tsx
rename to src/graph/CustomNode.tsx
diff --git a/GraphActions.tsx b/src/graph/GraphActions.tsx
similarity index 100%
rename from GraphActions.tsx
rename to src/graph/GraphActions.tsx
diff --git a/GraphApp.css b/src/graph/GraphApp.css
similarity index 100%
rename from GraphApp.css
rename to src/graph/GraphApp.css
diff --git a/GraphApp.tsx b/src/graph/GraphApp.tsx
similarity index 100%
rename from GraphApp.tsx
rename to src/graph/GraphApp.tsx
diff --git a/GraphContext.test.tsx b/src/graph/GraphContext.test.tsx
similarity index 100%
rename from GraphContext.test.tsx
rename to src/graph/GraphContext.test.tsx
diff --git a/GraphContext.tsx b/src/graph/GraphContext.tsx
similarity index 100%
rename from GraphContext.tsx
rename to src/graph/GraphContext.tsx
diff --git a/GraphPanel.css b/src/graph/GraphPanel.css
similarity index 100%
rename from GraphPanel.css
rename to src/graph/GraphPanel.css
diff --git a/GraphPanel.tsx b/src/graph/GraphPanel.tsx
similarity index 100%
rename from GraphPanel.tsx
rename to src/graph/GraphPanel.tsx
diff --git a/JsonUtil.fromjson.test.tsx b/src/graph/JsonUtil.fromjson.test.tsx
similarity index 100%
rename from JsonUtil.fromjson.test.tsx
rename to src/graph/JsonUtil.fromjson.test.tsx
diff --git a/JsonUtil.tojson.test.tsx b/src/graph/JsonUtil.tojson.test.tsx
similarity index 100%
rename from JsonUtil.tojson.test.tsx
rename to src/graph/JsonUtil.tojson.test.tsx
diff --git a/JsonUtil.tsx b/src/graph/JsonUtil.tsx
similarity index 100%
rename from JsonUtil.tsx
rename to src/graph/JsonUtil.tsx
diff --git a/NodeData.fromjson.test.tsx b/src/graph/NodeData.fromjson.test.tsx
similarity index 100%
rename from NodeData.fromjson.test.tsx
rename to src/graph/NodeData.fromjson.test.tsx
diff --git a/NodeData.tojson.test.tsx b/src/graph/NodeData.tojson.test.tsx
similarity index 100%
rename from NodeData.tojson.test.tsx
rename to src/graph/NodeData.tojson.test.tsx
diff --git a/NodeData.ts b/src/graph/NodeData.ts
similarity index 100%
rename from NodeData.ts
rename to src/graph/NodeData.ts
diff --git a/ResizeIcon.tsx b/src/graph/ResizeIcon.tsx
similarity index 100%
rename from ResizeIcon.tsx
rename to src/graph/ResizeIcon.tsx
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..e7d4bb2
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,72 @@
+@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
new file mode 100644
index 0000000..01572ed
--- /dev/null
+++ b/src/main.test.tsx
@@ -0,0 +1,34 @@
+// 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
new file mode 100644
index 0000000..9733a89
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,24 @@
+// 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
new file mode 100644
index 0000000..b1747bb
--- /dev/null
+++ b/src/redux/store.ts
@@ -0,0 +1,21 @@
+// 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
new file mode 100644
index 0000000..4eee535
--- /dev/null
+++ b/src/redux/userInfo.store.ts
@@ -0,0 +1,39 @@
+// 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
new file mode 100644
index 0000000..c7dccbd
--- /dev/null
+++ b/src/routes/AppRoutes.tsx
@@ -0,0 +1,34 @@
+// 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
new file mode 100644
index 0000000..7e676ba
--- /dev/null
+++ b/src/utils/ConfigManager.ts
@@ -0,0 +1,81 @@
+// 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
new file mode 100644
index 0000000..52bf95a
--- /dev/null
+++ b/src/utils/JsonIO.ts
@@ -0,0 +1,64 @@
+// 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
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..680cf18
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,19 @@
+// 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
new file mode 100644
index 0000000..47d9ee1
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,29 @@
+{
+ "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
new file mode 100644
index 0000000..c452f43
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "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
new file mode 100644
index 0000000..db0becc
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "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
new file mode 100644
index 0000000..6f11007
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,36 @@
+///
+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://yourdomain.com' // K8s deploy
+};
+
+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