diff --git a/apps/web/src/app/api/microdao/[slug]/agents/route.ts b/apps/web/src/app/api/microdao/[slug]/agents/route.ts new file mode 100644 index 00000000..ba7bf82a --- /dev/null +++ b/apps/web/src/app/api/microdao/[slug]/agents/route.ts @@ -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 } + ); + } +} + diff --git a/apps/web/src/app/microdao/[slug]/page.tsx b/apps/web/src/app/microdao/[slug]/page.tsx index ef936e12..64cf0960 100644 --- a/apps/web/src/app/microdao/[slug]/page.tsx +++ b/apps/web/src/app/microdao/[slug]/page.tsx @@ -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} /> -
- {/* Agents */} -
-

- - Агентська команда - ({microdao.agents.length}) -

+ {/* MicroDAO Agents Section - using new API */} + - {microdao.agents.length === 0 ? ( -
Агенти ще не привʼязані.
- ) : ( -
- {microdao.agents.map((a) => ( -
-
- - {a.display_name} - {a.agent_id === microdao.orchestrator_agent_id && ( - +
+ {/* Legacy Agents (from microdao detail) - hidden if new API has data */} + {microdaoAgents.length === 0 && ( +
+

+ + Агентська команда + ({microdao.agents.length}) +

+ + {microdao.agents.length === 0 ? ( +
Агенти ще не привʼязані.
+ ) : ( +
+ {microdao.agents.map((a) => ( +
+
+ + {a.display_name} + {a.agent_id === microdao.orchestrator_agent_id && ( + + )} + + {a.role && ( +
{a.role}
)} - - {a.role && ( -
{a.role}
+
+ {a.is_core && ( + + Core + )}
- {a.is_core && ( - - Core - - )} -
- ))} -
- )} -
+ ))} +
+ )} + + )} {/* Public Citizens */}
diff --git a/apps/web/src/components/microdao/MicrodaoAgentsSection.tsx b/apps/web/src/components/microdao/MicrodaoAgentsSection.tsx new file mode 100644 index 00000000..bf7456b7 --- /dev/null +++ b/apps/web/src/components/microdao/MicrodaoAgentsSection.tsx @@ -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 = { + orchestrator: { + label: "Orchestrator", + chipClass: "bg-fuchsia-500/10 text-fuchsia-300 border-fuchsia-500/30", + icon: , + }, + district_lead: { + label: "District Lead", + chipClass: "bg-purple-500/10 text-purple-300 border-purple-500/30", + icon: , + }, + core_team: { + label: "Core Team", + chipClass: "bg-indigo-500/10 text-indigo-300 border-indigo-500/30", + icon: , + }, + member: { + label: "Member", + chipClass: "bg-slate-500/10 text-slate-300 border-slate-500/30", + icon: , + }, + guardian: { + label: "Guardian", + chipClass: "bg-rose-500/10 text-rose-300 border-rose-500/30", + icon: , + }, + steward: { + label: "Steward", + chipClass: "bg-emerald-500/10 text-emerald-300 border-emerald-500/30", + icon: , + }, +}; + +const STATUS_COLORS: Record = { + 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 ( +
+

+ + Агенти MicroDAO +

+

+ Для цього MicroDAO ще не призначені агенти. +

+
+ ); + } + + // 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 ( +
+
+

+ + Агенти MicroDAO + ({agents.length}) +

+
+ + {/* Orchestrator(s) */} + {orchestrators.length > 0 && ( +
+
+ Orchestrator +
+
+ {orchestrators.map(agent => ( + + ))} +
+
+ )} + + {/* Core Team */} + {coreTeam.length > 0 && ( +
+
+ Core Team +
+
+ {coreTeam.map(agent => ( + + ))} +
+
+ )} + + {/* Other Members */} + {others.length > 0 && ( +
+
+ Members +
+
+ {others.map(agent => ( + + ))} +
+
+ )} +
+ ); +} + +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 ( + +
+
+
+ {agent.avatar_url ? ( + {agent.name} + ) : ( + + )} +
+ +
+
+
{agent.name}
+
+
+ + ); + } + + return ( + +
+
+ {/* Avatar */} +
+
+ {agent.avatar_url ? ( + {agent.name} + ) : ( + + )} +
+ +
+ + {/* Info */} +
+
+ + {agent.name} + + {agent.is_core && ( + + Core + + )} +
+ + {/* Role badge */} +
+ {roleMeta.icon} + {roleMeta.label} +
+ + {/* Kind and Gov Level */} + {(agent.kind || agent.gov_level) && ( +
+ {agent.kind && {agent.kind}} + {agent.kind && agent.gov_level && } + {agent.gov_level && {agent.gov_level}} +
+ )} +
+
+
+ + ); +} + diff --git a/apps/web/src/components/microdao/MicrodaoRoomsSection.tsx b/apps/web/src/components/microdao/MicrodaoRoomsSection.tsx index 4a9f12d8..b4d52fc9 100644 --- a/apps/web/src/components/microdao/MicrodaoRoomsSection.tsx +++ b/apps/web/src/components/microdao/MicrodaoRoomsSection.tsx @@ -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, }, + // New room roles for MicroDAO + operations: { + label: "Operations", + chipClass: "bg-orange-500/10 text-orange-300 border-orange-500/30", + icon: , + }, + knowledge: { + label: "Knowledge Base", + chipClass: "bg-cyan-500/10 text-cyan-300 border-cyan-500/30", + icon: , + }, + treasury: { + label: "Treasury", + chipClass: "bg-yellow-500/10 text-yellow-300 border-yellow-500/30", + icon: , + }, + "ai-core": { + label: "AI Core", + chipClass: "bg-purple-500/10 text-purple-300 border-purple-500/30", + icon: , + }, + // District-specific + events: { + label: "Events", + chipClass: "bg-pink-500/10 text-pink-300 border-pink-500/30", + icon: , + }, + masters: { + label: "Masters", + chipClass: "bg-indigo-500/10 text-indigo-300 border-indigo-500/30", + icon: , + }, + supply: { + label: "Supply Chain", + chipClass: "bg-green-500/10 text-green-300 border-green-500/30", + icon: , + }, + producers: { + label: "Producers", + chipClass: "bg-lime-500/10 text-lime-300 border-lime-500/30", + icon: , + }, + compute: { + label: "Compute Grid", + chipClass: "bg-amber-500/10 text-amber-300 border-amber-500/30", + icon: , + }, + providers: { + label: "Providers", + chipClass: "bg-orange-500/10 text-orange-300 border-orange-500/30", + icon: , + }, }; export function MicrodaoRoomsSection({ diff --git a/apps/web/src/hooks/useMicrodao.ts b/apps/web/src/hooks/useMicrodao.ts index f7d7f60c..7e4fa04c 100644 --- a/apps/web/src/hooks/useMicrodao.ts +++ b/apps/web/src/hooks/useMicrodao.ts @@ -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; +} + +export function useMicrodaoAgents( + slug: string | undefined, + options: UseMicrodaoAgentsOptions = {} +): UseMicrodaoAgentsResult { + const { refreshInterval = 60000, enabled = true } = options; + + const [agents, setAgents] = useState([]); + const [microdaoId, setMicrodaoId] = useState(null); + const [microdaoSlug, setMicrodaoSlug] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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, + }; +}