Add audio logging and WebSocket enhancements to webpage

- Introduced new state properties for audio send logging.
- Implemented functions to flush audio send logs and handle binary data sending.
- Updated WebSocket logging to differentiate between send and receive actions.
- Enhanced UI layout and styles for better responsiveness and usability.
- Improved overall structure and readability of the code.
This commit is contained in:
Xin Wang
2026-05-21 14:25:20 +08:00
parent 092f532f51
commit f3eab5b272
3 changed files with 347 additions and 171 deletions

View File

@@ -69,6 +69,9 @@ const state = {
audioDeltaLogCount: 0,
audioDeltaLogBytes: 0,
lastAudioDeltaLogAt: 0,
audioSendLogCount: 0,
audioSendLogBytes: 0,
lastAudioSendLogAt: 0,
};
/* ------------------------------------------------------------------ UI */
@@ -201,7 +204,12 @@ function addWsLog(direction, detail, kind = direction) {
const dir = document.createElement("span");
dir.className = "ws-log__direction";
dir.textContent = direction;
dir.textContent =
direction === "send"
? "SEND"
: direction === "recv"
? "RECV"
: direction.toUpperCase();
const body = document.createElement("span");
body.className = "ws-log__detail";
@@ -227,8 +235,30 @@ function flushAudioDeltaLog() {
state.lastAudioDeltaLogAt = performance.now();
}
function flushAudioSendLog() {
if (state.audioSendLogCount === 0) return;
addWsLog(
"send",
`input.audio binary x${state.audioSendLogCount} (${state.audioSendLogBytes} bytes)`,
);
state.audioSendLogCount = 0;
state.audioSendLogBytes = 0;
state.lastAudioSendLogAt = performance.now();
}
function flushPendingWsLogs() {
flushAudioDeltaLog();
flushAudioSendLog();
}
function logWsPayload(direction, payload) {
if (payload?.type === "response.audio.delta") {
if (direction === "send") {
flushAudioSendLog();
} else {
flushAudioDeltaLog();
}
if (direction === "recv" && payload?.type === "response.audio.delta") {
state.audioDeltaLogCount += 1;
state.audioDeltaLogBytes += payload.bytes || payload.audio?.length || 0;
const now = performance.now();
@@ -238,13 +268,50 @@ function logWsPayload(direction, payload) {
return;
}
flushAudioDeltaLog();
addWsLog(direction, compactWsPayload(payload));
}
function logBinarySend(byteLength) {
state.audioSendLogCount += 1;
state.audioSendLogBytes += byteLength;
const now = performance.now();
if (now - state.lastAudioSendLogAt >= AUDIO_DELTA_LOG_INTERVAL_MS) {
flushAudioSendLog();
}
}
function wsSend(data) {
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return false;
if (typeof data === "string") {
try {
logWsPayload("send", JSON.parse(data));
} catch (_) {
flushAudioSendLog();
flushAudioDeltaLog();
addWsLog("send", truncateLogValue(data));
}
} else {
const byteLength =
data instanceof ArrayBuffer
? data.byteLength
: ArrayBuffer.isView(data)
? data.byteLength
: 0;
if (byteLength > 0) {
logBinarySend(byteLength);
}
}
state.ws.send(data);
return true;
}
function clearWsLog() {
state.audioDeltaLogCount = 0;
state.audioDeltaLogBytes = 0;
state.audioSendLogCount = 0;
state.audioSendLogBytes = 0;
els.wsLog.innerHTML =
'<div class="ws-log__empty">No websocket events yet.</div>';
}
@@ -335,14 +402,14 @@ async function startMic() {
const data = event.data;
if (!data || data.type !== "frame") return;
updateMeter(data.rms || 0);
if (state.connected && state.ws && state.ws.readyState === WebSocket.OPEN) {
state.ws.send(data.buffer);
if (state.connected) {
wsSend(data.buffer);
}
};
state.micSourceNode.connect(state.recorderNode);
state.micEnabled = true;
addWsLog("send", "input.audio binary stream started");
addWsLog("system", "mic capture started (binary input.audio frames)");
setMicButton();
}
@@ -378,7 +445,8 @@ function stopMic() {
state.micEnabled = false;
updateMeter(0);
if (wasEnabled) {
addWsLog("send", "input.audio binary stream stopped");
flushAudioSendLog();
addWsLog("system", "mic capture stopped");
}
setMicButton();
}
@@ -483,8 +551,7 @@ function sendText(text) {
// `response.text.final(interrupted=true)` for the in-flight assistant
// turn, which finalizes that bubble in place. A brand-new bubble for the
// reply will be created when `response.text.started` arrives.
state.ws.send(JSON.stringify(message));
logWsPayload("send", message);
wsSend(JSON.stringify(message));
stopPlaybackQueue();
addBubble("user", value);
return true;
@@ -629,8 +696,7 @@ async function connect() {
setMicSelectEnabled();
refreshMicDevices();
ws.send(JSON.stringify(startMessage));
logWsPayload("send", startMessage);
wsSend(JSON.stringify(startMessage));
addBubble("system", "Session started.");
setComposerEnabled(true);
els.textInput.focus();
@@ -678,7 +744,7 @@ async function connect() {
setMicSelectEnabled();
setComposerEnabled(false);
setBotIndicator(false);
flushAudioDeltaLog();
flushPendingWsLogs();
addWsLog(
"system",
`websocket close code=${event.code}${
@@ -702,8 +768,7 @@ function disconnect() {
try {
if (state.ws.readyState === WebSocket.OPEN) {
const stopMessage = { type: "session.stop", reason: "client_disconnect" };
state.ws.send(JSON.stringify(stopMessage));
logWsPayload("send", stopMessage);
wsSend(JSON.stringify(stopMessage));
}
} catch (_) {
/* ignore */