Newer
Older
cortex-hub / ui / client-app / src / components / MultiNodeConsole.js
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;