From 93338567e8d740aa01548d66a883c797361acd85 Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Sun, 22 Mar 2026 12:27:17 -0400 Subject: [PATCH] Feat: restore ElevenLabs Adam voice for read aloud with browser TTS fallback; add client-side error logging --- docker-compose.yml | 2 +- public/js/app.js | 67 +++++++++++++++++++++++++++++++++++++++------- src/routes/logs.js | 19 +++++++++++++ 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f348549..40a2327 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: pediatric-scribe: - image: danielonyejesi/pediatric-ai-scribe-v3:v1.8 + image: danielonyejesi/pediatric-ai-scribe-v3:v1.9 ports: - "3552:3000" env_file: diff --git a/public/js/app.js b/public/js/app.js index 2935bd9..7629ff7 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2,6 +2,28 @@ // APP.JS — Core utilities, tabs, model selector, helpers // ============================================================ +// ── Client-side error logging ────────────────────────────── +(function() { + function sendError(data) { + try { + var token = localStorage.getItem('ped_scribe_token') || ''; + navigator.sendBeacon('/api/logs/client-error', new Blob( + [JSON.stringify(data)], { type: 'application/json' } + )); + } catch(e) {} + } + window.onerror = function(msg, src, line, col, err) { + // Ignore errors from browser extensions + if (src && src.indexOf('moz-extension') !== -1) return; + if (src && src.indexOf('chrome-extension') !== -1) return; + sendError({ type: 'uncaught', message: String(msg), source: src, line: line, col: col, stack: err && err.stack }); + }; + window.addEventListener('unhandledrejection', function(e) { + var msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled promise rejection'; + sendError({ type: 'unhandledrejection', message: msg, stack: e.reason && e.reason.stack }); + }); +})(); + document.addEventListener('DOMContentLoaded', function() { // --- TAB NAVIGATION --- @@ -179,17 +201,44 @@ function speakText(elementId) { if (!el) return; var text = (el.innerText || el.textContent).trim(); if (!text) { showToast('Nothing to read', 'error'); return; } - if (!('speechSynthesis' in window)) { showToast('TTS not supported', 'error'); return; } - window.speechSynthesis.cancel(); - var utter = new SpeechSynthesisUtterance(text); - utter.rate = 0.9; - utter.onend = function() { stopReading(); }; - utter.onerror = function() { stopReading(); }; + currentlyReadingId = elementId; var btn = findReadButton(elementId); - if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; } - window.speechSynthesis.speak(utter); - showToast('Reading — click Stop to end', 'info'); + if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Loading...'; } + + fetch('/api/text-to-speech', { + method: 'POST', + headers: window.getAuthHeaders ? window.getAuthHeaders() : { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: text }) + }) + .then(function(r) { + if (!r.ok) throw new Error('TTS request failed (' + r.status + ')'); + return r.blob(); + }) + .then(function(blob) { + var url = URL.createObjectURL(blob); + currentAudio = new Audio(url); + currentAudio.onended = function() { URL.revokeObjectURL(url); stopReading(); }; + currentAudio.onerror = function() { URL.revokeObjectURL(url); stopReading(); showToast('Audio playback error', 'error'); }; + currentAudio.play(); + if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; } + showToast('Reading aloud (Adam/ElevenLabs)', 'info'); + }) + .catch(function(err) { + stopReading(); + // Fallback to browser TTS if ElevenLabs fails + if ('speechSynthesis' in window) { + var utter = new SpeechSynthesisUtterance(text); + utter.rate = 0.9; + utter.onend = function() { stopReading(); }; + currentlyReadingId = elementId; + if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; } + window.speechSynthesis.speak(utter); + showToast('ElevenLabs unavailable — using browser voice', 'info'); + } else { + showToast('Read aloud failed: ' + err.message, 'error'); + } + }); } function stopReading() { diff --git a/src/routes/logs.js b/src/routes/logs.js index fee0539..b1ef8fd 100644 --- a/src/routes/logs.js +++ b/src/routes/logs.js @@ -32,4 +32,23 @@ router.get('/logs/access', authMiddleware, async function(req, res) { } catch (err) { res.status(500).json({ error: err.message }); } }); +// Client-side error receiver — no auth required (sendBeacon can't send auth headers) +router.post('/logs/client-error', function(req, res) { + try { + var e = req.body || {}; + console.error('[CLIENT ERROR]', JSON.stringify({ + type: e.type, + message: e.message, + source: e.source, + line: e.line, + col: e.col, + stack: e.stack, + ip: req.ip, + ua: req.headers['user-agent'], + time: new Date().toISOString() + })); + res.status(204).end(); + } catch(err) { res.status(204).end(); } +}); + module.exports = router;