Newer
Older
cortex-hub / frontend / src / features / agents / components / AgentDrillDown.js
import React, { useState, useEffect } from 'react';
import ChatWindow from '../../chat/components/ChatWindow';
import FileSystemNavigator from '../../../shared/components/FileSystemNavigator';
import { getAgents, getSessionMessages, fetchWithAuth, updateAgentConfig, getUserConfig, clearSessionHistory, getSessionTokenStatus, getAgentTriggers, createAgentTrigger, deleteAgentTrigger, getUserAccessibleNodes } from '../../../services/apiService';

export default function AgentDrillDown({ agentId, onNavigate }) {
    const [agent, setAgent] = useState(null);
    const [chatHistory, setChatHistory] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [overrideText, setOverrideText] = useState("");
    
    // Day 2 Configurations
    const [activeTab, setActiveTab] = useState('config'); // workspace or config
    const [editConfig, setEditConfig] = useState(null);
    const [saving, setSaving] = useState(false);
    const [userConfig, setUserConfig] = useState(null);
    const [tokenUsage, setTokenUsage] = useState({ token_count: 0, token_limit: 0, percentage: 0 });
    const [clearing, setClearing] = useState(false);
    const [triggers, setTriggers] = useState([]);
    const [newTriggerType, setNewTriggerType] = useState('webhook');
    const [newCronValue, setNewCronValue] = useState('0 * * * *');
    const [creatingTrigger, setCreatingTrigger] = useState(false);
    const [modalConfig, setModalConfig] = useState(null);
    const [nodes, setNodes] = useState([]);

    useEffect(() => {
        const loadConf = async () => {
            try {
                const conf = await getUserConfig();
                setUserConfig(conf);
            } catch (e) {}
            try {
                const nList = await getUserAccessibleNodes();
                setNodes(nList);
            } catch (e) {}
        };
        loadConf();
    }, []);

    const fetchData = async () => {
        try {
            // Find agent
            const allAgents = await getAgents();
            const found = allAgents.find(a => a.id === agentId);
            if (!found) throw new Error("Agent not found");
            setAgent(found);

            // Populate form only on first load using the agent context
            setEditConfig(prev => prev || {
                name: found.template?.name || "",
                system_prompt: found.template?.system_prompt_path || "",
                max_loop_iterations: found.template?.max_loop_iterations || 20,
                mesh_node_id: found.mesh_node_id || "",
                provider_name: ""
            });

            // Fetch chat history if session exists
            if (found.session_id) {
                const historyResp = await getSessionMessages(found.session_id);
                const formatted = (historyResp.messages || []).map(m => ({
                    text: m.content,
                    isUser: m.sender === 'user',
                    reasoning: m.reasoning_content,
                    status: null,
                    sender: m.sender,
                    timestamp: m.created_at,
                    id: m.id,
                    tool_calls: m.tool_calls
                }));
                setChatHistory(formatted);
                
                try {
                    const usage = await getSessionTokenStatus(found.session_id);
                    setTokenUsage(usage);
                } catch(e) {}

                try {
                    const tList = await getAgentTriggers(agentId);
                    setTriggers(tList);
                } catch(e) {}
            }
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
    };

    useEffect(() => {
        fetchData();
        const interval = setInterval(fetchData, 3000);
        return () => clearInterval(interval);
    }, [agentId]);

    const handleInjectOverride = async (e) => {
        e.preventDefault();
        if (!overrideText.trim() || !agent?.session_id) return;
        
        try {
            await fetchWithAuth(`/agents/${agentId}/webhook`, {
                method: "POST",
                body: { override_prompt: overrideText }
            });
            setOverrideText("");
            fetchData();
        } catch (err) {
            setModalConfig({ title: 'Injection Failed', message: err.message, type: 'error' });
        }
    };

    const handleSaveConfig = async () => {
        try {
            setSaving(true);
            const payload = {
                name: editConfig.name,
                system_prompt: editConfig.system_prompt,
                max_loop_iterations: parseInt(editConfig.max_loop_iterations, 10) || 20,
                mesh_node_id: editConfig.mesh_node_id || null
            };
            if (editConfig.provider_name) {
                payload.provider_name = editConfig.provider_name;
            }
            await updateAgentConfig(agentId, payload);
            fetchData();
            setModalConfig({ title: 'Success', message: 'Configuration Saved Successfully!', type: 'success' });
        } catch (err) {
            setModalConfig({ title: 'Save Failed', message: err.message, type: 'error' });
        } finally {
            setSaving(false);
        }
    };

    const handleAddTrigger = async () => {
        try {
            setCreatingTrigger(true);
            const payload = { trigger_type: newTriggerType };
            if (newTriggerType === 'cron') payload.cron_expression = newCronValue;
            await createAgentTrigger(agentId, payload);
            fetchData();
        } catch (err) {
            setModalConfig({ title: 'Trigger Failed', message: err.message, type: 'error' });
        } finally {
            setCreatingTrigger(false);
        }
    };

    const handleDeleteTrigger = async (triggerId) => {
        try {
            await deleteAgentTrigger(triggerId);
            fetchData();
        } catch (err) {
            setModalConfig({ title: 'Delete Failed', message: err.message, type: 'error' });
        }
    };

    if (loading && !agent) return (
        <div className="flex h-screen items-center justify-center bg-[#070b14] text-white">
            <div className="animate-spin w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full font-mono text-sm ml-4"></div>
        </div>
    );

    if (error) return (
        <div className="flex h-screen items-center justify-center bg-gray-950 text-red-500">
            Error loading Agent Drilldown: {error}
        </div>
    );

    return (
        <div className="flex flex-col flex-grow h-full bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans overflow-hidden">
            
            {/* Minimal Header */}
            <div className="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800 px-6 py-4 flex items-center justify-between z-10 shrink-0 shadow-sm">
                <div className="flex items-center space-x-4">
                    <button onClick={() => onNavigate('/agents')} className="text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors p-2 -ml-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
                        <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
                    </button>
                    <div>
                        <h2 className="text-xl font-bold tracking-tight">{agent?.id?.split('-')[0]} Dashboard</h2>
                        <div className="flex items-center space-x-2 mt-1">
                            <span className="flex h-2 w-2 relative">
                                {agent?.status === 'active' && <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>}
                                <span className={`relative inline-flex rounded-full h-2 w-2 ${agent?.status === 'active' ? 'bg-emerald-500' : 'bg-amber-500'}`}></span>
                            </span>
                            <span className="text-[10px] text-gray-500 dark:text-gray-400 uppercase tracking-widest font-mono">Status: {agent?.status}</span>
                        </div>
                    </div>
                </div>
                <div className="flex items-center space-x-4 font-mono text-xs text-gray-500 dark:text-gray-400">
                    <span>Node: <span className="text-indigo-600 dark:text-indigo-400 font-bold">{nodes.find(n => n.id === agent?.mesh_node_id)?.name || agent?.mesh_node_id || 'unassigned'}</span></span>
                    <span>Jail: <span className="text-pink-600 dark:text-pink-400 font-bold">{agent?.current_workspace_jail || '/tmp'}</span></span>
                </div>
            </div>

            {/* Main Content Area - 50/50 Split */}
            <div className="flex-1 grid grid-cols-2 overflow-hidden gap-1 p-1 bg-gray-200 dark:bg-gray-950">
                
                {/* Left Pane: Chat Tracker */}
                <div className="flex flex-col bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800/80 rounded-xl overflow-hidden relative shadow-lg">
                    <div className="px-4 py-2 border-b border-gray-200 dark:border-gray-800/80 bg-gray-50 dark:bg-gray-900/50 backdrop-blur-md flex justify-between items-center shrink-0">
                        <span className="text-[10px] uppercase tracking-[0.2em] font-black text-indigo-400 flex items-center gap-2">
                           <div className="w-1.5 h-1.5 bg-indigo-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(99,102,241,0.5)]"></div>
                           Live Thought Process
                        </span>
                    </div>

                    <div className="flex-1 overflow-hidden relative">
                         {agent?.session_id ? (
                            <ChatWindow 
                                chatHistory={chatHistory} 
                                maxHeight="100%" 
                                autoCollapse={true} 
                            />
                         ) : (
                             <div className="flex h-full items-center justify-center text-gray-500 italic text-sm">
                                 No session bounds established for this agent.
                             </div>
                         )}
                    </div>

                    {/* Inject Prompt Override */}
                    <div className="p-4 border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/90 shrink-0">
                        <form onSubmit={handleInjectOverride} className="relative flex items-center">
                            <input 
                                type="text" 
                                value={overrideText}
                                onChange={(e) => setOverrideText(e.target.value)}
                                placeholder="Steer agent execution loop..." 
                                className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-xl py-3 pl-4 pr-12 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 transition-all font-mono"
                            />
                            <button 
                                type="submit" 
                                disabled={!overrideText.trim()}
                                className="absolute right-2 px-3 py-1.5 bg-indigo-600/20 text-indigo-400 rounded-lg text-xs font-bold hover:bg-indigo-600 hover:text-white transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-indigo-400 uppercase tracking-wider"
                            >
                                Inject
                            </button>
                        </form>
                    </div>
                </div>

                {/* Right Pane: Multi-Tab Container */}
                <div className="flex flex-col bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800/80 rounded-xl overflow-hidden shadow-lg">
                    {/* Tab Header */}
                    <div className="flex border-b border-gray-200 dark:border-gray-800/80 bg-gray-50 dark:bg-gray-900/50 px-2 pt-2 shrink-0">
                        <button 
                            onClick={() => setActiveTab('config')}
                            className={`px-4 py-2 text-xs font-bold uppercase tracking-widest rounded-t-lg transition-colors ${activeTab === 'config' ? 'bg-white dark:bg-gray-900 text-indigo-500 border-t border-l border-r border-gray-200 dark:border-gray-800/80 -mb-px' : 'text-gray-500 hover:text-gray-400'}`}
                        >
                            Metadata & System
                        </button>
                        <button 
                            onClick={() => setActiveTab('workspace')}
                            className={`px-4 py-2 text-xs font-bold uppercase tracking-widest rounded-t-lg transition-colors ${activeTab === 'workspace' ? 'bg-white dark:bg-gray-900 text-indigo-500 border-t border-l border-r border-gray-200 dark:border-gray-800/80 -mb-px' : 'text-gray-500 hover:text-gray-400'}`}
                        >
                            Mesh Workspace
                        </button>
                    </div>

                    {/* Tab Body */}
                    <div className="flex-1 overflow-auto relative">
                        {activeTab === 'workspace' && (
                            agent ? (
                                <FileSystemNavigator 
                                    nodeId={agent.mesh_node_id || "local"} 
                                    initialPath={agent.current_workspace_jail || "."} 
                                    sessionId={agent.session_id}
                                    showSyncStatus={true}
                                />
                            ) : (
                                <div className="flex h-full items-center justify-center text-gray-500">
                                    Initializing workspace bridge...
                                </div>
                            )
                        )}
                        
                        {activeTab === 'config' && (
                            <div className="p-6 flex flex-col gap-6">
                                {/* Token & History Utilities */}
                                <div className="p-4 rounded-xl border border-gray-200 dark:border-gray-700/50 bg-gray-50 dark:bg-gray-800/30 flex justify-between items-center">
                                    <div className="flex flex-col">
                                        <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold mb-1">Session Context Window</span>
                                        <div className="flex items-center gap-3 w-48">
                                            <div className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
                                                <div 
                                                    className="h-full bg-gradient-to-r from-emerald-500 to-indigo-500" 
                                                    style={{ width: `${Math.min(tokenUsage.percentage, 100)}%` }}
                                                />
                                            </div>
                                            <span className="text-xs font-mono whitespace-nowrap text-gray-500">{Math.round(tokenUsage.percentage)}%</span>
                                        </div>
                                    </div>
                                </div>

                                <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                                    <div>
                                        <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-1">Agent Name</span>
                                        <input type="text" value={editConfig?.name || ""} onChange={(e) => setEditConfig({...editConfig, name: e.target.value})} className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2.5 px-3 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 shadow-sm" placeholder="e.g. Documentation Assistant" />
                                    </div>
                                    <div>
                                        <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-1">Active LLM Provider</span>
                                        <select
                                            value={editConfig?.provider_name || userConfig?.effective?.llm?.active_provider || ""}
                                            onChange={e => setEditConfig({...editConfig, provider_name: e.target.value})}
                                            className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2.5 px-3 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 shadow-sm"
                                        >
                                            {userConfig?.effective?.llm?.providers && Object.keys(userConfig.effective.llm.providers).map(pid => (
                                                <option key={pid} value={pid}>{pid} {userConfig.effective.llm.providers[pid].model ? `(${userConfig.effective.llm.providers[pid].model})` : ''}</option>
                                            ))}
                                        </select>
                                    </div>
                                    <div className="grid grid-cols-2 gap-4">
                                        <div>
                                            <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-1">Target Mesh Node</span>
                                            <select 
                                                value={editConfig?.mesh_node_id || ""} 
                                                onChange={(e) => setEditConfig({...editConfig, mesh_node_id: e.target.value})} 
                                                className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2.5 px-3 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 shadow-sm"
                                            >
                                                <option value="">-- Let System Decide --</option>
                                                {nodes.map(n => <option key={n.id} value={n.id}>{n.name}</option>)}
                                            </select>
                                        </div>
                                        <div>
                                            <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-1">Max Iterations</span>
                                            <input type="number" min="1" max="100" value={editConfig?.max_loop_iterations || 20} onChange={(e) => setEditConfig({...editConfig, max_loop_iterations: e.target.value})} className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2.5 px-3 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 shadow-sm" />
                                        </div>
                                    </div>
                                </div>
                                
                                {/* Execution Triggers Box */}
                                <div>
                                    <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-2">Execution Triggers</span>
                                    <div className="bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 rounded-lg p-4 shadow-sm">
                                        
                                        {/* List existing triggers */}
                                        {triggers.length > 0 && (
                                            <div className="mb-4 space-y-2">
                                                {triggers.map(t => (
                                                    <div key={t.id} className="flex items-center justify-between bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded px-3 py-2 text-sm">
                                                        <div className="flex flex-col">
                                                            <span className="font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wider text-[10px]">{t.trigger_type} ID: {t.id.split('-')[0]}</span>
                                                            <span className="font-mono text-gray-500 dark:text-gray-400 text-xs">
                                                                {t.trigger_type === 'cron' ? `Schedule: ${t.cron_expression}` : `Secret: ${t.webhook_secret}`}
                                                            </span>
                                                        </div>
                                                        <button 
                                                            onClick={e => { e.preventDefault(); handleDeleteTrigger(t.id); }}
                                                            className="text-red-500 hover:text-red-700 font-bold text-xs bg-red-50 dark:bg-red-900/20 px-2 py-1 rounded"
                                                        >
                                                            Delete
                                                        </button>
                                                    </div>
                                                ))}
                                            </div>
                                        )}
                                        
                                        {/* Add new trigger form */}
                                        <div className="flex items-end gap-3 border-t border-gray-200 dark:border-gray-800 pt-4 mt-2">
                                            <div className="w-1/3">
                                                <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-1">Type</span>
                                                <select 
                                                    value={newTriggerType} 
                                                    onChange={e => setNewTriggerType(e.target.value)}
                                                    className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2 px-3 focus:outline-none"
                                                >
                                                    <option value="webhook">Webhook Listener</option>
                                                    <option value="cron">CRON Schedule</option>
                                                </select>
                                            </div>
                                            
                                            {newTriggerType === 'cron' && (
                                                <div className="flex-1">
                                                    <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-1">CRON Expr</span>
                                                    <input 
                                                        type="text" 
                                                        value={newCronValue} 
                                                        onChange={e => setNewCronValue(e.target.value)}
                                                        className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2 px-3 font-mono focus:outline-none" 
                                                    />
                                                </div>
                                            )}
                                            
                                            <button 
                                                onClick={e => { e.preventDefault(); handleAddTrigger(); }}
                                                disabled={creatingTrigger}
                                                className="px-4 py-2 text-xs font-bold bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800 hover:bg-indigo-200 dark:hover:bg-indigo-800/50 rounded-md transition-colors whitespace-nowrap"
                                            >
                                                + Add Trigger
                                            </button>
                                        </div>
                                    </div>
                                </div>
                                
                                <div className="flex-1 flex flex-col min-h-[300px]">
                                    <span className="uppercase text-[10px] tracking-widest text-gray-500 font-bold block mb-2">Core System Instruction Prompt (Modifiable Live)</span>
                                    <textarea value={editConfig?.system_prompt || ""} onChange={(e) => setEditConfig({...editConfig, system_prompt: e.target.value})} className="flex-1 resize-none w-full bg-gray-900 text-emerald-400 p-4 rounded-lg text-sm leading-relaxed font-mono shadow-inner border border-gray-700 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" placeholder="You are an expert AI agent..."></textarea>
                                    <p className="text-xs text-gray-500 mt-2 font-mono">Changes to the system prompt will be immediately picked up by the agent loop on its next invocation turn.</p>
                                </div>
                                <div className="flex justify-end pt-4">
                                    <button onClick={handleSaveConfig} disabled={saving} className="px-6 py-2.5 text-sm font-bold bg-indigo-600 hover:bg-indigo-700 text-white shadow-md rounded-lg transition-all active:scale-95 flex items-center gap-2">
                                        {saving ? "Saving Configuration..." : "Apply Live Configuration"}
                                    </button>
                                </div>
                            </div>
                        )}
                    </div>
                </div>

            </div>
            
            {/* Modal Overlay Component */}
            {modalConfig && (
                <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/40 backdrop-blur-sm p-4">
                    <div className="bg-white dark:bg-gray-800 shadow-2xl rounded-2xl w-full max-w-sm overflow-hidden border border-gray-100 dark:border-gray-700 transform transition-all p-6 relative">
                        <div className={`absolute top-0 right-0 w-64 h-64 ${modalConfig.type === 'error' ? 'bg-red-500/5' : 'bg-emerald-500/5'} blur-[80px] rounded-full pointer-events-none -z-10`} />
                        <h3 className={`text-xl font-bold mb-2 ${modalConfig.type === 'error' ? 'text-red-500' : 'text-emerald-500'}`}>{modalConfig.title}</h3>
                        <p className="text-gray-600 dark:text-gray-300 text-sm mb-6 font-mono">{modalConfig.message}</p>
                        <div className="flex justify-end">
                            <button onClick={() => setModalConfig(null)} className="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-lg text-sm font-semibold transition-colors">
                                Close
                            </button>
                        </div>
                    </div>
                </div>
            )}
        </div>
    );
}