diff --git a/src/translate-modal.js b/src/translate-modal.js new file mode 100644 index 0000000..5af7257 --- /dev/null +++ b/src/translate-modal.js @@ -0,0 +1,540 @@ +// Translation Modal functionality +import { initializeChunkFiveFont } from "./ui-themes.js"; +import { getUIText } from "./ui-translations.js"; +import { extractWebpageContent } from "./page-content.js"; +import { handleTranslation } from "./translation.js"; +import { SUPPORTED_LANGUAGES } from "./languages/index.js"; + +// Get USE_CUSTOM_STYLING from window or default +const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false; + +export function openTranslateModal() { + // Ensure ChunkFive font is loaded + initializeChunkFiveFont(); + + // Check if Hermes modal is open to set appropriate z-index + const hermesModal = document.getElementById("uncloseai-embedded-modal"); + const zIndex = hermesModal ? "2001" : "1001"; + + const modal = document.createElement("dialog"); + modal.style.zIndex = zIndex; + modal.style.position = "fixed"; + + if (USE_CUSTOM_STYLING) { + modal.style.cssText += ` + width: 95vw; + max-width: 800px; + max-height: 95vh; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border: none; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + background: white; + margin: 0; + overflow: auto; + `; + } else { + modal.style.cssText += ` + width: 95vw; + max-width: 800px; + max-height: 95vh; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + overflow: auto; + `; + } + + const article = document.createElement("article"); + modal.appendChild(article); + + const header = document.createElement("header"); + if (USE_CUSTOM_STYLING) { + header.style.cssText = ` + background: linear-gradient(45deg, #4CAF50 0%, #45a049 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.innerHTML = `uncloseai. ${getUIText("translateModalTitle")}`; + if (USE_CUSTOM_STYLING) { + h1.style.cssText = ` + margin: 0; + 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; + touch-action: manipulation; + `; + } 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; + touch-action: manipulation; + `; + closeButton.onmouseenter = () => (closeButton.style.opacity = "0.7"); + closeButton.onmouseleave = () => (closeButton.style.opacity = "1"); + } + closeButton.onclick = () => { + modal.close(); + document.body.removeChild(modal); + }; + header.appendChild(closeButton); + + // Translation mode tabs + const modeContainer = document.createElement("div"); + modeContainer.style.cssText = + "display: flex; gap: 10px; margin-bottom: 20px;"; + + const pageTab = document.createElement("button"); + pageTab.textContent = getUIText("translatePageTab"); + pageTab.setAttribute("data-i18n", "translatePageTab"); + pageTab.style.cssText = + "flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: #4CAF50; color: white; cursor: pointer;"; + + const customTab = document.createElement("button"); + customTab.textContent = getUIText("customTextTab"); + customTab.setAttribute("data-i18n", "customTextTab"); + customTab.style.cssText = + "flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: white; color: #4CAF50; cursor: pointer;"; + + modeContainer.appendChild(pageTab); + modeContainer.appendChild(customTab); + article.appendChild(modeContainer); + + // Content areas + const pageContent = document.createElement("div"); + const customContent = document.createElement("div"); + customContent.style.display = "none"; + + // Page translation content + const pageInfo = document.createElement("p"); + pageInfo.textContent = getUIText("translatePageInfo"); + pageInfo.setAttribute("data-i18n", "translatePageInfo"); + pageContent.appendChild(pageInfo); + + // Custom text content + const textArea = document.createElement("textarea"); + if (USE_CUSTOM_STYLING) { + textArea.style.cssText = ` + width: 100%; + height: 150px; + `; + } else { + textArea.style.cssText = ` + width: 100%; + height: 150px; + background: rgba(255,255,255,0.1); + color: inherit; + border: 1px solid rgba(128,128,128,0.3); + border-radius: 8px; + backdrop-filter: blur(10px); + padding: 0.5rem; + `; + } + textArea.placeholder = getUIText("translatePlaceholder"); + customContent.appendChild(textArea); + + article.appendChild(pageContent); + article.appendChild(customContent); + + // Language selection + const languageContainer = document.createElement("div"); + languageContainer.style.cssText = "margin: 20px 0;"; + + const languageLabel = document.createElement("label"); + languageLabel.textContent = getUIText("translateToLabel"); + languageLabel.setAttribute("data-i18n", "translateToLabel"); + languageLabel.style.display = "block"; + languageLabel.style.marginBottom = "8px"; + + const languageSelect = document.createElement("select"); + languageSelect.style.cssText = + "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"; + + Object.entries(SUPPORTED_LANGUAGES).forEach(([code, name]) => { + const option = document.createElement("option"); + option.value = code; + option.textContent = name; + languageSelect.appendChild(option); + }); + + languageContainer.appendChild(languageLabel); + languageContainer.appendChild(languageSelect); + article.appendChild(languageContainer); + + // Translate button + const translateButton = document.createElement("button"); + translateButton.textContent = getUIText("translateButton"); + translateButton.setAttribute("data-i18n", "translateButton"); + if (USE_CUSTOM_STYLING) { + translateButton.style.cssText = ` + padding: 12px 24px; + background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + margin: 16px 10px 0 0; + width: 100%; + `; + } else { + translateButton.style.cssText = + "width: 100%; padding: 12px; margin: 16px 0;"; + } + + // Result area + const resultContainer = document.createElement("div"); + resultContainer.style.cssText = "margin-top: 20px; display: none;"; + + const resultLabel = document.createElement("h3"); + resultLabel.textContent = "Translation:"; + + const resultArea = document.createElement("div"); + resultArea.style.cssText = + "border: 1px solid #ccc; border-radius: 4px; padding: 15px; max-height: 400px; overflow-y: auto; background: #f9f9f9; white-space: pre-wrap;"; + + resultContainer.appendChild(resultLabel); + resultContainer.appendChild(resultArea); + article.appendChild(resultContainer); + + article.appendChild(translateButton); + + // Tab switching + let isPageMode = true; + + pageTab.onclick = () => { + if (!isPageMode) { + isPageMode = true; + pageTab.style.background = "#4CAF50"; + pageTab.style.color = "white"; + customTab.style.background = "white"; + customTab.style.color = "#4CAF50"; + pageContent.style.display = "block"; + customContent.style.display = "none"; + translateButton.textContent = getUIText("translatePageButton"); + } + }; + + customTab.onclick = () => { + if (isPageMode) { + isPageMode = false; + customTab.style.background = "#4CAF50"; + customTab.style.color = "white"; + pageTab.style.background = "white"; + pageTab.style.color = "#4CAF50"; + customContent.style.display = "block"; + pageContent.style.display = "none"; + translateButton.textContent = getUIText("translateTextButton"); + } + }; + + // Translation logic + translateButton.onclick = async () => { + const targetLanguage = languageSelect.value; + const targetLanguageName = SUPPORTED_LANGUAGES[targetLanguage]; + + try { + translateButton.textContent = getUIText("processingText"); + translateButton.disabled = true; + resultContainer.style.display = "none"; + + // Get token count and estimated time for progress + const { countTokens } = await import("./token_estimator.js"); + const { estimateProcessingTime } = await import("./translation.js"); + const textToAnalyze = isPageMode + ? document.documentElement.outerHTML + : textArea.value.trim(); + const tokenInfo = countTokens(textToAnalyze); + const estimatedSeconds = estimateProcessingTime(tokenInfo.totalTokens); + + // Create progress indicator + const progressContainer = document.createElement("div"); + progressContainer.style.cssText = ` + margin: 20px 0; + padding: 20px; + border: 2px solid #4CAF50; + border-radius: 8px; + background: #f0fff0; + text-align: center; + `; + + const progressText = document.createElement("div"); + progressText.style.cssText = + "font-weight: bold; color: #4CAF50; margin-bottom: 10px;"; + progressText.textContent = getUIText("processingTokens", { tokens: tokenInfo.totalTokens }); + + const etaText = document.createElement("div"); + etaText.style.cssText = "color: #666; margin-bottom: 15px;"; + etaText.textContent = `Estimated time: ${estimatedSeconds} seconds`; + + const progressBar = document.createElement("div"); + progressBar.style.cssText = ` + width: 100%; + height: 20px; + background: #e0e0e0; + border-radius: 10px; + overflow: hidden; + margin-bottom: 10px; + `; + + const progressFill = document.createElement("div"); + progressFill.style.cssText = ` + height: 100%; + background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%); + width: 0%; + transition: width 0.5s ease; + border-radius: 10px; + `; + progressBar.appendChild(progressFill); + + const statusText = document.createElement("div"); + statusText.style.cssText = "color: #777; font-size: 0.9em;"; + statusText.textContent = "Initializing translation..."; + + progressContainer.appendChild(progressText); + progressContainer.appendChild(etaText); + progressContainer.appendChild(progressBar); + progressContainer.appendChild(statusText); + + // Insert progress before result container + resultContainer.parentNode.insertBefore( + progressContainer, + resultContainer, + ); + + // Start progress animation + const startTime = Date.now(); + const progressInterval = setInterval(() => { + const elapsed = Date.now() - startTime; + const progress = Math.min( + (elapsed / (estimatedSeconds * 1000)) * 100, + 95, + ); + progressFill.style.width = `${progress}%`; + + const remainingTime = Math.max( + 0, + estimatedSeconds - Math.floor(elapsed / 1000), + ); + etaText.textContent = + remainingTime > 0 + ? `Time remaining: ${remainingTime}s` + : "Almost done..."; + + if (elapsed < 3000) statusText.textContent = "Analyzing content..."; + else if (elapsed < estimatedSeconds * 500) + statusText.textContent = "Translating with Hermes AI..."; + else statusText.textContent = "Finalizing translation..."; + }, 500); + + let translatedText; + + if (isPageMode) { + translatedText = await translateCurrentPage(targetLanguage); + } else { + const customText = textArea.value.trim(); + if (!customText) { + alert("Please enter some text to translate!"); + return; + } + translatedText = await translateText(customText, targetLanguage); + } + + resultLabel.textContent = `Translation (${targetLanguageName}):`; + + // Check if the translated content looks like HTML + const isHTML = + translatedText.includes("<") && + translatedText.includes(">") && + (translatedText.includes(" + window.UNCLOSEAI_SKIP_INIT = true; + + `; + + if (translatedText.includes("") + ? translatedText.replace( + "", + `${scriptTag}`, + ) + : translatedText.replace( + "", + `${scriptTag}`, + ); + } else if (translatedText.includes("")) { + // Has head but no DOCTYPE + htmlWithBase = `\n${translatedText.replace("", `${scriptTag}`)}`; + } else { + // No DOCTYPE or head + htmlWithBase = `\n${scriptTag}${translatedText}`; + } + + previewWindow.document.write(htmlWithBase); + previewWindow.document.close(); + previewWindow.document.title = `Translation Preview (${targetLanguageName}) - AI Enabled`; + + // Add a notice about AI functionality only in preview window (wait for body to be available) + const addNotice = () => { + if ( + previewWindow && + previewWindow.document && + previewWindow.document.body + ) { + console.log("Adding AI notice to preview window"); + const notice = previewWindow.document.createElement("div"); + notice.style.cssText = + "position: fixed; top: 10px; right: 10px; background: #4CAF50; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; z-index: 10000; box-shadow: 0 2px 8px rgba(0,0,0,0.2);"; + notice.textContent = `🤖 AI features should work in ${targetLanguageName}!`; + previewWindow.document.body.appendChild(notice); + + // Auto-hide notice after 5 seconds + setTimeout(() => { + if (notice.parentNode) notice.parentNode.removeChild(notice); + }, 5000); + } else if (previewWindow) { + // Wait a bit and try again + setTimeout(addNotice, 100); + } + }; + + // Only add notice if we have a valid preview window + if (previewWindow) { + addNotice(); + } + } + + // Show raw HTML toggle in modal + resultArea.innerHTML = ` +
+ +
${translatedText.replace(//g, ">")}
+
+
+
✅ Preview Opened in New Window
+

The translated HTML page has opened in a new window with full AI functionality!

+ ${previewWindow ? "" : '

Note: Please allow popups for preview window

'} +
+ `; + resultArea.style.whiteSpace = "normal"; + } else { + resultArea.textContent = translatedText; + resultArea.style.whiteSpace = "pre-wrap"; + } + + resultContainer.style.display = "block"; + + // Complete progress bar + clearInterval(progressInterval); + progressFill.style.width = "100%"; + etaText.textContent = "Translation completed!"; + statusText.textContent = "✅ Ready to preview"; + + // Hide progress after a moment + setTimeout(() => { + if (progressContainer.parentNode) { + progressContainer.parentNode.removeChild(progressContainer); + } + }, 2000); + + // Scroll to result + resultContainer.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } catch (error) { + // Clean up progress on error + if (typeof progressInterval !== "undefined") { + clearInterval(progressInterval); + } + const progressContainer = document.querySelector( + '[style*="border: 2px solid #4CAF50"]', + ); + if (progressContainer && progressContainer.parentNode) { + progressContainer.parentNode.removeChild(progressContainer); + } + alert(`Translation failed: ${error.message}`); + } finally { + translateButton.textContent = isPageMode + ? "🌐 Translate Page" + : "🌐 Translate Text"; + translateButton.disabled = false; + } + }; + + document.body.appendChild(modal); + + // Use showModal() for proper mobile support and backdrop + modal.showModal(); + + // Close modal when clicking backdrop + modal.addEventListener("click", (e) => { + if (e.target === modal) { + modal.close(); + document.body.removeChild(modal); + } + }); +} diff --git a/src/tts-modal.js b/src/tts-modal.js new file mode 100644 index 0000000..b9a34b4 --- /dev/null +++ b/src/tts-modal.js @@ -0,0 +1,335 @@ +// TTS Modal functionality +import { initializeChunkFiveFont } from "./ui-themes.js"; +import { getUIText } from "./ui-translations.js"; +import { handleTTS, downloadAudio } from "./tts.js"; + +// Get USE_CUSTOM_STYLING from window or default +const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false; + +export function openTTSModal() { + // Ensure ChunkFive font is loaded + initializeChunkFiveFont(); + + // Check if Hermes modal is open to set appropriate z-index + const hermesModal = document.getElementById("uncloseai-embedded-modal"); + const zIndex = hermesModal ? "2001" : "1001"; + + const modal = document.createElement("dialog"); + modal.style.zIndex = zIndex; + modal.style.position = "fixed"; + + if (USE_CUSTOM_STYLING) { + modal.style.cssText += ` + width: 95vw; + max-width: 800px; + max-height: 95vh; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + padding: 0; + overflow: hidden; + `; + } else { + modal.style.cssText += ` + position: fixed; + inset: 0; + max-width: 100%; + max-height: 100%; + height: 100%; + width: 100%; + border: 0; + padding: 0; + background: var(--background-color); + color: var(--color); + `; + } + + document.body.appendChild(modal); + + const article = document.createElement("article"); + if (USE_CUSTOM_STYLING) { + article.style.cssText = ` + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + `; + } + + const header = document.createElement("header"); + if (USE_CUSTOM_STYLING) { + header.style.cssText = ` + background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + `; + } + + const h1 = document.createElement("h1"); + h1.innerHTML = `uncloseai. ${getUIText("ttsModalTitle")}`; + if (USE_CUSTOM_STYLING) { + h1.style.cssText = ` + margin: 0; + font-size: 24px; + `; + } + 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 = () => { + modal.close(); + document.body.removeChild(modal); + }; + header.appendChild(closeButton); + + const textArea = document.createElement("textarea"); + if (USE_CUSTOM_STYLING) { + textArea.style.cssText = ` + width: 100%; + height: 240px; + `; + } else { + textArea.style.cssText = ` + width: 100%; + height: 240px; + background: rgba(255,255,255,0.1); + color: inherit; + border: 1px solid rgba(128,128,128,0.3); + border-radius: 8px; + backdrop-filter: blur(10px); + padding: 0.5rem; + `; + } + textArea.placeholder = getUIText("ttsPlaceholder"); + 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; + flex-wrap: wrap; + gap: 10px; + margin: 16px 0; + `; + } + + voices.forEach((voice, index) => { + const label = document.createElement("label"); + if (USE_CUSTOM_STYLING) { + label.style.cssText = ` + display: flex; + align-items: center; + gap: 5px; + 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 (index === 0) radio.checked = true; + label.appendChild(radio); + label.appendChild(document.createTextNode(voice)); + voiceSelection.appendChild(label); + }); + + // Speed control + const speedControl = document.createElement("div"); + if (USE_CUSTOM_STYLING) { + speedControl.style.cssText = ` + margin: 16px 0; + display: flex; + align-items: center; + gap: 10px; + `; + } + + const speedLabel = document.createElement("label"); + const speedValue = document.createElement("span"); + speedValue.textContent = "1.0"; + + speedLabel.textContent = `${getUIText("speedLabel")} ${speedValue.textContent}`; + speedLabel.setAttribute("data-i18n", "speedLabel"); + const speedSlider = document.createElement("input"); + speedSlider.type = "range"; + speedSlider.min = "0.25"; + speedSlider.max = "4.0"; + speedSlider.step = "0.25"; + speedSlider.value = "1.0"; + speedSlider.oninput = () => { + speedValue.textContent = speedSlider.value; + speedLabel.textContent = `${getUIText("speedLabel")} ${speedSlider.value}`; + }; + + speedControl.appendChild(speedLabel); + speedControl.appendChild(speedSlider); + + const playButton = document.createElement("button"); + playButton.textContent = getUIText("playText"); + if (USE_CUSTOM_STYLING) { + playButton.style.cssText = ` + padding: 12px 24px; + background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 16px; + margin: 16px 0; + `; + } else { + playButton.style.cssText = ` + width: 100%; + padding: 0.75rem; + margin: 1rem 0; + `; + } + + const resultDiv = document.createElement("div"); + resultDiv.id = "tts-result"; + if (USE_CUSTOM_STYLING) { + resultDiv.style.cssText = ` + margin-top: 16px; + padding: 16px; + background: #f5f5f5; + border-radius: 6px; + display: none; + `; + } + + // Content container with padding + const contentContainer = document.createElement("div"); + if (USE_CUSTOM_STYLING) { + contentContainer.style.cssText = ` + padding: 20px; + overflow-y: auto; + flex: 1; + `; + } + contentContainer.appendChild(textArea); + contentContainer.appendChild(voiceSelection); + contentContainer.appendChild(speedControl); + contentContainer.appendChild(playButton); + contentContainer.appendChild(resultDiv); + + article.appendChild(header); + article.appendChild(contentContainer); + modal.appendChild(article); + + modal.showModal(); + + // Play button handler + playButton.onclick = async () => { + const text = textArea.value.trim(); + if (!text) { + alert(getUIText("pleaseEnterText")); + return; + } + + const voice = document.querySelector('input[name="tts-voice"]:checked').value; + const speed = parseFloat(speedSlider.value); + + try { + playButton.textContent = getUIText("processingText"); + playButton.disabled = true; + + const result = await handleTTS(text, voice, speed); + if (result.audio) { + resultDiv.style.display = "block"; + resultDiv.innerHTML = ""; + + const audio = new Audio(result.blobUrl); + audio.controls = true; + audio.style.width = "100%"; + resultDiv.appendChild(audio); + + const downloadBtn = document.createElement("button"); + downloadBtn.textContent = "💾 Download Audio"; + if (USE_CUSTOM_STYLING) { + downloadBtn.style.cssText = ` + margin-top: 8px; + padding: 8px 16px; + background: #4CAF50; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + } + downloadBtn.onclick = () => downloadAudio(result.blobUrl, "tts-audio.mp3"); + resultDiv.appendChild(downloadBtn); + + // Auto-play + audio.play(); + } + } catch (error) { + alert(getUIText("ttsFailed", { error: error.message })); + } finally { + playButton.textContent = getUIText("playText"); + playButton.disabled = false; + } + }; + + // Focus the textarea + setTimeout(() => { + textArea.focus(); + }, 100); + + // Expose globally + window.closeTTSModal = () => { + modal.close(); + document.body.removeChild(modal); + }; + + // Clean up when closed + modal.addEventListener("close", () => { + if (modal.parentNode) { + document.body.removeChild(modal); + } + }); +} \ No newline at end of file diff --git a/src/ui.js b/src/ui.js index d137b70..bf170c4 100644 --- a/src/ui.js +++ b/src/ui.js @@ -16,6 +16,8 @@ import { setUserLanguagePreference, } from "./ui-translations.js"; import { NATIVE_LANGUAGE_NAMES } from "./translation.js"; +import { openTTSModal } from "./tts-modal.js"; +import { openTranslateModal } from "./translate-modal.js"; // Refresh UI elements when language preference changes function refreshUILanguage() { @@ -3950,851 +3952,41 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga article.appendChild(inputArea); } -// TTS Modal functionality -export function openTTSModal() { - // Ensure ChunkFive font is loaded - initializeChunkFiveFont(); - - // Check if Hermes modal is open to set appropriate z-index - const hermesModal = document.getElementById("uncloseai-embedded-modal"); - const zIndex = hermesModal ? "2001" : "1001"; - - const modal = document.createElement("dialog"); - modal.style.zIndex = zIndex; - modal.style.position = "fixed"; - - if (USE_CUSTOM_STYLING) { - modal.style.cssText += ` - width: 95vw; - max-width: 800px; - max-height: 95vh; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border: none; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0,0,0,0.2); - background: white; - margin: 0; - overflow: auto; - `; - } else { - modal.style.cssText += ` - width: 95vw; - max-width: 800px; - max-height: 95vh; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - margin: 0; - overflow: auto; - `; - } - - const article = document.createElement("article"); - 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.innerHTML = `uncloseai. ${getUIText("ttsModalTitle")}`; - if (USE_CUSTOM_STYLING) { - h1.style.cssText = ` - margin: 0; - 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 = () => { - modal.close(); - document.body.removeChild(modal); - }; - header.appendChild(closeButton); - - const textArea = document.createElement("textarea"); - if (USE_CUSTOM_STYLING) { - textArea.style.cssText = ` - width: 100%; - height: 240px; - `; - } else { - textArea.style.cssText = ` - width: 100%; - height: 240px; - background: rgba(255,255,255,0.1); - color: inherit; - border: 1px solid rgba(128,128,128,0.3); - border-radius: 8px; - backdrop-filter: blur(10px); - padding: 0.5rem; - `; - } - textArea.placeholder = getUIText("ttsPlaceholder"); - 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; - flex-wrap: wrap; - gap: 10px; - margin: 16px 0; - `; - } else { - voiceSelection.style.cssText = ` - display: flex; - flex-wrap: wrap; - 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"; - - speedLabel.textContent = `${getUIText("speedLabel")} ${speedValue.textContent}`; - speedLabel.setAttribute("data-i18n", "speedLabel"); - 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 = `${getUIText("speedLabel")} ${speedSlider.value}`; - }; - - article.appendChild(speedLabel); - article.appendChild(speedSlider); - - const playButton = document.createElement("button"); - playButton.textContent = getUIText("playText"); - playButton.setAttribute("data-i18n", "playText"); - 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 = Number.parseFloat(speedSlider.value); - - if (!currentText) return; - - if (!ttsAudio || lastTTSInput !== currentText) { - if (ttsAudio) ttsAudio.pause(); - playButton.textContent = getUIText("processingText"); - playButton.disabled = true; - lastTTSInput = currentText; - const result = await speakText(currentText, selectedVoice, selectedSpeed); - ttsAudio = result.audio; - ttsBlob = result.blob; - playButton.disabled = false; - - // Auto-play the audio (handle browser auto-play policies) - console.log("TTS MP3 ready, attempting auto-play..."); - try { - const playPromise = ttsAudio.play(); - await playPromise; - console.log("✅ TTS audio auto-playing successfully"); - playButton.textContent = "Pause Text"; - } catch (error) { - console.log("❌ Auto-play prevented by browser policy:", error.message); - console.log("User will need to manually click play"); - playButton.textContent = "Play Text"; - // Don't throw error, just log it - user can manually click play - } - - // 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); - } - } else { - // Audio already exists, toggle play/pause - if (ttsAudio.paused) { - ttsAudio.play(); - playButton.textContent = "Pause Text"; +export function initializeSystem() { + // Check for skip init flag - allow partial initialization for preview windows + if (window.UNCLOSEAI_SKIP_INIT === true) { + console.log("uncloseai.js: Partial initialization for preview window"); + // Only create floating button in preview windows if not explicitly disabled + if (window.UNCLOSEAI_FLOATING_BUTTON !== false) { + console.log("uncloseai.js: Creating floating button in preview window"); + // Ensure DOM is ready before creating button + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", createFloatingAIButton); } else { - ttsAudio.pause(); - playButton.textContent = "Play Text"; + createFloatingAIButton(); } } + return; + } - ttsAudio.onended = () => { - playButton.textContent = "Play Text"; - }; - }; + console.log("uncloseai.js: Full initialization"); + initializeChatInterface(); - article.appendChild(playButton); - - document.body.appendChild(modal); - - // Use showModal() for proper mobile support and backdrop - modal.showModal(); - - // Close modal when clicking backdrop - modal.addEventListener("click", (e) => { - if (e.target === modal) { - modal.close(); - document.body.removeChild(modal); - } - }); -} - -// Open translation modal -export function openTranslateModal() { - // Ensure ChunkFive font is loaded - initializeChunkFiveFont(); - - // Check if Hermes modal is open to set appropriate z-index - const hermesModal = document.getElementById("uncloseai-embedded-modal"); - const zIndex = hermesModal ? "2001" : "1001"; - - const modal = document.createElement("dialog"); - modal.style.zIndex = zIndex; - modal.style.position = "fixed"; - - if (USE_CUSTOM_STYLING) { - modal.style.cssText += ` - width: 95vw; - max-width: 800px; - max-height: 95vh; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border: none; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0,0,0,0.2); - background: white; - margin: 0; - overflow: auto; - `; + // Only create floating button if not disabled + if (SHOW_FLOATING_BUTTON) { + console.log( + "uncloseai.js: SHOW_FLOATING_BUTTON is true, calling createFloatingAIButton()", + ); + createFloatingAIButton(); } else { - modal.style.cssText += ` - width: 95vw; - max-width: 800px; - max-height: 95vh; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - margin: 0; - overflow: auto; - `; + console.log( + "uncloseai.js: SHOW_FLOATING_BUTTON is false, not creating floating button.", + ); } - const article = document.createElement("article"); - modal.appendChild(article); + // Initialize class-based elements + initializeUncloseaiElements(); - const header = document.createElement("header"); - if (USE_CUSTOM_STYLING) { - header.style.cssText = ` - background: linear-gradient(45deg, #4CAF50 0%, #45a049 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.innerHTML = `uncloseai. ${getUIText("translateModalTitle")}`; - if (USE_CUSTOM_STYLING) { - h1.style.cssText = ` - margin: 0; - 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; - touch-action: manipulation; - `; - } 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; - touch-action: manipulation; - `; - closeButton.onmouseenter = () => (closeButton.style.opacity = "0.7"); - closeButton.onmouseleave = () => (closeButton.style.opacity = "1"); - } - closeButton.onclick = () => { - modal.close(); - document.body.removeChild(modal); - }; - header.appendChild(closeButton); - - // Translation mode tabs - const modeContainer = document.createElement("div"); - modeContainer.style.cssText = - "display: flex; gap: 10px; margin-bottom: 20px;"; - - const pageTab = document.createElement("button"); - pageTab.textContent = getUIText("translatePageTab"); - pageTab.setAttribute("data-i18n", "translatePageTab"); - pageTab.style.cssText = - "flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: #4CAF50; color: white; cursor: pointer;"; - - const customTab = document.createElement("button"); - customTab.textContent = getUIText("customTextTab"); - customTab.setAttribute("data-i18n", "customTextTab"); - customTab.style.cssText = - "flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: white; color: #4CAF50; cursor: pointer;"; - - modeContainer.appendChild(pageTab); - modeContainer.appendChild(customTab); - article.appendChild(modeContainer); - - // Content areas - const pageContent = document.createElement("div"); - const customContent = document.createElement("div"); - customContent.style.display = "none"; - - // Page translation content - const pageInfo = document.createElement("p"); - pageInfo.textContent = getUIText("translatePageInfo"); - pageInfo.setAttribute("data-i18n", "translatePageInfo"); - pageContent.appendChild(pageInfo); - - // Custom text content - const textArea = document.createElement("textarea"); - if (USE_CUSTOM_STYLING) { - textArea.style.cssText = ` - width: 100%; - height: 150px; - `; - } else { - textArea.style.cssText = ` - width: 100%; - height: 150px; - background: rgba(255,255,255,0.1); - color: inherit; - border: 1px solid rgba(128,128,128,0.3); - border-radius: 8px; - backdrop-filter: blur(10px); - padding: 0.5rem; - `; - } - textArea.placeholder = getUIText("translatePlaceholder"); - customContent.appendChild(textArea); - - article.appendChild(pageContent); - article.appendChild(customContent); - - // Language selection - const languageContainer = document.createElement("div"); - languageContainer.style.cssText = "margin: 20px 0;"; - - const languageLabel = document.createElement("label"); - languageLabel.textContent = getUIText("translateToLabel"); - languageLabel.setAttribute("data-i18n", "translateToLabel"); - languageLabel.style.display = "block"; - languageLabel.style.marginBottom = "8px"; - - const languageSelect = document.createElement("select"); - languageSelect.style.cssText = - "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"; - - Object.entries(SUPPORTED_LANGUAGES).forEach(([code, name]) => { - const option = document.createElement("option"); - option.value = code; - option.textContent = name; - languageSelect.appendChild(option); - }); - - languageContainer.appendChild(languageLabel); - languageContainer.appendChild(languageSelect); - article.appendChild(languageContainer); - - // Translate button - const translateButton = document.createElement("button"); - translateButton.textContent = getUIText("translateButton"); - translateButton.setAttribute("data-i18n", "translateButton"); - if (USE_CUSTOM_STYLING) { - translateButton.style.cssText = ` - padding: 12px 24px; - background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%); - color: white; - border: none; - border-radius: 8px; - cursor: pointer; - font-weight: bold; - margin: 16px 10px 0 0; - width: 100%; - `; - } else { - translateButton.style.cssText = - "width: 100%; padding: 12px; margin: 16px 0;"; - } - - // Result area - const resultContainer = document.createElement("div"); - resultContainer.style.cssText = "margin-top: 20px; display: none;"; - - const resultLabel = document.createElement("h3"); - resultLabel.textContent = "Translation:"; - - const resultArea = document.createElement("div"); - resultArea.style.cssText = - "border: 1px solid #ccc; border-radius: 4px; padding: 15px; max-height: 400px; overflow-y: auto; background: #f9f9f9; white-space: pre-wrap;"; - - resultContainer.appendChild(resultLabel); - resultContainer.appendChild(resultArea); - article.appendChild(resultContainer); - - article.appendChild(translateButton); - - // Tab switching - let isPageMode = true; - - pageTab.onclick = () => { - if (!isPageMode) { - isPageMode = true; - pageTab.style.background = "#4CAF50"; - pageTab.style.color = "white"; - customTab.style.background = "white"; - customTab.style.color = "#4CAF50"; - pageContent.style.display = "block"; - customContent.style.display = "none"; - translateButton.textContent = getUIText("translatePageButton"); - } - }; - - customTab.onclick = () => { - if (isPageMode) { - isPageMode = false; - customTab.style.background = "#4CAF50"; - customTab.style.color = "white"; - pageTab.style.background = "white"; - pageTab.style.color = "#4CAF50"; - customContent.style.display = "block"; - pageContent.style.display = "none"; - translateButton.textContent = getUIText("translateTextButton"); - } - }; - - // Translation logic - translateButton.onclick = async () => { - const targetLanguage = languageSelect.value; - const targetLanguageName = SUPPORTED_LANGUAGES[targetLanguage]; - - try { - translateButton.textContent = getUIText("processingText"); - translateButton.disabled = true; - resultContainer.style.display = "none"; - - // Get token count and estimated time for progress - const { countTokens } = await import("./token_estimator.js"); - const { estimateProcessingTime } = await import("./translation.js"); - const textToAnalyze = isPageMode - ? document.documentElement.outerHTML - : textArea.value.trim(); - const tokenInfo = countTokens(textToAnalyze); - const estimatedSeconds = estimateProcessingTime(tokenInfo.totalTokens); - - // Create progress indicator - const progressContainer = document.createElement("div"); - progressContainer.style.cssText = ` - margin: 20px 0; - padding: 20px; - border: 2px solid #4CAF50; - border-radius: 8px; - background: #f0fff0; - text-align: center; - `; - - const progressText = document.createElement("div"); - progressText.style.cssText = - "font-weight: bold; color: #4CAF50; margin-bottom: 10px;"; - progressText.textContent = getUIText("processingTokens", { tokens: tokenInfo.totalTokens }); - - const etaText = document.createElement("div"); - etaText.style.cssText = "color: #666; margin-bottom: 15px;"; - etaText.textContent = `Estimated time: ${estimatedSeconds} seconds`; - - const progressBar = document.createElement("div"); - progressBar.style.cssText = ` - width: 100%; - height: 20px; - background: #e0e0e0; - border-radius: 10px; - overflow: hidden; - margin-bottom: 10px; - `; - - const progressFill = document.createElement("div"); - progressFill.style.cssText = ` - height: 100%; - background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%); - width: 0%; - transition: width 0.5s ease; - border-radius: 10px; - `; - progressBar.appendChild(progressFill); - - const statusText = document.createElement("div"); - statusText.style.cssText = "color: #777; font-size: 0.9em;"; - statusText.textContent = "Initializing translation..."; - - progressContainer.appendChild(progressText); - progressContainer.appendChild(etaText); - progressContainer.appendChild(progressBar); - progressContainer.appendChild(statusText); - - // Insert progress before result container - resultContainer.parentNode.insertBefore( - progressContainer, - resultContainer, - ); - - // Start progress animation - const startTime = Date.now(); - const progressInterval = setInterval(() => { - const elapsed = Date.now() - startTime; - const progress = Math.min( - (elapsed / (estimatedSeconds * 1000)) * 100, - 95, - ); - progressFill.style.width = `${progress}%`; - - const remainingTime = Math.max( - 0, - estimatedSeconds - Math.floor(elapsed / 1000), - ); - etaText.textContent = - remainingTime > 0 - ? `Time remaining: ${remainingTime}s` - : "Almost done..."; - - if (elapsed < 3000) statusText.textContent = "Analyzing content..."; - else if (elapsed < estimatedSeconds * 500) - statusText.textContent = "Translating with Hermes AI..."; - else statusText.textContent = "Finalizing translation..."; - }, 500); - - let translatedText; - - if (isPageMode) { - translatedText = await translateCurrentPage(targetLanguage); - } else { - const customText = textArea.value.trim(); - if (!customText) { - alert("Please enter some text to translate!"); - return; - } - translatedText = await translateText(customText, targetLanguage); - } - - resultLabel.textContent = `Translation (${targetLanguageName}):`; - - // Check if the translated content looks like HTML - const isHTML = - translatedText.includes("<") && - translatedText.includes(">") && - (translatedText.includes(" - window.UNCLOSEAI_SKIP_INIT = true; - - `; - - if (translatedText.includes("") - ? translatedText.replace( - "", - `${scriptTag}`, - ) - : translatedText.replace( - "", - `${scriptTag}`, - ); - } else if (translatedText.includes("")) { - // Has head but no DOCTYPE - htmlWithBase = `\n${translatedText.replace("", `${scriptTag}`)}`; - } else { - // No DOCTYPE or head - htmlWithBase = `\n${scriptTag}${translatedText}`; - } - - previewWindow.document.write(htmlWithBase); - previewWindow.document.close(); - previewWindow.document.title = `Translation Preview (${targetLanguageName}) - AI Enabled`; - - // Add a notice about AI functionality only in preview window (wait for body to be available) - const addNotice = () => { - if ( - previewWindow && - previewWindow.document && - previewWindow.document.body - ) { - console.log("Adding AI notice to preview window"); - const notice = previewWindow.document.createElement("div"); - notice.style.cssText = - "position: fixed; top: 10px; right: 10px; background: #4CAF50; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; z-index: 10000; box-shadow: 0 2px 8px rgba(0,0,0,0.2);"; - notice.textContent = `🤖 AI features should work in ${targetLanguageName}!`; - previewWindow.document.body.appendChild(notice); - - // Auto-hide notice after 5 seconds - setTimeout(() => { - if (notice.parentNode) notice.parentNode.removeChild(notice); - }, 5000); - } else if (previewWindow) { - // Wait a bit and try again - setTimeout(addNotice, 100); - } - }; - - // Only add notice if we have a valid preview window - if (previewWindow) { - addNotice(); - } - } - - // Show raw HTML toggle in modal - resultArea.innerHTML = ` -
- -
${translatedText.replace(//g, ">")}
-
-
-
✅ Preview Opened in New Window
-

The translated HTML page has opened in a new window with full AI functionality!

- ${previewWindow ? "" : '

Note: Please allow popups for preview window

'} -
- `; - resultArea.style.whiteSpace = "normal"; - } else { - resultArea.textContent = translatedText; - resultArea.style.whiteSpace = "pre-wrap"; - } - - resultContainer.style.display = "block"; - - // Complete progress bar - clearInterval(progressInterval); - progressFill.style.width = "100%"; - etaText.textContent = "Translation completed!"; - statusText.textContent = "✅ Ready to preview"; - - // Hide progress after a moment - setTimeout(() => { - if (progressContainer.parentNode) { - progressContainer.parentNode.removeChild(progressContainer); - } - }, 2000); - - // Scroll to result - resultContainer.scrollIntoView({ behavior: "smooth", block: "nearest" }); - } catch (error) { - // Clean up progress on error - if (typeof progressInterval !== "undefined") { - clearInterval(progressInterval); - } - const progressContainer = document.querySelector( - '[style*="border: 2px solid #4CAF50"]', - ); - if (progressContainer && progressContainer.parentNode) { - progressContainer.parentNode.removeChild(progressContainer); - } - alert(`Translation failed: ${error.message}`); - } finally { - translateButton.textContent = isPageMode - ? "🌐 Translate Page" - : "🌐 Translate Text"; - translateButton.disabled = false; - } - }; - - document.body.appendChild(modal); - - // Use showModal() for proper mobile support and backdrop - modal.showModal(); - - // Close modal when clicking backdrop - modal.addEventListener("click", (e) => { - if (e.target === modal) { - modal.close(); - document.body.removeChild(modal); - } - }); -} - -// Initialize the system export function initializeSystem() { // Check for skip init flag - allow partial initialization for preview windows if (window.UNCLOSEAI_SKIP_INIT === true) { @@ -4830,3 +4022,7 @@ export function initializeSystem() { // Initialize class-based elements initializeUncloseaiElements(); } + +// Export modal functions globally for onclick handlers +window.openTTSModal = openTTSModal; +window.openTranslateModal = openTranslateModal;