Compare commits
4 Commits
84cd4b6a54
...
bc64b1c718
Author | SHA1 | Date | |
---|---|---|---|
bc64b1c718 | |||
020eff9d5c | |||
dff4938f51 | |||
b6500b84da |
@ -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) => {
|
||||
|
@ -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()
|
||||
};
|
||||
|
@ -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()
|
||||
};
|
||||
|
@ -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 <operator> 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()
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user