Update voice demo language problem

This commit is contained in:
Xin Wang
2026-06-02 12:31:34 +08:00
parent 82a0cec38e
commit aae751f3f3
3 changed files with 136 additions and 104 deletions

View File

@@ -170,7 +170,7 @@ function setConnectButton() {
els.chatId.disabled = state.connected || state.connecting; els.chatId.disabled = state.connected || state.connecting;
els.copyChatIdBtn.disabled = !state.connected || !state.chatId; els.copyChatIdBtn.disabled = !state.connected || !state.chatId;
if (state.connecting) { if (state.connecting) {
els.connectBtn.textContent = "Connecting…"; els.connectBtn.textContent = "连接中…";
els.connectBtn.disabled = true; els.connectBtn.disabled = true;
els.connectBtn.classList.remove("is-disconnect"); els.connectBtn.classList.remove("is-disconnect");
} else if (state.connected) { } else if (state.connected) {
@@ -207,8 +207,8 @@ async function copyChatId() {
function setMicButton() { function setMicButton() {
els.micBtn.disabled = !state.connected; els.micBtn.disabled = !state.connected;
els.micBtn.setAttribute("aria-pressed", state.micEnabled ? "true" : "false"); els.micBtn.setAttribute("aria-pressed", state.micEnabled ? "true" : "false");
els.micBtn.title = state.micEnabled ? "Mute mic" : "Unmute mic"; els.micBtn.title = state.micEnabled ? "关闭麦克风" : "开启麦克风";
els.micLabel.textContent = state.micEnabled ? "Mute mic" : "Enable mic"; els.micLabel.textContent = state.micEnabled ? "关闭麦克风" : "开启麦克风";
els.micIndicator.classList.toggle("is-active", state.micEnabled); els.micIndicator.classList.toggle("is-active", state.micEnabled);
} }
@@ -231,8 +231,8 @@ function setAssistantState(value) {
const label = text.length > 32 ? `${text.slice(0, 31)}` : text; const label = text.length > 32 ? `${text.slice(0, 31)}` : text;
state.assistantState = text; state.assistantState = text;
els.stateIndicator.classList.toggle("is-active", Boolean(text)); els.stateIndicator.classList.toggle("is-active", Boolean(text));
els.stateLabel.textContent = label ? `State ${label}` : "State -"; els.stateLabel.textContent = label ? `状态 ${label}` : "状态 -";
els.stateIndicator.title = label ? `Assistant state: ${text}` : "Assistant state"; els.stateIndicator.title = label ? `助手状态:${text}` : "助手状态";
syncCameraDrawer(text); syncCameraDrawer(text);
} }
@@ -253,7 +253,7 @@ function syncCameraDrawer(value) {
els.conversation.classList.toggle("has-camera", open); els.conversation.classList.toggle("has-camera", open);
els.cameraDrawer.setAttribute("aria-hidden", open ? "false" : "true"); els.cameraDrawer.setAttribute("aria-hidden", open ? "false" : "true");
if (open) { if (open) {
els.cameraState.textContent = `State ${value}`; els.cameraState.textContent = `状态 ${value}`;
els.cameraQuestion.textContent = prompt; els.cameraQuestion.textContent = prompt;
renderSampleThumbnails(); renderSampleThumbnails();
selectDefaultImage(); selectDefaultImage();
@@ -261,7 +261,7 @@ function syncCameraDrawer(value) {
if (!state.cameraActive) populateDeviceSelect(); if (!state.cameraActive) populateDeviceSelect();
}); });
} else { } else {
els.cameraState.textContent = "State -"; els.cameraState.textContent = "状态 -";
els.cameraQuestion.textContent = ""; els.cameraQuestion.textContent = "";
if (wasOpen) resetCameraInput(); if (wasOpen) resetCameraInput();
} }
@@ -277,7 +277,7 @@ function addBubble(role, text) {
if (role !== "system") { if (role !== "system") {
const tag = document.createElement("span"); const tag = document.createElement("span");
tag.className = "bubble__role"; tag.className = "bubble__role";
tag.textContent = role === "user" ? "You" : "Assistant"; tag.textContent = role === "user" ? "" : "助手";
bubble.appendChild(tag); bubble.appendChild(tag);
} }
const body = document.createElement("span"); const body = document.createElement("span");
@@ -299,7 +299,7 @@ function addImageBubble(role, imageUrl, text) {
if (role !== "system") { if (role !== "system") {
const tag = document.createElement("span"); const tag = document.createElement("span");
tag.className = "bubble__role"; tag.className = "bubble__role";
tag.textContent = role === "user" ? "You" : "Assistant"; tag.textContent = role === "user" ? "" : "助手";
bubble.appendChild(tag); bubble.appendChild(tag);
} }
const img = document.createElement("img"); const img = document.createElement("img");
@@ -334,7 +334,7 @@ function clearChat() {
setAssistantState(""); setAssistantState("");
const empty = document.createElement("div"); const empty = document.createElement("div");
empty.className = "chat__empty"; empty.className = "chat__empty";
empty.innerHTML = "<p>Chat cleared.</p>"; empty.innerHTML = "<p>对话已清空。</p>";
els.chatLog.appendChild(empty); els.chatLog.appendChild(empty);
} }
@@ -656,7 +656,7 @@ function wsSend(data) {
function clearWsLog() { function clearWsLog() {
state.wsLogGroup = null; state.wsLogGroup = null;
els.wsLog.innerHTML = els.wsLog.innerHTML =
'<div class="ws-log__empty">No websocket events yet.</div>'; '<div class="ws-log__empty">暂无 WebSocket 事件。</div>';
} }
/* ---------------------------------------------------------------- Audio */ /* ---------------------------------------------------------------- Audio */
@@ -679,7 +679,7 @@ function renderMicDevices() {
const defaultOption = document.createElement("option"); const defaultOption = document.createElement("option");
defaultOption.value = ""; defaultOption.value = "";
defaultOption.textContent = "Default microphone"; defaultOption.textContent = "默认麦克风";
els.micSelect.appendChild(defaultOption); els.micSelect.appendChild(defaultOption);
state.micDevices.forEach((device, index) => { state.micDevices.forEach((device, index) => {
@@ -752,7 +752,7 @@ async function startMic() {
state.micSourceNode.connect(state.recorderNode); state.micSourceNode.connect(state.recorderNode);
state.micEnabled = true; state.micEnabled = true;
addWsLog("system", "mic capture started (binary input.audio frames)"); addWsLog("system", "麦克风已开启PCM 音频流)");
setMicButton(); setMicButton();
} }
@@ -788,7 +788,7 @@ function stopMic() {
state.micEnabled = false; state.micEnabled = false;
updateMeter(0); updateMeter(0);
if (wasEnabled) { if (wasEnabled) {
addWsLog("system", "mic capture stopped"); addWsLog("system", "麦克风已关闭");
} }
setMicButton(); setMicButton();
} }
@@ -902,7 +902,7 @@ function mediaToPayload(source) {
try { try {
dataUrl = canvas.toDataURL("image/jpeg", IMAGE_JPEG_QUALITY); dataUrl = canvas.toDataURL("image/jpeg", IMAGE_JPEG_QUALITY);
} catch (err) { } catch (err) {
addWsLog("system", `image encode failed: ${err.message || err}`); addWsLog("system", `图片编码失败:${err.message || err}`);
return null; return null;
} }
return { dataUrl, mime: "image/jpeg", width: w, height: h }; return { dataUrl, mime: "image/jpeg", width: w, height: h };
@@ -952,7 +952,7 @@ function populateDeviceSelect(activeDeviceId) {
async function startCamera(deviceId) { async function startCamera(deviceId) {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
addWsLog("system", "getUserMedia not available in this browser"); addWsLog("system", "该浏览器不支持摄像头访问");
return; return;
} }
stopCameraStream(); stopCameraStream();
@@ -965,7 +965,7 @@ async function startCamera(deviceId) {
audio: false, audio: false,
}); });
} catch (err) { } catch (err) {
addWsLog("system", `camera error: ${err.message || err}`); addWsLog("system", `摄像头错误:${err.message || err}`);
return; return;
} }
els.cameraVideo.srcObject = state.cameraStream; els.cameraVideo.srcObject = state.cameraStream;
@@ -1029,7 +1029,7 @@ async function selectFileImage(file) {
clearSampleSelection(); clearSampleSelection();
setPendingImage(payload); setPendingImage(payload);
} catch (err) { } catch (err) {
addWsLog("system", `upload error: ${err.message || err}`); addWsLog("system", `上传错误:${err.message || err}`);
} finally { } finally {
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl);
} }
@@ -1045,7 +1045,7 @@ async function selectSampleImage(src, buttonEl) {
if (buttonEl) buttonEl.classList.add("is-selected"); if (buttonEl) buttonEl.classList.add("is-selected");
setPendingImage(payload); setPendingImage(payload);
} catch (err) { } catch (err) {
addWsLog("system", `sample error: ${err.message || err}`); addWsLog("system", `示例图加载错误:${err.message || err}`);
} }
} }
@@ -1260,9 +1260,9 @@ async function connect() {
state.connecting = true; state.connecting = true;
state.chatId = chatId; state.chatId = chatId;
els.chatId.value = chatId; els.chatId.value = chatId;
setStatus("connecting", "Connecting…"); setStatus("connecting", "连接中…");
setConnectButton(); setConnectButton();
addWsLog("system", `connecting ${url}`); addWsLog("system", `正在连接 ${url}`);
try { try {
// Pre-warm audio context on user gesture so playback works on Safari. // Pre-warm audio context on user gesture so playback works on Safari.
@@ -1309,15 +1309,15 @@ async function connect() {
state.connecting = false; state.connecting = false;
state.connected = true; state.connected = true;
resetPlaybackClock(); resetPlaybackClock();
addWsLog("system", "websocket open"); addWsLog("system", "连接已建立");
setStatus("connected", "Connected"); setStatus("connected", "已连接");
setConnectButton(); setConnectButton();
setMicButton(); setMicButton();
setMicSelectEnabled(); setMicSelectEnabled();
refreshMicDevices(); refreshMicDevices();
wsSend(JSON.stringify(startMessage)); wsSend(JSON.stringify(startMessage));
addBubble("system", "Session started."); addBubble("system", "会话已开始。");
setComposerEnabled(true); setComposerEnabled(true);
setCameraButtonEnabled(); setCameraButtonEnabled();
els.textInput.focus(); els.textInput.focus();
@@ -1378,11 +1378,11 @@ async function connect() {
if (wasConnected) { if (wasConnected) {
addBubble( addBubble(
"system", "system",
`Session ended${event.reason ? `${event.reason}` : ""}.`, `会话已结束${event.reason ? `${event.reason}` : ""}`,
); );
setStatus("idle", "Disconnected"); setStatus("idle", "未连接");
} else { } else {
setStatus("error", "Connection closed"); setStatus("error", "连接已断开");
} }
}); });
} }
@@ -1424,7 +1424,7 @@ els.micBtn.addEventListener("click", async () => {
} }
} catch (err) { } catch (err) {
console.error("Mic error", err); console.error("Mic error", err);
addBubble("system", `Mic error: ${err.message || err}`); addBubble("system", `麦克风错误:${err.message || err}`);
} finally { } finally {
els.micBtn.disabled = !state.connected; els.micBtn.disabled = !state.connected;
} }
@@ -1441,7 +1441,7 @@ els.micSelect.addEventListener("change", async () => {
await startMic(); await startMic();
} catch (err) { } catch (err) {
console.error("Mic switch error", err); console.error("Mic switch error", err);
addBubble("system", `Mic switch error: ${err.message || err}`); addBubble("system", `麦克风切换错误:${err.message || err}`);
} finally { } finally {
setMicButton(); setMicButton();
setMicSelectEnabled(); setMicSelectEnabled();
@@ -1534,7 +1534,7 @@ window.addEventListener("beforeunload", () => {
els.url.value = defaultWsUrl(); els.url.value = defaultWsUrl();
setStatus("idle", "Disconnected"); setStatus("idle", "未连接");
setConnectButton(); setConnectButton();
setMicButton(); setMicButton();
setMicSelectEnabled(); setMicSelectEnabled();

View File

@@ -16,7 +16,7 @@
<div class="connection"> <div class="connection">
<label class="connection__field"> <label class="connection__field">
<span>WebSocket URL</span> <span>服务器地址</span>
<input <input
id="ws-url" id="ws-url"
type="text" type="text"
@@ -26,12 +26,12 @@
/> />
</label> </label>
<label class="connection__field connection__field--chat"> <label class="connection__field connection__field--chat">
<span>Chat ID</span> <span>会话 ID</span>
<div class="chat-id-control"> <div class="chat-id-control">
<input <input
id="chat-id" id="chat-id"
type="text" type="text"
placeholder="optional chatId" placeholder="可选"
spellcheck="false" spellcheck="false"
autocomplete="off" autocomplete="off"
/> />
@@ -40,8 +40,8 @@
class="chat-id-control__copy" class="chat-id-control__copy"
type="button" type="button"
disabled disabled
title="Copy Chat ID" title="复制会话 ID"
aria-label="Copy Chat ID" aria-label="复制会话 ID"
> >
<svg class="copy-icon copy-icon--default" viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true"> <svg class="copy-icon copy-icon--default" viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true">
<rect x="5" y="5" width="8" height="9" rx="1.5" stroke="currentColor" stroke-width="1.4"/> <rect x="5" y="5" width="8" height="9" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
@@ -54,13 +54,13 @@
</div> </div>
</label> </label>
<button id="connect-btn" class="btn btn--primary" type="button"> <button id="connect-btn" class="btn btn--primary" type="button">
Connect 连接
</button> </button>
</div> </div>
<div class="status"> <div class="status">
<span id="status-dot" class="status__dot status__dot--idle"></span> <span id="status-dot" class="status__dot status__dot--idle"></span>
<span id="status-text" class="status__text">Disconnected</span> <span id="status-text" class="status__text">未连接</span>
</div> </div>
</header> </header>
@@ -70,13 +70,13 @@
<aside <aside
id="camera-drawer" id="camera-drawer"
class="camera-drawer" class="camera-drawer"
aria-label="Camera capture step" aria-label="拍照步骤"
aria-hidden="true" aria-hidden="true"
> >
<div class="camera-drawer__panel"> <div class="camera-drawer__panel">
<div class="camera-drawer__header"> <div class="camera-drawer__header">
<div> <div>
<p class="camera-drawer__eyebrow">Camera</p> <p class="camera-drawer__eyebrow">拍照</p>
<h2>拍照步骤</h2> <h2>拍照步骤</h2>
</div> </div>
<span id="camera-state" class="camera-drawer__state">State -</span> <span id="camera-state" class="camera-drawer__state">State -</span>
@@ -109,21 +109,48 @@
<p id="camera-question" class="camera-drawer__question"></p> <p id="camera-question" class="camera-drawer__question"></p>
<div class="camera-drawer__sources"> <div class="camera-drawer__sources">
<button <div class="camera-drawer__camera-row">
id="camera-start-btn" <label class="device-picker">
class="btn btn--ghost camera-drawer__source" <span class="device-picker__label">摄像头</span>
type="button" <select
> id="camera-device-select"
使用摄像头 class="device-picker__select"
</button> disabled
<select >
id="camera-device-select" <option value="">默认摄像头</option>
class="camera-drawer__select" </select>
aria-label="选择摄像头" </label>
disabled <button
> id="camera-start-btn"
<option value="">默认摄像头</option> class="mic-btn cam-btn"
</select> type="button"
title="打开摄像头"
>
<svg
class="mic-btn__icon"
viewBox="0 0 24 24"
width="24"
height="24"
aria-hidden="true"
>
<path
d="M4 8h3l1.2-1.6A1 1 0 0 1 9 6h6a1 1 0 0 1 .8.4L17 8h3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"
fill="none"
stroke="currentColor"
stroke-width="1.6"
/>
<circle
cx="12"
cy="13"
r="3.2"
fill="none"
stroke="currentColor"
stroke-width="1.6"
/>
</svg>
<span class="mic-btn__label">使用摄像头</span>
</button>
</div>
<label <label
class="btn btn--ghost camera-drawer__source camera-drawer__source--upload" class="btn btn--ghost camera-drawer__source camera-drawer__source--upload"
> >
@@ -155,7 +182,7 @@
</div> </div>
</aside> </aside>
<section class="chat" aria-label="Conversation history"> <section class="chat" aria-label="对话记录">
<div id="chat-log" class="chat__log" role="log" aria-live="polite"> <div id="chat-log" class="chat__log" role="log" aria-live="polite">
<div class="chat__empty"> <div class="chat__empty">
<p>Connect to the engine, enable your mic, and start talking.</p> <p>Connect to the engine, enable your mic, and start talking.</p>
@@ -168,7 +195,7 @@
</section> </section>
</div> </div>
<footer class="controls" aria-label="Chat controls"> <footer class="controls" aria-label="操作栏">
<div class="meter" aria-hidden="true"> <div class="meter" aria-hidden="true">
<div id="meter-fill" class="meter__fill"></div> <div id="meter-fill" class="meter__fill"></div>
</div> </div>
@@ -178,7 +205,7 @@
id="text-input" id="text-input"
class="composer__input" class="composer__input"
rows="1" rows="1"
placeholder="Type a message, or use the mic…" placeholder="输入消息,或使用麦克风…"
disabled disabled
></textarea> ></textarea>
<button <button
@@ -186,17 +213,17 @@
class="btn btn--primary composer__send" class="btn btn--primary composer__send"
type="submit" type="submit"
disabled disabled
title="Send message (Enter)" title="发送消息 (Enter)"
> >
Send 发送
</button> </button>
</form> </form>
<div class="controls__row"> <div class="controls__row">
<label class="device-picker"> <label class="device-picker">
<span class="device-picker__label">Microphone</span> <span class="device-picker__label">麦克风</span>
<select id="mic-select" class="device-picker__select" disabled> <select id="mic-select" class="device-picker__select" disabled>
<option value="">Default microphone</option> <option value="">默认麦克风</option>
</select> </select>
</label> </label>
@@ -206,7 +233,7 @@
type="button" type="button"
disabled disabled
aria-pressed="false" aria-pressed="false"
title="Mic is off" title="麦克风已关闭"
> >
<svg <svg
class="mic-btn__icon" class="mic-btn__icon"
@@ -224,52 +251,52 @@
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
<span class="mic-btn__label">Enable mic</span> <span class="mic-btn__label">开启麦克风</span>
</button> </button>
<div class="indicators"> <div class="indicators">
<span id="mic-indicator" class="indicator"> <span id="mic-indicator" class="indicator">
<span class="indicator__dot indicator__dot--mic"></span> <span class="indicator__dot indicator__dot--mic"></span>
<span class="indicator__label">Mic</span> <span class="indicator__label">麦克风</span>
</span> </span>
<span id="bot-indicator" class="indicator"> <span id="bot-indicator" class="indicator">
<span class="indicator__dot indicator__dot--bot"></span> <span class="indicator__dot indicator__dot--bot"></span>
<span class="indicator__label">Bot</span> <span class="indicator__label">助手</span>
</span> </span>
<span id="state-indicator" class="indicator indicator--state"> <span id="state-indicator" class="indicator indicator--state">
<span class="indicator__dot indicator__dot--state"></span> <span class="indicator__dot indicator__dot--state"></span>
<span id="state-label" class="indicator__label">State -</span> <span id="state-label" class="indicator__label">状态 -</span>
</span> </span>
</div> </div>
<button id="clear-btn" class="btn btn--ghost" type="button"> <button id="clear-btn" class="btn btn--ghost" type="button">
Clear 清空
</button> </button>
</div> </div>
<p class="hint"> <p class="hint">
Press <kbd>Enter</kbd> to send, <kbd>Shift</kbd>+<kbd>Enter</kbd> <kbd>Enter</kbd> 发送,<kbd>Shift</kbd>+<kbd>Enter</kbd>
for newline. Sending text will interrupt the bot if it's speaking. 换行。发送文字会打断正在说话的助手。
Browser echo cancellation is on; use headphones if echo persists. 浏览器回声消除已开启,如有回音请使用耳机。
</p> </p>
</footer> </footer>
</div> </div>
<section class="ws-log" aria-label="WebSocket log"> <section class="ws-log" aria-label="WebSocket 日志">
<div class="ws-log__header"> <div class="ws-log__header">
<div class="ws-log__header-left"> <div class="ws-log__header-left">
<h2>WebSocket Log</h2> <h2>WebSocket 日志</h2>
<div class="ws-log__legend" aria-hidden="true"> <div class="ws-log__legend" aria-hidden="true">
<span class="ws-log__legend-item ws-log__legend-item--send">Send</span> <span class="ws-log__legend-item ws-log__legend-item--send">发送</span>
<span class="ws-log__legend-item ws-log__legend-item--recv">Recv</span> <span class="ws-log__legend-item ws-log__legend-item--recv">接收</span>
</div> </div>
</div> </div>
<button id="clear-ws-log-btn" class="btn btn--ghost" type="button"> <button id="clear-ws-log-btn" class="btn btn--ghost" type="button">
Clear log 清空日志
</button> </button>
</div> </div>
<div id="ws-log" class="ws-log__body" role="log" aria-live="polite"> <div id="ws-log" class="ws-log__body" role="log" aria-live="polite">
<div class="ws-log__empty">No websocket events yet.</div> <div class="ws-log__empty">暂无 WebSocket 事件。</div>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -271,11 +271,34 @@ body {
} }
.camera-drawer__sources { .camera-drawer__sources {
display: grid; display: flex;
grid-template-columns: 1fr 1fr; flex-direction: column;
gap: 8px; gap: 8px;
} }
/* Mirror the mic controls: labeled device select + an action pill button. */
.camera-drawer__camera-row {
display: flex;
align-items: flex-end;
gap: 8px;
}
.camera-drawer__camera-row .device-picker {
flex: 1 1 auto;
max-width: none;
}
.cam-btn {
flex-shrink: 0;
}
.cam-btn.is-active {
background: var(--success);
border-color: var(--success);
color: #fff;
box-shadow: 0 0 0 6px rgba(45, 210, 139, 0.18);
}
.camera-drawer__source { .camera-drawer__source {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -287,30 +310,6 @@ body {
cursor: pointer; cursor: pointer;
} }
.camera-drawer__select {
appearance: none;
width: 100%;
min-height: 38px;
background: var(--bg-soft);
color: var(--text);
border: 1px solid var(--border);
border-radius: 10px;
padding: 7px 28px 7px 10px;
font: inherit;
font-size: 13px;
outline: none;
cursor: pointer;
}
.camera-drawer__select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Upload spans the full width on its own row below camera + device list. */
.camera-drawer__source--upload {
grid-column: 1 / -1;
}
.camera-drawer__source.is-active { .camera-drawer__source.is-active {
border-color: var(--success); border-color: var(--success);
@@ -709,8 +708,8 @@ body {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
color: var(--text-dim); color: var(--text-dim);
text-transform: uppercase; letter-spacing: 0.5px;
letter-spacing: 0.8px; white-space: nowrap;
} }
.ws-log__header-left { .ws-log__header-left {
@@ -965,6 +964,12 @@ body {
outline: none; outline: none;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
text-overflow: ellipsis;
}
.device-picker__select:disabled {
opacity: 0.5;
cursor: not-allowed;
} }
.device-picker__select:focus { .device-picker__select:focus {