Upgraded Agent Role Screenshot Node

This commit is contained in:
Nicole Rappe 2025-05-30 05:17:22 -06:00
parent 516618c0d2
commit a999dae19c

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 React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow"; import { Handle, Position, useReactFlow, useStore } from "reactflow";
import ShareIcon from "@mui/icons-material/Share"; import ShareIcon from "@mui/icons-material/Share";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
if (!window.BorealisValueBus) { /*
window.BorealisValueBus = {}; Agent Role: Screenshot Node (Modern, Sidebar Config Enabled)
}
if (!window.BorealisUpdateRate) { - Defines a screenshot region to be captured by a remote Borealis Agent.
window.BorealisUpdateRate = 100; - 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 { setNodes, getNodes } = useReactFlow();
const edges = useStore(state => state.edges); const edges = useStore(state => state.edges);
const [interval, setInterval] = useState(data?.interval || 1000); // Core config values pulled from sidebar config (with defaults)
const [region, setRegion] = useState({ const interval = parseInt(data?.interval || 1000, 10) || 1000;
x: data?.x ?? 250, const region = {
y: data?.y ?? 100, x: parseInt(data?.x ?? 250, 10),
w: data?.w ?? 300, y: parseInt(data?.y ?? 100, 10),
h: data?.h ?? 200, w: parseInt(data?.w ?? 300, 10),
}); h: parseInt(data?.h ?? 200, 10)
const [visible, setVisible] = useState(data?.visible ?? true); };
const [alias, setAlias] = useState(data?.alias || ""); const visible = (data?.visible ?? "true") === "true";
const [imageBase64, setImageBase64] = useState(""); const alias = data?.alias || "";
const [imageBase64, setImageBase64] = useState(data?.value || "");
const base64Ref = useRef(""); // Always push current imageBase64 into BorealisValueBus at the global update rate
const regionRef = useRef(region);
// Push current state into BorealisValueBus at intervals
useEffect(() => { useEffect(() => {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const val = base64Ref.current; if (imageBase64) {
if (val) { window.BorealisValueBus[id] = imageBase64;
window.BorealisValueBus[id] = val;
setNodes(nds => setNodes(nds =>
nds.map(n => 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); }, window.BorealisUpdateRate || 100);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [id, setNodes]); }, [id, imageBase64, setNodes]);
// Listen for agent screenshot + overlay updates // Listen for agent screenshot and overlay region updates
useEffect(() => { useEffect(() => {
const socket = window.BorealisSocket; const socket = window.BorealisSocket;
if (!socket) return; if (!socket) return;
const handleScreenshot = (payload) => { const handleScreenshot = (payload) => {
if (payload?.node_id !== id) return; if (payload?.node_id !== id) return;
// image update (optional) if (payload.image_base64) {
if (payload.image_base64) {
base64Ref.current = payload.image_base64;
setImageBase64(payload.image_base64); setImageBase64(payload.image_base64);
window.BorealisValueBus[id] = payload.image_base64; window.BorealisValueBus[id] = payload.image_base64;
} }
const { x, y, w, h } = payload;
// geometry update if (
const { x, y, w, h } = payload; x !== undefined &&
if (x !== undefined && y !== undefined && w !== undefined && h !== undefined) { y !== undefined &&
const newRegion = { x, y, w, h }; w !== undefined &&
const prev = regionRef.current; h !== undefined
const changed = Object.entries(newRegion).some(([k, v]) => prev[k] !== v); ) {
setNodes(nds =>
if (changed) { nds.map(n =>
regionRef.current = newRegion; n.id === id ? { ...n, data: { ...n.data, x, y, w, h } } : n
setRegion(newRegion); )
setNodes(nds => );
nds.map(n => }
n.id === id ? { ...n, data: { ...n.data, ...newRegion } } : n
)
);
}
}
}; };
socket.on("agent_screenshot_task", handleScreenshot); socket.on("agent_screenshot_task", handleScreenshot);
return () => socket.off("agent_screenshot_task", handleScreenshot); return () => socket.off("agent_screenshot_task", handleScreenshot);
}, [id, setNodes]); }, [id, setNodes]);
// Bi-directional instruction export // Register this node for the agent provisioning sync
window.__BorealisInstructionNodes = window.__BorealisInstructionNodes || {}; window.__BorealisInstructionNodes = window.__BorealisInstructionNodes || {};
window.__BorealisInstructionNodes[id] = () => ({ window.__BorealisInstructionNodes[id] = () => ({
node_id: id, node_id: id,
@ -91,10 +87,10 @@ const ScreenshotInstructionNode = ({ id, data }) => {
interval, interval,
visible, visible,
alias, alias,
...regionRef.current ...region
}); });
// Manual live view copy // Manual live view copy button
const handleCopyLiveViewLink = () => { const handleCopyLiveViewLink = () => {
const agentEdge = edges.find(e => e.target === id && e.sourceHandle === "provisioner"); const agentEdge = edges.find(e => e.target === id && e.sourceHandle === "provisioner");
const agentNode = getNodes().find(n => n.id === agentEdge?.source); 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)); .catch(err => console.error("Clipboard copy failed:", err));
}; };
// Node card UI - config handled in sidebar
return ( return (
<div className="borealis-node" style={{ position: "relative" }}> <div className="borealis-node" style={{ position: "relative" }}>
<Handle type="target" position={Position.Left} className="borealis-handle" /> <Handle type="target" position={Position.Left} className="borealis-handle" />
<Handle type="source" position={Position.Right} 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" }}> <div className="borealis-node-content" style={{ fontSize: "9px" }}>
<label>Update Interval (ms):</label> <div>
<input <b>Region:</b> X:{region.x} Y:{region.y} W:{region.w} H:{region.h}
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> </div>
<div>
<div style={{ marginBottom: "4px" }}> <b>Interval:</b> {interval} ms
<label> </div>
<input <div>
type="checkbox" <b>Overlay:</b> {visible ? "Yes" : "No"}
checked={visible} </div>
onChange={() => setVisible(!visible)} <div>
style={{ marginRight: "4px" }} <b>Label:</b> {alias || <span style={{ color: "#666" }}>none</span>}
/>
Show Overlay on Agent
</label>
</div> </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" }}> <div style={{ textAlign: "center", fontSize: "8px", color: "#aaa" }}>
{imageBase64 {imageBase64
? `Last image: ${Math.round(imageBase64.length / 1024)} KB` ? `Last image: ${Math.round(imageBase64.length / 1024)} KB`
: "Awaiting Screenshot Data..."} : "Awaiting Screenshot Data..."}
</div> </div>
</div> </div>
<div style={{ position: "absolute", top: 4, right: 4 }}> <div style={{ position: "absolute", top: 4, right: 4 }}>
<IconButton size="small" onClick={handleCopyLiveViewLink}> <IconButton size="small" onClick={handleCopyLiveViewLink}>
<ShareIcon style={{ fontSize: 14 }} /> <ShareIcon style={{ fontSize: 14 }} />
@ -185,16 +144,90 @@ const ScreenshotInstructionNode = ({ id, data }) => {
); );
}; };
// Node registration for Borealis catalog (sidebar config enabled)
export default { export default {
type: "Agent_Role_Screenshot", type: "Agent_Role_Screenshot",
label: "Agent Role: Screenshot", label: "Agent Role: Screenshot",
description: ` 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 - Define region (X, Y, Width, Height)
- Allows custom update interval and overlay - Select update interval (ms)
- Emits captured base64 PNG data from agent - Optionally show a visual overlay with a label
- Pushes base64 PNG stream to downstream nodes
- Use copy button to share live view URL
`.trim(), `.trim(),
content: "Capture screenshot region via agent", 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()
}; };