From d327d98f7723c2bb8660962350cad2c4ddc83ce4 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Mon, 30 Jun 2025 22:17:17 -0400 Subject: [PATCH] feat: Restore TTS Anything modal with speed slider and voice selection --- src/ui.js | 398 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 315 insertions(+), 83 deletions(-) diff --git a/src/ui.js b/src/ui.js index f11f9a2..40079c6 100644 --- a/src/ui.js +++ b/src/ui.js @@ -875,6 +875,8 @@ export async function openHermesModal() { } else if (msg.role === 'assistant') { const parsedContent = marked.parse(msg.content); messageDiv.innerHTML = `

AI: ${parsedContent}

`; + // Add TTS button to AI messages in history + addTTSButtonToMessage(messageDiv, msg.content); } messageDiv.appendChild(deleteBtn); @@ -951,6 +953,9 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga // Cache the generated introduction localStorage.setItem(cacheKey, generatedIntro); + // Add TTS button to the generated introduction + addTTSButtonToMessage(introDiv, generatedIntro); + chatBox.scrollTop = chatBox.scrollHeight; } catch (error) { @@ -969,9 +974,89 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga introDiv.style.cssText = 'position: relative; margin-bottom: 10px; padding: 10px; border-radius: 5px; background: rgba(0,100,200,0.1); border-left: 4px solid #0066cc;'; introDiv.innerHTML = `

🤖 Hermes: ${introText}

`; chatBox.appendChild(introDiv); + + // Add TTS button to introduction + addTTSButtonToMessage(introDiv, introText); + chatBox.scrollTop = chatBox.scrollHeight; } + // Add TTS button to any message + function addTTSButtonToMessage(messageDiv, messageText) { + const ttsContainer = document.createElement('div'); + ttsContainer.style.cssText = 'display: flex; gap: 8px; margin: 8px 0; align-items: center;'; + + const ttsBtn = document.createElement('button'); + ttsBtn.textContent = '🔊 Play Response'; + if (USE_CUSTOM_STYLING) { + ttsBtn.style.cssText = 'padding: 4px 8px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc; font-size: 12px;'; + } else { + ttsBtn.style.cssText = 'padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;'; + } + + const downloadBtn = document.createElement('button'); + downloadBtn.textContent = '💾 Download'; + downloadBtn.style.display = 'none'; + if (USE_CUSTOM_STYLING) { + downloadBtn.style.cssText = 'padding: 4px 8px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc; font-size: 12px;'; + } else { + downloadBtn.style.cssText = 'padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;'; + } + + let messageAudio = null; + let messageBlob = null; + + ttsBtn.onclick = async () => { + if (!messageAudio) { + ttsBtn.textContent = 'Processing...'; + ttsBtn.disabled = true; + + try { + const selectedVoice = voiceSelect.value; + const result = await speakText(messageText, selectedVoice, 0.9); + messageAudio = result.audio; + messageBlob = result.blob; + + ttsBtn.textContent = '⏸️ Pause'; + ttsBtn.disabled = false; + downloadBtn.style.display = 'inline-block'; + + messageAudio.play(); + + messageAudio.onended = () => { + ttsBtn.textContent = '🔊 Play Response'; + }; + + } catch (error) { + console.error('Error generating TTS:', error); + ttsBtn.textContent = '🔊 Play Response'; + ttsBtn.disabled = false; + } + } else { + if (messageAudio.paused) { + messageAudio.play(); + ttsBtn.textContent = '⏸️ Pause'; + } else { + messageAudio.pause(); + ttsBtn.textContent = '▶️ Resume'; + } + } + }; + + downloadBtn.onclick = () => { + if (messageBlob) { + const a = document.createElement('a'); + a.href = URL.createObjectURL(messageBlob); + a.download = `hermes-response-${Date.now()}.mp3`; + a.click(); + } + }; + + ttsContainer.appendChild(ttsBtn); + ttsContainer.appendChild(downloadBtn); + messageDiv.appendChild(ttsContainer); + } + // Show the modal first, then load intro asynchronously document.body.appendChild(modal); @@ -1075,7 +1160,7 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga chatBox.scrollTop = chatBox.scrollHeight; } - // Add delete button to messages + // Add delete button and TTS button to messages [userDiv, aiDiv].forEach((div, index) => { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '×'; @@ -1088,6 +1173,11 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga } }; div.appendChild(deleteBtn); + + // Add TTS button to AI response + if (index === 1) { // aiDiv + addTTSButtonToMessage(div, response); + } }); } catch (error) { @@ -1116,96 +1206,238 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga // TTS Modal functionality export function openTTSModal() { + // Check if Hermes modal is open to set appropriate z-index + const hermesModal = document.getElementById('hermes-modal'); + const zIndex = hermesModal ? '2001' : '1001'; + const modal = document.createElement('dialog'); - modal.id = 'tts-modal'; modal.open = true; + modal.style.zIndex = zIndex; + modal.style.position = 'fixed'; - // Modal styling - modal.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 90%; - max-width: 500px; - border: none; - border-radius: 8px; - padding: 20px; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - z-index: 3000; - `; - + if (USE_CUSTOM_STYLING) { + modal.style.cssText += ` + max-width: 720px; + border: none; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + background: white; + `; + } + const article = document.createElement('article'); - article.innerHTML = ` -
-

Text to Speech

- -
-
- -
- - -
- -
-
- `; - modal.appendChild(article); + + const header = document.createElement('header'); + if (USE_CUSTOM_STYLING) { + header.style.cssText = ` + background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 16px 20px; + margin: -1em -1em 1em -1em; + display: flex; + justify-content: space-between; + align-items: center; + `; + } else { + header.style.cssText = ` + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + margin: -1em -1em 1em -1em; + `; + } + article.appendChild(header); + + const h1 = document.createElement('h1'); + h1.textContent = 'TTS Anything!'; + if (USE_CUSTOM_STYLING) { + h1.style.cssText = ` + margin: 0; + font-family: 'ChunkFiveRegular', monospace; + font-size: 20px; + `; + } + header.appendChild(h1); + + const closeButton = document.createElement('button'); + closeButton.textContent = 'X'; + if (USE_CUSTOM_STYLING) { + closeButton.style.cssText = ` + background: none; + border: none; + color: white; + font-size: 24px; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + `; + } else { + closeButton.style.cssText = ` + background: var(--background-color); + border: 1px solid var(--border-color); + color: var(--color); + font-size: 18px; + cursor: pointer; + padding: 4px; + width: 28px; + height: 28px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.2s; + `; + closeButton.onmouseenter = () => closeButton.style.opacity = '0.7'; + closeButton.onmouseleave = () => closeButton.style.opacity = '1'; + } + closeButton.onclick = () => document.body.removeChild(modal); + header.appendChild(closeButton); + + const textArea = document.createElement('textarea'); + textArea.style.width = '100%'; + textArea.style.height = '240px'; + textArea.placeholder = 'text-to-speech: write any message & have the artificial intelligence speak it!'; + article.appendChild(textArea); + + // Voice selection + const voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']; + const voiceSelection = document.createElement('div'); + if (USE_CUSTOM_STYLING) { + voiceSelection.style.cssText = ` + display: flex; + gap: 10px; + margin: 16px 0; + `; + } + voices.forEach((voice) => { + const label = document.createElement('label'); + if (USE_CUSTOM_STYLING) { + label.style.cssText = ` + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + `; + } else { + label.style.display = 'inline-block'; + label.style.marginRight = '10px'; + } + const radio = document.createElement('input'); + radio.type = 'radio'; + radio.name = 'tts-voice'; + radio.value = voice; + if (voice === 'alloy') radio.checked = true; + label.appendChild(radio); + label.appendChild(document.createTextNode(voice)); + voiceSelection.appendChild(label); + }); + article.appendChild(voiceSelection); + + // Speed selection + const speedLabel = document.createElement('label'); + const speedValue = document.createElement('span'); + speedValue.textContent = '0.9'; - // Add generate button functionality - const generateBtn = modal.querySelector('#tts-generate-btn'); - generateBtn.onclick = async () => { - const text = modal.querySelector('#tts-modal-text').value.trim(); - const voice = modal.querySelector('#tts-modal-voice').value; - const resultDiv = modal.querySelector('#tts-modal-result'); - - if (!text) { - alert('Please enter some text first!'); - return; - } - - generateBtn.disabled = true; - generateBtn.textContent = 'Generating...'; - resultDiv.innerHTML = 'Converting to speech...'; - - try { - const result = await speakText(text, voice, 1.0); - const audioControls = document.createElement('div'); - audioControls.style.cssText = 'margin: 10px 0;'; - - const playButton = document.createElement('button'); - playButton.textContent = '▶️ Play'; - playButton.style.cssText = 'margin: 2px; padding: 6px 12px;'; - playButton.onclick = () => result.audio.play(); - - const pauseButton = document.createElement('button'); - pauseButton.textContent = '⏸️ Pause'; - pauseButton.style.cssText = 'margin: 2px; padding: 6px 12px;'; - pauseButton.onclick = () => result.audio.pause(); - - audioControls.appendChild(playButton); - audioControls.appendChild(pauseButton); - - resultDiv.innerHTML = ''; - resultDiv.appendChild(result.audio); - resultDiv.appendChild(audioControls); - } catch (error) { - resultDiv.innerHTML = 'Error: ' + error.message; - } finally { - generateBtn.disabled = false; - generateBtn.textContent = 'Generate Speech'; - } + speedLabel.textContent = `Speed: ${speedValue.textContent}`; + const speedSlider = document.createElement('input'); + speedSlider.type = 'range'; + speedSlider.min = '0.25'; + speedSlider.max = '4.0'; + speedSlider.step = '0.05'; + speedSlider.value = '0.9'; + + speedSlider.oninput = () => { + speedValue.textContent = speedSlider.value; + speedLabel.textContent = `Speed: ${speedSlider.value}`; }; + article.appendChild(speedLabel); + article.appendChild(speedSlider); + + const playButton = document.createElement('button'); + playButton.textContent = 'Play Text'; + if (USE_CUSTOM_STYLING) { + playButton.style.cssText = ` + padding: 12px 24px; + background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + margin: 16px 10px 0 0; + `; + } + + let ttsAudio = null; + let ttsBlob = null; + let lastTTSInput = ''; + + playButton.onclick = async () => { + const currentText = textArea.value.trim(); + const selectedVoice = document.querySelector('input[name="tts-voice"]:checked').value; + const selectedSpeed = parseFloat(speedSlider.value); + + if (!currentText) return; + + if (!ttsAudio || lastTTSInput !== currentText) { + if (ttsAudio) ttsAudio.pause(); + playButton.textContent = 'Processing...'; + playButton.disabled = true; + lastTTSInput = currentText; + const result = await speakText(currentText, selectedVoice, selectedSpeed); + ttsAudio = result.audio; + ttsBlob = result.blob; + playButton.textContent = 'Pause Text'; + playButton.disabled = false; + + // Add download button if not exists + let downloadButton = article.querySelector('.download-btn'); + if (!downloadButton) { + downloadButton = document.createElement('button'); + downloadButton.className = 'download-btn'; + downloadButton.textContent = 'Download MP3'; + if (USE_CUSTOM_STYLING) { + downloadButton.style.cssText = ` + padding: 12px 24px; + background: #f5f5f5; + border: 1px solid #ccc; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + margin: 16px 0 0 10px; + `; + } + downloadButton.onclick = () => { + const a = document.createElement('a'); + a.href = URL.createObjectURL(ttsBlob); + a.download = `tts-${Date.now()}.mp3`; + a.click(); + }; + article.appendChild(downloadButton); + } + } + + if (ttsAudio.paused) { + ttsAudio.play(); + playButton.textContent = 'Pause Text'; + } else { + ttsAudio.pause(); + playButton.textContent = 'Play Text'; + } + + ttsAudio.onended = () => { + playButton.textContent = 'Play Text'; + }; + }; + + article.appendChild(playButton); document.body.appendChild(modal); }