feat: implement Agent Chat Widget for entity pages
TASK_PHASE_AGENT_CHAT_WIDGET_MVP.md completed:
Backend:
- Add /api/v1/agents/{agent_id}/chat-room endpoint
- Add /api/v1/nodes/{node_id}/chat-room endpoint
- Add /api/v1/microdaos/{slug}/chat-room endpoint
Frontend:
- Create AgentChatWidget.tsx floating chat component
- Integrate into /agents/:agentId page
- Integrate into /nodes/:nodeId page
- Integrate into /microdao/:slug page
Ontology rule implemented:
'No page without agents' = ability to directly talk to agents on that page
This commit is contained in:
@@ -21,6 +21,7 @@ import { updateAgentVisibility, AgentVisibilityUpdate } from '@/lib/api/agents';
|
||||
import { ensureOrchestratorRoom } from '@/lib/api/microdao';
|
||||
import { Bot, Settings, FileText, Building2, Cpu, MessageSquare, BarChart3, Users, Globe, Lock, Eye, EyeOff, ChevronLeft, Loader2, MessageCircle, PlusCircle } from 'lucide-react';
|
||||
import { CityChatWidget } from '@/components/city/CityChatWidget';
|
||||
import { AgentChatWidget } from '@/components/chat/AgentChatWidget';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// Tab types
|
||||
@@ -664,6 +665,9 @@ export default function AgentConsolePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Chat Widget */}
|
||||
<AgentChatWidget contextType="agent" contextId={agentId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MicrodaoRoomsSection } from "@/components/microdao/MicrodaoRoomsSection
|
||||
import { MicrodaoRoomsAdminPanel } from "@/components/microdao/MicrodaoRoomsAdminPanel";
|
||||
import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot, MessageCircle } from "lucide-react";
|
||||
import { CityChatWidget } from "@/components/city/CityChatWidget";
|
||||
import { AgentChatWidget } from "@/components/chat/AgentChatWidget";
|
||||
import { ensureOrchestratorRoom } from "@/lib/api/microdao";
|
||||
|
||||
export default function MicrodaoDetailPage() {
|
||||
@@ -398,6 +399,9 @@ export default function MicrodaoDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Chat Widget */}
|
||||
<AgentChatWidget contextType="microdao" contextId={slug} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
NodeStandardComplianceCard
|
||||
} from '@/components/node-dashboard';
|
||||
import { NodeGuardianCard } from '@/components/nodes/NodeGuardianCard';
|
||||
import { AgentChatWidget } from '@/components/chat/AgentChatWidget';
|
||||
|
||||
function getNodeLabel(nodeId: string): string {
|
||||
if (nodeId.includes('node-1')) return 'НОДА1';
|
||||
@@ -182,6 +183,9 @@ export default function NodeCabinetPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Chat Widget */}
|
||||
<AgentChatWidget contextType="node" contextId={nodeId} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -337,6 +341,9 @@ export default function NodeCabinetPage() {
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Floating Chat Widget */}
|
||||
<AgentChatWidget contextType="node" contextId={nodeId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
296
apps/web/src/components/chat/AgentChatWidget.tsx
Normal file
296
apps/web/src/components/chat/AgentChatWidget.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { MessageCircle, X, Bot, Server, Building2, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { CityChatWidget } from '@/components/city/CityChatWidget';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type ChatContextType = 'agent' | 'node' | 'microdao';
|
||||
|
||||
interface AgentInfo {
|
||||
id: string;
|
||||
display_name: string;
|
||||
avatar_url?: string | null;
|
||||
kind?: string;
|
||||
role?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface ChatRoomInfo {
|
||||
room_slug: string;
|
||||
room_id?: string | null;
|
||||
matrix_room_id?: string | null;
|
||||
chat_available: boolean;
|
||||
// Agent context
|
||||
agent_id?: string;
|
||||
agent_display_name?: string;
|
||||
agent_avatar_url?: string | null;
|
||||
agent_status?: string;
|
||||
agent_kind?: string;
|
||||
// Node context
|
||||
node_id?: string;
|
||||
node_name?: string;
|
||||
node_status?: string;
|
||||
agents?: AgentInfo[];
|
||||
// MicroDAO context
|
||||
microdao_id?: string;
|
||||
microdao_slug?: string;
|
||||
microdao_name?: string;
|
||||
orchestrator?: AgentInfo | null;
|
||||
}
|
||||
|
||||
interface AgentChatWidgetProps {
|
||||
contextType: ChatContextType;
|
||||
contextId: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating Agent Chat Widget
|
||||
*
|
||||
* Displays a floating chat button in the bottom-right corner.
|
||||
* When expanded, shows a chat panel connected to the appropriate Matrix room.
|
||||
*
|
||||
* Usage:
|
||||
* - <AgentChatWidget contextType="agent" contextId="daarwizz" />
|
||||
* - <AgentChatWidget contextType="node" contextId="node-1-hetzner-gex44" />
|
||||
* - <AgentChatWidget contextType="microdao" contextId="daarion" />
|
||||
*/
|
||||
export function AgentChatWidget({ contextType, contextId, className }: AgentChatWidgetProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [chatInfo, setChatInfo] = useState<ChatRoomInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch chat room info based on context
|
||||
const fetchChatInfo = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let endpoint = '';
|
||||
switch (contextType) {
|
||||
case 'agent':
|
||||
endpoint = `/api/v1/agents/${encodeURIComponent(contextId)}/chat-room`;
|
||||
break;
|
||||
case 'node':
|
||||
endpoint = `/api/v1/nodes/${encodeURIComponent(contextId)}/chat-room`;
|
||||
break;
|
||||
case 'microdao':
|
||||
endpoint = `/api/v1/microdaos/${encodeURIComponent(contextId)}/chat-room`;
|
||||
break;
|
||||
}
|
||||
|
||||
const res = await fetch(endpoint);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.detail || 'Failed to load chat info');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setChatInfo(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch chat info:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load chat');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [contextType, contextId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchChatInfo();
|
||||
}, [fetchChatInfo]);
|
||||
|
||||
// Get display info based on context
|
||||
const getDisplayInfo = () => {
|
||||
if (!chatInfo) return { name: 'Chat', icon: MessageCircle, status: 'offline' };
|
||||
|
||||
switch (contextType) {
|
||||
case 'agent':
|
||||
return {
|
||||
name: chatInfo.agent_display_name || 'Agent Chat',
|
||||
icon: Bot,
|
||||
status: chatInfo.agent_status || 'offline',
|
||||
avatarUrl: chatInfo.agent_avatar_url
|
||||
};
|
||||
case 'node':
|
||||
return {
|
||||
name: chatInfo.node_name || 'Node Support',
|
||||
icon: Server,
|
||||
status: chatInfo.node_status || 'offline',
|
||||
agents: chatInfo.agents
|
||||
};
|
||||
case 'microdao':
|
||||
return {
|
||||
name: chatInfo.microdao_name || 'MicroDAO Chat',
|
||||
icon: Building2,
|
||||
status: chatInfo.orchestrator?.status || 'offline',
|
||||
avatarUrl: chatInfo.orchestrator?.avatar_url
|
||||
};
|
||||
default:
|
||||
return { name: 'Chat', icon: MessageCircle, status: 'offline' };
|
||||
}
|
||||
};
|
||||
|
||||
const displayInfo = getDisplayInfo();
|
||||
const IconComponent = displayInfo.icon;
|
||||
const isOnline = displayInfo.status === 'online';
|
||||
|
||||
// Collapsed button
|
||||
if (!isExpanded) {
|
||||
return (
|
||||
<div className={cn("fixed bottom-6 right-6 z-50", className)}>
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className={cn(
|
||||
"group relative w-14 h-14 rounded-full shadow-lg transition-all duration-300",
|
||||
"bg-gradient-to-br from-violet-600 to-purple-700 hover:from-violet-500 hover:to-purple-600",
|
||||
"flex items-center justify-center",
|
||||
"hover:scale-110 active:scale-95"
|
||||
)}
|
||||
>
|
||||
{/* Avatar or Icon */}
|
||||
{displayInfo.avatarUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={displayInfo.avatarUrl}
|
||||
alt={displayInfo.name}
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent className="w-6 h-6 text-white" />
|
||||
)}
|
||||
|
||||
{/* Status indicator */}
|
||||
<span className={cn(
|
||||
"absolute bottom-0 right-0 w-4 h-4 rounded-full border-2 border-slate-900",
|
||||
isOnline ? "bg-emerald-500" : "bg-slate-500"
|
||||
)} />
|
||||
|
||||
{/* Tooltip */}
|
||||
<span className="absolute right-full mr-3 px-3 py-1.5 bg-slate-800 text-white text-sm rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||
Чат з {displayInfo.name}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Pulse animation for online status */}
|
||||
{isOnline && (
|
||||
<span className="absolute bottom-0 right-0 w-4 h-4 rounded-full bg-emerald-500 animate-ping opacity-75" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Expanded chat panel
|
||||
return (
|
||||
<div className={cn(
|
||||
"fixed bottom-6 right-6 z-50",
|
||||
"w-[380px] max-w-[calc(100vw-48px)]",
|
||||
"h-[500px] max-h-[calc(100vh-100px)]",
|
||||
"bg-slate-900 rounded-2xl shadow-2xl border border-white/10",
|
||||
"flex flex-col overflow-hidden",
|
||||
"animate-in slide-in-from-bottom-4 duration-300",
|
||||
className
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 bg-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Avatar/Icon */}
|
||||
<div className={cn(
|
||||
"w-10 h-10 rounded-full flex items-center justify-center overflow-hidden",
|
||||
"bg-gradient-to-br from-violet-500/30 to-purple-600/30"
|
||||
)}>
|
||||
{displayInfo.avatarUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={displayInfo.avatarUrl}
|
||||
alt={displayInfo.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent className="w-5 h-5 text-violet-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">{displayInfo.name}</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={cn(
|
||||
"w-2 h-2 rounded-full",
|
||||
isOnline ? "bg-emerald-500" : "bg-slate-500"
|
||||
)} />
|
||||
<span className={cn(
|
||||
"text-xs",
|
||||
isOnline ? "text-emerald-400" : "text-slate-500"
|
||||
)}>
|
||||
{isOnline ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsExpanded(false)}
|
||||
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-white/60" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chat content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="w-8 h-8 text-violet-400 animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||
<AlertCircle className="w-12 h-12 text-red-400 mb-4" />
|
||||
<p className="text-white/60 text-sm mb-4">{error}</p>
|
||||
<button
|
||||
onClick={fetchChatInfo}
|
||||
className="px-4 py-2 bg-violet-500/20 hover:bg-violet-500/30 text-violet-400 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
Спробувати знову
|
||||
</button>
|
||||
</div>
|
||||
) : !chatInfo?.chat_available ? (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||
<MessageCircle className="w-12 h-12 text-white/20 mb-4" />
|
||||
<p className="text-white/60 text-sm mb-2">Чат тимчасово недоступний</p>
|
||||
<p className="text-white/40 text-xs">
|
||||
Кімната чату ще не налаштована для цієї сутності.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<CityChatWidget
|
||||
roomSlug={chatInfo.room_slug}
|
||||
mode="embedded"
|
||||
showHeader={false}
|
||||
className="h-full border-0 rounded-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Node agents list (for node context) */}
|
||||
{contextType === 'node' && chatInfo?.agents && chatInfo.agents.length > 0 && (
|
||||
<div className="px-4 py-2 border-t border-white/10 bg-white/5">
|
||||
<p className="text-xs text-white/40 mb-2">Агенти підтримки:</p>
|
||||
<div className="flex gap-2">
|
||||
{chatInfo.agents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="flex items-center gap-1.5 px-2 py-1 bg-white/5 rounded-lg"
|
||||
>
|
||||
<Bot className="w-3 h-3 text-cyan-400" />
|
||||
<span className="text-xs text-white/70">{agent.display_name}</span>
|
||||
<span className="text-[10px] text-white/40">({agent.role})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
229
docs/debug/agent_chat_widget_mvp_report_20251201.md
Normal file
229
docs/debug/agent_chat_widget_mvp_report_20251201.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Agent Chat Widget MVP - Implementation Report
|
||||
|
||||
**Date**: 2025-12-01
|
||||
**Task**: `docs/tasks/TASK_PHASE_AGENT_CHAT_WIDGET_MVP.md`
|
||||
**Status**: ✅ COMPLETED
|
||||
|
||||
---
|
||||
|
||||
## 1. Summary
|
||||
|
||||
Реалізовано **floating Agent Chat Widget** на ключових сторінках MVP:
|
||||
- `/agents/:agentId` — чат з конкретним агентом
|
||||
- `/nodes/:nodeId` — чат з Node Guardian/Steward агентами
|
||||
- `/microdao/:slug` — чат з orchestrator-агентом MicroDAO
|
||||
|
||||
Правило онтології виконано:
|
||||
> "Немає сторінки без агентів" означає не лише присутність аватарів, а й можливість **напряму говорити з агентом** на цій сторінці.
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend Implementation
|
||||
|
||||
### 2.1. Нові API Endpoints
|
||||
|
||||
Додано до `services/city-service/routes_city.py`:
|
||||
|
||||
| Endpoint | Method | Опис |
|
||||
|----------|--------|------|
|
||||
| `/api/v1/agents/{agent_id}/chat-room` | GET | Інформація про кімнату чату для агента |
|
||||
| `/api/v1/nodes/{node_id}/chat-room` | GET | Інформація про кімнату чату для ноди |
|
||||
| `/api/v1/microdaos/{slug}/chat-room` | GET | Інформація про кімнату чату для MicroDAO |
|
||||
|
||||
### 2.2. Response Schema
|
||||
|
||||
**Agent Chat Room:**
|
||||
```json
|
||||
{
|
||||
"agent_id": "daarwizz",
|
||||
"agent_display_name": "DAARWIZZ",
|
||||
"agent_avatar_url": "https://...",
|
||||
"agent_status": "online",
|
||||
"agent_kind": "orchestrator",
|
||||
"room_slug": "agent-console-daarwizz",
|
||||
"room_id": "room-uuid",
|
||||
"matrix_room_id": "!abc:matrix.daarion.space",
|
||||
"chat_available": true
|
||||
}
|
||||
```
|
||||
|
||||
**Node Chat Room:**
|
||||
```json
|
||||
{
|
||||
"node_id": "node-1-hetzner-gex44",
|
||||
"node_name": "Hetzner GEX44 Production",
|
||||
"node_status": "online",
|
||||
"room_slug": "node-support-1_hetzner_gex44",
|
||||
"room_id": null,
|
||||
"matrix_room_id": null,
|
||||
"chat_available": false,
|
||||
"agents": [
|
||||
{"id": "guardian-os", "display_name": "GuardianOS", "kind": "service", "role": "guardian"},
|
||||
{"id": "pulse", "display_name": "Pulse", "kind": "service", "role": "steward"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**MicroDAO Chat Room:**
|
||||
```json
|
||||
{
|
||||
"microdao_id": "dao_daarion",
|
||||
"microdao_slug": "daarion",
|
||||
"microdao_name": "DAARION DAO",
|
||||
"room_slug": "microdao-lobby-daarion",
|
||||
"room_id": "room-uuid",
|
||||
"matrix_room_id": "!xyz:matrix.daarion.space",
|
||||
"chat_available": true,
|
||||
"orchestrator": {
|
||||
"id": "daarwizz",
|
||||
"display_name": "DAARWIZZ",
|
||||
"avatar_url": "https://...",
|
||||
"kind": "orchestrator",
|
||||
"role": "orchestrator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend Implementation
|
||||
|
||||
### 3.1. Новий Компонент
|
||||
|
||||
**File**: `apps/web/src/components/chat/AgentChatWidget.tsx`
|
||||
|
||||
**Features:**
|
||||
- Floating button у правому нижньому куті
|
||||
- Collapsed state: кругла кнопка з аватаром агента та індикатором статусу
|
||||
- Expanded state: панель чату ~500px висотою
|
||||
- Автоматичний fetch chat-room info при mount
|
||||
- Інтеграція з існуючим `CityChatWidget` для Matrix chat
|
||||
- Показ агентів підтримки для Node context
|
||||
- Graceful degradation коли кімната чату недоступна
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface AgentChatWidgetProps {
|
||||
contextType: 'agent' | 'node' | 'microdao';
|
||||
contextId: string;
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2. Інтеграція у Сторінки
|
||||
|
||||
| Сторінка | Файл | Props |
|
||||
|----------|------|-------|
|
||||
| Agent Cabinet | `apps/web/src/app/agents/[agentId]/page.tsx` | `contextType="agent" contextId={agentId}` |
|
||||
| Node Cabinet | `apps/web/src/app/nodes/[nodeId]/page.tsx` | `contextType="node" contextId={nodeId}` |
|
||||
| MicroDAO Dashboard | `apps/web/src/app/microdao/[slug]/page.tsx` | `contextType="microdao" contextId={slug}` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Room Mapping
|
||||
|
||||
Відповідно до `Rooms_Layer_Architecture_v1.md`:
|
||||
|
||||
| Context | Room Slug Pattern | Учасники |
|
||||
|---------|------------------|----------|
|
||||
| Agent | `agent-console-{agentSlug}` | Конкретний агент |
|
||||
| Node | `node-support-{nodeSlug}` | GuardianOS, Pulse, інші core-агенти |
|
||||
| MicroDAO | `microdao-lobby-{slug}` | Orchestrator агент + системні агенти DAO |
|
||||
|
||||
---
|
||||
|
||||
## 5. UX/UI Details
|
||||
|
||||
### 5.1. Collapsed State
|
||||
- Кругла кнопка 56x56px
|
||||
- Gradient background: violet-600 → purple-700
|
||||
- Avatar агента або іконка (Bot/Server/Building2)
|
||||
- Status indicator: green (online) / gray (offline)
|
||||
- Pulse animation для online агентів
|
||||
- Hover tooltip: "Чат з {name}"
|
||||
|
||||
### 5.2. Expanded State
|
||||
- Panel: 380px × 500px (responsive)
|
||||
- Header: Avatar, Name, Status
|
||||
- Chat area: Інтеграція з CityChatWidget
|
||||
- Footer (Node): Список агентів підтримки
|
||||
- Close button: X у верхньому правому куті
|
||||
|
||||
### 5.3. Error States
|
||||
- Loading: Spinner
|
||||
- Error: Alert з кнопкою "Спробувати знову"
|
||||
- Chat unavailable: Інформаційне повідомлення
|
||||
|
||||
---
|
||||
|
||||
## 6. Files Changed
|
||||
|
||||
### Backend
|
||||
- `services/city-service/routes_city.py` — додано 3 нові endpoints
|
||||
|
||||
### Frontend
|
||||
- `apps/web/src/components/chat/AgentChatWidget.tsx` — **NEW**
|
||||
- `apps/web/src/app/agents/[agentId]/page.tsx` — інтеграція
|
||||
- `apps/web/src/app/nodes/[nodeId]/page.tsx` — інтеграція
|
||||
- `apps/web/src/app/microdao/[slug]/page.tsx` — інтеграція
|
||||
|
||||
---
|
||||
|
||||
## 7. Deployment Steps
|
||||
|
||||
```bash
|
||||
# 1. Commit changes
|
||||
git add .
|
||||
git commit -m "feat: implement Agent Chat Widget for entity pages"
|
||||
|
||||
# 2. Push to GitHub
|
||||
git push origin main
|
||||
|
||||
# 3. Pull on NODE1
|
||||
ssh root@144.76.224.179 "cd /opt/microdao-daarion && git pull"
|
||||
|
||||
# 4. Rebuild services
|
||||
ssh root@144.76.224.179 "cd /opt/microdao-daarion && docker compose up -d --build daarion-city-service daarion-web"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Verification Checklist
|
||||
|
||||
- [ ] `/agents/daarwizz` — floating chat button visible
|
||||
- [ ] `/agents/daarwizz` — click opens chat panel
|
||||
- [ ] `/agents/daarwizz` — chat connects to Matrix room (if available)
|
||||
- [ ] `/nodes/node-1-hetzner-gex44` — floating chat button visible
|
||||
- [ ] `/nodes/node-1-hetzner-gex44` — shows Guardian/Steward agents
|
||||
- [ ] `/microdao/daarion` — floating chat button visible
|
||||
- [ ] `/microdao/daarion` — shows orchestrator agent info
|
||||
|
||||
---
|
||||
|
||||
## 9. Known Limitations (MVP)
|
||||
|
||||
1. **Room Creation**: Widget does NOT create rooms automatically. Rooms must exist in `city_rooms` table with Matrix integration.
|
||||
2. **Authentication**: Chat requires user to be logged in (via JWT).
|
||||
3. **Node Rooms**: Node support rooms likely don't exist yet — widget will show "Chat unavailable".
|
||||
4. **Mobile**: Panel takes ~90% of screen width on mobile — may need refinement.
|
||||
|
||||
---
|
||||
|
||||
## 10. Next Steps
|
||||
|
||||
1. **Create Missing Rooms**: Run `/city/matrix/backfill` or create rooms manually for:
|
||||
- Agent console rooms
|
||||
- Node support rooms
|
||||
- MicroDAO lobby rooms
|
||||
|
||||
2. **Auto-Create Rooms**: Consider adding room auto-creation in `/chat-room` endpoints.
|
||||
|
||||
3. **Direct Messages**: Add support for direct 1:1 chat with agents (not room-based).
|
||||
|
||||
4. **Voice/Video**: Future extension for audio/video calls with agents.
|
||||
|
||||
---
|
||||
|
||||
**Report generated**: 2025-12-01
|
||||
**Author**: Cursor AI
|
||||
|
||||
@@ -1255,6 +1255,158 @@ async def chat_bootstrap(
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Chat Room API (TASK_PHASE_AGENT_CHAT_WIDGET_MVP)
|
||||
# =============================================================================
|
||||
|
||||
@api_router.get("/agents/{agent_id}/chat-room")
|
||||
async def get_agent_chat_room(agent_id: str):
|
||||
"""
|
||||
Отримати інформацію про кімнату чату для агента.
|
||||
Повертає room_id, agent info для ініціалізації чату.
|
||||
"""
|
||||
try:
|
||||
agent = await repo_city.get_agent_by_id(agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}")
|
||||
|
||||
# Get agent's primary room or create room slug
|
||||
room_slug = f"agent-console-{agent.get('public_slug') or agent_id}"
|
||||
room = await repo_city.get_room_by_slug(room_slug)
|
||||
|
||||
# If room doesn't exist, try to get agent's primary room from dashboard
|
||||
if not room:
|
||||
rooms = await repo_city.get_agent_rooms(agent_id)
|
||||
if rooms and len(rooms) > 0:
|
||||
room = rooms[0]
|
||||
room_slug = room.get("slug", room_slug)
|
||||
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"agent_display_name": agent.get("display_name"),
|
||||
"agent_avatar_url": agent.get("avatar_url"),
|
||||
"agent_status": agent.get("status", "offline"),
|
||||
"agent_kind": agent.get("kind"),
|
||||
"room_slug": room_slug,
|
||||
"room_id": room.get("id") if room else None,
|
||||
"matrix_room_id": room.get("matrix_room_id") if room else None,
|
||||
"chat_available": room is not None and room.get("matrix_room_id") is not None
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get agent chat room for {agent_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get agent chat room")
|
||||
|
||||
|
||||
@api_router.get("/nodes/{node_id}/chat-room")
|
||||
async def get_node_chat_room(node_id: str):
|
||||
"""
|
||||
Отримати інформацію про кімнату чату для ноди.
|
||||
Повертає room_id, guardian/steward agents info.
|
||||
"""
|
||||
try:
|
||||
node = await repo_city.get_node_by_id(node_id)
|
||||
if not node:
|
||||
raise HTTPException(status_code=404, detail=f"Node not found: {node_id}")
|
||||
|
||||
# Get node support room
|
||||
node_slug = node_id.replace("node-", "").replace("-", "_")
|
||||
room_slug = f"node-support-{node_slug}"
|
||||
room = await repo_city.get_room_by_slug(room_slug)
|
||||
|
||||
# Get guardian and steward agents
|
||||
guardian_agent = None
|
||||
steward_agent = None
|
||||
|
||||
if node.get("guardian_agent"):
|
||||
guardian_agent = {
|
||||
"id": node["guardian_agent"].get("id"),
|
||||
"display_name": node["guardian_agent"].get("name"),
|
||||
"kind": node["guardian_agent"].get("kind"),
|
||||
"role": "guardian"
|
||||
}
|
||||
|
||||
if node.get("steward_agent"):
|
||||
steward_agent = {
|
||||
"id": node["steward_agent"].get("id"),
|
||||
"display_name": node["steward_agent"].get("name"),
|
||||
"kind": node["steward_agent"].get("kind"),
|
||||
"role": "steward"
|
||||
}
|
||||
|
||||
return {
|
||||
"node_id": node_id,
|
||||
"node_name": node.get("name"),
|
||||
"node_status": node.get("status", "offline"),
|
||||
"room_slug": room_slug,
|
||||
"room_id": room.get("id") if room else None,
|
||||
"matrix_room_id": room.get("matrix_room_id") if room else None,
|
||||
"chat_available": room is not None and room.get("matrix_room_id") is not None,
|
||||
"agents": [a for a in [guardian_agent, steward_agent] if a is not None]
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get node chat room for {node_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get node chat room")
|
||||
|
||||
|
||||
@api_router.get("/microdaos/{slug}/chat-room")
|
||||
async def get_microdao_chat_room(slug: str):
|
||||
"""
|
||||
Отримати інформацію про кімнату чату для MicroDAO.
|
||||
Повертає room_id, orchestrator agent info.
|
||||
"""
|
||||
try:
|
||||
dao = await repo_city.get_microdao_by_slug(slug)
|
||||
if not dao:
|
||||
raise HTTPException(status_code=404, detail=f"MicroDAO not found: {slug}")
|
||||
|
||||
# Get MicroDAO lobby room
|
||||
room_slug = f"microdao-lobby-{slug}"
|
||||
room = await repo_city.get_room_by_slug(room_slug)
|
||||
|
||||
# If no lobby room, try to get primary room
|
||||
if not room:
|
||||
rooms = await repo_city.get_microdao_rooms(dao["id"])
|
||||
if rooms and len(rooms) > 0:
|
||||
# Find primary room or first room
|
||||
primary = next((r for r in rooms if r.get("room_role") == "primary"), rooms[0])
|
||||
room = primary
|
||||
room_slug = room.get("slug", room_slug)
|
||||
|
||||
# Get orchestrator agent
|
||||
orchestrator = None
|
||||
orchestrator_id = dao.get("orchestrator_agent_id")
|
||||
if orchestrator_id:
|
||||
orch_agent = await repo_city.get_agent_by_id(orchestrator_id)
|
||||
if orch_agent:
|
||||
orchestrator = {
|
||||
"id": orchestrator_id,
|
||||
"display_name": orch_agent.get("display_name"),
|
||||
"avatar_url": orch_agent.get("avatar_url"),
|
||||
"kind": orch_agent.get("kind"),
|
||||
"role": "orchestrator"
|
||||
}
|
||||
|
||||
return {
|
||||
"microdao_id": dao["id"],
|
||||
"microdao_slug": slug,
|
||||
"microdao_name": dao.get("name"),
|
||||
"room_slug": room_slug,
|
||||
"room_id": room.get("id") if room else None,
|
||||
"matrix_room_id": room.get("matrix_room_id") if room else None,
|
||||
"chat_available": room is not None and room.get("matrix_room_id") is not None,
|
||||
"orchestrator": orchestrator
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get microdao chat room for {slug}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get microdao chat room")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# City Feed API
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user