diff --git a/.gitignore b/.gitignore index f808f7b..68f3365 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ Borealis-Server.exe /Agent/ # Misc Files/Folders -.vs/s \ No newline at end of file +.vs/s +AI_Model_Custom_Instructions.md \ No newline at end of file diff --git a/Data/Server/WebUI/package.json b/Data/Server/WebUI/package.json index 4b83d42..8ace92a 100644 --- a/Data/Server/WebUI/package.json +++ b/Data/Server/WebUI/package.json @@ -12,6 +12,7 @@ "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "react-resizable": "3.0.5", + "react-color": "2.19.3", "reactflow": "11.11.4", "socket.io-client": "4.8.1", "react-simple-keyboard": "3.8.62", diff --git a/Data/Server/WebUI/src/Flow_Editor.jsx b/Data/Server/WebUI/src/Flow_Editor.jsx index 584f2bf..629c6b7 100644 --- a/Data/Server/WebUI/src/Flow_Editor.jsx +++ b/Data/Server/WebUI/src/Flow_Editor.jsx @@ -1,4 +1,4 @@ -////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Flow_Editor.jsx +// //////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Flow_Editor.jsx import React, { useState, useEffect, useCallback, useRef } from "react"; import ReactFlow, { @@ -8,25 +8,20 @@ import ReactFlow, { applyEdgeChanges, useReactFlow } from "reactflow"; -import { Menu, MenuItem } from "@mui/material"; +import { Menu, MenuItem, MenuList, Slider, Box } from "@mui/material"; import { Polyline as PolylineIcon, - DeleteForever as DeleteForeverIcon + DeleteForever as DeleteForeverIcon, + DoubleArrow as DoubleArrowIcon, + LinearScale as LinearScaleIcon, + Timeline as TimelineIcon, + FormatColorFill as FormatColorFillIcon, + ArrowRight as ArrowRightIcon, + Edit as EditIcon } from "@mui/icons-material"; - +import { SketchPicker } from "react-color"; import "reactflow/dist/style.css"; -/** - * Single flow editor component. - * - * Props: - * - nodes - * - edges - * - setNodes - * - setEdges - * - nodeTypes - * - categorizedNodes (used to find node meta info on drop) - */ export default function FlowEditor({ nodes, edges, @@ -38,36 +33,38 @@ export default function FlowEditor({ const wrapperRef = useRef(null); const { project } = useReactFlow(); const [contextMenu, setContextMenu] = useState(null); + const [edgeContextMenu, setEdgeContextMenu] = useState(null); + const [selectedEdgeId, setSelectedEdgeId] = useState(null); + const [showColorPicker, setShowColorPicker] = useState(false); + const [colorPickerMode, setColorPickerMode] = useState(null); + const [labelPadding, setLabelPadding] = useState([8, 4]); + const [labelBorderRadius, setLabelBorderRadius] = useState(4); + const [labelOpacity, setLabelOpacity] = useState(0.8); + const [tempColor, setTempColor] = useState({ hex: "#58a6ff" }); + const [pickerPos, setPickerPos] = useState({ x: 0, y: 0 }); + + const edgeStyles = { + step: "step", + curved: "bezier", + straight: "straight" + }; + + const animationStyles = { + dashes: { animated: true, style: { strokeDasharray: "6 3" } }, + dots: { animated: true, style: { strokeDasharray: "2 4" } }, + none: { animated: false, style: {} } + }; const onDrop = useCallback( (event) => { event.preventDefault(); const type = event.dataTransfer.getData("application/reactflow"); if (!type) return; - const bounds = wrapperRef.current.getBoundingClientRect(); - const position = project({ - x: event.clientX - bounds.left, - y: event.clientY - bounds.top - }); - + const position = project({ x: event.clientX - bounds.left, y: event.clientY - bounds.top }); const id = "node-" + Date.now(); - - // Find node definition in the categorizedNodes - const nodeMeta = Object.values(categorizedNodes) - .flat() - .find((n) => n.type === type); - - const newNode = { - id: id, - type: type, - position: position, - data: { - label: nodeMeta?.label || type, - content: nodeMeta?.content - } - }; - + const nodeMeta = Object.values(categorizedNodes).flat().find((n) => n.type === type); + const newNode = { id, type, position, data: { label: nodeMeta?.label || type, content: nodeMeta?.content } }; setNodes((nds) => [...nds, newNode]); }, [project, setNodes, categorizedNodes] @@ -79,21 +76,19 @@ export default function FlowEditor({ }, []); const onConnect = useCallback( - (params) => + (params) => { setEdges((eds) => addEdge( { ...params, - type: "smoothstep", + type: "bezier", animated: true, - style: { - strokeDasharray: "6 3", - stroke: "#58a6ff" - } + style: { strokeDasharray: "6 3", stroke: "#58a6ff" } }, eds ) - ), + ); + }, [setEdges] ); @@ -101,7 +96,6 @@ export default function FlowEditor({ (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [setNodes] ); - const onEdgesChange = useCallback( (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [setEdges] @@ -109,45 +103,119 @@ export default function FlowEditor({ const handleRightClick = (e, node) => { e.preventDefault(); - setContextMenu({ - mouseX: e.clientX + 2, - mouseY: e.clientY - 6, - nodeId: node.id - }); + setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6, nodeId: node.id }); }; - const handleDisconnect = () => { - if (contextMenu?.nodeId) { + const handleEdgeRightClick = (e, edge) => { + e.preventDefault(); + setEdgeContextMenu({ edgeId: edge.id, mouseX: e.clientX + 2, mouseY: e.clientY - 6 }); + setSelectedEdgeId(edge.id); + }; + + const changeEdgeType = (newType) => { + setEdges((eds) => + eds.map((e) => + e.id === selectedEdgeId ? { ...e, type: edgeStyles[newType] } : e + ) + ); + setEdgeContextMenu(null); + }; + + const changeEdgeAnimation = (newAnim) => { + setEdges((eds) => + eds.map((e) => { + if (e.id !== selectedEdgeId) return e; + const strokeColor = e.style?.stroke || "#58a6ff"; + const anim = animationStyles[newAnim] || {}; + return { + ...e, + animated: anim.animated, + style: { ...anim.style, stroke: strokeColor }, + markerEnd: e.markerEnd ? { ...e.markerEnd, color: strokeColor } : undefined + }; + }) + ); + setEdgeContextMenu(null); + }; + + const handleColorChange = (color) => { + setEdges((eds) => + eds.map((e) => { + if (e.id !== selectedEdgeId) return e; + const updated = { ...e }; + if (colorPickerMode === "stroke") { + updated.style = { ...e.style, stroke: color.hex }; + if (e.markerEnd) updated.markerEnd = { ...e.markerEnd, color: color.hex }; + } else if (colorPickerMode === "labelText") { + updated.labelStyle = { ...e.labelStyle, fill: color.hex }; + } else if (colorPickerMode === "labelBg") { + updated.labelBgStyle = { ...e.labelBgStyle, fill: color.hex, fillOpacity: labelOpacity }; + } + return updated; + }) + ); + }; + + const handleAddLabel = () => { + setEdges((eds) => + eds.map((e) => + e.id === selectedEdgeId ? { ...e, label: "New Label" } : e + ) + ); + setEdgeContextMenu(null); + }; + + const handleEditLabel = () => { + const newText = prompt("Enter label text:"); + if (newText !== null) { setEdges((eds) => - eds.filter( - (e) => - e.source !== contextMenu.nodeId && - e.target !== contextMenu.nodeId + eds.map((e) => + e.id === selectedEdgeId ? { ...e, label: newText } : e ) ); } - setContextMenu(null); + setEdgeContextMenu(null); }; - const handleRemoveNode = () => { - if (contextMenu?.nodeId) { - setNodes((nds) => nds.filter((n) => n.id !== contextMenu.nodeId)); - setEdges((eds) => - eds.filter( - (e) => - e.source !== contextMenu.nodeId && - e.target !== contextMenu.nodeId - ) - ); - } - setContextMenu(null); + const handleRemoveLabel = () => { + setEdges((eds) => + eds.map((e) => + e.id === selectedEdgeId ? { ...e, label: undefined } : e + ) + ); + setEdgeContextMenu(null); + }; + + const handlePickColor = (mode) => { + setColorPickerMode(mode); + setTempColor({ hex: "#58a6ff" }); + setPickerPos({ x: edgeContextMenu?.mouseX || 0, y: edgeContextMenu?.mouseY || 0 }); + setShowColorPicker(true); + }; + + const applyLabelStyleExtras = () => { + setEdges((eds) => + eds.map((e) => + e.id === selectedEdgeId + ? { + ...e, + labelBgPadding: labelPadding, + labelBgStyle: { + ...e.labelBgStyle, + fillOpacity: labelOpacity, + rx: labelBorderRadius, + ry: labelBorderRadius + } + } + : e + ) + ); + setEdgeContextMenu(null); }; useEffect(() => { const nodeCountEl = document.getElementById("nodeCount"); - if (nodeCountEl) { - nodeCountEl.innerText = nodes.length; - } + if (nodeCountEl) nodeCountEl.innerText = nodes.length; }, [nodes]); return ( @@ -162,26 +230,18 @@ export default function FlowEditor({ onDrop={onDrop} onDragOver={onDragOver} onNodeContextMenu={handleRightClick} + onEdgeContextMenu={handleEdgeRightClick} defaultViewport={{ x: 0, y: 0, zoom: 1.5 }} edgeOptions={{ - type: "smoothstep", + type: "bezier", animated: true, - style: { - strokeDasharray: "6 3", - stroke: "#58a6ff" - } + style: { strokeDasharray: "6 3", stroke: "#58a6ff" } }} proOptions={{ hideAttribution: true }} > - + - {/* Right-click node menu */} setContextMenu(null)} @@ -191,23 +251,196 @@ export default function FlowEditor({ ? { top: contextMenu.mouseY, left: contextMenu.mouseX } : undefined } - PaperProps={{ - sx: { - bgcolor: "#1e1e1e", - color: "#fff", - fontSize: "13px" - } - }} + PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }} > - - + { + if (contextMenu?.nodeId) { + setEdges((eds) => + eds.filter( + (e) => + e.source !== contextMenu.nodeId && + e.target !== contextMenu.nodeId + ) + ); + } + setContextMenu(null); + }} + > + Disconnect All Edges - - + { + if (contextMenu?.nodeId) { + setNodes((nds) => + nds.filter((n) => n.id !== contextMenu.nodeId) + ); + setEdges((eds) => + eds.filter( + (e) => + e.source !== contextMenu.nodeId && + e.target !== contextMenu.nodeId + ) + ); + } + setContextMenu(null); + }} + > + Remove Node + + setEdgeContextMenu(null)} + anchorReference="anchorPosition" + anchorPosition={ + edgeContextMenu + ? { top: edgeContextMenu.mouseY, left: edgeContextMenu.mouseX } + : undefined + } + PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }} + > + + setEdges((eds) => + eds.filter((e) => e.id !== selectedEdgeId) + ) + } + > + + Unlink Edge + + + Edge Styles + + changeEdgeType("step")}>Step + changeEdgeType("curved")}>Curved + changeEdgeType("straight")}>Straight + + + + Animations + + changeEdgeAnimation("dashes")}>Dashes + changeEdgeAnimation("dots")}>Dots + changeEdgeAnimation("none")}>Solid Line + + + + Label + + Add + Remove + + + Edit + + handlePickColor("labelText")}>Text Color + handlePickColor("labelBg")}>Background Color + + Padding: + { + const parts = e.target.value.split(",").map((v) => parseInt(v.trim())); + if (parts.length === 2 && parts.every(Number.isFinite)) { + setLabelPadding(parts); + } + }} + /> + + + Radius: + { + const val = parseInt(e.target.value); + if (!isNaN(val)) setLabelBorderRadius(val); + }} + /> + + + Opacity: + + setLabelOpacity(v)} + step={0.05} + min={0} + max={1} + style={{ width: 100 }} + /> + { + const v = parseFloat(e.target.value); + if (!isNaN(v)) setLabelOpacity(v); + }} + /> + + + + Apply Label Style Changes + + + + handlePickColor("stroke")}>Color + + + {showColorPicker && ( +
+ setTempColor(c)} /> +
+ +
+
+ )} ); } diff --git a/project_directory_tree.txt b/project_directory_tree.txt new file mode 100644 index 0000000..0cb0590 --- /dev/null +++ b/project_directory_tree.txt @@ -0,0 +1,69 @@ +Folder Structure: +├── Dependencies +├── Launch-Borealis.ps1 +├── Launch-Borealis.sh +└── Data + ├── Agent + │ ├── agent-requirements.txt + │ ├── Borealis.ico + │ ├── borealis-agent.py + │ └── Package_Borealis-Agent.ps1 + └── Server + ├── Borealis.ico + ├── Package-Borealis-Server.ps1 + ├── server.py + ├── server-requirements.txt + ├── Python_API_Endpoints + │ ├── ocr_engines.py + │ └── Tesseract-OCR + ├── Sounds + │ └── Short_Beep.wav + ├── WebUI + │ ├── package.json + │ ├── public + │ │ ├── Borealis_Logo.png + │ │ ├── Borealis_Logo_Full.png + │ │ ├── favicon.ico + │ │ └── index.html + │ └── src + │ ├── App.jsx + │ ├── Borealis.css + │ ├── Dialogs.jsx + │ ├── Flow_Editor.jsx + │ ├── Flow_Tabs.jsx + │ ├── index.js + │ ├── Node_Sidebar.jsx + │ ├── Status_Bar.jsx + │ └── nodes + │ ├── Agents + │ │ ├── Node_Agent.jsx + │ │ └── Node_Agent_Role_Screenshot.jsx + │ ├── Alerting + │ │ └── Node_Alert_Sound.jsx + │ ├── Data Analysis + │ │ └── Node_OCR_Text_Extraction.jsx + │ ├── Data Manipulation + │ │ └── Node_Array_Index_Extractor.jsx + │ ├── Flyff Universe + │ ├── General Purpose + │ │ ├── Node_Data.jsx + │ │ ├── Node_Logical_Operators.jsx + │ │ └── Node_Math_Operation.jsx + │ ├── Image Processing + │ │ ├── Node_Adjust_Contrast.jsx + │ │ ├── Node_BW_Threshold.jsx + │ │ ├── Node_Convert_to_Grayscale.jsx + │ │ ├── Node_Export_Image.jsx + │ │ ├── Node_Image_Viewer.jsx + │ │ └── Node_Upload_Image.jsx + │ ├── Macro Automation + │ │ └── Node_Macro_KeyPress.jsx + │ ├── Organization + │ │ └── Node_Backdrop_Group_Box.jsx + │ └── Reporting + │ └── Node_Export_to_CSV.jsx + └── Workflows + ├── Examples + │ └── Logic-Comparison-Example.json + └── Flyff Universe + └── Flyff_OCR_Workflow.json \ No newline at end of file