Implemented Live Macro Window Detection
This commit is contained in:
@ -43,13 +43,27 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
|
||||
return config.map((field, index) => {
|
||||
const value = nodeData?.[field.key] || "";
|
||||
|
||||
return (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
|
||||
{field.label || field.key}
|
||||
</Typography>
|
||||
// ---- DYNAMIC DROPDOWN SUPPORT ----
|
||||
if (field.type === "select") {
|
||||
let options = field.options || [];
|
||||
|
||||
{field.type === "select" ? (
|
||||
// Handle dynamic options for things like Target Window
|
||||
if (field.dynamicOptions && nodeData?.windowList && Array.isArray(nodeData.windowList)) {
|
||||
options = nodeData.windowList
|
||||
.map(win => ({
|
||||
value: String(win.handle),
|
||||
label: `${win.title} (${win.handle})`
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
|
||||
} else {
|
||||
options = options.map(opt => ({ value: opt, label: opt }));
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
|
||||
{field.label || field.key}
|
||||
</Typography>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
@ -112,42 +126,57 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(field.options || []).map((opt, idx) => (
|
||||
<MenuItem key={idx} value={opt}>
|
||||
{opt}
|
||||
{options.length === 0 ? (
|
||||
<MenuItem disabled value="">
|
||||
{field.label === "Target Window"
|
||||
? "No windows detected"
|
||||
: "No options"}
|
||||
</MenuItem>
|
||||
))}
|
||||
) : (
|
||||
options.map((opt, idx) => (
|
||||
<MenuItem key={idx} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))
|
||||
)}
|
||||
</TextField>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
// ---- END DYNAMIC DROPDOWN SUPPORT ----
|
||||
|
||||
) : (
|
||||
<TextField
|
||||
variant="outlined"
|
||||
size="small"
|
||||
fullWidth
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
if (!nodeId) return;
|
||||
effectiveSetNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === nodeId
|
||||
? { ...n, data: { ...n.data, [field.key]: newValue } }
|
||||
: n
|
||||
)
|
||||
);
|
||||
window.BorealisValueBus[nodeId] = newValue;
|
||||
}}
|
||||
InputProps={{
|
||||
sx: {
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#ccc",
|
||||
"& fieldset": { borderColor: "#444" },
|
||||
"&:hover fieldset": { borderColor: "#666" },
|
||||
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
|
||||
{field.label || field.key}
|
||||
</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
size="small"
|
||||
fullWidth
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
if (!nodeId) return;
|
||||
effectiveSetNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === nodeId
|
||||
? { ...n, data: { ...n.data, [field.key]: newValue } }
|
||||
: n
|
||||
)
|
||||
);
|
||||
window.BorealisValueBus[nodeId] = newValue;
|
||||
}}
|
||||
InputProps={{
|
||||
sx: {
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#ccc",
|
||||
"& fieldset": { borderColor: "#444" },
|
||||
"&:hover fieldset": { borderColor: "#666" },
|
||||
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
@ -17,21 +17,130 @@ const OPERATION_MODES = [
|
||||
"Trigger-Continuous"
|
||||
];
|
||||
|
||||
const MACRO_TYPES = [
|
||||
"keypress",
|
||||
"typed_text"
|
||||
];
|
||||
|
||||
const statusColors = {
|
||||
idle: "#333",
|
||||
running: "#00d18c",
|
||||
error: "#ff4f4f",
|
||||
success: "#00d18c"
|
||||
};
|
||||
|
||||
const MacroKeyPressNode = ({ id, data }) => {
|
||||
const { setNodes, getNodes } = useReactFlow();
|
||||
const edges = useStore((state) => state.edges);
|
||||
const [windowList, setWindowList] = useState([]);
|
||||
const [status, setStatus] = useState({ state: "idle", message: "" });
|
||||
const socketRef = useRef(null);
|
||||
|
||||
// Determine if agent is connected
|
||||
const agentEdge = edges.find((e) => e.target === id && e.targetHandle === "agent");
|
||||
const agentNode = agentEdge && getNodes().find((n) => n.id === agentEdge.source);
|
||||
const agentConnection = !!(agentNode && agentNode.data && agentNode.data.agent_id);
|
||||
const agent_id = agentNode && agentNode.data && agentNode.data.agent_id;
|
||||
|
||||
// Macro run/trigger state (sidebar sets this via config, but node UI just shows status)
|
||||
const running = data?.active === true || data?.active === "true";
|
||||
|
||||
// Node UI (no config fields, only status)
|
||||
// Store for last macro error/status
|
||||
const [lastMacroStatus, setLastMacroStatus] = useState({ success: true, message: "", timestamp: null });
|
||||
|
||||
// Setup WebSocket for agent macro status updates
|
||||
useEffect(() => {
|
||||
if (!window.BorealisSocket) return;
|
||||
const socket = window.BorealisSocket;
|
||||
socketRef.current = socket;
|
||||
|
||||
function handleMacroStatus(payload) {
|
||||
if (
|
||||
payload &&
|
||||
payload.agent_id === agent_id &&
|
||||
payload.node_id === id
|
||||
) {
|
||||
setLastMacroStatus({
|
||||
success: !!payload.success,
|
||||
message: payload.message || "",
|
||||
timestamp: payload.timestamp || Date.now()
|
||||
});
|
||||
setStatus({
|
||||
state: payload.success ? "success" : "error",
|
||||
message: payload.message || (payload.success ? "Success" : "Error")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
socket.on("macro_status", handleMacroStatus);
|
||||
return () => {
|
||||
socket.off("macro_status", handleMacroStatus);
|
||||
};
|
||||
}, [agent_id, id]);
|
||||
|
||||
// Auto-refresh window list from agent
|
||||
useEffect(() => {
|
||||
let intervalId = null;
|
||||
async function fetchWindows() {
|
||||
if (window.BorealisSocket && agentConnection) {
|
||||
window.BorealisSocket.emit("list_agent_windows", {
|
||||
agent_id
|
||||
});
|
||||
}
|
||||
}
|
||||
fetchWindows();
|
||||
intervalId = setInterval(fetchWindows, WINDOW_LIST_REFRESH_MS);
|
||||
|
||||
// Listen for agent_window_list updates
|
||||
function handleAgentWindowList(payload) {
|
||||
if (payload?.agent_id === agent_id && Array.isArray(payload.windows)) {
|
||||
setWindowList(payload.windows);
|
||||
|
||||
// Store windowList in node data for sidebar dynamic dropdowns
|
||||
setNodes(nds =>
|
||||
nds.map(n =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, windowList: payload.windows } }
|
||||
: n
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (window.BorealisSocket) {
|
||||
window.BorealisSocket.on("agent_window_list", handleAgentWindowList);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
if (window.BorealisSocket) {
|
||||
window.BorealisSocket.off("agent_window_list", handleAgentWindowList);
|
||||
}
|
||||
};
|
||||
}, [agent_id, agentConnection, setNodes, id]);
|
||||
|
||||
// UI: Start/Pause Button
|
||||
const handleToggleMacro = () => {
|
||||
setNodes(nds =>
|
||||
nds.map(n =>
|
||||
n.id === id
|
||||
? {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
active: n.data?.active === true || n.data?.active === "true" ? "false" : "true"
|
||||
}
|
||||
}
|
||||
: n
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// Optional: Show which window is targeted by name
|
||||
const selectedWindow = (windowList || []).find(w => String(w.handle) === String(data?.window_handle));
|
||||
|
||||
// Node UI (no config fields, only status + window list)
|
||||
return (
|
||||
<div className="borealis-node" style={{ minWidth: 240, position: "relative" }}>
|
||||
<div className="borealis-node" style={{ minWidth: 280, position: "relative" }}>
|
||||
{/* --- INPUT LABELS & HANDLES --- */}
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
@ -85,16 +194,105 @@ const MacroKeyPressNode = ({ id, data }) => {
|
||||
transform: "translateY(-50%)",
|
||||
height: "10px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: running ? "#00d18c" : "#333",
|
||||
backgroundColor:
|
||||
status.state === "error"
|
||||
? statusColors.error
|
||||
: running
|
||||
? statusColors.running
|
||||
: statusColors.idle,
|
||||
border: "1px solid #222"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="borealis-node-content">
|
||||
<strong>Status</strong>: {running ? "Running" : "Idle"}
|
||||
<strong>Status</strong>:{" "}
|
||||
{status.state === "error"
|
||||
? (
|
||||
<span style={{ color: "#ff4f4f" }}>
|
||||
Error{lastMacroStatus.message ? `: ${lastMacroStatus.message}` : ""}
|
||||
</span>
|
||||
)
|
||||
: running
|
||||
? (
|
||||
<span style={{ color: "#00d18c" }}>
|
||||
Running{lastMacroStatus.message ? ` (${lastMacroStatus.message})` : ""}
|
||||
</span>
|
||||
)
|
||||
: "Idle"}
|
||||
<br />
|
||||
<strong>Agent Connection</strong>: {agentConnection ? "Connected" : "Not Connected"}
|
||||
<br />
|
||||
<strong>Target Window</strong>:{" "}
|
||||
{selectedWindow
|
||||
? `${selectedWindow.title} (${selectedWindow.handle})`
|
||||
: data?.window_handle
|
||||
? `Handle: ${data.window_handle}`
|
||||
: <span style={{ color: "#888" }}>Not set</span>}
|
||||
<br />
|
||||
<strong>Mode</strong>: {data?.operation_mode || DEFAULT_OPERATION_MODE}
|
||||
<br />
|
||||
<strong>Macro Type</strong>: {data?.macro_type || "keypress"}
|
||||
<br />
|
||||
<button
|
||||
onClick={handleToggleMacro}
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: "4px 10px",
|
||||
background: running ? "#3a3a3a" : "#0475c2",
|
||||
color: running ? "#fff" : "#fff",
|
||||
border: "1px solid #0475c2",
|
||||
borderRadius: 3,
|
||||
fontSize: "11px",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
{running ? "Pause Macro" : "Start Macro"}
|
||||
</button>
|
||||
<br />
|
||||
<span style={{ fontSize: "9px", color: "#aaa" }}>
|
||||
{lastMacroStatus.timestamp
|
||||
? `Last event: ${new Date(lastMacroStatus.timestamp).toLocaleTimeString()}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Show available windows for debug (hidden from sidebar, but helpful for quick dropdown) */}
|
||||
<div style={{ marginTop: 8, fontSize: "9px", color: "#58a6ff" }}>
|
||||
<b>Windows:</b>{" "}
|
||||
{windowList.length === 0
|
||||
? <span style={{ color: "#999" }}>No windows detected.</span>
|
||||
: windowList.map((w) =>
|
||||
<span
|
||||
key={w.handle}
|
||||
style={{
|
||||
background:
|
||||
String(w.handle) === String(data?.window_handle)
|
||||
? "#333"
|
||||
: "transparent",
|
||||
borderRadius: 3,
|
||||
padding: "1px 4px",
|
||||
marginRight: 4,
|
||||
border:
|
||||
String(w.handle) === String(data?.window_handle)
|
||||
? "1px solid #58a6ff"
|
||||
: "1px solid #222",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={() =>
|
||||
setNodes(nds =>
|
||||
nds.map(n =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, window_handle: w.handle } }
|
||||
: n
|
||||
)
|
||||
)
|
||||
}
|
||||
title={w.title}
|
||||
>
|
||||
{w.title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -111,17 +309,18 @@ Supports manual, continuous, trigger, and one-shot modes for automation and even
|
||||
content: "Send Key Press or Typed Text to Window via Agent",
|
||||
component: MacroKeyPressNode,
|
||||
config: [
|
||||
{ key: "window_handle", label: "Target Window", type: "text", defaultValue: "" },
|
||||
{ key: "macro_type", label: "Macro Type", type: "select", options: ["keypress", "typed_text"], defaultValue: "keypress" },
|
||||
{ key: "key", label: "Key", type: "text", defaultValue: "" },
|
||||
{ key: "text", label: "Typed Text", type: "text", defaultValue: "" },
|
||||
{ key: "interval_ms", label: "Interval (ms)", type: "text", defaultValue: "1000" },
|
||||
{ key: "randomize_interval", label: "Randomize Interval", type: "select", options: ["true", "false"], defaultValue: "false" },
|
||||
{ key: "random_min", label: "Random Min (ms)", type: "text", defaultValue: "750" },
|
||||
{ key: "random_max", label: "Random Max (ms)", type: "text", defaultValue: "950" },
|
||||
{ key: "operation_mode", label: "Operation Mode", type: "select", options: OPERATION_MODES, defaultValue: "Continuous" },
|
||||
{ key: "active", label: "Macro Enabled", type: "select", options: ["true", "false"], defaultValue: "false" }
|
||||
],
|
||||
{ key: "window_handle", label: "Target Window", type: "select", dynamicOptions: true, defaultValue: "" },
|
||||
{ key: "macro_type", label: "Macro Type", type: "select", options: ["keypress", "typed_text"], defaultValue: "keypress" },
|
||||
{ key: "key", label: "Key", type: "text", defaultValue: "" },
|
||||
{ key: "text", label: "Typed Text", type: "text", defaultValue: "" },
|
||||
{ key: "interval_ms", label: "Interval (ms)", type: "text", defaultValue: "1000" },
|
||||
{ key: "randomize_interval", label: "Randomize Interval", type: "select", options: ["true", "false"], defaultValue: "false" },
|
||||
{ key: "random_min", label: "Random Min (ms)", type: "text", defaultValue: "750" },
|
||||
{ key: "random_max", label: "Random Max (ms)", type: "text", defaultValue: "950" },
|
||||
{ key: "operation_mode", label: "Operation Mode", type: "select", options: OPERATION_MODES, defaultValue: "Continuous" },
|
||||
{ key: "active", label: "Macro Enabled", type: "select", options: ["true", "false"], defaultValue: "false" },
|
||||
{ key: "trigger", label: "Trigger Value", type: "text", defaultValue: "0" }
|
||||
],
|
||||
usage_documentation: `
|
||||
### Agent Role: Macro
|
||||
|
||||
@ -141,6 +340,9 @@ Supports manual, continuous, trigger, and one-shot modes for automation and even
|
||||
**Event-Driven Support:**
|
||||
- Chain with other Borealis nodes (text recognition, event triggers, etc).
|
||||
|
||||
**Live Status:**
|
||||
- Displays last agent macro event and error feedback in node.
|
||||
|
||||
---
|
||||
`.trim()
|
||||
};
|
||||
|
@ -256,6 +256,40 @@ def receive_screenshot(data):
|
||||
def on_disconnect():
|
||||
print("[WebSocket] Connection Disconnected")
|
||||
|
||||
# Macro Websocket Handlers
|
||||
@socketio.on("macro_status")
|
||||
def receive_macro_status(data):
|
||||
"""
|
||||
Receives macro status/errors from agent and relays to all clients
|
||||
Expected payload: {
|
||||
"agent_id": ...,
|
||||
"node_id": ...,
|
||||
"success": True/False,
|
||||
"message": "...",
|
||||
"timestamp": ...
|
||||
}
|
||||
"""
|
||||
print(f"[Macro Status] Agent {data.get('agent_id')} Node {data.get('node_id')} Success: {data.get('success')} Msg: {data.get('message')}")
|
||||
emit("macro_status", data, broadcast=True)
|
||||
|
||||
@socketio.on("list_agent_windows")
|
||||
def handle_list_agent_windows(data):
|
||||
"""
|
||||
Forwards list_agent_windows event to all agents (or filter for a specific agent_id).
|
||||
"""
|
||||
agent_id = data.get("agent_id")
|
||||
# You can target a specific agent if you track rooms/sessions.
|
||||
# For now, broadcast to all agents so the correct one can reply.
|
||||
emit("list_agent_windows", data, broadcast=True)
|
||||
|
||||
@socketio.on("agent_window_list")
|
||||
def handle_agent_window_list(data):
|
||||
"""
|
||||
Relay the list of windows from the agent back to all connected clients.
|
||||
"""
|
||||
emit("agent_window_list", data, broadcast=True)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Server Launch
|
||||
# ---------------------------------------------
|
||||
|
Reference in New Issue
Block a user