- 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:
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
177
Data/WebUI/src/nodes/Data Collection/API_Data_Collector.jsx
Normal file
177
Data/WebUI/src/nodes/Data Collection/API_Data_Collector.jsx
Normal 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
|
||||
};
|
60
Data/WebUI/src/nodes/Image Processing/Image_Viewer.jsx
Normal file
60
Data/WebUI/src/nodes/Image Processing/Image_Viewer.jsx
Normal 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
|
||||
};
|
@ -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",
|
||||
|
Reference in New Issue
Block a user