import React, { useState, useEffect } from 'react';
import { getAgents, getAgentTelemetry, updateAgentStatus, deployAgent, deleteAgent, getUserConfig, getUserAccessibleNodes } from '../../../services/apiService';
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
// Polling interval in ms
const POLLING_INTERVAL = 5000;
export default function AgentHarnessPage({ onNavigate }) {
const [agents, setAgents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showDeploy, setShowDeploy] = useState(false);
const fetchAgents = async () => {
try {
const data = await getAgents();
setAgents(data);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchAgents();
const interval = setInterval(fetchAgents, POLLING_INTERVAL);
return () => clearInterval(interval);
}, []);
return (
<div className="flex-1 h-full overflow-y-auto bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 p-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header Section */}
<div className="flex justify-between items-end mb-8 relative">
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-600/20 blur-[130px] rounded-full pointer-events-none -z-10" />
<div>
<h1 className="text-4xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 mb-2">
Agent Dashboard
</h1>
<p className="text-gray-500 dark:text-gray-400 text-sm tracking-wide uppercase">Cortex System Orchestrator</p>
</div>
{/* Deploy New Agent Button */}
<button
onClick={() => setShowDeploy(true)}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-semibold text-sm tracking-wide transition-all shadow-lg shadow-blue-900/30 hover:shadow-blue-600/40 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
Deploy New Agent
</button>
</div>
{error && (
<div className="bg-red-900/40 border border-red-500/50 text-red-200 px-6 py-4 rounded-xl flex items-center justify-between">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-200 cursor-pointer text-xl font-bold">×</button>
</div>
)}
{loading && agents.length === 0 ? (
<div className="flex items-center justify-center p-20 animate-pulse">
<div className="flex items-center space-x-3 text-blue-500 font-medium">
<div className="w-5 h-5 rounded-full border-t-2 border-l-2 border-blue-500 animate-spin" />
<span>Loading Agents...</span>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 relative z-10">
{agents.map(agent => (
<AgentCard key={agent.id} agent={agent} onNavigate={onNavigate} onStatusChange={fetchAgents} />
))}
{agents.length === 0 && (
<div className="col-span-full py-20 text-center text-gray-500 border border-dashed border-gray-300 dark:border-gray-700 rounded-2xl bg-white/50 dark:bg-gray-800/30">
No active agents found in the system.
</div>
)}
</div>
)}
{/* Deploy New Agent Modal */}
{showDeploy && (
<DeployAgentModal
onClose={() => setShowDeploy(false)}
onDeployed={() => {
setShowDeploy(false);
fetchAgents();
}}
/>
)}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Deploy Agent Modal
// ─────────────────────────────────────────────────────────────
const DeployAgentModal = ({ onClose, onDeployed }) => {
const [form, setForm] = useState({
name: '',
description: '',
system_prompt: '',
mesh_node_id: '',
max_loop_iterations: 20,
initial_prompt: '',
provider_name: '' // Dynamic LLM selection
});
const [deploying, setDeploying] = useState(false);
const [result, setResult] = useState(null);
const [deployError, setDeployError] = useState(null);
const [userConfig, setUserConfig] = useState(null);
const [nodes, setNodes] = useState([]);
useEffect(() => {
const loadConfig = async () => {
try {
const conf = await getUserConfig();
setUserConfig(conf);
const providers = Object.keys(conf?.effective?.llm?.providers || {});
const defaultProvider = conf?.effective?.llm?.active_provider || (providers.length > 0 ? providers[0] : '');
setForm(f => ({ ...f, provider_name: defaultProvider }));
} catch (e) {
console.warn("Failed to load user config for provider setup", e);
}
try {
const fetchedNodes = await getUserAccessibleNodes();
setNodes(fetchedNodes);
} catch (e) {
console.warn("Failed to load nodes", e);
}
};
loadConfig();
}, []);
const handleDeploy = async (e) => {
e.preventDefault();
if (!form.name.trim()) return;
setDeploying(true);
setDeployError(null);
try {
const res = await deployAgent({
...form,
mesh_node_id: form.mesh_node_id || null,
system_prompt: form.system_prompt || null,
initial_prompt: form.initial_prompt || null,
description: form.description || null,
provider_name: form.provider_name || null
});
setResult(res);
setTimeout(() => onDeployed(), 1500);
} catch (err) {
setDeployError(err.message);
} finally {
setDeploying(false);
}
};
const update = (key, val) => setForm(prev => ({ ...prev, [key]: val }));
return (
<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-lg overflow-hidden border border-gray-100 dark:border-gray-700 transform transition-all p-8 relative">
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 blur-[80px] rounded-full pointer-events-none -z-10" />
<div className="flex justify-between items-start mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-1">Deploy Agent</h2>
<p className="text-gray-500 dark:text-gray-400 text-sm">Configure a new autonomous instance to connect to the mesh.</p>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors text-xl">×</button>
</div>
{result ? (
<div className="text-center py-8">
<div className="text-5xl mb-4">🚀</div>
<p className="text-emerald-400 font-bold text-lg">{result.message}</p>
<p className="text-gray-500 text-xs mt-2 font-mono">ID: {result.instance_id}</p>
</div>
) : (
<form onSubmit={handleDeploy} className="space-y-5 relative z-10">
{/* Name & Provider */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-widest block mb-1.5">Agent Name</label>
<input
autoFocus
value={form.name}
onChange={e => update('name', e.target.value)}
placeholder="e.g. System Monitor, QA Tester"
className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100"
required
/>
</div>
<div>
<label className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-widest block mb-1.5">Active LLM Provider</label>
<select
value={form.provider_name}
onChange={e => update('provider_name', e.target.value)}
className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100"
>
{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>
{/* Description */}
<div>
<label className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-widest block mb-1.5">Description</label>
<input
value={form.description}
onChange={e => update('description', e.target.value)}
placeholder="Brief description of what this agent does"
className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100"
/>
</div>
{/* System Prompt */}
<div>
<label className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-widest block mb-1.5">System Prompt (Persona)</label>
<textarea
value={form.system_prompt}
onChange={e => update('system_prompt', e.target.value)}
placeholder="Define the agent's role and constraints..."
rows={3}
className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 resize-none"
/>
</div>
{/* Node + Iterations row */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-widest block mb-1.5">Target Mesh Node</label>
<select
value={form.mesh_node_id}
onChange={e => update('mesh_node_id', e.target.value)}
className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100"
>
<option value="">-- Auto-Schedule to Mesh --</option>
{nodes.map(n => <option key={n.id} value={n.id}>{n.name}</option>)}
</select>
</div>
<div>
<label className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-widest block mb-1.5">Max Iterations</label>
<input
type="number"
value={form.max_loop_iterations}
onChange={e => update('max_loop_iterations', parseInt(e.target.value) || 20)}
className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100"
/>
</div>
</div>
{/* Initial Prompt */}
<div>
<label className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-widest block mb-1.5">Initial Prompt (optional — starts loop immediately)</label>
<textarea
value={form.initial_prompt}
onChange={e => update('initial_prompt', e.target.value)}
placeholder="What should the agent do first? Leave empty for idle mode."
rows={2}
className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 resize-none"
/>
</div>
{deployError && (
<div className="bg-red-900/30 border border-red-500/30 text-red-300 text-sm px-4 py-2 rounded-lg">
{deployError}
</div>
)}
<button
type="submit"
disabled={deploying || !form.name.trim()}
className="w-full py-3 rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold tracking-wide transition-all disabled:opacity-40 disabled:cursor-not-allowed"
>
{deploying ? 'Deploying...' : '🚀 Deploy Agent'}
</button>
</form>
)}
</div>
</div>
);
};
// ─────────────────────────────────────────────────────────────
// Agent Card Component
// ─────────────────────────────────────────────────────────────
const AgentCard = ({ agent, onNavigate, onStatusChange }) => {
const [telemetryList, setTelemetryList] = useState([]);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deleteError, setDeleteError] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
let isMounted = true;
const fetchTelemetry = async () => {
try {
const t = await getAgentTelemetry(agent.id);
if (isMounted) {
setTelemetryList(prev => {
const next = [...prev, { time: new Date().toLocaleTimeString(), cpu: t.cpu_usage || 0, ram: t.memory_usage || 0 }];
return next.slice(-20);
});
}
} catch (err) {
console.error("Failed fetching telemetry for agent " + agent.id, err);
}
};
fetchTelemetry();
const interval = setInterval(fetchTelemetry, 3000);
return () => {
isMounted = false;
clearInterval(interval);
};
}, [agent.id]);
const handleAction = async (status) => {
try {
await updateAgentStatus(agent.id, status);
onStatusChange();
} catch (e) {
console.error('Failed to update status: ', e);
}
};
const confirmDelete = async () => {
setIsDeleting(true);
setDeleteError(null);
try {
await deleteAgent(agent.id);
onStatusChange();
} catch (e) {
setDeleteError(e.message);
setIsDeleting(false);
}
};
const isError = agent.status === 'error_suspended';
const isPaused = agent.status === 'paused_mid_loop';
const statusTheme = isError ? 'text-rose-400 bg-rose-500/10 border-rose-500/30 ring-rose-500/20' :
isPaused ? 'text-amber-400 bg-amber-500/10 border-amber-500/30 ring-amber-500/20' :
'text-emerald-400 bg-emerald-500/10 border-emerald-500/30 ring-emerald-500/20';
const latestTelemetry = telemetryList.length > 0 ? telemetryList[telemetryList.length - 1] : { cpu: 0, ram: 0 };
return (
<div className="group relative rounded-2xl bg-white dark:bg-gray-800 shadow-lg border border-gray-200 dark:border-gray-700 p-6 flex flex-col justify-between overflow-hidden transition-all duration-300 hover:shadow-indigo-500/10 hover:border-indigo-500/30">
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 blur-[80px] group-hover:bg-indigo-500/10 transition-colors pointer-events-none" />
{showDeleteConfirm && (
<div className="absolute inset-0 z-50 bg-gray-900/60 backdrop-blur-sm flex items-center justify-center p-4 rounded-2xl">
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-2xl rounded-xl p-5 w-full max-w-sm animate-in fade-in zoom-in duration-200">
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2">Confirm Deletion</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">Are you sure you want to delete this agent? This cannot be undone.</p>
{deleteError && (
<p className="text-xs text-red-500 mb-4 p-2 bg-red-50 dark:bg-red-500/10 rounded">{deleteError}</p>
)}
<div className="flex gap-3 justify-end mt-2">
<button
onClick={() => setShowDeleteConfirm(false)}
disabled={isDeleting}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
onClick={confirmDelete}
disabled={isDeleting}
className="px-4 py-2 text-sm font-bold text-white bg-red-600 hover:bg-red-500 rounded-lg shadow transition-colors disabled:opacity-50 flex items-center gap-2"
>
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
)}
<div className="relative z-10">
<div className="flex justify-between items-start mb-4">
<div className="truncate pr-4">
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 truncate cursor-pointer hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
onClick={() => onNavigate(`/agents/drilldown/${agent.id}`)}
title={agent.id}>
{agent.id.split('-')[0]}...
</h2>
<span className="text-[10px] uppercase tracking-widest text-gray-500 dark:text-gray-400 block mt-1">Instance ID</span>
</div>
<div className={`px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest border ring-1 ring-inset shadow-inner whitespace-nowrap ${statusTheme}`}>
{agent.status}
</div>
</div>
{/* Node Info */}
{agent.mesh_node_id && (
<div className="mb-3 text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-400">
Node: <span className="text-indigo-600 dark:text-indigo-400 font-bold">{agent.mesh_node_id}</span>
</div>
)}
<div className="mb-6 grid grid-cols-2 gap-4">
<div>
<span className="text-xs text-gray-500 dark:text-gray-400 block mb-1 uppercase tracking-wide">CPU</span>
<span className="text-xl font-medium text-gray-800 dark:text-gray-200">{latestTelemetry.cpu.toFixed(1)}%</span>
</div>
<div>
<span className="text-xs text-gray-500 dark:text-gray-400 block mb-1 uppercase tracking-wide">RAM</span>
<span className="text-xl font-medium text-gray-800 dark:text-gray-200">{latestTelemetry.ram}MB</span>
</div>
</div>
<div className="h-16 w-full mb-6">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={telemetryList}>
<defs>
<linearGradient id={"colorCpu" + agent.id} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
</linearGradient>
</defs>
<Area type="monotone" dataKey="cpu" stroke="#3b82f6" strokeWidth={2} fillOpacity={1} fill={`url(#colorCpu${agent.id})`} isAnimationActive={false} />
<Tooltip content={() => null} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="flex gap-2 relative z-10 border-t border-gray-200 dark:border-gray-800/50 pt-4 mt-auto">
<button
onClick={() => setShowDeleteConfirm(true)}
className="p-2 rounded-lg bg-gray-100 hover:bg-red-50 text-gray-500 hover:text-red-500 dark:bg-gray-800/50 dark:hover:bg-red-500/10 dark:hover:text-red-400 border border-gray-200 dark:border-gray-700 dark:hover:border-red-500/30 hover:border-red-200 transition-all duration-200"
title="Delete Agent"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
<button
onClick={() => handleAction('error_suspended')}
className="flex-1 py-1 rounded-lg bg-rose-50 dark:bg-rose-500/10 hover:bg-rose-100 dark:hover:bg-rose-500/20 border border-rose-200 dark:border-rose-500/20 text-rose-500 dark:text-rose-400 text-xs font-semibold uppercase tracking-wider transition-colors"
>
Kill-Switch
</button>
<button
onClick={() => handleAction(isPaused ? 'active' : 'paused_mid_loop')}
className={`flex-1 py-1 rounded-lg border text-xs font-semibold uppercase tracking-wider transition-colors ${
isPaused ? 'bg-emerald-50 dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20 border-emerald-200 dark:border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
: 'bg-amber-50 dark:bg-amber-500/10 hover:bg-amber-100 dark:hover:bg-amber-500/20 border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-500'
}`}
>
{isPaused ? 'Resume' : 'Pause'}
</button>
</div>
</div>
);
};