feat: UI alignment - Agent Console, Citizens, MicroDAO Dashboard (TASK 2)
This commit is contained in:
@@ -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'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'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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}"
|
||||
“{citizen.public_tagline}”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
“{citizen.public_tagline}”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
16
apps/web/src/lib/types/index.ts
Normal file
16
apps/web/src/lib/types/index.ts
Normal 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';
|
||||
|
||||
128
apps/web/src/lib/types/microdao.ts
Normal file
128
apps/web/src/lib/types/microdao.ts
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user