Added Tooltips and Misc Fixes

This commit is contained in:
2025-05-01 00:51:05 -06:00
parent cf14ee0111
commit a6f40d2502
8 changed files with 543 additions and 459 deletions

View File

@ -2,103 +2,183 @@
// Core React Imports // Core React Imports
import React, { import React, {
useState, useState,
useEffect, useEffect,
useCallback, useCallback,
useRef useRef
} from "react"; } 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"]
});
}
// Global Node Update Timer Variable // Material UI - Components
if (!window.BorealisUpdateRate) { import {
window.BorealisUpdateRate = 200; AppBar,
} Toolbar,
Typography,
// Dynamically load all node components via Vite Box,
const modules = import.meta.glob('./nodes/**/*.jsx', { eager: true }); Menu,
const nodeTypes = {}; MenuItem,
const categorizedNodes = {}; Button,
CssBaseline,
ThemeProvider,
createTheme
} from "@mui/material";
Object.entries(modules).forEach(([path, mod]) => { // Material UI - Icons
const comp = mod.default; import {
if (!comp) return; KeyboardArrowDown as KeyboardArrowDownIcon,
const { type, component } = comp; InfoOutlined as InfoOutlinedIcon,
if (!type || !component) return; MergeType as MergeTypeIcon,
People as PeopleIcon
} from "@mui/icons-material";
// derive category folder name from path: "./nodes/<Category>/File.jsx" // React Flow
const parts = path.replace('./nodes/', '').split('/'); import { ReactFlowProvider } from "reactflow";
const category = parts[0];
if (!categorizedNodes[category]) { // Styles
categorizedNodes[category] = []; import "reactflow/dist/style.css";
}
categorizedNodes[category].push(comp); // Import Borealis Modules
nodeTypes[type] = component; 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"]
}); });
}
const darkTheme = createTheme({ // Global Node Update Timer Variable
palette: { if (!window.BorealisUpdateRate) {
mode: "dark", window.BorealisUpdateRate = 200;
background: { }
default: "#121212",
paper: "#1e1e1e" // Dynamically load all node components via Vite
}, const modules = import.meta.glob('./nodes/**/*.jsx', { eager: true });
text: { const nodeTypes = {};
primary: "#ffffff" const categorizedNodes = {};
Object.entries(modules).forEach(([path, mod]) => {
const comp = mod.default;
if (!comp) return;
const { type, component } = comp;
if (!type || !component) return;
// derive category folder name from path: "./nodes/<Category>/File.jsx"
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"
}
} }
} }
}); }
});
export default function App() {
const [tabs, setTabs] = useState([
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);
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", id: "flow_1",
tab_name: "Flow 1", tab_name: "Flow 1",
@ -106,239 +186,149 @@ import React, {
edges: [] edges: []
} }
]); ]);
const [activeTabId, setActiveTabId] = useState("flow_1"); setActiveTabId("flow_1");
setConfirmCloseOpen(false);
const [aboutAnchorEl, setAboutAnchorEl] = useState(null); };
const [creditsDialogOpen, setCreditsDialogOpen] = useState(false);
const [confirmCloseOpen, setConfirmCloseOpen] = useState(false); const createNewTab = () => {
const [renameDialogOpen, setRenameDialogOpen] = useState(false); const nextIndex = tabs.length + 1;
const [renameTabId, setRenameTabId] = useState(null); const newId = "flow_" + nextIndex;
const [renameValue, setRenameValue] = useState(""); setTabs((old) => [
const [tabMenuAnchor, setTabMenuAnchor] = useState(null); ...old,
const [tabMenuTabId, setTabMenuTabId] = useState(null); {
const fileInputRef = useRef(null); id: newId,
tab_name: "Flow " + nextIndex,
const handleSetNodes = useCallback( nodes: [],
(callbackOrArray, tId) => { edges: []
const targetId = tId || activeTabId; }
setTabs((old) => ]);
old.map((tab) => { setActiveTabId(newId);
if (tab.id !== targetId) return tab; };
const newNodes =
typeof callbackOrArray === "function" const handleTabChange = (newActiveTabId) => {
? callbackOrArray(tab.nodes) setActiveTabId(newActiveTabId);
: callbackOrArray; };
return { ...tab, nodes: newNodes };
}) const handleTabRightClick = (evt, tabId) => {
); evt.preventDefault();
}, setTabMenuAnchor({ x: evt.clientX, y: evt.clientY });
[activeTabId] setTabMenuTabId(tabId);
); };
const handleSetEdges = useCallback( const handleCloseTabMenu = () => {
(callbackOrArray, tId) => { setTabMenuAnchor(null);
const targetId = tId || activeTabId; setTabMenuTabId(null);
setTabs((old) => };
old.map((tab) => {
if (tab.id !== targetId) return tab; const handleRenameTab = () => {
const newEdges = setRenameDialogOpen(true);
typeof callbackOrArray === "function" setRenameTabId(tabMenuTabId);
? callbackOrArray(tab.edges) const t = tabs.find((x) => x.id === tabMenuTabId);
: callbackOrArray; setRenameValue(t ? t.tab_name : "");
return { ...tab, edges: newEdges }; handleCloseTabMenu();
}) };
);
}, const handleCloseTab = () => {
[activeTabId] setTabs((old) => {
); const idx = old.findIndex((t) => t.id === tabMenuTabId);
if (idx === -1) return old;
const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
const handleAboutMenuClose = () => setAboutAnchorEl(null); const newList = [...old];
const openCreditsDialog = () => { newList.splice(idx, 1);
handleAboutMenuClose();
setCreditsDialogOpen(true); if (tabMenuTabId === activeTabId && newList.length > 0) {
}; setActiveTabId(newList[0].id);
} else if (newList.length === 0) {
const handleOpenCloseAllDialog = () => setConfirmCloseOpen(true); newList.push({
const handleCloseDialog = () => setConfirmCloseOpen(false);
const handleConfirmCloseAll = () => {
setTabs([
{
id: "flow_1", id: "flow_1",
tab_name: "Flow 1", tab_name: "Flow 1",
nodes: [], nodes: [],
edges: [] edges: []
} });
]); setActiveTabId("flow_1");
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) => return newList;
old.map((tab) => });
tab.id === renameTabId handleCloseTabMenu();
? { ...tab, tab_name: renameValue } };
: tab
) const handleRenameDialogSave = () => {
); if (!renameTabId) {
setRenameDialogOpen(false); setRenameDialogOpen(false);
}; return;
}
const handleExportFlow = async () => { setTabs((old) =>
const activeTab = tabs.find((x) => x.id === activeTabId); old.map((tab) =>
if (!activeTab) return; tab.id === renameTabId
? { ...tab, tab_name: renameValue }
const data = JSON.stringify( : tab
{ )
nodes: activeTab.nodes, );
edges: activeTab.edges, setRenameDialogOpen(false);
tab_name: activeTab.tab_name };
},
null, const handleExportFlow = async () => {
2 const activeTab = tabs.find((x) => x.id === activeTabId);
); if (!activeTab) return;
const blob = new Blob([data], { type: "application/json" });
const sanitizedTabName = activeTab.tab_name.replace(/\s+/g, "_").toLowerCase(); const data = JSON.stringify(
const suggestedFilename = sanitizedTabName + "_workflow.json"; {
nodes: activeTab.nodes,
if (window.showSaveFilePicker) { edges: activeTab.edges,
try { tab_name: activeTab.tab_name
const fileHandle = await window.showSaveFilePicker({ },
suggestedName: suggestedFilename, null,
types: [ 2
{ );
description: "Workflow JSON File", const blob = new Blob([data], { type: "application/json" });
accept: { "application/json": [".json"] } const sanitizedTabName = activeTab.tab_name.replace(/\s+/g, "_").toLowerCase();
} const suggestedFilename = sanitizedTabName + "_workflow.json";
]
}); if (window.showSaveFilePicker) {
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 { 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 text = await file.text();
const json = JSON.parse(text); const json = JSON.parse(text);
const newId = "flow_" + (tabs.length + 1); const newId = "flow_" + (tabs.length + 1);
setTabs((prev) => [ setTabs((prev) => [
...prev, ...prev,
@ -351,111 +341,136 @@ import React, {
]); ]);
setActiveTabId(newId); setActiveTabId(newId);
} catch (err) { } catch (err) {
console.error("Failed to read file:", err); console.error("Import cancelled or failed:", err);
} }
}; } else {
fileInputRef.current?.click();
return ( }
<ThemeProvider theme={darkTheme}> };
<CssBaseline />
const handleFileInputChange = async (e) => {
<Box sx={{ width: "100vw", height: "100vh", display: "flex", flexDirection: "column", overflow: "hidden" }}> const file = e.target.files[0];
<AppBar position="static" sx={{ bgcolor: "#16191d" }}> if (!file) return;
<Toolbar sx={{ minHeight: "36px" }}> try {
<Box component="img" src="/Borealis_Logo_Full.png" alt="Borealis Logo" sx={{ height: "52px", marginRight: "8px" }} /> const text = await file.text();
<Typography variant="h6" sx={{ flexGrow: 1, fontSize: "1rem" }}></Typography> const json = JSON.parse(text);
<Button
color="inherit" const newId = "flow_" + (tabs.length + 1);
onClick={handleAboutMenuOpen} setTabs((prev) => [
endIcon={<KeyboardArrowDownIcon />} ...prev,
startIcon={<InfoOutlinedIcon />} {
sx={{ height: "36px" }} id: newId,
> tab_name: json.tab_name || "Imported Flow " + (tabs.length + 1),
About nodes: json.nodes || [],
</Button> edges: json.edges || []
<Menu anchorEl={aboutAnchorEl} open={Boolean(aboutAnchorEl)} onClose={handleAboutMenuClose}> }
<MenuItem onClick={() => { handleAboutMenuClose(); window.open("https://git.bunny-lab.io/Borealis", "_blank"); }}> ]);
<MergeTypeIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Gitea Project setActiveTabId(newId);
</MenuItem> } catch (err) {
<MenuItem onClick={openCreditsDialog}> console.error("Failed to read file:", err);
<PeopleIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Credits }
</MenuItem> };
</Menu>
</Toolbar> return (
</AppBar> <ThemeProvider theme={darkTheme}>
<CssBaseline />
<Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
<NodeSidebar <Box sx={{ width: "100vw", height: "100vh", display: "flex", flexDirection: "column", overflow: "hidden" }}>
categorizedNodes={categorizedNodes} <AppBar position="static" sx={{ bgcolor: "#16191d" }}>
handleExportFlow={handleExportFlow} <Toolbar sx={{ minHeight: "36px" }}>
handleImportFlow={handleImportFlow} <Box component="img" src="/Borealis_Logo_Full.png" alt="Borealis Logo" sx={{ height: "52px", marginRight: "8px" }} />
handleOpenCloseAllDialog={handleOpenCloseAllDialog} <Typography variant="h6" sx={{ flexGrow: 1, fontSize: "1rem" }}></Typography>
fileInputRef={fileInputRef} <Button
onFileInputChange={handleFileInputChange} color="inherit"
onClick={handleAboutMenuOpen}
endIcon={<KeyboardArrowDownIcon />}
startIcon={<InfoOutlinedIcon />}
sx={{ height: "36px" }}
>
About
</Button>
<Menu anchorEl={aboutAnchorEl} open={Boolean(aboutAnchorEl)} onClose={handleAboutMenuClose}>
<MenuItem onClick={() => { handleAboutMenuClose(); window.open("https://git.bunny-lab.io/Borealis", "_blank"); }}>
<MergeTypeIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Gitea Project
</MenuItem>
<MenuItem onClick={openCreditsDialog}>
<PeopleIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Credits
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
<Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
<NodeSidebar
categorizedNodes={categorizedNodes}
handleExportFlow={handleExportFlow}
handleImportFlow={handleImportFlow}
handleOpenCloseAllDialog={handleOpenCloseAllDialog}
fileInputRef={fileInputRef}
onFileInputChange={handleFileInputChange}
/>
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1, overflow: "hidden" }}>
<FlowTabs
tabs={tabs}
activeTabId={activeTabId}
onTabChange={handleTabChange}
onAddTab={createNewTab}
onTabRightClick={handleTabRightClick}
/> />
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1, overflow: "hidden" }}> <Box sx={{ flexGrow: 1, position: "relative" }}>
<FlowTabs {tabs.map((tab) => (
tabs={tabs} <Box
activeTabId={activeTabId} key={tab.id}
onTabChange={handleTabChange} sx={{
onAddTab={createNewTab} position: "absolute",
onTabRightClick={handleTabRightClick} top: 0,
/> bottom: 0,
left: 0,
<Box sx={{ flexGrow: 1, position: "relative" }}> right: 0,
{tabs.map((tab) => ( display: tab.id === activeTabId ? "block" : "none"
<Box }}
key={tab.id} >
sx={{ <ReactFlowProvider id={tab.id}>
position: "absolute", <FlowEditor
top: 0, flowId={tab.id} //Used to Fix Grid Issues Across Multiple Flow Tabs
bottom: 0, nodes={tab.nodes}
left: 0, edges={tab.edges}
right: 0, setNodes={(val) => handleSetNodes(val, tab.id)}
display: tab.id === activeTabId ? "block" : "none" setEdges={(val) => handleSetEdges(val, tab.id)}
}} nodeTypes={nodeTypes}
> categorizedNodes={categorizedNodes}
<ReactFlowProvider id={tab.id}> />
<FlowEditor </ReactFlowProvider>
flowId={tab.id} //Used to Fix Grid Issues Across Multiple Flow Tabs </Box>
nodes={tab.nodes} ))}
edges={tab.edges}
setNodes={(val) => handleSetNodes(val, tab.id)}
setEdges={(val) => handleSetEdges(val, tab.id)}
nodeTypes={nodeTypes}
categorizedNodes={categorizedNodes}
/>
</ReactFlowProvider>
</Box>
))}
</Box>
</Box> </Box>
</Box> </Box>
<StatusBar />
</Box> </Box>
<CloseAllDialog <StatusBar />
open={confirmCloseOpen} </Box>
onClose={handleCloseDialog}
onConfirm={handleConfirmCloseAll} <CloseAllDialog
/> open={confirmCloseOpen}
<CreditsDialog open={creditsDialogOpen} onClose={() => setCreditsDialogOpen(false)} /> onClose={handleCloseDialog}
<RenameTabDialog onConfirm={handleConfirmCloseAll}
open={renameDialogOpen} />
value={renameValue} <CreditsDialog open={creditsDialogOpen} onClose={() => setCreditsDialogOpen(false)} />
onChange={setRenameValue} <RenameTabDialog
onCancel={() => setRenameDialogOpen(false)} open={renameDialogOpen}
onSave={handleRenameDialogSave} value={renameValue}
/> onChange={setRenameValue}
<TabContextMenu onCancel={() => setRenameDialogOpen(false)}
anchor={tabMenuAnchor} onSave={handleRenameDialogSave}
onClose={handleCloseTabMenu} />
onRename={handleRenameTab} <TabContextMenu
onCloseTab={handleCloseTab} anchor={tabMenuAnchor}
/> onClose={handleCloseTabMenu}
</ThemeProvider> onRename={handleRenameTab}
); onCloseTab={handleCloseTab}
} />
</ThemeProvider>
);
}

