diff --git a/examples/web_client.html b/examples/web_client.html
index 05edb2a..02ab70e 100644
--- a/examples/web_client.html
+++ b/examples/web_client.html
@@ -192,7 +192,7 @@
}
.log {
- height: 520px;
+ height: 320px;
overflow: auto;
padding: 12px;
background: #0d0d14;
@@ -202,6 +202,33 @@
line-height: 1.4;
}
+ .chat {
+ height: 260px;
+ overflow: auto;
+ padding: 12px;
+ background: #0d0d14;
+ border-radius: 12px;
+ border: 1px solid var(--grid);
+ font-size: 0.9rem;
+ line-height: 1.45;
+ }
+
+ .chat-entry {
+ padding: 8px 10px;
+ margin-bottom: 8px;
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ }
+
+ .chat-entry.user {
+ border-left: 3px solid var(--accent-2);
+ }
+
+ .chat-entry.ai {
+ border-left: 3px solid var(--good);
+ }
+
.log-entry {
padding: 6px 8px;
border-bottom: 1px dashed rgba(255, 255, 255, 0.06);
@@ -256,6 +283,9 @@
.log {
height: 360px;
}
+ .chat {
+ height: 260px;
+ }
}
@@ -314,9 +344,15 @@
-
- Event Log
-
+
@@ -342,6 +378,7 @@
const clearLogBtn = document.getElementById("clearLogBtn");
const chatInput = document.getElementById("chatInput");
const logEl = document.getElementById("log");
+ const chatHistory = document.getElementById("chatHistory");
const statusDot = document.getElementById("statusDot");
const statusText = document.getElementById("statusText");
const statusSub = document.getElementById("statusSub");
@@ -381,12 +418,35 @@
logEl.scrollTop = logEl.scrollHeight;
}
+ function addChat(role, text) {
+ const entry = document.createElement("div");
+ entry.className = `chat-entry ${role === "AI" ? "ai" : "user"}`;
+ entry.textContent = `${role}: ${text}`;
+ chatHistory.appendChild(entry);
+ chatHistory.scrollTop = chatHistory.scrollHeight;
+ }
+
function setStatus(connected, detail) {
statusDot.classList.toggle("on", connected);
statusText.textContent = connected ? "Connected" : "Disconnected";
statusSub.textContent = detail || "";
}
+ async function ensureAudioContext() {
+ if (audioCtx) return;
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ playbackDest = audioCtx.createMediaStreamDestination();
+ audioOut.srcObject = playbackDest.stream;
+ try {
+ await audioOut.play();
+ } catch (err) {
+ logLine("sys", "Audio playback blocked (user gesture needed)", { err: String(err) });
+ }
+ if (outputSelect.value) {
+ await setOutputDevice(outputSelect.value);
+ }
+ }
+
function downsampleBuffer(buffer, inRate, outRate) {
if (outRate === inRate) return buffer;
const ratio = inRate / outRate;
@@ -443,6 +503,7 @@
ws.onopen = () => {
setStatus(true, "Session open");
logLine("sys", "WebSocket connected");
+ ensureAudioContext();
};
ws.onclose = () => {
@@ -486,6 +547,12 @@
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 === "llmResponse" && event.isFinal && event.text) {
+ addChat("AI", event.text);
+ }
if (type === "trackStart") {
discardAudio = false;
playbackTime = audioCtx ? audioCtx.currentTime : 0;
@@ -500,12 +567,7 @@
logLine("sys", "Connect before starting mic");
return;
}
- if (!audioCtx) {
- audioCtx = new AudioContext();
- playbackDest = audioCtx.createMediaStreamDestination();
- audioOut.srcObject = playbackDest.stream;
- await audioOut.play().catch(() => {});
- }
+ await ensureAudioContext();
const deviceId = inputSelect.value || undefined;
micStream = await navigator.mediaDevices.getUserMedia({
audio: deviceId ? { deviceId: { exact: deviceId } } : true,
@@ -595,6 +657,8 @@
sendChatBtn.addEventListener("click", () => {
const text = chatInput.value.trim();
if (!text) return;
+ ensureAudioContext();
+ addChat("You", text);
sendCommand({ command: "chat", text });
chatInput.value = "";
});