feat(ui): add microphone mute control
This commit is contained in:
+38
-10
@@ -670,6 +670,12 @@ const stopBtn = $("#stop-btn");
|
||||
const whiteMicBtn = $("#white-mic-circle");
|
||||
/** @type {HTMLButtonElement} */
|
||||
const whiteStopBtn = $("#white-stop-btn");
|
||||
/** @type {HTMLButtonElement} */
|
||||
const whiteMuteBtn = $("#white-mute-btn");
|
||||
/** @type {HTMLElement} */
|
||||
const whiteMuteLabel = $("#white-mute-label");
|
||||
/** @type {HTMLElement} */
|
||||
const whiteSessionControls = $("#white-session-controls");
|
||||
/** @type {HTMLParagraphElement} */
|
||||
const whiteMicCaption = $("#white-mic-caption");
|
||||
/** @type {HTMLParagraphElement} */
|
||||
@@ -1917,12 +1923,28 @@ let micMuted = false;
|
||||
/** @type {Promise<void> | null} */
|
||||
let teardownPromise = null;
|
||||
|
||||
function syncMicControls() {
|
||||
const live = LIVE_STATES.has(currentState);
|
||||
const canMute = live && !!micStream && !!client && !teardownPromise;
|
||||
const label = micMuted ? "Activar micrófono" : "Silenciar micrófono";
|
||||
for (const button of [micBtn, whiteMuteBtn]) {
|
||||
button.disabled = !canMute;
|
||||
button.classList.toggle("muted", micMuted);
|
||||
button.setAttribute("aria-label", label);
|
||||
button.title = label;
|
||||
}
|
||||
whiteMuteBtn.hidden = !live || s2sTheme !== "white";
|
||||
whiteMuteLabel.textContent = label;
|
||||
}
|
||||
|
||||
function syncVoiceControls() {
|
||||
const view = STATE_VIEWS[currentState];
|
||||
const live = LIVE_STATES.has(currentState);
|
||||
const disabled = view.disabled || !!teardownPromise;
|
||||
const mutedWhileListening = micMuted && (currentState === "listening" || currentState === "user-speaking");
|
||||
const label = currentState === "error" ? "Reintentar conversación de voz"
|
||||
: currentState === "idle" ? "Iniciar conversación de voz" : view.caption;
|
||||
: currentState === "idle" ? "Iniciar conversación de voz"
|
||||
: mutedWhileListening ? "Micrófono silenciado" : view.caption;
|
||||
circleBtn.disabled = disabled;
|
||||
circleBtn.setAttribute("aria-label", label);
|
||||
whiteMicBtn.disabled = disabled;
|
||||
@@ -1930,6 +1952,8 @@ function syncVoiceControls() {
|
||||
whiteMicBtn.className = `white-mic-circle ${STATE_CLASS[currentState]}`;
|
||||
whiteStopBtn.hidden = !live || s2sTheme !== "white";
|
||||
whiteStopBtn.disabled = !live || !!teardownPromise;
|
||||
whiteSessionControls.hidden = !live || s2sTheme !== "white";
|
||||
syncMicControls();
|
||||
|
||||
const waiting = currentState === "queued" || currentState === "your-turn";
|
||||
chatStatus.dataset.status = live ? "online" : currentState === "error" ? "error"
|
||||
@@ -2017,7 +2041,8 @@ function updateRestartAvailability() {
|
||||
* @param {"" | "error" | "muted"} [kind]
|
||||
*/
|
||||
function setCaption(text, kind = "") {
|
||||
const trimmed = text.trim();
|
||||
const mutedWhileListening = micMuted && (currentState === "listening" || currentState === "user-speaking") && kind !== "error";
|
||||
const trimmed = mutedWhileListening ? "Micrófono silenciado" : text.trim();
|
||||
circleCaption.textContent = trimmed;
|
||||
// .idle-cta is the JS-based fallback (adds gradient CTA style) for browsers
|
||||
// that don't support :has(). The primary selector is .stage:has(.state-idle) .circle-caption.
|
||||
@@ -2028,7 +2053,7 @@ function setCaption(text, kind = "") {
|
||||
if (addIdleCta) parts.push("idle-cta");
|
||||
circleCaption.className = parts.join(" ");
|
||||
whiteMicCaption.textContent = trimmed;
|
||||
whiteMicCaption.classList.toggle("muted", kind === "muted");
|
||||
whiteMicCaption.classList.toggle("muted", kind === "muted" || mutedWhileListening);
|
||||
whiteMicCaption.classList.toggle("error", kind === "error");
|
||||
}
|
||||
|
||||
@@ -3012,17 +3037,21 @@ async function handleStartError(err) {
|
||||
onFatalError(err);
|
||||
}
|
||||
|
||||
micBtn.addEventListener("click", () => {
|
||||
if (!micStream || !client) return;
|
||||
function toggleMicMute() {
|
||||
if (!micStream || !client || !LIVE_STATES.has(currentState) || teardownPromise) return;
|
||||
micMuted = !micMuted;
|
||||
for (const track of micStream.getAudioTracks()) {
|
||||
track.enabled = !micMuted;
|
||||
}
|
||||
client.setMuted(micMuted);
|
||||
micBtn.classList.toggle("muted", micMuted);
|
||||
micBtn.setAttribute("aria-label", micMuted ? "Activar micrófono" : "Silenciar micrófono");
|
||||
micBtn.title = micMuted ? "Activar micrófono" : "Silenciar micrófono";
|
||||
});
|
||||
syncVoiceControls();
|
||||
if ((currentState === "listening" || currentState === "user-speaking") && !whiteMicCaption.classList.contains("error")) {
|
||||
setCaption(STATE_VIEWS[currentState].caption);
|
||||
}
|
||||
}
|
||||
|
||||
micBtn.addEventListener("click", toggleMicMute);
|
||||
whiteMuteBtn.addEventListener("click", toggleMicMute);
|
||||
|
||||
stopBtn.addEventListener("click", async () => {
|
||||
await teardown();
|
||||
@@ -3440,7 +3469,6 @@ async function finishTeardown(closingClient, finalState) {
|
||||
micStream = null;
|
||||
}
|
||||
micMuted = false;
|
||||
micBtn.classList.remove("muted");
|
||||
setState(finalState);
|
||||
// Refresh the chip's remaining-today after the budget moved.
|
||||
if (limiterOn) void account.refresh();
|
||||
|
||||
Reference in New Issue
Block a user