177 lines
6.5 KiB
TypeScript
177 lines
6.5 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { Agent, AgentTeam, defaultTeams } from "@/lib/council/types";
|
||
import AgentModal from "./AgentModal";
|
||
|
||
const STORAGE_KEY = "horus:council";
|
||
|
||
export default function Council() {
|
||
const [teams, setTeams] = useState<AgentTeam[]>(defaultTeams);
|
||
const [selectedTeam, setSelectedTeam] = useState<string | null>(null);
|
||
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
||
const [runningTask, setRunningTask] = useState<string>("");
|
||
const [agentOutputs, setAgentOutputs] = useState<Record<string, string>>({});
|
||
|
||
// Load from localStorage
|
||
useEffect(() => {
|
||
if (typeof window === "undefined") return;
|
||
const saved = localStorage.getItem(STORAGE_KEY);
|
||
if (saved) {
|
||
try {
|
||
setTeams(JSON.parse(saved));
|
||
} catch {}
|
||
}
|
||
}, []);
|
||
|
||
// Save changes
|
||
useEffect(() => {
|
||
if (typeof window === "undefined") return;
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(teams));
|
||
}, [teams]);
|
||
|
||
const getStatusColor = (status: Agent["status"]) => {
|
||
switch (status) {
|
||
case "working": return "text-yellow-400 bg-yellow-400/20";
|
||
case "completed": return "text-green-400 bg-green-400/20";
|
||
case "error": return "text-red-400 bg-red-400/20";
|
||
default: return "text-white/50 bg-white/10";
|
||
}
|
||
};
|
||
|
||
const spawnAgent = async (agent: Agent, task: string) => {
|
||
if (!task.trim()) return;
|
||
|
||
// Update agent status
|
||
setTeams(prev => prev.map(team => ({
|
||
...team,
|
||
agents: team.agents.map(a => a.id === agent.id ? { ...a, status: "working" as const, currentTask: task } : a)
|
||
})));
|
||
setRunningTask("");
|
||
|
||
// Simulate agent work (in reality this would call actual agents)
|
||
const output = `[🤖 ${agent.name}] Starting task: "${task}"\n\n`;
|
||
setAgentOutputs(prev => ({ ...prev, [agent.id]: output + "⏳ Processing...\n" }));
|
||
|
||
// Simulate completion after delay
|
||
setTimeout(() => {
|
||
const result = output + `✅ Task completed: ${task}\n\n💡 Result: Task processed successfully.`;
|
||
setAgentOutputs(prev => ({ ...prev, [agent.id]: result }));
|
||
|
||
setTeams(prev => prev.map(team => ({
|
||
...team,
|
||
agents: team.agents.map(a => a.id === agent.id ? {
|
||
...a,
|
||
status: "completed" as const,
|
||
lastRun: new Date().toISOString(),
|
||
currentTask: undefined
|
||
} : a)
|
||
})));
|
||
}, 3000 + Math.random() * 2000);
|
||
};
|
||
|
||
const currentTeam = teams.find(t => t.id === selectedTeam);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div>
|
||
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||
🏛️ Council
|
||
</h2>
|
||
<p className="text-white/60 mt-1">
|
||
Manage AI agent teams for each project. I'm the boss — I delegate tasks and oversee execution.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Team Grid */}
|
||
<div className="grid gap-4 md:grid-cols-3">
|
||
{teams.map((team) => (
|
||
<button
|
||
key={team.id}
|
||
onClick={() => setSelectedTeam(team.id)}
|
||
className={`p-4 rounded-xl border text-left transition ${
|
||
selectedTeam === team.id
|
||
? "border-brand-pink bg-brand-pink/10"
|
||
: "border-white/10 bg-white/5 hover:border-white/30"
|
||
}`}
|
||
>
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<span className="text-2xl">{team.icon}</span>
|
||
<div>
|
||
<p className="font-semibold">{team.name}</p>
|
||
<p className="text-xs text-white/50">{team.agents.length} agents</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-white/60">{team.description}</p>
|
||
<div className="mt-3 flex gap-1">
|
||
{team.agents.slice(0, 4).map(a => (
|
||
<span key={a.id} className={`w-2 h-2 rounded-full ${getStatusColor(a.status).split(' ')[1].replace('/', '-')}`} />
|
||
))}
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Selected Team Detail */}
|
||
{currentTeam && (
|
||
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-2xl">{currentTeam.icon}</span>
|
||
<div>
|
||
<h3 className="font-semibold">{currentTeam.name}</h3>
|
||
<p className="text-sm text-white/50">{currentTeam.description}</p>
|
||
</div>
|
||
</div>
|
||
<button onClick={() => setSelectedTeam(null)} className="text-white/50 hover:text-white">✕</button>
|
||
</div>
|
||
|
||
{/* Agents - Grid Layout */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||
{currentTeam.agents.map((agent) => (
|
||
<div key={agent.id} className="p-4 rounded-lg border border-white/10 bg-white/5">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${getStatusColor(agent.status)}`}>
|
||
{agent.status.toUpperCase()}
|
||
</span>
|
||
<span className="font-medium">{agent.name}</span>
|
||
</div>
|
||
|
||
<p className="text-xs text-white/60 mb-3">{agent.role}</p>
|
||
|
||
{/* Open Agent Command Center */}
|
||
<button
|
||
onClick={() => setSelectedAgent(agent.id)}
|
||
className="w-full px-3 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-lg text-sm text-center transition"
|
||
>
|
||
🧑💼 Open
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
type="text"
|
||
value={runningTask}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Horus Boss Note */}
|
||
<div className="p-4 rounded-xl border border-brand-pink/30 bg-brand-pink/10">
|
||
<p className="text-sm text-brand-pink">
|
||
👁️ <strong>Note:</strong> I coordinate these agents. They run as sub-tasks within my sessions.
|
||
In production, they'd be separate AI processes. Currently simulating — real agent spawning requires
|
||
the OpenClaw sub-agent system to be configured.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Agent Modal */}
|
||
{selectedAgent && (
|
||
<AgentModal agentId={selectedAgent} onClose={() => setSelectedAgent(null)} />
|
||
)}
|
||
</div>
|
||
);
|
||
}
|