feat(ui): add microphone mute control
This commit is contained in:
+21
-11
@@ -256,7 +256,7 @@
|
||||
- Blue mic circular button (116 px, deep blue, white mic)
|
||||
- Two waveform banks left + right (audio-reactive CSS bars)
|
||||
- Listening status text
|
||||
- Stop pill button below.
|
||||
- Mute and stop controls below.
|
||||
Mirrors the screenshot center UI exactly. -->
|
||||
<div class="white-mic-stage" hidden data-theme-only="white" aria-label="Control de voz">
|
||||
<!-- Waveform left side: 7 bars mirror right side, heights driven by JS vars -->
|
||||
@@ -302,16 +302,26 @@
|
||||
<p id="white-mic-caption" class="white-mic-caption" role="status">Toca para empezar</p>
|
||||
<p id="white-mic-subcaption" class="white-mic-subcaption" hidden></p>
|
||||
|
||||
<!-- Secondary stop pill below caption, with a dark stop-square icon. -->
|
||||
<button id="white-stop-btn"
|
||||
class="white-stop-btn"
|
||||
type="button"
|
||||
hidden
|
||||
aria-label="Detener"
|
||||
title="Detener sesión de voz">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
||||
<span>Detener</span>
|
||||
</button>
|
||||
<div id="white-session-controls" class="white-session-controls" role="group" aria-label="Controles de la conversación" hidden>
|
||||
<button id="white-mute-btn" class="white-mute-btn" type="button" hidden disabled aria-label="Silenciar micrófono" title="Silenciar micrófono">
|
||||
<svg class="mic-on" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="19" x2="12" y2="22"/>
|
||||
</svg>
|
||||
<svg class="mic-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<line x1="2" y1="2" x2="22" y2="22"/><path d="M9 5a3 3 0 0 1 6 0v4"/><path d="M9 10v1a3 3 0 0 0 5.1 2.1"/><path d="M19 10a7 7 0 0 1-1.24 3.97"/><path d="M5 10a7 7 0 0 0 11 5.67"/><line x1="12" y1="19" x2="12" y2="22"/>
|
||||
</svg>
|
||||
<span id="white-mute-label">Silenciar micrófono</span>
|
||||
</button>
|
||||
<button id="white-stop-btn"
|
||||
class="white-stop-btn"
|
||||
type="button"
|
||||
hidden
|
||||
aria-label="Detener"
|
||||
title="Detener sesión de voz">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
||||
<span>Detener</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="circle-caption" class="circle-caption" role="status">Toca para empezar</p>
|
||||
|
||||
+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();
|
||||
|
||||
@@ -542,6 +542,8 @@ a:hover {
|
||||
0 0 34px -6px color-mix(in srgb, var(--brand) 60%, transparent);
|
||||
}
|
||||
#ident-user-name[hidden],
|
||||
#white-session-controls[hidden],
|
||||
#white-mute-btn[hidden],
|
||||
#white-stop-btn[hidden],
|
||||
#white-mic-subcaption[hidden] {
|
||||
display: none !important;
|
||||
@@ -4623,6 +4625,7 @@ html[data-theme="white"] .white-mic-circle:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
html[data-theme="white"] .white-mic-circle:focus-visible,
|
||||
html[data-theme="white"] .white-mute-btn:focus-visible,
|
||||
html[data-theme="white"] .white-stop-btn:focus-visible {
|
||||
outline: 3px solid #1d4ed8;
|
||||
outline-offset: 4px;
|
||||
@@ -4706,6 +4709,60 @@ html[data-theme="white"] .white-mic-subcaption {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Active conversation controls ──────────────────────────────────── */
|
||||
html[data-theme="white"] .white-session-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: #1d4ed8;
|
||||
font: inherit;
|
||||
font-size: clamp(14px, 3.4vw, 16px);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn:hover:not(:disabled) {
|
||||
background: #eff6ff;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn.muted {
|
||||
background: #fffbeb;
|
||||
border-color: #fcd34d;
|
||||
color: #92400e;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn.muted:hover:not(:disabled) {
|
||||
background: #fef3c7;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn svg {
|
||||
flex: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn .mic-off,
|
||||
html[data-theme="white"] .white-mute-btn.muted .mic-on {
|
||||
display: none;
|
||||
}
|
||||
html[data-theme="white"] .white-mute-btn.muted .mic-off {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── "Detener" pill ─────────────────────────────────────────────────── */
|
||||
html[data-theme="white"] .white-stop-btn {
|
||||
display: inline-flex;
|
||||
|
||||
Reference in New Issue
Block a user