View File

@ -163,4 +163,38 @@ label {
display: inline-block !important; display: inline-block !important;
width: auto !important; width: auto !important;
max-width: 1000px; /* or whatever max width you like */ max-width: 1000px; /* or whatever max width you like */
} }
/* ======================================= */
/* NUMBER INPUT SPINNER OVERRIDE */
/* ======================================= */
input[type="number"] {
background-color: #2c2c2c;
color: #ccc;
border: 1px solid #444;
padding: 4px;
font-size: 12px;
}
/* Webkit browsers */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
appearance: none;
background-color: #2c2c2c;
border-left: 1px solid #444;
border-right: 1px solid #444;
height: 100%;
width: 16px;
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg width='12' height='12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4l3 3H3z' fill='%2358a6ff'/%3E%3Cpath d='M6 8l3-3H3z' fill='%2358a6ff'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
background-size: 10px 10px;
cursor: pointer;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}

View File

@ -33,10 +33,23 @@ export function CloseAllDialog({ open, onClose, onConfirm }) {
export function CreditsDialog({ open, onClose }) { export function CreditsDialog({ open, onClose }) {
return ( return (
<Dialog open={open} onClose={onClose} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}> <Dialog open={open} onClose={onClose} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
<DialogTitle>Borealis Workflow Automation Tool</DialogTitle> <DialogContent sx={{ textAlign: "center", pt: 3 }}>
<DialogContent> <img
src="/Borealis_Logo.png"
alt="Borealis Logo"
style={{ width: "120px", marginBottom: "12px" }}
/>
<DialogTitle sx={{ p: 0, mb: 1 }}>Borealis Workflow Automation Tool</DialogTitle>
<DialogContentText sx={{ color: "#ccc" }}> <DialogContentText sx={{ color: "#ccc" }}>
Designed by Nicole Rappe @ Bunny Lab Designed by Nicole Rappe @{" "}
<a
href="https://bunny-lab.io"
target="_blank"
rel="noopener noreferrer"
style={{ color: "#58a6ff", textDecoration: "none" }}
>
Bunny Lab
</a>
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>

View File

@ -1,7 +1,7 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Flow_Tabs.jsx ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Flow_Tabs.jsx
import React from "react"; import React from "react";
import { Box, Tabs, Tab } from "@mui/material"; import { Box, Tabs, Tab, Tooltip } from "@mui/material";
import { Add as AddIcon } from "@mui/icons-material"; import { Add as AddIcon } from "@mui/icons-material";
/** /**
@ -82,16 +82,18 @@ export default function FlowTabs({
/> />
))} ))}
{/* The "plus" tab has a special value */} {/* The "plus" tab has a special value */}
<Tab <Tooltip title="Create a New Concurrent Tab" arrow>
icon={<AddIcon />} <Tab
value="__addtab__" icon={<AddIcon />}
sx={{ value="__addtab__"
minHeight: "36px", sx={{
height: "36px", minHeight: "36px",
color: "#58a6ff", height: "36px",
textTransform: "none" color: "#58a6ff",
}} textTransform: "none"
/> }}
/>
</Tooltip>
</Tabs> </Tabs>
</Box> </Box>
); );

View File

@ -8,7 +8,8 @@ import {
Button, Button,
Tooltip, Tooltip,
Typography, Typography,
IconButton IconButton,
Box
} from "@mui/material"; } from "@mui/material";
import { import {
ExpandMore as ExpandMoreIcon, ExpandMore as ExpandMoreIcon,
@ -71,15 +72,21 @@ export default function NodeSidebar({
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}> <AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
<Button fullWidth startIcon={<SaveIcon />} onClick={handleExportFlow} sx={buttonStyle}> <Tooltip title="Export Current Tab to a JSON File" placement="right" arrow>
Export Current Flow <Button fullWidth startIcon={<SaveIcon />} onClick={handleExportFlow} sx={buttonStyle}>
</Button> Export Current Flow
<Button fullWidth startIcon={<FileOpenIcon />} onClick={handleImportFlow} sx={buttonStyle}> </Button>
Import Flow </Tooltip>
</Button> <Tooltip title="Import JSON File into New Flow Tab" placement="right" arrow>
<Button fullWidth startIcon={<DeleteForeverIcon />} onClick={handleOpenCloseAllDialog} sx={buttonStyle}> <Button fullWidth startIcon={<FileOpenIcon />} onClick={handleImportFlow} sx={buttonStyle}>
Close All Flows Import Flow
</Button> </Button>
</Tooltip>
<Tooltip title="Destroy all Flow Tabs Immediately" placement="right" arrow>
<Button fullWidth startIcon={<DeleteForeverIcon />} onClick={handleOpenCloseAllDialog} sx={buttonStyle}>
Close All Flows
</Button>
</Tooltip>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -176,17 +183,30 @@ export default function NodeSidebar({
</div> </div>
{/* Bottom toggle button */} {/* Bottom toggle button */}
<div style={{ padding: "6px", borderTop: "1px solid #333", display: "flex", justifyContent: "center" }}> <Tooltip title={collapsed ? "Expand Sidebar" : "Collapse Sidebar"} placement="left">
<Tooltip title={collapsed ? "Expand Sidebar" : "Collapse Sidebar"} placement="right"> <Box
<IconButton onClick={() => setCollapsed(!collapsed)}
onClick={() => setCollapsed(!collapsed)} sx={{
size="small" height: "36px",
sx={{ color: "#888" }} borderTop: "1px solid #333",
> cursor: "pointer",
{collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />} display: "flex",
</IconButton> alignItems: "center",
</Tooltip> justifyContent: "center",
</div> color: "#888",
backgroundColor: "#121212",
transition: "background-color 0.2s ease",
"&:hover": {
backgroundColor: "#1e1e1e"
},
"&:active": {
backgroundColor: "#2a2a2a"
}
}}
>
{collapsed ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</Box>
</Tooltip>
</div> </div>
); );
} }

View File

@ -211,8 +211,9 @@ const AlertSoundNode = ({ id, data }) => {
{data?.label || "Alert Sound"} {data?.label || "Alert Sound"}
<div style={{ <div style={{
position: "absolute", position: "absolute",
top: "12px", // Adjusted from 6px to 12px for better centering top: "50%",
right: "6px", right: "8px",
transform: "translateY(-50%)",
width: "10px", width: "10px",
height: "10px", height: "10px",
borderRadius: "50%", borderRadius: "50%",

View File

@ -234,9 +234,7 @@ const numberInputStyle = {
export default { export default {
type: "OCR_Text_Extraction", type: "OCR_Text_Extraction",
label: "OCR Text Extraction", label: "OCR Text Extraction",
description: ` description: `Extract text from upstream image using backend OCR engine via API. Includes rate limiting and sensitivity detection for smart processing.`,
Extract text from upstream image using backend OCR engine via API.
Includes rate limiting and sensitivity detection for smart processing.`,
content: "Extract Multi-Line Text from Upstream Image Node", content: "Extract Multi-Line Text from Upstream Image Node",
component: OCRNode component: OCRNode
}; };

View File

@ -70,9 +70,10 @@ const KeyPressNode = ({ id, data }) => {
<div <div
style={{ style={{
position: "absolute", position: "absolute",
top: "12px", top: "50%",
right: "6px", right: "8px",
width: "10px", width: "10px",
transform: "translateY(-50%)",
height: "10px", height: "10px",
borderRadius: "50%", borderRadius: "50%",
backgroundColor: "#333", backgroundColor: "#333",