Added Edge Styling & Labeling

This commit is contained in:
Nicole Rappe 2025-04-29 05:05:31 -06:00
parent 5f4125b197
commit e5e5b26a4c
4 changed files with 400 additions and 96 deletions

3
.gitignore vendored
View File

@ -16,4 +16,5 @@ Borealis-Server.exe
/Agent/
# Misc Files/Folders
.vs/s
.vs/s
AI_Model_Custom_Instructions.md

View File

@ -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",

View File

@ -1,4 +1,4 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Flow_Editor.jsx
// //////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/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 }}
>
<Background
variant="lines"
gap={65}
size={1}
color="rgba(255, 255, 255, 0.2)"
/>
<Background variant="lines" gap={65} size={1} color="rgba(255,255,255,0.2)" />
</ReactFlow>
{/* Right-click node menu */}
<Menu
open={Boolean(contextMenu)}
onClose={() => 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" } }}
>
<MenuItem onClick={handleDisconnect}>
<PolylineIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} />
<MenuItem
onClick={() => {
if (contextMenu?.nodeId) {
setEdges((eds) =>
eds.filter(
(e) =>
e.source !== contextMenu.nodeId &&
e.target !== contextMenu.nodeId
)
);
}
setContextMenu(null);
}}
>
<PolylineIcon
sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }}
/>
Disconnect All Edges
</MenuItem>
<MenuItem onClick={handleRemoveNode}>
<DeleteForeverIcon sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }} />
<MenuItem
onClick={() => {
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);
}}
>
<DeleteForeverIcon
sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }}
/>
Remove Node
</MenuItem>
</Menu>
<Menu
open={Boolean(edgeContextMenu)}
onClose={() => setEdgeContextMenu(null)}
anchorReference="anchorPosition"
anchorPosition={
edgeContextMenu
? { top: edgeContextMenu.mouseY, left: edgeContextMenu.mouseX }
: undefined
}
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
>
<MenuItem
onClick={() =>
setEdges((eds) =>
eds.filter((e) => e.id !== selectedEdgeId)
)
}
>
<DeleteForeverIcon
sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }}
/>
Unlink Edge
</MenuItem>
<MenuItem>
Edge Styles
<MenuList>
<MenuItem onClick={() => changeEdgeType("step")}>Step</MenuItem>
<MenuItem onClick={() => changeEdgeType("curved")}>Curved</MenuItem>
<MenuItem onClick={() => changeEdgeType("straight")}>Straight</MenuItem>
</MenuList>
</MenuItem>
<MenuItem>
Animations
<MenuList>
<MenuItem onClick={() => changeEdgeAnimation("dashes")}>Dashes</MenuItem>
<MenuItem onClick={() => changeEdgeAnimation("dots")}>Dots</MenuItem>
<MenuItem onClick={() => changeEdgeAnimation("none")}>Solid Line</MenuItem>
</MenuList>
</MenuItem>
<MenuItem>
Label
<MenuList>
<MenuItem onClick={handleAddLabel}>Add</MenuItem>
<MenuItem onClick={handleRemoveLabel}>Remove</MenuItem>
<MenuItem onClick={handleEditLabel}>
<EditIcon sx={{ fontSize: 16, mr: 1 }} />
Edit
</MenuItem>
<MenuItem onClick={() => handlePickColor("labelText")}>Text Color</MenuItem>
<MenuItem onClick={() => handlePickColor("labelBg")}>Background Color</MenuItem>
<MenuItem>
Padding:
<input
type="text"
defaultValue={`${labelPadding[0]},${labelPadding[1]}`}
style={{ width: 80, marginLeft: 8 }}
onBlur={(e) => {
const parts = e.target.value.split(",").map((v) => parseInt(v.trim()));
if (parts.length === 2 && parts.every(Number.isFinite)) {
setLabelPadding(parts);
}
}}
/>
</MenuItem>
<MenuItem>
Radius:
<input
type="number"
min="0"
max="20"
defaultValue={labelBorderRadius}
style={{ width: 60, marginLeft: 8 }}
onBlur={(e) => {
const val = parseInt(e.target.value);
if (!isNaN(val)) setLabelBorderRadius(val);
}}
/>
</MenuItem>
<MenuItem>
Opacity:
<Box display="flex" alignItems="center" ml={1}>
<Slider
value={labelOpacity}
onChange={(_, v) => setLabelOpacity(v)}
step={0.05}
min={0}
max={1}
style={{ width: 100 }}
/>
<input
type="number"
step="0.05"
min="0"
max="1"
value={labelOpacity}
style={{ width: 60, marginLeft: 8 }}
onChange={(e) => {
const v = parseFloat(e.target.value);
if (!isNaN(v)) setLabelOpacity(v);
}}
/>
</Box>
</MenuItem>
<MenuItem onClick={applyLabelStyleExtras}>
Apply Label Style Changes
</MenuItem>
</MenuList>
</MenuItem>
<MenuItem onClick={() => handlePickColor("stroke")}>Color</MenuItem>
</Menu>
{showColorPicker && (
<div
style={{
position: "absolute",
top: pickerPos.y,
left: pickerPos.x,
zIndex: 9999,
background: "#1e1e1e",
padding: "10px",
borderRadius: "8px"
}}
>
<SketchPicker color={tempColor.hex} onChange={(c) => setTempColor(c)} />
<div style={{ marginTop: "10px", textAlign: "center" }}>
<button
onClick={() => {
handleColorChange(tempColor);
setShowColorPicker(false);
}}
style={{
backgroundColor: "#58a6ff",
color: "#121212",
border: "none",
padding: "6px 12px",
borderRadius: "4px",
cursor: "pointer",
fontWeight: "bold"
}}
>
Set Color
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -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