uncloseai.com/src/widget-library.js
Russell Ballestrini 3f9cb2c40f refactor: replace all inline styles with CSS classes for better maintainability
- Moved 115+ inline styles from uncloseai-embed-modal.js to CSS classes
- Cleaned all inline styles from page-reader.js, widget-library.js, chat.js, models.js, file-upload.js
- Added CSS custom properties for theming support
- Created reusable CSS classes for buttons, containers, and UI elements
- Only retained legitimate dynamic styles (height calculations, position adjustments)
- Improved consistency with PicoCSS overrides
2025-07-09 21:32:55 -04:00

386 lines
No EOL
13 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";
import { handleFileUpload } from "./file-upload.js";
// Create full chat interface
export function createFullInterface(container) {
// Load appropriate CSS based on whether we're using PicoCSS or built-in styles
// Detect if PicoCSS is actually present on the page
const hasPicoCSS = document.querySelector('link[href*="pico"]') !== null;
// Load appropriate CSS based on actual PicoCSS presence
const cssFile = hasPicoCSS ? 'uncloseai-modal-pico.css' : 'uncloseai-modal-builtin.css';
if (!document.querySelector(`link[href*="${cssFile}"]`)) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `https://uncloseai.com/src/${cssFile}`;
document.head.appendChild(link);
}
// Chat area
const chatContainer = document.createElement("div");
chatContainer.innerHTML = `
<div id="chat-box" class="widget-chat-box"></div>
<div class="widget-input-container">
<input type="text" id="user-input" placeholder="${getUIText("askPagePlaceholder")}" class="widget-input-field">
<button onclick="handleUserInput()" class="widget-send-btn">Send</button>
</div>
`;
// Control buttons
const controlsDiv = document.createElement("div");
controlsDiv.className = "widget-custom-grid";
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"; // Hidden file input
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.className = "widget-custom-grid";
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");
chatDiv.innerHTML = `
<h4>AI Chat</h4>
<div class="widget-feature-container" data-chat-box></div>
<input type="text" placeholder="${getUIText("askAnythingPlaceholder")}" class="widget-input-field" data-chat-input>
<button onclick="handleCustomChat(this)" class="widget-send-btn">Send</button>
`;
container.appendChild(chatDiv);
}
export function createTTSFeature(container) {
const ttsDiv = document.createElement("div");
ttsDiv.innerHTML = `
<h4>Text to Speech</h4>
<textarea placeholder="${getUIText("enterTextToSpeakPlaceholder")}" class="widget-tts-container" data-tts-input></textarea>
<button onclick="handleTTSFromElement(this)" class="widget-send-btn">🔊 Convert to Speech</button>
<div data-tts-result></div>
`;
container.appendChild(ttsDiv);
}
export function createUploadFeature(container) {
const uploadDiv = document.createElement("div");
uploadDiv.innerHTML = `
<h4>File Upload</h4>
<input type="file" class="widget-input-field" data-upload-input>
<button onclick="handleUploadFromElement(this)" class="widget-send-btn">📁 Upload & Analyze</button>
<div data-upload-result style="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()" class="widget-send-btn" 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.className = "widget-send-btn";
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.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.className = "widget-send-btn";
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.className = "widget-send-btn";
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.className = "tts-audio-controls";
const playButton = document.createElement("button");
playButton.textContent = "▶️ Play";
playButton.onclick = () => result.audio.play();
const pauseButton = document.createElement("button");
pauseButton.textContent = "⏸️ Pause";
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.classList.add("show");
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(
".widget-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 = "widget-translate-dropdown";
// Styling handled by CSS class
// Add current language indicator
const currentLangDiv = document.createElement("div");
currentLangDiv.className = "widget-translate-dropdown-current";
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.className = "option";
// Color handled by CSS class
// Format: "es • Spanish • Español"
option.innerHTML = `
<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>
`;
// Hover effects handled by CSS
option.onclick = async () => {
try {
// Show loading
option.innerHTML = `
<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>
`;
option.className = "option option-disabled";
// 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 = `
<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>
`;
option.className = "option";
}
};
dropdown.appendChild(option);
});
// Position handled by CSS
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;