Compare commits

...

4 Commits

4 changed files with 339 additions and 409 deletions

View File

@ -199,17 +199,26 @@ export default function FlowEditor({
});
const id = "node-" + Date.now();
const nodeMeta = Object.values(categorizedNodes).flat().find((n) => n.type === type);
// Seed config defaults:
const configDefaults = {};
(nodeMeta?.config || []).forEach(cfg => {
if (cfg.defaultValue !== undefined) {
configDefaults[cfg.key] = cfg.defaultValue;
}
});
const newNode = {
id,
type,
position,
data: {
label: nodeMeta?.label || type,
content: nodeMeta?.content
content: nodeMeta?.content,
...configDefaults
},
dragHandle: ".borealis-node-header"
};
setNodes((nds) => [...nds, newNode]);
}, [project, setNodes, categorizedNodes]);
const onDragOver = useCallback((event) => {

View File

@ -1,9 +1,9 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: Node_OCR_Text_Extraction.jsx
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Image Processing/Node_OCR_Text_Extraction.jsx
import React, { useEffect, useState, useRef } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
// Base64 comparison using hash (lightweight)
// Lightweight hash for image change detection
const getHashScore = (str = "") => {
let hash = 0;
for (let i = 0; i < str.length; i += 101) {
@ -22,43 +22,22 @@ const OCRNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const [ocrOutput, setOcrOutput] = useState("");
const [engine, setEngine] = useState(data?.engine || "None");
const [backend, setBackend] = useState(data?.backend || "CPU");
const [dataType, setDataType] = useState(data?.dataType || "Mixed");
const [customRateEnabled, setCustomRateEnabled] = useState(data?.customRateEnabled ?? true);
const [customRateMs, setCustomRateMs] = useState(data?.customRateMs || 1000);
const [changeThreshold, setChangeThreshold] = useState(data?.changeThreshold || 0);
const valueRef = useRef("");
const lastUsed = useRef({ engine: "", backend: "", dataType: "" });
const lastProcessedAt = useRef(0);
const lastImageHash = useRef(0);
// Sync updated settings back into node.data for persistence
useEffect(() => {
setNodes((nodes) =>
nodes.map((n) =>
n.id === id
? {
...n,
data: {
...n.data,
engine,
backend,
dataType,
customRateEnabled,
customRateMs,
changeThreshold
}
}
: n
)
);
}, [engine, backend, dataType, customRateEnabled, customRateMs, changeThreshold]);
// Always get config from props (sidebar sets these in node.data)
const engine = data?.engine || "None";
const backend = data?.backend || "CPU";
const dataType = data?.dataType || "Mixed";
const customRateEnabled = data?.customRateEnabled ?? true;
const customRateMs = data?.customRateMs || 1000;
const changeThreshold = data?.changeThreshold || 0;
// OCR API Call
const sendToOCRAPI = async (base64) => {
const cleanBase64 = base64.replace(/^data:image\/[a-zA-Z]+;base64,/, "");
try {
const response = await fetch("/api/ocr", {
method: "POST",
@ -74,6 +53,7 @@ const OCRNode = ({ id, data }) => {
}
};
// Filter lines based on user type
const filterLines = (lines) => {
if (dataType === "Numerical") {
return lines.map(line => line.replace(/[^\d.%\s]/g, '').replace(/\s+/g, ' ').trim()).filter(Boolean);
@ -149,57 +129,6 @@ const OCRNode = ({ id, data }) => {
<div style={{ fontSize: "9px", marginBottom: "8px", color: "#ccc" }}>
Extract Multi-Line Text from Upstream Image Node
</div>
<label style={labelStyle}>OCR Engine:</label>
<select value={engine} onChange={(e) => setEngine(e.target.value)} style={dropdownStyle}>
<option value="None">None</option>
<option value="TesseractOCR">TesseractOCR</option>
<option value="EasyOCR">EasyOCR</option>
</select>
<label style={labelStyle}>Compute:</label>
<select value={backend} onChange={(e) => setBackend(e.target.value)} style={dropdownStyle} disabled={engine === "None"}>
<option value="CPU">CPU</option>
<option value="GPU">GPU</option>
</select>
<label style={labelStyle}>Data Type:</label>
<select value={dataType} onChange={(e) => setDataType(e.target.value)} style={dropdownStyle}>
<option value="Mixed">Mixed Data</option>
<option value="Numerical">Numerical Data</option>
<option value="String">String Data</option>
</select>
<label style={labelStyle}>Custom API Rate-Limit (ms):</label>
<div style={{ display: "flex", alignItems: "center", marginBottom: "8px" }}>
<input
type="checkbox"
checked={customRateEnabled}
onChange={(e) => setCustomRateEnabled(e.target.checked)}
style={{ marginRight: "8px" }}
/>
<input
type="number"
min="100"
step="100"
value={customRateMs}
onChange={(e) => setCustomRateMs(Number(e.target.value))}
disabled={!customRateEnabled}
style={numberInputStyle}
/>
</div>
<label style={labelStyle}>Change Detection Sensitivity Threshold:</label>
<input
type="number"
min="0"
max="100"
step="1"
value={changeThreshold}
onChange={(e) => setChangeThreshold(Number(e.target.value))}
style={numberInputStyle}
/>
<label style={labelStyle}>OCR Output:</label>
<textarea
readOnly
@ -229,31 +158,81 @@ const labelStyle = {
marginBottom: "2px"
};
const dropdownStyle = {
width: "100%",
fontSize: "9px",
padding: "4px",
background: "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px"
};
const numberInputStyle = {
width: "80px",
fontSize: "9px",
background: "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
padding: "2px 4px",
marginBottom: "8px"
};
// Node registration for Borealis (modern, sidebar-enabled)
export default {
type: "OCR_Text_Extraction",
label: "OCR Text Extraction",
description: `Extract text from upstream image using backend OCR engine via API. Includes rate limiting and sensitivity detection for smart processing.`,
description: `Extract text from upstream image using backend OCR engine via API. Includes rate limiting and sensitivity detection for smart processing.`,
content: "Extract Multi-Line Text from Upstream Image Node",
component: OCRNode
component: OCRNode,
config: [
{
key: "engine",
label: "OCR Engine",
type: "select",
options: ["None", "TesseractOCR", "EasyOCR"],
defaultValue: "None"
},
{
key: "backend",
label: "Compute Backend",
type: "select",
options: ["CPU", "GPU"],
defaultValue: "CPU"
},
{
key: "dataType",
label: "Data Type Filter",
type: "select",
options: ["Mixed", "Numerical", "String"],
defaultValue: "Mixed"
},
{
key: "customRateEnabled",
label: "Custom API Rate Limit Enabled",
type: "select",
options: ["true", "false"],
defaultValue: "true"
},
{
key: "customRateMs",
label: "Custom API Rate Limit (ms)",
type: "text",
defaultValue: "1000"
},
{
key: "changeThreshold",
label: "Change Detection Sensitivity (0-100)",
type: "text",
defaultValue: "0"
}
],
usage_documentation: `
### OCR Text Extraction Node
Extracts text (lines) from an **upstream image node** using a selectable backend OCR engine (Tesseract or EasyOCR). Designed for screenshots, scanned forms, and live image data pipelines.
**Features:**
- **Engine:** Select between None, TesseractOCR, or EasyOCR
- **Backend:** Choose CPU or GPU (if supported)
- **Data Type Filter:** Post-processes recognized lines for numerical-only or string-only content
- **Custom API Rate Limit:** When enabled, you can set a custom polling rate for OCR requests (in ms)
- **Change Detection Sensitivity:** Node will only re-OCR if the input image changes significantly (hash-based, 0 disables)
**Outputs:**
- Array of recognized lines, pushed to downstream nodes
- Output is displayed in the node (read-only)
**Usage:**
- Connect an image node (base64 output) to this node's input
- Configure OCR engine and options in the sidebar
- Useful for extracting values from screen regions, live screenshots, PDF scans, etc.
**Notes:**
- Setting Engine to 'None' disables OCR
- Use numerical/string filter for precise downstream parsing
- Polling rate too fast may cause backend overload
- Change threshold is a 0-100 scale (0 = always run, 100 = image must change completely)
`.trim()
};

View File

@ -1,67 +1,27 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Collection/Node_API_Request.jsx
import React, { useState, useEffect, useRef } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
// API Request Node (Modern, Sidebar Config Enabled)
const APIRequestNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const edges = useStore((state) => state.edges);
if (!window.BorealisValueBus) window.BorealisValueBus = {};
const [url, setUrl] = useState(data?.url || "http://localhost:5000/health");
const [editUrl, setEditUrl] = useState(data?.url || "http://localhost:5000/health");
const [useProxy, setUseProxy] = useState(data?.useProxy ?? true);
const [body, setBody] = useState(data?.body || "");
const [intervalSec, setIntervalSec] = useState(data?.intervalSec ?? 10);
// Use config values, but coerce types
const url = data?.url || "http://localhost:5000/health";
// Note: Store useProxy as a string ("true"/"false"), convert to boolean for logic
const useProxy = (data?.useProxy ?? "true") === "true";
const body = data?.body || "";
const intervalSec = parseInt(data?.intervalSec || "10", 10) || 10;
// Status State
const [error, setError] = useState(null);
const [statusCode, setStatusCode] = useState(null);
const [statusText, setStatusText] = useState("");
const resultRef = useRef(null);
const handleUrlInputChange = (e) => setEditUrl(e.target.value);
const handleToggleProxy = (e) => {
const flag = e.target.checked;
setUseProxy(flag);
setNodes((nds) =>
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, useProxy: flag } } : n))
);
};
const handleUpdateUrl = () => {
setUrl(editUrl);
setError(null);
setStatusCode(null);
setStatusText("");
resultRef.current = null;
window.BorealisValueBus[id] = undefined;
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, url: editUrl, result: undefined } }
: n
)
);
};
const handleBodyChange = (e) => {
const newBody = e.target.value;
setBody(newBody);
setNodes((nds) =>
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, body: newBody } } : n))
);
};
const handleIntervalChange = (e) => {
const sec = parseInt(e.target.value, 10) || 1;
setIntervalSec(sec);
setNodes((nds) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, intervalSec: sec } } : n
)
);
};
useEffect(() => {
let cancelled = false;
@ -69,12 +29,9 @@ const APIRequestNode = ({ id, data }) => {
try {
setError(null);
// Allow dynamic URL override from upstream node (if present)
const inputEdge = edges.find((e) => e.target === id);
const upstreamUrl = inputEdge ? window.BorealisValueBus[inputEdge.source] : null;
if (upstreamUrl && upstreamUrl !== editUrl) {
setEditUrl(upstreamUrl);
}
const resolvedUrl = upstreamUrl || url;
let target = useProxy ? `/api/proxy?url=${encodeURIComponent(resolvedUrl)}` : resolvedUrl;
@ -126,119 +83,111 @@ const APIRequestNode = ({ id, data }) => {
};
}, [url, body, intervalSec, useProxy, id, setNodes, edges]);
// Upstream disables direct editing of URL in the UI
const inputEdge = edges.find((e) => e.target === id);
const hasUpstream = Boolean(inputEdge && inputEdge.source);
// -- Node Card Render (minimal: sidebar handles config) --
return (
<div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" />
<div className="borealis-node-header">API Request</div>
<div className="borealis-node-content">
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>Request URL:</label>
<input
type="text"
value={editUrl}
onChange={handleUrlInputChange}
disabled={hasUpstream}
style={{
fontSize: "9px",
padding: "4px",
width: "100%",
background: hasUpstream ? "#2a2a2a" : "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px"
}}
/>
<button
onClick={handleUpdateUrl}
disabled={hasUpstream}
style={{
fontSize: "9px",
marginTop: "6px",
padding: "2px 6px",
background: "#333",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
cursor: hasUpstream ? "not-allowed" : "pointer"
}}
>
Update URL
</button>
<div style={{ marginTop: "6px" }}>
<input
id={`${id}-proxy-toggle`}
type="checkbox"
checked={useProxy}
onChange={handleToggleProxy}
/>
<label
htmlFor={`${id}-proxy-toggle`}
title="Query a remote API server using internal Borealis mechanisms to bypass CORS limitations."
style={{ fontSize: "8px", marginLeft: "4px", cursor: "help" }}
>
Remote API Endpoint
</label>
</div>
<label style={{ fontSize: "9px", display: "block", margin: "8px 0 4px" }}>Request Body:</label>
<textarea
value={body}
onChange={handleBodyChange}
rows={4}
style={{
fontSize: "9px",
padding: "4px",
width: "100%",
background: "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
resize: "vertical"
}}
/>
<label style={{ fontSize: "9px", display: "block", margin: "8px 0 4px" }}>Polling Interval (sec):</label>
<input
type="number"
min="1"
value={intervalSec}
onChange={handleIntervalChange}
style={{
fontSize: "9px",
padding: "4px",
width: "100%",
background: "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px"
}}
/>
{statusCode !== null && statusCode >= 200 && statusCode < 300 && (
<div style={{ color: "#6f6", fontSize: "8px", marginTop: "6px" }}>
Status: {statusCode} {statusText}
</div>
)}
{error && (
<div style={{ color: "#f66", fontSize: "8px", marginTop: "6px" }}>
Error: {error}
</div>
)}
<div className="borealis-node-header">
{data?.label || "API Request"}
</div>
<div className="borealis-node-content" style={{ fontSize: "9px", color: "#ccc" }}>
<div>
<b>Status:</b>{" "}
{error ? (
<span style={{ color: "#f66" }}>{error}</span>
) : statusCode !== null ? (
<span style={{ color: "#6f6" }}>{statusCode} {statusText}</span>
) : (
"N/A"
)}
</div>
<div style={{ marginTop: "4px" }}>
<b>Result:</b>
<pre style={{
background: "#181818",
color: "#b6ffb4",
fontSize: "8px",
maxHeight: 62,
overflow: "auto",
margin: 0,
padding: "4px",
borderRadius: "2px"
}}>{data?.result ? String(data.result).slice(0, 350) : "No data"}</pre>
</div>
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
// Node Registration Object with sidebar config + docs
export default {
type: "API_Request",
label: "API Request",
description: "Fetch JSON from an API endpoint with optional body, proxy toggle, and polling interval.",
content: "Fetch JSON from HTTP or remote API via internal proxy to bypass CORS.",
component: APIRequestNode
description: "Fetch JSON from an API endpoint with optional POST body, polling, and proxy toggle. Accepts URL from upstream.",
content: "Fetch JSON from HTTP or remote API endpoint, with CORS proxy option.",
component: APIRequestNode,
config: [
{
key: "url",
label: "Request URL",
type: "text",
defaultValue: "http://localhost:5000/health"
},
{
key: "useProxy",
label: "Use Proxy (bypass CORS)",
type: "select",
options: ["true", "false"],
defaultValue: "true"
},
{
key: "body",
label: "Request Body (JSON)",
type: "textarea",
defaultValue: ""
},
{
key: "intervalSec",
label: "Polling Interval (sec)",
type: "text",
defaultValue: "10"
}
],
usage_documentation: `
### API Request Node
Fetches JSON from an HTTP or HTTPS API endpoint, with an option to POST a JSON body and control polling interval.
**Features:**
- **URL**: You can set a static URL, or connect an upstream node to dynamically control the API endpoint.
- **Use Proxy**: When enabled, requests route through the Borealis backend proxy to bypass CORS/browser restrictions.
- **Request Body**: POST JSON data (leave blank for GET).
- **Polling Interval**: Set how often (in seconds) to re-fetch the API.
**Outputs:**
- The downstream value is the parsed JSON object from the API response.
**Typical Use Cases:**
- Poll external APIs (weather, status, data, etc)
- Connect to local/internal REST endpoints
- Build data pipelines with API triggers
**Input & UI Behavior:**
- If an upstream node is connected, its output value will override the Request URL.
- All config is handled in the right sidebar (Node Properties).
**Error Handling:**
- If the fetch fails, the node displays the error in the UI.
- Only 2xx status codes are considered successful.
**Security Note:**
- Use Proxy mode for APIs requiring CORS bypass or additional privacy.
`.trim()
};

View File

@ -1,179 +1,172 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Math_Operations.jsx
/**
* ============================================
* Borealis - Math Operation Node (Multi-Input A/B)
* ============================================
*
* COMPONENT ROLE:
* Performs live math operations on *two grouped input sets* (A and B).
*
* FUNCTIONALITY:
* - Inputs connected to Handle A are summed
* - Inputs connected to Handle B are summed
* - Math operation is applied as: A &lt;operator&gt; B
* - Result pushed via BorealisValueBus[id]
*
* SUPPORTED OPERATORS:
* - Add, Subtract, Multiply, Divide, Average
*/
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";
// Init shared memory bus if not already set
if (!window.BorealisValueBus) window.BorealisValueBus = {};
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
const MathNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const edges = useStore(state => state.edges);
const { setNodes } = useReactFlow();
const edges = useStore(state => state.edges);
const [renderResult, setRenderResult] = useState(data?.value || "0");
const resultRef = useRef(renderResult);
const [operator, setOperator] = useState(data?.operator || "Add");
const [result, setResult] = useState("0");
const resultRef = useRef(0);
useEffect(() => {
let intervalId = null;
let currentRate = window.BorealisUpdateRate;
useEffect(() => {
let intervalId = null;
let currentRate = window.BorealisUpdateRate;
const runLogic = () => {
const operator = data?.operator || "Add";
const inputsA = edges.filter(e => e.target === id && e.targetHandle === "a");
const inputsB = edges.filter(e => e.target === id && e.targetHandle === "b");
const runLogic = () => {
const inputsA = edges.filter(e => e.target === id && e.targetHandle === "a");
const inputsB = edges.filter(e => e.target === id && e.targetHandle === "b");
const sum = (list) =>
list
.map(e => parseFloat(window.BorealisValueBus[e.source]) || 0)
.reduce((a, b) => a + b, 0);
const sum = (list) =>
list.map(e => parseFloat(window.BorealisValueBus[e.source]) || 0).reduce((a, b) => a + b, 0);
const valA = sum(inputsA);
const valB = sum(inputsB);
const valA = sum(inputsA);
const valB = sum(inputsB);
let value = 0;
switch (operator) {
case "Add":
value = valA + valB;
break;
case "Subtract":
value = valA - valB;
break;
case "Multiply":
value = valA * valB;
break;
case "Divide":
value = valB !== 0 ? valA / valB : 0;
break;
case "Average":
const totalInputs = inputsA.length + inputsB.length;
const totalSum = valA + valB;
value = totalInputs > 0 ? totalSum / totalInputs : 0;
break;
}
let value = 0;
switch (operator) {
case "Add":
value = valA + valB;
break;
case "Subtract":
value = valA - valB;
break;
case "Multiply":
value = valA * valB;
break;
case "Divide":
value = valB !== 0 ? valA / valB : 0;
break;
case "Average":
const totalInputs = inputsA.length + inputsB.length;
const totalSum = valA + valB;
value = totalInputs > 0 ? totalSum / totalInputs : 0;
break;
}
resultRef.current = value;
setRenderResult(value.toString());
window.BorealisValueBus[id] = value.toString();
resultRef.current = value;
setResult(value.toString());
window.BorealisValueBus[id] = value.toString();
setNodes(nds =>
nds.map(n =>
n.id === id
? { ...n, data: { ...n.data, value: value.toString() } }
: n
)
);
};
setNodes(nds =>
nds.map(n =>
n.id === id ? { ...n, data: { ...n.data, operator, value: value.toString() } } : n
)
);
};
intervalId = setInterval(runLogic, currentRate);
// Watch for update rate changes
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate;
if (newRate !== currentRate) {
clearInterval(intervalId);
currentRate = newRate;
intervalId = setInterval(runLogic, currentRate);
}
}, 250);
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate;
if (newRate !== currentRate) {
clearInterval(intervalId);
currentRate = newRate;
intervalId = setInterval(runLogic, currentRate);
}
}, 250);
return () => {
clearInterval(intervalId);
clearInterval(monitor);
};
}, [id, edges, setNodes, data?.operator]);
return () => {
clearInterval(intervalId);
clearInterval(monitor);
};
}, [id, operator, edges, 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 || "Math Operation"}</span>
</div>
<div className="borealis-node-header">
{data?.label || "Math Operation"}
</div>
<div className="borealis-node-content" style={{ fontSize: "9px", color: "#ccc", marginTop: 4 }}>
Result: {renderResult}
</div>
<div className="borealis-node-content">
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>
Aggregates A and B inputs then performs operation.
</div>
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
Operator:
</label>
<select
value={operator}
onChange={(e) => setOperator(e.target.value)}
style={dropdownStyle}
>
<option value="Add">Add</option>
<option value="Subtract">Subtract</option>
<option value="Multiply">Multiply</option>
<option value="Divide">Divide</option>
<option value="Average">Average</option>
</select>
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
Result:
</label>
<input
type="text"
value={result}
disabled
style={resultBoxStyle}
/>
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
const dropdownStyle = {
fontSize: "9px",
padding: "4px",
background: "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
width: "100%",
marginBottom: "8px"
};
const resultBoxStyle = {
fontSize: "9px",
padding: "4px",
background: "#2a2a2a",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
width: "100%"
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
export default {
type: "MathNode",
label: "Math Operation",
description: `
Perform Math on Aggregated Inputs
type: "MathNode",
label: "Math Operation",
description: `
Live math node for computing on two grouped inputs.
- A and B groups are independently summed
- Performs: Add, Subtract, Multiply, Divide, or Average
- Result = A <op> B
- Emits result via BorealisValueBus every update tick
`.trim(),
content: "Perform Math Operations",
component: MathNode
- Sums all A and B handle inputs separately
- Performs selected math operation: Add, Subtract, Multiply, Divide, Average
- Emits result as string via BorealisValueBus
- Updates at the global update rate
**Common Uses:**
Live dashboard math, sensor fusion, calculation chains, dynamic thresholds
`.trim(),
content: "Perform Math Operations",
component: MathNode,
config: [
{
key: "operator",
label: "Operator",
type: "select",
options: [
"Add",
"Subtract",
"Multiply",
"Divide",
"Average"
]
}
],
usage_documentation: `
### Math Operation Node
Performs live math between two groups of inputs (**A** and **B**).
#### Usage
- Connect any number of nodes to the **A** and **B** input handles.
- The node **sums all values** from A and from B before applying the operator.
- Select the math operator in the sidebar config:
- **Add**: A + B
- **Subtract**: A - B
- **Multiply**: A * B
- **Divide**: A / B (0 if B=0)
- **Average**: (A + B) / total number of inputs
#### Output
- The computed result is pushed as a string to downstream nodes every update tick.
#### Input Handles
- **A** (Top Left)
- **B** (Middle Left)
#### Example
If three nodes outputting 5, 10, 15 are connected to A,
and one node outputs 2 is connected to B,
and operator is Multiply:
- **A** = 5 + 10 + 15 = 30
- **B** = 2
- **Result** = 30 * 2 = 60
`.trim()
};