diff --git a/static/app.js b/static/app.js index b6c4aa8..aa27df6 100644 --- a/static/app.js +++ b/static/app.js @@ -409,9 +409,9 @@ function selectVoice(wrap) { } let _toastTimer; -function toast(msg, type = '') { +function toast(msg, type = '', ms = 3500) { const el = $('toast'); el.textContent = msg; el.className = 'show ' + type; - clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.className = '', 3500); + clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.className = '', ms); } function status(msg) { $('status-bar').textContent = msg; } function escHtml(s) { @@ -8188,6 +8188,27 @@ $('s-import-voices-file')?.addEventListener('change', async function () { const turnHistory = $('conv-turn-history'); if (!chatWindow || !micBtn) return; + // Warn if microphone API is unavailable (HTTP on non-localhost = insecure context) + if (!navigator.mediaDevices?.getUserMedia) { + if (micStatus) { + micStatus.textContent = 'Mic unavailable — insecure context'; + micStatus.style.color = 'var(--red, #e05)'; + } + if (micBtn) { + micBtn.disabled = true; + micBtn.title = 'Browser blocks microphone on HTTP. Use http://localhost:7890 or HTTPS.\n' + + 'Chrome fix: chrome://flags/#unsafely-treat-insecure-origin-as-secure'; + micBtn.style.opacity = '0.4'; + } + const warn = document.createElement('div'); + warn.style.cssText = 'background:var(--red,#c00);color:#fff;padding:10px 16px;border-radius:8px;margin:12px 0;font-size:13px;line-height:1.5'; + warn.innerHTML = '⚠ Microphone blocked by browser
' + + 'Browsers only allow microphone access on secure contexts (HTTPS or localhost).
' + + 'Quick fix: open the app at http://localhost:7890 instead of the IP address.
' + + 'Remote access fix: add the URL in chrome://flags/#unsafely-treat-insecure-origin-as-secure'; + chatWindow && chatWindow.parentElement && chatWindow.parentElement.insertBefore(warn, chatWindow); + } + // Restore conv LLM URL from server settings if (llmUrlInp && _appSettings && _appSettings.conv_llm_url) llmUrlInp.value = _appSettings.conv_llm_url; if (llmUrlInp) llmUrlInp.addEventListener('input', () => { @@ -8367,6 +8388,11 @@ $('s-import-voices-file')?.addEventListener('change', async function () { async function startRecording() { if (isProcessing) return; + if (!navigator.mediaDevices?.getUserMedia) { + toast('Microphone unavailable — browser requires a secure context (HTTPS or localhost). ' + + 'Access the app via http://localhost:7890 or enable it in chrome://flags/#unsafely-treat-insecure-origin-as-secure', 'error', 8000); + return; + } let stream; try { stream = await navigator.mediaDevices.getUserMedia({ audio: true });