diff --git a/apps/web/src/app/agents/page.tsx b/apps/web/src/app/agents/page.tsx index 992ab7ac..a42478ee 100644 --- a/apps/web/src/app/agents/page.tsx +++ b/apps/web/src/app/agents/page.tsx @@ -1,47 +1,184 @@ -import Link from 'next/link' -import { Bot, Zap, Clock, CheckCircle2, XCircle, Sparkles } from 'lucide-react' -import { api, Agent } from '@/lib/api' -import { cn } from '@/lib/utils' +'use client'; -// Force dynamic rendering -export const dynamic = 'force-dynamic' +import Link from 'next/link'; +import { Bot, Users, Building2, Server, ExternalLink } from 'lucide-react'; +import { useAgentList } from '@/hooks/useAgents'; +import { AgentSummary } from '@/lib/types/agents'; -async function getAgents(): Promise { - try { - return await api.getAgents() - } catch (error) { - console.error('Failed to fetch agents:', error) - return [] - } +// Kind emoji mapping +const kindEmoji: Record = { + vision: '👁️', + curator: '🎨', + security: '🛡️', + finance: '💰', + civic: '🏛️', + oracle: '🔮', + builder: '🏗️', + research: '🔬', + marketing: '📢', + orchestrator: '🎭', + mediator: '⚖️', + assistant: '🤖', +}; + +function getNodeBadge(nodeId: string | undefined | null): { label: string; color: string } { + if (!nodeId) return { label: 'Unknown', color: 'bg-gray-500/20 text-gray-400' }; + if (nodeId.includes('node-1')) return { label: 'НОДА1', color: 'bg-emerald-500/20 text-emerald-400' }; + if (nodeId.includes('node-2')) return { label: 'НОДА2', color: 'bg-amber-500/20 text-amber-400' }; + return { label: 'НОДА', color: 'bg-purple-500/20 text-purple-400' }; } -export default async function AgentsPage() { - const agents = await getAgents() +function AgentCard({ agent }: { agent: AgentSummary }) { + const isOnline = agent.status === 'online'; + const statusColor = isOnline ? 'text-emerald-400' : 'text-white/40'; + const emoji = kindEmoji[agent.kind] || '🤖'; + const nodeBadge = getNodeBadge(agent.home_node?.id); return ( -
-
+ + {/* Header */} +
+
+ {agent.avatar_url ? ( + // eslint-disable-next-line @next/next/no-img-element + {agent.display_name} + ) : ( + {emoji} + )} +
+
+

+ {agent.display_name} +

+ {agent.public_title && ( +

{agent.public_title}

+ )} +

{agent.kind}

+
+
+ + {/* District & MicroDAO */} +
+ {agent.district && ( +
+ + {agent.district} +
+ )} + {agent.microdao_memberships.length > 0 && ( +
+ + {agent.microdao_memberships[0].microdao_name} +
+ )} +
+ + {/* Footer */} +
+
+ + + {isOnline ? 'online' : 'offline'} + + + {nodeBadge.label} + + {agent.is_public && ( + + Public + + )} +
+ + Open → + +
+ + ); +} + +export default function AgentsPage() { + const { agents, total, isLoading, error } = useAgentList({ limit: 100 }); + + return ( +
+
{/* Header */}
-
-
- -
-
-

Agent Console

-

Управління та виклик AI-агентів

+
+ +

+ Agent Console +

+
+

+ Всі AI-агенти мережі DAARION +

+

+ Знайдено агентів: {total} +

+
+ + {/* Filters */} +
+
+
+ + Фільтр по нодах: + + НОДА1 (Production) + + + НОДА2 (Development) +
- {/* Agents Grid */} - {agents.length === 0 ? ( -
- + {/* Content */} + {error && ( +
+

Помилка завантаження агентів

+
+ )} + + {isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : agents.length === 0 ? ( +
+

Агенти не знайдені

-

+

Наразі немає доступних агентів. Спробуйте пізніше.

@@ -53,150 +190,26 @@ export default async function AgentsPage() {
)} - {/* Quick Actions */} -
-

- - Швидкі дії -

- -
- - - - -
+ {/* Links */} +
+ + + Публічні громадяни + + + + + Node Dashboard + +
- ) + ); } - -function AgentCard({ agent }: { agent: Agent }) { - const isOnline = agent.status === 'active' && agent.is_active - - return ( - -
- {/* Avatar */} -
- {agent.avatar_url ? ( - // eslint-disable-next-line @next/next/no-img-element - {agent.name} - ) : ( - - )} -
- - {/* Info */} -
-
-

- {agent.name} -

- {isOnline ? ( - - ) : ( - - )} -
- -

- {agent.description || 'Без опису'} -

- -
- - {agent.kind} - - {agent.model && ( - - {agent.model} - - )} -
-
-
- - {/* Capabilities */} - {agent.capabilities && agent.capabilities.length > 0 && ( -
-
- {agent.capabilities.slice(0, 3).map((cap, i) => ( - - {cap} - - ))} - {agent.capabilities.length > 3 && ( - - +{agent.capabilities.length - 3} - - )} -
-
- )} - - ) -} - -function QuickAction({ - icon: Icon, - title, - description, - href -}: { - icon: React.ComponentType<{ className?: string }> - title: string - description: string - href: string -}) { - return ( - - -

{title}

-

{description}

- - ) -} - diff --git a/apps/web/src/app/api/agents/list/route.ts b/apps/web/src/app/api/agents/list/route.ts new file mode 100644 index 00000000..642aa0bc --- /dev/null +++ b/apps/web/src/app/api/agents/list/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || 'http://daarion-city-service:7001'; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const kind = searchParams.get('kind'); + const node_id = searchParams.get('node_id'); + const limit = searchParams.get('limit') || '100'; + const offset = searchParams.get('offset') || '0'; + + const params = new URLSearchParams(); + if (kind) params.set('kind', kind); + if (node_id) params.set('node_id', node_id); + params.set('limit', limit); + params.set('offset', offset); + + const response = await fetch(`${CITY_API_URL}/public/agents?${params.toString()}`, { + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }); + + if (!response.ok) { + const text = await response.text(); + console.error('Failed to fetch agents:', response.status, text); + return NextResponse.json( + { error: 'Failed to fetch agents' }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching agents:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + diff --git a/apps/web/src/hooks/useAgents.ts b/apps/web/src/hooks/useAgents.ts new file mode 100644 index 00000000..1581f316 --- /dev/null +++ b/apps/web/src/hooks/useAgents.ts @@ -0,0 +1,58 @@ +'use client'; + +import useSWR from 'swr'; +import { AgentSummary, AgentListResponse, AgentDashboard } from '@/lib/types/agents'; + +const fetcher = async (url: string) => { + const res = await fetch(url); + if (!res.ok) { + throw new Error('Failed to fetch'); + } + return res.json(); +}; + +export function useAgentList(params?: { + kind?: string; + node_id?: string; + limit?: number; + offset?: number; +}) { + const searchParams = new URLSearchParams(); + if (params?.kind) searchParams.set('kind', params.kind); + if (params?.node_id) searchParams.set('node_id', params.node_id); + if (params?.limit) searchParams.set('limit', params.limit.toString()); + if (params?.offset) searchParams.set('offset', params.offset.toString()); + + const queryString = searchParams.toString(); + const url = `/api/agents/list${queryString ? `?${queryString}` : ''}`; + + const { data, error, isLoading, mutate } = useSWR(url, fetcher, { + revalidateOnFocus: false, + }); + + return { + agents: data?.items || [], + total: data?.total || 0, + isLoading, + error, + mutate, + }; +} + +export function useAgentDashboard(agentId: string | undefined) { + const { data, error, isLoading, mutate } = useSWR( + agentId ? `/api/agents/${agentId}/dashboard` : null, + fetcher, + { + revalidateOnFocus: false, + } + ); + + return { + agent: data, + isLoading, + error, + mutate, + }; +} + diff --git a/apps/web/src/lib/types/agents.ts b/apps/web/src/lib/types/agents.ts new file mode 100644 index 00000000..5efd1dec --- /dev/null +++ b/apps/web/src/lib/types/agents.ts @@ -0,0 +1,54 @@ +import { HomeNodeInfo } from './citizens'; + +export interface AgentMicrodaoMembership { + microdao_id: string; + microdao_slug: string; + microdao_name: string; + role?: string; + is_core: boolean; +} + +export interface AgentSummary { + id: string; + display_name: string; + kind: string; + avatar_url?: string | null; + status: string; + is_public: boolean; + public_slug?: string | null; + public_title?: string | null; + district?: string | null; + home_node?: HomeNodeInfo | null; + microdao_memberships: AgentMicrodaoMembership[]; +} + +export interface AgentListResponse { + items: AgentSummary[]; + total: number; +} + +export interface AgentDashboard { + id: string; + display_name: string; + kind: string; + avatar_url?: string | null; + status: string; + is_public: boolean; + public_slug?: string | null; + public_title?: string | null; + public_tagline?: string | null; + public_skills: string[]; + district?: string | null; + home_node?: HomeNodeInfo | null; + microdao_memberships: AgentMicrodaoMembership[]; + system_prompts?: { + core?: string; + safety?: string; + governance?: string; + tools?: string; + }; + capabilities: string[]; + model?: string | null; + role?: string | null; +} +