From 275d9f0cb340f1664c97693845ff764a3e708204 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Thu, 10 Apr 2025 21:11:30 -0600 Subject: [PATCH] Added Functional Audio Alert Node --- .../src/nodes/Alerting/Node_Alert_Sound.jsx | 323 ++++++++++++++++++ .../Node_Borealis_Agent.jsx | 17 +- .../src/nodes/General Purpose/Node_Data.jsx | 2 +- Data/server.py | 4 +- 4 files changed, 335 insertions(+), 11 deletions(-) create mode 100644 Data/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx rename Data/WebUI/src/nodes/{Agent => Data Collection}/Node_Borealis_Agent.jsx (93%) diff --git a/Data/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx b/Data/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx new file mode 100644 index 0000000..9a11fb6 --- /dev/null +++ b/Data/WebUI/src/nodes/Alerting/Node_Alert_Sound.jsx @@ -0,0 +1,323 @@ +/** + * ================================================== + * Borealis - Alert Sound Node (with Base64 Restore) + * ================================================== + * + * COMPONENT ROLE: + * Plays a sound when input = "1". Provides a visual indicator: + * - Green dot: input is 0 + * - Red dot: input is 1 + * + * Modes: + * - "Once": Triggers once when going 0 -> 1 + * - "Constant": Triggers repeatedly every X ms while input = 1 + * + * Supports embedding base64 audio directly into the workflow. + */ + +import React, { useEffect, useRef, useState } from "react"; +import { Handle, Position, useReactFlow, useStore } from "reactflow"; + +if (!window.BorealisValueBus) window.BorealisValueBus = {}; +if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100; + +const AlertSoundNode = ({ id, data }) => { + const edges = useStore(state => state.edges); + const { setNodes } = useReactFlow(); + + const [alertType, setAlertType] = useState(data?.alertType || "Once"); + const [intervalMs, setIntervalMs] = useState(data?.interval || 1000); + const [prevInput, setPrevInput] = useState("0"); + const [customAudioBase64, setCustomAudioBase64] = useState(data?.audio || null); + const [currentInput, setCurrentInput] = useState("0"); + + const audioRef = useRef(null); + + const playSound = () => { + if (audioRef.current) { + console.log(`[Alert Node ${id}] Attempting to play sound`); + try { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + audioRef.current.load(); + audioRef.current.play().then(() => { + console.log(`[Alert Node ${id}] Sound played successfully`); + }).catch((err) => { + console.warn(`[Alert Node ${id}] Audio play blocked or errored:`, err); + }); + } catch (err) { + console.error(`[Alert Node ${id}] Failed to play sound:`, err); + } + } else { + console.warn(`[Alert Node ${id}] No audioRef loaded`); + } + }; + + const handleFileUpload = (event) => { + const file = event.target.files[0]; + if (!file) return; + + console.log(`[Alert Node ${id}] File selected:`, file.name, file.type); + + const supportedTypes = ["audio/wav", "audio/mp3", "audio/mpeg", "audio/ogg"]; + if (!supportedTypes.includes(file.type)) { + console.warn(`[Alert Node ${id}] Unsupported audio type: ${file.type}`); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const base64 = e.target.result; + const mimeType = file.type || "audio/mpeg"; + const safeURL = base64.startsWith("data:") + ? base64 + : `data:${mimeType};base64,${base64}`; + + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ""; + audioRef.current.load(); + audioRef.current = null; + } + + const newAudio = new Audio(); + newAudio.src = safeURL; + + let readyFired = false; + + newAudio.addEventListener("canplaythrough", () => { + if (readyFired) return; + readyFired = true; + console.log(`[Alert Node ${id}] Audio is decodable and ready: ${file.name}`); + + setCustomAudioBase64(safeURL); + audioRef.current = newAudio; + newAudio.load(); + + setNodes(nds => + nds.map(n => + n.id === id + ? { ...n, data: { ...n.data, audio: safeURL } } + : n + ) + ); + }); + + setTimeout(() => { + if (!readyFired) { + console.warn(`[Alert Node ${id}] WARNING: Audio not marked ready in time. May fail silently.`); + } + }, 2000); + }; + + reader.onerror = (e) => { + console.error(`[Alert Node ${id}] File read error:`, e); + }; + + reader.readAsDataURL(file); + }; + + // Restore embedded audio from saved workflow + useEffect(() => { + if (customAudioBase64) { + console.log(`[Alert Node ${id}] Loading embedded audio from workflow`); + + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ""; + audioRef.current.load(); + audioRef.current = null; + } + + const loadedAudio = new Audio(customAudioBase64); + loadedAudio.addEventListener("canplaythrough", () => { + console.log(`[Alert Node ${id}] Embedded audio ready`); + }); + + audioRef.current = loadedAudio; + loadedAudio.load(); + } else { + console.log(`[Alert Node ${id}] No custom audio, using fallback silent wav`); + audioRef.current = new Audio("data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YRAAAAAA"); + audioRef.current.load(); + } + }, [customAudioBase64]); + + useEffect(() => { + let currentRate = window.BorealisUpdateRate; + let intervalId = null; + + const runLogic = () => { + const inputEdge = edges.find(e => e.target === id); + const sourceId = inputEdge?.source || null; + const val = sourceId ? (window.BorealisValueBus[sourceId] || "0") : "0"; + + setCurrentInput(val); + + if (alertType === "Once") { + if (val === "1" && prevInput !== "1") { + console.log(`[Alert Node ${id}] Triggered ONCE playback`); + playSound(); + } + } + + setPrevInput(val); + }; + + const start = () => { + if (alertType === "Constant") { + intervalId = setInterval(() => { + const inputEdge = edges.find(e => e.target === id); + const sourceId = inputEdge?.source || null; + const val = sourceId ? (window.BorealisValueBus[sourceId] || "0") : "0"; + setCurrentInput(val); + if (String(val) === "1") { + console.log(`[Alert Node ${id}] Triggered CONSTANT playback`); + playSound(); + } + }, intervalMs); + } else { + intervalId = setInterval(runLogic, currentRate); + } + }; + + start(); + + const monitor = setInterval(() => { + const newRate = window.BorealisUpdateRate; + if (newRate !== currentRate && alertType === "Once") { + currentRate = newRate; + clearInterval(intervalId); + start(); + } + }, 250); + + return () => { + clearInterval(intervalId); + clearInterval(monitor); + }; + }, [edges, alertType, intervalMs, prevInput]); + + const indicatorColor = currentInput === "1" ? "#ff4444" : "#44ff44"; + + return ( +
+ + + {/* Header with indicator dot */} +
+ {data?.label || "Alert Sound"} +
+
+ +
+
+ Play a sound when input equals "1" +
+ + + + + + setIntervalMs(parseInt(e.target.value))} + disabled={alertType === "Once"} + style={{ + ...inputStyle, + background: alertType === "Once" ? "#2a2a2a" : "#1e1e1e" + }} + /> + + + +
+ + +
+
+
+ ); +}; + +const dropdownStyle = { + fontSize: "9px", + padding: "4px", + background: "#1e1e1e", + color: "#ccc", + border: "1px solid #444", + borderRadius: "2px", + width: "100%", + marginBottom: "8px" +}; + +const inputStyle = { + fontSize: "9px", + padding: "4px", + color: "#ccc", + border: "1px solid #444", + borderRadius: "2px", + width: "100%", + marginBottom: "8px" +}; + +export default { + type: "AlertSoundNode", + label: "Alert Sound", + description: ` +Plays a sound alert when input = "1" + +- "Once" = Only when 0 -> 1 transition +- "Constant" = Repeats every X ms while input stays 1 +- Custom audio supported (MP3/WAV/OGG) +- Base64 audio embedded in workflow and restored +- Visual status indicator (green = 0, red = 1) +- Manual "Test" button for validation +`.trim(), + content: "Sound alert when input value = 1", + component: AlertSoundNode +}; diff --git a/Data/WebUI/src/nodes/Agent/Node_Borealis_Agent.jsx b/Data/WebUI/src/nodes/Data Collection/Node_Borealis_Agent.jsx similarity index 93% rename from Data/WebUI/src/nodes/Agent/Node_Borealis_Agent.jsx rename to Data/WebUI/src/nodes/Data Collection/Node_Borealis_Agent.jsx index 2478b37..867a517 100644 --- a/Data/WebUI/src/nodes/Agent/Node_Borealis_Agent.jsx +++ b/Data/WebUI/src/nodes/Data Collection/Node_Borealis_Agent.jsx @@ -27,7 +27,6 @@ const BorealisAgentNode = ({ id, data }) => { useEffect(() => { socket.on('new_screenshot', (data) => { - console.log("[DEBUG] Screenshot received", data); if (data.agent_id === selectedAgent) { setImageData(data.image_base64); imageRef.current = data.image_base64; @@ -54,7 +53,6 @@ const BorealisAgentNode = ({ id, data }) => { } return updated; }); - } }, window.BorealisUpdateRate || 100); @@ -101,11 +99,16 @@ const BorealisAgentNode = ({ id, data }) => { style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }} > - {Object.entries(agents).map(([id, info]) => ( - - ))} + {Object.entries(agents).map(([id, info]) => { + const statusLabel = info.status === "provisioned" + ? "(Provisioned)" + : "(Not Provisioned)"; + return ( + + ); + })} diff --git a/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx b/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx index 6720996..adcbe77 100644 --- a/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx +++ b/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx @@ -176,7 +176,7 @@ const DataNode = ({ id, data }) => { export default { type: "DataNode", // REQUIRED: unique identifier for the node type - label: "String / Number Data", + label: "String / Number", description: ` Foundational Data Node diff --git a/Data/server.py b/Data/server.py index e62adaa..082ceaf 100644 --- a/Data/server.py +++ b/Data/server.py @@ -65,7 +65,7 @@ def provision_agent(): # ---------------------------------------------- # Canvas Image Feed Viewer for Screenshot Agents # ---------------------------------------------- -@app.route("/api/agent//screenshot") +@app.route("/api/agent//screenshot/live") def screenshot_viewer(agent_id): if agent_configurations.get(agent_id, {}).get("task") != "screenshot": return "

Agent not provisioned as Screenshot Collector

", 400 @@ -173,8 +173,6 @@ def screenshot_viewer(agent_id): """ - - @app.route("/api/agent//screenshot/raw") # Fallback Non-Live Screenshot Preview Code for Legacy Purposes def screenshot_raw(agent_id): entry = latest_images.get(agent_id)