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 = `
-
-
-
-
-
-
-
-
-
-
- `;
-
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);
}