From bafd2194aba55ff000fcbfc9e465467ff5278baa Mon Sep 17 00:00:00 2001 From: Russell Date: Tue, 11 Nov 2025 09:56:57 -0500 Subject: [PATCH] - Add download button next to Play button for all messages (#33) * Add download button for TTS audio - Add download button next to Play button for all messages - Button is initially hidden and appears after TTS audio is generated - Works for both manual play and auto-play modes - Works for both regular and streaming messages - Handles cached audio properly - Download filename includes message ID and voice name * Refactor: Extract download button logic into helper function - Create enableDownloadButton() helper to eliminate code duplication - Replace 4 identical blocks (56 lines) with 4 function calls (4 lines) - Improves maintainability and follows DRY principle - Handles both cached and fresh audio in both speakText functions * Remove hardcoded voice fallbacks, use API or empty list - Remove hardcoded voice options from HTML dropdown - Remove all fallbacks to default voices (tts-1:onyx) - If voices API fails, leave dropdown empty instead of falling back - localStorage persistence for voice selection already implemented - Voices API caching already working (1-minute cache like models) - Voice selection now purely driven by API response * Fix: Make download button visible after TTS audio loads - Add download button to previous_messages handler (was missing) - Change display from "" to "inline-block" for visibility - Download button now appears properly after TTS processes * Add debug logging for download button issue - Add console.log to trace enableDownloadButton execution - Change === to == for messageId comparison (handle type coercion) - Log wrapper status, button status, and ID matching - This will help identify why download button doesn't appear * Remove debug logging, keep type coercion fix - Remove console.log statements now that issue is identified - Keep == comparison (was the actual fix) - Add comment explaining why == instead of === - dataset.messageId is string, messageId param is number --------- Co-authored-by: Claude --- templates/chat.html | 81 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/templates/chat.html b/templates/chat.html index f58f057..eeb848f 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -33,12 +33,7 @@
@@ -161,7 +156,7 @@ function syncInputsAndQueryString() { // Get current values const currentUsername = usernameInputDesktop?.value || username || "guest"; const currentModel = modelSelectDesktop.value; - const currentVoice = voiceSelectDesktop.value || 'tts-1:onyx'; + const currentVoice = voiceSelectDesktop.value; // Update global username variable username = currentUsername; @@ -261,11 +256,13 @@ document.addEventListener('DOMContentLoaded', (event) => { } }); - // Set initial value from URL or default to first option + // Set initial value from URL, localStorage, or first available option const urlParams = new URLSearchParams(window.location.search); - const initialVoice = urlParams.get("voice") || localStorage.getItem('selectedVoice') || voiceSelectDesktop.options[0]?.value || "tts-1:onyx"; - voiceSelectDesktop.value = initialVoice; - if (voiceSelectMobile) voiceSelectMobile.value = initialVoice; + const initialVoice = urlParams.get("voice") || localStorage.getItem('selectedVoice') || voiceSelectDesktop.options[0]?.value; + if (initialVoice) { + voiceSelectDesktop.value = initialVoice; + if (voiceSelectMobile) voiceSelectMobile.value = initialVoice; + } } // Memoization with localStorage (1-minute cache) @@ -322,9 +319,9 @@ document.addEventListener('DOMContentLoaded', (event) => { }) .catch(error => { console.error("Error fetching voices:", error); - // Fallback to default voice if fetch fails - voiceSelectDesktop.innerHTML = ''; - if (voiceSelectMobile) voiceSelectMobile.innerHTML = ''; + // Leave dropdown empty if API fails + voiceSelectDesktop.innerHTML = ''; + if (voiceSelectMobile) voiceSelectMobile.innerHTML = ''; }); } @@ -339,7 +336,7 @@ document.addEventListener('DOMContentLoaded', (event) => { // Set initial model, voice, and username from URL, localStorage, or defaults const initialModel = urlParams.get("model") || storedModel || "None"; - const initialVoice = urlParams.get("voice") || storedVoice || "tts-1:onyx"; + const initialVoice = urlParams.get("voice") || storedVoice || null; const initialUsername = username; // Already set to URL param or "guest" modelSelectDesktop.value = initialModel; @@ -541,6 +538,24 @@ socket.on('update_room_list', function(updatedRoom) { } }); +// Helper function to enable and wire download button +function enableDownloadButton(messageId, playButton, audioUrl, voice) { + const messageWrapper = playButton.closest('.message-wrapper'); + if (!messageWrapper) return; + + const downloadButton = messageWrapper.querySelector('.tts-download-button'); + // Use == instead of === because dataset values are strings, messageId might be number + if (downloadButton && downloadButton.dataset.messageId == messageId) { + downloadButton.style.display = "inline-block"; + downloadButton.onclick = () => { + const link = document.createElement('a'); + link.href = audioUrl; + link.download = `tts-${messageId}-${voice}.mp3`; + link.click(); + }; + } +} + // Function to read text using TTS (for manual button clicks) async function speakText(text, playButton, messageId) { console.log("speakText called with:", {text: text, messageId, autoPlay: autoPlayTTS}); @@ -557,6 +572,7 @@ async function speakText(text, playButton, messageId) { // Check if the audio is already cached if (audioCache[cacheKey]) { const audio = audioCache[cacheKey]; + enableDownloadButton(messageId, playButton, audio.src, voice); toggleAudioPlayback(audio, playButton); return; } @@ -590,6 +606,9 @@ async function speakText(text, playButton, messageId) { // Cache the audio only after it is successfully created audioCache[cacheKey] = audio; + // Enable and show download button + enableDownloadButton(messageId, playButton, audioUrl, voice); + // Enable button and change text to "Pause" playButton.disabled = false; toggleAudioPlayback(audio, playButton); @@ -627,7 +646,9 @@ async function speakTextQueued(text, playButton, messageId) { // Check if audio is cached if (audioCache[cacheKey]) { - playAudio(audioCache[cacheKey]); + const audio = audioCache[cacheKey]; + enableDownloadButton(messageId, playButton, audio.src, voice); + playAudio(audio); return; } @@ -655,6 +676,10 @@ async function speakTextQueued(text, playButton, messageId) { const audio = new Audio(audioUrl); audio.playbackRate = 0.9; audioCache[cacheKey] = audio; + + // Enable and show download button + enableDownloadButton(messageId, playButton, audioUrl, voice); + playAudio(audio); }) .catch(reject); @@ -828,6 +853,14 @@ socket.on("chat_message", (data) => { playButton.onclick = () => speakText(data.content, playButton, data.id); buttonContainer.appendChild(playButton); + // Create the download button for TTS audio (hidden initially) + const downloadButton = document.createElement("button"); + downloadButton.textContent = "Download"; + downloadButton.className = "tts-download-button"; + downloadButton.style.display = "none"; + downloadButton.dataset.messageId = data.id; + buttonContainer.appendChild(downloadButton); + messageWrapper.appendChild(buttonContainer); } @@ -919,6 +952,14 @@ socket.on("previous_messages", (data) => { playButton.onclick = () => speakText(data.content, playButton, data.id); buttonContainer.appendChild(playButton); + // Create the download button for TTS audio (hidden initially) + const downloadButton = document.createElement("button"); + downloadButton.textContent = "Download"; + downloadButton.className = "tts-download-button"; + downloadButton.style.display = "none"; + downloadButton.dataset.messageId = data.id; + buttonContainer.appendChild(downloadButton); + messageWrapper.appendChild(buttonContainer); messageWrapper.appendChild(newMessage); @@ -1065,6 +1106,14 @@ socket.on("message_chunk", (data) => { }; buttonContainer.appendChild(playButton); + // Create the download button for TTS audio (hidden initially) + const downloadButton = document.createElement("button"); + downloadButton.textContent = "Download"; + downloadButton.className = "tts-download-button"; + downloadButton.style.display = "none"; + downloadButton.dataset.messageId = data.id; + buttonContainer.appendChild(downloadButton); + // Insert the button container at the beginning of the message wrapper (before header and content) messageWrapper.insertBefore(buttonContainer, messageWrapper.firstChild);