implement class-based initialization system and DRY refactoring for uncloseai.js

- Add .uncloseai class support with data-features configuration
- Create shared utility functions for buttons, chat messages, TTS controls, voice selects, and modal headers
- Refactor class-based feature creators to use shared utilities
- Add comprehensive documentation for class-based integration in index.html
- Add live examples of class-based usage in demo.html
- Maintain backward compatibility with existing floating button and API functions
This commit is contained in:
Russell Ballestrini 2025-06-30 16:50:16 -04:00
parent db57965ef4
commit 0b573a2a86
3 changed files with 800 additions and 14 deletions

117
demo.html
View file

@ -72,6 +72,123 @@
<button onclick="handleUserInput()">Send</button>
</div>
<h3>Class-Based Integration Examples</h3>
<p>You can now embed AI features directly into your HTML using CSS classes and data attributes:</p>
<h4>Full AI Interface</h4>
<p>Add a complete AI chat interface with all features:</p>
<div class="uncloseai" data-features="full"></div>
<h4>Custom Feature Selection</h4>
<p>Choose specific features you want:</p>
<h5>Chat Only</h5>
<div class="uncloseai" data-features="chat"></div>
<h5>Buttons Only (TTS + Upload + Read)</h5>
<div class="uncloseai" data-features="tts,upload,read"></div>
<h5>TTS Only</h5>
<div class="uncloseai" data-features="tts"></div>
<h3>Individual Function Examples</h3>
<p>You can also call uncloseai.js functions directly from your own buttons and interfaces:</p>
<h4>Direct AI Chat</h4>
<button onclick="directChatExample()">💬 Ask AI about this page</button>
<div id="direct-chat-result" style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 5px; display: none;"></div>
<h4>Text-to-Speech</h4>
<textarea id="tts-text" placeholder="Enter text to convert to speech..." style="width: 100%; height: 80px; margin: 5px 0;"></textarea>
<button onclick="directTTSExample()">🔊 Convert to Speech</button>
<div id="tts-result" style="margin: 10px 0;"></div>
<h4>Page Reading</h4>
<button onclick="readPageWithHermes()">📖 Read this page with AI</button>
<h4>File Upload (if you have files)</h4>
<input type="file" id="demo-file-input" style="margin: 5px 0;">
<button onclick="directFileExample()">📁 Upload & Analyze File</button>
<div id="file-result" style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 5px; display: none;"></div>
<script>
// Example functions showing direct API usage
async function directChatExample() {
const resultDiv = document.getElementById('direct-chat-result');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<em>AI is thinking...</em>';
try {
let response = '';
for await (const chunk of sendMessage('Tell me the main purpose of this page in 2 sentences')) {
response += chunk;
resultDiv.innerHTML = `<strong>AI Response:</strong> ${response}`;
}
} catch (error) {
resultDiv.innerHTML = '<strong>Error:</strong> Could not get AI response. ' + error.message;
}
}
async function directTTSExample() {
const text = document.getElementById('tts-text').value.trim();
const resultDiv = document.getElementById('tts-result');
if (!text) {
alert('Please enter some text first!');
return;
}
resultDiv.innerHTML = '<em>Converting to speech...</em>';
try {
const result = await speakText(text, 'alloy', 0.9);
resultDiv.innerHTML = `
<button onclick="this.previousElementSibling.play()" style="margin: 5px;">▶️ Play</button>
<button onclick="this.previousElementSibling.previousElementSibling.pause()" style="margin: 5px;">⏸️ Pause</button>
<button onclick="downloadAudio()" style="margin: 5px;">💾 Download</button>
`;
resultDiv.insertBefore(result.audio, resultDiv.firstChild);
window.currentAudioBlob = result.blob; // Store for download
} catch (error) {
resultDiv.innerHTML = '<strong>Error:</strong> Could not convert to speech. ' + error.message;
}
}
async function directFileExample() {
const fileInput = document.getElementById('demo-file-input');
const resultDiv = document.getElementById('file-result');
if (!fileInput.files[0]) {
alert('Please select a file first!');
return;
}
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<em>Uploading and analyzing file...</em>';
try {
showProgressIndicator('Processing file...');
const response = await uploadFile(fileInput.files[0]);
hideProgressIndicator();
resultDiv.innerHTML = `<strong>File Analysis:</strong><br>${response}`;
fileInput.value = ''; // Clear the input
} catch (error) {
hideProgressIndicator();
resultDiv.innerHTML = '<strong>Error:</strong> Could not process file. ' + error.message;
}
}
function downloadAudio() {
if (window.currentAudioBlob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(window.currentAudioBlob);
a.download = `tts-audio-${Date.now()}.mp3`;
a.click();
}
}
</script>
</main>
<footer>

