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:
@@ -80,6 +80,10 @@ export default {
|
||||
'chat.rejected_by_user': 'Denied by user.',
|
||||
'chat.truncated': 'Response truncated by the token limit (↓{tokens} tok).',
|
||||
'chat.not_connected': 'Not connected — reconnecting. Your message is still in the box: press Enter again in a moment.',
|
||||
'chat.mic.insecure': 'Your browser only allows the microphone over a secure connection. Open Skald at http://localhost (on the machine itself) or put it behind HTTPS.',
|
||||
'chat.mic.unsupported': 'This browser does not support voice recording.',
|
||||
'chat.mic.denied': 'Microphone access was denied. Allow it for this site in your browser settings, then try again.',
|
||||
'chat.mic.failed': 'Could not start recording: {error}',
|
||||
|
||||
// ── Copilot render ─────────────────────────────────────────────────────────
|
||||
'copilot.open_in_viewer': 'Open in viewer',
|
||||
|
||||
@@ -80,6 +80,10 @@ export default {
|
||||
'chat.rejected_by_user': 'Refusé par l\'utilisateur.',
|
||||
'chat.truncated': 'Réponse tronquée par la limite de tokens (↓{tokens} tok).',
|
||||
'chat.not_connected': 'Non connecté — reconnexion en cours. Votre message est resté dans le champ : appuyez de nouveau sur Entrée dans un instant.',
|
||||
'chat.mic.insecure': 'Votre navigateur n\'autorise le microphone que sur une connexion sécurisée. Ouvrez Skald sur http://localhost (depuis la machine elle-même) ou placez-le derrière HTTPS.',
|
||||
'chat.mic.unsupported': 'Ce navigateur ne prend pas en charge l\'enregistrement vocal.',
|
||||
'chat.mic.denied': 'Accès au microphone refusé. Autorisez-le pour ce site dans les réglages du navigateur, puis réessayez.',
|
||||
'chat.mic.failed': 'Impossible de démarrer l\'enregistrement : {error}',
|
||||
|
||||
// ── Copilot render ─────────────────────────────────────────────────────────
|
||||
'copilot.open_in_viewer': 'Ouvrir dans le visualiseur',
|
||||
|
||||
@@ -80,6 +80,10 @@ export default {
|
||||
'chat.rejected_by_user': 'Negata dall\'utente.',
|
||||
'chat.truncated': 'Risposta troncata dal limite di token (↓{tokens} tok).',
|
||||
'chat.not_connected': 'Non connesso — riconnessione in corso. Il messaggio è rimasto nella casella: premi di nuovo Invio tra un istante.',
|
||||
'chat.mic.insecure': 'Il browser consente il microfono solo su connessione sicura. Apri Skald su http://localhost (dalla macchina stessa) oppure mettilo dietro HTTPS.',
|
||||
'chat.mic.unsupported': 'Questo browser non supporta la registrazione vocale.',
|
||||
'chat.mic.denied': 'Accesso al microfono negato. Consentilo per questo sito nelle impostazioni del browser e riprova.',
|
||||
'chat.mic.failed': 'Impossibile avviare la registrazione: {error}',
|
||||
|
||||
// ── Copilot render ─────────────────────────────────────────────────────────
|
||||
'copilot.open_in_viewer': 'Apri nel visualizzatore',
|
||||
|
||||
@@ -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' }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user