feat: Restore TTS Anything modal with speed slider and voice selection

This commit is contained in:
Russell Ballestrini 2025-06-30 22:17:17 -04:00
parent d9185376e7
commit d327d98f77

398
src/ui.js
View file

@ -875,6 +875,8 @@ export async function openHermesModal() {
} else if (msg.role === 'assistant') {
const parsedContent = marked.parse(msg.content);
messageDiv.innerHTML = `<p><strong>AI:</strong> ${parsedContent}</p>`;
// 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 = `<p><strong>🤖 Hermes:</strong> ${introText}</p>`;
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 = `
<header>
<h3>Text to Speech</h3>
<button onclick="this.closest('dialog').remove()" style="float: right; background: none; border: none; font-size: 20px; cursor: pointer;">&times;</button>
</header>
<div>
<textarea id="tts-modal-text" placeholder="Enter text to convert to speech..." style="width: 100%; height: 120px; margin: 10px 0; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"></textarea>
<div style="display: flex; gap: 10px; align-items: center; margin: 10px 0;">
<label>Voice:</label>
<select id="tts-modal-voice" style="flex: 1; padding: 4px;">
<option value="alloy">Alloy</option>
<option value="echo">Echo</option>
<option value="fable">Fable</option>
<option value="onyx">Onyx</option>
<option value="nova">Nova</option>
<option value="shimmer">Shimmer</option>
</select>
</div>
<button id="tts-generate-btn" style="width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">Generate Speech</button>
<div id="tts-modal-result" style="margin: 15px 0;"></div>
</div>
`;
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 = '<em>Converting to speech...</em>';
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 = '<strong>Error:</strong> ' + 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);
}