////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/App.jsx // Core React Imports import React, { useState, useEffect, useCallback, useRef } from "react"; // Material UI - Components import { AppBar, Toolbar, Typography, Box, Menu, MenuItem, Button, CssBaseline, ThemeProvider, createTheme } from "@mui/material"; // Material UI - Icons import { KeyboardArrowDown as KeyboardArrowDownIcon, InfoOutlined as InfoOutlinedIcon, MergeType as MergeTypeIcon, People as PeopleIcon } from "@mui/icons-material"; // React Flow import { ReactFlowProvider } from "reactflow"; // Styles import "reactflow/dist/style.css"; // Import Borealis Modules import FlowTabs from "./Flow_Tabs"; import FlowEditor from "./Flow_Editor"; import NodeSidebar from "./Node_Sidebar"; import { CloseAllDialog, CreditsDialog, RenameTabDialog, TabContextMenu } from "./Dialogs"; import StatusBar from "./Status_Bar"; // Websocket Functionality import { io } from "socket.io-client"; if (!window.BorealisSocket) { window.BorealisSocket = io(window.location.origin, { transports: ["websocket"] }); } if (!window.BorealisUpdateRate) { window.BorealisUpdateRate = 200; } const modules = import.meta.glob('./nodes/**/*.jsx', { eager: true }); const nodeTypes = {}; const categorizedNodes = {}; Object.entries(modules).forEach(([path, mod]) => { const comp = mod.default; if (!comp) return; const { type, component } = comp; if (!type || !component) return; const parts = path.replace('./nodes/', '').split('/'); const category = parts[0]; if (!categorizedNodes[category]) { categorizedNodes[category] = []; } categorizedNodes[category].push(comp); nodeTypes[type] = component; }); const darkTheme = createTheme({ palette: { mode: "dark", background: { default: "#121212", paper: "#1e1e1e" }, text: { primary: "#ffffff" } }, components: { MuiTooltip: { styleOverrides: { tooltip: { backgroundColor: "#2a2a2a", color: "#ccc", fontSize: "0.75rem", border: "1px solid #444" }, arrow: { color: "#2a2a2a" } } } } }); const LOCAL_STORAGE_KEY = "borealis_persistent_state"; export default function App() { const [tabs, setTabs] = useState([ { id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] } ]); const [activeTabId, setActiveTabId] = useState("flow_1"); const [aboutAnchorEl, setAboutAnchorEl] = useState(null); const [creditsDialogOpen, setCreditsDialogOpen] = useState(false); const [confirmCloseOpen, setConfirmCloseOpen] = useState(false); const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [renameTabId, setRenameTabId] = useState(null); const [renameValue, setRenameValue] = useState(""); const [tabMenuAnchor, setTabMenuAnchor] = useState(null); const [tabMenuTabId, setTabMenuTabId] = useState(null); const fileInputRef = useRef(null); useEffect(() => { const saved = localStorage.getItem(LOCAL_STORAGE_KEY); if (saved) { try { const parsed = JSON.parse(saved); if (Array.isArray(parsed.tabs) && parsed.activeTabId) { setTabs(parsed.tabs); setActiveTabId(parsed.activeTabId); } } catch (err) { console.warn("Failed to parse saved state:", err); } } }, []); useEffect(() => { const timeout = setTimeout(() => { const data = JSON.stringify({ tabs, activeTabId }); localStorage.setItem(LOCAL_STORAGE_KEY, data); }, 1000); return () => clearTimeout(timeout); }, [tabs, activeTabId]); 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] ); const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget); const handleAboutMenuClose = () => setAboutAnchorEl(null); const openCreditsDialog = () => { handleAboutMenuClose(); setCreditsDialogOpen(true); }; 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); }; const createNewTab = () => { const nextIndex = tabs.length + 1; const newId = "flow_" + nextIndex; setTabs((old) => [ ...old, { id: newId, tab_name: "Flow " + nextIndex, nodes: [], edges: [] } ]); setActiveTabId(newId); }; const handleTabChange = (newActiveTabId) => { setActiveTabId(newActiveTabId); }; const handleTabRightClick = (evt, tabId) => { evt.preventDefault(); setTabMenuAnchor({ x: evt.clientX, y: evt.clientY }); setTabMenuTabId(tabId); }; const handleCloseTabMenu = () => { setTabMenuAnchor(null); setTabMenuTabId(null); }; 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(); }; const handleRenameDialogSave = () => { if (!renameTabId) { setRenameDialogOpen(false); return; } setTabs((old) => old.map((tab) => tab.id === renameTabId ? { ...tab, tab_name: renameValue } : tab ) ); setRenameDialogOpen(false); }; 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" }); const sanitizedTabName = activeTab.tab_name.replace(/\s+/g, "_").toLowerCase(); const suggestedFilename = sanitizedTabName + "_workflow.json"; 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 { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = suggestedFilename; a.style.display = "none"; document.body.appendChild(a); a.click(); URL.revokeObjectURL(a.href); document.body.removeChild(a); } }; 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); } } else { fileInputRef.current?.click(); } }; 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); } }; return ( { handleAboutMenuClose(); window.open("https://git.bunny-lab.io/bunny-lab/Borealis", "_blank"); }}> Gitea Project Credits {tabs.map((tab) => ( handleSetNodes(val, tab.id)} setEdges={(val) => handleSetEdges(val, tab.id)} nodeTypes={nodeTypes} categorizedNodes={categorizedNodes} /> ))} setCreditsDialogOpen(false)} /> setRenameDialogOpen(false)} onSave={handleRenameDialogSave} /> ); }