- 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
386 lines
No EOL
13 KiB
JavaScript
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; |