Compare commits
4 Commits
651a5ce92b
...
94d6359f91
Author | SHA1 | Date | |
---|---|---|---|
94d6359f91 | |||
a999dae19c | |||
516618c0d2 | |||
8d043f6a77 |
@ -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()
|
||||
};
|
||||
|
@ -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()
|
||||
};
|
||||
|
@ -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()
|
||||
};
|
||||
|
@ -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()
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user