tts: queued/loading indicator on play button

Auto-play TTS used to show zero feedback between message arrival and
audio start — users waited blind. Now the play button shows:

  Queued + pulsing dot  — message is waiting in line
  Loading + spinner     — actively fetching audio from speech service
  Pause                 — audio is actually playing

Manual Play also uses the same spinner instead of the old 'Streaming...'
text-only state. Indicator clears on play, pause, end, error, and when
auto-play is toggled off.
This commit is contained in:
russell@unturf.com 2026-06-03 11:19:19 -04:00
parent d405ebc6f9
commit adc4fd79d2
No known key found for this signature in database
2 changed files with 78 additions and 1 deletions

View file

@ -1392,3 +1392,39 @@ a:hover {
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
/* TTS play-button states: queued (waiting in line) loading (fetching) playing.
Gives auto-play users a visible signal during the gap between message arrival
and audio start, so the wait doesn't feel like a hang. */
.tts-queued {
cursor: wait !important;
opacity: 0.85;
}
.tts-queued::after {
content: "•";
display: inline-block;
margin-left: 6px;
animation: ttsPulse 1.2s ease-in-out infinite;
}
.tts-loading {
cursor: progress !important;
}
.tts-loading::after {
content: "";
display: inline-block;
width: 10px;
height: 10px;
margin-left: 8px;
vertical-align: -1px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: ttsSpin 0.7s linear infinite;
}
@keyframes ttsPulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
@keyframes ttsSpin {
to { transform: rotate(360deg); }
}

View file

@ -1207,7 +1207,8 @@ async function speakText(text, playButton, messageId) {
}
// Set button to streaming state
playButton.textContent = "Streaming...";
playButton.textContent = "Loading";
playButton.classList.add("tts-loading");
playButton.disabled = true;
// Use streaming TTS
@ -1215,6 +1216,7 @@ async function speakText(text, playButton, messageId) {
const audio = result.audio;
// Enable button immediately and start playback
playButton.classList.remove("tts-loading");
playButton.disabled = false;
toggleAudioPlayback(audio, playButton);
@ -1234,6 +1236,7 @@ async function speakText(text, playButton, messageId) {
}
} catch (error) {
console.error('Error in TTS:', error);
playButton.classList.remove("tts-loading");
playButton.textContent = "Play"; // Reset button text on error
playButton.disabled = false;
}
@ -1251,13 +1254,29 @@ async function speakTextQueued(text, playButton, messageId) {
function bindLifecycle(audio) {
currentQueuedAudio = audio;
audio.onplay = () => {
playButton.classList.remove("tts-queued", "tts-loading");
playButton.disabled = false;
playButton.textContent = "Pause";
currentAudio = audio;
currentAudio.playButton = playButton;
};
audio.onpause = () => {
if (!audio.ended) playButton.textContent = "Play";
};
audio.onended = () => {
console.log("TTS finished for:", messageId);
playButton.classList.remove("tts-queued", "tts-loading");
playButton.disabled = false;
playButton.textContent = "Play";
currentQueuedAudio = null;
resolve();
};
audio.onerror = () => {
console.error("TTS audio error for:", messageId);
playButton.classList.remove("tts-queued", "tts-loading");
playButton.disabled = false;
playButton.textContent = "Play";
currentQueuedAudio = null;
reject(new Error("Audio playback failed"));
};
@ -1319,6 +1338,9 @@ async function speakTextQueued(text, playButton, messageId) {
audio.play().catch(reject);
}
} catch (error) {
playButton.classList.remove("tts-queued", "tts-loading");
playButton.disabled = false;
playButton.textContent = "Play";
reject(error);
}
});
@ -1328,6 +1350,11 @@ async function speakTextQueued(text, playButton, messageId) {
function queueTTS(text, playButton, messageId) {
ttsQueue.push({ text, playButton, messageId });
console.log("Added to TTS queue:", messageId, "Queue length:", ttsQueue.length);
// Immediately show "Queued" so users see TTS is incoming even before fetch starts.
playButton.classList.remove("tts-loading");
playButton.classList.add("tts-queued");
playButton.disabled = true;
playButton.textContent = "Queued";
processNextTTS();
}
@ -1342,6 +1369,12 @@ function processNextTTS() {
currentQueuedMessageId = messageId;
console.log("Processing TTS from queue:", messageId);
// Swap "Queued" indicator for active "Loading" spinner while we fetch audio.
playButton.classList.remove("tts-queued");
playButton.classList.add("tts-loading");
playButton.disabled = true;
playButton.textContent = "Loading";
// Use non-blocking async processing
speakTextQueued(text, playButton, messageId)
.then(() => {
@ -1413,6 +1446,14 @@ function toggleAutoPlayTTS() {
// If turning off, clear the queue and stop current audio
if (!autoPlayTTS) {
console.log("Clearing TTS queue, had", ttsQueue.length, "items");
// Reset any buttons that were showing the Queued/Loading indicator.
ttsQueue.forEach((item) => {
if (item.playButton) {
item.playButton.classList.remove("tts-queued", "tts-loading");
item.playButton.disabled = false;
item.playButton.textContent = "Play";
}
});
ttsQueue = [];
isPlayingTTS = false;