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 });