- Implemented Agent-based Data Collection Nodes

- Added More Dark Theming throughout Borealis
- Added API Data Collector Node
- Added Image Viewer Node
- Added Agent Deployment Script (Powershell)
This commit is contained in:
2025-04-04 03:23:01 -06:00
parent dca79b8556
commit a75c472c98
14 changed files with 693 additions and 222 deletions

View File

@ -451,7 +451,7 @@ export default function App() {
}}
sx={{ color: "#58a6ff", borderColor: "#58a6ff", fontSize: "0.75rem", textTransform: "none", px: 1.5 }}
>
Update Rate
Apply Rate
</Button>
</Box>
</Box>

View File

@ -61,4 +61,18 @@
background: #58a6ff;
width: 10px;
height: 10px;
}
}
/* Global dark form inputs */
input, select, button {
background-color: #2a2a2a;
color: #ccc;
border: 1px solid #444;
font-size: 10px;
}
/* Label / Dark Text styling */
label {
color: #aaa;
font-size: 10px;
}

View File

@ -0,0 +1,177 @@
import React, { useEffect, useState } from "react";
import { Handle, Position, useReactFlow } from "reactflow";
const APINode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const [agents, setAgents] = useState([]);
const [selectedAgent, setSelectedAgent] = useState(data.agent_id || "");
const [selectedType, setSelectedType] = useState(data.data_type || "screenshot");
const [imageData, setImageData] = useState("");
const [intervalMs, setIntervalMs] = useState(data.interval || 1000);
const [paused, setPaused] = useState(false);
const [overlayVisible, setOverlayVisible] = useState(true);
// Refresh agents every 5s
useEffect(() => {
const fetchAgents = () => fetch("/api/agents").then(res => res.json()).then(setAgents);
fetchAgents();
const interval = setInterval(fetchAgents, 5000);
return () => clearInterval(interval);
}, []);
// Pull image if agent provisioned
useEffect(() => {
if (!selectedAgent || paused) return;
const interval = setInterval(() => {
fetch(`/api/agent/image?agent_id=${selectedAgent}`)
.then(res => res.json())
.then(json => {
if (json.image_base64) {
setImageData(json.image_base64);
window.BorealisValueBus = window.BorealisValueBus || {};
window.BorealisValueBus[id] = json.image_base64;
}
})
.catch(() => { });
}, intervalMs);
return () => clearInterval(interval);
}, [selectedAgent, id, paused, intervalMs]);
const provisionAgent = () => {
if (!selectedAgent) return;
fetch("/api/agent/provision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_id: selectedAgent,
x: 250,
y: 100,
w: 300,
h: 200,
interval: intervalMs,
visible: overlayVisible,
task: selectedType
})
}).then(() => {
setNodes(nds =>
nds.map(n => n.id === id
? {
...n,
data: {
...n.data,
agent_id: selectedAgent,
data_type: selectedType,
interval: intervalMs
}
}
: n
)
);
});
};
const resetAgent = () => {
if (!selectedAgent) return;
fetch("/api/agent/reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: selectedAgent })
}).then(() => {
setSelectedAgent("");
});
};
const toggleOverlay = () => {
const newVisibility = !overlayVisible;
setOverlayVisible(newVisibility);
if (selectedAgent) {
fetch("/api/agent/overlay_visibility", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: selectedAgent, visible: newVisibility })
});
}
};
return (
<div className="borealis-node">
<Handle type="source" position={Position.Right} className="borealis-handle" />
<div className="borealis-node-header">API Data Collector</div>
<div className="borealis-node-content">
<label style={{ fontSize: "10px" }}>Agent:</label>
<select
value={selectedAgent}
onChange={(e) => setSelectedAgent(e.target.value)}
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
>
<option value="">-- Select --</option>
{Object.entries(agents).map(([id, info]) => (
<option key={id} value={id} disabled={info.status === "provisioned"}>
{id} {info.status === "provisioned" ? "(Adopted)" : ""}
</option>
))}
</select>
<label style={{ fontSize: "10px" }}>Data Type:</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
>
<option value="screenshot">Screenshot Region</option>
</select>
<label style={{ fontSize: "10px" }}>Update Rate (ms):</label>
<input
type="number"
min="100"
step="100"
value={intervalMs}
onChange={(e) => setIntervalMs(Number(e.target.value))}
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
/>
<div style={{ marginBottom: "6px" }}>
<label style={{ fontSize: "10px" }}>
<input
type="checkbox"
checked={paused}
onChange={() => setPaused(!paused)}
style={{ marginRight: "4px" }}
/>
Pause Data Collection
</label>
</div>
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap" }}>
<button
style={{ flex: 1, fontSize: "9px" }}
onClick={provisionAgent}
>
Provision
</button>
<button
style={{ flex: 1, fontSize: "9px" }}
onClick={resetAgent}
>
Reset Agent
</button>
<button
style={{ flex: 1, fontSize: "9px" }}
onClick={toggleOverlay}
>
{overlayVisible ? "Hide Overlay" : "Show Overlay"}
</button>
</div>
</div>
</div>
);
};
export default {
type: "API_Data_Collector",
label: "API Data Collector",
description: "Connects to a remote agent via API and collects data such as screenshots, OCR results, and more.",
content: "Publishes agent-collected data into the workflow ValueBus.",
component: APINode
};

