feat: TASK 031-033 - Node Guardian/Steward + Agent & MicroDAO Chat Widgets

TASK 031: Node Agents Discovery
- Documented existing Monitor agents (NODE1, NODE2)
- Created NODE_AGENTS_INVENTORY.md

TASK 032: Node Guardian/Steward Formalize
- Added migration 030_node_guardian_steward.sql
- Added is_node_guardian, is_node_steward to agents
- Added guardian_agent_id, steward_agent_id to node_cache
- Updated repo_city.py for guardian/steward in node queries
- Added NodeAgentsPanel component for Node Dashboard

TASK 033: Agent & MicroDAO Chat Widgets
- Added CityRoomSummary model
- Added primary_city_room to AgentDashboard API
- Added primary_city_room to MicrodaoDetail API
- Added get_microdao_primary_room() function
- Updated Agent Console with Matrix chat section
- Updated MicroDAO page with public chat section
- Reused existing CityChatWidget component
This commit is contained in:
Apple
2025-11-28 13:51:51 -08:00
parent 4d7c4b9744
commit 773a955ecc
13 changed files with 744 additions and 67 deletions

View File

@@ -18,7 +18,8 @@ import {
import { api, Agent, AgentInvokeResponse } from '@/lib/api';
import { VisibilityScope, getNodeBadgeLabel } from '@/lib/types/agents';
import { updateAgentVisibility, AgentVisibilityUpdate } from '@/lib/api/agents';
import { Bot, Settings, FileText, Building2, Cpu, MessageSquare, BarChart3, Users, Globe, Lock, Eye, EyeOff, ChevronLeft, Loader2 } from 'lucide-react';
import { Bot, Settings, FileText, Building2, Cpu, MessageSquare, BarChart3, Users, Globe, Lock, Eye, EyeOff, ChevronLeft, Loader2, MessageCircle } from 'lucide-react';
import { CityChatWidget } from '@/components/city/CityChatWidget';
// Tab types
type TabId = 'dashboard' | 'prompts' | 'microdao' | 'identity' | 'models' | 'chat';
@@ -461,70 +462,106 @@ export default function AgentConsolePage() {
{/* Chat Tab */}
{activeTab === 'chat' && (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden">
{/* Messages */}
<div className="h-[500px] overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-white/50 py-8">
<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 => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] p-3 rounded-xl ${
msg.role === 'user'
? 'bg-cyan-500/20 text-white'
: 'bg-white/10 text-white'
}`}
>
<p className="whitespace-pre-wrap">{msg.content}</p>
{msg.meta && (
<div className="mt-2 text-xs text-white/30 flex gap-2">
{msg.meta.latency_ms && <span>{msg.meta.latency_ms}ms</span>}
{msg.meta.tokens_out && <span>{msg.meta.tokens_out} tokens</span>}
</div>
)}
<div className="space-y-6">
{/* Direct Chat with Agent via DAGI Router */}
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden">
<div className="p-4 border-b border-white/10">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-cyan-400" />
Прямий чат з агентом
</h3>
<p className="text-sm text-white/50">Спілкування через DAGI Router</p>
</div>
{/* Messages */}
<div className="h-[400px] overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-white/50 py-8">
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>Start a conversation with {profile?.display_name || agentId}</p>
</div>
</div>
))}
{invoking && (
<div className="flex justify-start">
<div className="bg-white/10 p-3 rounded-xl">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 text-cyan-500 animate-spin" />
<span className="text-white/50">Thinking...</span>
)}
{messages.map(msg => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] p-3 rounded-xl ${
msg.role === 'user'
? 'bg-cyan-500/20 text-white'
: 'bg-white/10 text-white'
}`}
>
<p className="whitespace-pre-wrap">{msg.content}</p>
{msg.meta && (
<div className="mt-2 text-xs text-white/30 flex gap-2">
{msg.meta.latency_ms && <span>{msg.meta.latency_ms}ms</span>}
{msg.meta.tokens_out && <span>{msg.meta.tokens_out} tokens</span>}
</div>
)}
</div>
</div>
))}
{invoking && (
<div className="flex justify-start">
<div className="bg-white/10 p-3 rounded-xl">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 text-cyan-500 animate-spin" />
<span className="text-white/50">Thinking...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="border-t border-white/10 p-4">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSendMessage()}
placeholder="Type a message..."
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-white/30 focus:outline-none focus:border-cyan-500/50"
disabled={invoking}
/>
<button
onClick={handleSendMessage}
disabled={!input.trim() || invoking}
className="px-4 py-3 bg-cyan-500 hover:bg-cyan-400 disabled:bg-white/10 disabled:text-white/30 text-white rounded-xl transition-colors"
>
Send
</button>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* Input */}
<div className="border-t border-white/10 p-4">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSendMessage()}
placeholder="Type a message..."
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-white/30 focus:outline-none focus:border-cyan-500/50"
disabled={invoking}
/>
<button
onClick={handleSendMessage}
disabled={!input.trim() || invoking}
className="px-4 py-3 bg-cyan-500 hover:bg-cyan-400 disabled:bg-white/10 disabled:text-white/30 text-white rounded-xl transition-colors"
>
Send
</button>
</div>
{/* Matrix City Room Chat */}
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
<h3 className="text-lg font-semibold text-white flex items-center gap-2 mb-4">
<MessageCircle className="w-5 h-5 text-purple-400" />
Публічна кімната агента
</h3>
{dashboard?.primary_city_room ? (
<div className="space-y-3">
<p className="text-sm text-white/60">
Matrix-чат у кімнаті: <span className="text-purple-400">{dashboard.primary_city_room.name}</span>
</p>
<CityChatWidget roomSlug={dashboard.primary_city_room.slug} />
</div>
) : (
<div className="text-center py-8 text-white/50">
<MessageCircle className="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>Для цього агента ще не налаштована публічна кімната.</p>
<p className="text-sm mt-2">
Прив'яжіть агента до MicroDAO або створіть кімнату в City Service.
</p>
</div>
)}
</div>
</div>
)}

