mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-07-27 05:08:29 -06:00
Added Tooltips and Misc Fixes
This commit is contained in:
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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%",
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
Reference in New Issue
Block a user