View File

@ -0,0 +1,60 @@
import React, { useEffect, useState } from "react";
import { Handle, Position, useReactFlow } from "reactflow";
const ImageViewerNode = ({ id, data }) => {
const { getEdges } = useReactFlow();
const [imageBase64, setImageBase64] = useState("");
const [selectedType, setSelectedType] = useState("base64");
// Watch upstream value
useEffect(() => {
const interval = setInterval(() => {
const edges = getEdges();
const inputEdge = edges.find(e => e.target === id);
if (inputEdge) {
const sourceId = inputEdge.source;
const valueBus = window.BorealisValueBus || {};
const value = valueBus[sourceId];
if (typeof value === "string") {
setImageBase64(value);
}
}
}, 1000);
return () => clearInterval(interval);
}, [id, getEdges]);
return (
<div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" />
<div className="borealis-node-header">Image Viewer</div>
<div className="borealis-node-content">
<label style={{ fontSize: "10px" }}>Data Type:</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
>
<option value="base64">Base64 Encoded Image</option>
</select>
{imageBase64 ? (
<img
src={`data:image/png;base64,${imageBase64}`}
alt="Live"
style={{ width: "100%", border: "1px solid #333", marginTop: "6px" }}
/>
) : (
<div style={{ fontSize: "9px", color: "#888" }}>Waiting for image...</div>
)}
</div>
</div>
);
};
export default {
type: "Image_Viewer",
label: "Image Viewer",
description: "Displays base64 image pulled from ValueBus of upstream node.",
content: "Visual preview of base64 image",
component: ImageViewerNode
};

View File

@ -32,7 +32,8 @@ const BackdropGroupBoxNode = ({ id, data }) => {
}
}, [isEditing]);
const handleTitleClick = () => {
const handleTitleClick = (e) => {
e.stopPropagation();
setIsEditing(true);
};
@ -47,22 +48,32 @@ const BackdropGroupBoxNode = ({ id, data }) => {
};
return (
<div style={{ pointerEvents: "auto", zIndex: -1 }}> {/* Prevent blocking other nodes */}
<div style={{ pointerEvents: "auto" }}>
<ResizableBox
width={200}
height={120}
minConstraints={[120, 80]}
maxConstraints={[600, 600]}
resizeHandles={["se"]}
className="borealis-node"
handle={(h) => (
<span
className={`react-resizable-handle react-resizable-handle-${h}`}
style={{ pointerEvents: "auto" }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/>
)}
onClick={(e) => e.stopPropagation()}
style={{
backgroundColor: "rgba(44, 44, 44, 0.5)",
border: "1px solid #3a3a3a",
borderRadius: "4px",
boxShadow: "0 0 5px rgba(88, 166, 255, 0.15)",
overflow: "hidden",
position: "relative"
position: "relative",
zIndex: 0
}}
onClick={(e) => e.stopPropagation()} // prevent drag on resize
>
<div
onClick={handleTitleClick}
@ -82,6 +93,8 @@ const BackdropGroupBoxNode = ({ id, data }) => {
value={title}
onChange={handleTitleChange}
onBlur={handleBlur}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
style={{
fontSize: "10px",
padding: "2px",