View file

@ -74,18 +74,51 @@
<p>This web client-only solution uses <a href="https://uncloseai.com/uncloseai.js" target="_blank">uncloseai.js</a> to make the browser act as a client, directly interacting with the API without needing an intermediary server. By eliminating the need for a valid API key, the API handles requests on behalf of the browser client, making it efficient and accessible thin client, especially those on battery power like phones & laptops.</p>
<div id="chat-container">
<div id="chat-box"></div>
<div></div>
</div>
<div>
<input type="text" id="user-input" placeholder="Ask about this page...">
<button onclick="handleUserInput()">Send</button>
</div>
<br/>
<p>This static site has a live LLM demonstration above!</p>
<p>A stand alone demo is here: <a href="https://ai.unturf.com/demo.html">uncloseai.js demostration</a></p>
<h3>Installing uncloseai.js on Your Website</h3>
<p>Add AI capabilities to any static website or web application with just one line of code. uncloseai.js automatically creates a floating "uncloseai." button that provides access to Hermes AI, text-to-speech, file upload, and more.</p>
<h4>Quick Installation</h4>
<p>Add this script tag to your HTML - that's it! The floating AI button appears automatically:</p>
<pre><code class="html">&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
<h4>Configuration Options</h4>
<p>To use your CSS framework's styling instead of uncloseai.js custom styles:</p>
<pre><code class="javascript">&lt;script&gt;
window.UNCLOSEAI_CUSTOM_STYLING = false;
&lt;/script&gt;</code></pre>
<p>To disable the floating button (if you only want the API functions):</p>
<pre><code class="javascript">&lt;script&gt;
window.UNCLOSEAI_SHOW_BUTTON = false;
&lt;/script&gt;</code></pre>
<h4>Class-Based Integration</h4>
<p>Embed AI features directly into your HTML using CSS classes and data attributes:</p>
<p><strong>Full Interface:</strong> Complete AI chat with all features</p>
<pre><code class="html">&lt;div class="uncloseai" data-features="full"&gt;&lt;/div&gt;</code></pre>
<p><strong>Chat Only:</strong> Just the AI chat interface</p>
<pre><code class="html">&lt;div class="uncloseai" data-features="chat"&gt;&lt;/div&gt;</code></pre>
<p><strong>Specific Features:</strong> Choose which buttons to include</p>
<pre><code class="html">&lt;div class="uncloseai" data-features="tts,upload,read"&gt;&lt;/div&gt;
&lt;div class="uncloseai" data-features="tts"&gt;&lt;/div&gt;</code></pre>
<p><strong>Available Features:</strong> <code>chat</code>, <code>tts</code>, <code>upload</code>, <code>read</code>, <code>full</code></p>
<h4>What You Get</h4>
<ul>
<li><strong>🤖 Floating AI Assistant</strong> - Always-accessible "uncloseai." button in bottom right</li>
<li><strong>📖 Page-Aware</strong> - Hermes AI understands your page content and generates contextual introductions</li>
<li><strong>🔊 Text-to-Speech</strong> - Convert any text to speech with multiple voice options</li>
<li><strong>📁 File Upload</strong> - Upload and discuss documents, images, and more</li>
<li><strong>💾 Conversation History</strong> - Persistent chat history with delete functionality</li>
<li><strong>🎨 Framework Compatible</strong> - Works with PicoCSS, PureCSS, Bootstrap, and more</li>
</ul>
<p><strong>Live Demo:</strong> <a href="https://ai.unturf.com/demo.html">Try uncloseai.js in action here</a> - see the floating button and AI features working on a real page!</p>
<h3>Installing the OpenAI Client</h3>
<h4>Python</h4>

View file

