feat(microdao-rooms): Add MicroDAO rooms creation/deletion and agent chat
Backend:
- POST /city/microdao/{slug}/rooms - create new room for MicroDAO
- DELETE /city/microdao/{slug}/rooms/{room_id} - soft-delete room
- POST /city/agents/{agent_id}/ensure-room - create personal agent room
Frontend:
- MicrodaoRoomsSection: Added create room modal with name, description, type
- MicrodaoRoomsSection: Added delete room functionality for managers
- Agent page: Added 'Поговорити' button to open chat in City Room
Models:
- Added CreateMicrodaoRoomRequest model
Task: TASK_PHASE_MICRODAO_ROOMS_AND_PUBLIC_CHAT_v3
This commit is contained in:
@@ -268,6 +268,26 @@ export default function AgentConsolePage() {
|
||||
|
||||
{/* Status & Actions */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Chat Button */}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/city/agents/${agentId}/ensure-room`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.room_slug) {
|
||||
window.location.href = `/city/${data.room_slug}`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to open chat', e);
|
||||
}
|
||||
}}
|
||||
className="bg-cyan-600 hover:bg-cyan-500"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
Поговорити
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
profile?.status === 'online' ? 'bg-emerald-500' : 'bg-white/30'
|
||||
|
||||
@@ -246,9 +246,12 @@ export default function MicrodaoDetailPage() {
|
||||
{/* Multi-Room Section with Chats (includes Team Chat) */}
|
||||
<MicrodaoRoomsSection
|
||||
rooms={displayRooms}
|
||||
microdaoSlug={slug}
|
||||
primaryRoomSlug={microdao.primary_city_room?.slug}
|
||||
canManage={canManage}
|
||||
onEnsureOrchestratorRoom={handleEnsureOrchestratorRoom}
|
||||
onRoomCreated={handleRoomUpdated}
|
||||
onRoomDeleted={handleRoomUpdated}
|
||||
/>
|
||||
|
||||
{/* MicroDAO Agents Section - using new API */}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2, Bot, PlusCircle, Crown } from "lucide-react";
|
||||
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2, Bot, PlusCircle, Crown, Plus, Trash2, X, Loader2 } from "lucide-react";
|
||||
import { CityRoomSummary } from "@/lib/types/microdao";
|
||||
import { CityChatWidget } from "@/components/city/CityChatWidget";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -9,10 +9,13 @@ import { useState } from "react";
|
||||
|
||||
interface MicrodaoRoomsSectionProps {
|
||||
rooms: CityRoomSummary[];
|
||||
microdaoSlug?: string;
|
||||
primaryRoomSlug?: string | null;
|
||||
showAllChats?: boolean;
|
||||
canManage?: boolean;
|
||||
onEnsureOrchestratorRoom?: () => Promise<void>;
|
||||
onRoomCreated?: () => void;
|
||||
onRoomDeleted?: () => void;
|
||||
}
|
||||
|
||||
const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.ReactNode }> = {
|
||||
@@ -107,12 +110,22 @@ const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.
|
||||
|
||||
export function MicrodaoRoomsSection({
|
||||
rooms,
|
||||
microdaoSlug,
|
||||
primaryRoomSlug,
|
||||
showAllChats = false,
|
||||
canManage = false,
|
||||
onEnsureOrchestratorRoom
|
||||
onEnsureOrchestratorRoom,
|
||||
onRoomCreated,
|
||||
onRoomDeleted
|
||||
}: MicrodaoRoomsSectionProps) {
|
||||
const [isCreatingTeam, setIsCreatingTeam] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newRoomName, setNewRoomName] = useState("");
|
||||
const [newRoomDescription, setNewRoomDescription] = useState("");
|
||||
const [newRoomRole, setNewRoomRole] = useState("general");
|
||||
const [newRoomIsPublic, setNewRoomIsPublic] = useState(true);
|
||||
const [deletingRoomId, setDeletingRoomId] = useState<string | null>(null);
|
||||
|
||||
const handleCreateTeam = async () => {
|
||||
if (!onEnsureOrchestratorRoom) return;
|
||||
@@ -126,6 +139,53 @@ export function MicrodaoRoomsSection({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateRoom = async () => {
|
||||
if (!microdaoSlug || !newRoomName.trim()) return;
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const res = await fetch(`/api/city/microdao/${microdaoSlug}/rooms`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: newRoomName.trim(),
|
||||
description: newRoomDescription.trim() || null,
|
||||
room_role: newRoomRole,
|
||||
is_public: newRoomIsPublic,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to create room");
|
||||
setShowCreateModal(false);
|
||||
setNewRoomName("");
|
||||
setNewRoomDescription("");
|
||||
setNewRoomRole("general");
|
||||
setNewRoomIsPublic(true);
|
||||
onRoomCreated?.();
|
||||
} catch (e) {
|
||||
console.error("Failed to create room", e);
|
||||
alert("Не вдалося створити кімнату");
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRoom = async (roomId: string, roomName: string) => {
|
||||
if (!microdaoSlug) return;
|
||||
if (!confirm(`Видалити кімнату "${roomName}"?`)) return;
|
||||
setDeletingRoomId(roomId);
|
||||
try {
|
||||
const res = await fetch(`/api/city/microdao/${microdaoSlug}/rooms/${roomId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to delete room");
|
||||
onRoomDeleted?.();
|
||||
} catch (e) {
|
||||
console.error("Failed to delete room", e);
|
||||
alert("Не вдалося видалити кімнату");
|
||||
} finally {
|
||||
setDeletingRoomId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (!rooms || rooms.length === 0) {
|
||||
return (
|
||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||
@@ -213,14 +273,113 @@ export function MicrodaoRoomsSection({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Room Modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-2xl p-6 w-full max-w-md space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white">Нова кімната</h3>
|
||||
<button onClick={() => setShowCreateModal(false)} className="text-slate-400 hover:text-white">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm text-slate-400 mb-1">Назва *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRoomName}
|
||||
onChange={(e) => setNewRoomName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-cyan-500"
|
||||
placeholder="Назва кімнати"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-slate-400 mb-1">Опис</label>
|
||||
<textarea
|
||||
value={newRoomDescription}
|
||||
onChange={(e) => setNewRoomDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-cyan-500 h-20 resize-none"
|
||||
placeholder="Опис кімнати (опціонально)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-slate-400 mb-1">Тип кімнати</label>
|
||||
<select
|
||||
value={newRoomRole}
|
||||
onChange={(e) => setNewRoomRole(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="general">Загальна</option>
|
||||
<option value="lobby">Лобі</option>
|
||||
<option value="team">Команда</option>
|
||||
<option value="research">Дослідження</option>
|
||||
<option value="governance">Управління</option>
|
||||
<option value="security">Безпека</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newRoomIsPublic}
|
||||
onChange={(e) => setNewRoomIsPublic(e.target.checked)}
|
||||
className="w-4 h-4 rounded bg-slate-800 border-slate-600 text-cyan-500 focus:ring-cyan-500"
|
||||
/>
|
||||
<span className="text-sm text-slate-300">Публічна кімната</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button variant="outline" onClick={() => setShowCreateModal(false)}>
|
||||
Скасувати
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateRoom}
|
||||
disabled={isCreating || !newRoomName.trim()}
|
||||
className="bg-cyan-600 hover:bg-cyan-500"
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Створення...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Створити
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* General Rooms Section */}
|
||||
<div className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
|
||||
<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>
|
||||
<div className="flex items-center gap-3">
|
||||
<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>
|
||||
{canManage && microdaoSlug && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="border-cyan-500/50 text-cyan-400 hover:bg-cyan-500/10"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Нова кімната
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mini-map */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
Reference in New Issue
Block a user