Configuration Panel Milestone 5

This commit is contained in:
Nicole Rappe 2025-05-15 01:52:10 -06:00
parent 9aff34e8de
commit e35495c7e3
3 changed files with 280 additions and 191 deletions

View File

@ -1,5 +1,5 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Node_Configuration_Sidebar.jsx ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Node_Configuration_Sidebar.jsx
import { Box, Typography, Tabs, Tab, TextField } from "@mui/material"; import { Box, Typography, Tabs, Tab, TextField, MenuItem } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import { useReactFlow } from "reactflow"; import { useReactFlow } from "reactflow";
import ReactMarkdown from "react-markdown"; // Used for Node Usage Documentation import ReactMarkdown from "react-markdown"; // Used for Node Usage Documentation
@ -13,39 +13,117 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
const config = nodeData?.config || []; const config = nodeData?.config || [];
const nodeId = nodeData?.nodeId; const nodeId = nodeData?.nodeId;
return config.map((field, index) => ( return config.map((field, index) => {
<Box key={index} sx={{ mb: 2 }}> const value = nodeData?.[field.key] || "";
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
{field.label || field.key} return (
</Typography> <Box key={index} sx={{ mb: 2 }}>
<TextField <Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
variant="outlined" {field.label || field.key}
size="small" </Typography>
fullWidth
value={nodeData?.[field.key] || ""} {field.type === "select" ? (
onChange={(e) => { <TextField
const newValue = e.target.value; select
if (!nodeId) return; fullWidth
setNodes((nds) => size="small"
nds.map((n) => value={value}
n.id === nodeId onChange={(e) => {
? { ...n, data: { ...n.data, [field.key]: newValue } } const newValue = e.target.value;
: n if (!nodeId) return;
) setNodes((nds) =>
); nds.map((n) =>
window.BorealisValueBus[nodeId] = newValue; n.id === nodeId
}} ? { ...n, data: { ...n.data, [field.key]: newValue } }
InputProps={{ : n
sx: { )
backgroundColor: "#1e1e1e", );
color: "#ccc", window.BorealisValueBus[nodeId] = newValue;
"& fieldset": { borderColor: "#444" }, }}
"&:hover fieldset": { borderColor: "#666" } SelectProps={{
} MenuProps: {
}} PaperProps: {
/> sx: {
</Box> bgcolor: "#1e1e1e",
)); color: "#ccc",
border: "1px solid #58a6ff",
"& .MuiMenuItem-root": {
color: "#ccc",
fontSize: "0.85rem",
"&:hover": {
backgroundColor: "#2a2a2a"
},
"&.Mui-selected": {
backgroundColor: "#2c2c2c !important",
color: "#58a6ff"
},
"&.Mui-selected:hover": {
backgroundColor: "#2a2a2a !important"
}
}
}
}
}
}}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#1e1e1e",
color: "#ccc",
fontSize: "0.85rem",
"& fieldset": {
borderColor: "#444"
},
"&:hover fieldset": {
borderColor: "#58a6ff"
},
"&.Mui-focused fieldset": {
borderColor: "#58a6ff"
}
},
"& .MuiSelect-select": {
backgroundColor: "#1e1e1e"
}
}}
>
{(field.options || []).map((opt, idx) => (
<MenuItem key={idx} value={opt}>
{opt}
</MenuItem>
))}
</TextField>
) : (
<TextField
variant="outlined"
size="small"
fullWidth
value={value}
onChange={(e) => {
const newValue = e.target.value;
if (!nodeId) return;
setNodes((nds) =>
nds.map((n) =>
n.id === nodeId
? { ...n, data: { ...n.data, [field.key]: newValue } }
: n
)
);
window.BorealisValueBus[nodeId] = newValue;
}}
InputProps={{
sx: {
backgroundColor: "#1e1e1e",
color: "#ccc",
"& fieldset": { borderColor: "#444" },
"&:hover fieldset": { borderColor: "#666" },
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
}
}}
/>
)}
</Box>
);
});
}; };
return ( return (
@ -95,7 +173,7 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
onChange={handleTabChange} onChange={handleTabChange}
variant="fullWidth" variant="fullWidth"
textColor="inherit" textColor="inherit"
TabIndicatorProps={{ style: { backgroundColor: "#58a6ff" } }} TabIndicatorProps={{ style: { backgroundColor: "#ccc" } }}
sx={{ sx={{
borderTop: "1px solid #333", borderTop: "1px solid #333",
borderBottom: "1px solid #333", borderBottom: "1px solid #333",
@ -103,8 +181,27 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
height: "36px" height: "36px"
}} }}
> >
<Tab label="Config" sx={{ color: "#ccc", minHeight: "36px", height: "36px", textTransform: "none" }} /> <Tab
<Tab label="Usage Docs" sx={{ color: "#ccc", minHeight: "36px", height: "36px", textTransform: "none" }} /> label="Config"
sx={{
color: "#ccc",
"&.Mui-selected": { color: "#ccc" },
minHeight: "36px",
height: "36px",
textTransform: "none"
}}
/>
<Tab
label="Usage Docs"
sx={{
color: "#ccc",
"&.Mui-selected": { color: "#ccc" },
minHeight: "36px",
height: "36px",
textTransform: "none"
}}
/>
</Tabs> </Tabs>
</Box> </Box>

View File

@ -1,5 +1,5 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow"; import { Handle, Position, useReactFlow, useStore } from "reactflow";
import { IconButton } from "@mui/material"; import { IconButton } from "@mui/material";
import { Settings as SettingsIcon } from "@mui/icons-material"; import { Settings as SettingsIcon } from "@mui/icons-material";
@ -10,10 +10,12 @@ if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
const DataNode = ({ id, data }) => { const DataNode = ({ id, data }) => {
const { setNodes } = useReactFlow(); const { setNodes } = useReactFlow();
const edges = useStore((state) => state.edges); const edges = useStore((state) => state.edges);
const valueRef = useRef(data?.value || ""); const [renderValue, setRenderValue] = useState(data?.value || "");
const valueRef = useRef(renderValue);
useEffect(() => { useEffect(() => {
valueRef.current = data?.value || ""; valueRef.current = data?.value || "";
setRenderValue(valueRef.current);
window.BorealisValueBus[id] = valueRef.current; window.BorealisValueBus[id] = valueRef.current;
}, [data?.value, id]); }, [data?.value, id]);
@ -29,6 +31,7 @@ const DataNode = ({ id, data }) => {
const upstreamValue = window.BorealisValueBus[inputEdge.source] ?? ""; const upstreamValue = window.BorealisValueBus[inputEdge.source] ?? "";
if (upstreamValue !== valueRef.current) { if (upstreamValue !== valueRef.current) {
valueRef.current = upstreamValue; valueRef.current = upstreamValue;
setRenderValue(upstreamValue);
window.BorealisValueBus[id] = upstreamValue; window.BorealisValueBus[id] = upstreamValue;
setNodes((nds) => setNodes((nds) =>
nds.map((n) => nds.map((n) =>
@ -60,19 +63,22 @@ const DataNode = ({ id, data }) => {
return ( return (
<div className="borealis-node"> <div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" /> <Handle type="target" position={Position.Left} className="borealis-handle" />
<div className="borealis-node-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div className="borealis-node-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span>{data?.label || "Data Node"}</span> <span>{data?.label || "Data Node"}</span>
<IconButton <IconButton
size="small" size="small"
onClick={() => onClick={() => window.BorealisOpenDrawer && window.BorealisOpenDrawer(id, { ...data, nodeId: id })}
window.BorealisOpenDrawer && sx={{ color: "#888", padding: 0, marginLeft: "auto" }}
window.BorealisOpenDrawer(id, { ...data, nodeId: id })
}
sx={{ padding: 0, marginRight: "-3px", color: "#58a6ff", width: "20px", height: "20px" }}
> >
<SettingsIcon sx={{ fontSize: "16px" }} /> <SettingsIcon sx={{ fontSize: 16 }} />
</IconButton> </IconButton>
</div> </div>
<div className="borealis-node-content" style={{ fontSize: "9px", color: "#ccc", marginTop: 4 }}>
Value: {renderValue}
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" /> <Handle type="source" position={Position.Right} className="borealis-handle" />
</div> </div>
); );

View File

@ -1,175 +1,161 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Logical_Operators.jsx ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Logical_Operators.jsx
/**
* ==============================================
* Borealis - Comparison Node (Logic Evaluation)
* ==============================================
*
* COMPONENT ROLE:
* This node takes two input values and evaluates them using a selected comparison operator.
* It returns 1 (true) or 0 (false) depending on the result of the comparison.
*
* FEATURES:
* - Dropdown to select input type: "Number" or "String"
* - Dropdown to select comparison operator: ==, !=, >, <, >=, <=
* - Dynamically disables numeric-only operators for string inputs
* - Automatically resets operator to == when switching to String
* - Supports summing multiple inputs per side (A, B)
* - For "String" mode: concatenates inputs in connection order
* - Uses BorealisValueBus for input/output
* - Controlled by global update timer
*
* STRUCTURE:
* - Label and Description
* - Input A (top-left) and Input B (middle-left)
* - Output (right edge) result: 1 (true) or 0 (false)
* - Operator dropdown and Input Type dropdown
*/
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow"; import { Handle, Position, useReactFlow, useStore } from "reactflow";
import { IconButton } from "@mui/material";
import SettingsIcon from "@mui/icons-material/Settings";
if (!window.BorealisValueBus) window.BorealisValueBus = {}; if (!window.BorealisValueBus) window.BorealisValueBus = {};
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100; if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
const ComparisonNode = ({ id, data }) => { const ComparisonNode = ({ id, data }) => {
const { setNodes } = useReactFlow(); const { setNodes } = useReactFlow();
const edges = useStore(state => state.edges); const edges = useStore(state => state.edges);
const [renderValue, setRenderValue] = useState("0");
const valueRef = useRef("0");
const [inputType, setInputType] = useState(data?.inputType || "Number"); useEffect(() => {
const [operator, setOperator] = useState(data?.operator || "Equal (==)"); let currentRate = window.BorealisUpdateRate;
const [renderValue, setRenderValue] = useState("0"); let intervalId = null;
const valueRef = useRef("0");
useEffect(() => { const runNodeLogic = () => {
if (inputType === "String" && !["Equal (==)", "Not Equal (!=)"].includes(operator)) { let inputType = data?.inputType || "Number";
setOperator("Equal (==)"); let operator = data?.operator || "Equal (==)";
if (inputType === "String" && !["Equal (==)", "Not Equal (!=)"].includes(operator)) {
operator = "Equal (==)";
setNodes(nds =>
nds.map(n =>
n.id === id ? { ...n, data: { ...n.data, operator } } : n
)
);
}
const edgeInputsA = edges.filter(e => e?.target === id && e.targetHandle === "a");
const edgeInputsB = edges.filter(e => e?.target === id && e.targetHandle === "b");
const extractValues = (edgeList) => {
const values = edgeList.map(e => window.BorealisValueBus[e.source]).filter(v => v !== undefined);
if (inputType === "Number") {
return values.reduce((sum, v) => sum + (parseFloat(v) || 0), 0);
} }
}, [inputType]); return values.join("");
};
useEffect(() => { const a = extractValues(edgeInputsA);
let currentRate = window.BorealisUpdateRate; const b = extractValues(edgeInputsB);
let intervalId = null;
const runNodeLogic = () => { const resultMap = {
const edgeInputsA = edges.filter(e => e?.target === id && e.targetHandle === "a"); "Equal (==)": a === b,
const edgeInputsB = edges.filter(e => e?.target === id && e.targetHandle === "b"); "Not Equal (!=)": a !== b,
"Greater Than (>)": a > b,
"Less Than (<)": a < b,
"Greater Than or Equal (>=)": a >= b,
"Less Than or Equal (<=)": a <= b
};
const extractValues = (edgeList) => { const result = resultMap[operator] ? "1" : "0";
const values = edgeList.map(e => window.BorealisValueBus[e.source]).filter(v => v !== undefined);
if (inputType === "Number") {
return values.reduce((sum, v) => sum + (parseFloat(v) || 0), 0);
}
return values.join("");
};
const a = extractValues(edgeInputsA); valueRef.current = result;
const b = extractValues(edgeInputsB); setRenderValue(result);
window.BorealisValueBus[id] = result;
const resultMap = { setNodes(nds =>
"Equal (==)": a === b, nds.map(n =>
"Not Equal (!=)": a !== b, n.id === id ? { ...n, data: { ...n.data, value: result } } : n
"Greater Than (>)": a > b, )
"Less Than (<)": a < b, );
"Greater Than or Equal (>=)": a >= b, };
"Less Than or Equal (<=)": a <= b
};
const result = resultMap[operator] ? "1" : "0"; intervalId = setInterval(runNodeLogic, currentRate);
valueRef.current = result;
setRenderValue(result);
window.BorealisValueBus[id] = result;
setNodes(nds =>
nds.map(n =>
n.id === id ? { ...n, data: { ...n.data, value: result, inputType, operator } } : n
)
);
};
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate;
if (newRate !== currentRate) {
clearInterval(intervalId);
currentRate = newRate;
intervalId = setInterval(runNodeLogic, currentRate); intervalId = setInterval(runNodeLogic, currentRate);
}
}, 250);
const monitor = setInterval(() => { return () => {
const newRate = window.BorealisUpdateRate; clearInterval(intervalId);
if (newRate !== currentRate) { clearInterval(monitor);
clearInterval(intervalId); };
currentRate = newRate; }, [id, edges, data?.inputType, data?.operator, setNodes]);
intervalId = setInterval(runNodeLogic, currentRate);
}
}, 250);
return () => { return (
clearInterval(intervalId); <div className="borealis-node">
clearInterval(monitor); <div style={{ position: "absolute", left: -16, top: 12, fontSize: "8px", color: "#ccc" }}>A</div>
}; <div style={{ position: "absolute", left: -16, top: 50, fontSize: "8px", color: "#ccc" }}>B</div>
}, [id, edges, inputType, operator, setNodes]); <Handle type="target" position={Position.Left} id="a" style={{ top: 12 }} className="borealis-handle" />
<Handle type="target" position={Position.Left} id="b" style={{ top: 50 }} className="borealis-handle" />
return ( <div className="borealis-node-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div className="borealis-node"> <span>{data?.label || "Logic Comparison"}</span>
<div style={{ position: "absolute", left: -16, top: 12, fontSize: "8px", color: "#ccc" }}>A</div> <IconButton
<div style={{ position: "absolute", left: -16, top: 50, fontSize: "8px", color: "#ccc" }}>B</div> size="small"
<Handle type="target" position={Position.Left} id="a" style={{ top: 12 }} className="borealis-handle" /> onClick={() => window.BorealisOpenDrawer(id, { ...data, nodeId: id })}
<Handle type="target" position={Position.Left} id="b" style={{ top: 50 }} className="borealis-handle" /> sx={{ color: "#888", padding: 0, marginLeft: "auto" }}
>
<SettingsIcon sx={{ fontSize: 16 }} />
</IconButton>
</div>
<div className="borealis-node-header"> <div className="borealis-node-content" style={{ fontSize: "9px", color: "#ccc", marginTop: 4 }}>
{data?.label || "Comparison Node"} Result: {renderValue}
</div> </div>
<div className="borealis-node-content"> <Handle type="source" position={Position.Right} className="borealis-handle" />
<div style={{ marginBottom: "6px", fontSize: "9px", color: "#ccc" }}> </div>
{data?.content || "Evaluates A vs B and outputs 1 (true) or 0 (false)."} );
</div>
<label style={{ fontSize: "9px" }}>Input Type:</label>
<select value={inputType} onChange={(e) => setInputType(e.target.value)} style={dropdownStyle}>
<option value="Number">Number</option>
<option value="String">String</option>
</select>
<label style={{ fontSize: "9px", marginTop: "6px" }}>Operator:</label>
<select value={operator} onChange={(e) => setOperator(e.target.value)} style={dropdownStyle}>
<option>Equal (==)</option>
<option>Not Equal (!=)</option>
<option disabled={inputType === "String"}>Greater Than (&gt;)</option>
<option disabled={inputType === "String"}>Less Than (&lt;)</option>
<option disabled={inputType === "String"}>Greater Than or Equal (&gt;=)</option>
<option disabled={inputType === "String"}>Less Than or Equal (&lt;=)</option>
</select>
<div style={{ marginTop: "8px", fontSize: "9px" }}>Result: {renderValue}</div>
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
const dropdownStyle = {
width: "100%",
fontSize: "9px",
padding: "4px",
background: "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
marginBottom: "4px"
}; };
export default { export default {
type: "ComparisonNode", type: "ComparisonNode",
label: "Logic Comparison", label: "Logic Comparison",
description: ` description: "Compare A vs B using logic operators",
Compare Two Inputs (A vs B) content: "Compare A and B using Logic",
component: ComparisonNode,
config: [
{
key: "inputType",
label: "Input Type",
type: "select",
options: ["Number", "String"]
},
{
key: "operator",
label: "Operator",
type: "select",
options: [
"Equal (==)",
"Not Equal (!=)",
"Greater Than (>)",
"Less Than (<)",
"Greater Than or Equal (>=)",
"Less Than or Equal (<=)"
]
}
],
usage_documentation: `
### Logic Comparison Node
- Uses configurable operator This node compares two inputs (A and B) using the selected operator.
- Supports numeric and string comparison
- Aggregates multiple inputs by summing (Number) or joining (String in connection order) **Modes:**
- Only == and != are valid for String mode - **Number**: Sums all connected inputs and compares.
- Automatically resets operator when switching to String mode - **String**: Concatenates all inputs for comparison.
- Outputs 1 (true) or 0 (false) into BorealisValueBus - Only **Equal (==)** and **Not Equal (!=)** are valid for strings.
- Live-updates based on global timer
`.trim(), **Output:**
content: "Compare A and B using Logic", - Returns \`1\` if comparison is true.
component: ComparisonNode - Returns \`0\` if comparison is false.
**Input Notes:**
- A and B can each have multiple inputs.
- Input order matters for strings (concatenation).
- Input handles:
- **A** = Top left
- **B** = Middle left
`.trim()
}; };