From ec53ed3abaa120d07118db18d5090badead3dc69 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Tue, 1 Sep 2026 07:59:10 +0200 Subject: [PATCH] Add DM typing indicator to the game chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server broadcasts a "typing" event to the whole room right when it starts the DM turn, and broadcasts turn failures to everyone too (previously only the sender saw them, which would've left other players' indicators stuck forever). The frontend shows a pulsing "Dungeon Master schreibt …" line that clears once the real reply or an error arrives. Co-Authored-By: Claude Sonnet 5 --- backend/app/api/ws_game.py | 3 ++- frontend/src/components/TranscriptPane.tsx | 15 +++++++++++++-- frontend/src/hooks/useGameSocket.ts | 4 ++++ frontend/src/pages/GameSession.tsx | 6 +++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/app/api/ws_game.py b/backend/app/api/ws_game.py index 0d7542c..5afab89 100644 --- a/backend/app/api/ws_game.py +++ b/backend/app/api/ws_game.py @@ -118,12 +118,13 @@ async def game_websocket(websocket: WebSocket, game_id: uuid.UUID, ticket: str) game_id, {"type": "message", "message": await _serialize_message(session, player_message)}, ) + await manager.broadcast(game_id, {"type": "typing"}) try: dm_message = await run_dm_turn(session, game_id, latest_player_message=content) except Exception: # noqa: BLE001 logger.exception("DM turn failed for game %s", game_id) - await websocket.send_json({"type": "error", "detail": "dm_turn_failed"}) + await manager.broadcast(game_id, {"type": "error", "detail": "dm_turn_failed"}) continue await manager.broadcast( diff --git a/frontend/src/components/TranscriptPane.tsx b/frontend/src/components/TranscriptPane.tsx index 7644640..ff6e7d4 100644 --- a/frontend/src/components/TranscriptPane.tsx +++ b/frontend/src/components/TranscriptPane.tsx @@ -9,12 +9,18 @@ function senderLabel(message: Message): string { return message.player_name ?? "Spieler"; } -export default function TranscriptPane({ messages }: { messages: Message[] }) { +export default function TranscriptPane({ + messages, + typing, +}: { + messages: Message[]; + typing?: boolean; +}) { const bottomRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ block: "end" }); - }, [messages]); + }, [messages, typing]); return (
@@ -34,6 +40,11 @@ export default function TranscriptPane({ messages }: { messages: Message[] }) { {m.content}
))} + {typing && ( +
+ Dungeon Master schreibt … +
+ )}
); diff --git a/frontend/src/hooks/useGameSocket.ts b/frontend/src/hooks/useGameSocket.ts index c96b6aa..729374c 100644 --- a/frontend/src/hooks/useGameSocket.ts +++ b/frontend/src/hooks/useGameSocket.ts @@ -7,12 +7,15 @@ export function useGameSocket( gameId: string | undefined, onMessage: (message: Message) => void, onError: (detail: string) => void, + onTyping: () => void, ) { const wsRef = useRef(null); const onMessageRef = useRef(onMessage); onMessageRef.current = onMessage; const onErrorRef = useRef(onError); onErrorRef.current = onError; + const onTypingRef = useRef(onTyping); + onTypingRef.current = onTyping; useEffect(() => { if (!gameId) return; @@ -31,6 +34,7 @@ export function useGameSocket( const data = JSON.parse(event.data); if (data.type === "message") onMessageRef.current(data.message); else if (data.type === "error") onErrorRef.current(data.detail); + else if (data.type === "typing") onTypingRef.current(); }; ws.onclose = () => { if (!intentionalClose) onErrorRef.current("connection_lost"); diff --git a/frontend/src/pages/GameSession.tsx b/frontend/src/pages/GameSession.tsx index 71e0cb9..175255b 100644 --- a/frontend/src/pages/GameSession.tsx +++ b/frontend/src/pages/GameSession.tsx @@ -19,6 +19,7 @@ export default function GameSession() { const [joinCode, setJoinCode] = useState(""); const [selectedCharacterId, setSelectedCharacterId] = useState(""); const [joinError, setJoinError] = useState(null); + const [isTyping, setIsTyping] = useState(false); async function loadGame() { if (!gameId) return; @@ -69,12 +70,14 @@ export default function GameSession() { const { send } = useGameSocket( game?.is_participant ? gameId : undefined, (incoming) => { + setIsTyping(false); setMessages((prev) => { const next = [...prev, incoming]; return showingFull ? next : next.slice(-5); }); }, (detail) => { + setIsTyping(false); const text = detail === "connection_lost" ? "Verbindung zum Server verloren. Bitte lade die Seite neu." @@ -94,6 +97,7 @@ export default function GameSession() { return showingFull ? next : next.slice(-5); }); }, + () => setIsTyping(true), ); async function handleSend(content: string) { @@ -173,7 +177,7 @@ export default function GameSession() {
- +