import React, { useState, useEffect, KeyboardEvent, useCallback } from "react"; import { Input, Paper, Typography } from "@mui/material"; import { Session, Lobby } from "./GlobalContext"; import { UserList } from "./UserList"; import { LobbyChat } from "./LobbyChat"; import BotManager from "./BotManager"; import "./App.css"; import { ws_base, base } from "./Common"; import { Box, Button, Tooltip } from "@mui/material"; import { BrowserRouter as Router, Route, Routes, useParams } from "react-router-dom"; import useWebSocket, { ReadyState } from "react-use-websocket"; import ConnectionStatus from "./ConnectionStatus"; import { sessionsApi, LobbyCreateRequest } from "./api-client"; console.log(`AI Voice Chat Build: ${process.env.REACT_APP_AI_VOICECHAT_BUILD}`); type LobbyProps = { session: Session; setSession: React.Dispatch>; setError: React.Dispatch>; }; const LobbyView: React.FC = (props: LobbyProps) => { const { session, setSession, setError } = props; const { lobbyName = "default" } = useParams<{ lobbyName: string }>(); const [lobby, setLobby] = useState(null); const [editName, setEditName] = useState(""); const [editPassword, setEditPassword] = useState(""); const [socketUrl, setSocketUrl] = useState(null); const [creatingLobby, setCreatingLobby] = useState(false); const [reconnectAttempt, setReconnectAttempt] = useState(0); const [shouldRetryLobby, setShouldRetryLobby] = useState(false); // Check if lobbyName looks like a lobby ID (32 hex characters) and redirect to default useEffect(() => { if (lobbyName && /^[a-f0-9]{32}$/i.test(lobbyName)) { console.log(`Lobby - Detected lobby ID in URL (${lobbyName}), redirecting to default lobby`); window.history.replaceState(null, "", `${base}/lobby/default`); window.location.reload(); // Force reload to use the new URL } }, [lobbyName]); const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(socketUrl, { onOpen: () => { console.log("app - WebSocket connection opened."); setReconnectAttempt(0); }, onClose: () => { console.log("app - WebSocket connection closed."); setReconnectAttempt((prev) => prev + 1); }, onError: (event: Event) => { console.error("app - WebSocket error observed:", event); // If we get a WebSocket error, it might be due to invalid lobby ID // Reset the lobby state to force recreation if (lobby) { console.log("app - WebSocket error, clearing lobby state to force refresh"); setLobby(null); setSocketUrl(null); } }, shouldReconnect: (closeEvent) => { // Don't reconnect if the lobby doesn't exist (4xx errors) if (closeEvent.code >= 4000 && closeEvent.code < 5000) { console.log("app - WebSocket closed with client error, not reconnecting"); return false; } return true; }, reconnectInterval: 5000, // Retry every 5 seconds onReconnectStop: (numAttempts) => { console.log(`Stopped reconnecting after ${numAttempts} attempts`); }, share: true, }); useEffect(() => { if (lobby && session) { setSocketUrl(`${ws_base}/${lobby.id}/${session.id}`); } }, [lobby, session]); useEffect(() => { if (!lastJsonMessage || !session) { return; } const data: any = lastJsonMessage; switch (data.type) { case "update_name": if (data.data && "name" in data.data) { console.log(`Lobby - name set to ${data.data.name}`); setSession((s) => (s ? { ...s, name: data.data.name } : null)); } break; case "error": console.error(`Lobby - Server error: ${data.data.error}`); setError(data.data.error); // If the error is about lobby not found, reset the lobby state if (data.data.error && data.data.error.includes("Lobby not found")) { console.log("Lobby - Lobby not found error, clearing lobby state"); setLobby(null); setSocketUrl(null); } break; default: break; } }, [lastJsonMessage, session, setError, setSession]); useEffect(() => { console.log("app - WebSocket connection status: ", readyState); }, [readyState]); // Retry lobby creation when session is restored after a failure useEffect(() => { if (session && shouldRetryLobby && !lobby && !creatingLobby) { console.log("Lobby - Session restored, retrying lobby creation"); setShouldRetryLobby(false); // The main lobby creation effect will trigger automatically due to session change } }, [session, shouldRetryLobby, lobby, creatingLobby]); useEffect(() => { if (!session || !lobbyName || creatingLobby || (lobby && lobby.name === lobbyName)) { return; } // Clear any existing lobby state when switching to a new lobby name if (lobby && lobby.name !== lobbyName) { console.log(`Lobby - Clearing previous lobby state: ${lobby.name} -> ${lobbyName}`); setLobby(null); setSocketUrl(null); } const getLobby = async (lobbyName: string, session: Session) => { try { const lobbyRequest: LobbyCreateRequest = { type: "lobby_create", data: { name: lobbyName, private: false, }, }; const response = await sessionsApi.createLobby(session.id, lobbyRequest); if (response.type !== "lobby_created") { console.error(`Lobby - Unexpected response type: ${response.type}`); setError(`Unexpected response from server`); return; } const lobby: Lobby = response.data; console.log(`Lobby - Joined lobby`, lobby); setLobby(lobby); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to create/join lobby"; console.error("Lobby creation error:", errorMessage); setError(errorMessage); // If it's a server error (5xx), mark for retry when session is restored if ( err instanceof Error && (err.message.includes("502") || err.message.includes("503") || err.message.includes("500")) ) { console.log("Lobby - Server error detected, will retry when session is restored"); setShouldRetryLobby(true); } else { console.log("Lobby - Non-retryable error, clearing session"); setSession(null); } } }; setCreatingLobby(true); getLobby(lobbyName, session).finally(() => { setCreatingLobby(false); }); }, [session, lobbyName, lobby, setLobby, setError, creatingLobby]); const setName = (name: string) => { sendJsonMessage({ type: "set_name", data: { name, password: editPassword ? editPassword : undefined }, }); }; const handleKeyDown = (event: KeyboardEvent): void => { if (event.key === "Enter") { event.preventDefault(); const newName = editName.trim(); if (!newName || session?.name === newName) { return; } setName(newName); setEditName(""); } }; return ( {readyState !== ReadyState.OPEN || !session ? ( ) : ( <> AI Voice Chat Lobby: {lobbyName} {session.name && You are logged in as: {session.name}} {!session.name && ( Enter your name to join: You can optionally set a password to reserve this name; supply it again to takeover the name from another client. { setEditName(e.target.value); }} onKeyDown={handleKeyDown} placeholder="Your name" /> setEditPassword(e.target.value)} placeholder="Optional password" /> )} {session.name && ( <> {/* {session.lobbies.map((lobby: string) => ( ))} */} {session && socketUrl && lobby && ( )} {session && socketUrl && lobby && ( )} {session && lobby && ( console.log(`Bot ${botName} added to lobby`)} sx={{ minWidth: "300px" }} /> )} )} )} ); }; const App = () => { const [session, setSession] = useState(null); const [error, setError] = useState(null); const [sessionRetryAttempt, setSessionRetryAttempt] = useState(0); useEffect(() => { if (error) { setTimeout(() => setError(null), 5000); } }, [error]); useEffect(() => { if (!session) { return; } console.log(`App - sessionId`, session.id); }, [session]); const getSession = useCallback(async () => { try { const session = await sessionsApi.getCurrent(); setSession(session); setSessionRetryAttempt(0); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error occurred"; console.error("Failed to get session:", errorMessage); setError(errorMessage); // Schedule retry after 5 seconds setSessionRetryAttempt((prev) => prev + 1); setTimeout(() => { getSession(); // Retry }, 5000); } }, []); useEffect(() => { if (session) { return; } getSession(); }, [session, getSession]); return ( {!session && ( 0 ? ReadyState.CLOSED : ReadyState.CONNECTING} reconnectAttempt={sessionRetryAttempt} /> )} {session && ( } path={`${base}/:lobbyName`} /> } path={`${base}`} /> )} {error && ( {error} )} ); }; export default App;