From a6f40d250286d5bf84203c78761739bb2cfa1eb0 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Thu, 1 May 2025 00:51:05 -0600 Subject: [PATCH] Added Tooltips and Misc Fixes --- Data/Server/WebUI/src/App.jsx | 847 +++++++++--------- Data/Server/WebUI/src/Borealis.css | 36 +- Data/Server/WebUI/src/Dialogs.jsx | 19 +- Data/Server/WebUI/src/Flow_Tabs.jsx | 24 +- Data/Server/WebUI/src/Node_Sidebar.jsx | 62 +- .../src/nodes/Alerting/Node_Alert_Sound.jsx | 5 +- .../Node_OCR_Text_Extraction.jsx | 4 +- .../Macro Automation/Node_Macro_KeyPress.jsx | 5 +- 8 files changed, 543 insertions(+), 459 deletions(-) diff --git a/Data/Server/WebUI/src/App.jsx b/Data/Server/WebUI/src/App.jsx index 721256f..96a9347 100644 --- a/Data/Server/WebUI/src/App.jsx +++ b/Data/Server/WebUI/src/App.jsx @@ -2,103 +2,183 @@ // 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"] - }); - } + useState, + useEffect, + useCallback, + useRef +} from "react"; - // Global Node Update Timer Variable - if (!window.BorealisUpdateRate) { - window.BorealisUpdateRate = 200; - } - - // Dynamically load all node components via Vite - const modules = import.meta.glob('./nodes/**/*.jsx', { eager: true }); - const nodeTypes = {}; - const categorizedNodes = {}; +// Material UI - Components +import { + AppBar, + Toolbar, + Typography, + Box, + Menu, + MenuItem, + Button, + CssBaseline, + ThemeProvider, + createTheme +} from "@mui/material"; - Object.entries(modules).forEach(([path, mod]) => { - const comp = mod.default; - if (!comp) return; - const { type, component } = comp; - if (!type || !component) return; +// Material UI - Icons +import { + KeyboardArrowDown as KeyboardArrowDownIcon, + InfoOutlined as InfoOutlinedIcon, + MergeType as MergeTypeIcon, + People as PeopleIcon +} from "@mui/icons-material"; - // derive category folder name from path: "./nodes//File.jsx" - const parts = path.replace('./nodes/', '').split('/'); - const category = parts[0]; +// React Flow +import { ReactFlowProvider } from "reactflow"; - if (!categorizedNodes[category]) { - categorizedNodes[category] = []; - } - categorizedNodes[category].push(comp); - nodeTypes[type] = component; +// 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"] }); +} - const darkTheme = createTheme({ - palette: { - mode: "dark", - background: { - default: "#121212", - paper: "#1e1e1e" - }, - text: { - primary: "#ffffff" +// Global Node Update Timer Variable +if (!window.BorealisUpdateRate) { + window.BorealisUpdateRate = 200; +} + +// Dynamically load all node components via Vite +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; + + // derive category folder name from path: "./nodes//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", tab_name: "Flow 1", @@ -106,239 +186,149 @@ import React, { 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([ - { + 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"); - 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; + }); + setActiveTabId("flow_1"); } - setTabs((old) => - old.map((tab) => - tab.id === renameTabId - ? { ...tab, tab_name: renameValue } - : tab - ) - ); + return newList; + }); + handleCloseTabMenu(); + }; + + const handleRenameDialogSave = () => { + if (!renameTabId) { 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; + 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, @@ -351,111 +341,136 @@ import React, { ]); setActiveTabId(newId); } catch (err) { - console.error("Failed to read file:", err); + console.error("Import cancelled or failed:", err); } - }; - - return ( - - - - - - - - - - - { handleAboutMenuClose(); window.open("https://git.bunny-lab.io/Borealis", "_blank"); }}> - Gitea Project - - - Credits - - - - - - - { + 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/Borealis", "_blank"); }}> + Gitea Project + + + Credits + + + + + + + + + + - - - - - - {tabs.map((tab) => ( - - - handleSetNodes(val, tab.id)} - setEdges={(val) => handleSetEdges(val, tab.id)} - nodeTypes={nodeTypes} - categorizedNodes={categorizedNodes} - /> - - - ))} - + + + {tabs.map((tab) => ( + + + handleSetNodes(val, tab.id)} + setEdges={(val) => handleSetEdges(val, tab.id)} + nodeTypes={nodeTypes} + categorizedNodes={categorizedNodes} + /> + + + ))} - - - - - setCreditsDialogOpen(false)} /> - setRenameDialogOpen(false)} - onSave={handleRenameDialogSave} - /> - - - ); - } - \ No newline at end of file + + + + + + setCreditsDialogOpen(false)} /> + setRenameDialogOpen(false)} + onSave={handleRenameDialogSave} + /> + + + ); +} diff --git a/Data/Server/WebUI/src/Borealis.css b/Data/Server/WebUI/src/Borealis.css index b513965..a4c9688 100644 --- a/Data/Server/WebUI/src/Borealis.css +++ b/Data/Server/WebUI/src/Borealis.css @@ -163,4 +163,38 @@ label { display: inline-block !important; width: auto !important; max-width: 1000px; /* or whatever max width you like */ - } \ No newline at end of file + } + + /* ======================================= */ +/* 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; + } + \ No newline at end of file diff --git a/Data/Server/WebUI/src/Dialogs.jsx b/Data/Server/WebUI/src/Dialogs.jsx index f19ef3d..d04b6e3 100644 --- a/Data/Server/WebUI/src/Dialogs.jsx +++ b/Data/Server/WebUI/src/Dialogs.jsx @@ -33,10 +33,23 @@ export function CloseAllDialog({ open, onClose, onConfirm }) { export function CreditsDialog({ open, onClose }) { return ( - Borealis Workflow Automation Tool - + + Borealis Logo + Borealis Workflow Automation Tool - Designed by Nicole Rappe @ Bunny Lab + Designed by Nicole Rappe @{" "} + + Bunny Lab + diff --git a/Data/Server/WebUI/src/Flow_Tabs.jsx b/Data/Server/WebUI/src/Flow_Tabs.jsx index a8a21d7..5ff89cd 100644 --- a/Data/Server/WebUI/src/Flow_Tabs.jsx +++ b/Data/Server/WebUI/src/Flow_Tabs.jsx @@ -1,7 +1,7 @@ ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Flow_Tabs.jsx 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"; /** @@ -82,16 +82,18 @@ export default function FlowTabs({ /> ))} {/* The "plus" tab has a special value */} - } - value="__addtab__" - sx={{ - minHeight: "36px", - height: "36px", - color: "#58a6ff", - textTransform: "none" - }} - /> + + } + value="__addtab__" + sx={{ + minHeight: "36px", + height: "36px", + color: "#58a6ff", + textTransform: "none" + }} + /> + ); diff --git a/Data/Server/WebUI/src/Node_Sidebar.jsx b/Data/Server/WebUI/src/Node_Sidebar.jsx index ab0ae68..2a0a808 100644 --- a/Data/Server/WebUI/src/Node_Sidebar.jsx +++ b/Data/Server/WebUI/src/Node_Sidebar.jsx @@ -8,7 +8,8 @@ import { Button, Tooltip, Typography, - IconButton + IconButton, + Box } from "@mui/material"; import { ExpandMore as ExpandMoreIcon, @@ -71,15 +72,21 @@ export default function NodeSidebar({ - - - + + + + + + + + + @@ -176,17 +183,30 @@ export default function NodeSidebar({ {/* Bottom toggle button */} -
- - setCollapsed(!collapsed)} - size="small" - sx={{ color: "#888" }} - > - {collapsed ? : } - - -
+ + setCollapsed(!collapsed)} + sx={{ + height: "36px", + borderTop: "1px solid #333", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + color: "#888", + backgroundColor: "#121212", + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: "#1e1e1e" + }, + "&:active": { + backgroundColor: "#2a2a2a" + } + }} + > + {collapsed ? : } + + ); } diff --git a/Data/Server/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx b/Data/Server/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx index 035af4f..ecd1052 100644 --- a/Data/Server/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx +++ b/Data/Server/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx @@ -211,8 +211,9 @@ const AlertSoundNode = ({ id, data }) => { {data?.label || "Alert Sound"}
{