feat: TASK 037A/B - MicroDAO Multi-Room Cleanup & UI Polish

TASK 037A: Backend Consistency
- Added db/sql/037_microdao_agent_audit.sql
- Added services/city-service/tools/fix_microdao_agent_consistency.py
- Updated repo_city.get_public_citizens with stricter filtering (node_id, microdao_membership)
- Updated PublicCitizenSummary model to include home_microdao and primary_city_room
- Updated NodeProfile model and get_node_by_id to include microdaos list

TASK 037B: UI Polish
- Updated MicrodaoRoomsSection with role-based colors/icons and mini-map
- Updated /microdao/[slug] with new Hero Block (badges, stats, orchestrator)
- Updated /citizens/[slug] with MicroDAO cross-link in DAIS profile
- Updated /nodes/[nodeId] with MicroDAO Presence section
This commit is contained in:
Apple
2025-11-29 01:35:54 -08:00
parent 86f5b58de5
commit 3ccc0e2d43
11 changed files with 862 additions and 360 deletions

View File

@@ -268,7 +268,18 @@ export default function CitizenProfilePage() {
{/* 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="flex items-center justify-between">
<h2 className="text-white font-semibold">DAIS Public Passport</h2>
{citizen.microdao && (
<Link
href={`/microdao/${citizen.microdao.slug}`}
className="inline-flex items-center gap-1.5 text-xs text-cyan-300 hover:text-cyan-200 transition-colors"
>
<Building2 className="w-3.5 h-3.5" />
{citizen.microdao.name}
</Link>
)}
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-xs uppercase text-white/40">Identity</p>

View File

@@ -54,9 +54,12 @@ export default function MicrodaoDetailPage() {
const publicCitizens = microdao.public_citizens ?? [];
const childMicrodaos = microdao.child_microdaos ?? [];
// Use fetched rooms if available, otherwise fallback to microdao.rooms
const displayRooms = rooms.length > 0 ? rooms : (microdao.rooms || []);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
<div className="max-w-4xl mx-auto px-4 py-8 space-y-8">
<div className="max-w-5xl mx-auto px-4 py-8 space-y-8">
{/* Back link */}
<Link
href="/microdao"
@@ -66,102 +69,96 @@ export default function MicrodaoDetailPage() {
Всі 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-3">
<div className="flex items-center gap-3">
{microdao.logo_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={microdao.logo_url}
alt={microdao.name}
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>
<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>
{/* Hero Section (TASK 037B) */}
<section className="rounded-3xl border border-white/10 bg-gradient-to-br from-sky-950/50 via-slate-900 to-black p-6 md:p-8 space-y-6 relative overflow-hidden shadow-2xl shadow-sky-900/10">
{/* Background pattern */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] opacity-20" />
<div className="relative z-10 flex flex-col gap-6 md:flex-row md:items-start md:justify-between">
<div className="space-y-5 max-w-3xl">
{/* Badges */}
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap items-center gap-2">
<span className={`px-2.5 py-0.5 rounded-full text-[11px] uppercase tracking-wider font-semibold border ${
microdao.is_platform
? "border-amber-500/40 text-amber-300 bg-amber-500/10"
: "border-cyan-400/40 text-cyan-300 bg-cyan-500/10"
}`}>
{microdao.is_platform ? "Platform District" : "MicroDAO"}
</span>
{microdao.district && (
<span
className={`text-xs px-3 py-1 rounded-full border font-medium ${
DISTRICT_COLORS[microdao.district] ||
"bg-slate-500/10 text-slate-400 border-slate-500/30"
}`}
>
<span className="px-2.5 py-0.5 rounded-full text-[11px] uppercase tracking-wider font-medium border border-white/10 text-white/60 bg-white/5">
{microdao.district}
</span>
)}
<span
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.parent_microdao_slug && (
<span className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] border border-white/10 text-white/50 bg-white/5">
<Layers className="w-3 h-3" />
<span className="opacity-60">part of</span>
<Link href={`/microdao/${microdao.parent_microdao_slug}`} className="hover:text-white transition-colors font-medium">
{microdao.parent_microdao_slug}
</Link>
</span>
)}
<span className={`px-2.5 py-0.5 rounded-full text-[11px] font-medium border flex items-center gap-1.5 ${
microdao.is_active
? "border-emerald-500/30 text-emerald-400 bg-emerald-500/5"
: "border-slate-600 text-slate-400 bg-slate-800"
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${microdao.is_active ? 'bg-emerald-400' : 'bg-slate-500'}`} />
{microdao.is_active ? "Active" : "Inactive"}
</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>
{/* Title & Description */}
<div className="space-y-3">
<h1 className="text-3xl md:text-5xl font-bold text-white tracking-tight leading-tight">
{microdao.name}
</h1>
{microdao.description && (
<p className="text-base md:text-lg text-slate-300 leading-relaxed max-w-2xl">
{microdao.description}
</p>
)}
</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"
{/* Key Stats & Orchestrator */}
<div className="flex flex-wrap gap-3 pt-2">
{orchestrator && (
<Link
href={`/agents/${orchestrator.agent_id}`}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-amber-500/10 border border-amber-500/20 hover:bg-amber-500/20 transition-colors group"
>
{microdao.parent_microdao_slug}
<Crown className="w-4 h-4 text-amber-400 group-hover:scale-110 transition-transform" />
<span className="text-sm text-amber-200 font-medium">Orchestrator: {orchestrator.display_name}</span>
</Link>
)}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10">
<Users className="w-4 h-4 text-white/60" />
<span className="text-sm text-white/80">{publicCitizens.length} citizens</span>
</div>
)}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10">
<MessageCircle className="w-4 h-4 text-white/60" />
<span className="text-sm text-white/80">{displayRooms.length} rooms</span>
</div>
</div>
</div>
{/* Orchestrator */}
{orchestrator && (
<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-slate-100 hover:text-cyan-400 transition-colors"
>
{orchestrator.display_name}
</Link>
</div>
)}
{/* Logo */}
<div className="hidden md:flex flex-col gap-3">
<div className="w-24 h-24 rounded-2xl bg-slate-800 border border-white/10 flex items-center justify-center overflow-hidden shadow-xl">
{microdao.logo_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={microdao.logo_url} alt={microdao.name} className="w-full h-full object-cover" />
) : (
<Building2 className="w-10 h-10 text-slate-600" />
)}
</div>
</div>
</div>
</header>
</section>
{/* Child MicroDAOs */}
{childMicrodaos.length > 0 && (
@@ -170,19 +167,19 @@ export default function MicrodaoDetailPage() {
<Layers className="w-5 h-5 text-cyan-400" />
Дочірні MicroDAO
</h2>
<div className="grid gap-3 sm:grid-cols-2">
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3">
{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"
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 group"
>
<div>
<p className="text-sm font-medium text-slate-200">{child.name}</p>
<p className="text-sm font-medium text-slate-200 group-hover:text-cyan-300 transition-colors">{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">
<span className="text-[10px] px-2 py-1 rounded bg-amber-500/10 text-amber-400 border border-amber-500/20">
Platform
</span>
)}
@@ -192,200 +189,11 @@ export default function MicrodaoDetailPage() {
</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">
<Bot className="w-5 h-5 text-cyan-400" />
Агентська команда
<span className="text-sm font-normal text-slate-500">({microdao.agents.length})</span>
</h2>
{microdao.agents.length === 0 ? (
<div className="text-sm text-slate-500">Агенти ще не привʼязані.</div>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{microdao.agents.map((a) => (
<div
key={a.agent_id}
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 flex items-center justify-between"
>
<div className="space-y-0.5">
<Link
href={`/agents/${a.agent_id}`}
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors"
>
{a.display_name}
</Link>
{a.role && (
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
)}
</div>
<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">
<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 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-slate-200 font-medium">{citizen.display_name}</p>
{citizen.public_title && (
<p className="text-sm text-slate-400">{citizen.public_title}</p>
)}
</div>
{citizen.district && (
<span className="text-xs text-slate-500">{citizen.district}</span>
)}
</Link>
))}
</div>
</section>
)}
{/* 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">
<MessageSquare className="w-5 h-5 text-cyan-400" />
Канали та кімнати
</h2>
{telegramChannels.length === 0 &&
matrixChannels.length === 0 &&
cityRooms.length === 0 &&
crewChannels.length === 0 ? (
<div className="text-sm text-slate-500">Канали ще не налаштовані.</div>
) : (
<div className="space-y-4">
{/* Telegram */}
{telegramChannels.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">Telegram</div>
<div className="flex flex-wrap gap-2">
{telegramChannels.map((c) => (
<a
key={c.ref_id}
href={`https://t.me/${c.ref_id.replace("@", "")}`}
target="_blank"
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"
>
{c.display_name || c.ref_id}
</a>
))}
</div>
</div>
)}
{/* Matrix */}
{matrixChannels.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">Matrix</div>
<div className="flex flex-wrap gap-2">
{matrixChannels.map((c) => (
<span
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"
>
{c.display_name || c.ref_id}
</span>
))}
</div>
</div>
)}
{/* City Rooms */}
{cityRooms.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">Міські кімнати</div>
<div className="flex flex-wrap gap-2">
{cityRooms.map((c) => (
<Link
key={c.ref_id}
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"
>
{c.display_name || c.ref_id}
</Link>
))}
</div>
</div>
)}
{/* CrewAI */}
{crewChannels.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">CrewAI сценарії</div>
<div className="flex flex-wrap gap-2">
{crewChannels.map((c) => (
<span
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"
>
{c.display_name || c.ref_id}
</span>
))}
</div>
</div>
)}
</div>
)}
</section>
{/* 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">
<BarChart3 className="w-5 h-5 text-cyan-400" />
Статистика
</h2>
<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>
{/* Orchestrator Room Management Panel */}
{orchestrator && (
<MicrodaoRoomsAdminPanel
microdaoSlug={slug}
rooms={rooms.length > 0 ? rooms : (microdao.rooms || [])}
rooms={displayRooms}
canManage={true} // TODO: check if current user is orchestrator
onRoomUpdated={handleRoomUpdated}
/>
@@ -393,22 +201,183 @@ export default function MicrodaoDetailPage() {
{/* Multi-Room Section with Chats */}
<MicrodaoRoomsSection
rooms={rooms.length > 0 ? rooms : (microdao.rooms || [])}
rooms={displayRooms}
primaryRoomSlug={microdao.primary_city_room?.slug}
/>
<div className="grid md:grid-cols-2 gap-8">
{/* Agents */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4 h-full">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Bot className="w-5 h-5 text-cyan-400" />
Агентська команда
<span className="text-sm font-normal text-slate-500">({microdao.agents.length})</span>
</h2>
{microdao.agents.length === 0 ? (
<div className="text-sm text-slate-500">Агенти ще не привʼязані.</div>
) : (
<div className="space-y-2">
{microdao.agents.map((a) => (
<div
key={a.agent_id}
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 flex items-center justify-between hover:border-slate-600/50 transition-colors"
>
<div className="space-y-0.5">
<Link
href={`/agents/${a.agent_id}`}
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors flex items-center gap-2"
>
{a.display_name}
{a.agent_id === microdao.orchestrator_agent_id && (
<Crown className="w-3 h-3 text-amber-400" />
)}
</Link>
{a.role && (
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
)}
</div>
{a.is_core && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
Core
</span>
)}
</div>
))}
</div>
)}
</section>
{/* Public Citizens */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4 h-full">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Users className="w-5 h-5 text-cyan-400" />
Громадяни
<span className="text-sm font-normal text-slate-500">({publicCitizens.length})</span>
</h2>
{publicCitizens.length === 0 ? (
<div className="text-sm text-slate-500">Немає публічних громадян.</div>
) : (
<div className="space-y-2">
{publicCitizens.map((citizen) => (
<Link
key={citizen.slug}
href={`/citizens/${citizen.slug}`}
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-slate-200 font-medium text-sm">{citizen.display_name}</p>
{citizen.public_title && (
<p className="text-xs text-slate-400">{citizen.public_title}</p>
)}
</div>
<div className="flex items-center gap-2">
{citizen.district && (
<span className="text-[10px] text-slate-500 border border-slate-700 px-1.5 py-0.5 rounded">{citizen.district}</span>
)}
</div>
</Link>
))}
</div>
)}
</section>
</div>
{/* Channels & Stats Row */}
<div className="grid md:grid-cols-3 gap-8">
<section className="md:col-span-2 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">
<MessageSquare className="w-5 h-5 text-cyan-400" />
Канали звʼязку
</h2>
{telegramChannels.length === 0 &&
matrixChannels.length === 0 &&
cityRooms.length === 0 &&
crewChannels.length === 0 ? (
<div className="text-sm text-slate-500">Канали ще не налаштовані.</div>
) : (
<div className="flex flex-wrap gap-3">
{telegramChannels.map((c) => (
<a
key={c.ref_id}
href={`https://t.me/${c.ref_id.replace("@", "")}`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 px-3 py-1.5 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-400 hover:bg-blue-500/20 transition-colors"
>
{c.display_name || c.ref_id}
</a>
))}
{matrixChannels.map((c) => (
<span
key={c.ref_id}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-green-500/10 border border-green-500/30 rounded-lg text-sm text-green-400"
>
{c.display_name || c.ref_id}
</span>
))}
{cityRooms.map((c) => (
<Link
key={c.ref_id}
href={`/city/${c.ref_id}`}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-violet-500/10 border border-violet-500/30 rounded-lg text-sm text-violet-400 hover:bg-violet-500/20 transition-colors"
>
{c.display_name || c.ref_id}
</Link>
))}
{crewChannels.map((c) => (
<span
key={c.ref_id}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-orange-500/10 border border-orange-500/30 rounded-lg text-sm text-orange-400"
>
{c.display_name || c.ref_id}
</span>
))}
</div>
)}
</section>
<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">
<BarChart3 className="w-5 h-5 text-cyan-400" />
Інфо
</h2>
<div className="space-y-3">
<div className="flex justify-between items-center text-sm border-b border-slate-700/50 pb-2">
<span className="text-slate-400">Агентів</span>
<span className="text-slate-200 font-medium">{microdao.agents.length}</span>
</div>
<div className="flex justify-between items-center text-sm border-b border-slate-700/50 pb-2">
<span className="text-slate-400">Громадян</span>
<span className="text-slate-200 font-medium">{publicCitizens.length}</span>
</div>
<div className="flex justify-between items-center text-sm border-b border-slate-700/50 pb-2">
<span className="text-slate-400">Каналів</span>
<span className="text-slate-200 font-medium">{microdao.channels.length}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-400">Кімнат</span>
<span className="text-slate-200 font-medium">{displayRooms.length}</span>
</div>
</div>
</section>
</div>
{/* Visibility Settings (only for orchestrator) */}
{orchestrator && (
<MicrodaoVisibilityCard
microdaoId={microdao.id}
isPublic={microdao.is_public}
isPlatform={microdao.is_platform}
isOrchestrator={true} // TODO: check if current user is orchestrator
onUpdated={() => {
// Refresh the page data
window.location.reload();
}}
/>
<div className="pt-8 border-t border-white/5">
<MicrodaoVisibilityCard
microdaoId={microdao.id}
isPublic={microdao.is_public}
isPlatform={microdao.is_platform}
isOrchestrator={true} // TODO: check if current user is orchestrator
onUpdated={() => {
window.location.reload();
}}
/>
</div>
)}
</div>
</div>

View File

@@ -128,6 +128,32 @@ export default function NodeCabinetPage() {
guardian={nodeProfile?.guardian_agent}
steward={nodeProfile?.steward_agent}
/>
{/* MicroDAO Presence */}
{nodeProfile?.microdaos && nodeProfile.microdaos.length > 0 && (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
<Activity className="w-5 h-5 text-cyan-400" />
MicroDAO Presence
</h2>
<ul className="space-y-2">
{nodeProfile.microdaos.map((dao) => (
<li key={dao.id} className="flex items-center justify-between bg-slate-900/30 rounded-lg p-3">
<Link
href={`/microdao/${dao.slug}`}
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors"
>
{dao.name}
</Link>
<span className="text-xs text-slate-500 bg-slate-800/50 px-2 py-1 rounded">
{dao.rooms_count} rooms
</span>
</li>
))}
</ul>
</div>
)}
<NodeStandardComplianceCard node={dashboard.node} />
<MatrixCard matrix={dashboard.matrix} />
<ModulesCard modules={dashboard.node.modules} />
@@ -259,6 +285,31 @@ export default function NodeCabinetPage() {
/>
</div>
{/* MicroDAO Presence */}
{nodeProfile?.microdaos && nodeProfile.microdaos.length > 0 && (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 mb-6">
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
<Activity className="w-5 h-5 text-cyan-400" />
MicroDAO Presence
</h2>
<ul className="space-y-2">
{nodeProfile.microdaos.map((dao) => (
<li key={dao.id} className="flex items-center justify-between bg-slate-900/30 rounded-lg p-3">
<Link
href={`/microdao/${dao.slug}`}
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors"
>
{dao.name}
</Link>
<span className="text-xs text-slate-500 bg-slate-800/50 px-2 py-1 rounded">
{dao.rooms_count} rooms
</span>
</li>
))}
</ul>
</div>
)}
{/* Notice for non-NODE1 */}
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 mb-6">
<p className="text-amber-400 text-sm">
@@ -280,4 +331,3 @@ export default function NodeCabinetPage() {
</div>
);
}

View File

@@ -1,41 +1,47 @@
"use client";
import Link from "next/link";
import { MessageCircle, Home, Users, FlaskConical, Shield, Vote, Hash } from "lucide-react";
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2 } from "lucide-react";
import { CityRoomSummary } from "@/lib/types/microdao";
import { CityChatWidget } from "@/components/city/CityChatWidget";
interface MicrodaoRoomsSectionProps {
rooms: CityRoomSummary[];
primaryRoomSlug?: string | null;
showAllChats?: boolean; // If true, show chat widgets for all rooms
showAllChats?: boolean;
}
const ROLE_LABELS: Record<string, string> = {
primary: "Основна кімната",
lobby: "Лобі",
team: "Командна кімната",
research: "Дослідницька лабораторія",
security: "Безпека",
governance: "Управління",
};
const ROLE_ICONS: Record<string, React.ReactNode> = {
primary: <Home className="w-4 h-4" />,
lobby: <MessageCircle className="w-4 h-4" />,
team: <Users className="w-4 h-4" />,
research: <FlaskConical className="w-4 h-4" />,
security: <Shield className="w-4 h-4" />,
governance: <Vote className="w-4 h-4" />,
};
const ROLE_COLORS: Record<string, string> = {
primary: "text-cyan-400 bg-cyan-500/10 border-cyan-500/30",
lobby: "text-green-400 bg-green-500/10 border-green-500/30",
team: "text-blue-400 bg-blue-500/10 border-blue-500/30",
research: "text-purple-400 bg-purple-500/10 border-purple-500/30",
security: "text-red-400 bg-red-500/10 border-red-500/30",
governance: "text-amber-400 bg-amber-500/10 border-amber-500/30",
const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.ReactNode }> = {
primary: {
label: "Primary / Lobby",
chipClass: "bg-emerald-500/10 text-emerald-300 border-emerald-500/30",
icon: <Home className="w-3.5 h-3.5" />,
},
lobby: {
label: "Lobby",
chipClass: "bg-sky-500/10 text-sky-300 border-sky-500/30",
icon: <MessageCircle className="w-3.5 h-3.5" />,
},
team: {
label: "Team",
chipClass: "bg-indigo-500/10 text-indigo-300 border-indigo-500/30",
icon: <Users2 className="w-3.5 h-3.5" />,
},
research: {
label: "Research",
chipClass: "bg-violet-500/10 text-violet-300 border-violet-500/30",
icon: <FlaskConical className="w-3.5 h-3.5" />,
},
security: {
label: "Security",
chipClass: "bg-rose-500/10 text-rose-300 border-rose-500/30",
icon: <Shield className="w-3.5 h-3.5" />,
},
governance: {
label: "Governance",
chipClass: "bg-amber-500/10 text-amber-300 border-amber-500/30",
icon: <Gavel className="w-3.5 h-3.5" />,
},
};
export function MicrodaoRoomsSection({
@@ -64,31 +70,68 @@ export function MicrodaoRoomsSection({
const others = rooms.filter(r => r.id !== primary.id);
// Group by role for mini-map
const byRole = rooms.reduce((acc, r) => {
const role = r.room_role || 'other';
if (!acc[role]) acc[role] = [];
acc[role].push(r);
return acc;
}, {} as Record<string, CityRoomSummary[]>);
// Get meta for primary room
const primaryMeta = primary.room_role ? ROLE_META[primary.room_role] : undefined;
return (
<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">
<MessageCircle className="w-5 h-5 text-cyan-400" />
Кімнати MicroDAO
<span className="text-sm font-normal text-slate-500">({rooms.length})</span>
</h2>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-cyan-400" />
Кімнати MicroDAO
<span className="text-sm font-normal text-slate-500">({rooms.length})</span>
</h2>
{/* Mini-map */}
<div className="flex flex-wrap gap-2">
{Object.entries(byRole).map(([role, list]) => {
const meta = ROLE_META[role];
return (
<div
key={role}
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-[11px] ${
meta ? meta.chipClass : "bg-slate-700/30 text-slate-400 border-slate-700/50"
}`}
>
{meta?.icon || <Hash className="w-3 h-3" />}
<span>{meta?.label ?? (role === 'other' ? 'Other' : role)}</span>
<span className="opacity-60">({list.length})</span>
</div>
);
})}
</div>
</div>
{/* Primary room with inline chat */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg border ${ROLE_COLORS[primary.room_role || 'primary'] || ROLE_COLORS.primary}`}>
{ROLE_ICONS[primary.room_role || 'primary'] || <Hash className="w-4 h-4" />}
<div className={`p-2 rounded-lg border ${primaryMeta ? primaryMeta.chipClass : "text-cyan-400 bg-cyan-500/10 border-cyan-500/30"}`}>
{primaryMeta?.icon || <Home className="w-4 h-4" />}
</div>
<div>
<div className="text-base font-medium text-slate-100">{primary.name}</div>
<div className="text-base font-medium text-slate-100 flex items-center gap-2">
{primary.name}
<span className="text-[10px] uppercase tracking-wider text-emerald-400 bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20">
Primary
</span>
</div>
<div className="text-xs text-slate-500">
{ROLE_LABELS[primary.room_role || 'primary'] || primary.room_role || 'Кімната'}
{primaryMeta?.label || primary.room_role || 'Main Room'}
</div>
</div>
</div>
<Link
href={`/city/${primary.slug}`}
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors"
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors px-3 py-1.5 rounded-lg hover:bg-cyan-950/30 border border-transparent hover:border-cyan-500/20"
>
Відкрити окремо
</Link>
@@ -99,45 +142,49 @@ export function MicrodaoRoomsSection({
{/* Other rooms */}
{others.length > 0 && (
<div className="space-y-3">
<div className="text-sm text-slate-400 font-medium">Інші кімнати</div>
<div className="space-y-3 pt-2">
<div className="text-sm text-slate-400 font-medium px-1">Інші кімнати</div>
<div className="grid gap-3 md:grid-cols-2">
{others.map(room => (
<div
key={room.id}
className="bg-slate-900/50 border border-slate-700/30 rounded-xl p-4 space-y-3"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<div className={`p-1.5 rounded-lg border ${ROLE_COLORS[room.room_role || ''] || 'text-slate-400 bg-slate-500/10 border-slate-500/30'}`}>
{ROLE_ICONS[room.room_role || ''] || <Hash className="w-3.5 h-3.5" />}
</div>
<div>
<div className="text-sm font-medium text-slate-200">{room.name}</div>
<div className="text-xs text-slate-500">
{ROLE_LABELS[room.room_role || ''] || room.room_role || 'Кімната'}
{others.map(room => {
const meta = room.room_role ? ROLE_META[room.room_role] : undefined;
return (
<div
key={room.id}
className="bg-slate-900/50 border border-slate-700/30 rounded-xl p-4 space-y-3 hover:border-slate-600/50 transition-colors"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded-lg border ${meta ? meta.chipClass : "text-slate-400 bg-slate-700/30 border-slate-700/50"}`}>
{meta?.icon || <Hash className="w-3.5 h-3.5" />}
</div>
<div>
<div className="text-sm font-medium text-slate-200">{room.name}</div>
{meta && (
<div className="text-[11px] text-slate-500">
{meta.label}
</div>
)}
</div>
</div>
<Link
href={`/city/${room.slug}`}
className="text-xs text-slate-400 hover:text-cyan-400 transition-colors px-2 py-1"
>
Увійти
</Link>
</div>
<Link
href={`/city/${room.slug}`}
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors"
>
Відкрити
</Link>
{showAllChats && (
<div className="mt-2">
<CityChatWidget roomSlug={room.slug} compact />
</div>
)}
</div>
{showAllChats && (
<div className="mt-2">
<CityChatWidget roomSlug={room.slug} />
</div>
)}
</div>
))}
);
})}
</div>
</div>
)}
</section>
);
}

View File

@@ -5,6 +5,13 @@ export interface NodeAgentSummary {
slug?: string;
}
export interface NodeMicrodaoSummary {
id: string;
slug: string;
name: string;
rooms_count: number;
}
export interface NodeProfile {
node_id: string;
name: string;
@@ -20,6 +27,7 @@ export interface NodeProfile {
steward_agent_id?: string | null;
guardian_agent?: NodeAgentSummary | null;
steward_agent?: NodeAgentSummary | null;
microdaos?: NodeMicrodaoSummary[];
}
export interface NodeListResponse {