diff --git a/web/app.js b/web/app.js index d84f6e3..dda58da 100644 --- a/web/app.js +++ b/web/app.js @@ -162,6 +162,9 @@ function updateModeUI() { if (isText) { setState(room ? "listening" : "idle"); window.setTimeout(() => chatInput.focus(), 0); + } else { + // Refresh mode-specific copy even when the underlying state is unchanged. + setState(uiState); } } @@ -188,19 +191,28 @@ async function setConversationMode(mode) { stopVisualLoop(); statusEl.textContent = "Secure room · Text mode"; } else { - await room.startAudio(); - await room.localParticipant.setMicrophoneEnabled(true); micMuted = false; muteBtn.disabled = false; updateMuteIcon(); - setupMicAnalyser(); startVisualLoop(); - statusEl.textContent = "Secure room · Microphone on"; + try { + await room.startAudio(); + await room.localParticipant.setMicrophoneEnabled(true); + setupMicAnalyser(); + statusEl.textContent = "Secure room · Microphone on"; + } catch (err) { + // Keep the conversation usable even if this browser denies or does + // not implement microphone capture. The mute control can retry. + console.warn("Microphone could not be enabled:", err); + micMuted = true; + updateMuteIcon(); + statusEl.textContent = "Voice mode · Microphone unavailable"; + } } } -voiceModeBtn.addEventListener("click", () => setConversationMode("voice")); -textModeBtn.addEventListener("click", () => setConversationMode("text")); +voiceModeBtn.addEventListener("click", () => setConversationMode("voice").catch(console.error)); +textModeBtn.addEventListener("click", () => setConversationMode("text").catch(console.error)); // ── Audio analysers (mic + remote agent audio) ───────────────────────────── let micCtx = null; @@ -523,9 +535,15 @@ function handleDataPacket(payload, participant, topic) { agentReady = true; } else if (msg.type === "transcript") { addMessage(msg.role || "agent", msg.text); - if (conversationMode === "text" && (msg.role || "agent") === "agent") { - setState("listening"); - chatInput.focus(); + if ((msg.role || "agent") === "agent") { + if (conversationMode === "text") { + setState("listening"); + chatInput.focus(); + } else if (uiState === "thinking") { + // Text arrives even when headless playback or an analyser is + // unavailable; use it to release the thinking state. + setState("speaking"); + } } } else if (msg.type === "task_event") { handleTaskEvent(msg); @@ -913,9 +931,15 @@ stopBtn.addEventListener("click", () => { // ── Mic mute toggle ───────────────────────────────────────────────────────── muteBtn.addEventListener("click", async () => { if (!room || !room.localParticipant) return; - micMuted = !micMuted; - await room.localParticipant.setMicrophoneEnabled(!micMuted); - updateMuteIcon(); + const nextMuted = !micMuted; + try { + await room.localParticipant.setMicrophoneEnabled(!nextMuted); + micMuted = nextMuted; + updateMuteIcon(); + } catch (err) { + console.warn("Microphone toggle failed:", err); + statusEl.textContent = "Microphone unavailable in this browser"; + } }); function updateMuteIcon() {