Milestone 2

This commit is contained in:
Nicole Rappe 2025-05-15 00:03:21 -06:00
parent 8e1d7d0de7
commit 6f6fc96d17
3 changed files with 181 additions and 233 deletions

View File

@ -53,8 +53,7 @@ export default function FlowEditor({
return () => {
delete window.BorealisOpenDrawer;
};
}, []);
}, [nodes]);
const wrapperRef = useRef(null);
const { project } = useReactFlow();
@ -353,17 +352,25 @@ export default function FlowEditor({
if (nodeCountEl) nodeCountEl.innerText = nodes.length;
}, [nodes]);
const selectedNode = nodes.find((n) => n.data?.label === selectedNodeLabel);
const nodeTypeMeta = selectedNode
? Object.values(categorizedNodes).flat().find((def) => def.type === selectedNode.type)
: null;
return (
<div
className="flow-editor-container"
ref={wrapperRef}
style={{ position: "relative" }}
>
<NodeConfigurationSidebar
<NodeConfigurationSidebar
drawerOpen={drawerOpen}
setDrawerOpen={setDrawerOpen}
title={selectedNodeLabel}
/>
nodeData={nodeTypeMeta}
/>
<ReactFlow
nodes={nodes}

View File

@ -1,26 +1,38 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Node_Configuration_Sidebar.jsx
import { Box, Typography } from "@mui/material";
import { Box, Typography, Tabs, Tab, TextField } from "@mui/material";
import React, { useState } from "react";
export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, title }) {
return (
<>
{/* Dim overlay when drawer is open */}
{drawerOpen && (
<Box
onClick={() => setDrawerOpen(false)}
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.4)",
zIndex: 10
export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, title, nodeData }) {
const [activeTab, setActiveTab] = useState(0);
const handleTabChange = (_, newValue) => setActiveTab(newValue);
const renderConfigFields = () => {
const config = nodeData?.config || [];
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] || ""}
InputProps={{
sx: {
backgroundColor: "#1e1e1e",
color: "#ccc",
"& fieldset": { borderColor: "#444" },
"&:hover fieldset": { borderColor: "#666" }
}
}}
/>
)}
</Box>
));
};
{/* Right-Side Node Configuration Panel */}
return (
<>
<Box
onClick={() => setDrawerOpen(false)}
sx={{
@ -37,7 +49,6 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
}}
/>
{/* Animated right drawer panel */}
<Box
sx={{
position: "absolute",
@ -56,27 +67,32 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
}}
onClick={(e) => e.stopPropagation()}
>
{/* Title bar section */}
<Box
sx={{
backgroundColor: "#232323",
padding: "16px",
borderBottom: "1px solid #333"
}}
>
<Typography variant="h6" sx={{ color: "#0475c2" }}>
{title || "Node Configuration Panel"}
<Box sx={{ backgroundColor: "#232323", borderBottom: "1px solid #333" }}>
<Box sx={{ padding: "12px 16px" }}>
<Typography variant="h7" sx={{ color: "#0475c2", fontWeight: "bold" }}>
{"Edit " + (title || "Node")}
</Typography>
</Box>
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="fullWidth"
textColor="inherit"
TabIndicatorProps={{ style: { backgroundColor: "#58a6ff" } }}
sx={{ borderTop: "1px solid #333", borderBottom: "1px solid #333", minHeight: "36px", 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" }} />
</Tabs>
</Box>
{/* Content */}
<Box sx={{ padding: 2 }}>
<p style={{ fontSize: "0.85rem", color: "#aaa" }}>
This sidebar will be used to configure nodes in the future.
</p>
<p style={{ fontSize: "0.85rem", color: "#aaa" }}>
The idea is that this area will allow for more node configuration controls to be dynamically-populated by the nodes to allow more complex node documentation and configuration.
</p>
{activeTab === 0 && renderConfigFields()}
{activeTab === 1 && (
<Typography sx={{ whiteSpace: "pre-wrap", fontSize: "0.85rem", color: "#aaa" }}>
{nodeData?.usage_documentation || "No documentation provided for this node."}
</Typography>
)}
</Box>
</Box>
</>

View File

@ -1,81 +1,27 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx
/**
* ============================================
* Borealis - Standard Live Data Node Template
* ============================================
*
* COMPONENT ROLE:
* This component defines a "data conduit" node that can accept input,
* process/override it with local logic, and forward the output on a timed basis.
*
* It serves as the core behavior model for other nodes that rely on live propagation.
* Clone and extend this file to create nodes with specialized logic.
*
* CORE CONCEPTS:
* - Uses a centralized shared memory (window.BorealisValueBus) for value sharing
* - Synchronizes with upstream nodes based on ReactFlow edges
* - Emits to downstream nodes by updating its own BorealisValueBus[id] value
* - Controlled by a global polling timer (window.BorealisUpdateRate)
*
* LIFECYCLE SUMMARY:
* - onMount: initializes logic loop and sync monitor
* - onUpdate: watches edges and global rate, reconfigures as needed
* - onUnmount: cleans up all timers
*
* DATA FLOW OVERVIEW:
* - INPUT: if a left-edge (target) is connected, disables manual editing
* - OUTPUT: propagates renderValue to downstream nodes via right-edge (source)
*
* STRUCTURE:
* - Node UI includes:
* * Label (from data.label)
* * Body description (from data.content)
* * Input textbox (disabled if input is connected)
*
* HOW TO EXTEND:
* - For transformations, insert logic into runNodeLogic()
* - To validate or restrict input types, modify handleManualInput()
* - For side-effects or external API calls, add hooks inside runNodeLogic()
*/
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";
// Global Shared Bus for Node Data Propagation
if (!window.BorealisValueBus) {
window.BorealisValueBus = {};
}
// Global Update Rate (ms) for All Data Nodes
if (!window.BorealisUpdateRate) {
window.BorealisUpdateRate = 100;
}
if (!window.BorealisValueBus) window.BorealisValueBus = {};
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
const DataNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const edges = useStore(state => state.edges);
const edges = useStore((state) => state.edges);
const [renderValue, setRenderValue] = useState(data?.value || "");
const valueRef = useRef(renderValue);
// Manual input handler (disabled if connected to input)
const handleManualInput = (e) => {
const newValue = e.target.value;
// TODO: Add input validation/sanitization here if needed
valueRef.current = newValue;
setRenderValue(newValue);
window.BorealisValueBus[id] = newValue;
setNodes(nds =>
nds.map(n =>
n.id === id
? { ...n, data: { ...n.data, value: newValue } }
: n
setNodes((nds) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, value: newValue } } : n
)
);
};
@ -85,46 +31,33 @@ const DataNode = ({ id, data }) => {
let intervalId = null;
const runNodeLogic = () => {
const inputEdge = edges.find(e => e?.target === id);
const hasInput = Boolean(inputEdge);
const inputEdge = edges.find((e) => e?.target === id);
const hasInput = Boolean(inputEdge?.source);
if (hasInput && inputEdge.source) {
if (hasInput) {
const upstreamValue = window.BorealisValueBus[inputEdge.source] ?? "";
// TODO: Insert custom transform logic here (e.g., parseInt, apply formula)
if (upstreamValue !== valueRef.current) {
valueRef.current = upstreamValue;
setRenderValue(upstreamValue);
window.BorealisValueBus[id] = upstreamValue;
setNodes(nds =>
nds.map(n =>
n.id === id
? { ...n, data: { ...n.data, value: upstreamValue } }
: n
setNodes((nds) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, value: upstreamValue } } : n
)
);
}
} else {
// OUTPUT BROADCAST: emits to downstream via shared memory
window.BorealisValueBus[id] = valueRef.current;
}
};
const startInterval = () => {
intervalId = setInterval(runNodeLogic, currentRate);
};
startInterval();
// Monitor for global update rate changes
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate || 100;
if (newRate !== currentRate) {
currentRate = newRate;
clearInterval(intervalId);
startInterval();
currentRate = newRate;
intervalId = setInterval(runNodeLogic, currentRate);
}
}, 250);
@ -134,47 +67,28 @@ const DataNode = ({ id, data }) => {
};
}, [id, setNodes, edges]);
const inputEdge = edges.find(e => e?.target === id);
const inputEdge = edges.find((e) => e?.target === id);
const hasInput = Boolean(inputEdge);
const upstreamId = inputEdge?.source || "";
const upstreamValue = window.BorealisValueBus[upstreamId] || "";
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(data?.label || "Unknown Node")
window.BorealisOpenDrawer(data?.label || "Unknown Node", data)
}
sx={{
padding: "0px",
marginRight: "-3px",
color: "#58a6ff",
fontSize: "14px", // affects inner icon when no size prop
width: "20px",
height: "20px",
minWidth: "20px"
}}
sx={{ padding: 0, marginRight: "-3px", color: "#58a6ff", width: "20px", height: "20px" }}
>
<SettingsIcon sx={{ fontSize: "16px" }} />
</IconButton>
</div>
<div className="borealis-node-content">
{/* Description visible in node body */}
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>
{data?.content || "Foundational node for live value propagation."}
</div>
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
Value:
</label>
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>{data?.content}</div>
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>Value:</label>
<input
type="text"
value={renderValue}
@ -191,23 +105,34 @@ const DataNode = ({ id, data }) => {
}}
/>
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
export default {
type: "DataNode", // REQUIRED: unique identifier for the node type
type: "DataNode",
label: "String / Number",
description: `
Foundational Data Node
- Accepts input from another node
- If no input is connected, allows user-defined value
- Pushes value to downstream nodes every X ms
- Uses BorealisValueBus to communicate with other nodes
`.trim(),
description: "Foundational node for live value propagation.\n\n- Accepts input or manual value\n- Pushes downstream\n- Uses shared memory",
content: "Store a String or Number",
component: DataNode
component: DataNode,
config: [
{ key: "value", label: "Initial Value", type: "text" }
],
usage_documentation: `
### DataNode Usage
This node acts as a live data emitter. When connected to an upstream source, it inherits its value. Otherwise, it accepts user-defined input.
- **Use Cases**:
- Static constants
- Pass-through conduit
- Manual value input
- **Behavior**:
- Automatically updates on interval
- Emits data through BorealisValueBus
Ensure no input edge if manual input is required.
`.trim()
};