diff --git a/examples/web_client.html b/examples/web_client.html index 6838171..5eb24dc 100644 --- a/examples/web_client.html +++ b/examples/web_client.html @@ -229,6 +229,11 @@ border-left: 3px solid var(--good); } + .chat-entry.interim { + opacity: 0.7; + font-style: italic; + } + .log-entry { padding: 6px 8px; border-bottom: 1px dashed rgba(255, 255, 255, 0.06); @@ -306,7 +311,6 @@
-
@@ -366,7 +370,6 @@ const wsUrl = document.getElementById("wsUrl"); const connectBtn = document.getElementById("connectBtn"); const disconnectBtn = document.getElementById("disconnectBtn"); - const inviteBtn = document.getElementById("inviteBtn"); const inputSelect = document.getElementById("inputSelect"); const outputSelect = document.getElementById("outputSelect"); const startMicBtn = document.getElementById("startMicBtn"); @@ -390,6 +393,9 @@ let playbackDest = null; let playbackTime = 0; let discardAudio = false; + let playbackSources = []; + let interimUserEl = null; + let interimAiEl = null; const targetSampleRate = 16000; @@ -424,6 +430,26 @@ chatHistory.scrollTop = chatHistory.scrollHeight; } + function setInterim(role, text) { + const isAi = role === "AI"; + let el = isAi ? interimAiEl : interimUserEl; + if (!text) { + if (el) el.remove(); + if (isAi) interimAiEl = null; + else interimUserEl = null; + return; + } + if (!el) { + el = document.createElement("div"); + el.className = `chat-entry ${isAi ? "ai" : "user"} interim`; + chatHistory.appendChild(el); + if (isAi) interimAiEl = el; + else interimUserEl = el; + } + el.textContent = `${role} (interim): ${text}`; + chatHistory.scrollTop = chatHistory.scrollHeight; + } + function setStatus(connected, detail) { statusDot.classList.toggle("on", connected); statusText.textContent = connected ? "Connected" : "Disconnected"; @@ -491,6 +517,10 @@ const startTime = Math.max(audioCtx.currentTime + 0.02, playbackTime); source.start(startTime); playbackTime = startTime + buffer.duration; + playbackSources.push(source); + source.onended = () => { + playbackSources = playbackSources.filter((s) => s !== source); + }; } async function connect() { @@ -546,18 +576,46 @@ function handleEvent(event) { const type = event.event || "unknown"; logLine("event", type, event); - if (type === "transcript" && event.isFinal && event.text) { - addChat("You", event.text); + if (type === "transcript") { + if (event.isFinal && event.text) { + setInterim("You", ""); + addChat("You", event.text); + } else if (event.text) { + setInterim("You", event.text); + } } - if (type === "llmResponse" && event.isFinal && event.text) { - addChat("AI", event.text); + if (type === "llmResponse") { + if (event.isFinal && event.text) { + setInterim("AI", ""); + addChat("AI", event.text); + } else if (event.text) { + setInterim("AI", event.text); + } } if (type === "trackStart") { discardAudio = false; playbackTime = audioCtx ? audioCtx.currentTime : 0; } + if (type === "speaking") { + // User started speaking: clear any in-flight audio to avoid overlap + discardAudio = true; + playbackTime = audioCtx ? audioCtx.currentTime : 0; + playbackSources.forEach((s) => { + try { + s.stop(); + } catch (err) {} + }); + playbackSources = []; + } if (type === "interrupt") { discardAudio = true; + playbackTime = audioCtx ? audioCtx.currentTime : 0; + playbackSources.forEach((s) => { + try { + s.stop(); + } catch (err) {} + }); + playbackSources = []; } } @@ -647,9 +705,6 @@ await requestDeviceAccess(); await refreshDevices(); }); - inviteBtn.addEventListener("click", () => { - sendCommand({ command: "invite", option: { codec: "pcm", sampleRate: targetSampleRate } }); - }); startMicBtn.addEventListener("click", startMic); stopMicBtn.addEventListener("click", stopMic); sendChatBtn.addEventListener("click", () => {