Compare commits

...

4 Commits

Author SHA1 Message Date
94d6359f91 Upgraded Agent Node 2025-05-30 05:20:06 -06:00
a999dae19c Upgraded Agent Role Screenshot Node 2025-05-30 05:17:22 -06:00
516618c0d2 Upgraded Regex Search Node 2025-05-30 05:09:55 -06:00
8d043f6a77 Upgraded Regex Replace Node 2025-05-30 05:07:05 -06:00
4 changed files with 431 additions and 339 deletions

View File

@ -1,89 +1,85 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Agent/Node_Agent_Role_Screenshot.jsx
import React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
import ShareIcon from "@mui/icons-material/Share";
import IconButton from "@mui/material/IconButton";
if (!window.BorealisValueBus) {
window.BorealisValueBus = {};
}
/*
Agent Role: Screenshot Node (Modern, Sidebar Config Enabled)
if (!window.BorealisUpdateRate) {
window.BorealisUpdateRate = 100;
}
- Defines a screenshot region to be captured by a remote Borealis Agent.
- Pushes live base64 PNG data to downstream nodes.
- Region coordinates (x, y, w, h), visibility, overlay label, and interval are all persisted and synchronized.
- All configuration is moved to the right sidebar (Node Properties).
- Maintains full bi-directional write-back of coordinates and overlay settings.
*/
const ScreenshotInstructionNode = ({ id, data }) => {
if (!window.BorealisValueBus) window.BorealisValueBus = {};
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
const AgentScreenshotNode = ({ id, data }) => {
const { setNodes, getNodes } = useReactFlow();
const edges = useStore(state => state.edges);
const [interval, setInterval] = useState(data?.interval || 1000);
const [region, setRegion] = useState({
x: data?.x ?? 250,
y: data?.y ?? 100,
w: data?.w ?? 300,
h: data?.h ?? 200,
});
const [visible, setVisible] = useState(data?.visible ?? true);
const [alias, setAlias] = useState(data?.alias || "");
const [imageBase64, setImageBase64] = useState("");
// Core config values pulled from sidebar config (with defaults)
const interval = parseInt(data?.interval || 1000, 10) || 1000;
const region = {
x: parseInt(data?.x ?? 250, 10),
y: parseInt(data?.y ?? 100, 10),
w: parseInt(data?.w ?? 300, 10),
h: parseInt(data?.h ?? 200, 10)
};
const visible = (data?.visible ?? "true") === "true";
const alias = data?.alias || "";
const [imageBase64, setImageBase64] = useState(data?.value || "");
const base64Ref = useRef("");
const regionRef = useRef(region);
// Push current state into BorealisValueBus at intervals
// Always push current imageBase64 into BorealisValueBus at the global update rate
useEffect(() => {
const intervalId = setInterval(() => {
const val = base64Ref.current;
if (val) {
window.BorealisValueBus[id] = val;
if (imageBase64) {
window.BorealisValueBus[id] = imageBase64;
setNodes(nds =>
nds.map(n =>
n.id === id ? { ...n, data: { ...n.data, value: val } } : n
n.id === id ? { ...n, data: { ...n.data, value: imageBase64 } } : n
)
);
}
}, window.BorealisUpdateRate || 100);
return () => clearInterval(intervalId);
}, [id, setNodes]);
}, [id, imageBase64, setNodes]);
// Listen for agent screenshot + overlay updates
// Listen for agent screenshot and overlay region updates
useEffect(() => {
const socket = window.BorealisSocket;
if (!socket) return;
const handleScreenshot = (payload) => {
if (payload?.node_id !== id) return;
if (payload?.node_id !== id) return;
// image update (optional)
if (payload.image_base64) {
base64Ref.current = payload.image_base64;
if (payload.image_base64) {
setImageBase64(payload.image_base64);
window.BorealisValueBus[id] = payload.image_base64;
}
// geometry update
const { x, y, w, h } = payload;
if (x !== undefined && y !== undefined && w !== undefined && h !== undefined) {
const newRegion = { x, y, w, h };
const prev = regionRef.current;
const changed = Object.entries(newRegion).some(([k, v]) => prev[k] !== v);
if (changed) {
regionRef.current = newRegion;
setRegion(newRegion);
setNodes(nds =>
nds.map(n =>
n.id === id ? { ...n, data: { ...n.data, ...newRegion } } : n
)
);
}
}
}
const { x, y, w, h } = payload;
if (
x !== undefined &&
y !== undefined &&
w !== undefined &&
h !== undefined
) {
setNodes(nds =>
nds.map(n =>
n.id === id ? { ...n, data: { ...n.data, x, y, w, h } } : n
)
);
}
};
socket.on("agent_screenshot_task", handleScreenshot);
return () => socket.off("agent_screenshot_task", handleScreenshot);
}, [id, setNodes]);
// Bi-directional instruction export
// Register this node for the agent provisioning sync
window.__BorealisInstructionNodes = window.__BorealisInstructionNodes || {};
window.__BorealisInstructionNodes[id] = () => ({
node_id: id,
@ -91,10 +87,10 @@ const ScreenshotInstructionNode = ({ id, data }) => {
interval,
visible,
alias,
...regionRef.current
...region
});
// Manual live view copy
// Manual live view copy button
const handleCopyLiveViewLink = () => {
const agentEdge = edges.find(e => e.target === id && e.sourceHandle === "provisioner");
const agentNode = getNodes().find(n => n.id === agentEdge?.source);
@ -111,71 +107,34 @@ const ScreenshotInstructionNode = ({ id, data }) => {
.catch(err => console.error("Clipboard copy failed:", err));
};
// Node card UI - config handled in sidebar
return (
<div className="borealis-node" style={{ position: "relative" }}>
<Handle type="target" position={Position.Left} className="borealis-handle" />
<Handle type="source" position={Position.Right} className="borealis-handle" />
<div className="borealis-node-header">Agent Role: Screenshot</div>
<div className="borealis-node-header">
{data?.label || "Agent Role: Screenshot"}
</div>
<div className="borealis-node-content" style={{ fontSize: "9px" }}>
<label>Update Interval (ms):</label>
<input
type="number"
min="100"
step="100"
value={interval}
onChange={(e) => setInterval(Number(e.target.value))}
style={{ width: "100%", marginBottom: "4px" }}
/>
<label>Region X / Y / W / H:</label>
<div style={{ display: "flex", gap: "4px", marginBottom: "4px" }}>
<input type="number" value={region.x} onChange={(e) => {
const x = Number(e.target.value);
const updated = { ...region, x }; setRegion(updated); regionRef.current = updated;
}} style={{ width: "25%" }} />
<input type="number" value={region.y} onChange={(e) => {
const y = Number(e.target.value);
const updated = { ...region, y }; setRegion(updated); regionRef.current = updated;
}} style={{ width: "25%" }} />
<input type="number" value={region.w} onChange={(e) => {
const w = Number(e.target.value);
const updated = { ...region, w }; setRegion(updated); regionRef.current = updated;
}} style={{ width: "25%" }} />
<input type="number" value={region.h} onChange={(e) => {
const h = Number(e.target.value);
const updated = { ...region, h }; setRegion(updated); regionRef.current = updated;
}} style={{ width: "25%" }} />
<div>
<b>Region:</b> X:{region.x} Y:{region.y} W:{region.w} H:{region.h}
</div>
<div style={{ marginBottom: "4px" }}>
<label>
<input
type="checkbox"
checked={visible}
onChange={() => setVisible(!visible)}
style={{ marginRight: "4px" }}
/>
Show Overlay on Agent
</label>
<div>
<b>Interval:</b> {interval} ms
</div>
<div>
<b>Overlay:</b> {visible ? "Yes" : "No"}
</div>
<div>
<b>Label:</b> {alias || <span style={{ color: "#666" }}>none</span>}
</div>
<label>Overlay Label:</label>
<input
type="text"
value={alias}
onChange={(e) => setAlias(e.target.value)}
placeholder="Label (optional)"
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
/>
<div style={{ textAlign: "center", fontSize: "8px", color: "#aaa" }}>
{imageBase64
? `Last image: ${Math.round(imageBase64.length / 1024)} KB`
: "Awaiting Screenshot Data..."}
</div>
</div>
<div style={{ position: "absolute", top: 4, right: 4 }}>
<IconButton size="small" onClick={handleCopyLiveViewLink}>
<ShareIcon style={{ fontSize: 14 }} />
@ -185,16 +144,90 @@ const ScreenshotInstructionNode = ({ id, data }) => {
);
};
// Node registration for Borealis catalog (sidebar config enabled)
export default {
type: "Agent_Role_Screenshot",
label: "Agent Role: Screenshot",
description: `
Agent Role Node: Screenshot Region
Capture a live screenshot of a defined region from a remote Borealis Agent.
- Defines a single region capture role
- Allows custom update interval and overlay
- Emits captured base64 PNG data from agent
- Define region (X, Y, Width, Height)
- Select update interval (ms)
- Optionally show a visual overlay with a label
- Pushes base64 PNG stream to downstream nodes
- Use copy button to share live view URL
`.trim(),
content: "Capture screenshot region via agent",
component: ScreenshotInstructionNode
component: AgentScreenshotNode,
config: [
{
key: "interval",
label: "Update Interval (ms)",
type: "text",
defaultValue: "1000"
},
{
key: "x",
label: "Region X",
type: "text",
defaultValue: "250"
},
{
key: "y",
label: "Region Y",
type: "text",
defaultValue: "100"
},
{
key: "w",
label: "Region Width",
type: "text",
defaultValue: "300"
},
{
key: "h",
label: "Region Height",
type: "text",
defaultValue: "200"
},
{
key: "visible",
label: "Show Overlay on Agent",
type: "select",
options: ["true", "false"],
defaultValue: "true"
},
{
key: "alias",
label: "Overlay Label",
type: "text",
defaultValue: ""
}
],
usage_documentation: `
### Agent Role: Screenshot Node
This node defines a screenshot-capture role for a Borealis Agent.
**How It Works**
- The region (X, Y, W, H) is sent to the Agent for real-time screenshot capture.
- The interval determines how often the Agent captures and pushes new images.
- Optionally, an overlay with a label can be displayed on the Agent's screen for visual feedback.
- The captured screenshot (as a base64 PNG) is available to downstream nodes.
- Use the share button to copy a live viewing URL for the screenshot stream.
**Configuration**
- All fields are edited via the right sidebar.
- Coordinates update live if region is changed from the Agent.
**Warning**
- Changing region from the Agent UI will update this node's coordinates.
- Do not remove the bi-directional region write-back: if the region moves, this node updates immediately.
**Example Use Cases**
- Automated visual QA (comparing regions of apps)
- OCR on live application windows
- Remote monitoring dashboards
`.trim()
};

View File

@ -2,6 +2,7 @@
import React, { useEffect, useState, useCallback, useMemo, useRef } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
// Modern Node: Borealis Agent (Sidebar Config Enabled)
const BorealisAgentNode = ({ id, data }) => {
const { getNodes, setNodes } = useReactFlow();
const edges = useStore((state) => state.edges);
@ -10,7 +11,7 @@ const BorealisAgentNode = ({ id, data }) => {
const [isConnected, setIsConnected] = useState(false);
const prevRolesRef = useRef([]);
// ---------------- Agent List & Sorting ----------------
// Agent List Sorted (Online First)
const agentList = useMemo(() => {
if (!agents || typeof agents !== "object") return [];
return Object.entries(agents)
@ -23,7 +24,7 @@ const BorealisAgentNode = ({ id, data }) => {
.sort((a, b) => b.last_seen - a.last_seen);
}, [agents]);
// ---------------- Periodic Agent Fetching ----------------
// Fetch Agents Periodically
useEffect(() => {
const fetchAgents = () => {
fetch("/api/agents")
@ -36,7 +37,7 @@ const BorealisAgentNode = ({ id, data }) => {
return () => clearInterval(interval);
}, []);
// ---------------- Node Data Sync ----------------
// Sync node data with sidebar changes
useEffect(() => {
setNodes((nds) =>
nds.map((n) =>
@ -44,9 +45,9 @@ const BorealisAgentNode = ({ id, data }) => {
)
);
setIsConnected(false);
}, [selectedAgent]);
}, [selectedAgent, setNodes, id]);
// ---------------- Attached Role Collection ----------------
// Attached Roles logic
const attachedRoleIds = useMemo(
() =>
edges
@ -54,7 +55,6 @@ const BorealisAgentNode = ({ id, data }) => {
.map((e) => e.target),
[edges, id]
);
const getAttachedRoles = useCallback(() => {
const allNodes = getNodes();
return attachedRoleIds
@ -65,9 +65,9 @@ const BorealisAgentNode = ({ id, data }) => {
.filter((r) => r);
}, [attachedRoleIds, getNodes]);
// ---------------- Provision Role Logic ----------------
// Provision Roles to Agent
const provisionRoles = useCallback((roles) => {
if (!selectedAgent) return; // Allow empty roles but require agent
if (!selectedAgent) return;
fetch("/api/agent/provision", {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -79,12 +79,10 @@ const BorealisAgentNode = ({ id, data }) => {
})
.catch(() => {});
}, [selectedAgent]);
const handleConnect = useCallback(() => {
const roles = getAttachedRoles();
provisionRoles(roles); // Always call even with empty roles
provisionRoles(roles);
}, [getAttachedRoles, provisionRoles]);
const handleDisconnect = useCallback(() => {
if (!selectedAgent) return;
fetch("/api/agent/provision", {
@ -99,7 +97,7 @@ const BorealisAgentNode = ({ id, data }) => {
.catch(() => {});
}, [selectedAgent]);
// ---------------- Auto-Provision When Roles Change ----------------
// Auto-provision on role change
useEffect(() => {
const newRoles = getAttachedRoles();
const prevSerialized = JSON.stringify(prevRolesRef.current || []);
@ -109,7 +107,7 @@ const BorealisAgentNode = ({ id, data }) => {
}
}, [attachedRoleIds, isConnected, getAttachedRoles, provisionRoles]);
// ---------------- Status Label ----------------
// Status Label
const selectedAgentStatus = useMemo(() => {
if (!selectedAgent) return "Unassigned";
const agent = agents[selectedAgent];
@ -117,7 +115,7 @@ const BorealisAgentNode = ({ id, data }) => {
return agent.status === "provisioned" ? "Connected" : "Available";
}, [agents, selectedAgent]);
// ---------------- Render ----------------
// Render (Sidebar handles config)
return (
<div className="borealis-node">
<Handle
@ -173,16 +171,51 @@ const BorealisAgentNode = ({ id, data }) => {
);
};
// Node Registration Object with sidebar config and docs
export default {
type: "Borealis_Agent",
label: "Borealis Agent",
description: `
Main Agent Node
- Selects an available agent
- Connect/disconnects via button
- Auto-updates roles when attached roles change
Select and connect to a remote Borealis Agent.
- Assign roles to agent dynamically by connecting "Agent Role" nodes.
- Auto-provisions agent as role assignments change.
- See live agent status and re-connect/disconnect easily.
`.trim(),
content: "Select and manage an Agent with dynamic roles",
component: BorealisAgentNode,
config: [
{
key: "agent_id",
label: "Agent",
type: "text", // NOTE: UI populates via agent fetch, but config drives default for sidebar.
defaultValue: ""
}
],
usage_documentation: `
### Borealis Agent Node
This node represents an available Borealis Agent (Python client) you can control from your workflow.
#### Features
- **Select** an agent from the list of online agents.
- **Connect/Disconnect** from the agent at any time.
- **Attach roles** (by connecting "Agent Role" nodes to this node's output handle) to assign behaviors dynamically.
- **Live status** shows if the agent is available, connected, or offline.
#### How to Use
1. **Drag in a Borealis Agent node.**
2. **Pick an agent** from the dropdown list (auto-populates from backend).
3. **Click "Connect to Agent"** to provision it for the workflow.
4. **Attach Agent Role Nodes** (e.g., Screenshot, Macro Keypress) to the "provisioner" output handle to define what the agent should do.
5. Agent will automatically update its roles as you change connected Role Nodes.
#### Output Handle
- "provisioner" (bottom): Connect Agent Role nodes here.
#### Good to Know
- If an agent disconnects or goes offline, its status will show "Reconnecting..." until it returns.
- Node config can be edited in the right sidebar.
- **Roles update LIVE**: Any time you change attached roles, the agent gets updated instantly.
`.trim()
};

View File

@ -2,30 +2,20 @@
import React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
// Shared memory bus setup
if (!window.BorealisValueBus) window.BorealisValueBus = {};
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
// -- Modern Regex Replace Node -- //
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);
// Maintain output live value
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;
@ -39,11 +29,11 @@ const RegexReplaceNode = ({ id, data }) => {
setOriginal(inputValue);
let newVal = inputValue;
try {
if (enabled && pattern) {
const regex = new RegExp(pattern, flags);
let safeReplacement = replacement.trim();
if ((data?.enabled ?? true) && data?.pattern) {
const regex = new RegExp(data.pattern, data.flags || "g");
let safeReplacement = (data.replacement ?? "").trim();
// Remove quotes if user adds them
if (
safeReplacement.startsWith('"') &&
safeReplacement.endsWith('"')
@ -65,6 +55,7 @@ const RegexReplaceNode = ({ id, data }) => {
intervalId = setInterval(runNodeLogic, currentRate);
// Monitor update rate changes
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate;
if (newRate !== currentRate) {
@ -78,84 +69,57 @@ const RegexReplaceNode = ({ id, data }) => {
clearInterval(intervalId);
clearInterval(monitor);
};
}, [id, edges, pattern, replacement, flags, enabled]);
}, [id, edges, data?.pattern, data?.replacement, data?.flags, data?.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-header">
{data?.label || "Regex Replace"}
</div>
<div className="borealis-node-content">
<div style={{ marginBottom: "6px", fontSize: "9px", color: "#ccc" }}>
Perform regex replacement on upstream string
Performs live regex-based find/replace on incoming string value.
</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 style={{ fontSize: "9px", color: "#ccc", marginBottom: 2 }}>
<b>Pattern:</b> {data?.pattern || <i>(not set)</i>}<br />
<b>Flags:</b> {data?.flags || "g"}<br />
<b>Enabled:</b> {(data?.enabled ?? true) ? "Yes" : "No"}
</div>
<label>Original Input:</label>
<label style={{ fontSize: "8px", color: "#888" }}>Original:</label>
<textarea
readOnly
value={original}
rows={2}
style={textAreaStyle}
style={{
width: "100%",
fontSize: "9px",
background: "#222",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
padding: "3px",
resize: "vertical",
marginBottom: "6px"
}}
/>
<label>Output:</label>
<label style={{ fontSize: "8px", color: "#888" }}>Output:</label>
<textarea
readOnly
value={result}
rows={2}
style={textAreaStyle}
style={{
width: "100%",
fontSize: "9px",
background: "#2a2a2a",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
padding: "3px",
resize: "vertical"
}}
/>
</div>
@ -164,38 +128,84 @@ const RegexReplaceNode = ({ id, data }) => {
);
};
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"
};
// Modern Node Export: Sidebar config, usage docs, sensible defaults
export default {
type: "RegexReplace",
label: "Regex Replacer",
label: "Regex Replace",
description: `
Enhanced Regex Replacer:
- Add regex flags (g, i, m, etc)
- Live preview of input vs output
- Optional enable toggle for replacement logic
Live regex-based string find/replace node.
- Runs a JavaScript regular expression on every input update.
- Useful for cleanup, format fixes, redacting, token extraction.
- Configurable flags, replacement text, and enable toggle.
- Handles errors gracefully, shows live preview in the sidebar.
`.trim(),
content: "Perform regex replacement on upstream string",
component: RegexReplaceNode
content: "Perform regex replacement on incoming string",
component: RegexReplaceNode,
config: [
{
key: "pattern",
label: "Regex Pattern",
type: "text",
defaultValue: "\\d+"
},
{
key: "replacement",
label: "Replacement",
type: "text",
defaultValue: ""
},
{
key: "flags",
label: "Regex Flags",
type: "text",
defaultValue: "g"
},
{
key: "enabled",
label: "Enable Replacement",
type: "select",
options: ["true", "false"],
defaultValue: "true"
}
],
usage_documentation: `
### Regex Replace Node
**Purpose:**
Perform flexible find-and-replace on strings using JavaScript-style regular expressions.
#### Typical Use Cases
- Clean up text, numbers, or IDs in a data stream
- Mask or redact sensitive info (emails, credit cards, etc)
- Extract tokens, words, or reformat content
#### Configuration (see "Config" tab):
- **Regex Pattern**: The search pattern (supports capture groups)
- **Replacement**: The replacement string. You can use \`$1, $2\` for capture groups.
- **Regex Flags**: Default \`g\` (global). Add \`i\` (case-insensitive), \`m\` (multiline), etc.
- **Enable Replacement**: On/Off toggle (for easy debugging)
#### Behavior
- Upstream value is live-updated.
- When enabled, node applies the regex and emits the result downstream.
- Shows both input and output in the sidebar for debugging.
- If the regex is invalid, error is displayed as output.
#### Output
- Emits the transformed string to all downstream nodes.
- Updates in real time at the global Borealis update rate.
#### Example
Pattern: \`(\\d+)\`
Replacement: \`[number:$1]\`
Input: \`abc 123 def 456\`
Output: \`abc [number:123] def [number:456]\`
---
**Tips:**
- Use double backslashes (\\) in patterns when needed (e.g. \`\\\\d+\`).
- Flags can be any combination (e.g. \`gi\`).
`.trim()
};

View File

@ -1,124 +1,140 @@
////////// 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 React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
// Modern Regex Search Node: Config via Sidebar
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 { setNodes } = useReactFlow();
const edges = useStore((state) => state.edges);
const [pattern, setPattern] = useState(data?.pattern || "");
const [flags, setFlags] = useState(data?.flags || "i");
// Pattern/flags always come from sidebar config (with defaults)
const pattern = data?.pattern ?? "";
const flags = data?.flags ?? "i";
const valueRef = useRef("0");
const valueRef = useRef("0");
const [matched, setMatched] = useState("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 matchResult = false;
try {
if (pattern) {
const regex = new RegExp(pattern, flags);
matchResult = regex.test(inputVal);
}
} catch {
matchResult = false;
}
const result = matchResult ? "1" : "0";
if (result !== valueRef.current) {
valueRef.current = result;
setMatched(result);
window.BorealisValueBus[id] = result;
setNodes((nds) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, match: result } } : n
)
);
}
};
useEffect(() => {
let intervalId = null;
let currentRate = window.BorealisUpdateRate;
intervalId = setInterval(runNodeLogic, currentRate);
const runNodeLogic = () => {
const inputEdge = edges.find((e) => e.target === id);
const inputVal = inputEdge ? window.BorealisValueBus[inputEdge.source] || "" : "";
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate;
if (newRate !== currentRate) {
clearInterval(intervalId);
intervalId = setInterval(runNodeLogic, newRate);
currentRate = newRate;
}
}, 300);
let matched = false;
try {
if (pattern) {
const regex = new RegExp(pattern, flags);
matched = regex.test(inputVal);
}
} catch {
matched = false;
}
return () => {
clearInterval(intervalId);
clearInterval(monitor);
};
}, [id, edges, pattern, flags, setNodes]);
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"
return (
<div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" />
<div className="borealis-node-header">
{data?.label || "Regex Search"}
</div>
<div className="borealis-node-content" style={{ fontSize: "9px", color: "#ccc" }}>
Match: {matched}
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
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
type: "RegexSearch",
label: "Regex Search",
description: `
Test for text matches with a regular expression pattern.
- Accepts a regex pattern and flags (e.g. "i", "g", "m")
- Connect any node to the input to test its value.
- Outputs "1" if the regex matches, otherwise "0".
- Useful for input validation, filtering, or text triggers.
`.trim(),
content: "Outputs '1' if regex matches input, otherwise '0'",
component: RegexSearchNode,
config: [
{
key: "pattern",
label: "Regex Pattern",
type: "text",
defaultValue: "",
placeholder: "e.g. World"
},
{
key: "flags",
label: "Regex Flags",
type: "text",
defaultValue: "i",
placeholder: "e.g. i"
}
],
usage_documentation: `
### Regex Search Node
This node tests its input value against a user-supplied regular expression pattern.
**Configuration (Sidebar):**
- **Regex Pattern**: Standard JavaScript regex pattern.
- **Regex Flags**: Any combination of \`i\` (ignore case), \`g\` (global), \`m\` (multiline), etc.
**Input:**
- Accepts a string from any upstream node.
**Output:**
- Emits "1" if the pattern matches the input string.
- Emits "0" if there is no match or the pattern/flags are invalid.
**Common Uses:**
- Search for words/phrases in extracted text.
- Filter values using custom patterns.
- Create triggers based on input structure (e.g. validate an email, detect phone numbers, etc).
#### Example:
- **Pattern:** \`World\`
- **Flags:** \`i\`
- **Input:** \`Hello world!\`
- **Output:** \`1\` (matched, case-insensitive)
`.trim()
};