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() {
- +