import React, { useEffect, useState, useRef } from 'react';
import { getNodeStreamUrl } from '../services/apiService';
const MultiNodeConsole = ({ attachedNodeIds }) => {
const [logs, setLogs] = useState({}); // node_id -> array of log strings
const scrollRefs = useRef({});
useEffect(() => {
if (!attachedNodeIds || attachedNodeIds.length === 0) return;
const ws = new WebSocket(getNodeStreamUrl());
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (!attachedNodeIds.includes(msg.node_id)) return;
const timestamp = new Date().toLocaleTimeString();
let logLine = "";
switch (msg.event) {
case 'task_assigned':
logLine = `[${timestamp}] 📥 ASSIGNED: ${msg.data.command || 'browser task'}`;
break;
case 'task_start':
logLine = `[${timestamp}] 🚀 START: Running payload...`;
break;
case 'task_complete':
logLine = `[${timestamp}] ✅ COMPLETE: Success`;
break;
case 'task_error':
logLine = `[${timestamp}] ❌ ERROR: ${JSON.stringify(msg.data)}`;
break;
case 'browser_event':
const type = msg.data.type === 'console' ? '🖥️' : '🌐';
logLine = `[${timestamp}] ${type} ${msg.data.text || msg.data.url}`;
break;
case 'sync_status':
logLine = `[${timestamp}] 📁 SYNC: ${msg.data.message}`;
break;
default:
return; // Ignore heartbeats etc.
}
setLogs(prev => ({
...prev,
[msg.node_id]: [...(prev[msg.node_id] || []), logLine].slice(-100)
}));
};
return () => ws.close();
}, [attachedNodeIds]);
// Handle auto-scroll
useEffect(() => {
Object.keys(scrollRefs.current).forEach(nodeId => {
const ref = scrollRefs.current[nodeId];
if (ref) ref.scrollTop = ref.scrollHeight;
});
}, [logs]);
if (!attachedNodeIds || attachedNodeIds.length === 0) return null;
return (
<div className="flex flex-col h-full bg-gray-950 text-gray-300 font-mono text-[10px] border-t dark:border-gray-800">
<div className="flex bg-gray-900 border-b dark:border-gray-800 px-4 py-1.5 justify-between items-center">
<span className="flex items-center">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"></span>
AGENT EXECUTION MESH
</span>
<span className="text-gray-500 uppercase tracking-widest text-[9px]">Live gRPC Stream</span>
</div>
<div className={`flex flex-1 overflow-hidden divide-x divide-gray-800`}>
{attachedNodeIds.map(nodeId => (
<div key={nodeId} className="flex-1 flex flex-col min-w-0">
<div className="bg-gray-900/50 px-3 py-1 border-b border-gray-800 text-indigo-400 font-bold flex justify-between">
<span>{nodeId}</span>
<span className="text-gray-600">NODE_CONTEXT</span>
</div>
<div
ref={el => scrollRefs.current[nodeId] = el}
className="flex-1 overflow-y-auto p-3 space-y-1 scrollbar-thin scrollbar-thumb-gray-800"
>
{(logs[nodeId] || ["[*] Awaiting task..."]).map((line, i) => (
<div key={i} className="whitespace-pre-wrap break-all opacity-80 hover:opacity-100 transition-opacity">
{line}
</div>
))}
</div>
</div>
))}
</div>
</div>
);
};
export default MultiNodeConsole;