Files
sitemente/components/council/Council.tsx
T
2026-02-27 18:44:43 +01:00

177 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}