fix: say why the microphone is unavailable, instead of freezing the button

`navigator.mediaDevices` only exists in a secure context — HTTPS, or
localhost. Over plain http on a LAN address the property is undefined, so
`_startRecording` threw on its first line, the catch wrote one console line
and returned, and `_recording` stayed false: the button sat there unchanged
with nothing to read anywhere a user would look.

The unavailable cases are now named before the attempt rather than guessed
at afterwards — insecure context, unsupported browser, denied permission,
anything else — and surfaced in the chat through `_pushError`, which every
chat surface already shares. The button is deliberately still rendered when
the context is insecure: hiding it would read as "transcription is not
configured", which is the wrong diagnosis to hand someone.

Adds docs/voice.md, since "why doesn't the microphone work" is a question
the assistant will be asked and the answer is entirely outside Skald.
This commit is contained in:
2026-08-04 15:19:50 +01:00
parent f900d803f2
commit e29dc40202
6 changed files with 74 additions and 1 deletions
+28
View File
@@ -1042,8 +1042,31 @@ export class ChatSession extends LightElement {
}
}
/**
* Why the microphone can be missing even when the server has a transcribe
* model: `navigator.mediaDevices` is only exposed in a **secure context**
* (HTTPS, or localhost/127.0.0.1) — over http on a LAN address the property
* is `undefined`, not a denied permission. That used to die in the `catch`
* below as a bare console line, leaving the button frozen with no
* explanation, so the unavailable cases are named here instead of guessed at.
*/
_micUnavailableReason() {
if (!navigator.mediaDevices?.getUserMedia) {
return window.isSecureContext === false
? t('chat.mic.insecure')
: t('chat.mic.unsupported');
}
if (typeof MediaRecorder === 'undefined') return t('chat.mic.unsupported');
return null;
}
async _startRecording(fromShortcut = false) {
if (this._recording) return;
const unavailable = this._micUnavailableReason();
if (unavailable) {
this._pushError(unavailable);
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._audioChunks = [];
@@ -1072,6 +1095,11 @@ export class ChatSession extends LightElement {
this._recording = true;
} catch (err) {
console.error('mic error:', err);
this._pushError(
err?.name === 'NotAllowedError' || err?.name === 'SecurityError'
? t('chat.mic.denied')
: t('chat.mic.failed', { error: err?.message || err?.name || 'unknown' }),
);
}
}