340 lines
15 KiB
TypeScript
340 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
const AGENT_CONFIG: Record<string, { name: string; avatar: string; color: string; role: string; description: string }> = {
|
|
horus: { name: "Horus", avatar: "👁️", color: "#ff7bc0", role: "Master Orchestrator", description: "Command center, delegates tasks, manages all agents" },
|
|
thoth: { name: "Thoth", avatar: "🧠", color: "#8b5cf6", role: "Strategy & Research", description: "SiteMente planning, research, analysis" },
|
|
"thoth-trading": { name: "Thoth Trading", avatar: "📈", color: "#f59e0b", role: "Market Research", description: "Crypto market analysis and research" },
|
|
ptah: { name: "Ptah", avatar: "🏗️", color: "#10b981", role: "Dev & Ops", description: "Development, deployment, technical implementation" },
|
|
seshat: { name: "Seshat", avatar: "📜", color: "#ec4899", role: "Content & SEO", description: "Content strategy, SEO, marketing copy" },
|
|
anubis: { name: "Anubis", avatar: "🐺", color: "#6366f1", role: "Outreach & Growth", description: "Lead generation, client acquisition" },
|
|
sekhmet: { name: "Sekhmet", avatar: "⚔️", color: "#ef4444", role: "Risk Management", description: "Trade execution and risk" },
|
|
};
|
|
|
|
export default function AgentCenterPage({ params }: { params: { id: string } }) {
|
|
const router = useRouter();
|
|
const agentId = params.id;
|
|
const agent = AGENT_CONFIG[agentId] || { name: agentId, avatar: "🤖", color: "#666", role: "Agent", description: "" };
|
|
|
|
const [tasks, setTasks] = useState<any[]>([]);
|
|
const [templates, setTemplates] = useState<any[]>([]);
|
|
const [logs, setLogs] = useState<any[]>([]);
|
|
const [changelog, setChangelog] = useState<any[]>([]);
|
|
const [brainownNotes, setBrainownNotes] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [agentStatus, setAgentStatus] = useState<"idle" | "active" | "error">("idle");
|
|
const [tokens, setTokens] = useState("0");
|
|
const [lastRun, setLastRun] = useState<string | null>(null);
|
|
|
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({
|
|
tasks: true,
|
|
recurring: true,
|
|
logs: true,
|
|
changelog: true,
|
|
brainown: true,
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchAllData();
|
|
}, [agentId]);
|
|
|
|
const fetchAllData = async () => {
|
|
setLoading(true);
|
|
|
|
try {
|
|
const tRes = await fetch("/api/tasks");
|
|
if (tRes.ok) {
|
|
const allTasks = await tRes.json();
|
|
const agentTasks = allTasks.filter((t: any) =>
|
|
t.assignee === agentId || t.project === getAgentProject(agentId)
|
|
);
|
|
setTasks(agentTasks);
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
const rRes = await fetch("/api/recurring");
|
|
if (rRes.ok) {
|
|
const allTemplates = await rRes.json();
|
|
const agentTemplates = allTemplates.filter((t: any) => t.agent === agentId);
|
|
setTemplates(agentTemplates);
|
|
if (agentTemplates.length > 0) {
|
|
const last = agentTemplates.reduce((prev: any, curr: any) =>
|
|
(!curr.lastRun || new Date(curr.lastRun) > new Date(prev.lastRun || 0) ? curr : prev), {});
|
|
if (last.lastRun) setLastRun(last.lastRun);
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
const lRes = await fetch(`/api/execution-logs?agent=${agentId}&limit=20`);
|
|
if (lRes.ok) {
|
|
const agentLogs = await lRes.json();
|
|
setLogs(agentLogs);
|
|
if (agentLogs.length > 0) {
|
|
setLastRun(agentLogs[0].startedAt);
|
|
const running = agentLogs.find((l: any) => l.status === "running");
|
|
setAgentStatus(running ? "active" : "idle");
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
const cRes = await fetch(`/api/changelog?agent=${agentId}&limit=20`);
|
|
if (cRes.ok) setChangelog(await cRes.json());
|
|
} catch {}
|
|
|
|
try {
|
|
const bRes = await fetch(`/api/agent-outputs?agent=${agentId}`);
|
|
if (bRes.ok) {
|
|
const dates = await bRes.json();
|
|
const notes: any[] = [];
|
|
for (const date of dates.slice(0, 10)) {
|
|
const noteRes = await fetch(`/api/agent-outputs?agent=${agentId}&date=${date}`);
|
|
if (noteRes.ok) {
|
|
const note = await noteRes.json();
|
|
if (note.content) notes.push({ date, content: note.content });
|
|
}
|
|
}
|
|
setBrainownNotes(notes);
|
|
}
|
|
} catch {}
|
|
|
|
setTokens((Math.random() * 2).toFixed(1));
|
|
setLoading(false);
|
|
};
|
|
|
|
const getAgentProject = (id: string): string => {
|
|
const map: Record<string, string> = {
|
|
thoth: "sitemente", "thoth-trading": "trading", ptah: "infrastructure",
|
|
seshat: "sitemente", anubis: "sitemente", sekhmet: "trading",
|
|
};
|
|
return map[id] || "sitemente";
|
|
};
|
|
|
|
const toggleSection = (section: string) => {
|
|
setExpanded(prev => ({ ...prev, [section]: !prev[section] }));
|
|
};
|
|
|
|
const toggleTemplate = async (templateId: string, enabled: boolean) => {
|
|
await fetch("/api/recurring", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ action: "toggle", templateId, enabled })
|
|
});
|
|
fetchAllData();
|
|
};
|
|
|
|
const runNow = async (templateId: string) => {
|
|
setAgentStatus("active");
|
|
await fetch("/api/recurring", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ action: "run-now", templateId })
|
|
});
|
|
setTimeout(fetchAllData, 2000);
|
|
};
|
|
|
|
const todoTasks = tasks.filter((t: any) => t.status === "todo");
|
|
const inProgressTasks = tasks.filter((t: any) => t.status === "in_progress");
|
|
const doneTasks = tasks.filter((t: any) => t.status === "done");
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case "active": return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-500/20 text-green-400">● Active</span>;
|
|
case "idle": return <span className="px-2 py-1 rounded-full text-xs font-medium bg-white/10 text-white/50">● Idle</span>;
|
|
case "error": return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-500/20 text-red-400">● Error</span>;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="p-6 text-center text-white/50">Loading {agent.name}...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-[#1a1a2e] to-[#16213e] p-4 md:p-6">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<button
|
|
onClick={() => router.push("/mission-control?category=council")}
|
|
className="p-2 bg-white/10 hover:bg-white/20 rounded-lg transition"
|
|
>
|
|
←
|
|
</button>
|
|
<div
|
|
className="w-12 h-12 rounded-full flex items-center justify-center text-2xl"
|
|
style={{ backgroundColor: agent.color + "20", border: `2px solid ${agent.color}` }}
|
|
>
|
|
{agent.avatar}
|
|
</div>
|
|
<div className="flex-1">
|
|
<h1 className="text-xl font-bold" style={{ color: agent.color }}>
|
|
{agent.name}
|
|
</h1>
|
|
<p className="text-white/70 text-sm">{agent.role}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
{getStatusBadge(agentStatus)}
|
|
<p className="text-xs text-white/50 mt-1">Tokens: {tokens}k</p>
|
|
{lastRun && <p className="text-xs text-white/40">Last: {new Date(lastRun).toLocaleTimeString()}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 📋 TASKS */}
|
|
<div className="bg-white/5 rounded-xl border border-white/10 mb-4 overflow-hidden">
|
|
<button onClick={() => toggleSection("tasks")} className="w-full p-3 flex justify-between items-center hover:bg-white/5">
|
|
<span className="font-semibold">📋 TASKS</span>
|
|
<span className="text-white/50">{expanded.tasks ? "▲" : "▼"}</span>
|
|
</button>
|
|
{expanded.tasks && (
|
|
<div className="p-3 pt-0 grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
<div className="bg-white/5 rounded-lg p-3">
|
|
<h4 className="text-xs text-white/50 mb-2 text-center">To Do</h4>
|
|
<div className="space-y-2">
|
|
{todoTasks.length === 0 ? <p className="text-xs text-white/30 text-center">—</p> :
|
|
todoTasks.map((t: any) => (
|
|
<div key={t.id} className="p-2 bg-white/5 rounded text-xs">
|
|
<span className={t.priority === "critical" ? "text-red-400" : t.priority === "high" ? "text-orange-400" : "text-white/50"}>[{t.priority}]</span>
|
|
<span className="ml-1">{t.title}</span>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
<div className="bg-blue-500/10 rounded-lg p-3">
|
|
<h4 className="text-xs text-blue-400 mb-2 text-center">In Progress</h4>
|
|
<div className="space-y-2">
|
|
{inProgressTasks.length === 0 ? <p className="text-xs text-white/30 text-center">—</p> :
|
|
inProgressTasks.map((t: any) => (
|
|
<div key={t.id} className="p-2 bg-white/5 rounded text-xs">{t.title}</div>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
<div className="bg-green-500/10 rounded-lg p-3">
|
|
<h4 className="text-xs text-green-400 mb-2 text-center">Done</h4>
|
|
<div className="space-y-2">
|
|
{doneTasks.length === 0 ? <p className="text-xs text-white/30 text-center">—</p> :
|
|
doneTasks.slice(0, 3).map((t: any) => (
|
|
<div key={t.id} className="p-2 bg-white/5 rounded text-xs line-through opacity-50">{t.title}</div>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 🔄 RECURRING */}
|
|
<div className="bg-white/5 rounded-xl border border-white/10 mb-4 overflow-hidden">
|
|
<button onClick={() => toggleSection("recurring")} className="w-full p-3 flex justify-between items-center hover:bg-white/5">
|
|
<span className="font-semibold">🔄 RECURRING</span>
|
|
<span className="text-white/50">{expanded.recurring ? "▲" : "▼"}</span>
|
|
</button>
|
|
{expanded.recurring && (
|
|
<div className="p-3 pt-0 space-y-2">
|
|
{templates.length === 0 ? <p className="text-sm text-white/50">No recurring tasks</p> :
|
|
templates.map((t: any) => (
|
|
<div key={t.id} className="p-3 bg-white/5 rounded-lg flex justify-between items-center">
|
|
<div>
|
|
<p className="font-medium text-sm">{t.taskTemplate.title}</p>
|
|
<p className="text-xs text-white/50">
|
|
{t.schedule.type === "hourly" ? "Hourly" : t.schedule.time ? `${t.schedule.time} CET` : t.schedule.type}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => runNow(t.id)} className="px-2 py-1 bg-brand-pink rounded text-xs">▶</button>
|
|
<button onClick={() => toggleTemplate(t.id, !t.enabled)}
|
|
className={`px-2 py-1 rounded text-xs ${t.enabled ? "bg-green-500" : "bg-white/10"}`}>
|
|
{t.enabled ? "ON" : "OFF"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 📊 LOGS */}
|
|
<div className="bg-white/5 rounded-xl border border-white/10 mb-4 overflow-hidden">
|
|
<button onClick={() => toggleSection("logs")} className="w-full p-3 flex justify-between items-center hover:bg-white/5">
|
|
<span className="font-semibold">📊 EXECUTION LOGS</span>
|
|
<span className="text-white/50">{expanded.logs ? "▲" : "▼"}</span>
|
|
</button>
|
|
{expanded.logs && (
|
|
<div className="p-3 pt-0 space-y-2">
|
|
{logs.length === 0 ? <p className="text-sm text-white/50">No logs</p> :
|
|
logs.slice(0, 5).map((l: any) => (
|
|
<div key={l.id} className="p-2 bg-white/5 rounded flex justify-between items-center text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className={`w-2 h-2 rounded-full ${l.status === "completed" ? "bg-green-500" : l.status === "failed" ? "bg-red-500" : "bg-yellow-500"}`} />
|
|
<span>{l.taskTitle}</span>
|
|
</div>
|
|
<span className={`text-xs px-2 py-0.5 rounded ${l.status === "completed" ? "bg-green-500/20 text-green-400" : l.status === "failed" ? "bg-red-500/20 text-red-400" : "bg-yellow-500/20 text-yellow-400"}`}>
|
|
{l.status}
|
|
</span>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 📝 CHANGE LOG */}
|
|
<div className="bg-white/5 rounded-xl border border-white/10 mb-4 overflow-hidden">
|
|
<button onClick={() => toggleSection("changelog")} className="w-full p-3 flex justify-between items-center hover:bg-white/5">
|
|
<span className="font-semibold">📝 CHANGE LOG</span>
|
|
<span className="text-white/50">{expanded.changelog ? "▲" : "▼"}</span>
|
|
</button>
|
|
{expanded.changelog && (
|
|
<div className="p-3 pt-0 space-y-2">
|
|
{changelog.length === 0 ? <p className="text-sm text-white/50">No changes</p> :
|
|
changelog.slice(0, 5).map((c: any) => (
|
|
<div key={c.id} className="p-2 bg-white/5 rounded">
|
|
<div className="flex justify-between">
|
|
<span className="text-xs px-2 py-0.5 rounded bg-purple-500/20 text-purple-400">v{c.version || "1.0"}</span>
|
|
<span className="text-xs text-white/40">{new Date(c.date).toLocaleDateString()}</span>
|
|
</div>
|
|
<p className="text-sm mt-1">{c.description}</p>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 🧠 BRAINOWN */}
|
|
<div className="bg-white/5 rounded-xl border border-white/10 mb-4 overflow-hidden">
|
|
<button onClick={() => toggleSection("brainown")} className="w-full p-3 flex justify-between items-center hover:bg-white/5">
|
|
<span className="font-semibold">🧠 BRAINOWN</span>
|
|
<span className="text-white/50">{expanded.brainown ? "▲" : "▼"}</span>
|
|
</button>
|
|
{expanded.brainown && (
|
|
<div className="p-3 pt-0 space-y-2">
|
|
{brainownNotes.length === 0 ? <p className="text-sm text-white/50">No outputs</p> :
|
|
brainownNotes.map((n: any) => (
|
|
<div key={n.date} className="p-2 bg-white/5 rounded">
|
|
<span className="text-sm">📄 {n.date}</span>
|
|
<pre className="text-xs text-white/60 mt-1 truncate">{n.content.substring(0, 100)}...</pre>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Back */}
|
|
<button
|
|
onClick={() => router.push("/mission-control?category=council")}
|
|
className="w-full py-3 bg-white/10 hover:bg-white/20 rounded-lg transition"
|
|
>
|
|
← Back to Agent Teams
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|