diff --git a/Data/WebUI/src/App.jsx b/Data/WebUI/src/App.jsx
index 8c0b2e1..322926f 100644
--- a/Data/WebUI/src/App.jsx
+++ b/Data/WebUI/src/App.jsx
@@ -1,13 +1,15 @@
+// App.jsx
+
// Core React Imports
import React, {
useState,
useEffect,
useCallback,
useRef
-} from "react";
-
-// Material UI - Components
-import {
+ } from "react";
+
+ // Material UI - Components
+ import {
AppBar,
Toolbar,
Typography,
@@ -18,1185 +20,684 @@ import {
CssBaseline,
ThemeProvider,
createTheme,
- Accordion,
- AccordionSummary,
- AccordionDetails,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Divider,
- Tooltip,
- Tabs,
- Tab,
TextField
-} from "@mui/material";
-
-// Material UI - Icons
-import {
- DragIndicator as DragIndicatorIcon,
+ } from "@mui/material";
+
+ // Material UI - Icons
+ import {
KeyboardArrowDown as KeyboardArrowDownIcon,
- ExpandMore as ExpandMoreIcon,
- Save as SaveIcon,
- FileOpen as FileOpenIcon,
- DeleteForever as DeleteForeverIcon,
InfoOutlined as InfoOutlinedIcon,
- Polyline as PolylineIcon,
MergeType as MergeTypeIcon,
- People as PeopleIcon,
- Add as AddIcon
-} from "@mui/icons-material";
-
-// React Flow
-import ReactFlow, {
- Background,
- addEdge,
- applyNodeChanges,
- applyEdgeChanges,
- ReactFlowProvider,
- useReactFlow
-} from "reactflow";
-
-// Styles
-import "reactflow/dist/style.css";
-import "./Borealis.css";
-
-// Global Node Update Timer Variable
-if (!window.BorealisUpdateRate) {
+ People as PeopleIcon
+ } from "@mui/icons-material";
+
+ // React Flow
+ import { ReactFlowProvider } from "reactflow";
+
+ // Styles
+ import "reactflow/dist/style.css";
+ import "./Borealis.css";
+
+ // Import our new components
+ import FlowTabs from "./Flow_Tabs";
+ import FlowEditor from "./Flow_Editor";
+ import NodeSidebar from "./Node_Sidebar";
+
+ // Global Node Update Timer Variable
+ if (!window.BorealisUpdateRate) {
window.BorealisUpdateRate = 200;
-}
-
-// Dynamically load all node components
-const nodeContext = require.context("./nodes", true, /\.jsx$/);
-const nodeTypes = {};
-const categorizedNodes = {};
-
-nodeContext.keys().forEach((path) => {
+ }
+
+ // Dynamically load all node components
+ const nodeContext = require.context("./nodes", true, /\.jsx$/);
+ const nodeTypes = {};
+ const categorizedNodes = {};
+
+ nodeContext.keys().forEach((path) => {
const mod = nodeContext(path);
if (!mod.default) return;
const { type, label, component } = mod.default;
if (!type || !component) return;
-
+
const pathParts = path.replace("./", "").split("/");
if (pathParts.length < 2) return;
const category = pathParts[0];
-
+
if (!categorizedNodes[category]) {
- categorizedNodes[category] = [];
+ categorizedNodes[category] = [];
}
categorizedNodes[category].push(mod.default);
nodeTypes[type] = component;
-});
-
-// Single flow editor
-function FlowEditor({ nodes, edges, setNodes, setEdges, nodeTypes }) {
- const wrapperRef = useRef(null);
- const { project } = useReactFlow();
- const [contextMenu, setContextMenu] = useState(null);
-
- const onDrop = useCallback(
- (event) => {
- event.preventDefault();
- const type = event.dataTransfer.getData("application/reactflow");
- if (!type) return;
-
- const bounds = wrapperRef.current.getBoundingClientRect();
- const position = project({
- x: event.clientX - bounds.left,
- y: event.clientY - bounds.top
- });
-
- const id = "node-" + Date.now();
-
- const nodeMeta = Object.values(categorizedNodes)
- .flat()
- .find((n) => n.type === type);
-
- const newNode = {
- id: id,
- type: type,
- position: position,
- data: {
- label: nodeMeta?.label || type,
- content: nodeMeta?.content
- }
- };
-
- setNodes((nds) => [...nds, newNode]);
- },
- [project, setNodes]
- );
-
- const onDragOver = useCallback((event) => {
- event.preventDefault();
- event.dataTransfer.dropEffect = "move";
- }, []);
-
- const onConnect = useCallback(
- (params) =>
- setEdges((eds) =>
- addEdge(
- {
- ...params,
- type: "smoothstep",
- animated: true,
- style: {
- strokeDasharray: "6 3",
- stroke: "#58a6ff"
- }
- },
- eds
- )
- ),
- [setEdges]
- );
-
- const onNodesChange = useCallback(
- (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
- [setNodes]
- );
-
- const onEdgesChange = useCallback(
- (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
- [setEdges]
- );
-
- const handleRightClick = (e, node) => {
- e.preventDefault();
- setContextMenu({
- mouseX: e.clientX + 2,
- mouseY: e.clientY - 6,
- nodeId: node.id
- });
- };
-
- const handleDisconnect = () => {
- if (contextMenu?.nodeId) {
- setEdges((eds) =>
- eds.filter(
- (e) =>
- e.source !== contextMenu.nodeId &&
- e.target !== contextMenu.nodeId
- )
- );
- }
- setContextMenu(null);
- };
-
- const handleRemoveNode = () => {
- if (contextMenu?.nodeId) {
- setNodes((nds) =>
- nds.filter((n) => n.id !== contextMenu.nodeId)
- );
- setEdges((eds) =>
- eds.filter(
- (e) =>
- e.source !== contextMenu.nodeId &&
- e.target !== contextMenu.nodeId
- )
- );
- }
- setContextMenu(null);
- };
-
- useEffect(() => {
- const nodeCountEl = document.getElementById("nodeCount");
- if (nodeCountEl) {
- nodeCountEl.innerText = nodes.length;
- }
- }, [nodes]);
-
- return (
-
-
-
-
-
- {/* Right-click node menu */}
-
-
- );
-}
-
-const darkTheme = createTheme({
+ });
+
+ const darkTheme = createTheme({
palette: {
- mode: "dark",
- background: {
- default: "#121212",
- paper: "#1e1e1e"
- },
- text: {
- primary: "#ffffff"
- }
+ mode: "dark",
+ background: {
+ default: "#121212",
+ paper: "#1e1e1e"
+ },
+ text: {
+ primary: "#ffffff"
+ }
}
-});
-
-export default function App() {
+ });
+
+ export default function App() {
const [tabs, setTabs] = useState([
+ {
+ id: "flow_1",
+ tab_name: "Flow 1",
+ nodes: [],
+ edges: []
+ }
+ ]);
+ const [activeTabId, setActiveTabId] = useState("flow_1");
+
+ // About menu
+ const [aboutAnchorEl, setAboutAnchorEl] = useState(null);
+ const [creditsDialogOpen, setCreditsDialogOpen] = useState(false);
+
+ // Close all flows
+ const [confirmCloseOpen, setConfirmCloseOpen] = useState(false);
+
+ // Rename tab
+ const [renameDialogOpen, setRenameDialogOpen] = useState(false);
+ const [renameTabId, setRenameTabId] = useState(null);
+ const [renameValue, setRenameValue] = useState("");
+
+ // Right-click tab menu
+ const [tabMenuAnchor, setTabMenuAnchor] = useState(null);
+ const [tabMenuTabId, setTabMenuTabId] = useState(null);
+
+ // File input ref (for imports on older browsers)
+ const fileInputRef = useRef(null);
+
+ // Setup callbacks to update nodes/edges in the currently active tab
+ const handleSetNodes = useCallback(
+ (callbackOrArray, tId) => {
+ const targetId = tId || activeTabId;
+ setTabs((old) =>
+ old.map((tab) => {
+ if (tab.id !== targetId) return tab;
+ const newNodes =
+ typeof callbackOrArray === "function"
+ ? callbackOrArray(tab.nodes)
+ : callbackOrArray;
+ return { ...tab, nodes: newNodes };
+ })
+ );
+ },
+ [activeTabId]
+ );
+
+ const handleSetEdges = useCallback(
+ (callbackOrArray, tId) => {
+ const targetId = tId || activeTabId;
+ setTabs((old) =>
+ old.map((tab) => {
+ if (tab.id !== targetId) return tab;
+ const newEdges =
+ typeof callbackOrArray === "function"
+ ? callbackOrArray(tab.edges)
+ : callbackOrArray;
+ return { ...tab, edges: newEdges };
+ })
+ );
+ },
+ [activeTabId]
+ );
+
+ // About menu
+ const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
+ const handleAboutMenuClose = () => setAboutAnchorEl(null);
+
+ // Credits
+ const openCreditsDialog = () => {
+ handleAboutMenuClose();
+ setCreditsDialogOpen(true);
+ };
+
+ // Close all dialog
+ const handleOpenCloseAllDialog = () => setConfirmCloseOpen(true);
+ const handleCloseDialog = () => setConfirmCloseOpen(false);
+ const handleConfirmCloseAll = () => {
+ setTabs([
{
+ id: "flow_1",
+ tab_name: "Flow 1",
+ nodes: [],
+ edges: []
+ }
+ ]);
+ setActiveTabId("flow_1");
+ setConfirmCloseOpen(false);
+ };
+
+ // Create new tab
+ const createNewTab = () => {
+ const nextIndex = tabs.length + 1;
+ const newId = "flow_" + nextIndex;
+ setTabs((old) => [
+ ...old,
+ {
+ id: newId,
+ tab_name: "Flow " + nextIndex,
+ nodes: [],
+ edges: []
+ }
+ ]);
+ setActiveTabId(newId);
+ };
+
+ // Handle user clicking on a tab
+ const handleTabChange = (newActiveTabId) => {
+ setActiveTabId(newActiveTabId);
+ };
+
+ // Right-click tab menu
+ const handleTabRightClick = (evt, tabId) => {
+ evt.preventDefault();
+ setTabMenuAnchor({ x: evt.clientX, y: evt.clientY });
+ setTabMenuTabId(tabId);
+ };
+ const handleCloseTabMenu = () => {
+ setTabMenuAnchor(null);
+ setTabMenuTabId(null);
+ };
+
+ // Rename / close tab
+ const handleRenameTab = () => {
+ setRenameDialogOpen(true);
+ setRenameTabId(tabMenuTabId);
+ const t = tabs.find((x) => x.id === tabMenuTabId);
+ setRenameValue(t ? t.tab_name : "");
+ handleCloseTabMenu();
+ };
+ const handleCloseTab = () => {
+ setTabs((old) => {
+ const idx = old.findIndex((t) => t.id === tabMenuTabId);
+ if (idx === -1) return old;
+
+ const newList = [...old];
+ newList.splice(idx, 1);
+
+ // If we closed the current tab, pick a new active tab
+ if (tabMenuTabId === activeTabId && newList.length > 0) {
+ setActiveTabId(newList[0].id);
+ } else if (newList.length === 0) {
+ // If we closed the only tab, create a fresh one
+ newList.push({
id: "flow_1",
tab_name: "Flow 1",
nodes: [],
edges: []
+ });
+ setActiveTabId("flow_1");
}
- ]);
- const [activeTabId, setActiveTabId] = useState("flow_1");
-
- const [aboutAnchorEl, setAboutAnchorEl] = useState(null);
- const [confirmCloseOpen, setConfirmCloseOpen] = useState(false);
- const [creditsDialogOpen, setCreditsDialogOpen] = useState(false);
- const fileInputRef = useRef(null);
-
- const [renameDialogOpen, setRenameDialogOpen] = useState(false);
- const [renameTabId, setRenameTabId] = useState(null);
- const [renameValue, setRenameValue] = useState("");
-
- const [tabMenuAnchor, setTabMenuAnchor] = useState(null);
- const [tabMenuTabId, setTabMenuTabId] = useState(null);
-
- // Update nodes/edges in a particular tab
- const handleSetNodes = useCallback(
- (callbackOrArray, tId) => {
- const targetId = tId || activeTabId;
- setTabs((old) =>
- old.map((tab) => {
- if (tab.id !== targetId) return tab;
- const newNodes =
- typeof callbackOrArray === "function"
- ? callbackOrArray(tab.nodes)
- : callbackOrArray;
- return { ...tab, nodes: newNodes };
- })
- );
- },
- [activeTabId]
- );
-
- const handleSetEdges = useCallback(
- (callbackOrArray, tId) => {
- const targetId = tId || activeTabId;
- setTabs((old) =>
- old.map((tab) => {
- if (tab.id !== targetId) return tab;
- const newEdges =
- typeof callbackOrArray === "function"
- ? callbackOrArray(tab.edges)
- : callbackOrArray;
- return { ...tab, edges: newEdges };
- })
- );
- },
- [activeTabId]
- );
-
- // About menu
- const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
- const handleAboutMenuClose = () => setAboutAnchorEl(null);
-
- // Close all flows
- const handleOpenCloseAllDialog = () => setConfirmCloseOpen(true);
- const handleCloseDialog = () => setConfirmCloseOpen(false);
- const handleConfirmCloseAll = () => {
- setTabs([
- {
- id: "flow_1",
- tab_name: "Flow 1",
- nodes: [],
- edges: []
- }
- ]);
- setActiveTabId("flow_1");
- setConfirmCloseOpen(false);
- };
-
- // Create new tab
- const createNewTab = () => {
- const nextIndex = tabs.length + 1;
- const newId = "flow_" + nextIndex;
- setTabs((old) => [
- ...old,
- {
- id: newId,
- tab_name: "Flow " + nextIndex,
- nodes: [],
- edges: []
- }
- ]);
- setActiveTabId(newId);
- };
-
- // Right-click tab menu
- const handleTabRightClick = (evt, tabId) => {
- evt.preventDefault();
- setTabMenuAnchor({ x: evt.clientX, y: evt.clientY });
- setTabMenuTabId(tabId);
- };
- const handleCloseTabMenu = () => {
- setTabMenuAnchor(null);
- setTabMenuTabId(null);
- };
-
- // Rename / close tab
- const handleRenameTab = () => {
- setRenameDialogOpen(true);
- setRenameTabId(tabMenuTabId);
- const t = tabs.find((x) => x.id === tabMenuTabId);
- setRenameValue(t ? t.tab_name : "");
- handleCloseTabMenu();
- };
- const handleCloseTab = () => {
- setTabs((old) => {
- const idx = old.findIndex((t) => t.id === tabMenuTabId);
- if (idx === -1) return old;
-
- const newList = [...old];
- newList.splice(idx, 1);
-
- if (tabMenuTabId === activeTabId && newList.length > 0) {
- setActiveTabId(newList[0].id);
- } else if (newList.length === 0) {
- newList.push({
- id: "flow_1",
- tab_name: "Flow 1",
- nodes: [],
- edges: []
- });
- setActiveTabId("flow_1");
- }
- return newList;
- });
- handleCloseTabMenu();
+ return newList;
+ });
+ handleCloseTabMenu();
};
+
const handleRenameDialogSave = () => {
- if (!renameTabId) {
- setRenameDialogOpen(false);
- return;
- }
- setTabs((old) =>
- old.map((tab) =>
- tab.id === renameTabId
- ? { ...tab, tab_name: renameValue }
- : tab
- )
- );
+ if (!renameTabId) {
setRenameDialogOpen(false);
+ return;
+ }
+ setTabs((old) =>
+ old.map((tab) =>
+ tab.id === renameTabId
+ ? { ...tab, tab_name: renameValue }
+ : tab
+ )
+ );
+ setRenameDialogOpen(false);
};
-
+
// Export current tab
const handleExportFlow = async () => {
- const activeTab = tabs.find((x) => x.id === activeTabId);
- if (!activeTab) return;
-
- const data = JSON.stringify(
- {
- nodes: activeTab.nodes,
- edges: activeTab.edges,
- tab_name: activeTab.tab_name
- },
- null,
- 2
- );
- const blob = new Blob([data], { type: "application/json" });
-
- if (window.showSaveFilePicker) {
- try {
- const fileHandle = await window.showSaveFilePicker({
- suggestedName: "workflow.json",
- types: [
- {
- description: "Workflow JSON File",
- accept: { "application/json": [".json"] }
- }
- ]
- });
- const writable = await fileHandle.createWritable();
- await writable.write(blob);
- await writable.close();
- } catch (err) {
- console.error("Save cancelled or failed:", err);
- }
- } else {
- const a = document.createElement("a");
- a.href = URL.createObjectURL(blob);
- a.download = "workflow.json";
- a.style.display = "none";
- document.body.appendChild(a);
- a.click();
- URL.revokeObjectURL(a.href);
- document.body.removeChild(a);
+ const activeTab = tabs.find((x) => x.id === activeTabId);
+ if (!activeTab) return;
+
+ // Build JSON data from the active tab
+ const data = JSON.stringify(
+ {
+ nodes: activeTab.nodes,
+ edges: activeTab.edges,
+ tab_name: activeTab.tab_name
+ },
+ null,
+ 2
+ );
+ const blob = new Blob([data], { type: "application/json" });
+
+ // Suggested filename based on the tab name
+ // e.g. "Nicole Work Flow" => "nicole_work_flow_workflow.json"
+ const sanitizedTabName = activeTab.tab_name
+ .replace(/\s+/g, "_")
+ .toLowerCase();
+ const suggestedFilename = sanitizedTabName + "_workflow.json";
+
+ // Check if showSaveFilePicker is available (Chrome/Edge)
+ if (window.showSaveFilePicker) {
+ try {
+ const fileHandle = await window.showSaveFilePicker({
+ suggestedName: suggestedFilename,
+ types: [
+ {
+ description: "Workflow JSON File",
+ accept: { "application/json": [".json"] }
+ }
+ ]
+ });
+
+ const writable = await fileHandle.createWritable();
+ await writable.write(blob);
+ await writable.close();
+ } catch (err) {
+ console.error("Save cancelled or failed:", err);
}
+ } else {
+ // Fallback for browsers like Firefox
+ // (Relies on browser settings to ask user where to save)
+ const a = document.createElement("a");
+ a.href = URL.createObjectURL(blob);
+ a.download = suggestedFilename; // e.g. nicole_work_flow_workflow.json
+ a.style.display = "none";
+ document.body.appendChild(a);
+ a.click();
+ // Cleanup
+ URL.revokeObjectURL(a.href);
+ document.body.removeChild(a);
+ }
};
-
+
// Import flow -> new tab
const handleImportFlow = async () => {
- if (window.showOpenFilePicker) {
- try {
- const [fileHandle] = await window.showOpenFilePicker({
- types: [
- {
- description: "Workflow JSON File",
- accept: { "application/json": [".json"] }
- }
- ]
- });
- const file = await fileHandle.getFile();
- const text = await file.text();
- const json = JSON.parse(text);
-
- const newId = "flow_" + (tabs.length + 1);
- setTabs((prev) => [
- ...prev,
- {
- id: newId,
- tab_name:
- json.tab_name ||
- "Imported Flow " + (tabs.length + 1),
- nodes: json.nodes || [],
- edges: json.edges || []
- }
- ]);
- setActiveTabId(newId);
- } catch (err) {
- console.error("Import cancelled or failed:", err);
+ if (window.showOpenFilePicker) {
+ try {
+ const [fileHandle] = await window.showOpenFilePicker({
+ types: [
+ {
+ description: "Workflow JSON File",
+ accept: { "application/json": [".json"] }
+ }
+ ]
+ });
+ const file = await fileHandle.getFile();
+ const text = await file.text();
+ const json = JSON.parse(text);
+
+ const newId = "flow_" + (tabs.length + 1);
+ setTabs((prev) => [
+ ...prev,
+ {
+ id: newId,
+ tab_name: json.tab_name || "Imported Flow " + (tabs.length + 1),
+ nodes: json.nodes || [],
+ edges: json.edges || []
}
- } else {
- fileInputRef.current?.click();
+ ]);
+ setActiveTabId(newId);
+ } catch (err) {
+ console.error("Import cancelled or failed:", err);
}
+ } else {
+ // Fallback for older browsers
+ fileInputRef.current?.click();
+ }
};
-
+
// Fallback import
const handleFileInputChange = async (e) => {
- const file = e.target.files[0];
- if (!file) return;
- try {
- const text = await file.text();
- const json = JSON.parse(text);
-
- const newId = "flow_" + (tabs.length + 1);
- setTabs((prev) => [
- ...prev,
- {
- id: newId,
- tab_name:
- json.tab_name ||
- "Imported Flow " + (tabs.length + 1),
- nodes: json.nodes || [],
- edges: json.edges || []
- }
- ]);
- setActiveTabId(newId);
- } catch (err) {
- console.error("Failed to read file:", err);
- }
+ const file = e.target.files[0];
+ if (!file) return;
+ try {
+ const text = await file.text();
+ const json = JSON.parse(text);
+
+ const newId = "flow_" + (tabs.length + 1);
+ setTabs((prev) => [
+ ...prev,
+ {
+ id: newId,
+ tab_name: json.tab_name || "Imported Flow " + (tabs.length + 1),
+ nodes: json.nodes || [],
+ edges: json.edges || []
+ }
+ ]);
+ setActiveTabId(newId);
+ } catch (err) {
+ console.error("Failed to read file:", err);
+ }
};
-
- /**
- * Tab onChange logic:
- * If user clicks the plus βtabβ, newValue = β__addtab__β.
- * Otherwise, newValue is an index: setActiveTab accordingly.
- */
- const handleTabChange = (event, newValue) => {
- if (newValue === "__addtab__") {
- // Create the new tab
- createNewTab();
- } else {
- // Normal tab index
- setActiveTabId(tabs[newValue].id);
- }
- };
-
+
return (
-
-
-
-
+
+
+
+
+
+ {/* Logo */}
+
-
-
- {/* Logo */}
-
-
-
-
-
-
- }
- startIcon={}
- sx={{ height: "36px" }}
- >
- About
-
-
-
-
-
-
-
-
+
+
+ {/* Additional Title/Info if desired */}
+
+
+ }
+ startIcon={}
+ sx={{ height: "36px" }}
+ >
+ About
+
+
+
-
- {/* Bottom status bar */}
-
- Nodes: 0
-
- Update Rate (ms):
-
-
-
-
-
- {/* Close All Dialog */}
-
-
- {/* Credits */}
-
-
- {/* Tab Context Menu */}
-
-
- {/* Rename Tab Dialog */}
-
-
- {/* Hidden file input fallback */}
-
+ Gitea Project
+
+
+
+
+
+
+
+ {/* Sidebar */}
+
-
+
+ {/* Right content: tab bar + flow editors */}
+
+ {/* Tab bar */}
+
+
+ {/* The flow editors themselves */}
+
+ {tabs.map((tab) => (
+
+
+ handleSetNodes(val, tab.id)}
+ setEdges={(val) => handleSetEdges(val, tab.id)}
+ nodeTypes={nodeTypes}
+ categorizedNodes={categorizedNodes}
+ />
+
+
+ ))}
+
+
+
+
+ {/* Bottom status bar */}
+
+ Nodes: 0
+
+ Update Rate (ms):
+
+
+
+
+
+ {/* Close All Dialog */}
+
+
+ {/* Credits */}
+
+
+ {/* Tab Context Menu */}
+
+
+ {/* Rename Tab Dialog */}
+
+
);
-}
+ }
+
\ No newline at end of file
diff --git a/Data/WebUI/src/Flow_Editor.jsx b/Data/WebUI/src/Flow_Editor.jsx
new file mode 100644
index 0000000..7bc1436
--- /dev/null
+++ b/Data/WebUI/src/Flow_Editor.jsx
@@ -0,0 +1,214 @@
+// Flow_Editor.jsx
+
+import React, { useState, useEffect, useCallback, useRef } from "react";
+import ReactFlow, {
+ Background,
+ addEdge,
+ applyNodeChanges,
+ applyEdgeChanges,
+ useReactFlow
+} from "reactflow";
+import { Menu, MenuItem } from "@mui/material";
+import {
+ Polyline as PolylineIcon,
+ DeleteForever as DeleteForeverIcon
+} from "@mui/icons-material";
+
+import "reactflow/dist/style.css";
+import "./Borealis.css";
+
+/**
+ * Single flow editor component.
+ *
+ * Props:
+ * - nodes
+ * - edges
+ * - setNodes
+ * - setEdges
+ * - nodeTypes
+ * - categorizedNodes (used to find node meta info on drop)
+ */
+export default function FlowEditor({
+ nodes,
+ edges,
+ setNodes,
+ setEdges,
+ nodeTypes,
+ categorizedNodes
+}) {
+ const wrapperRef = useRef(null);
+ const { project } = useReactFlow();
+ const [contextMenu, setContextMenu] = useState(null);
+
+ const onDrop = useCallback(
+ (event) => {
+ event.preventDefault();
+ const type = event.dataTransfer.getData("application/reactflow");
+ if (!type) return;
+
+ const bounds = wrapperRef.current.getBoundingClientRect();
+ const position = project({
+ x: event.clientX - bounds.left,
+ y: event.clientY - bounds.top
+ });
+
+ const id = "node-" + Date.now();
+
+ // Find node definition in the categorizedNodes
+ const nodeMeta = Object.values(categorizedNodes)
+ .flat()
+ .find((n) => n.type === type);
+
+ const newNode = {
+ id: id,
+ type: type,
+ position: position,
+ data: {
+ label: nodeMeta?.label || type,
+ content: nodeMeta?.content
+ }
+ };
+
+ setNodes((nds) => [...nds, newNode]);
+ },
+ [project, setNodes, categorizedNodes]
+ );
+
+ const onDragOver = useCallback((event) => {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ }, []);
+
+ const onConnect = useCallback(
+ (params) =>
+ setEdges((eds) =>
+ addEdge(
+ {
+ ...params,
+ type: "smoothstep",
+ animated: true,
+ style: {
+ strokeDasharray: "6 3",
+ stroke: "#58a6ff"
+ }
+ },
+ eds
+ )
+ ),
+ [setEdges]
+ );
+
+ const onNodesChange = useCallback(
+ (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
+ [setNodes]
+ );
+
+ const onEdgesChange = useCallback(
+ (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
+ [setEdges]
+ );
+
+ const handleRightClick = (e, node) => {
+ e.preventDefault();
+ setContextMenu({
+ mouseX: e.clientX + 2,
+ mouseY: e.clientY - 6,
+ nodeId: node.id
+ });
+ };
+
+ const handleDisconnect = () => {
+ if (contextMenu?.nodeId) {
+ setEdges((eds) =>
+ eds.filter(
+ (e) =>
+ e.source !== contextMenu.nodeId &&
+ e.target !== contextMenu.nodeId
+ )
+ );
+ }
+ setContextMenu(null);
+ };
+
+ const handleRemoveNode = () => {
+ if (contextMenu?.nodeId) {
+ setNodes((nds) => nds.filter((n) => n.id !== contextMenu.nodeId));
+ setEdges((eds) =>
+ eds.filter(
+ (e) =>
+ e.source !== contextMenu.nodeId &&
+ e.target !== contextMenu.nodeId
+ )
+ );
+ }
+ setContextMenu(null);
+ };
+
+ useEffect(() => {
+ const nodeCountEl = document.getElementById("nodeCount");
+ if (nodeCountEl) {
+ nodeCountEl.innerText = nodes.length;
+ }
+ }, [nodes]);
+
+ return (
+
+
+
+
+
+ {/* Right-click node menu */}
+
+
+ );
+}
diff --git a/Data/WebUI/src/Flow_Tabs.jsx b/Data/WebUI/src/Flow_Tabs.jsx
new file mode 100644
index 0000000..e7539e2
--- /dev/null
+++ b/Data/WebUI/src/Flow_Tabs.jsx
@@ -0,0 +1,98 @@
+// Flow_Tabs.jsx
+
+import React from "react";
+import { Box, Tabs, Tab } from "@mui/material";
+import { Add as AddIcon } from "@mui/icons-material";
+
+/**
+ * Renders the tab bar (including the "add tab" button).
+ *
+ * Props:
+ * - tabs (array of {id, tab_name, nodes, edges})
+ * - activeTabId (string)
+ * - onTabChange(newActiveTabId: string)
+ * - onAddTab()
+ * - onTabRightClick(evt: MouseEvent, tabId: string)
+ */
+export default function FlowTabs({
+ tabs,
+ activeTabId,
+ onTabChange,
+ onAddTab,
+ onTabRightClick
+}) {
+ // Determine the currently active tab index
+ const activeIndex = (() => {
+ const idx = tabs.findIndex((t) => t.id === activeTabId);
+ return idx >= 0 ? idx : 0;
+ })();
+
+ // Handle tab clicks
+ const handleChange = (event, newValue) => {
+ if (newValue === "__addtab__") {
+ // The "plus" tab
+ onAddTab();
+ } else {
+ // normal tab index
+ const newTab = tabs[newValue];
+ if (newTab) {
+ onTabChange(newTab.id);
+ }
+ }
+ };
+
+ return (
+
+
+ {tabs.map((tab, index) => (
+ onTabRightClick(evt, tab.id)}
+ sx={{
+ minHeight: "36px",
+ height: "36px",
+ textTransform: "none",
+ backgroundColor: tab.id === activeTabId ? "#2C2C2C" : "transparent",
+ color: "#58a6ff"
+ }}
+ />
+ ))}
+ {/* The "plus" tab has a special value */}
+ }
+ value="__addtab__"
+ sx={{
+ minHeight: "36px",
+ height: "36px",
+ color: "#58a6ff",
+ textTransform: "none"
+ }}
+ />
+
+
+ );
+}
diff --git a/Data/WebUI/src/Node_Sidebar.jsx b/Data/WebUI/src/Node_Sidebar.jsx
new file mode 100644
index 0000000..c063dc6
--- /dev/null
+++ b/Data/WebUI/src/Node_Sidebar.jsx
@@ -0,0 +1,265 @@
+// Node_Sidebar.jsx
+
+import React from "react";
+import {
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ Button,
+ Divider,
+ Tooltip,
+ Typography
+} from "@mui/material";
+import {
+ ExpandMore as ExpandMoreIcon,
+ Save as SaveIcon,
+ FileOpen as FileOpenIcon,
+ DeleteForever as DeleteForeverIcon,
+ DragIndicator as DragIndicatorIcon,
+ Polyline as PolylineIcon
+} from "@mui/icons-material";
+
+/**
+ * Left sidebar for managing workflows and node categories.
+ *
+ * Props:
+ * - categorizedNodes (object of arrays, e.g. { "Category": [{...}, ...], ... })
+ * - handleExportFlow() => void
+ * - handleImportFlow() => void
+ * - handleOpenCloseAllDialog() => void
+ * - fileInputRef (ref to hidden file input)
+ * - onFileInputChange(event) => void
+ */
+export default function NodeSidebar({
+ categorizedNodes,
+ handleExportFlow,
+ handleImportFlow,
+ handleOpenCloseAllDialog,
+ fileInputRef,
+ onFileInputChange
+}) {
+ return (
+
+
+ }
+ sx={{
+ backgroundColor: "#2c2c2c",
+ minHeight: "36px",
+ "& .MuiAccordionSummary-content": {
+ margin: 0
+ }
+ }}
+ >
+
+ Workflows
+
+
+
+ }
+ sx={{
+ color: "#ccc",
+ backgroundColor: "#232323",
+ justifyContent: "flex-start",
+ pl: 2,
+ fontSize: "0.9rem",
+ textTransform: "none",
+ "&:hover": {
+ backgroundColor: "#2a2a2a"
+ }
+ }}
+ onClick={handleExportFlow}
+ >
+ Export Current Flow
+
+ }
+ sx={{
+ color: "#ccc",
+ backgroundColor: "#232323",
+ justifyContent: "flex-start",
+ pl: 2,
+ fontSize: "0.9rem",
+ textTransform: "none",
+ "&:hover": {
+ backgroundColor: "#2a2a2a"
+ }
+ }}
+ onClick={handleImportFlow}
+ >
+ Import Flow
+
+ }
+ sx={{
+ color: "#ccc",
+ backgroundColor: "#232323",
+ justifyContent: "flex-start",
+ pl: 2,
+ fontSize: "0.9rem",
+ textTransform: "none",
+ "&:hover": {
+ backgroundColor: "#2a2a2a"
+ }
+ }}
+ onClick={handleOpenCloseAllDialog}
+ >
+ Close All Flows
+
+
+
+
+
+ }
+ sx={{
+ backgroundColor: "#2c2c2c",
+ minHeight: "36px",
+ "& .MuiAccordionSummary-content": {
+ margin: 0
+ }
+ }}
+ >
+
+ Nodes
+
+
+
+ {Object.entries(categorizedNodes).map(([category, items]) => (
+
+
+
+ {category}
+
+
+ {items.map((nodeDef) => (
+
+ {nodeDef.description || "Drag & Drop into Editor"}
+
+ }
+ placement="right"
+ arrow
+ >
+
+
+ ))}
+
+ ))}
+
+
+
+ {/* Hidden file input fallback for older browsers */}
+
+
+ );
+}