Files
sitemente/components/council/AgentModal.tsx
T

427 lines
21 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
const AGENT_CONFIG: Record<string, { name: string; avatar: string; color: string; role: string; description: string; skills: string[] }> = {
horus: { name: "Horus", avatar: "👁️", color: "#ff7bc0", role: "Master Orchestrator", description: "Command center, delegates tasks, manages all agents", skills: ["Task delegation", "System orchestration", "Strategic planning"] },
thoth: { name: "Thoth", avatar: "🧠", color: "#8b5cf6", role: "Strategy & Research", description: "SiteMente planning, research, analysis", skills: ["Market research", "Competitor analysis", "AI trends", "Lead research"] },
"thoth-trading": { name: "Thoth Trading", avatar: "📈", color: "#f59e0b", role: "Market Research", description: "Crypto market analysis and research", skills: ["Technical analysis", "Price alerts", "Portfolio tracking"] },
ptah: { name: "Ptah", avatar: "🏗️", color: "#10b981", role: "Dev & Ops", description: "Development, deployment, technical implementation", skills: ["Code review", "Deployments", "Infra management", "Bug fixes"] },
seshat: { name: "Seshat", avatar: "📜", color: "#ec4899", role: "Content & SEO", description: "Content strategy, SEO, marketing copy", skills: ["SEO optimization", "Copywriting", "Blog posts", "Social media"] },
anubis: { name: "Anubis", avatar: "🐺", color: "#6366f1", role: "Outreach & Growth", description: "Lead generation, client acquisition", skills: ["Lead research", "Cold outreach", "Follow-ups", "Deal closing"] },
sekhmet: { name: "Sekhmet", avatar: "⚔️", color: "#ef4444", role: "Risk Management", description: "Trade execution and risk", skills: ["Risk assessment", "Trade execution", "Stop-loss management"] },
};
interface AgentModalProps {
agentId: string;
onClose: () => void;
}
export default function AgentModal({ agentId, onClose }: AgentModalProps) {
const agent = AGENT_CONFIG[agentId] || { name: agentId, avatar: "🤖", color: "#666", role: "Agent", description: "", skills: [] };
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);
// Command input
const [command, setCommand] = useState("");
const [conversation, setConversation] = useState<{role: string, content: string}[]>([]);
const [processing, setProcessing] = useState(false);
const conversationEndRef = useRef<HTMLDivElement>(null);
const [expanded, setExpanded] = useState<Record<string, boolean>>({
tasks: true,
recurring: true,
logs: true,
changelog: true,
brainown: true,
});
useEffect(() => {
fetchAllData();
}, [agentId]);
useEffect(() => {
conversationEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [conversation]);
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 sendCommand = async () => {
if (!command.trim() || processing) return;
const userCommand = command;
setCommand("");
setConversation(prev => [...prev, { role: "user", content: userCommand }]);
setProcessing(true);
// Simulate agent response
setTimeout(() => {
const responses: Record<string, string> = {
thoth: `I've researched "${userCommand}" for SiteMente. Here are my findings:\n\n1. Market trends show increasing demand for AI chatbots\n2. Local competitors: 3 agencies in Málaga\n3. Recommended approach: Focus on restaurants first`,
"thoth-trading": `Market analysis for "${userCommand}":\n\n📊 BTC: $67,234 (+2.3%)\n📊 ETH: $3,456 (+1.8%)\n📊 SOL: $145 (+3.1%)\n\nNo significant alerts at this time.`,
ptah: `Running "${userCommand}" on infrastructure...\n\n✅ Completed: Code reviewed, no critical issues found.\n💡 Suggestion: Consider adding unit tests for the new module.`,
seshat: `Creating content for "${userCommand}"...\n\n📝 Blog post outline generated\n📝 3 social media posts drafted\n📝 SEO keywords optimized`,
anubis: `Searching for leads for "${userCommand}"...\n\n🔍 Found 12 potential restaurants in Benalmádena\n📋 Top prospect: Restaurante El Puerto (50 seats, no website)\n📞 Recommended: Schedule demo call this week`,
sekhmet: `Risk assessment for "${userCommand}":\n\n⚠️ Current drawdown: 3.2%\n✅ All positions within risk limits\n💡 Recommendation: No action needed`,
horus: `I've noted your request: "${userCommand}"\n\nI'll delegate this to the appropriate agent and track progress. You can see updates in the Mission Control dashboard.`,
};
setConversation(prev => [...prev, {
role: "agent",
content: responses[agentId] || `Understood: "${userCommand}". I'm processing this request and will update you shortly.`
}]);
setProcessing(false);
}, 1500);
};
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;
}
};
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-2 md:p-4">
<div className="bg-gradient-to-br from-[#1a1a2e] to-[#16213e] rounded-2xl w-full max-w-3xl max-h-[95vh] overflow-hidden border border-white/20 shadow-2xl flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10 shrink-0">
<div className="flex items-center gap-3">
<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>
<h2 className="font-bold text-lg" style={{ color: agent.color }}>{agent.name}</h2>
<p className="text-xs text-white/50">{agent.role}</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-lg transition"></button>
</div>
{/* Status Bar */}
<div className="px-4 py-2 bg-white/5 flex items-center justify-between text-sm shrink-0 border-b border-white/10">
<div className="flex items-center gap-3">
{getStatusBadge(agentStatus)}
<span className="text-white/50">Tokens: {tokens}k</span>
{lastRun && <span className="text-white/40">Last: {new Date(lastRun).toLocaleTimeString()}</span>}
</div>
<div className="text-xs text-white/40">{agent.project || getAgentProject(agentId)}</div>
</div>
{/* Skills */}
<div className="px-4 py-2 bg-white/5 border-b border-white/10 shrink-0">
<div className="flex flex-wrap gap-1">
{agent.skills.map((skill, i) => (
<span key={i} className="px-2 py-0.5 bg-white/10 rounded text-xs text-white/70">{skill}</span>
))}
</div>
</div>
{/* Content - Scrollable */}
<div className="overflow-y-auto flex-1 p-4 space-y-3">
{/* 💬 CONVERSATION */}
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<div className="p-3 border-b border-white/10">
<span className="font-semibold">💬 Chat with {agent.name}</span>
</div>
<div className="p-3 max-h-48 overflow-y-auto space-y-2">
{conversation.length === 0 ? (
<p className="text-sm text-white/50 text-center py-4">
Ask {agent.name} anything about {agent.role}...
</p>
) : (
conversation.map((msg, i) => (
<div key={i} className={`text-sm ${msg.role === "user" ? "text-right" : "text-left"}`}>
<span className={`inline-block px-3 py-2 rounded-lg max-w-[85%] ${
msg.role === "user"
? "bg-brand-pink/20 text-white"
: "bg-white/10 text-white/90"
}`}>
{msg.content}
</span>
</div>
))
)}
{processing && (
<div className="text-left">
<span className="inline-block px-3 py-2 rounded-lg bg-white/10 text-white/50">
Processing...
</span>
</div>
)}
<div ref={conversationEndRef} />
</div>
<div className="p-3 border-t border-white/10">
<div className="flex gap-2">
<input
type="text"
value={command}
onChange={(e) => setCommand(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendCommand()}
placeholder={`Ask ${agent.name} to...`}
className="flex-1 bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink"
disabled={processing}
/>
<button
onClick={sendCommand}
disabled={processing || !command.trim()}
className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium disabled:opacity-50 hover:bg-[#ff7bc0] transition"
>
Send
</button>
</div>
</div>
</div>
{/* 📋 TASKS */}
<div className="bg-white/5 rounded-xl border border-white/10 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 ({tasks.length})</span>
<span className="text-white/50">{expanded.tasks ? "▲" : "▼"}</span>
</button>
{expanded.tasks && (
<div className="p-3 pt-0 grid grid-cols-3 gap-2">
<div className="bg-white/5 rounded-lg p-2">
<h4 className="text-xs text-white/50 mb-2 text-center">To Do ({todoTasks.length})</h4>
<div className="space-y-1">
{todoTasks.slice(0, 3).map((t: any) => (
<div key={t.id} className="p-1.5 bg-white/5 rounded text-xs truncate">{t.title}</div>
))}
</div>
</div>
<div className="bg-blue-500/10 rounded-lg p-2">
<h4 className="text-xs text-blue-400 mb-2 text-center">In Progress ({inProgressTasks.length})</h4>
<div className="space-y-1">
{inProgressTasks.slice(0, 3).map((t: any) => (
<div key={t.id} className="p-1.5 bg-white/5 rounded text-xs truncate">{t.title}</div>
))}
</div>
</div>
<div className="bg-green-500/10 rounded-lg p-2">
<h4 className="text-xs text-green-400 mb-2 text-center">Done ({doneTasks.length})</h4>
<div className="space-y-1">
{doneTasks.slice(0, 3).map((t: any) => (
<div key={t.id} className="p-1.5 bg-white/5 rounded text-xs truncate opacity-50">{t.title}</div>
))}
</div>
</div>
</div>
)}
</div>
{/* 🔄 RECURRING */}
<div className="bg-white/5 rounded-xl border border-white/10 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 ({templates.length})</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-2 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-1">
<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 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 ({logs.length})</span>
<span className="text-white/50">{expanded.logs ? "▲" : "▼"}</span>
</button>
{expanded.logs && (
<div className="p-3 pt-0 space-y-1">
{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 className="truncate">{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 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 ({changelog.length})</span>
<span className="text-white/50">{expanded.changelog ? "▲" : "▼"}</span>
</button>
{expanded.changelog && (
<div className="p-3 pt-0 space-y-1">
{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 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 ({brainownNotes.length})</span>
<span className="text-white/50">{expanded.brainown ? "▲" : "▼"}</span>
</button>
{expanded.brainown && (
<div className="p-3 pt-0 space-y-1">
{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, 80)}...</pre>
</div>
))
}
</div>
)}
</div>
</div>
</div>
</div>
);
}