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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
76
apps/web/src/components/nodes/NodeAgentsPanel.tsx
Normal file
76
apps/web/src/components/nodes/NodeAgentsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user