import React, { useState, useEffect, useCallback, useRef } from "react"; import { AppBar, Toolbar, Typography, Box, Menu, MenuItem, Button, CssBaseline, ThemeProvider, createTheme, Accordion, AccordionSummary, AccordionDetails, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions } from "@mui/material"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ReactFlow, { Background, addEdge, applyNodeChanges, applyEdgeChanges, ReactFlowProvider, useReactFlow, Handle, Position } from "reactflow"; import "reactflow/dist/style.css"; import "./Borealis.css"; // ✅ Custom styled node with handles const CustomNode = ({ data }) => { return (
{data.label || "Custom Node"}
{data.content || "Placeholder"}
); }; function FlowEditor({ nodes, edges, setNodes, setEdges }) { const reactFlowWrapper = useRef(null); const { project } = useReactFlow(); const onDrop = useCallback( (event) => { event.preventDefault(); const type = event.dataTransfer.getData("application/reactflow"); if (!type) return; const bounds = reactFlowWrapper.current.getBoundingClientRect(); const position = project({ x: event.clientX - bounds.left, y: event.clientY - bounds.top }); const id = `node-${Date.now()}`; const newNode = { id, type: "custom", position, data: { label: "Custom Node", content: "Placeholder" } }; setNodes((nds) => [...nds, newNode]); }, [project, setNodes] ); const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, []); const onConnect = useCallback( (params) => setEdges((eds) => addEdge( { ...params, type: "smoothstep", animated: true, style: { strokeDasharray: "6 3", stroke: "#58a6ff" } }, eds ) ), [setEdges] ); const onNodesChange = useCallback( (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [setNodes] ); const onEdgesChange = useCallback( (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [setEdges] ); useEffect(() => { const nodeCountEl = document.getElementById("nodeCount"); if (nodeCountEl) { nodeCountEl.innerText = nodes.length; } }, [nodes]); return (
); } const darkTheme = createTheme({ palette: { mode: "dark", background: { default: "#121212", paper: "#1e1e1e" }, text: { primary: "#ffffff" } } }); export default function App() { const [aboutAnchorEl, setAboutAnchorEl] = useState(null); const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); const [confirmCloseOpen, setConfirmCloseOpen] = useState(false); const fileInputRef = useRef(null); const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget); const handleAboutMenuClose = () => setAboutAnchorEl(null); const handleOpenCloseDialog = () => setConfirmCloseOpen(true); const handleCloseDialog = () => setConfirmCloseOpen(false); const handleConfirmCloseWorkflow = () => { setNodes([]); setEdges([]); setConfirmCloseOpen(false); }; const handleSaveWorkflow = async () => { const data = JSON.stringify({ nodes, edges }, null, 2); const blob = new Blob([data], { type: "application/json" }); if (window.showSaveFilePicker) { try { const fileHandle = await window.showSaveFilePicker({ suggestedName: "workflow.json", types: [{ description: "Workflow JSON File", accept: { "application/json": [".json"] } }] }); const writable = await fileHandle.createWritable(); await writable.write(blob); await writable.close(); } catch (error) { console.error("Save cancelled or failed:", error); } } else { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "workflow.json"; a.style.display = "none"; document.body.appendChild(a); a.click(); URL.revokeObjectURL(a.href); document.body.removeChild(a); } }; const handleOpenWorkflow = 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 { nodes: loadedNodes, edges: loadedEdges } = JSON.parse(text); const confirm = window.confirm("Opening a workflow will overwrite your current one. Continue?"); if (!confirm) return; setNodes(loadedNodes); setEdges(loadedEdges); } catch (error) { console.error("Open cancelled or failed:", error); } } else { fileInputRef.current?.click(); } }; return ( Borealis - Workflow Automation Tool Gitea Project Credits } sx={accordionHeaderStyle}> Workflows } sx={accordionHeaderStyle}> Nodes Nodes: 0 | Update Rate: 500ms Close Workflow? Are you sure you want to reset the workflow? All nodes will be removed. { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const { nodes: loadedNodes, edges: loadedEdges } = JSON.parse(text); const confirm = window.confirm("Opening a workflow will overwrite your current one. Continue?"); if (!confirm) return; setNodes(loadedNodes); setEdges(loadedEdges); } catch (err) { console.error("Failed to read file:", err); } }} /> ); } const sidebarBtnStyle = { color: "#ccc", backgroundColor: "#232323", justifyContent: "flex-start", pl: 2, fontSize: "0.9rem" }; const accordionHeaderStyle = { backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } };