feat: MicroDAO Agents Section + Room roles

Frontend:
- MicrodaoAgentsSection component with role badges
- useMicrodaoAgents hook
- Extended room_role mapping (operations, knowledge, treasury, ai-core, etc.)
- API route for /api/microdao/[slug]/agents

Matrix: All 13 new rooms synced with Matrix
This commit is contained in:
Apple
2025-11-30 11:57:24 -08:00
parent a7adddb60d
commit 7b61786c96
5 changed files with 442 additions and 40 deletions

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
const CITY_API_URL = process.env.CITY_API_URL || "http://localhost:7001";
/**
* GET /api/microdao/[slug]/agents
* Get all agents for a MicroDAO
*/
export async function GET(
request: NextRequest,
context: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await context.params;
const response = await fetch(`${CITY_API_URL}/city/microdao/${slug}/agents`, {
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
});
if (!response.ok) {
const text = await response.text();
console.error("Failed to get microdao agents:", response.status, text);
return NextResponse.json(
{ error: `Backend error: ${response.status}` },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error getting microdao agents:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -2,11 +2,12 @@
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import { useMicrodaoDetail, useMicrodaoRooms } from "@/hooks/useMicrodao";
import { useMicrodaoDetail, useMicrodaoRooms, useMicrodaoAgents } from "@/hooks/useMicrodao";
import { DISTRICT_COLORS } from "@/lib/microdao";
import { MicrodaoVisibilityCard } from "@/components/microdao/MicrodaoVisibilityCard";
import { MicrodaoRoomsSection } from "@/components/microdao/MicrodaoRoomsSection";
import { MicrodaoRoomsAdminPanel } from "@/components/microdao/MicrodaoRoomsAdminPanel";
import { MicrodaoAgentsSection } from "@/components/microdao/MicrodaoAgentsSection";
import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot, MessageCircle } from "lucide-react";
import { CityChatWidget } from "@/components/city/CityChatWidget";
import { AgentChatWidget } from "@/components/chat/AgentChatWidget";
@@ -18,6 +19,7 @@ export default function MicrodaoDetailPage() {
const slug = params?.slug as string;
const { microdao, isLoading, error, mutate: refreshMicrodao } = useMicrodaoDetail(slug);
const { rooms, mutate: refreshRooms } = useMicrodaoRooms(slug);
const { agents: microdaoAgents } = useMicrodaoAgents(slug);
const handleRoomUpdated = () => {
refreshRooms();
@@ -224,48 +226,53 @@ export default function MicrodaoDetailPage() {
onEnsureOrchestratorRoom={handleEnsureOrchestratorRoom}
/>
<div className="grid md:grid-cols-2 gap-8">
{/* Agents */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4 h-full">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Bot className="w-5 h-5 text-cyan-400" />
Агентська команда
<span className="text-sm font-normal text-slate-500">({microdao.agents.length})</span>
</h2>
{/* MicroDAO Agents Section - using new API */}
<MicrodaoAgentsSection agents={microdaoAgents} microdaoSlug={slug} />
{microdao.agents.length === 0 ? (
<div className="text-sm text-slate-500">Агенти ще не привʼязані.</div>
) : (
<div className="space-y-2">
{microdao.agents.map((a) => (
<div
key={a.agent_id}
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 flex items-center justify-between hover:border-slate-600/50 transition-colors"
>
<div className="space-y-0.5">
<Link
href={`/agents/${a.agent_id}`}
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors flex items-center gap-2"
>
{a.display_name}
{a.agent_id === microdao.orchestrator_agent_id && (
<Crown className="w-3 h-3 text-amber-400" />
<div className="grid md:grid-cols-2 gap-8">
{/* Legacy Agents (from microdao detail) - hidden if new API has data */}
{microdaoAgents.length === 0 && (
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4 h-full">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Bot className="w-5 h-5 text-cyan-400" />
Агентська команда
<span className="text-sm font-normal text-slate-500">({microdao.agents.length})</span>
</h2>
{microdao.agents.length === 0 ? (
<div className="text-sm text-slate-500">Агенти ще не привʼязані.</div>
) : (
<div className="space-y-2">
{microdao.agents.map((a) => (
<div
key={a.agent_id}
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 flex items-center justify-between hover:border-slate-600/50 transition-colors"
>
<div className="space-y-0.5">
<Link
href={`/agents/${a.agent_id}`}
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors flex items-center gap-2"
>
{a.display_name}
{a.agent_id === microdao.orchestrator_agent_id && (
<Crown className="w-3 h-3 text-amber-400" />
)}
</Link>
{a.role && (
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
)}
</Link>
{a.role && (
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
</div>
{a.is_core && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
Core
</span>
)}
</div>
{a.is_core && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
Core
</span>
)}
</div>
))}
</div>
)}
</section>
))}
</div>
)}
</section>
)}
{/* Public Citizens */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4 h-full">

View File

@@ -0,0 +1,214 @@
"use client";
import Link from "next/link";
import { Bot, Crown, Users, Shield, Sparkles } from "lucide-react";
import { MicrodaoAgent } from "@/hooks/useMicrodao";
interface MicrodaoAgentsSectionProps {
agents: MicrodaoAgent[];
microdaoSlug?: string;
}
const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.ReactNode }> = {
orchestrator: {
label: "Orchestrator",
chipClass: "bg-fuchsia-500/10 text-fuchsia-300 border-fuchsia-500/30",
icon: <Crown className="w-3.5 h-3.5" />,
},
district_lead: {
label: "District Lead",
chipClass: "bg-purple-500/10 text-purple-300 border-purple-500/30",
icon: <Crown className="w-3.5 h-3.5" />,
},
core_team: {
label: "Core Team",
chipClass: "bg-indigo-500/10 text-indigo-300 border-indigo-500/30",
icon: <Users className="w-3.5 h-3.5" />,
},
member: {
label: "Member",
chipClass: "bg-slate-500/10 text-slate-300 border-slate-500/30",
icon: <Bot className="w-3.5 h-3.5" />,
},
guardian: {
label: "Guardian",
chipClass: "bg-rose-500/10 text-rose-300 border-rose-500/30",
icon: <Shield className="w-3.5 h-3.5" />,
},
steward: {
label: "Steward",
chipClass: "bg-emerald-500/10 text-emerald-300 border-emerald-500/30",
icon: <Sparkles className="w-3.5 h-3.5" />,
},
};
const STATUS_COLORS: Record<string, string> = {
active: "bg-emerald-400",
inactive: "bg-slate-500",
suspended: "bg-yellow-400",
offline: "bg-slate-600",
};
export function MicrodaoAgentsSection({ agents, microdaoSlug }: MicrodaoAgentsSectionProps) {
if (!agents || agents.length === 0) {
return (
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Bot className="w-5 h-5 text-violet-400" />
Агенти MicroDAO
</h2>
<p className="text-sm text-slate-500">
Для цього MicroDAO ще не призначені агенти.
</p>
</section>
);
}
// Group agents by role
const orchestrators = agents.filter(a => a.role === "orchestrator" || a.role === "district_lead");
const coreTeam = agents.filter(a => a.role === "core_team" || a.is_core);
const others = agents.filter(a => !orchestrators.includes(a) && !coreTeam.includes(a));
return (
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Bot className="w-5 h-5 text-violet-400" />
Агенти MicroDAO
<span className="text-sm font-normal text-slate-500">({agents.length})</span>
</h2>
</div>
{/* Orchestrator(s) */}
{orchestrators.length > 0 && (
<div className="space-y-3">
<div className="text-xs text-slate-400 uppercase tracking-wider font-medium">
Orchestrator
</div>
<div className="grid gap-3">
{orchestrators.map(agent => (
<AgentCard key={agent.id} agent={agent} featured />
))}
</div>
</div>
)}
{/* Core Team */}
{coreTeam.length > 0 && (
<div className="space-y-3">
<div className="text-xs text-slate-400 uppercase tracking-wider font-medium">
Core Team
</div>
<div className="grid gap-3 md:grid-cols-2">
{coreTeam.map(agent => (
<AgentCard key={agent.id} agent={agent} />
))}
</div>
</div>
)}
{/* Other Members */}
{others.length > 0 && (
<div className="space-y-3">
<div className="text-xs text-slate-400 uppercase tracking-wider font-medium">
Members
</div>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{others.map(agent => (
<AgentCard key={agent.id} agent={agent} compact />
))}
</div>
</div>
)}
</section>
);
}
interface AgentCardProps {
agent: MicrodaoAgent;
featured?: boolean;
compact?: boolean;
}
function AgentCard({ agent, featured = false, compact = false }: AgentCardProps) {
const roleMeta = ROLE_META[agent.role] || ROLE_META.member;
const statusColor = STATUS_COLORS[agent.status] || STATUS_COLORS.offline;
if (compact) {
return (
<Link href={`/agents/${agent.id}`}>
<div className="flex items-center gap-3 p-3 bg-slate-900/50 border border-slate-700/30 rounded-lg hover:border-slate-600/50 transition-colors">
<div className="relative">
<div className="w-8 h-8 rounded-full bg-violet-500/20 flex items-center justify-center">
{agent.avatar_url ? (
<img src={agent.avatar_url} alt={agent.name} className="w-8 h-8 rounded-full" />
) : (
<Bot className="w-4 h-4 text-violet-400" />
)}
</div>
<span className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-slate-900 ${statusColor}`} />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-slate-200 truncate">{agent.name}</div>
</div>
</div>
</Link>
);
}
return (
<Link href={`/agents/${agent.id}`}>
<div className={`p-4 rounded-xl border transition-colors ${
featured
? "bg-gradient-to-br from-fuchsia-950/30 to-violet-950/30 border-fuchsia-500/30 hover:border-fuchsia-400/50"
: "bg-slate-900/50 border-slate-700/30 hover:border-slate-600/50"
}`}>
<div className="flex items-start gap-4">
{/* Avatar */}
<div className="relative">
<div className={`rounded-full bg-violet-500/20 flex items-center justify-center ${
featured ? "w-14 h-14" : "w-10 h-10"
}`}>
{agent.avatar_url ? (
<img src={agent.avatar_url} alt={agent.name} className={`rounded-full ${featured ? "w-14 h-14" : "w-10 h-10"}`} />
) : (
<Bot className={`text-violet-400 ${featured ? "w-7 h-7" : "w-5 h-5"}`} />
)}
</div>
<span className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-slate-900 ${statusColor}`} />
</div>
{/* Info */}
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center gap-2">
<span className={`font-medium ${featured ? "text-lg text-white" : "text-base text-slate-200"}`}>
{agent.name}
</span>
{agent.is_core && (
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-300 border border-amber-500/20">
Core
</span>
)}
</div>
{/* Role badge */}
<div className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full border text-[11px] ${roleMeta.chipClass}`}>
{roleMeta.icon}
<span>{roleMeta.label}</span>
</div>
{/* Kind and Gov Level */}
{(agent.kind || agent.gov_level) && (
<div className="flex items-center gap-2 text-xs text-slate-500">
{agent.kind && <span>{agent.kind}</span>}
{agent.kind && agent.gov_level && <span></span>}
{agent.gov_level && <span>{agent.gov_level}</span>}
</div>
)}
</div>
</div>
</div>
</Link>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2, Bot, PlusCircle } from "lucide-react";
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2, Bot, PlusCircle, Crown } from "lucide-react";
import { CityRoomSummary } from "@/lib/types/microdao";
import { CityChatWidget } from "@/components/city/CityChatWidget";
import { Button } from "@/components/ui/button";
@@ -51,6 +51,58 @@ const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.
chipClass: "bg-fuchsia-500/10 text-fuchsia-300 border-fuchsia-500/30",
icon: <Bot className="w-3.5 h-3.5" />,
},
// New room roles for MicroDAO
operations: {
label: "Operations",
chipClass: "bg-orange-500/10 text-orange-300 border-orange-500/30",
icon: <Users className="w-3.5 h-3.5" />,
},
knowledge: {
label: "Knowledge Base",
chipClass: "bg-cyan-500/10 text-cyan-300 border-cyan-500/30",
icon: <FlaskConical className="w-3.5 h-3.5" />,
},
treasury: {
label: "Treasury",
chipClass: "bg-yellow-500/10 text-yellow-300 border-yellow-500/30",
icon: <Shield className="w-3.5 h-3.5" />,
},
"ai-core": {
label: "AI Core",
chipClass: "bg-purple-500/10 text-purple-300 border-purple-500/30",
icon: <Bot className="w-3.5 h-3.5" />,
},
// District-specific
events: {
label: "Events",
chipClass: "bg-pink-500/10 text-pink-300 border-pink-500/30",
icon: <Users className="w-3.5 h-3.5" />,
},
masters: {
label: "Masters",
chipClass: "bg-indigo-500/10 text-indigo-300 border-indigo-500/30",
icon: <Crown className="w-3.5 h-3.5" />,
},
supply: {
label: "Supply Chain",
chipClass: "bg-green-500/10 text-green-300 border-green-500/30",
icon: <Users className="w-3.5 h-3.5" />,
},
producers: {
label: "Producers",
chipClass: "bg-lime-500/10 text-lime-300 border-lime-500/30",
icon: <Users2 className="w-3.5 h-3.5" />,
},
compute: {
label: "Compute Grid",
chipClass: "bg-amber-500/10 text-amber-300 border-amber-500/30",
icon: <Bot className="w-3.5 h-3.5" />,
},
providers: {
label: "Providers",
chipClass: "bg-orange-500/10 text-orange-300 border-orange-500/30",
icon: <Users2 className="w-3.5 h-3.5" />,
},
};
export function MicrodaoRoomsSection({

View File

@@ -215,3 +215,90 @@ export function useMicrodaoRooms(
mutate: fetchData,
};
}
// =============================================================================
// useMicrodaoAgents - fetch all agents for a MicroDAO
// =============================================================================
export interface MicrodaoAgent {
id: string;
name: string;
kind: string;
status: string;
avatar_url?: string;
gov_level?: string;
role: string;
is_core: boolean;
}
interface UseMicrodaoAgentsOptions {
refreshInterval?: number;
enabled?: boolean;
}
interface UseMicrodaoAgentsResult {
agents: MicrodaoAgent[];
microdaoId: string | null;
microdaoSlug: string | null;
isLoading: boolean;
error: Error | null;
mutate: () => Promise<void>;
}
export function useMicrodaoAgents(
slug: string | undefined,
options: UseMicrodaoAgentsOptions = {}
): UseMicrodaoAgentsResult {
const { refreshInterval = 60000, enabled = true } = options;
const [agents, setAgents] = useState<MicrodaoAgent[]>([]);
const [microdaoId, setMicrodaoId] = useState<string | null>(null);
const [microdaoSlug, setMicrodaoSlug] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
if (!slug || !enabled) return;
try {
setIsLoading(true);
const res = await fetch(`/api/microdao/${encodeURIComponent(slug)}/agents`);
if (!res.ok) {
throw new Error(`Failed to fetch agents: ${res.status}`);
}
const data = await res.json();
setAgents(data.agents || []);
setMicrodaoId(data.microdao_id);
setMicrodaoSlug(data.microdao_slug);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
} finally {
setIsLoading(false);
}
}, [slug, enabled]);
// Initial fetch
useEffect(() => {
fetchData();
}, [fetchData]);
// Auto-refresh
useEffect(() => {
if (!enabled || refreshInterval <= 0) return;
const interval = setInterval(fetchData, refreshInterval);
return () => clearInterval(interval);
}, [fetchData, refreshInterval, enabled]);
return {
agents,
microdaoId,
microdaoSlug,
isLoading,
error,
mutate: fetchData,
};
}