diff --git a/Data/Agent/borealis-agent.py b/Data/Agent/borealis-agent.py index e6e92cd..2ff9193 100644 --- a/Data/Agent/borealis-agent.py +++ b/Data/Agent/borealis-agent.py @@ -8,31 +8,28 @@ import threading import socketio from io import BytesIO import socket +import os from PyQt5 import QtCore, QtGui, QtWidgets from PIL import ImageGrab # ---------------- Configuration ---------------- -SERVER_URL = "http://localhost:5000" # WebSocket-enabled Internal URL -#SERVER_URL = "https://borealis.bunny-lab.io" # WebSocket-enabled Public URL" +SERVER_URL = os.environ.get("SERVER_URL", "http://localhost:5000") HOSTNAME = socket.gethostname().lower() RANDOM_SUFFIX = uuid.uuid4().hex[:8] AGENT_ID = f"{HOSTNAME}-agent-{RANDOM_SUFFIX}" -# ---------------- State ---------------- +# ---------------- App State ---------------- app_instance = None -region_widget = None -current_interval = 1000 -config_ready = threading.Event() -overlay_visible = True +overlay_widgets = {} +region_launchers = {} +running_roles = {} +running_threads = {} -LAST_CONFIG = {} - -# WebSocket client setup +# ---------------- Socket Setup ---------------- sio = socketio.Client() -# ---------------- WebSocket Handlers ---------------- @sio.event def connect(): print(f"[WebSocket] Agent ID: {AGENT_ID} connected to Borealis.") @@ -45,30 +42,18 @@ def disconnect(): @sio.on('agent_config') def on_agent_config(config): - global current_interval, overlay_visible, LAST_CONFIG + print("[PROVISIONED] Received new configuration from Borealis.") - if config != LAST_CONFIG: - print("[PROVISIONED] Received new configuration from Borealis.") - x = config.get("x", 100) - y = config.get("y", 100) - w = config.get("w", 300) - h = config.get("h", 200) - current_interval = config.get("interval", 1000) - overlay_visible = config.get("visible", True) + roles = config.get("roles", []) + stop_all_roles() + for role in roles: + start_role_thread(role) - if not region_widget: - region_launcher.trigger.emit(x, y, w, h) - else: - region_widget.setGeometry(x, y, w, h) - region_widget.setVisible(overlay_visible) - - LAST_CONFIG = config - config_ready.set() - -# ---------------- Region Overlay ---------------- +# ---------------- Overlay Class ---------------- class ScreenshotRegion(QtWidgets.QWidget): - def __init__(self, x=100, y=100, w=300, h=200): + def __init__(self, node_id, x=100, y=100, w=300, h=200): super().__init__() + self.node_id = node_id self.setGeometry(x, y, w, h) self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -78,7 +63,7 @@ class ScreenshotRegion(QtWidgets.QWidget): self.setVisible(True) self.label = QtWidgets.QLabel(self) - self.label.setText(AGENT_ID) + self.label.setText(f"{node_id[:8]}") self.label.setStyleSheet("color: lime; background: transparent; font-size: 10px;") self.label.move(8, 4) @@ -87,7 +72,6 @@ class ScreenshotRegion(QtWidgets.QWidget): def paintEvent(self, event): painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.Antialiasing) - painter.setBrush(QtCore.Qt.transparent) painter.setPen(QtGui.QPen(QtGui.QColor(0, 255, 0), 2)) painter.drawRect(self.rect()) @@ -102,8 +86,8 @@ class ScreenshotRegion(QtWidgets.QWidget): def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: - if (event.pos().x() > self.width() - self.resize_handle_size and - event.pos().y() > self.height() - self.resize_handle_size): + if event.pos().x() > self.width() - self.resize_handle_size and \ + event.pos().y() > self.height() - self.resize_handle_size: self.resizing = True else: self.drag_offset = event.globalPos() - self.frameGeometry().topLeft() @@ -124,58 +108,91 @@ class ScreenshotRegion(QtWidgets.QWidget): geo = self.geometry() return geo.x(), geo.y(), geo.width(), geo.height() -# ---------------- Screenshot Capture ---------------- -def capture_loop(): - config_ready.wait() - - while region_widget is None: - time.sleep(0.2) - - while True: - if overlay_visible: - x, y, w, h = region_widget.get_geometry() - try: - img = ImageGrab.grab(bbox=(x, y, x + w, y + h)) - buffer = BytesIO() - img.save(buffer, format="PNG") - encoded_image = base64.b64encode(buffer.getvalue()).decode("utf-8") - - sio.emit('screenshot', { - 'agent_id': AGENT_ID, - 'image_base64': encoded_image - }) - except Exception as e: - print(f"[ERROR] Screenshot error: {e}") - - time.sleep(current_interval / 1000) - -# ---------------- UI Launcher ---------------- +# ---------------- Region UI Handler ---------------- class RegionLauncher(QtCore.QObject): trigger = QtCore.pyqtSignal(int, int, int, int) - def __init__(self): + def __init__(self, node_id): super().__init__() + self.node_id = node_id self.trigger.connect(self.handle) def handle(self, x, y, w, h): - launch_region(x, y, w, h) + print(f"[Overlay] Launching overlay for {self.node_id} at ({x},{y},{w},{h})") + if self.node_id in overlay_widgets: + return + widget = ScreenshotRegion(self.node_id, x, y, w, h) + overlay_widgets[self.node_id] = widget + widget.show() -region_launcher = None +# ---------------- Role Management ---------------- +def stop_all_roles(): + for node_id, thread in running_threads.items(): + if thread and thread.is_alive(): + print(f"[Role] Terminating previous task: {node_id}") + running_roles.clear() + running_threads.clear() -def launch_region(x, y, w, h): - global region_widget - if region_widget: +def start_role_thread(role_cfg): + role = role_cfg.get("role") + node_id = role_cfg.get("node_id") + if not role or not node_id: + print("[ERROR] Invalid role configuration (missing role or node_id).") return - region_widget = ScreenshotRegion(x, y, w, h) - region_widget.show() + + if role == "screenshot": + thread = threading.Thread(target=run_screenshot_loop, args=(node_id, role_cfg), daemon=True) + else: + print(f"[SKIP] Unknown role: {role}") + return + + running_roles[node_id] = role_cfg + running_threads[node_id] = thread + thread.start() + print(f"[Role] Started task: {role} ({node_id})") + +# ---------------- Screenshot Role Loop ---------------- +def run_screenshot_loop(node_id, cfg): + interval = cfg.get("interval", 1000) + visible = cfg.get("visible", True) + x = cfg.get("x", 100) + y = cfg.get("y", 100) + w = cfg.get("w", 300) + h = cfg.get("h", 200) + + if node_id not in region_launchers: + launcher = RegionLauncher(node_id) + region_launchers[node_id] = launcher + launcher.trigger.emit(x, y, w, h) + + widget = overlay_widgets.get(node_id) + if widget: + widget.setGeometry(x, y, w, h) + widget.setVisible(visible) + + while True: + try: + if node_id in overlay_widgets: + widget = overlay_widgets[node_id] + x, y, w, h = widget.get_geometry() + print(f"[Capture] Screenshot task {node_id} at ({x},{y},{w},{h})") + img = ImageGrab.grab(bbox=(x, y, x + w, y + h)) + buffer = BytesIO() + img.save(buffer, format="PNG") + encoded = base64.b64encode(buffer.getvalue()).decode("utf-8") + + sio.emit("agent_screenshot_task", { + "agent_id": AGENT_ID, + "node_id": node_id, + "image_base64": encoded + }) + except Exception as e: + print(f"[ERROR] Screenshot task {node_id} failed: {e}") + + time.sleep(interval / 1000) # ---------------- Main ---------------- if __name__ == "__main__": app_instance = QtWidgets.QApplication(sys.argv) - region_launcher = RegionLauncher() - - sio.connect(SERVER_URL, transports=['websocket']) - - threading.Thread(target=capture_loop, daemon=True).start() - + sio.connect(SERVER_URL, transports=["websocket"]) sys.exit(app_instance.exec_()) diff --git a/Data/WebUI/src/App.jsx b/Data/WebUI/src/App.jsx index 5d792b3..9d2bbac 100644 --- a/Data/WebUI/src/App.jsx +++ b/Data/WebUI/src/App.jsx @@ -48,6 +48,15 @@ import React, { } from "./Dialogs"; import StatusBar from "./Status_Bar"; + // Websocket Functionality + import { io } from "socket.io-client"; + + if (!window.BorealisSocket) { + window.BorealisSocket = io(window.location.origin, { + transports: ["websocket"] + }); + } + // Global Node Update Timer Variable if (!window.BorealisUpdateRate) { window.BorealisUpdateRate = 200; diff --git a/Data/WebUI/src/Node_Sidebar.jsx b/Data/WebUI/src/Node_Sidebar.jsx index 794bd38..f7c5afb 100644 --- a/Data/WebUI/src/Node_Sidebar.jsx +++ b/Data/WebUI/src/Node_Sidebar.jsx @@ -35,7 +35,7 @@ export default function NodeSidebar({ return ( <div style={{ - width: 320, + width: 300, //Width of the Node Sidebar backgroundColor: "#121212", borderRight: "1px solid #333", overflowY: "auto" diff --git a/Data/WebUI/src/nodes/Agents/Node_Agent.jsx b/Data/WebUI/src/nodes/Agents/Node_Agent.jsx new file mode 100644 index 0000000..bc5bdb4 --- /dev/null +++ b/Data/WebUI/src/nodes/Agents/Node_Agent.jsx @@ -0,0 +1,154 @@ +////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Agent/Node_Agent.jsx + +import React, { useEffect, useState } from "react"; +import { Handle, Position, useReactFlow, useStore } from "reactflow"; + +const BorealisAgentNode = ({ id, data }) => { + const { getNodes, getEdges, setNodes } = useReactFlow(); + const [agents, setAgents] = useState([]); + const [selectedAgent, setSelectedAgent] = useState(data.agent_id || ""); + + // ------------------------------- + // Load agent list from backend + // ------------------------------- + useEffect(() => { + fetch("/api/agents") + .then(res => res.json()) + .then(setAgents); + + const interval = setInterval(() => { + fetch("/api/agents") + .then(res => res.json()) + .then(setAgents); + }, 5000); + + return () => clearInterval(interval); + }, []); + + // ------------------------------- + // Helper: Get all provisioner role nodes connected to bottom port + // ------------------------------- + const getAttachedProvisioners = () => { + const allNodes = getNodes(); + const allEdges = getEdges(); + const attached = []; + + for (const edge of allEdges) { + if (edge.source === id && edge.sourceHandle === "provisioner") { + const roleNode = allNodes.find(n => n.id === edge.target); + if (roleNode && typeof window.__BorealisInstructionNodes?.[roleNode.id] === "function") { + attached.push(window.__BorealisInstructionNodes[roleNode.id]()); + } + } + } + + return attached; + }; + + // ------------------------------- + // Provision Agent with all Roles + // ------------------------------- + const handleProvision = () => { + if (!selectedAgent) return; + + const provisionRoles = getAttachedProvisioners(); + if (!provisionRoles.length) { + console.warn("No provisioner nodes connected to agent."); + return; + } + + const configPayload = { + agent_id: selectedAgent, + roles: provisionRoles + }; + + fetch("/api/agent/provision", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(configPayload) + }) + .then(res => res.json()) + .then(() => { + console.log(`[Provision] Agent ${selectedAgent} updated with ${provisionRoles.length} roles.`); + }); + }; + + return ( + <div className="borealis-node"> + {/* This bottom port is used for bi-directional provisioning & feedback */} + <Handle + type="source" + position={Position.Bottom} + id="provisioner" + className="borealis-handle" + style={{ top: "100%", background: "#58a6ff" }} + /> + + <div className="borealis-node-header">Borealis Agent</div> + <div className="borealis-node-content" style={{ fontSize: "9px" }}> + <label>Agent:</label> + <select + value={selectedAgent} + onChange={(e) => { + const newId = e.target.value; + setSelectedAgent(newId); + setNodes(nds => + nds.map(n => + n.id === id + ? { ...n, data: { ...n.data, agent_id: newId } } + : n + ) + ); + }} + style={{ width: "100%", marginBottom: "6px", fontSize: "9px" }} + > + <option value="">-- Select --</option> + {Object.entries(agents).map(([id, info]) => { + const label = info.status === "provisioned" ? "(Provisioned)" : "(Idle)"; + return ( + <option key={id} value={id}> + {id} {label} + </option> + ); + })} + </select> + + <button + onClick={handleProvision} + style={{ width: "100%", fontSize: "9px", padding: "4px", marginTop: "4px" }} + > + Provision Agent + </button> + + <hr style={{ margin: "6px 0", borderColor: "#444" }} /> + + <div style={{ fontSize: "8px", color: "#aaa" }}> + Connect <strong>Instruction Nodes</strong> below to define roles. + Each instruction node will send back its results (like screenshots) and act as a separate data output. + </div> + + <div style={{ fontSize: "8px", color: "#aaa", marginTop: "4px" }}> + <strong>Supported Roles:</strong> + <ul style={{ paddingLeft: "14px", marginTop: "2px", marginBottom: "0" }}> + <li><code>screenshot</code>: Capture a region with interval and overlay</li> + {/* Future roles will be listed here */} + </ul> + </div> + </div> + </div> + ); +}; + +export default { + type: "Borealis_Agent", + label: "Borealis Agent", + description: ` +Main Agent Node + +- Selects an available agent +- Connect instruction nodes below to assign tasks (roles) +- Roles include screenshots, keyboard macros, etc. +`.trim(), + content: "Select and provision a Borealis Agent with task roles", + component: BorealisAgentNode +}; diff --git a/Data/WebUI/src/nodes/Agents/Node_Agent_Role_Screenshot.jsx b/Data/WebUI/src/nodes/Agents/Node_Agent_Role_Screenshot.jsx new file mode 100644 index 0000000..6e86279 --- /dev/null +++ b/Data/WebUI/src/nodes/Agents/Node_Agent_Role_Screenshot.jsx @@ -0,0 +1,186 @@ +////////// 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 = {}; +} + +if (!window.BorealisUpdateRate) { + window.BorealisUpdateRate = 100; +} + +const ScreenshotInstructionNode = ({ 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(""); + + const base64Ref = useRef(""); + + const handleCopyLiveViewLink = () => { + const agentEdge = edges.find(e => e.target === id && e.sourceHandle === "provisioner"); + if (!agentEdge) { + alert("No upstream agent connection found."); + return; + } + + const agentNode = getNodes().find(n => n.id === agentEdge.source); + const selectedAgentId = agentNode?.data?.agent_id; + + if (!selectedAgentId) { + alert("Upstream agent node does not have a selected agent."); + return; + } + + const liveUrl = `${window.location.origin}/api/agent/${selectedAgentId}/node/${id}/screenshot/live`; + navigator.clipboard.writeText(liveUrl) + .then(() => console.log(`[Clipboard] Copied Live View URL: ${liveUrl}`)) + .catch(err => console.error("Clipboard copy failed:", err)); + }; + + useEffect(() => { + const intervalId = setInterval(() => { + const val = base64Ref.current; + + console.log(`[Screenshot Node] setInterval update. Current base64 length: ${val?.length || 0}`); + + if (!val) return; + + window.BorealisValueBus[id] = val; + setNodes(nds => + nds.map(n => + n.id === id + ? { ...n, data: { ...n.data, value: val } } + : n + ) + ); + }, window.BorealisUpdateRate || 100); + + return () => clearInterval(intervalId); + }, [id, setNodes]); + + useEffect(() => { + const socket = window.BorealisSocket || null; + if (!socket) { + console.warn("[Screenshot Node] BorealisSocket not available"); + return; + } + + console.log(`[Screenshot Node] Listening for agent_screenshot_task with node_id: ${id}`); + + const handleScreenshot = (payload) => { + console.log("[Screenshot Node] Received payload:", payload); + + if (payload?.node_id === id && payload?.image_base64) { + base64Ref.current = payload.image_base64; + setImageBase64(payload.image_base64); + window.BorealisValueBus[id] = payload.image_base64; + + console.log(`[Screenshot Node] Updated base64Ref and ValueBus for ${id}, length: ${payload.image_base64.length}`); + } else { + console.log(`[Screenshot Node] Ignored payload for mismatched node_id (${payload?.node_id})`); + } + }; + + socket.on("agent_screenshot_task", handleScreenshot); + return () => socket.off("agent_screenshot_task", handleScreenshot); + }, [id]); + + window.__BorealisInstructionNodes = window.__BorealisInstructionNodes || {}; + window.__BorealisInstructionNodes[id] = () => ({ + node_id: id, + role: "screenshot", + interval, + visible, + alias, + ...region + }); + + 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-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) => setRegion({ ...region, x: Number(e.target.value) })} style={{ width: "25%" }} /> + <input type="number" value={region.y} onChange={(e) => setRegion({ ...region, y: Number(e.target.value) })} style={{ width: "25%" }} /> + <input type="number" value={region.w} onChange={(e) => setRegion({ ...region, w: Number(e.target.value) })} style={{ width: "25%" }} /> + <input type="number" value={region.h} onChange={(e) => setRegion({ ...region, h: Number(e.target.value) })} style={{ width: "25%" }} /> + </div> + + <div style={{ marginBottom: "4px" }}> + <label> + <input + type="checkbox" + checked={visible} + onChange={() => setVisible(!visible)} + style={{ marginRight: "4px" }} + /> + Show Overlay on Agent + </label> + </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 }} /> + </IconButton> + </div> + </div> + ); +}; + +export default { + type: "Agent_Role_Screenshot", + label: "Agent Role: Screenshot", + description: ` +Agent Role Node: Screenshot Region + +- Defines a single region capture role +- Allows custom update interval and overlay +- Emits captured base64 PNG data from agent +`.trim(), + content: "Capture screenshot region via agent", + component: ScreenshotInstructionNode +}; diff --git a/Data/WebUI/src/nodes/Data Analysis/Node_OCR_Text_Extraction.jsx b/Data/WebUI/src/nodes/Data Analysis/Node_OCR_Text_Extraction.jsx index 15a0108..f71cfc6 100644 --- a/Data/WebUI/src/nodes/Data Analysis/Node_OCR_Text_Extraction.jsx +++ b/Data/WebUI/src/nodes/Data Analysis/Node_OCR_Text_Extraction.jsx @@ -233,7 +233,7 @@ const numberInputStyle = { export default { type: "OCR_Text_Extraction", - label: "OCR-Based Text Extraction", + label: "OCR Text Extraction", description: ` Extract text from upstream image using backend OCR engine via API. Includes rate limiting and sensitivity detection for smart processing.`, diff --git a/Data/WebUI/src/nodes/Data Collection/Node_Borealis_Agent.jsx b/Data/WebUI/src/nodes/Data Collection/Node_Borealis_Agent.jsx deleted file mode 100644 index 7f483c2..0000000 --- a/Data/WebUI/src/nodes/Data Collection/Node_Borealis_Agent.jsx +++ /dev/null @@ -1,172 +0,0 @@ -////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: Node_Borealis_Agent.jsx - -import React, { useEffect, useState, useRef } from "react"; -import { Handle, Position, useReactFlow } from "reactflow"; -import { io } from "socket.io-client"; - -const socket = io(window.location.origin, { - transports: ["websocket"] -}); - -const BorealisAgentNode = ({ 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 [intervalMs, setIntervalMs] = useState(data.interval || 1000); - const [paused, setPaused] = useState(false); - const [overlayVisible, setOverlayVisible] = useState(true); - const [imageData, setImageData] = useState(""); - const imageRef = useRef(""); - - useEffect(() => { - fetch("/api/agents").then(res => res.json()).then(setAgents); - const interval = setInterval(() => { - fetch("/api/agents").then(res => res.json()).then(setAgents); - }, 5000); - return () => clearInterval(interval); - }, []); - - useEffect(() => { - socket.on('new_screenshot', (data) => { - if (data.agent_id === selectedAgent) { - setImageData(data.image_base64); - imageRef.current = data.image_base64; - } - }); - - return () => socket.off('new_screenshot'); - }, [selectedAgent]); - - useEffect(() => { - const interval = setInterval(() => { - if (!paused && imageRef.current) { - window.BorealisValueBus = window.BorealisValueBus || {}; - window.BorealisValueBus[id] = imageRef.current; - - setNodes(nds => { - const updated = [...nds]; - const node = updated.find(n => n.id === id); - if (node) { - node.data = { - ...node.data, - value: imageRef.current - }; - } - return updated; - }); - } - }, window.BorealisUpdateRate || 100); - - return () => clearInterval(interval); - }, [id, paused, setNodes]); - - 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(res => res.json()) - .then(() => { - console.log("[DEBUG] Agent provisioned"); - }); - }; - - const toggleOverlay = () => { - const newVisibility = !overlayVisible; - setOverlayVisible(newVisibility); - provisionAgent(); - }; - - return ( - <div className="borealis-node"> - <Handle type="source" position={Position.Right} className="borealis-handle" /> - <div className="borealis-node-header">Borealis Agent</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]) => { - const statusLabel = info.status === "provisioned" - ? "(Provisioned)" - : "(Not Provisioned)"; - return ( - <option key={id} value={id}> - {id} {statusLabel} - </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} - > - (Re)Provision - </button> - <button - style={{ flex: 1, fontSize: "9px" }} - onClick={toggleOverlay} - > - {overlayVisible ? "Hide Overlay" : "Show Overlay"} - </button> - </div> - </div> - </div> - ); -}; - -export default { - type: "Borealis_Agent", - label: "Borealis Agent", - description: "Connects to and controls a Borealis Agent via WebSocket in real-time.", - content: "Provisions a Borealis Agent and streams collected data into the workflow graph.", - component: BorealisAgentNode -}; diff --git a/Data/server.py b/Data/server.py index 0f385e5..245061f 100644 --- a/Data/server.py +++ b/Data/server.py @@ -73,38 +73,36 @@ def get_agents(): def provision_agent(): data = request.json agent_id = data.get("agent_id") - config = { - "task": data.get("task", "screenshot"), - "x": data.get("x", 100), - "y": data.get("y", 100), - "w": data.get("w", 300), - "h": data.get("h", 200), - "interval": data.get("interval", 1000), - "visible": data.get("visible", True) - } + roles = data.get("roles", []) # <- MODULAR ROLES ARRAY + + if not agent_id or not isinstance(roles, list): + return jsonify({"error": "Missing agent_id or roles[] in provision payload."}), 400 + + # Save configuration + config = {"roles": roles} agent_configurations[agent_id] = config + + # Update status if agent already registered if agent_id in registered_agents: registered_agents[agent_id]["status"] = "provisioned" - # NEW: Emit config update back to the agent via WebSocket + # Emit config to the agent socketio.emit("agent_config", config) - return jsonify({"status": "provisioned"}) + return jsonify({"status": "provisioned", "roles": roles}) + # ---------------------------------------------- # Canvas Image Feed Viewer for Screenshot Agents # ---------------------------------------------- -@app.route("/api/agent/<agent_id>/screenshot/live") -def screenshot_viewer(agent_id): - if agent_configurations.get(agent_id, {}).get("task") != "screenshot": - return "<h1>Agent not provisioned as Screenshot Collector</h1>", 400 - +@app.route("/api/agent/<agent_id>/node/<node_id>/screenshot/live") +def screenshot_node_viewer(agent_id, node_id): return f""" <!DOCTYPE html> <html> <head> - <title>Borealis Live View - {agent_id}</title> + <title>Borealis Live View - {agent_id}:{node_id}</title> <style> body {{ margin: 0; @@ -118,105 +116,66 @@ def screenshot_viewer(agent_id): border: 1px solid #444; max-width: 90vw; max-height: 90vh; - width: auto; - height: auto; background-color: #111; }} </style> </head> <body> <canvas id="viewerCanvas"></canvas> - <script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script> <script> const agentId = "{agent_id}"; - const socket = io(window.location.origin, {{ transports: ["websocket"] }}); + const nodeId = "{node_id}"; const canvas = document.getElementById("viewerCanvas"); const ctx = canvas.getContext("2d"); + const socket = io(window.location.origin, {{ transports: ["websocket"] }}); - console.log("[Viewer] Canvas initialized for agent:", agentId); - - socket.on("connect", () => {{ - console.log("[WebSocket] Connected to Borealis server at", window.location.origin); - }}); - - socket.on("disconnect", () => {{ - console.warn("[WebSocket] Disconnected from Borealis server"); - }}); - - socket.on("new_screenshot", (data) => {{ - console.log("[WebSocket] Received screenshot event"); - - if (!data || typeof data !== "object") {{ - console.error("[Viewer] Screenshot event was not an object:", data); - return; - }} - - if (data.agent_id !== agentId) {{ - console.log("[Viewer] Ignored screenshot from different agent:", data.agent_id); - return; - }} - + socket.on("agent_screenshot_task", (data) => {{ + if (data.agent_id !== agentId || data.node_id !== nodeId) return; const base64 = data.image_base64; - console.log("[Viewer] Base64 length:", base64?.length || 0); - - if (!base64 || base64.length < 100) {{ - console.warn("[Viewer] Empty or too short base64 string."); - return; - }} - - // Peek at base64 to determine MIME type - let mimeType = "image/png"; - try {{ - const header = atob(base64.substring(0, 32)); - if (header.charCodeAt(0) === 0xFF && header.charCodeAt(1) === 0xD8) {{ - mimeType = "image/jpeg"; - }} - }} catch (e) {{ - console.warn("[Viewer] Failed to decode base64 header", e); - }} + if (!base64 || base64.length < 100) return; const img = new Image(); img.onload = () => {{ - console.log("[Viewer] Image loaded successfully:", img.width + "x" + img.height); - - console.log("[Viewer] Canvas size before:", canvas.width + "x" + canvas.height); - if (canvas.width !== img.width || canvas.height !== img.height) {{ canvas.width = img.width; canvas.height = img.height; - console.log("[Viewer] Canvas resized to match image"); }} - ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); - - console.log("[Viewer] Image drawn on canvas"); }}; - img.onerror = (err) => {{ - console.error("[Viewer] Failed to load image from base64. Possibly corrupted data?", err); - }}; - img.src = "data:" + mimeType + ";base64," + base64; + img.src = "data:image/png;base64," + base64; }}); </script> </body> </html> """ -@app.route("/api/agent/<agent_id>/screenshot/raw") # Fallback Non-Live Screenshot Preview Code for Legacy Purposes -def screenshot_raw(agent_id): - entry = latest_images.get(agent_id) - if not entry: - return "", 204 - try: - raw_img = base64.b64decode(entry["image_base64"]) - return Response(raw_img, mimetype="image/png") - except Exception: - return "", 204 - # --------------------------------------------- # WebSocket Events # --------------------------------------------- +@socketio.on("agent_screenshot_task") +def receive_screenshot_task(data): + agent_id = data.get("agent_id") + node_id = data.get("node_id") + image = data.get("image_base64") + + if not agent_id or not node_id or not image: + print("[WS] Screenshot task missing fields.") + return + + # Optional: Store for debugging + latest_images[f"{agent_id}:{node_id}"] = { + "image_base64": image, + "timestamp": time.time() + } + + emit("agent_screenshot_task", { + "agent_id": agent_id, + "node_id": node_id, + "image_base64": image + }, broadcast=True) + @socketio.on('connect_agent') def connect_agent(data): agent_id = data.get("agent_id") diff --git a/Launch-Borealis.ps1 b/Launch-Borealis.ps1 index fc5e115..da51ed3 100644 --- a/Launch-Borealis.ps1 +++ b/Launch-Borealis.ps1 @@ -102,7 +102,7 @@ switch ($choice) { } # React UI Deployment: Create default React app if no deployment folder exists if (-not (Test-Path $webUIDestination)) { - npx --yes create-react-app "$webUIDestination" --verbose | Out-Null + npx --yes create-react-app "$webUIDestination" | Out-Null } # Copy custom UI if it exists if (Test-Path $customUIPath) {