feat: UI alignment - Agent Console, Citizens, MicroDAO Dashboard (TASK 2)

This commit is contained in:
Apple
2025-11-28 09:09:00 -08:00
parent 37e1c8abbe
commit acc06f41d3
12 changed files with 1063 additions and 463 deletions

View File

@@ -16,7 +16,26 @@ import {
import { AgentVisibilityCard } from '@/components/agent-dashboard/AgentVisibilityCard';
import { api, Agent, AgentInvokeResponse } from '@/lib/api';
import { updateAgentVisibility } from '@/lib/api/agents';
import { AgentVisibilityPayload, VisibilityScope } from '@/lib/types/agents';
import { AgentVisibilityPayload, VisibilityScope, getNodeBadgeLabel } from '@/lib/types/agents';
import { Bot, Settings, FileText, Building2, Cpu, MessageSquare, BarChart3, Users, Globe, Lock, Eye, EyeOff, ChevronLeft, Loader2 } from 'lucide-react';
// Tab types
type TabId = 'dashboard' | 'prompts' | 'microdao' | 'identity' | 'models' | 'chat';
interface Tab {
id: TabId;
label: string;
icon: React.ReactNode;
}
const TABS: Tab[] = [
{ id: 'dashboard', label: 'Dashboard', icon: <BarChart3 className="w-4 h-4" /> },
{ id: 'prompts', label: 'System Prompts', icon: <FileText className="w-4 h-4" /> },
{ id: 'microdao', label: 'MicroDAO', icon: <Building2 className="w-4 h-4" /> },
{ id: 'identity', label: 'Identity', icon: <Bot className="w-4 h-4" /> },
{ id: 'models', label: 'Models', icon: <Cpu className="w-4 h-4" /> },
{ id: 'chat', label: 'Chat', icon: <MessageSquare className="w-4 h-4" /> },
];
// Chat Message type
interface Message {
@@ -31,10 +50,10 @@ interface Message {
};
}
export default function AgentPage() {
export default function AgentConsolePage() {
const params = useParams();
const agentId = params.agentId as string;
const [activeTab, setActiveTab] = useState<'dashboard' | 'chat'>('dashboard');
const [activeTab, setActiveTab] = useState<TabId>('dashboard');
// Dashboard state
const { dashboard, isLoading: dashboardLoading, error: dashboardError, refresh } = useAgentDashboard(agentId, {
@@ -43,7 +62,6 @@ export default function AgentPage() {
// Chat state
const [agent, setAgent] = useState<Agent | null>(null);
const [chatLoading, setChatLoading] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [invoking, setInvoking] = useState(false);
@@ -53,13 +71,10 @@ export default function AgentPage() {
useEffect(() => {
async function loadAgent() {
try {
setChatLoading(true);
const data = await api.getAgent(agentId);
setAgent(data);
} catch (error) {
console.error('Failed to load agent:', error);
} finally {
setChatLoading(false);
}
}
if (activeTab === 'chat') {
@@ -115,14 +130,14 @@ export default function AgentPage() {
};
// Loading state
if (dashboardLoading && !dashboard && activeTab === 'dashboard') {
if (dashboardLoading && !dashboard) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 p-6">
<div className="max-w-5xl mx-auto">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin w-12 h-12 border-4 border-cyan-500 border-t-transparent rounded-full mx-auto mb-4" />
<p className="text-white/70">Loading agent dashboard...</p>
<Loader2 className="w-12 h-12 text-cyan-500 animate-spin mx-auto mb-4" />
<p className="text-white/70">Loading agent console...</p>
</div>
</div>
</div>
@@ -131,12 +146,12 @@ export default function AgentPage() {
}
// Error state
if (dashboardError && activeTab === 'dashboard') {
if (dashboardError) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 p-6">
<div className="max-w-5xl mx-auto">
<div className="max-w-6xl mx-auto">
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl p-6 text-center">
<p className="text-red-400 text-lg mb-2">Failed to load agent dashboard</p>
<p className="text-red-400 text-lg mb-2">Failed to load agent console</p>
<p className="text-white/50 mb-4">{dashboardError.message}</p>
<div className="flex gap-4 justify-center">
<button
@@ -158,57 +173,146 @@ export default function AgentPage() {
);
}
const profile = dashboard?.profile;
const nodeLabel = profile?.node_id ? getNodeBadgeLabel(profile.node_id) : 'Unknown';
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 p-6">
<div className="max-w-5xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Link
href="/agents"
className="p-2 bg-white/5 hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-white">
{dashboard?.profile.display_name || agent?.name || agentId}
</h1>
<p className="text-white/50 text-sm">Agent Cabinet</p>
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900">
{/* Header */}
<div className="border-b border-white/10 bg-black/20 backdrop-blur-md sticky top-0 z-10">
<div className="max-w-6xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link
href="/agents"
className="p-2 bg-white/5 hover:bg-white/10 rounded-lg transition-colors"
>
<ChevronLeft className="w-5 h-5 text-white" />
</Link>
{/* Agent Avatar & Name */}
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-cyan-500/20 to-purple-500/20 flex items-center justify-center border border-white/10">
{profile?.avatar_url ? (
<img src={profile.avatar_url} alt="" className="w-full h-full rounded-xl object-cover" />
) : (
<Bot className="w-6 h-6 text-cyan-400" />
)}
</div>
<div>
<h1 className="text-xl font-bold text-white">
{profile?.display_name || agentId}
</h1>
<div className="flex items-center gap-2 text-sm">
<span className="text-white/50">{profile?.kind || 'agent'}</span>
<span className="text-white/30"></span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
nodeLabel === 'НОДА1' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-violet-500/20 text-violet-400'
}`}>
{nodeLabel}
</span>
{profile?.is_orchestrator && (
<>
<span className="text-white/30"></span>
<span className="px-2 py-0.5 rounded text-xs font-medium bg-amber-500/20 text-amber-400">
Orchestrator
</span>
</>
)}
</div>
</div>
</div>
</div>
{/* Status & Actions */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${
profile?.status === 'online' ? 'bg-emerald-500' : 'bg-white/30'
}`} />
<span className="text-sm text-white/50">
{profile?.status || 'offline'}
</span>
</div>
{/* Public/Private Badge */}
{profile?.is_public ? (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-cyan-500/20 text-cyan-400 text-sm">
<Globe className="w-4 h-4" />
Public Citizen
</span>
) : (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white/10 text-white/50 text-sm">
<Lock className="w-4 h-4" />
Private
</span>
)}
{/* Link to Citizen Profile if public */}
{profile?.is_public && profile?.public_slug && (
<Link
href={`/citizens/${profile.public_slug}`}
className="px-3 py-1.5 bg-white/5 hover:bg-white/10 text-white/70 text-sm rounded-lg transition-colors flex items-center gap-1.5"
>
<Eye className="w-4 h-4" />
View Public Profile
</Link>
)}
</div>
</div>
{/* Tabs */}
<div className="flex gap-2">
<button
onClick={() => setActiveTab('dashboard')}
className={`px-4 py-2 rounded-lg transition-colors ${
activeTab === 'dashboard'
? 'bg-cyan-500/20 text-cyan-400'
: 'bg-white/5 text-white/50 hover:bg-white/10'
}`}
>
📊 Dashboard
</button>
<button
onClick={() => setActiveTab('chat')}
className={`px-4 py-2 rounded-lg transition-colors ${
activeTab === 'chat'
? 'bg-cyan-500/20 text-cyan-400'
: 'bg-white/5 text-white/50 hover:bg-white/10'
}`}
>
💬 Chat
</button>
<div className="flex gap-1 mt-4 overflow-x-auto pb-1">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors whitespace-nowrap ${
activeTab === tab.id
? 'bg-cyan-500/20 text-cyan-400'
: 'bg-white/5 text-white/50 hover:bg-white/10 hover:text-white/70'
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
</div>
</div>
{/* Content */}
<div className="max-w-6xl mx-auto px-6 py-6">
{/* Dashboard Tab */}
{activeTab === 'dashboard' && dashboard && (
<div className="space-y-6">
{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="text-white/50 text-sm mb-1">MicroDAOs</div>
<div className="text-2xl font-bold text-white">{dashboard.microdao_memberships?.length || 0}</div>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="text-white/50 text-sm mb-1">Visibility</div>
<div className="text-lg font-medium text-white capitalize">
{dashboard.public_profile?.visibility_scope || 'city'}
</div>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="text-white/50 text-sm mb-1">Kind</div>
<div className="text-lg font-medium text-white capitalize">{profile?.kind}</div>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="text-white/50 text-sm mb-1">Status</div>
<div className={`text-lg font-medium ${profile?.status === 'online' ? 'text-emerald-400' : 'text-white/50'}`}>
{profile?.status || 'offline'}
</div>
</div>
</div>
{/* Main Info Cards */}
<AgentSummaryCard profile={dashboard.profile} runtime={dashboard.runtime} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<AgentDAISCard dais={dashboard.profile.dais} />
<div className="space-y-6">
@@ -216,13 +320,97 @@ export default function AgentPage() {
<AgentMetricsCard metrics={dashboard.metrics} />
</div>
</div>
{/* System Prompts - Full Width */}
</div>
)}
{/* System Prompts Tab */}
{activeTab === 'prompts' && dashboard && (
<div className="space-y-6">
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<FileText className="w-5 h-5 text-cyan-400" />
System Prompts
</h2>
<p className="text-white/50 mb-6">
Configure the agent&apos;s behavior through system prompts. These prompts define how the agent responds and operates.
</p>
</div>
<AgentSystemPromptsCard
agentId={dashboard.profile.agent_id}
systemPrompts={dashboard.system_prompts}
canEdit={true} // TODO: Check user role
canEdit={true}
onUpdated={refresh}
/>
</div>
)}
{/* MicroDAO Tab */}
{activeTab === 'microdao' && dashboard && (
<div className="space-y-6">
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<Building2 className="w-5 h-5 text-cyan-400" />
MicroDAO Membership
</h2>
<p className="text-white/50 mb-4">
Manage which MicroDAOs this agent belongs to. Every agent must belong to at least one MicroDAO.
</p>
{/* Primary MicroDAO */}
{profile?.primary_microdao_id && (
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-4 mb-4">
<div className="text-sm text-cyan-400 mb-1">Primary MicroDAO</div>
<Link
href={`/microdao/${profile.primary_microdao_slug}`}
className="text-lg font-medium text-white hover:text-cyan-400 transition-colors"
>
{profile.primary_microdao_name || profile.primary_microdao_slug}
</Link>
</div>
)}
{/* Orchestrator Actions */}
{profile?.is_orchestrator && (
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4">
<div className="flex items-center gap-2 text-amber-400 mb-2">
<Users className="w-4 h-4" />
<span className="font-medium">Orchestrator Privileges</span>
</div>
<p className="text-white/50 text-sm mb-3">
As an orchestrator, this agent can create and manage MicroDAOs.
</p>
<button
disabled
className="px-4 py-2 bg-amber-500/20 text-amber-400 rounded-lg text-sm opacity-50 cursor-not-allowed"
>
Create MicroDAO (Coming Soon)
</button>
</div>
)}
</div>
<AgentMicrodaoMembershipCard
agentId={dashboard.profile.agent_id}
memberships={dashboard.microdao_memberships ?? []}
canEdit={true}
onUpdated={refresh}
/>
</div>
)}
{/* Identity Tab */}
{activeTab === 'identity' && dashboard && (
<div className="space-y-6">
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<Bot className="w-5 h-5 text-cyan-400" />
Agent Identity & Visibility
</h2>
<p className="text-white/50 mb-4">
Configure how this agent appears to others and whether it&apos;s visible as a public citizen.
</p>
</div>
{/* Visibility Settings */}
<AgentVisibilityCard
@@ -239,19 +427,47 @@ export default function AgentPage() {
<AgentPublicProfileCard
agentId={dashboard.profile.agent_id}
publicProfile={dashboard.public_profile}
canEdit={true} // TODO: Check user role
onUpdated={refresh}
/>
<AgentMicrodaoMembershipCard
agentId={dashboard.profile.agent_id}
memberships={dashboard.microdao_memberships ?? []}
canEdit={true}
onUpdated={refresh}
/>
</div>
)}
{/* Models Tab */}
{activeTab === 'models' && dashboard && (
<div className="space-y-6">
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<Cpu className="w-5 h-5 text-cyan-400" />
Model Configuration
</h2>
<p className="text-white/50 mb-6">
Configure which AI models this agent uses for different tasks.
</p>
{/* Current Model */}
<div className="bg-white/5 rounded-lg p-4 border border-white/10 mb-4">
<div className="text-sm text-white/50 mb-1">Current Model</div>
<div className="text-lg font-medium text-white">
{dashboard.profile.model || 'Default (via DAGI Router)'}
</div>
</div>
{/* Model Bindings (placeholder) */}
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4">
<div className="flex items-center gap-2 text-yellow-400 mb-2">
<Settings className="w-4 h-4" />
<span className="font-medium">Model Bindings</span>
</div>
<p className="text-white/50 text-sm">
Advanced model configuration will be available in a future update.
Currently, models are managed through the DAGI Router.
</p>
</div>
</div>
</div>
)}
{/* Chat Tab */}
{activeTab === 'chat' && (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden">
@@ -259,8 +475,8 @@ export default function AgentPage() {
<div className="h-[500px] overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-white/50 py-8">
<p className="text-4xl mb-2">💬</p>
<p>Start a conversation with {dashboard?.profile.display_name || agent?.name || agentId}</p>
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>Start a conversation with {profile?.display_name || agentId}</p>
</div>
)}
{messages.map(msg => (
@@ -289,7 +505,7 @@ export default function AgentPage() {
<div className="flex justify-start">
<div className="bg-white/10 p-3 rounded-xl">
<div className="flex items-center gap-2">
<div className="animate-spin w-4 h-4 border-2 border-cyan-500 border-t-transparent rounded-full" />
<Loader2 className="w-4 h-4 text-cyan-500 animate-spin" />
<span className="text-white/50">Thinking...</span>
</div>
</div>
@@ -325,4 +541,3 @@ export default function AgentPage() {
</div>
);
}

View File

@@ -7,8 +7,9 @@ import { getAgentKindIcon } from '@/lib/agent-dashboard';
import { useCitizenProfile, useCitizenInteraction } from '@/hooks/useCitizens';
import { askCitizen } from '@/lib/api/citizens';
import { CityChatWidget } from '@/components/city/CityChatWidget';
import { ChevronLeft, Building2, MapPin, MessageSquare, HelpCircle, Loader2, Users } from 'lucide-react';
type LooseRecord = Record<string, any>;
type LooseRecord = Record<string, unknown>;
export default function CitizenProfilePage() {
const params = useParams<{ slug: string }>();
@@ -30,7 +31,7 @@ export default function CitizenProfilePage() {
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 p-6">
<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-12 h-12 border-4 border-cyan-500 border-t-transparent rounded-full" />
<Loader2 className="w-12 h-12 text-cyan-500 animate-spin" />
</div>
</div>
</div>
@@ -43,8 +44,9 @@ export default function CitizenProfilePage() {
<div className="max-w-4xl mx-auto">
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl p-6 text-center">
<p className="text-red-400 text-lg mb-4">{error?.message || 'Citizen not found'}</p>
<Link href="/citizens" className="text-cyan-400 hover:underline">
Back to Citizens
<Link href="/citizens" className="text-cyan-400 hover:underline flex items-center justify-center gap-2">
<ChevronLeft className="w-4 h-4" />
Back to Citizens
</Link>
</div>
</div>
@@ -53,73 +55,89 @@ export default function CitizenProfilePage() {
}
const status = citizen.status || 'unknown';
const statusColor =
status === 'online' ? 'bg-emerald-500/20 text-emerald-300' : 'bg-white/10 text-white/60';
const isOnline = status === 'online';
const daisCore = (citizen.dais_public?.core as LooseRecord) || {};
const daisPhenotype = (citizen.dais_public?.phenotype as LooseRecord) || {};
const daisMemex = (citizen.dais_public?.memex as LooseRecord) || {};
const daisEconomics = (citizen.dais_public?.economics as LooseRecord) || {};
const metricsEntries = Object.entries(citizen.metrics_public || {});
const actions = (citizen.interaction?.actions as string[]) || [];
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 p-6">
<div className="max-w-5xl mx-auto space-y-6">
{/* Back Link */}
<Link
href="/citizens"
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors"
>
Back to Citizens
<ChevronLeft className="w-4 h-4" />
Back to Citizens
</Link>
{/* Hero Section */}
<section className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden">
<div className="bg-gradient-to-r from-cyan-500/20 to-purple-500/20 p-8">
<div className="flex flex-col gap-6 md:flex-row md:items-start">
<div className="w-24 h-24 flex-shrink-0 rounded-2xl bg-gradient-to-br from-cyan-500/40 to-purple-500/40 flex items-center justify-center text-5xl shadow-xl">
{getAgentKindIcon(citizen.kind || '')}
{/* Avatar */}
<div className="w-24 h-24 flex-shrink-0 rounded-2xl bg-gradient-to-br from-cyan-500/40 to-purple-500/40 flex items-center justify-center text-5xl shadow-xl overflow-hidden">
{citizen.avatar_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={citizen.avatar_url} alt="" className="w-full h-full object-cover" />
) : (
getAgentKindIcon(citizen.kind || '')
)}
</div>
{/* Info */}
<div className="flex-1 space-y-3">
<h1 className="text-3xl font-bold text-white">{citizen.display_name}</h1>
<div>
<p className="text-cyan-400/80 text-sm mb-1">Громадянин DAARION City</p>
<h1 className="text-3xl font-bold text-white">{citizen.display_name}</h1>
</div>
<p className="text-cyan-200 text-lg">
{citizen.public_title || citizen.kind || 'Citizen of DAARION'}
{citizen.public_title || citizen.kind || 'Citizen'}
</p>
{/* Badges */}
<div className="flex flex-wrap gap-3 text-sm">
<span className={`px-3 py-1 rounded-full ${statusColor}`}>
{/* Status */}
<span className={`px-3 py-1 rounded-full flex items-center gap-1.5 ${
isOnline ? 'bg-emerald-500/20 text-emerald-300' : 'bg-white/10 text-white/60'
}`}>
<span className={`w-2 h-2 rounded-full ${isOnline ? 'bg-emerald-500' : 'bg-white/30'}`} />
{status}
</span>
{/* District */}
{citizen.district && (
<span className="px-3 py-1 rounded-full bg-white/10 text-white/70">
{citizen.district} District
<span className="px-3 py-1 rounded-full bg-white/10 text-white/70 flex items-center gap-1.5">
<MapPin className="w-3 h-3" />
{citizen.district}
</span>
)}
{/* MicroDAO */}
{citizen.microdao && (
<Link
href={`/microdao/${citizen.microdao.slug}`}
className="px-3 py-1 rounded-full bg-cyan-500/20 text-cyan-200 hover:bg-cyan-500/30"
className="px-3 py-1 rounded-full bg-purple-500/20 text-purple-200 hover:bg-purple-500/30 flex items-center gap-1.5 transition-colors"
>
MicroDAO: {citizen.microdao.name}
<Building2 className="w-3 h-3" />
{citizen.microdao.name}
</Link>
)}
</div>
</div>
{citizen.admin_panel_url && (
<Link
href={citizen.admin_panel_url}
className="px-4 py-2 bg-purple-500/20 text-purple-200 rounded-lg hover:bg-purple-500/30 transition-colors text-sm flex items-center gap-2"
>
Agent Dashboard
</Link>
)}
</div>
</div>
<div className="p-8 space-y-8">
{/* Content */}
<div className="p-8 space-y-6">
{/* Tagline */}
{citizen.public_tagline && (
<blockquote className="text-xl text-white/80 italic border-l-4 border-cyan-500/60 pl-4">
"{citizen.public_tagline}"
&ldquo;{citizen.public_tagline}&rdquo;
</blockquote>
)}
{/* Skills */}
{citizen.public_skills?.length > 0 && (
<div>
<h3 className="text-xs uppercase text-white/40 mb-2">Skills</h3>
@@ -135,74 +153,31 @@ export default function CitizenProfilePage() {
</div>
</div>
)}
<div className="grid gap-4 md:grid-cols-2">
{citizen.district && (
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
<p className="text-xs uppercase text-white/40">District</p>
<p className="text-white mt-1 text-lg">{citizen.district}</p>
</div>
)}
{citizen.city_presence?.primary_room_slug && (
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
<p className="text-xs uppercase text-white/40">Primary Room</p>
<p className="text-white mt-1 text-lg">
#{citizen.city_presence.primary_room_slug}
</p>
</div>
)}
{citizen.home_node && (
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
<p className="text-xs uppercase text-white/40">Home Node</p>
<div className="mt-2 space-y-1">
<p className="text-white text-lg">{citizen.home_node.name || citizen.node_id}</p>
{citizen.home_node.roles && citizen.home_node.roles.length > 0 && (
<div className="flex flex-wrap gap-1">
{citizen.home_node.roles.map((role) => (
<span
key={role}
className={`px-2 py-0.5 rounded text-xs ${
role === 'gpu' ? 'bg-amber-500/20 text-amber-300' :
role === 'core' ? 'bg-emerald-500/20 text-emerald-300' :
role === 'development' ? 'bg-purple-500/20 text-purple-300' :
'bg-white/10 text-white/60'
}`}
>
{role}
</span>
))}
</div>
)}
{citizen.home_node.environment && (
<span className={`inline-block px-2 py-0.5 rounded text-xs ${
citizen.home_node.environment === 'production'
? 'bg-emerald-500/20 text-emerald-300'
: 'bg-amber-500/20 text-amber-300'
}`}>
{citizen.home_node.environment}
</span>
)}
</div>
</div>
)}
</div>
</div>
</section>
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
<h2 className="text-white font-semibold">Взаємодія з громадянином</h2>
{/* Interaction Section */}
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-6">
<h2 className="text-white font-semibold flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-cyan-400" />
Взаємодія з громадянином
</h2>
<div className="space-y-2 text-sm">
<p className="text-white/60">Чат</p>
{/* Chat Link */}
<div className="space-y-2">
<p className="text-white/60 text-sm">Чат у кімнаті MicroDAO</p>
{interactionLoading ? (
<div className="text-white/40 text-xs">Завантаження</div>
<div className="text-white/40 text-xs flex items-center gap-2">
<Loader2 className="w-3 h-3 animate-spin" />
Завантаження
</div>
) : interaction?.primary_room_slug ? (
<Link
href={`/city/${interaction.primary_room_slug}`}
className="inline-flex items-center gap-2 px-4 py-2 text-sm border border-white/20 rounded-lg text-white hover:border-cyan-400/70 transition-colors"
className="inline-flex items-center gap-2 px-4 py-2 text-sm border border-cyan-500/30 bg-cyan-500/10 rounded-lg text-cyan-300 hover:bg-cyan-500/20 transition-colors"
>
Відкрити чат у кімнаті{' '}
{interaction.primary_room_name ?? interaction.primary_room_slug}
<MessageSquare className="w-4 h-4" />
Відкрити чат у кімнаті {interaction.primary_room_name ?? interaction.primary_room_slug}
</Link>
) : (
<div className="text-white/50 text-xs">
@@ -216,8 +191,12 @@ export default function CitizenProfilePage() {
)}
</div>
<div className="space-y-2 text-sm">
<p className="text-white/60">Поставити запитання</p>
{/* Ask Question */}
<div className="space-y-3">
<p className="text-white/60 text-sm flex items-center gap-2">
<HelpCircle className="w-4 h-4" />
Поставити запитання
</p>
<textarea
value={question}
onChange={(e) => setQuestion(e.target.value)}
@@ -245,9 +224,16 @@ export default function CitizenProfilePage() {
}
}}
disabled={asking || !question.trim()}
className="px-4 py-2 rounded-lg border border-white/20 text-sm text-white hover:border-cyan-400/70 transition-colors disabled:opacity-40"
className="px-4 py-2 rounded-lg bg-cyan-500/20 text-cyan-300 hover:bg-cyan-500/30 transition-colors disabled:opacity-40 flex items-center gap-2"
>
{asking ? 'Надсилання…' : 'Запитати'}
{asking ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Надсилання
</>
) : (
'Запитати'
)}
</button>
<button
onClick={() => {
@@ -262,26 +248,25 @@ export default function CitizenProfilePage() {
</div>
{askError && <div className="text-xs text-red-400">{askError}</div>}
{answer && (
<div className="mt-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white/90 whitespace-pre-wrap">
<div className="mt-2 rounded-xl border border-cyan-500/20 bg-cyan-500/5 px-4 py-3 text-sm text-white/90 whitespace-pre-wrap">
{answer}
</div>
)}
</div>
</section>
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
<h2 className="text-white font-semibold">Live-чат з громадянином</h2>
{interactionLoading ? (
<div className="text-sm text-white/70">Завантаження кімнати</div>
) : interaction?.primary_room_slug ? (
{/* Live Chat Widget */}
{interaction?.primary_room_slug && (
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
<h2 className="text-white font-semibold flex items-center gap-2">
<Users className="w-5 h-5 text-cyan-400" />
Live-чат
</h2>
<CityChatWidget roomSlug={interaction.primary_room_slug} />
) : (
<div className="text-sm text-white/60">
Для цього громадянина ще не налаштована публічна кімната чату.
</div>
)}
</section>
</section>
)}
{/* DAIS Public Passport */}
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
<h2 className="text-white font-semibold">DAIS Public Passport</h2>
<div className="grid gap-4 md:grid-cols-2">
@@ -295,33 +280,18 @@ export default function CitizenProfilePage() {
</p>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-xs uppercase text-white/40">Visual</p>
<p className="text-xs uppercase text-white/40">Visual Style</p>
<p className="text-white/70 text-sm">
{(daisPhenotype?.visual as Record<string, string>)?.style || ''}
</p>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-xs uppercase text-white/40">Memory</p>
<p className="text-white/70 text-sm">
{daisMemex && Object.keys(daisMemex).length > 0
? JSON.stringify(daisMemex)
: 'Shared city memory'}
</p>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-xs uppercase text-white/40">Economics</p>
<p className="text-white/70 text-sm">
{daisEconomics && Object.keys(daisEconomics).length > 0
? JSON.stringify(daisEconomics)
: 'per_task'}
{(daisPhenotype?.visual as Record<string, string>)?.style || 'Default'}
</p>
</div>
</div>
</section>
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
<h2 className="text-white font-semibold">City Presence</h2>
{citizen.city_presence?.rooms?.length ? (
{/* City Presence */}
{citizen.city_presence?.rooms && citizen.city_presence.rooms.length > 0 && (
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
<h2 className="text-white font-semibold">City Presence</h2>
<div className="space-y-2">
{citizen.city_presence.rooms.map((room) => (
<Link
@@ -331,57 +301,15 @@ export default function CitizenProfilePage() {
>
<div>
<p className="font-semibold">{room.name || room.slug}</p>
<p className="text-white/50 text-xs">{room.slug}</p>
<p className="text-white/50 text-xs">#{room.slug}</p>
</div>
<span className="text-white/50"></span>
<span className="text-cyan-400"></span>
</Link>
))}
</div>
) : (
<p className="text-white/50 text-sm">Публічні кімнати не вказані.</p>
)}
</section>
<section className="grid gap-4 md:grid-cols-2">
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
<h2 className="text-white font-semibold">Interaction</h2>
{actions.length > 0 ? (
<div className="flex flex-wrap gap-2">
{actions.map((action) => (
<span
key={action}
className="px-3 py-1 bg-cyan-500/20 text-cyan-200 rounded-full text-xs"
>
{action}
</span>
))}
</div>
) : (
<p className="text-white/50 text-sm">Публічні сценарії взаємодії готуються.</p>
)}
<button className="w-full mt-4 px-4 py-2 bg-cyan-500/20 text-cyan-100 rounded-lg hover:bg-cyan-500/30 transition-colors">
💬 Запросити до діалогу
</button>
</div>
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-3">
<h2 className="text-white font-semibold">Public Metrics</h2>
{metricsEntries.length ? (
<div className="space-y-2">
{metricsEntries.map(([key, value]) => (
<div key={key} className="flex items-center justify-between text-sm">
<span className="text-white/50">{key}</span>
<span className="text-white font-semibold">{String(value)}</span>
</div>
))}
</div>
) : (
<p className="text-white/50 text-sm">Метрики поки не опубліковані.</p>
)}
</div>
</section>
</section>
)}
</div>
</div>
);
}

View File

@@ -6,8 +6,10 @@ import { getAgentKindIcon } from '@/lib/agent-dashboard';
import { DISTRICTS } from '@/lib/microdao';
import { useCitizensList } from '@/hooks/useCitizens';
import type { PublicCitizenSummary } from '@/lib/types/citizens';
import { Users, Search, MapPin, Building2 } from 'lucide-react';
const CITIZEN_KINDS = [
'orchestrator',
'vision',
'curator',
'security',
@@ -16,6 +18,7 @@ const CITIZEN_KINDS = [
'oracle',
'builder',
'research',
'marketing',
];
export default function CitizensPage() {
@@ -34,24 +37,32 @@ export default function CitizensPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8 space-y-6">
<div>
<h1 className="text-3xl font-bold text-white mb-2">
🏛 Citizens of DAARION City
</h1>
<p className="text-white/60">
Публічні AI-агенти, відкриті для співпраці та взаємодії
</p>
<p className="text-sm text-cyan-300/80 mt-2">
{isLoading ? 'Оновлення списку…' : `Знайдено громадян: ${total}`}
</p>
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-cyan-500/30 to-purple-500/30 flex items-center justify-center">
<Users className="w-7 h-7 text-cyan-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-white">
Громадяни DAARION City
</h1>
<p className="text-white/60">
Публічні AI-агенти, відкриті для співпраці та взаємодії
</p>
</div>
</div>
<p className="text-sm text-cyan-300/80">
{isLoading ? 'Оновлення списку…' : `Знайдено громадян: ${total}`}
</p>
{/* Filters */}
<div className="bg-slate-900/60 border border-white/10 rounded-2xl p-4">
<div className="grid gap-4 md:grid-cols-3">
<div className="md:col-span-1">
<label className="text-xs uppercase text-white/40 block mb-2">
Пошук
<label className="text-xs uppercase text-white/40 block mb-2 flex items-center gap-1">
<Search className="w-3 h-3" /> Пошук
</label>
<input
type="text"
@@ -62,8 +73,8 @@ export default function CitizensPage() {
/>
</div>
<div>
<label className="text-xs uppercase text-white/40 block mb-2">
District
<label className="text-xs uppercase text-white/40 block mb-2 flex items-center gap-1">
<MapPin className="w-3 h-3" /> District
</label>
<select
value={district}
@@ -79,8 +90,8 @@ export default function CitizensPage() {
</select>
</div>
<div>
<label className="text-xs uppercase text-white/40 block mb-2">
Тип агента
<label className="text-xs uppercase text-white/40 block mb-2 flex items-center gap-1">
<Building2 className="w-3 h-3" /> Тип агента
</label>
<select
value={kind}
@@ -105,6 +116,7 @@ export default function CitizensPage() {
)}
</div>
{/* Citizens Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{isLoading ? (
Array.from({ length: 6 }).map((_, index) => (
@@ -122,6 +134,7 @@ export default function CitizensPage() {
{!isLoading && citizens.length === 0 && (
<div className="text-center py-12">
<Users className="w-16 h-16 text-white/20 mx-auto mb-4" />
<p className="text-white/40">Наразі немає публічних громадян за цими фільтрами.</p>
</div>
)}
@@ -130,20 +143,33 @@ export default function CitizensPage() {
);
}
/**
* Citizen Card - Public-facing view
* Shows only public information, no technical details
*/
function CitizenCard({ citizen }: { citizen: PublicCitizenSummary }) {
const status = citizen.online_status || 'unknown';
const statusColor =
status === 'online' ? 'text-emerald-400' : 'text-white/40';
const status = citizen.online_status || citizen.status || 'unknown';
const isOnline = status === 'online';
return (
<Link key={citizen.slug} href={`/citizens/${citizen.slug}`} className="group">
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 hover:border-cyan-500/50 transition-all hover:bg-white/10">
<Link href={`/citizens/${citizen.slug}`} className="group">
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 hover:border-cyan-500/50 transition-all hover:bg-white/10 h-full flex flex-col">
{/* Header */}
<div className="flex items-start gap-4 mb-4">
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-cyan-500/30 to-purple-500/30 flex items-center justify-center text-3xl">
{getAgentKindIcon(citizen.kind || '')}
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-cyan-500/30 to-purple-500/30 flex items-center justify-center text-3xl flex-shrink-0 overflow-hidden">
{citizen.avatar_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={citizen.avatar_url}
alt=""
className="w-full h-full object-cover"
/>
) : (
getAgentKindIcon(citizen.kind || '')
)}
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-white group-hover:text-cyan-400 transition-colors">
<div className="flex-1 min-w-0">
<h3 className="text-xl font-semibold text-white group-hover:text-cyan-400 transition-colors truncate">
{citizen.display_name}
</h3>
<p className="text-cyan-400 text-sm">
@@ -152,28 +178,33 @@ function CitizenCard({ citizen }: { citizen: PublicCitizenSummary }) {
</div>
</div>
{/* Tagline */}
{citizen.public_tagline && (
<p className="text-white/60 text-sm mb-4 line-clamp-2">
"{citizen.public_tagline}"
<p className="text-white/60 text-sm mb-4 line-clamp-2 italic">
&ldquo;{citizen.public_tagline}&rdquo;
</p>
)}
{/* MicroDAO & District */}
<div className="flex items-center gap-4 text-white/40 text-xs mb-4">
{citizen.district && (
<span className="flex items-center gap-1">
<span>📍</span> {citizen.district}
{citizen.microdao && (
<span className="flex items-center gap-1 text-purple-400">
<Building2 className="w-3 h-3" />
{citizen.microdao.name}
</span>
)}
{citizen.primary_room_slug && (
{citizen.district && (
<span className="flex items-center gap-1">
<span>🚪</span> #{citizen.primary_room_slug}
<MapPin className="w-3 h-3" />
{citizen.district}
</span>
)}
</div>
{/* Skills */}
{citizen.public_skills?.length > 0 && (
<div className="flex flex-wrap gap-1">
{citizen.public_skills.slice(0, 4).map((skill, index) => (
<div className="flex flex-wrap gap-1 mb-4 flex-grow">
{citizen.public_skills.slice(0, 3).map((skill, index) => (
<span
key={index}
className="px-2 py-0.5 bg-cyan-500/10 text-cyan-400 rounded text-xs"
@@ -181,35 +212,20 @@ function CitizenCard({ citizen }: { citizen: PublicCitizenSummary }) {
{skill}
</span>
))}
{citizen.public_skills.length > 4 && (
{citizen.public_skills.length > 3 && (
<span className="px-2 py-0.5 text-white/30 text-xs">
+{citizen.public_skills.length - 4}
+{citizen.public_skills.length - 3}
</span>
)}
</div>
)}
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`flex items-center gap-1.5 text-xs ${statusColor}`}>
<span
className={`w-2 h-2 rounded-full ${
status === 'online' ? 'bg-emerald-500' : 'bg-white/30'
}`}
/>
{status}
</span>
{citizen.home_node?.id && (
<span className={`px-2 py-0.5 rounded text-[10px] font-medium ${
citizen.home_node.environment === 'production'
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-amber-500/20 text-amber-400'
}`}>
{citizen.home_node.id.includes('node-1') ? 'НОДА1' :
citizen.home_node.id.includes('node-2') ? 'НОДА2' : 'НОДА'}
</span>
)}
</div>
{/* Footer */}
<div className="mt-auto pt-4 border-t border-white/10 flex items-center justify-between">
<span className={`flex items-center gap-1.5 text-xs ${isOnline ? 'text-emerald-400' : 'text-white/40'}`}>
<span className={`w-2 h-2 rounded-full ${isOnline ? 'bg-emerald-500' : 'bg-white/30'}`} />
{isOnline ? 'online' : 'offline'}
</span>
<span className="text-cyan-400 text-sm group-hover:translate-x-1 transition-transform">
View Profile
</span>
@@ -218,4 +234,3 @@ function CitizenCard({ citizen }: { citizen: PublicCitizenSummary }) {
</Link>
);
}

View File

@@ -4,6 +4,7 @@ import { useParams } from "next/navigation";
import Link from "next/link";
import { useMicrodaoDetail } from "@/hooks/useMicrodao";
import { DISTRICT_COLORS } from "@/lib/microdao";
import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot } from "lucide-react";
export default function MicrodaoDetailPage() {
const params = useParams();
@@ -23,8 +24,9 @@ export default function MicrodaoDetailPage() {
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 flex items-center justify-center">
<div className="text-center space-y-4">
<div className="text-red-400">MicroDAO не знайдено</div>
<Link href="/microdao" className="text-sm text-cyan-400 hover:underline">
Повернутися до списку
<Link href="/microdao" className="text-sm text-cyan-400 hover:underline flex items-center justify-center gap-2">
<ChevronLeft className="w-4 h-4" />
Повернутися до списку
</Link>
</div>
</div>
@@ -40,6 +42,7 @@ export default function MicrodaoDetailPage() {
const cityRooms = microdao.channels.filter((c) => c.kind === "city_room");
const crewChannels = microdao.channels.filter((c) => c.kind === "crew");
const publicCitizens = microdao.public_citizens ?? [];
const childMicrodaos = microdao.child_microdaos ?? [];
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
@@ -49,32 +52,43 @@ export default function MicrodaoDetailPage() {
href="/microdao"
className="inline-flex items-center gap-2 text-sm text-slate-400 hover:text-cyan-400 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<ChevronLeft className="w-4 h-4" />
Всі MicroDAO
</Link>
{/* Header */}
<header className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-2">
<div className="space-y-3">
<div className="flex items-center gap-3">
{microdao.logo_url && (
{microdao.logo_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={microdao.logo_url}
alt={microdao.name}
className="w-12 h-12 rounded-lg object-cover"
className="w-14 h-14 rounded-xl object-cover"
/>
) : (
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-violet-500/30 to-purple-500/30 flex items-center justify-center">
<Building2 className="w-7 h-7 text-violet-400" />
</div>
)}
<div>
<h1 className="text-2xl font-bold text-slate-100">{microdao.name}</h1>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-slate-100">{microdao.name}</h1>
{microdao.is_platform && (
<span className="px-2 py-0.5 rounded text-[10px] font-medium bg-amber-500/20 text-amber-400 border border-amber-500/30">
Platform
</span>
)}
</div>
{microdao.description && (
<p className="text-sm text-slate-400 mt-1">{microdao.description}</p>
)}
</div>
</div>
{/* Badges */}
<div className="flex flex-wrap gap-2">
{microdao.district && (
<span
@@ -87,28 +101,50 @@ export default function MicrodaoDetailPage() {
</span>
)}
<span
className={`text-xs px-3 py-1 rounded-full border font-medium ${
className={`text-xs px-3 py-1 rounded-full border font-medium flex items-center gap-1 ${
microdao.is_active
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30"
: "bg-amber-500/10 text-amber-400 border-amber-500/30"
}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${microdao.is_active ? 'bg-emerald-500' : 'bg-amber-500'}`} />
{microdao.is_active ? "Active" : "Inactive"}
</span>
{microdao.is_public && (
<span className="text-xs px-3 py-1 rounded-full border font-medium bg-blue-500/10 text-blue-400 border-blue-500/30">
Public
</span>
)}
<span className={`text-xs px-3 py-1 rounded-full border font-medium flex items-center gap-1 ${
microdao.is_public
? "bg-blue-500/10 text-blue-400 border-blue-500/30"
: "bg-slate-500/10 text-slate-400 border-slate-500/30"
}`}>
{microdao.is_public ? <Globe className="w-3 h-3" /> : <Lock className="w-3 h-3" />}
{microdao.is_public ? "Public" : "Private"}
</span>
</div>
{/* Parent MicroDAO */}
{microdao.parent_microdao_slug && (
<div className="flex items-center gap-2 text-sm text-slate-400">
<Layers className="w-4 h-4" />
<span>Parent:</span>
<Link
href={`/microdao/${microdao.parent_microdao_slug}`}
className="text-cyan-400 hover:text-cyan-300 transition-colors"
>
{microdao.parent_microdao_slug}
</Link>
</div>
)}
</div>
{/* Orchestrator */}
{orchestrator && (
<div className="text-right">
<div className="text-xs text-slate-500 mb-1">Оркестратор</div>
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-4">
<div className="text-xs text-amber-400 mb-1 flex items-center gap-1">
<Crown className="w-3 h-3" />
Оркестратор
</div>
<Link
href={`/agents/${orchestrator.agent_id}`}
className="text-sm font-medium text-cyan-400 hover:text-cyan-300 transition-colors"
className="text-sm font-medium text-slate-100 hover:text-cyan-400 transition-colors"
>
{orchestrator.display_name}
</Link>
@@ -117,13 +153,41 @@ export default function MicrodaoDetailPage() {
</div>
</header>
{/* Child MicroDAOs */}
{childMicrodaos.length > 0 && (
<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">
<Layers className="w-5 h-5 text-cyan-400" />
Дочірні MicroDAO
</h2>
<div className="grid gap-3 sm:grid-cols-2">
{childMicrodaos.map((child) => (
<Link
key={child.id}
href={`/microdao/${child.slug}`}
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 hover:border-cyan-500/30 transition-colors flex items-center justify-between"
>
<div>
<p className="text-sm font-medium text-slate-200">{child.name}</p>
<p className="text-xs text-slate-500">{child.slug}</p>
</div>
{child.is_platform && (
<span className="text-[10px] px-2 py-1 rounded bg-amber-500/10 text-amber-400">
Platform
</span>
)}
</Link>
))}
</div>
</section>
)}
{/* Agents */}
<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">
<svg className="w-5 h-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<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 ? (
@@ -146,45 +210,45 @@ export default function MicrodaoDetailPage() {
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
)}
</div>
{a.is_core && (
<span className="text-[10px] px-2 py-1 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
Core
</span>
)}
<div className="flex items-center gap-2">
{a.agent_id === microdao.orchestrator_agent_id && (
<Crown className="w-4 h-4 text-amber-400" />
)}
{a.is_core && (
<span className="text-[10px] px-2 py-1 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
Core
</span>
)}
</div>
</div>
))}
</div>
)}
</section>
{/* Public Citizens */}
{publicCitizens.length > 0 && (
<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">
<svg className="w-5 h-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<Users className="w-5 h-5 text-cyan-400" />
Громадяни цього MicroDAO
<span className="text-sm font-normal text-slate-500">({publicCitizens.length})</span>
</h2>
<div className="grid gap-3 md:grid-cols-2">
{publicCitizens.map((citizen) => (
<Link
key={citizen.slug}
href={`/citizens/${citizen.slug}`}
className="flex items-center justify-between border border-white/10 rounded-lg px-4 py-3 hover:border-cyan-500/40 transition-colors"
className="flex items-center justify-between bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 hover:border-cyan-500/40 transition-colors"
>
<div>
<p className="text-white font-medium">{citizen.display_name}</p>
<p className="text-slate-200 font-medium">{citizen.display_name}</p>
{citizen.public_title && (
<p className="text-sm text-white/60">{citizen.public_title}</p>
<p className="text-sm text-slate-400">{citizen.public_title}</p>
)}
</div>
{citizen.district && (
<span className="text-xs text-white/50">{citizen.district}</span>
<span className="text-xs text-slate-500">{citizen.district}</span>
)}
</Link>
))}
@@ -195,9 +259,7 @@ export default function MicrodaoDetailPage() {
{/* Channels */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<svg className="w-5 h-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<MessageSquare className="w-5 h-5 text-cyan-400" />
Канали та кімнати
</h2>
@@ -221,9 +283,6 @@ export default function MicrodaoDetailPage() {
rel="noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-500/10 border border-blue-500/30 rounded-full text-sm text-blue-400 hover:bg-blue-500/20 transition-colors"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
{c.display_name || c.ref_id}
</a>
))}
@@ -241,9 +300,6 @@ export default function MicrodaoDetailPage() {
key={c.ref_id}
className="inline-flex items-center gap-2 px-4 py-2 bg-green-500/10 border border-green-500/30 rounded-full text-sm text-green-400"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.33-.439.576-.12.245-.18.567-.18.958v5.043H4.833V7.81zm13.086 15.64V.55h1.648V0H24v24h-2.28v-.55z"/>
</svg>
{c.display_name || c.ref_id}
</span>
))}
@@ -262,9 +318,6 @@ export default function MicrodaoDetailPage() {
href={`/city/${c.ref_id}`}
className="inline-flex items-center gap-2 px-4 py-2 bg-violet-500/10 border border-violet-500/30 rounded-full text-sm text-violet-400 hover:bg-violet-500/20 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
{c.display_name || c.ref_id}
</Link>
))}
@@ -282,9 +335,6 @@ export default function MicrodaoDetailPage() {
key={c.ref_id}
className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500/10 border border-orange-500/30 rounded-full text-sm text-orange-400"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
{c.display_name || c.ref_id}
</span>
))}
@@ -295,21 +345,32 @@ export default function MicrodaoDetailPage() {
)}
</section>
{/* Future: Stats & Tokens */}
{/* Stats */}
<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">
<svg className="w-5 h-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Статистика та токени
<BarChart3 className="w-5 h-5 text-cyan-400" />
Статистика
</h2>
<div className="text-sm text-slate-500">
Цей блок буде наповнено метриками MicroDAO (участь, транзакції, голосування),
коли буде готова токеноміка та governance-шар.
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-slate-100">{microdao.agents.length}</div>
<div className="text-xs text-slate-500">Агентів</div>
</div>
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-slate-100">{publicCitizens.length}</div>
<div className="text-xs text-slate-500">Громадян</div>
</div>
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-slate-100">{microdao.channels.length}</div>
<div className="text-xs text-slate-500">Каналів</div>
</div>
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-slate-100">{childMicrodaos.length}</div>
<div className="text-xs text-slate-500">Дочірніх DAO</div>
</div>
</div>
</section>
</div>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { useState } from "react";
import { useMicrodaoList } from "@/hooks/useMicrodao";
import { DISTRICTS, DISTRICT_COLORS } from "@/lib/microdao";
import Link from "next/link";
import { Building2, Users, MessageSquare, Search, MapPin, Crown, Globe, Lock, Layers } from "lucide-react";
export default function MicrodaoListPage() {
const [district, setDistrict] = useState<string | undefined>();
@@ -15,36 +16,48 @@ export default function MicrodaoListPage() {
<div className="max-w-6xl mx-auto px-4 py-8 space-y-8">
{/* Header */}
<header className="space-y-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-violet-500/30 to-purple-500/30 flex items-center justify-center">
<Building2 className="w-7 h-7 text-violet-400" />
</div>
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-cyan-400 to-violet-400 bg-clip-text text-transparent">
MicroDAO
</h1>
<p className="text-sm text-slate-400 mt-1">
Кластери агентів і організацій у DAARION.city
Організації та кластери агентів у DAARION City
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<input
type="text"
placeholder="Пошук за назвою..."
value={q}
onChange={(e) => setQ(e.target.value)}
className="bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2 text-sm text-slate-200 placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50"
/>
<select
value={district ?? ""}
onChange={(e) => setDistrict(e.target.value || undefined)}
className="bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2 text-sm text-slate-200 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50"
>
<option value="">Всі дистрикти</option>
{DISTRICTS.map((d) => (
<option key={d} value={d}>
{d}
</option>
))}
</select>
</div>
{/* Filters */}
<div className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
type="text"
placeholder="Пошук за назвою..."
value={q}
onChange={(e) => setQ(e.target.value)}
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg pl-10 pr-4 py-2 text-sm text-slate-200 placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50"
/>
</div>
<div className="relative">
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<select
value={district ?? ""}
onChange={(e) => setDistrict(e.target.value || undefined)}
className="bg-slate-800/50 border border-slate-700 rounded-lg pl-10 pr-8 py-2 text-sm text-slate-200 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 appearance-none"
>
<option value="">Всі дистрикти</option>
{DISTRICTS.map((d) => (
<option key={d} value={d}>
{d}
</option>
))}
</select>
</div>
</div>
</div>
</header>
@@ -76,18 +89,28 @@ export default function MicrodaoListPage() {
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-cyan-500/5 to-violet-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative space-y-3">
{/* Title + District */}
{/* Title + Badges */}
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<h2 className="font-semibold text-slate-100 group-hover:text-cyan-400 transition-colors">
{m.name}
</h2>
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<h2 className="font-semibold text-slate-100 group-hover:text-cyan-400 transition-colors">
{m.name}
</h2>
{/* Platform Badge */}
{m.is_platform && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-medium bg-amber-500/20 text-amber-400 border border-amber-500/30">
Platform
</span>
)}
</div>
{m.description && (
<p className="text-xs text-slate-400 line-clamp-2">
{m.description}
</p>
)}
</div>
{/* District Badge */}
{m.district && (
<span
className={`shrink-0 text-[10px] px-2 py-1 rounded-full border font-medium ${
@@ -99,28 +122,50 @@ export default function MicrodaoListPage() {
)}
</div>
{/* Orchestrator */}
{m.orchestrator_agent_name && (
<div className="flex items-center gap-2 text-xs text-slate-400">
<Crown className="w-3 h-3 text-amber-400" />
<span>Orchestrator: {m.orchestrator_agent_name}</span>
</div>
)}
{/* Stats */}
<div className="flex items-center gap-4 text-xs text-slate-500">
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span>{m.agents_count} агентів</span>
<Users className="w-3.5 h-3.5" />
<span>{m.member_count || m.agents_count} агентів</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<MessageSquare className="w-3.5 h-3.5" />
<span>{m.channels_count} каналів</span>
</div>
</div>
{/* Status indicator */}
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${m.is_active ? "bg-emerald-500" : "bg-amber-500"}`} />
<span className="text-[10px] text-slate-500 uppercase tracking-wide">
{m.is_active ? "Active" : "Inactive"}
</span>
{/* Footer */}
<div className="flex items-center justify-between pt-2 border-t border-slate-700/50">
<div className="flex items-center gap-2">
{/* Status */}
<div className={`w-2 h-2 rounded-full ${m.is_active ? "bg-emerald-500" : "bg-amber-500"}`} />
<span className="text-[10px] text-slate-500 uppercase tracking-wide">
{m.is_active ? "Active" : "Inactive"}
</span>
</div>
{/* Public/Private */}
<div className="flex items-center gap-1 text-[10px] text-slate-500">
{m.is_public ? (
<>
<Globe className="w-3 h-3" />
<span>Public</span>
</>
) : (
<>
<Lock className="w-3 h-3" />
<span>Private</span>
</>
)}
</div>
</div>
</div>
</Link>
@@ -131,4 +176,3 @@ export default function MicrodaoListPage() {
</div>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import type { MicrodaoSummary, MicrodaoDetail } from '@/lib/microdao';
import type { MicrodaoSummary, MicrodaoDetail } from '@/lib/types/microdao';
interface UseMicrodaoListOptions {
district?: string;

View File

@@ -1,14 +1,44 @@
import { HomeNode } from './citizens';
/**
* Unified Agent Types for DAARION MVP
* Aligned with backend models from TASK 028
*/
export type VisibilityScope = 'city' | 'microdao' | 'owner_only';
// =============================================================================
// Core Types
// =============================================================================
export type VisibilityScope = 'global' | 'microdao' | 'private';
export type AgentStatus = 'online' | 'offline' | 'unknown';
// =============================================================================
// Home Node
// =============================================================================
export interface HomeNode {
id: string;
name?: string | null;
hostname?: string | null;
roles: string[];
environment?: string | null;
}
// =============================================================================
// MicroDAO Badge (for agent's microDAO list)
// =============================================================================
export interface MicrodaoBadge {
id: string;
name: string;
slug?: string | null;
role?: string | null;
is_public: boolean;
is_platform: boolean;
}
// =============================================================================
// Agent MicroDAO Membership (detailed)
// =============================================================================
export interface AgentMicrodaoMembership {
microdao_id: string;
microdao_slug: string;
@@ -17,26 +47,31 @@ export interface AgentMicrodaoMembership {
is_core: boolean;
}
// =============================================================================
// Agent Summary (unified for Agent Console & internal use)
// =============================================================================
export interface AgentSummary {
id: string;
slug?: string | null;
slug: string;
display_name: string;
title?: string | null;
tagline?: string | null;
kind: string;
avatar_url?: string | null;
status: string;
status: AgentStatus;
// Node info
node_id?: string | null;
node_id: string;
node_label?: string | null;
home_node?: HomeNode | null;
// Visibility
// Visibility & roles
visibility_scope: VisibilityScope;
is_listed_in_directory: boolean;
is_system: boolean;
is_public: boolean;
is_orchestrator: boolean;
// MicroDAO
primary_microdao_id?: string | null;
@@ -50,59 +85,97 @@ export interface AgentSummary {
public_skills: string[];
}
export interface AgentListResponse {
items: AgentSummary[];
total: number;
// =============================================================================
// Agent Dashboard (full profile for Agent Console)
// =============================================================================
export interface SystemPrompts {
core?: {
content: string;
version?: number;
created_at?: string;
note?: string;
} | null;
safety?: {
content: string;
version?: number;
created_at?: string;
note?: string;
} | null;
governance?: {
content: string;
version?: number;
created_at?: string;
note?: string;
} | null;
tools?: {
content: string;
version?: number;
created_at?: string;
note?: string;
} | null;
}
export interface AgentDashboard {
id: string;
slug?: string | null;
display_name: string;
kind: string;
avatar_url?: string | null;
status: string;
// Visibility
visibility_scope: VisibilityScope;
is_listed_in_directory: boolean;
is_system: boolean;
is_public: boolean;
// Profile
public_slug?: string | null;
public_title?: string | null;
public_tagline?: string | null;
public_skills: string[];
district?: string | null;
// Node
node_id?: string | null;
node_label?: string | null;
home_node?: HomeNode | null;
// MicroDAO
primary_microdao_id?: string | null;
primary_microdao_name?: string | null;
primary_microdao_slug?: string | null;
microdaos: MicrodaoBadge[];
microdao_memberships: AgentMicrodaoMembership[];
export interface ModelBindings {
primary_model?: string | null;
supported_kinds?: string[];
}
export interface UsageStats {
tokens_total_24h?: number;
calls_total_24h?: number;
}
export interface AgentDashboard extends AgentSummary {
// System prompts
system_prompts?: {
core?: string;
safety?: string;
governance?: string;
tools?: string;
};
system_prompts?: SystemPrompts;
// Capabilities
// Capabilities & model
capabilities: string[];
model?: string | null;
role?: string | null;
// Future: model bindings and usage stats
model_bindings?: ModelBindings | null;
usage_stats?: UsageStats | null;
}
// =============================================================================
// API Response Types
// =============================================================================
export interface AgentListResponse {
items: AgentSummary[];
total: number;
}
export interface AgentVisibilityPayload {
visibility_scope: VisibilityScope;
is_listed_in_directory: boolean;
}
// =============================================================================
// Helpers
// =============================================================================
/**
* Get node badge label (НОДА1 / НОДА2)
*/
export function getNodeBadgeLabel(nodeId?: string | null): string {
if (!nodeId) return 'Невідома нода';
if (nodeId.includes('node-1') || nodeId.includes('hetzner')) return 'НОДА1';
if (nodeId.includes('node-2') || nodeId.includes('macbook')) return 'НОДА2';
return nodeId;
}
/**
* Get visibility scope label
*/
export function getVisibilityScopeLabel(scope: VisibilityScope): string {
switch (scope) {
case 'global': return 'Публічний';
case 'microdao': return 'Тільки MicroDAO';
case 'private': return 'Приватний';
default: return scope;
}
}

View File

@@ -1,10 +1,16 @@
export interface HomeNode {
id?: string | null;
name?: string | null;
hostname?: string | null;
roles: string[];
environment?: string | null;
}
/**
* Public Citizens Types for DAARION MVP
* Citizens are public-facing agents (is_public = true)
*/
import { HomeNode, AgentStatus } from './agents';
// Re-export HomeNode for backward compatibility
export { HomeNode };
// =============================================================================
// Public Citizen Summary (for /citizens list)
// =============================================================================
export interface PublicCitizenSummary {
slug: string;
@@ -16,11 +22,22 @@ export interface PublicCitizenSummary {
district?: string | null;
primary_room_slug?: string | null;
public_skills: string[];
online_status?: "online" | "offline" | "unknown" | string;
status?: string | null;
online_status?: AgentStatus;
status?: string | null; // backward compatibility
home_node?: HomeNode | null;
// MicroDAO info (primary only for public display)
microdao?: {
slug: string;
name: string;
district?: string | null;
} | null;
}
// =============================================================================
// City Presence
// =============================================================================
export interface CityPresenceRoom {
room_id?: string | null;
slug?: string | null;
@@ -32,6 +49,10 @@ export interface CityPresence {
rooms: CityPresenceRoom[];
}
// =============================================================================
// Public Citizen Profile (for /citizens/[slug])
// =============================================================================
export interface PublicCitizenProfile {
slug: string;
display_name: string;
@@ -43,19 +64,33 @@ export interface PublicCitizenProfile {
status?: string | null;
node_id?: string | null;
public_skills: string[];
// City presence
city_presence?: CityPresence;
// Public data blocks
dais_public: Record<string, unknown>;
interaction: Record<string, unknown>;
metrics_public: Record<string, unknown>;
// Admin link (only for architects/admins)
admin_panel_url?: string | null;
// MicroDAO info
microdao?: {
slug: string;
name: string;
district?: string | null;
} | null;
// Home node (minimal for public display)
home_node?: HomeNode | null;
}
// =============================================================================
// Citizen Interaction
// =============================================================================
export interface CitizenInteractionInfo {
slug: string;
display_name: string;
@@ -73,5 +108,3 @@ export interface CitizenAskResponse {
agent_display_name: string;
agent_id: string;
}

View File

@@ -0,0 +1,16 @@
/**
* DAARION MVP Types - Central Export
*/
// Agent types
export * from './agents';
// Citizen types (public layer)
export * from './citizens';
// MicroDAO types
export * from './microdao';
// Node types
export * from './nodes';

View File

@@ -0,0 +1,128 @@
/**
* MicroDAO Types for DAARION MVP
* Aligned with backend models from TASK 028
*/
// =============================================================================
// MicroDAO Summary (for /microdao list)
// =============================================================================
export interface MicrodaoSummary {
id: string;
slug: string;
name: string;
description?: string | null;
district?: string | null;
// Visibility & type
is_public: boolean;
is_platform: boolean;
is_active: boolean;
// Orchestrator
orchestrator_agent_id?: string | null;
orchestrator_agent_name?: string | null;
// Hierarchy
parent_microdao_id?: string | null;
parent_microdao_slug?: string | null;
// Stats
logo_url?: string | null;
member_count: number;
agents_count: number; // backward compatibility
room_count: number;
rooms_count: number; // backward compatibility
channels_count: number;
}
// =============================================================================
// MicroDAO Agent View (agent within MicroDAO)
// =============================================================================
export interface MicrodaoAgentView {
agent_id: string;
display_name: string;
role?: string | null;
is_core: boolean;
}
// =============================================================================
// MicroDAO Channel View
// =============================================================================
export interface MicrodaoChannelView {
kind: string; // 'matrix' | 'telegram' | 'city_room' | 'crew'
ref_id: string;
display_name?: string | null;
is_primary: boolean;
}
// =============================================================================
// MicroDAO Citizen View (public citizen within MicroDAO)
// =============================================================================
export interface MicrodaoCitizenView {
slug: string;
display_name: string;
public_title?: string | null;
public_tagline?: string | null;
avatar_url?: string | null;
district?: string | null;
primary_room_slug?: string | null;
}
// =============================================================================
// MicroDAO Detail (for /microdao/[slug])
// =============================================================================
export interface MicrodaoDetail {
id: string;
slug: string;
name: string;
description?: string | null;
district?: string | null;
// Visibility & type
is_public: boolean;
is_platform: boolean;
is_active: boolean;
// Orchestrator
orchestrator_agent_id?: string | null;
orchestrator_display_name?: string | null;
// Hierarchy
parent_microdao_id?: string | null;
parent_microdao_slug?: string | null;
child_microdaos: MicrodaoSummary[];
// Content
logo_url?: string | null;
agents: MicrodaoAgentView[];
channels: MicrodaoChannelView[];
public_citizens: MicrodaoCitizenView[];
}
// =============================================================================
// MicroDAO Option (for selectors)
// =============================================================================
export interface MicrodaoOption {
id: string;
slug: string;
name: string;
}
// =============================================================================
// Agent MicroDAO Membership (for Agent Dashboard)
// =============================================================================
export interface AgentMicrodaoMembership {
microdao_id: string;
microdao_slug: string;
microdao_name: string;
role?: string;
is_core: boolean;
}

View File

@@ -0,0 +1,40 @@
-- Delete Mock Citizens Script
-- This script marks mock/test citizens (agents) as deleted
-- Run with caution in production!
-- Preview: Show agents that would be affected
-- (agents without node_id or without primary_microdao_id, and not already marked as test)
SELECT id, display_name, kind, node_id, primary_microdao_id, is_public
FROM agents
WHERE (
node_id IS NULL
OR primary_microdao_id IS NULL
)
AND COALESCE(is_test, false) = false
AND COALESCE(is_archived, false) = false
AND deleted_at IS NULL;
-- Uncomment to execute soft delete:
-- UPDATE agents
-- SET
-- is_test = true,
-- deleted_at = NOW()
-- WHERE (
-- node_id IS NULL
-- OR primary_microdao_id IS NULL
-- )
-- AND COALESCE(is_test, false) = false
-- AND COALESCE(is_archived, false) = false
-- AND deleted_at IS NULL;
-- Verify: Count remaining active agents
SELECT
COUNT(*) as total_active_agents,
COUNT(*) FILTER (WHERE is_public = true) as public_citizens,
COUNT(*) FILTER (WHERE node_id IS NOT NULL) as agents_with_node,
COUNT(*) FILTER (WHERE primary_microdao_id IS NOT NULL) as agents_with_microdao
FROM agents
WHERE COALESCE(is_test, false) = false
AND COALESCE(is_archived, false) = false
AND deleted_at IS NULL;

View File

@@ -0,0 +1,47 @@
-- Delete Mock MicroDAO Script
-- This script marks mock/test microDAOs as deleted
-- Run with caution in production!
-- Preview: Show microDAOs that would be affected
-- (microDAOs with 0 agents or without orchestrator)
SELECT
m.id,
m.slug,
m.name,
m.orchestrator_agent_id,
COUNT(ma.agent_id) as agent_count
FROM microdaos m
LEFT JOIN microdao_agents ma ON ma.microdao_id = m.id
WHERE COALESCE(m.is_test, false) = false
AND COALESCE(m.is_archived, false) = false
AND m.deleted_at IS NULL
GROUP BY m.id
HAVING COUNT(ma.agent_id) = 0 OR m.orchestrator_agent_id IS NULL;
-- Uncomment to execute soft delete:
-- UPDATE microdaos
-- SET
-- is_test = true,
-- deleted_at = NOW()
-- WHERE id IN (
-- SELECT m.id
-- FROM microdaos m
-- LEFT JOIN microdao_agents ma ON ma.microdao_id = m.id
-- WHERE COALESCE(m.is_test, false) = false
-- AND COALESCE(m.is_archived, false) = false
-- AND m.deleted_at IS NULL
-- GROUP BY m.id
-- HAVING COUNT(ma.agent_id) = 0 OR m.orchestrator_agent_id IS NULL
-- );
-- Verify: Count remaining active microDAOs
SELECT
COUNT(*) as total_active_microdaos,
COUNT(*) FILTER (WHERE is_public = true) as public_microdaos,
COUNT(*) FILTER (WHERE is_platform = true) as platforms,
COUNT(*) FILTER (WHERE orchestrator_agent_id IS NOT NULL) as with_orchestrator
FROM microdaos
WHERE COALESCE(is_test, false) = false
AND COALESCE(is_archived, false) = false
AND deleted_at IS NULL;