@ -901,8 +901,227 @@ let chatHistory = [
...loadConversationHistory()
];
// Initialize the chat interface
// Initialize uncloseai elements based on class
function initializeUncloseaiElements() {
const uncloseaiElements = document.querySelectorAll('.uncloseai');
uncloseaiElements.forEach(element => {
const features = element.dataset.features || 'full';
const type = element.dataset.type || 'standard';
// Create container for this uncloseai instance
const container = document.createElement('div');
container.className = 'uncloseai-container';
container.style.cssText = 'width: 100%; margin: 10px 0;';
if (features === 'full' || type === 'full') {
createFullInterface(container);
} else {
createCustomInterface(container, features.split(','));
}
element.appendChild(container);
});
}
// Create full chat interface
function createFullInterface(container) {
// Chat area
const chatContainer = document.createElement('div');
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="Ask about this page..." 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>
`;
// 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('📖 Read Page', () => readPageWithHermes());
const ttsBtn = createButton('🔊 TTS Anything', () => openTTSModal());
const uploadBtn = createButton('📁 Upload File', () => document.querySelector('[data-uncloseai-file-input]')?.click());
controlsDiv.appendChild(readBtn);
controlsDiv.appendChild(ttsBtn);
controlsDiv.appendChild(uploadBtn);
// Hidden file input
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.setAttribute('data-uncloseai-file-input', '');
fileInput.style.display = 'none';
fileInput.onchange = async (e) => {
if (e.target.files[0]) {
try {
showProgressIndicator('Processing file...');
const response = await uploadFile(e.target.files[0]);
await integrateMegafarceResponse(response);
hideProgressIndicator();
e.target.value = '';
} catch (error) {
hideProgressIndicator();
alert('File upload failed: ' + error.message);
}
}
};
container.appendChild(controlsDiv);
container.appendChild(chatContainer);
container.appendChild(fileInput);
}
// Create custom interface based on specific features
function createCustomInterface(container, features) {
features.forEach(feature => {
const featureDiv = document.createElement('div');
featureDiv.style.margin = '10px 0';
switch (feature.trim()) {
case 'chat':
createChatFeature(featureDiv);
break;
case 'tts':
createTTSFeature(featureDiv);
break;
case 'upload':
createUploadFeature(featureDiv);
break;
case 'read':
createReadFeature(featureDiv);
break;
}
container.appendChild(featureDiv);
});
}
// Individual feature creators
function createChatFeature(container) {
container.innerHTML = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">AI Chat</h4>
<div id="chat-box" style="min-height: 150px; border: 1px solid #eee; padding: 8px; margin: 10px 0; overflow-y: auto; border-radius: 4px;"></div>
<div style="display: flex; gap: 8px;">
<input type="text" id="user-input" placeholder="Chat with AI..." 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>
</div>
`;
}
function createTTSFeature(container) {
container.innerHTML = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">Text to Speech</h4>
<textarea placeholder="Enter text to convert to speech..." style="width: 100%; height: 80px; margin: 10px 0; padding: 8px; border: 1px solid #ccc; border-radius: 4px;" data-tts-input></textarea>
<button onclick="handleTTSFromElement(this)" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">🔊 Convert to Speech</button>
<div data-tts-result style="margin-top: 10px;"></div>
</div>
`;
}
function createUploadFeature(container) {
container.innerHTML = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">File Upload & Analysis</h4>
<input type="file" style="margin: 10px 0; width: 100%;" data-upload-input>
<button onclick="handleUploadFromElement(this)" style="padding: 8px 16px; background: #ffc107; color: black; border: none; border-radius: 4px; cursor: pointer;">📁 Upload & Analyze</button>
<div data-upload-result style="margin-top: 10px; padding: 8px; border: 1px solid #eee; border-radius: 4px; display: none;"></div>
</div>
`;
}
function createReadFeature(container) {
container.innerHTML = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">Page Reading</h4>
<p>Let AI read and analyze the current page content.</p>
<button onclick="readPageWithHermes()" style="padding: 8px 16px; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;">📖 Read Page with AI</button>
</div>
`;
}
// Helper functions for custom features
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;
}
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('Please enter some text first!');
return;
}
button.disabled = true;
button.textContent = 'Converting...';
resultDiv.innerHTML = '<em>Converting to speech...</em>';
try {
const result = await speakText(text, 'alloy', 0.9);
resultDiv.innerHTML = `
<div style="margin: 10px 0;">
<button onclick="this.previousElementSibling.play()" style="margin: 2px; padding: 4px 8px;"> Play</button>
<button onclick="this.previousElementSibling.previousElementSibling.pause()" style="margin: 2px; padding: 4px 8px;"> Pause</button>
</div>
`;
resultDiv.insertBefore(result.audio, resultDiv.firstChild);
} catch (error) {
resultDiv.innerHTML = '<strong>Error:</strong> ' + error.message;
} finally {
button.disabled = false;
button.textContent = '🔊 Convert to Speech';
}
}
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('Please select a file first!');
return;
}
button.disabled = true;
button.textContent = 'Processing...';
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<em>Uploading and analyzing file...</em>';
try {
showProgressIndicator('Processing file...');
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';
}
}
// Initialize the legacy chat interface (for backward compatibility)
function initializeChatInterface() {
// Only initialize if there are legacy elements (chat-container, user-input, etc.)
const legacyElements = document.querySelector('#chat-container, #user-input, #chat-box');
if (!legacyElements) return;
const pageContent = extractWebpageContent();
chatHistory.push({
role: "system",
@ -2011,6 +2230,200 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga
// Configuration for styling - set to false to disable custom styling
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
// -------------------------
// Shared Utility Functions for DRY Code
// -------------------------
// Create standardized button with consistent styling
function createStyledButton(text, type = 'primary', additionalStyles = '') {
const button = document.createElement('button');
button.textContent = text;
const baseStyles = 'padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;';
let typeStyles = '';
switch (type) {
case 'primary':
typeStyles = USE_CUSTOM_STYLING
? 'background: #007bff; color: white;'
: 'background: var(--primary, #007bff); color: white;';
break;
case 'success':
typeStyles = USE_CUSTOM_STYLING
? 'background: #28a745; color: white;'
: 'background: var(--success, #28a745); color: white;';
break;
case 'warning':
typeStyles = USE_CUSTOM_STYLING
? 'background: #ffc107; color: black;'
: 'background: var(--warning, #ffc107); color: black;';
break;
case 'info':
typeStyles = USE_CUSTOM_STYLING
? 'background: #17a2b8; color: white;'
: 'background: var(--info, #17a2b8); color: white;';
break;
case 'close':
typeStyles = USE_CUSTOM_STYLING
? 'background: none; border: none; color: currentColor; font-size: 24px; padding: 4px 8px;'
: 'float: right; background: none; border: none; font-size: 1.2em; padding: 4px 8px;';
break;
default:
typeStyles = USE_CUSTOM_STYLING
? 'background: #6c757d; color: white;'
: '';
}
button.style.cssText = baseStyles + typeStyles + additionalStyles;
return button;
}
// Create standardized chat message element
function createChatMessage(content, sender = 'user', isHTML = false) {
const messageDiv = document.createElement('div');
const bgColor = sender === 'user' ? '#e3f2fd' : '#f3e5f5';
const label = sender === 'user' ? 'You' : 'AI';
messageDiv.style.cssText = `margin-bottom: 10px; padding: 8px; background: ${bgColor}; border-radius: 4px;`;
if (isHTML) {
messageDiv.innerHTML = `<strong>${label}:</strong> ${content}`;
} else {
messageDiv.innerHTML = `<strong>${label}:</strong> ${content}`;
}
return messageDiv;
}
// Create standardized TTS control set (play/pause, regenerate, download)
function createTTSControls(textContent, voiceSelectElement, onAudioGenerated = null) {
const controlContainer = document.createElement('div');
controlContainer.style.cssText = 'display: grid; grid-template-columns: auto auto auto; gap: 8px; margin: 10px 0; align-items: center;';
const playBtn = createStyledButton('🔊 Play', 'primary');
const regenerateBtn = createStyledButton('🔄 Regenerate', 'secondary');
const downloadBtn = createStyledButton('💾 Download', 'secondary');
regenerateBtn.style.display = 'none';
downloadBtn.style.display = 'none';
let audio = null;
let audioBlob = null;
let currentVoice = voiceSelectElement.value;
const generateTTS = async () => {
playBtn.textContent = 'Processing...';
playBtn.disabled = true;
regenerateBtn.disabled = true;
const selectedVoice = voiceSelectElement.value;
currentVoice = selectedVoice;
const result = await speakText(textContent, selectedVoice, 0.9);
audio = result.audio;
audioBlob = result.blob;
playBtn.textContent = 'Pause';
playBtn.disabled = false;
regenerateBtn.disabled = false;
regenerateBtn.style.display = 'inline-block';
downloadBtn.style.display = 'inline-block';
if (onAudioGenerated) onAudioGenerated(audio, audioBlob);
audio.play();
};
playBtn.onclick = async () => {
if (!audio) {
await generateTTS();
} else {
if (audio.paused) {
audio.play();
playBtn.textContent = 'Pause';
} else {
audio.pause();
playBtn.textContent = 'Play';
}
}
};
regenerateBtn.onclick = async () => {
if (audio) audio.pause();
audio = null;
audioBlob = null;
await generateTTS();
};
downloadBtn.onclick = () => {
if (audioBlob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(audioBlob);
a.download = `tts-audio-${Date.now()}.mp3`;
a.click();
}
};
// Watch for voice changes
voiceSelectElement.addEventListener('change', () => {
if (audio && currentVoice !== voiceSelectElement.value) {
regenerateBtn.style.display = 'inline-block';
regenerateBtn.style.background = '#fffacd';
}
});
controlContainer.appendChild(playBtn);
controlContainer.appendChild(regenerateBtn);
controlContainer.appendChild(downloadBtn);
return { container: controlContainer, playBtn, regenerateBtn, downloadBtn };
}
// Create standardized voice selection dropdown
function createVoiceSelect(id = '', selectedVoice = 'alloy') {
const select = document.createElement('select');
if (id) select.id = id;
const voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
voices.forEach(voice => {
const option = document.createElement('option');
option.value = voice;
option.textContent = voice.charAt(0).toUpperCase() + voice.slice(1);
if (voice === selectedVoice) option.selected = true;
select.appendChild(option);
});
return select;
}
// Create standardized modal header with close button
function createModalHeader(title, onClose) {
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;
`;
}
const h1 = document.createElement('h1');
h1.textContent = title;
if (USE_CUSTOM_STYLING) {
h1.style.cssText = 'margin: 0; font-family: "ChunkFiveRegular", monospace; font-size: 20px;';
}
const closeButton = createStyledButton('X', 'close');
closeButton.onclick = onClose;
header.appendChild(h1);
header.appendChild(closeButton);
return header;
}
// Function to open TTS modal
function openTTSModal() {
// Check if Hermes modal is open to set appropriate z-index
@ -2243,12 +2656,235 @@ function scrollToChatBox() {
}
}
// -------------------------
// Class-based Initialization System
// -------------------------
// Initialize uncloseai elements based on class
function initializeUncloseaiElements() {
const uncloseaiElements = document.querySelectorAll('.uncloseai');
uncloseaiElements.forEach(element => {
const features = element.dataset.features || 'full';
const type = element.dataset.type || 'standard';
// Create container for this uncloseai instance
const container = document.createElement('div');
container.className = 'uncloseai-container';
container.style.cssText = 'width: 100%; margin: 10px 0;';
if (features === 'full' || type === 'full') {
createFullInterface(container);
} else {
createCustomInterface(container, features.split(','));
}
element.appendChild(container);
});
}
// Create full interface with all features
function createFullInterface(container) {
// Create chat section
const chatSection = createChatFeature();
container.appendChild(chatSection);
// Create controls section
const controlsSection = document.createElement('div');
controlsSection.style.cssText = 'display: flex; gap: 10px; flex-wrap: wrap; margin: 10px 0;';
const ttsFeature = createTTSFeature();
const uploadFeature = createUploadFeature();
const readFeature = createReadFeature();
controlsSection.appendChild(ttsFeature);
controlsSection.appendChild(uploadFeature);
controlsSection.appendChild(readFeature);
container.appendChild(controlsSection);
}
// Create custom interface with specific features
function createCustomInterface(container, features) {
const featureContainer = document.createElement('div');
featureContainer.style.cssText = 'display: flex; gap: 10px; flex-wrap: wrap; margin: 10px 0;';
features.forEach(feature => {
const featureName = feature.trim().toLowerCase();
let featureElement = null;
switch (featureName) {
case 'chat':
featureElement = createChatFeature();
container.appendChild(featureElement);
return; // Chat gets full width, don't add to featureContainer
case 'tts':
featureElement = createTTSFeature();
break;
case 'upload':
featureElement = createUploadFeature();
break;
case 'read':
featureElement = createReadFeature();
break;
default:
console.warn(`Unknown uncloseai feature: ${featureName}`);
}
if (featureElement) {
featureContainer.appendChild(featureElement);
}
});
if (featureContainer.children.length > 0) {
container.appendChild(featureContainer);
}
}
// Individual feature creators - refactored to use shared utilities
function createChatFeature() {
const chatDiv = document.createElement('div');
chatDiv.style.cssText = 'width: 100%; margin: 10px 0;';
const chatBox = document.createElement('div');
chatBox.style.cssText = `
border: 1px solid #ccc;
border-radius: 8px;
height: 300px;
overflow-y: auto;
padding: 16px;
margin-bottom: 10px;
background: #f9f9f9;
`;
const inputContainer = document.createElement('div');
inputContainer.style.cssText = 'display: flex; gap: 8px;';
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Ask AI about this page...';
input.style.cssText = 'flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;';
const sendBtn = createStyledButton('Send', 'primary');
// Set up chat functionality
const handleSend = async () => {
const message = input.value.trim();
if (!message) return;
input.value = '';
// Add user message using shared utility
const userMsg = createChatMessage(message, 'user');
chatBox.appendChild(userMsg);
// Add AI response using shared utility
const aiMsg = createChatMessage('<em>Thinking...</em>', 'ai', true);
chatBox.appendChild(aiMsg);
chatBox.scrollTop = chatBox.scrollHeight;
try {
let response = '';
for await (const chunk of sendMessage(message)) {
response += chunk;
aiMsg.innerHTML = `<strong>AI:</strong> ${marked.parse(response)}`;
chatBox.scrollTop = chatBox.scrollHeight;
}
} catch (error) {
aiMsg.innerHTML = `<strong>AI:</strong> <em>Error: ${error.message}</em>`;
}
};
sendBtn.onclick = handleSend;
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSend();
}
});
inputContainer.appendChild(input);
inputContainer.appendChild(sendBtn);
chatDiv.appendChild(chatBox);
chatDiv.appendChild(inputContainer);
return chatDiv;
}
function createTTSFeature() {
const ttsBtn = createStyledButton('🔊 TTS Anything', 'success');
ttsBtn.onclick = () => openTTSModal();
return ttsBtn;
}
function createUploadFeature() {
const uploadContainer = document.createElement('div');
uploadContainer.style.cssText = 'display: inline-block;';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
const uploadBtn = createStyledButton('📁 Upload File', 'warning');
uploadBtn.onclick = () => fileInput.click();
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
uploadBtn.textContent = 'Uploading...';
uploadBtn.disabled = true;
try {
showProgressIndicator('Processing file...');
const response = await uploadFile(file);
hideProgressIndicator();
// Show response in a simple alert for now - could be enhanced
alert(`File processed: ${response.substring(0, 200)}...`);
uploadBtn.textContent = '📁 Upload File';
uploadBtn.disabled = false;
fileInput.value = '';
} catch (error) {
hideProgressIndicator();
alert(`Upload failed: ${error.message}`);
uploadBtn.textContent = '📁 Upload File';
uploadBtn.disabled = false;
}
};
uploadContainer.appendChild(fileInput);
uploadContainer.appendChild(uploadBtn);
return uploadContainer;
}
function createReadFeature() {
const readBtn = createStyledButton('📖 Read Page', 'info');
readBtn.onclick = () => readPageWithHermes();
return readBtn;
}
// Configuration flag for showing floating button
const SHOW_FLOATING_BUTTON = window.UNCLOSEAI_SHOW_BUTTON !== false;
// Initialize on page load
window.onload = () => {
initializeChatInterface();
createModelSelectionDropdown();
addRefreshModelsButton();
createFloatingAIButton();
// Only create floating button if not disabled
if (SHOW_FLOATING_BUTTON) {
createFloatingAIButton();
}
// Initialize class-based elements
initializeUncloseaiElements();
};
// Export functions to global scope