mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-07-29 02:38:28 -06:00
Added Various API Functionality
This commit is contained in:
@ -0,0 +1,143 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: Node_Array_Index_Extractor.jsx
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||
|
||||
if (!window.BorealisValueBus) window.BorealisValueBus = {};
|
||||
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
|
||||
|
||||
const ArrayIndexExtractorNode = ({ id, data }) => {
|
||||
const edges = useStore((state) => state.edges);
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
const [lineNumber, setLineNumber] = useState(data?.lineNumber || 1);
|
||||
const [result, setResult] = useState("Line Does Not Exist");
|
||||
|
||||
const valueRef = useRef(result);
|
||||
|
||||
const handleLineNumberChange = (e) => {
|
||||
const num = parseInt(e.target.value, 10);
|
||||
const clamped = isNaN(num) ? 1 : Math.max(1, num);
|
||||
setLineNumber(clamped);
|
||||
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, lineNumber: clamped } } : n
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId = null;
|
||||
let currentRate = window.BorealisUpdateRate;
|
||||
|
||||
const runNodeLogic = () => {
|
||||
const inputEdge = edges.find((e) => e.target === id);
|
||||
if (!inputEdge) {
|
||||
valueRef.current = "Line Does Not Exist";
|
||||
setResult("Line Does Not Exist");
|
||||
window.BorealisValueBus[id] = "Line Does Not Exist";
|
||||
return;
|
||||
}
|
||||
|
||||
const upstreamValue = window.BorealisValueBus[inputEdge.source];
|
||||
if (!Array.isArray(upstreamValue)) {
|
||||
valueRef.current = "Line Does Not Exist";
|
||||
setResult("Line Does Not Exist");
|
||||
window.BorealisValueBus[id] = "Line Does Not Exist";
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Math.max(0, lineNumber - 1); // Convert 1-based input to 0-based
|
||||
const selected = upstreamValue[index] ?? "Line Does Not Exist";
|
||||
|
||||
if (selected !== valueRef.current) {
|
||||
valueRef.current = selected;
|
||||
setResult(selected);
|
||||
window.BorealisValueBus[id] = selected;
|
||||
}
|
||||
};
|
||||
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
|
||||
const monitor = setInterval(() => {
|
||||
const newRate = window.BorealisUpdateRate;
|
||||
if (newRate !== currentRate) {
|
||||
clearInterval(intervalId);
|
||||
currentRate = newRate;
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
clearInterval(monitor);
|
||||
};
|
||||
}, [id, edges, lineNumber]);
|
||||
|
||||
return (
|
||||
<div className="borealis-node">
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
<div className="borealis-node-header">Array Index Extractor</div>
|
||||
<div className="borealis-node-content" style={{ fontSize: "9px" }}>
|
||||
<div style={{ marginBottom: "6px", color: "#ccc" }}>
|
||||
Output a Specific Array Index's Value
|
||||
</div>
|
||||
|
||||
<label style={{ display: "block", marginBottom: "2px" }}>
|
||||
Line Number (1 = First Line):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={lineNumber}
|
||||
onChange={handleLineNumberChange}
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: "9px",
|
||||
background: "#1e1e1e",
|
||||
color: "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "3px",
|
||||
marginBottom: "6px"
|
||||
}}
|
||||
/>
|
||||
|
||||
<label style={{ display: "block", marginBottom: "2px" }}>
|
||||
Output:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={result}
|
||||
disabled
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: "9px",
|
||||
background: "#2a2a2a",
|
||||
color: "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "3px"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "ArrayIndexExtractor",
|
||||
label: "Array Index Extractor",
|
||||
description: `
|
||||
Outputs a specific line from an upstream array (e.g., OCR multi-line output).
|
||||
|
||||
- User specifies the line number (1-based index)
|
||||
- Outputs the value from that line if it exists
|
||||
- If the index is out of bounds, outputs "Line Does Not Exist"
|
||||
`.trim(),
|
||||
content: "Output a Specific Array Index's Value",
|
||||
component: ArrayIndexExtractorNode
|
||||
};
|
@ -0,0 +1,179 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Analysis & Manipulation/Node_JSON_Display.jsx
|
||||
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||
// For syntax highlighting, ensure prismjs is installed: npm install prismjs
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-json";
|
||||
import "prismjs/themes/prism-okaidia.css";
|
||||
|
||||
const JSONPrettyDisplayNode = ({ id, data }) => {
|
||||
const { setNodes } = useReactFlow();
|
||||
const edges = useStore((state) => state.edges);
|
||||
const containerRef = useRef(null);
|
||||
const resizingRef = useRef(false);
|
||||
const startPosRef = useRef({ x: 0, y: 0 });
|
||||
const startDimRef = useRef({ width: 0, height: 0 });
|
||||
|
||||
const [jsonData, setJsonData] = useState(data?.jsonData || {});
|
||||
const initW = parseInt(data?.width || "300", 10);
|
||||
const initH = parseInt(data?.height || "150", 10);
|
||||
const [dimensions, setDimensions] = useState({ width: initW, height: initH });
|
||||
const jsonRef = useRef(jsonData);
|
||||
|
||||
const persistDimensions = useCallback(() => {
|
||||
const w = `${Math.round(dimensions.width)}px`;
|
||||
const h = `${Math.round(dimensions.height)}px`;
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, width: w, height: h } }
|
||||
: n
|
||||
)
|
||||
);
|
||||
}, [dimensions, id, setNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMouseMove = (e) => {
|
||||
if (!resizingRef.current) return;
|
||||
const dx = e.clientX - startPosRef.current.x;
|
||||
const dy = e.clientY - startPosRef.current.y;
|
||||
setDimensions({
|
||||
width: Math.max(100, startDimRef.current.width + dx),
|
||||
height: Math.max(60, startDimRef.current.height + dy)
|
||||
});
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
if (resizingRef.current) {
|
||||
resizingRef.current = false;
|
||||
persistDimensions();
|
||||
}
|
||||
};
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
}, [persistDimensions]);
|
||||
|
||||
const onResizeMouseDown = (e) => {
|
||||
e.stopPropagation();
|
||||
resizingRef.current = true;
|
||||
startPosRef.current = { x: e.clientX, y: e.clientY };
|
||||
startDimRef.current = { ...dimensions };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let rate = window.BorealisUpdateRate;
|
||||
const tick = () => {
|
||||
const edge = edges.find((e) => e.target === id);
|
||||
if (edge && edge.source) {
|
||||
const upstream = window.BorealisValueBus[edge.source];
|
||||
if (typeof upstream === "object") {
|
||||
if (JSON.stringify(upstream) !== JSON.stringify(jsonRef.current)) {
|
||||
jsonRef.current = upstream;
|
||||
setJsonData(upstream);
|
||||
window.BorealisValueBus[id] = upstream;
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, jsonData: upstream } } : n
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
window.BorealisValueBus[id] = jsonRef.current;
|
||||
}
|
||||
};
|
||||
const iv = setInterval(tick, rate);
|
||||
const monitor = setInterval(() => {
|
||||
if (window.BorealisUpdateRate !== rate) {
|
||||
clearInterval(iv);
|
||||
clearInterval(monitor);
|
||||
}
|
||||
}, 200);
|
||||
return () => { clearInterval(iv); clearInterval(monitor); };
|
||||
}, [id, edges, setNodes]);
|
||||
|
||||
// Generate highlighted HTML
|
||||
const pretty = JSON.stringify(jsonData, null, 2);
|
||||
const highlighted = Prism.highlight(pretty, Prism.languages.json, "json");
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="borealis-node"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
overflow: "visible",
|
||||
position: "relative",
|
||||
boxSizing: "border-box"
|
||||
}}
|
||||
>
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
|
||||
<div className="borealis-node-header">Display JSON Data</div>
|
||||
<div
|
||||
className="borealis-node-content"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "4px",
|
||||
fontSize: "9px",
|
||||
color: "#ccc",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "4px" }}>
|
||||
Display prettified JSON from upstream.
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
background: "#1e1e1e",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "4px",
|
||||
overflowY: "auto",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "9px"
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{ __html: highlighted }}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseDown={onResizeMouseDown}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
right: "-4px",
|
||||
bottom: "-4px",
|
||||
cursor: "nwse-resize",
|
||||
background: "transparent",
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "Node_JSON_Pretty_Display",
|
||||
label: "Display JSON Data",
|
||||
description: "Display upstream JSON object as prettified JSON with syntax highlighting.",
|
||||
content: "Display prettified multi-line JSON from upstream node.",
|
||||
component: JSONPrettyDisplayNode
|
||||
};
|
@ -0,0 +1,132 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Analysis & Manipulation/Node_JSON_Value_Extractor.jsx
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Handle, Position, useReactFlow } from "reactflow";
|
||||
|
||||
const JSONValueExtractorNode = ({ id, data }) => {
|
||||
const { setNodes, getEdges } = useReactFlow();
|
||||
const [keyName, setKeyName] = useState(data?.keyName || "");
|
||||
const [value, setValue] = useState(data?.result || "");
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
const newKey = e.target.value;
|
||||
setKeyName(newKey);
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, keyName: newKey } }
|
||||
: n
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let currentRate = window.BorealisUpdateRate;
|
||||
let intervalId;
|
||||
|
||||
const runNodeLogic = () => {
|
||||
const edges = getEdges();
|
||||
const incoming = edges.filter((e) => e.target === id);
|
||||
const sourceId = incoming[0]?.source;
|
||||
let newValue = "Key Not Found";
|
||||
|
||||
if (sourceId && window.BorealisValueBus[sourceId] !== undefined) {
|
||||
let upstream = window.BorealisValueBus[sourceId];
|
||||
if (upstream && typeof upstream === "object" && keyName) {
|
||||
const pathSegments = keyName.split(".");
|
||||
let nodeVal = upstream;
|
||||
for (let segment of pathSegments) {
|
||||
if (
|
||||
nodeVal != null &&
|
||||
(typeof nodeVal === "object" || Array.isArray(nodeVal)) &&
|
||||
segment in nodeVal
|
||||
) {
|
||||
nodeVal = nodeVal[segment];
|
||||
} else {
|
||||
nodeVal = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nodeVal !== undefined) {
|
||||
newValue = String(nodeVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue !== value) {
|
||||
setValue(newValue);
|
||||
window.BorealisValueBus[id] = newValue;
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, result: newValue } }
|
||||
: n
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
runNodeLogic();
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
|
||||
const monitor = setInterval(() => {
|
||||
const newRate = window.BorealisUpdateRate;
|
||||
if (newRate !== currentRate) {
|
||||
clearInterval(intervalId);
|
||||
currentRate = newRate;
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
clearInterval(monitor);
|
||||
};
|
||||
}, [keyName, id, setNodes, getEdges, value]);
|
||||
|
||||
return (
|
||||
<div className="borealis-node">
|
||||
<div className="borealis-node-header">JSON Value Extractor</div>
|
||||
<div className="borealis-node-content">
|
||||
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
|
||||
Key:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={keyName}
|
||||
onChange={handleKeyChange}
|
||||
placeholder="e.g. name.en"
|
||||
style={{
|
||||
fontSize: "9px", padding: "4px", width: "100%",
|
||||
background: "#1e1e1e", color: "#ccc",
|
||||
border: "1px solid #444", borderRadius: "2px"
|
||||
}}
|
||||
/>
|
||||
<label style={{ fontSize: "9px", display: "block", margin: "8px 0 4px" }}>
|
||||
Value:
|
||||
</label>
|
||||
<textarea
|
||||
readOnly
|
||||
value={value}
|
||||
rows={2}
|
||||
style={{
|
||||
fontSize: "9px", padding: "4px", width: "100%",
|
||||
background: "#1e1e1e", color: "#ccc",
|
||||
border: "1px solid #444", borderRadius: "2px",
|
||||
resize: "none"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "JSON_Value_Extractor",
|
||||
label: "JSON Value Extractor",
|
||||
description: "Extract a nested value by dot-delimited path from upstream JSON data.",
|
||||
content: "Provide a dot-separated key path (e.g. 'name.en'); outputs the extracted string or 'Key Not Found'.",
|
||||
component: JSONValueExtractorNode
|
||||
};
|
@ -0,0 +1,259 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: Node_OCR_Text_Extraction.jsx
|
||||
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||
|
||||
// Base64 comparison using hash (lightweight)
|
||||
const getHashScore = (str = "") => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i += 101) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash |= 0;
|
||||
}
|
||||
return Math.abs(hash);
|
||||
};
|
||||
|
||||
if (!window.BorealisValueBus) window.BorealisValueBus = {};
|
||||
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
|
||||
|
||||
const OCRNode = ({ id, data }) => {
|
||||
const edges = useStore((state) => state.edges);
|
||||
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]);
|
||||
|
||||
const sendToOCRAPI = async (base64) => {
|
||||
const cleanBase64 = base64.replace(/^data:image\/[a-zA-Z]+;base64,/, "");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/ocr", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ image_base64: cleanBase64, engine, backend })
|
||||
});
|
||||
const result = await response.json();
|
||||
return response.ok && Array.isArray(result.lines)
|
||||
? result.lines
|
||||
: [`[ERROR] ${result.error || "Invalid OCR response."}`];
|
||||
} catch (err) {
|
||||
return [`[ERROR] OCR API request failed: ${err.message}`];
|
||||
}
|
||||
};
|
||||
|
||||
const filterLines = (lines) => {
|
||||
if (dataType === "Numerical") {
|
||||
return lines.map(line => line.replace(/[^\d.%\s]/g, '').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
||||
}
|
||||
if (dataType === "String") {
|
||||
return lines.map(line => line.replace(/[^a-zA-Z\s]/g, '').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId = null;
|
||||
let currentRate = window.BorealisUpdateRate || 100;
|
||||
|
||||
const runNodeLogic = async () => {
|
||||
const inputEdge = edges.find((e) => e.target === id);
|
||||
if (!inputEdge) {
|
||||
window.BorealisValueBus[id] = [];
|
||||
setOcrOutput("");
|
||||
return;
|
||||
}
|
||||
|
||||
const upstreamValue = window.BorealisValueBus[inputEdge.source] || "";
|
||||
const now = Date.now();
|
||||
|
||||
const effectiveRate = customRateEnabled ? customRateMs : window.BorealisUpdateRate || 100;
|
||||
const configChanged =
|
||||
lastUsed.current.engine !== engine ||
|
||||
lastUsed.current.backend !== backend ||
|
||||
lastUsed.current.dataType !== dataType;
|
||||
|
||||
const upstreamHash = getHashScore(upstreamValue);
|
||||
const hashDelta = Math.abs(upstreamHash - lastImageHash.current);
|
||||
const hashThreshold = (changeThreshold / 100) * 1000000000;
|
||||
|
||||
const imageChanged = hashDelta > hashThreshold;
|
||||
|
||||
if (!configChanged && (!imageChanged || (now - lastProcessedAt.current < effectiveRate))) return;
|
||||
|
||||
lastUsed.current = { engine, backend, dataType };
|
||||
lastProcessedAt.current = now;
|
||||
lastImageHash.current = upstreamHash;
|
||||
valueRef.current = upstreamValue;
|
||||
|
||||
const lines = await sendToOCRAPI(upstreamValue);
|
||||
const filtered = filterLines(lines);
|
||||
setOcrOutput(filtered.join("\n"));
|
||||
window.BorealisValueBus[id] = filtered;
|
||||
};
|
||||
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
|
||||
const monitor = setInterval(() => {
|
||||
const newRate = window.BorealisUpdateRate || 100;
|
||||
if (newRate !== currentRate) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = setInterval(runNodeLogic, newRate);
|
||||
currentRate = newRate;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
clearInterval(monitor);
|
||||
};
|
||||
}, [id, engine, backend, dataType, customRateEnabled, customRateMs, changeThreshold, edges]);
|
||||
|
||||
return (
|
||||
<div className="borealis-node" style={{ minWidth: "200px" }}>
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
<div className="borealis-node-header">OCR-Based Text Extraction</div>
|
||||
<div className="borealis-node-content">
|
||||
<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
|
||||
value={ocrOutput}
|
||||
rows={6}
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: "9px",
|
||||
background: "#1e1e1e",
|
||||
color: "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "4px",
|
||||
resize: "vertical"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const labelStyle = {
|
||||
fontSize: "9px",
|
||||
display: "block",
|
||||
marginTop: "6px",
|
||||
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"
|
||||
};
|
||||
|
||||
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.`,
|
||||
content: "Extract Multi-Line Text from Upstream Image Node",
|
||||
component: OCRNode
|
||||
};
|
@ -0,0 +1,201 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Manipulation/Node_Regex_Replace.jsx
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||
|
||||
if (!window.BorealisValueBus) window.BorealisValueBus = {};
|
||||
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
|
||||
|
||||
const RegexReplaceNode = ({ id, data }) => {
|
||||
const edges = useStore((state) => state.edges);
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
const [pattern, setPattern] = useState(data?.pattern || "");
|
||||
const [replacement, setReplacement] = useState(data?.replacement || "");
|
||||
const [flags, setFlags] = useState(data?.flags || "g");
|
||||
const [enabled, setEnabled] = useState(data?.enabled ?? true);
|
||||
const [result, setResult] = useState("");
|
||||
const [original, setOriginal] = useState("");
|
||||
|
||||
const valueRef = useRef("");
|
||||
|
||||
const updateNodeData = (key, val) => {
|
||||
setNodes(nds =>
|
||||
nds.map(n =>
|
||||
n.id === id ? { ...n, data: { ...n.data, [key]: val } } : n
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId = null;
|
||||
let currentRate = window.BorealisUpdateRate;
|
||||
|
||||
const runNodeLogic = () => {
|
||||
const inputEdge = edges.find((e) => e.target === id);
|
||||
const inputValue = inputEdge
|
||||
? window.BorealisValueBus[inputEdge.source] || ""
|
||||
: "";
|
||||
|
||||
setOriginal(inputValue);
|
||||
|
||||
let newVal = inputValue;
|
||||
|
||||
try {
|
||||
if (enabled && pattern) {
|
||||
const regex = new RegExp(pattern, flags);
|
||||
let safeReplacement = replacement.trim();
|
||||
if (
|
||||
safeReplacement.startsWith('"') &&
|
||||
safeReplacement.endsWith('"')
|
||||
) {
|
||||
safeReplacement = safeReplacement.slice(1, -1);
|
||||
}
|
||||
newVal = inputValue.replace(regex, safeReplacement);
|
||||
}
|
||||
} catch (err) {
|
||||
newVal = `[Error] ${err.message}`;
|
||||
}
|
||||
|
||||
if (newVal !== valueRef.current) {
|
||||
valueRef.current = newVal;
|
||||
setResult(newVal);
|
||||
window.BorealisValueBus[id] = newVal;
|
||||
}
|
||||
};
|
||||
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
|
||||
const monitor = setInterval(() => {
|
||||
const newRate = window.BorealisUpdateRate;
|
||||
if (newRate !== currentRate) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = setInterval(runNodeLogic, newRate);
|
||||
currentRate = newRate;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
clearInterval(monitor);
|
||||
};
|
||||
}, [id, edges, pattern, replacement, flags, enabled]);
|
||||
|
||||
return (
|
||||
<div className="borealis-node">
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
|
||||
<div className="borealis-node-header">Regex Replace</div>
|
||||
|
||||
<div className="borealis-node-content">
|
||||
<div style={{ marginBottom: "6px", fontSize: "9px", color: "#ccc" }}>
|
||||
Perform regex replacement on upstream string
|
||||
</div>
|
||||
|
||||
<label>Regex Pattern:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pattern}
|
||||
onChange={(e) => {
|
||||
setPattern(e.target.value);
|
||||
updateNodeData("pattern", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. \\d+"
|
||||
style={inputStyle}
|
||||
/>
|
||||
|
||||
<label>Replacement:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={replacement}
|
||||
onChange={(e) => {
|
||||
setReplacement(e.target.value);
|
||||
updateNodeData("replacement", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. $1"
|
||||
style={inputStyle}
|
||||
/>
|
||||
|
||||
<label>Regex Flags:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={flags}
|
||||
onChange={(e) => {
|
||||
setFlags(e.target.value);
|
||||
updateNodeData("flags", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. gi"
|
||||
style={inputStyle}
|
||||
/>
|
||||
|
||||
<div style={{ margin: "6px 0" }}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => {
|
||||
setEnabled(e.target.checked);
|
||||
updateNodeData("enabled", e.target.checked);
|
||||
}}
|
||||
style={{ marginRight: "6px" }}
|
||||
/>
|
||||
Enable Replacement
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>Original Input:</label>
|
||||
<textarea
|
||||
readOnly
|
||||
value={original}
|
||||
rows={2}
|
||||
style={textAreaStyle}
|
||||
/>
|
||||
|
||||
<label>Output:</label>
|
||||
<textarea
|
||||
readOnly
|
||||
value={result}
|
||||
rows={2}
|
||||
style={textAreaStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const inputStyle = {
|
||||
width: "100%",
|
||||
fontSize: "9px",
|
||||
background: "#1e1e1e",
|
||||
color: "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "3px",
|
||||
marginBottom: "6px"
|
||||
};
|
||||
|
||||
const textAreaStyle = {
|
||||
width: "100%",
|
||||
fontSize: "9px",
|
||||
background: "#2a2a2a",
|
||||
color: "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "3px",
|
||||
resize: "vertical",
|
||||
marginBottom: "6px"
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "RegexReplace",
|
||||
label: "Regex Replacer",
|
||||
description: `
|
||||
Enhanced Regex Replacer:
|
||||
- Add regex flags (g, i, m, etc)
|
||||
- Live preview of input vs output
|
||||
- Optional enable toggle for replacement logic
|
||||
`.trim(),
|
||||
content: "Perform regex replacement on upstream string",
|
||||
component: RegexReplaceNode
|
||||
};
|
@ -0,0 +1,124 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Analysis/Node_Regex_Search.jsx
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||
|
||||
if (!window.BorealisValueBus) window.BorealisValueBus = {};
|
||||
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
|
||||
|
||||
const RegexSearchNode = ({ id, data }) => {
|
||||
const edges = useStore((state) => state.edges);
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
const [pattern, setPattern] = useState(data?.pattern || "");
|
||||
const [flags, setFlags] = useState(data?.flags || "i");
|
||||
|
||||
const valueRef = useRef("0");
|
||||
|
||||
const updateNodeData = (key, val) => {
|
||||
setNodes(nds =>
|
||||
nds.map(n =>
|
||||
n.id === id ? { ...n, data: { ...n.data, [key]: val } } : n
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId = null;
|
||||
let currentRate = window.BorealisUpdateRate;
|
||||
|
||||
const runNodeLogic = () => {
|
||||
const inputEdge = edges.find((e) => e.target === id);
|
||||
const inputVal = inputEdge ? window.BorealisValueBus[inputEdge.source] || "" : "";
|
||||
|
||||
let matched = false;
|
||||
try {
|
||||
if (pattern) {
|
||||
const regex = new RegExp(pattern, flags);
|
||||
matched = regex.test(inputVal);
|
||||
}
|
||||
} catch {
|
||||
matched = false;
|
||||
}
|
||||
|
||||
const result = matched ? "1" : "0";
|
||||
|
||||
if (result !== valueRef.current) {
|
||||
valueRef.current = result;
|
||||
window.BorealisValueBus[id] = result;
|
||||
}
|
||||
};
|
||||
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
|
||||
const monitor = setInterval(() => {
|
||||
const newRate = window.BorealisUpdateRate;
|
||||
if (newRate !== currentRate) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = setInterval(runNodeLogic, newRate);
|
||||
currentRate = newRate;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
clearInterval(monitor);
|
||||
};
|
||||
}, [id, edges, pattern, flags]);
|
||||
|
||||
return (
|
||||
<div className="borealis-node">
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
<div className="borealis-node-header">Regex Search</div>
|
||||
<div className="borealis-node-content">
|
||||
<label>Regex Pattern:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pattern}
|
||||
onChange={(e) => {
|
||||
setPattern(e.target.value);
|
||||
updateNodeData("pattern", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. World"
|
||||
style={inputStyle}
|
||||
/>
|
||||
|
||||
<label>Regex Flags:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={flags}
|
||||
onChange={(e) => {
|
||||
setFlags(e.target.value);
|
||||
updateNodeData("flags", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. i"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const inputStyle = {
|
||||
width: "100%",
|
||||
fontSize: "9px",
|
||||
background: "#1e1e1e",
|
||||
color: "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "3px",
|
||||
marginBottom: "6px"
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "RegexSearch",
|
||||
label: "Regex Search",
|
||||
description: `
|
||||
Minimal RegEx Matcher:
|
||||
- Accepts pattern and flags
|
||||
- Outputs "1" if match is found, else "0"
|
||||
- No visual output display
|
||||
`.trim(),
|
||||
content: "Outputs '1' if regex matches input, otherwise '0'",
|
||||
component: RegexSearchNode
|
||||
};
|
@ -0,0 +1,190 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/nodes/Data Analysis/Node_TextArray_Display.jsx
|
||||
|
||||
/**
|
||||
* Display Multi-Line Array Node
|
||||
* --------------------------------------------------
|
||||
* A node to display upstream multi-line text arrays.
|
||||
* Has one input edge on left and passthrough output on right.
|
||||
* Custom drag-resize handle for width & height adjustments.
|
||||
* Inner textarea scrolls vertically; container overflow visible.
|
||||
*/
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||
|
||||
const TextArrayDisplayNode = ({ id, data }) => {
|
||||
const { setNodes } = useReactFlow();
|
||||
const edges = useStore((state) => state.edges);
|
||||
const containerRef = useRef(null);
|
||||
const resizingRef = useRef(false);
|
||||
const startPosRef = useRef({ x: 0, y: 0 });
|
||||
const startDimRef = useRef({ width: 0, height: 0 });
|
||||
|
||||
// Initialize lines and dimensions
|
||||
const [lines, setLines] = useState(data?.lines || []);
|
||||
const linesRef = useRef(lines);
|
||||
const initW = parseInt(data?.width || "300", 10);
|
||||
const initH = parseInt(data?.height || "150", 10);
|
||||
const [dimensions, setDimensions] = useState({ width: initW, height: initH });
|
||||
|
||||
// Persist dimensions to node data
|
||||
const persistDimensions = useCallback(() => {
|
||||
const w = `${Math.round(dimensions.width)}px`;
|
||||
const h = `${Math.round(dimensions.height)}px`;
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, width: w, height: h } }
|
||||
: n
|
||||
)
|
||||
);
|
||||
}, [dimensions, id, setNodes]);
|
||||
|
||||
// Mouse handlers for custom resize
|
||||
useEffect(() => {
|
||||
const onMouseMove = (e) => {
|
||||
if (!resizingRef.current) return;
|
||||
const dx = e.clientX - startPosRef.current.x;
|
||||
const dy = e.clientY - startPosRef.current.y;
|
||||
setDimensions({
|
||||
width: Math.max(100, startDimRef.current.width + dx),
|
||||
height: Math.max(60, startDimRef.current.height + dy)
|
||||
});
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
if (resizingRef.current) {
|
||||
resizingRef.current = false;
|
||||
persistDimensions();
|
||||
}
|
||||
};
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
}, [persistDimensions]);
|
||||
|
||||
// Start drag
|
||||
const onResizeMouseDown = (e) => {
|
||||
e.stopPropagation();
|
||||
resizingRef.current = true;
|
||||
startPosRef.current = { x: e.clientX, y: e.clientY };
|
||||
startDimRef.current = { ...dimensions };
|
||||
};
|
||||
|
||||
// Polling for upstream data
|
||||
useEffect(() => {
|
||||
let rate = window.BorealisUpdateRate;
|
||||
const tick = () => {
|
||||
const edge = edges.find((e) => e.target === id);
|
||||
if (edge && edge.source) {
|
||||
const arr = window.BorealisValueBus[edge.source] || [];
|
||||
if (JSON.stringify(arr) !== JSON.stringify(linesRef.current)) {
|
||||
linesRef.current = arr;
|
||||
setLines(arr);
|
||||
window.BorealisValueBus[id] = arr;
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, lines: arr } } : n
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
window.BorealisValueBus[id] = linesRef.current;
|
||||
}
|
||||
};
|
||||
const iv = setInterval(tick, rate);
|
||||
const monitor = setInterval(() => {
|
||||
if (window.BorealisUpdateRate !== rate) {
|
||||
clearInterval(iv);
|
||||
clearInterval(monitor);
|
||||
}
|
||||
}, 200);
|
||||
return () => { clearInterval(iv); clearInterval(monitor); };
|
||||
}, [id, edges, setNodes]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="borealis-node"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
overflow: "visible",
|
||||
position: "relative",
|
||||
boxSizing: "border-box"
|
||||
}}
|
||||
>
|
||||
{/* Connectors */}
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="borealis-node-header">
|
||||
{data?.label || "Display Multi-Line Array"}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className="borealis-node-content"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "4px",
|
||||
fontSize: "9px",
|
||||
color: "#ccc",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "4px" }}>
|
||||
{data?.content || "Display upstream multi-line text arrays."}
|
||||
</div>
|
||||
<label style={{ marginBottom: "4px" }}>Upstream Text Data:</label>
|
||||
<textarea
|
||||
value={lines.join("\n")}
|
||||
readOnly
|
||||
style={{
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
fontSize: "9px",
|
||||
background: "#1e1e1e",
|
||||
color: "#ccc",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "2px",
|
||||
padding: "4px",
|
||||
resize: "none",
|
||||
overflowY: "auto",
|
||||
boxSizing: "border-box"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Invisible drag-resize handle */}
|
||||
<div
|
||||
onMouseDown={onResizeMouseDown}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
right: "-4px",
|
||||
bottom: "-4px",
|
||||
cursor: "nwse-resize",
|
||||
background: "transparent",
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Export node metadata
|
||||
export default {
|
||||
type: "Node_TextArray_Display",
|
||||
label: "Display Multi-Line Array",
|
||||
description: "Display upstream multi-line text arrays.",
|
||||
content: "Display upstream multi-line text arrays.",
|
||||
component: TextArrayDisplayNode
|
||||
};
|
Reference in New Issue
Block a user