feat: Restore TTS Anything modal with speed slider and voice selection
This commit is contained in:
parent
d9185376e7
commit
d327d98f77
1 changed files with 315 additions and 83 deletions
398
src/ui.js
398
src/ui.js
|
|
@ -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;">×</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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue