- Extract massive modal functions to uncloseai-embed-modal.js (399 lines) - Extract widget creation functions to widget-library.js (447 lines) - Move addFileUploadButton to file-upload.js for better organization - Reduce ui.js from 3,595 lines to 567 lines (84% reduction) - Transform ui.js into clean coordinator as requested - All functionality preserved with proper imports and exports 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
448 lines
No EOL
16 KiB
JavaScript
448 lines
No EOL
16 KiB
JavaScript
// Widget and feature creation library
|
|
import { getUIText } from "./ui-translations.js";
|
|
import { speakText } from "./tts.js";
|
|
import { detectPageLanguage } from "./language-detection.js";
|
|
import { getThemeColors } from "./ui-themes.js";
|
|
import { openTTSModal } from "./tts-modal.js";
|
|
import { openTranslateModal } from "./translate-modal.js";
|
|
|
|
// Get USE_CUSTOM_STYLING from window or default
|
|
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
|
|
|
|
// Create full chat interface
|
|
export function createFullInterface(container) {
|
|
// Chat area
|
|
const chatContainer = document.createElement("div");
|
|
if (USE_CUSTOM_STYLING) {
|
|
chatContainer.innerHTML = `
|
|
<div id="chat-box" style="min-height: 200px; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; overflow-y: auto; border-radius: 4px;"></div>
|
|
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
|
<input type="text" id="user-input" placeholder="${getUIText("askPagePlaceholder")}" style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
|
|
<button onclick="handleUserInput()" style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// For blog sites without custom styling - use minimal, theme-agnostic styles
|
|
chatContainer.innerHTML = `
|
|
<div id="chat-box" style="min-height: 200px; border: 1px solid rgba(128,128,128,0.3); padding: 10px; margin-bottom: 10px; overflow-y: auto; border-radius: 4px; background: rgba(0,0,0,0.05); backdrop-filter: blur(10px);"></div>
|
|
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
|
<input type="text" id="user-input" placeholder="${getUIText("askPagePlaceholder")}" style="flex: 1; padding: 8px; border: 1px solid rgba(128,128,128,0.3); border-radius: 4px; background: rgba(255,255,255,0.1); backdrop-filter: blur(10px);">
|
|
<button onclick="handleUserInput()" style="padding: 8px 16px; background: rgba(0,123,255,0.8); color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Control buttons
|
|
const controlsDiv = document.createElement("div");
|
|
controlsDiv.style.cssText =
|
|
"display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 10px;";
|
|
|
|
const readBtn = createButton(getUIText("readPage"), () =>
|
|
readPageWithHermes(),
|
|
);
|
|
const ttsBtn = createButton(getUIText("ttsAnything"), () => openTTSModal());
|
|
const translateBtn = createButton(getUIText("translate"), () =>
|
|
openTranslateModal(),
|
|
);
|
|
|
|
controlsDiv.appendChild(readBtn);
|
|
controlsDiv.appendChild(ttsBtn);
|
|
controlsDiv.appendChild(translateBtn);
|
|
|
|
// Hidden file input
|
|
const fileInput = document.createElement("input");
|
|
fileInput.type = "file";
|
|
fileInput.setAttribute("data-uncloseai-file-input", "");
|
|
fileInput.style.display = "none";
|
|
fileInput.onchange = handleFileUpload;
|
|
|
|
container.appendChild(chatContainer);
|
|
container.appendChild(controlsDiv);
|
|
container.appendChild(fileInput);
|
|
}
|
|
|
|
// Create custom interface with specific features
|
|
export function createCustomInterface(container, features) {
|
|
const div = document.createElement("div");
|
|
div.style.cssText =
|
|
"display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;";
|
|
|
|
features.forEach((feature) => {
|
|
switch (feature.trim()) {
|
|
case "chat":
|
|
createChatFeature(div);
|
|
break;
|
|
case "tts":
|
|
createTTSFeature(div);
|
|
break;
|
|
case "translate":
|
|
createTranslateFeature(div);
|
|
break;
|
|
case "smart-translate":
|
|
createSmartTranslateFeature(div);
|
|
break;
|
|
case "upload":
|
|
createUploadFeature(div);
|
|
break;
|
|
case "read":
|
|
createReadFeature(div);
|
|
break;
|
|
}
|
|
});
|
|
|
|
container.appendChild(div);
|
|
}
|
|
|
|
// Individual feature creators
|
|
export function createChatFeature(container) {
|
|
const chatDiv = document.createElement("div");
|
|
|
|
if (USE_CUSTOM_STYLING) {
|
|
chatDiv.innerHTML = `
|
|
<h4>AI Chat</h4>
|
|
<div style="border: 1px solid #ccc; height: 150px; padding: 8px; margin: 5px 0; overflow-y: auto;" data-chat-box></div>
|
|
<input type="text" placeholder="${getUIText("askAnythingPlaceholder")}" style="width: 100%; margin: 2px 0;" data-chat-input>
|
|
<button onclick="handleCustomChat(this)" style="width: 100%; padding: 6px;">Send</button>
|
|
`;
|
|
} else {
|
|
// For blog sites without custom styling - use minimal, theme-agnostic styles
|
|
chatDiv.innerHTML = `
|
|
<h4>AI Chat</h4>
|
|
<div style="border: 1px solid rgba(128,128,128,0.3); height: 150px; padding: 8px; margin: 5px 0; overflow-y: auto; background: rgba(0,0,0,0.05); backdrop-filter: blur(10px);" data-chat-box></div>
|
|
<input type="text" placeholder="${getUIText("askAnythingPlaceholder")}" style="width: 100%; margin: 2px 0; background: rgba(255,255,255,0.1); border: 1px solid rgba(128,128,128,0.3); backdrop-filter: blur(10px);" data-chat-input>
|
|
<button onclick="handleCustomChat(this)" style="width: 100%; padding: 6px; background: rgba(0,123,255,0.8); color: white; border: none;">Send</button>
|
|
`;
|
|
}
|
|
container.appendChild(chatDiv);
|
|
}
|
|
|
|
export function createTTSFeature(container) {
|
|
const ttsDiv = document.createElement("div");
|
|
|
|
if (USE_CUSTOM_STYLING) {
|
|
ttsDiv.innerHTML = `
|
|
<h4>Text to Speech</h4>
|
|
<textarea placeholder="${getUIText("enterTextToSpeakPlaceholder")}" style="width: 100%; height: 80px; margin: 5px 0;" data-tts-input></textarea>
|
|
<button onclick="handleTTSFromElement(this)" style="width: 100%; padding: 6px;">🔊 Convert to Speech</button>
|
|
<div data-tts-result style="margin: 5px 0;"></div>
|
|
`;
|
|
} else {
|
|
// For blog sites without custom styling - use minimal, theme-agnostic styles
|
|
ttsDiv.innerHTML = `
|
|
<h4>Text to Speech</h4>
|
|
<textarea placeholder="${getUIText("enterTextToSpeakPlaceholder")}" style="width: 100%; height: 80px; margin: 5px 0; background: rgba(255,255,255,0.1); border: 1px solid rgba(128,128,128,0.3); backdrop-filter: blur(10px);" data-tts-input></textarea>
|
|
<button onclick="handleTTSFromElement(this)" style="width: 100%; padding: 6px; background: rgba(0,123,255,0.8); color: white; border: none;">🔊 Convert to Speech</button>
|
|
<div data-tts-result style="margin: 5px 0;"></div>
|
|
`;
|
|
}
|
|
container.appendChild(ttsDiv);
|
|
}
|
|
|
|
export function createUploadFeature(container) {
|
|
const uploadDiv = document.createElement("div");
|
|
uploadDiv.innerHTML = `
|
|
<h4>File Upload</h4>
|
|
<input type="file" style="width: 100%; margin: 5px 0;" data-upload-input>
|
|
<button onclick="handleUploadFromElement(this)" style="width: 100%; padding: 6px;">📁 Upload & Analyze</button>
|
|
<div data-upload-result style="margin: 5px 0; display: none;"></div>
|
|
`;
|
|
container.appendChild(uploadDiv);
|
|
}
|
|
|
|
export function createTranslateFeature(container) {
|
|
const translateDiv = document.createElement("div");
|
|
translateDiv.innerHTML = `
|
|
<h4 data-i18n="translationModalHeading">${getUIText("translationModalHeading")}</h4>
|
|
<button onclick="openTranslateModal()" style="width: 100%; padding: 6px;" data-i18n="translationModal">${getUIText("translationModal")}</button>
|
|
`;
|
|
container.appendChild(translateDiv);
|
|
}
|
|
|
|
export function createSmartTranslateFeature(container) {
|
|
const smartTranslateDiv = document.createElement("div");
|
|
const button = document.createElement("button");
|
|
button.textContent = getUIText("smartTranslate");
|
|
button.style.cssText = "width: 100%; padding: 6px;";
|
|
button.onclick = () => handleSmartTranslate(button);
|
|
|
|
const heading = document.createElement("h4");
|
|
heading.textContent = getUIText("smartTranslate");
|
|
heading.setAttribute("data-i18n", "smartTranslate");
|
|
|
|
smartTranslateDiv.appendChild(heading);
|
|
smartTranslateDiv.appendChild(button);
|
|
container.appendChild(smartTranslateDiv);
|
|
}
|
|
|
|
export function createReadFeature(container) {
|
|
const readDiv = document.createElement("div");
|
|
|
|
const heading = document.createElement("h4");
|
|
heading.textContent = getUIText("readPage");
|
|
heading.setAttribute("data-i18n", "readPage");
|
|
|
|
const description = document.createElement("p");
|
|
description.style.cssText = "font-size: 0.9em; margin: 5px 0;";
|
|
description.textContent = "Read this page with AI voice"; // TODO: Add translation key
|
|
|
|
const button = document.createElement("button");
|
|
button.textContent = getUIText("readPage");
|
|
button.setAttribute("data-i18n", "readPage");
|
|
button.style.cssText = "width: 100%; padding: 8px 16px; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;";
|
|
button.onclick = readPageWithHermes;
|
|
|
|
readDiv.appendChild(heading);
|
|
readDiv.appendChild(description);
|
|
readDiv.appendChild(button);
|
|
container.appendChild(readDiv);
|
|
}
|
|
|
|
// Helper functions for custom features
|
|
export function createButton(text, onclick) {
|
|
const btn = document.createElement("button");
|
|
btn.textContent = text;
|
|
btn.onclick = onclick;
|
|
btn.style.cssText =
|
|
"padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;";
|
|
return btn;
|
|
}
|
|
|
|
export async function handleTTSFromElement(button) {
|
|
const container =
|
|
button.closest("[data-tts-result]")?.parentElement || button.parentElement;
|
|
const textarea = container.querySelector("[data-tts-input]");
|
|
const resultDiv = container.querySelector("[data-tts-result]");
|
|
const text = textarea?.value?.trim();
|
|
|
|
if (!text) {
|
|
alert(getUIText("pleaseEnterText"));
|
|
return;
|
|
}
|
|
|
|
button.disabled = true;
|
|
button.textContent = "Converting...";
|
|
resultDiv.innerHTML = "<em>Converting to speech...</em>";
|
|
|
|
try {
|
|
const result = await speakText(text, "alloy", 0.9);
|
|
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: 4px 8px;";
|
|
playButton.onclick = () => result.audio.play();
|
|
|
|
const pauseButton = document.createElement("button");
|
|
pauseButton.textContent = "⏸️ Pause";
|
|
pauseButton.style.cssText = "margin: 2px; padding: 4px 8px;";
|
|
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 {
|
|
button.disabled = false;
|
|
button.textContent = "🔊 Convert to Speech";
|
|
}
|
|
}
|
|
|
|
export async function handleUploadFromElement(button) {
|
|
const container = button.parentElement;
|
|
const fileInput = container.querySelector("[data-upload-input]");
|
|
const resultDiv = container.querySelector("[data-upload-result]");
|
|
|
|
if (!fileInput.files[0]) {
|
|
alert(getUIText("pleaseSelectFile"));
|
|
return;
|
|
}
|
|
|
|
button.disabled = true;
|
|
button.textContent = getUIText("processingText");
|
|
resultDiv.style.display = "block";
|
|
resultDiv.innerHTML = "<em>Uploading and analyzing file...</em>";
|
|
|
|
try {
|
|
showProgressIndicator(getUIText("processingText"));
|
|
const response = await uploadFile(fileInput.files[0]);
|
|
hideProgressIndicator();
|
|
|
|
resultDiv.innerHTML = `<strong>Analysis Result:</strong><br>${response}`;
|
|
fileInput.value = "";
|
|
} catch (error) {
|
|
hideProgressIndicator();
|
|
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
|
} finally {
|
|
button.disabled = false;
|
|
button.textContent = "📁 Upload & Analyze";
|
|
}
|
|
}
|
|
|
|
export async function handleSmartTranslate(button) {
|
|
console.log("🤖 Smart translate clicked!", button);
|
|
|
|
// Check if dropdown already exists
|
|
const existingDropdown = button.parentElement.querySelector(
|
|
".translate-dropdown",
|
|
);
|
|
if (existingDropdown) {
|
|
existingDropdown.remove();
|
|
return;
|
|
}
|
|
|
|
// Detect page language
|
|
button.textContent = getUIText("detectingLanguage");
|
|
button.disabled = true;
|
|
|
|
try {
|
|
const currentLang = await detectPageLanguage();
|
|
button.textContent = getUIText("smartTranslate");
|
|
button.disabled = false;
|
|
|
|
// Import supported languages and native names
|
|
const { SUPPORTED_LANGUAGES, NATIVE_LANGUAGE_NAMES, translateCurrentPage } =
|
|
await import("./translation.js");
|
|
const colors = getThemeColors();
|
|
|
|
// Create dropdown
|
|
const dropdown = document.createElement("div");
|
|
dropdown.className = "translate-dropdown";
|
|
dropdown.style.cssText = `
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: ${colors.panelBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px ${colors.shadowColor};
|
|
z-index: 1000;
|
|
min-width: 200px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
margin-top: 4px;
|
|
`;
|
|
|
|
// Add current language indicator
|
|
const currentLangDiv = document.createElement("div");
|
|
currentLangDiv.style.cssText = `
|
|
padding: 8px 12px;
|
|
font-size: 12px;
|
|
color: ${colors.mutedText};
|
|
border-bottom: 1px solid ${colors.dividerColor};
|
|
background: ${colors.contentBackground};
|
|
`;
|
|
const currentLangName =
|
|
NATIVE_LANGUAGE_NAMES[currentLang] ||
|
|
SUPPORTED_LANGUAGES[currentLang] ||
|
|
"Unknown";
|
|
currentLangDiv.textContent = getUIText("currentPage", {
|
|
lang: currentLangName,
|
|
});
|
|
dropdown.appendChild(currentLangDiv);
|
|
|
|
// Add translation options with code, English, and native names
|
|
Object.entries(NATIVE_LANGUAGE_NAMES).forEach(([code, nativeName]) => {
|
|
// Skip current language
|
|
if (code === currentLang) return;
|
|
|
|
const englishName = SUPPORTED_LANGUAGES[code];
|
|
|
|
const option = document.createElement("div");
|
|
option.style.cssText = `
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
color: ${colors.primaryText};
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
`;
|
|
|
|
// Format: "es • Spanish • Español"
|
|
option.innerHTML = `
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<span style="font-family: monospace; color: ${colors.mutedText}; font-size: 12px; min-width: 24px;">${code}</span>
|
|
<span style="color: ${colors.mutedText};">•</span>
|
|
<span style="color: ${colors.secondaryText};">${englishName}</span>
|
|
<span style="color: ${colors.mutedText};">•</span>
|
|
<span style="font-weight: 500;">${nativeName}</span>
|
|
</div>
|
|
`;
|
|
|
|
option.onmouseenter = () => {
|
|
option.style.background = colors.buttonHover;
|
|
};
|
|
option.onmouseleave = () => {
|
|
option.style.background = "transparent";
|
|
};
|
|
|
|
option.onclick = async () => {
|
|
try {
|
|
// Show loading
|
|
option.innerHTML = `
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<span style="font-family: monospace; color: ${colors.mutedText}; font-size: 12px; min-width: 24px;">${code}</span>
|
|
<span style="color: ${colors.mutedText};">•</span>
|
|
<span style="color: ${colors.secondaryText};">${getUIText("translatingTo")}</span>
|
|
</div>
|
|
`;
|
|
option.style.pointerEvents = "none";
|
|
|
|
// Translate the page
|
|
const translatedHtml = await translateCurrentPage(code);
|
|
|
|
// Open in new tab
|
|
const newWindow = window.open("", "_blank");
|
|
newWindow.document.write(translatedHtml);
|
|
newWindow.document.close();
|
|
newWindow.document.title = `${document.title} (${name})`;
|
|
|
|
// Close dropdown
|
|
dropdown.remove();
|
|
} catch (error) {
|
|
alert(`Translation failed: ${error.message}`);
|
|
// Restore original format
|
|
option.innerHTML = `
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<span style="font-family: monospace; color: ${colors.mutedText}; font-size: 12px; min-width: 24px;">${code}</span>
|
|
<span style="color: ${colors.mutedText};">•</span>
|
|
<span style="color: ${colors.secondaryText};">${englishName}</span>
|
|
<span style="color: ${colors.mutedText};">•</span>
|
|
<span style="font-weight: 500;">${nativeName}</span>
|
|
</div>
|
|
`;
|
|
option.style.pointerEvents = "auto";
|
|
}
|
|
};
|
|
|
|
dropdown.appendChild(option);
|
|
});
|
|
|
|
// Position dropdown relative to button
|
|
button.style.position = "relative";
|
|
button.parentElement.appendChild(dropdown);
|
|
|
|
// Close dropdown when clicking outside
|
|
const closeDropdown = (e) => {
|
|
if (!dropdown.contains(e.target) && e.target !== button) {
|
|
dropdown.remove();
|
|
document.removeEventListener("click", closeDropdown);
|
|
}
|
|
};
|
|
setTimeout(() => document.addEventListener("click", closeDropdown), 100);
|
|
} catch (error) {
|
|
button.textContent = getUIText("smartTranslate");
|
|
button.disabled = false;
|
|
alert(getUIText("languageDetectionFailed", { error: error.message }));
|
|
}
|
|
}
|
|
|
|
// Export functions for global access
|
|
window.handleTTSFromElement = handleTTSFromElement;
|
|
window.handleUploadFromElement = handleUploadFromElement;
|
|
window.handleSmartTranslate = handleSmartTranslate;
|
|
window.openTranslateModal = openTranslateModal; |