Fix: revert speakText to sync speechSynthesis; robust copyText fallback

- speakText: revert async ElevenLabs fetch chain — browser requires
  speechSynthesis.speak() to be called synchronously within user gesture;
  async .catch() context caused silent failure across browsers
- copyText: fix fallback that always showed 'Copied!' even when it failed;
  now shows real error message, uses selectNodeContents for accuracy,
  handles missing clipboard API gracefully
This commit is contained in:
Daniel Onyejesi 2026-03-21 21:31:48 -04:00
parent 3ab109b84a
commit 12083a5ed1

View file

@ -117,18 +117,41 @@ function showToast(message, type) {
function copyText(elementId) { function copyText(elementId) {
var el = document.getElementById(elementId); var el = document.getElementById(elementId);
if (!el) return; if (!el) return;
var text = el.innerText || el.textContent; var text = el.innerText || el.textContent || '';
navigator.clipboard.writeText(text).then(function() { if (!text.trim()) { showToast('Nothing to copy', 'error'); return; }
showToast('Copied!', 'success');
}).catch(function() { // Try modern clipboard API (requires HTTPS or localhost)
var range = document.createRange(); if (navigator.clipboard && navigator.clipboard.writeText) {
range.selectNode(el); navigator.clipboard.writeText(text).then(function() {
window.getSelection().removeAllRanges(); showToast('Copied!', 'success');
window.getSelection().addRange(range); }).catch(function(err) {
document.execCommand('copy'); // Fallback: select + execCommand
window.getSelection().removeAllRanges(); try {
showToast('Copied!', 'success'); var range = document.createRange();
}); range.selectNodeContents(el);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
var ok = document.execCommand('copy');
window.getSelection().removeAllRanges();
showToast(ok ? 'Copied!' : 'Copy failed — please select text manually', ok ? 'success' : 'error');
} catch (e) {
showToast('Copy not supported in this browser', 'error');
}
});
} else {
// No clipboard API — use execCommand directly
try {
var range = document.createRange();
range.selectNodeContents(el);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
var ok = document.execCommand('copy');
window.getSelection().removeAllRanges();
showToast(ok ? 'Copied!' : 'Copy failed — please select text manually', ok ? 'success' : 'error');
} catch (e) {
showToast('Copy not supported in this browser', 'error');
}
}
} }
// Speak / Stop // Speak / Stop
@ -142,41 +165,17 @@ function speakText(elementId) {
if (!el) return; if (!el) return;
var text = (el.innerText || el.textContent).trim(); var text = (el.innerText || el.textContent).trim();
if (!text) { showToast('Nothing to read', 'error'); return; } 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; currentlyReadingId = elementId;
var btn = findReadButton(elementId); var btn = findReadButton(elementId);
if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = '<i class="fas fa-stop"></i> Stop'; } if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = '<i class="fas fa-stop"></i> Stop'; }
window.speechSynthesis.speak(utter);
// Try ElevenLabs backend first showToast('Reading — click Stop to end', 'info');
var token = window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || '';
fetch('/api/text-to-speech', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ text: text })
})
.then(function(r) {
if (!r.ok) throw new Error('not configured');
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(); };
currentAudio.play();
showToast('Reading — click Stop to end', 'info');
})
.catch(function() {
// Fallback to native speech synthesis
if (!('speechSynthesis' in window)) { stopReading(); 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(); };
window.speechSynthesis.speak(utter);
showToast('Reading — click Stop to end', 'info');
});
} }
function stopReading() { function stopReading() {