uncloseai.com/src/widget-library.js
Russell Ballestrini e99db238b6 refactor: major ui.js modularization - extract 3000+ lines
- 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>
2025-07-03 16:16:46 -04:00

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;