Feat: restore ElevenLabs Adam voice for read aloud with browser TTS fallback; add client-side error logging

This commit is contained in:
Daniel Onyejesi 2026-03-22 12:27:17 -04:00
parent e74c0ccb0c
commit 93338567e8
3 changed files with 78 additions and 10 deletions

View file

@ -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:

View file

@ -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 = '<i class="fas fa-stop"></i> Stop'; }
window.speechSynthesis.speak(utter);
showToast('Reading — click Stop to end', 'info');
if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 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 = '<i class="fas fa-stop"></i> 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 = '<i class="fas fa-stop"></i> Stop'; }
window.speechSynthesis.speak(utter);
showToast('ElevenLabs unavailable — using browser voice', 'info');
} else {
showToast('Read aloud failed: ' + err.message, 'error');
}
});
}
function stopReading() {

View file

@ -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;