View File

@@ -5,7 +5,8 @@ import Link from "next/link";
import { useMicrodaoDetail } from "@/hooks/useMicrodao";
import { DISTRICT_COLORS } from "@/lib/microdao";
import { MicrodaoVisibilityCard } from "@/components/microdao/MicrodaoVisibilityCard";
import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot } from "lucide-react";
import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot, MessageCircle } from "lucide-react";
import { CityChatWidget } from "@/components/city/CityChatWidget";
export default function MicrodaoDetailPage() {
const params = useParams();
@@ -372,6 +373,36 @@ export default function MicrodaoDetailPage() {
</div>
</section>
{/* Public Chat Room */}
<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">
<MessageCircle className="w-5 h-5 text-purple-400" />
Публічний чат MicroDAO
</h2>
{microdao.primary_city_room ? (
<div className="space-y-3">
<p className="text-sm text-slate-400">
Matrix-чат у кімнаті: <span className="text-purple-400">{microdao.primary_city_room.name}</span>
</p>
{orchestrator && (
<p className="text-xs text-slate-500">
Оркестратор: <Link href={`/agents/${orchestrator.agent_id}`} className="text-cyan-400 hover:underline">{orchestrator.display_name}</Link>
</p>
)}
<CityChatWidget roomSlug={microdao.primary_city_room.slug} />
</div>
) : (
<div className="text-center py-8 text-slate-500">
<MessageCircle className="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>Для цього MicroDAO ще не налаштована публічна кімната.</p>
<p className="text-sm mt-2 text-slate-600">
Налаштуйте primary room у City Service, щоб увімкнути чат.
</p>
</div>
)}
</section>
{/* Visibility Settings (only for orchestrator) */}
{orchestrator && (
<MicrodaoVisibilityCard

View File

@@ -14,6 +14,7 @@ import {
ModulesCard,
NodeStandardComplianceCard
} from '@/components/node-dashboard';
import { NodeAgentsPanel } from '@/components/nodes/NodeAgentsPanel';
function getNodeLabel(nodeId: string): string {
if (nodeId.includes('node-1')) return 'НОДА1';
@@ -122,6 +123,11 @@ export default function NodeCabinetPage() {
</div>
<div className="space-y-6">
{/* Node Guardian & Steward Agents */}
<NodeAgentsPanel
guardian={nodeProfile?.guardian_agent}
steward={nodeProfile?.steward_agent}
/>
<NodeStandardComplianceCard node={dashboard.node} />
<MatrixCard matrix={dashboard.matrix} />
<ModulesCard modules={dashboard.node.modules} />
@@ -245,6 +251,14 @@ export default function NodeCabinetPage() {
)}
</div>
{/* Node Guardian & Steward Agents */}
<div className="mb-6">
<NodeAgentsPanel
guardian={nodeProfile?.guardian_agent}
steward={nodeProfile?.steward_agent}
/>
</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">

View File

@@ -0,0 +1,76 @@
"use client";
import Link from "next/link";
import { Shield, Wrench } from "lucide-react";
interface NodeAgent {
id: string;
name: string;
kind?: string;
slug?: string;
}
interface NodeAgentsPanelProps {
guardian?: NodeAgent | null;
steward?: NodeAgent | null;
}
export function NodeAgentsPanel({ guardian, steward }: NodeAgentsPanelProps) {
if (!guardian && !steward) {
return null;
}
return (
<div className="bg-slate-800/50 border border-slate-700/50 rounded-xl p-4 space-y-3">
<h3 className="text-sm font-medium text-slate-300 flex items-center gap-2">
<Shield className="w-4 h-4 text-emerald-400" />
Системні агенти ноди
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Guardian Agent */}
<div className="bg-slate-900/50 rounded-lg p-3 border border-slate-700/30">
<div className="flex items-center gap-2 mb-1">
<Shield className="w-4 h-4 text-cyan-400" />
<span className="text-xs text-slate-400 uppercase tracking-wide">Node Guardian</span>
</div>
{guardian ? (
<Link
href={`/agents/${guardian.id}`}
className="text-sm text-white hover:text-cyan-400 transition-colors font-medium"
>
{guardian.name}
</Link>
) : (
<span className="text-sm text-slate-500 italic">Не призначено</span>
)}
{guardian?.kind && (
<p className="text-xs text-slate-500 mt-1">{guardian.kind}</p>
)}
</div>
{/* Steward Agent */}
<div className="bg-slate-900/50 rounded-lg p-3 border border-slate-700/30">
<div className="flex items-center gap-2 mb-1">
<Wrench className="w-4 h-4 text-amber-400" />
<span className="text-xs text-slate-400 uppercase tracking-wide">Node Steward</span>
</div>
{steward ? (
<Link
href={`/agents/${steward.id}`}
className="text-sm text-white hover:text-amber-400 transition-colors font-medium"
>
{steward.name}
</Link>
) : (
<span className="text-sm text-slate-500 italic">Не призначено</span>
)}
{steward?.kind && (
<p className="text-xs text-slate-500 mt-1">{steward.kind}</p>
)}
</div>
</div>
</div>
);
}

View File

@@ -153,6 +153,13 @@ export interface AgentPublicProfile {
is_system?: boolean;
}
export interface CityRoomSummary {
id: string;
slug: string;
name: string;
matrix_room_id?: string | null;
}
export interface AgentDashboard {
profile: AgentProfile;
node?: AgentNode;
@@ -162,6 +169,7 @@ export interface AgentDashboard {
system_prompts?: AgentSystemPrompts;
public_profile?: AgentPublicProfile;
microdao_memberships?: AgentMicrodaoMembership[];
primary_city_room?: CityRoomSummary | null;
}
// ============================================================================

View File

@@ -72,6 +72,17 @@ export interface MicrodaoCitizenView {
primary_room_slug?: string | null;
}
// =============================================================================
// City Room Summary (for chat embedding)
// =============================================================================
export interface CityRoomSummary {
id: string;
slug: string;
name: string;
matrix_room_id?: string | null;
}
// =============================================================================
// MicroDAO Detail (for /microdao/[slug])
// =============================================================================
@@ -102,6 +113,9 @@ export interface MicrodaoDetail {
agents: MicrodaoAgentView[];
channels: MicrodaoChannelView[];
public_citizens: MicrodaoCitizenView[];
// Primary city room for chat
primary_city_room?: CityRoomSummary | null;
}
// =============================================================================

View File

@@ -1,3 +1,10 @@
export interface NodeAgentSummary {
id: string;
name: string;
kind?: string;
slug?: string;
}
export interface NodeProfile {
node_id: string;
name: string;
@@ -9,6 +16,10 @@ export interface NodeProfile {
agents_total: number;
agents_online: number;
last_heartbeat?: string | null;
guardian_agent_id?: string | null;
steward_agent_id?: string | null;
guardian_agent?: NodeAgentSummary | null;
steward_agent?: NodeAgentSummary | null;
}
export interface NodeListResponse {