import { useAgent } from "agents/react"; import { nanoid } from "nanoid"; import { useState, useEffect, useRef, useCallback } from "react"; import { Button, Input, Surface, Empty, Badge, Text } from "@cloudflare/kumo"; import { DemoWrapper } from "../../layout"; import { LogPanel, ConnectionStatus, CodeExplanation, type CodeSection } from "../../components"; import { useLogs } from "../../hooks"; import type { LobbyAgent, LobbyState, RoomInfo } from "./lobby-agent"; import type { RoomAgent, RoomState, ChatMessage } from "./room-agent"; const codeSections: CodeSection[] = [ { title: "Lobby + Room agent architecture", description: "A single LobbyAgent tracks all rooms. Each room is a separate RoomAgent instance. The lobby creates rooms via getAgentByName and the room handles its own members and messages.", code: `// lobby-agent.ts class LobbyAgent extends Agent { @callable() async createRoom(roomId: string) { const room = await getAgentByName(this.env.RoomAgent, roomId); await room.initialize({ name: roomId }); this.setState({ ...this.state, rooms: [...this.state.rooms, roomId], }); } } // room-agent.ts class RoomAgent extends Agent { @callable() sendMessage(userId: string, text: string) { const msg = { id: crypto.randomUUID(), userId, text }; this.broadcast(JSON.stringify({ type: "chat_message", message: msg })); } }` }, { title: "Connect to multiple agents from one page", description: "Use multiple useAgent hooks — one for the lobby, one for the current room. Set enabled: false to defer the connection until a room is selected.", code: `const lobby = useAgent({ agent: "lobby-agent", name: "main" }); const room = useAgent({ agent: "room-agent", name: currentRoom || "unused", enabled: !!currentRoom, // only connect when a room is selected });` } ]; export function ChatRoomsDemo() { const { logs, addLog, clearLogs } = useLogs(); const [username, setUsername] = useState(() => `user-${nanoid(4)}`); const [rooms, setRooms] = useState([]); const [currentRoom, setCurrentRoom] = useState(null); const [messages, setMessages] = useState([]); const [members, setMembers] = useState([]); const [newMessage, setNewMessage] = useState(""); const [newRoomName, setNewRoomName] = useState(""); const messagesEndRef = useRef(null); const lobby = useAgent({ agent: "lobby-agent", name: "main", onOpen: () => { addLog("info", "lobby_connected"); refreshRooms(); }, onClose: () => addLog("info", "lobby_disconnected"), onError: () => addLog("error", "error", "Lobby connection error"), onMessage: (message) => { try { const data = JSON.parse(message.data as string); if ( data.type === "room_created" || data.type === "room_updated" || data.type === "room_deleted" ) { refreshRooms(); } } catch { // ignore } } }); const room = useAgent({ agent: "room-agent", name: currentRoom || "unused", enabled: !!currentRoom, onOpen: async () => { if (currentRoom) { addLog("info", "room_connected", currentRoom); await joinRoom(); } }, onClose: () => { if (currentRoom) { addLog("info", "room_disconnected"); } }, onError: () => addLog("error", "error", "Room connection error"), onMessage: (message) => { try { const data = JSON.parse(message.data as string); handleRoomEvent(data); } catch { // ignore } } }); const handleRoomEvent = (data: { type: string; userId?: string; message?: ChatMessage; memberCount?: number; }) => { if (data.type === "member_joined") { addLog("in", "member_joined", data.userId); refreshMembers(); } else if (data.type === "member_left") { addLog("in", "member_left", data.userId); refreshMembers(); } else if (data.type === "chat_message" && data.message) { addLog("in", "chat_message", data.message); setMessages((prev) => [...prev, data.message as ChatMessage]); } }; const refreshRooms = useCallback(async () => { try { const result = await lobby.call("listRooms"); setRooms(result); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }, [lobby, addLog]); const refreshMembers = async () => { if (!currentRoom) return; try { const result = await room.call("getMembers"); setMembers(result); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const joinRoom = async () => { try { await room.call("join", [username]); const msgs = await room.call("getMessages", [50]); setMessages(msgs); await refreshMembers(); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const handleCreateRoom = async () => { const roomId = newRoomName.trim() || `room-${nanoid(4)}`; addLog("out", "call", `createRoom("${roomId}")`); try { await lobby.call("createRoom", [roomId]); setNewRoomName(""); await refreshRooms(); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const handleJoinRoom = async (roomId: string) => { if (currentRoom) { try { await room.call("leave", [username]); } catch { // ignore } } setCurrentRoom(roomId); setMessages([]); setMembers([]); addLog("out", "join_room", roomId); }; const handleLeaveRoom = async () => { if (currentRoom) { try { await room.call("leave", [username]); } catch { // ignore } setCurrentRoom(null); setMessages([]); setMembers([]); addLog("out", "leave_room"); } }; const handleSendMessage = async () => { if (!newMessage.trim() || !currentRoom) return; addLog("out", "send", newMessage); try { await room.call("sendMessage", [username, newMessage]); setNewMessage(""); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); useEffect(() => { if (lobby.readyState === WebSocket.OPEN) { refreshRooms(); } }, [lobby.readyState, refreshRooms]); return ( A two-tier agent architecture: a single{" "} LobbyAgent {" "} tracks all rooms, while each room is a separate{" "} RoomAgent {" "} instance handling its own members and messages. The client uses two{" "} useAgent {" "} hooks simultaneously — one for the lobby, one for the active room. Create a room and start chatting. } statusIndicator={ } >
{/* Lobby & Room List */}
{/* Username */} ) => setUsername(e.target.value) } className="w-full" placeholder="Enter username" /> {/* Lobby Connection */} {/* Create Room */}
) => setNewRoomName(e.target.value) } className="flex-1" placeholder="Room name (optional)" />
{/* Room List */}
{rooms.length > 0 ? ( rooms.map((r) => ( )) ) : ( )}
{/* Chat Area */}
{currentRoom ? ( <> {/* Room Header */}
{currentRoom} {members.length} members
{/* Messages */}
{messages.length > 0 ? ( messages.map((msg) => (
{msg.userId}
{msg.text}
)) ) : ( )}
{/* Input */}
) => setNewMessage(e.target.value) } onKeyDown={(e: React.KeyboardEvent) => e.key === "Enter" && handleSendMessage() } className="flex-1" placeholder="Type a message..." />
) : (
Select a room to start chatting
)} {/* Members */} {currentRoom && members.length > 0 && (
Members
{members.map((member) => ( {member} ))}
)}
{/* Logs */}
); }