From e35495c7e3e6b9a83eec189b55b468f53954b148 Mon Sep 17 00:00:00 2001 From: Nicole Rappe <nicole.rappe@bunny-lab.io> Date: Thu, 15 May 2025 01:52:10 -0600 Subject: [PATCH] Configuration Panel Milestone 5 --- .../WebUI/src/Node_Configuration_Sidebar.jsx | 171 ++++++++--- .../src/nodes/General Purpose/Node_Data.jsx | 22 +- .../Node_Logical_Operators.jsx | 278 +++++++++--------- 3 files changed, 280 insertions(+), 191 deletions(-) diff --git a/Data/Server/WebUI/src/Node_Configuration_Sidebar.jsx b/Data/Server/WebUI/src/Node_Configuration_Sidebar.jsx index 55a48d4..db619dd 100644 --- a/Data/Server/WebUI/src/Node_Configuration_Sidebar.jsx +++ b/Data/Server/WebUI/src/Node_Configuration_Sidebar.jsx @@ -1,5 +1,5 @@ ////////// 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 { useReactFlow } from "reactflow"; 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 nodeId = nodeData?.nodeId; - return config.map((field, index) => ( - <Box key={index} sx={{ mb: 2 }}> - <Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}> - {field.label || field.key} - </Typography> - <TextField - variant="outlined" - size="small" - fullWidth - value={nodeData?.[field.key] || ""} - 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" } - } - }} - /> - </Box> - )); + return config.map((field, index) => { + const value = nodeData?.[field.key] || ""; + + return ( + <Box key={index} sx={{ mb: 2 }}> + <Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}> + {field.label || field.key} + </Typography> + + {field.type === "select" ? ( + <TextField + select + fullWidth + size="small" + 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; + }} + SelectProps={{ + MenuProps: { + PaperProps: { + sx: { + 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 ( @@ -95,7 +173,7 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti onChange={handleTabChange} variant="fullWidth" textColor="inherit" - TabIndicatorProps={{ style: { backgroundColor: "#58a6ff" } }} + TabIndicatorProps={{ style: { backgroundColor: "#ccc" } }} sx={{ borderTop: "1px solid #333", borderBottom: "1px solid #333", @@ -103,8 +181,27 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti height: "36px" }} > - <Tab label="Config" sx={{ color: "#ccc", minHeight: "36px", height: "36px", textTransform: "none" }} /> - <Tab label="Usage Docs" sx={{ color: "#ccc", minHeight: "36px", height: "36px", textTransform: "none" }} /> + <Tab + 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> </Box> diff --git a/Data/Server/WebUI/src/nodes/General Purpose/Node_Data.jsx b/Data/Server/WebUI/src/nodes/General Purpose/Node_Data.jsx index 77da697..df2426c 100644 --- a/Data/Server/WebUI/src/nodes/General Purpose/Node_Data.jsx +++ b/Data/Server/WebUI/src/nodes/General Purpose/Node_Data.jsx @@ -1,5 +1,5 @@ ////////// 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 { IconButton } from "@mui/material"; import { Settings as SettingsIcon } from "@mui/icons-material"; @@ -10,10 +10,12 @@ if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100; const DataNode = ({ id, data }) => { const { setNodes } = useReactFlow(); const edges = useStore((state) => state.edges); - const valueRef = useRef(data?.value || ""); + const [renderValue, setRenderValue] = useState(data?.value || ""); + const valueRef = useRef(renderValue); useEffect(() => { valueRef.current = data?.value || ""; + setRenderValue(valueRef.current); window.BorealisValueBus[id] = valueRef.current; }, [data?.value, id]); @@ -29,6 +31,7 @@ const DataNode = ({ id, data }) => { const upstreamValue = window.BorealisValueBus[inputEdge.source] ?? ""; if (upstreamValue !== valueRef.current) { valueRef.current = upstreamValue; + setRenderValue(upstreamValue); window.BorealisValueBus[id] = upstreamValue; setNodes((nds) => nds.map((n) => @@ -60,19 +63,22 @@ const DataNode = ({ id, data }) => { return ( <div className="borealis-node"> <Handle type="target" position={Position.Left} className="borealis-handle" /> + <div className="borealis-node-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <span>{data?.label || "Data Node"}</span> <IconButton size="small" - onClick={() => - window.BorealisOpenDrawer && - window.BorealisOpenDrawer(id, { ...data, nodeId: id }) - } - sx={{ padding: 0, marginRight: "-3px", color: "#58a6ff", width: "20px", height: "20px" }} + onClick={() => window.BorealisOpenDrawer && window.BorealisOpenDrawer(id, { ...data, nodeId: id })} + sx={{ color: "#888", padding: 0, marginLeft: "auto" }} > - <SettingsIcon sx={{ fontSize: "16px" }} /> + <SettingsIcon sx={{ fontSize: 16 }} /> </IconButton> </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" /> </div> ); diff --git a/Data/Server/WebUI/src/nodes/General Purpose/Node_Logical_Operators.jsx b/Data/Server/WebUI/src/nodes/General Purpose/Node_Logical_Operators.jsx index d36106e..3497609 100644 --- a/Data/Server/WebUI/src/nodes/General Purpose/Node_Logical_Operators.jsx +++ b/Data/Server/WebUI/src/nodes/General Purpose/Node_Logical_Operators.jsx @@ -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 - -/** - * ============================================== - * 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 { 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.BorealisUpdateRate) window.BorealisUpdateRate = 100; const ComparisonNode = ({ id, data }) => { - const { setNodes } = useReactFlow(); - const edges = useStore(state => state.edges); + const { setNodes } = useReactFlow(); + const edges = useStore(state => state.edges); + const [renderValue, setRenderValue] = useState("0"); + const valueRef = useRef("0"); - const [inputType, setInputType] = useState(data?.inputType || "Number"); - const [operator, setOperator] = useState(data?.operator || "Equal (==)"); - const [renderValue, setRenderValue] = useState("0"); - const valueRef = useRef("0"); + useEffect(() => { + let currentRate = window.BorealisUpdateRate; + let intervalId = null; - useEffect(() => { - if (inputType === "String" && !["Equal (==)", "Not Equal (!=)"].includes(operator)) { - setOperator("Equal (==)"); + const runNodeLogic = () => { + let inputType = data?.inputType || "Number"; + 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(() => { - let currentRate = window.BorealisUpdateRate; - let intervalId = null; + const a = extractValues(edgeInputsA); + const b = extractValues(edgeInputsB); - const runNodeLogic = () => { - const edgeInputsA = edges.filter(e => e?.target === id && e.targetHandle === "a"); - const edgeInputsB = edges.filter(e => e?.target === id && e.targetHandle === "b"); + const resultMap = { + "Equal (==)": a === 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 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 result = resultMap[operator] ? "1" : "0"; - const a = extractValues(edgeInputsA); - const b = extractValues(edgeInputsB); + valueRef.current = result; + setRenderValue(result); + window.BorealisValueBus[id] = result; - const resultMap = { - "Equal (==)": a === 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 - }; + setNodes(nds => + nds.map(n => + n.id === id ? { ...n, data: { ...n.data, value: result } } : n + ) + ); + }; - const result = resultMap[operator] ? "1" : "0"; - - 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 - ) - ); - }; + intervalId = setInterval(runNodeLogic, currentRate); + const monitor = setInterval(() => { + const newRate = window.BorealisUpdateRate; + if (newRate !== currentRate) { + clearInterval(intervalId); + currentRate = newRate; intervalId = setInterval(runNodeLogic, currentRate); + } + }, 250); - const monitor = setInterval(() => { - const newRate = window.BorealisUpdateRate; - if (newRate !== currentRate) { - clearInterval(intervalId); - currentRate = newRate; - intervalId = setInterval(runNodeLogic, currentRate); - } - }, 250); + return () => { + clearInterval(intervalId); + clearInterval(monitor); + }; + }, [id, edges, data?.inputType, data?.operator, setNodes]); - return () => { - clearInterval(intervalId); - clearInterval(monitor); - }; - }, [id, edges, inputType, operator, setNodes]); + return ( + <div className="borealis-node"> + <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> + <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"> - <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> - <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" /> + <div className="borealis-node-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> + <span>{data?.label || "Logic Comparison"}</span> + <IconButton + size="small" + onClick={() => window.BorealisOpenDrawer(id, { ...data, nodeId: id })} + sx={{ color: "#888", padding: 0, marginLeft: "auto" }} + > + <SettingsIcon sx={{ fontSize: 16 }} /> + </IconButton> + </div> - <div className="borealis-node-header"> - {data?.label || "Comparison Node"} - </div> + <div className="borealis-node-content" style={{ fontSize: "9px", color: "#ccc", marginTop: 4 }}> + Result: {renderValue} + </div> - <div className="borealis-node-content"> - <div style={{ marginBottom: "6px", fontSize: "9px", color: "#ccc" }}> - {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 (>)</option> - <option disabled={inputType === "String"}>Less Than (<)</option> - <option disabled={inputType === "String"}>Greater Than or Equal (>=)</option> - <option disabled={inputType === "String"}>Less Than or Equal (<=)</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" + <Handle type="source" position={Position.Right} className="borealis-handle" /> + </div> + ); }; export default { - type: "ComparisonNode", - label: "Logic Comparison", - description: ` -Compare Two Inputs (A vs B) + type: "ComparisonNode", + label: "Logic Comparison", + description: "Compare A vs B using logic operators", + 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 -- Supports numeric and string comparison -- Aggregates multiple inputs by summing (Number) or joining (String in connection order) -- Only == and != are valid for String mode -- Automatically resets operator when switching to String mode -- Outputs 1 (true) or 0 (false) into BorealisValueBus -- Live-updates based on global timer - `.trim(), - content: "Compare A and B using Logic", - component: ComparisonNode +This node compares two inputs (A and B) using the selected operator. + +**Modes:** +- **Number**: Sums all connected inputs and compares. +- **String**: Concatenates all inputs for comparison. + - Only **Equal (==)** and **Not Equal (!=)** are valid for strings. + +**Output:** +- Returns \`1\` if comparison is true. +- 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() };