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:
parent
db57965ef4
commit
0b573a2a86
3 changed files with 800 additions and 14 deletions
117
demo.html
117
demo.html
|
|
@ -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>
|
||||
|
|
|
|||
57
index.html
57
index.html
|
|
@ -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"><script src="https://uncloseai.com/uncloseai.js" type="module"></script></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"><script>
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script></code></pre>
|
||||
|
||||
<p>To disable the floating button (if you only want the API functions):</p>
|
||||
<pre><code class="javascript"><script>
|
||||
window.UNCLOSEAI_SHOW_BUTTON = false;
|
||||
</script></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"><div class="uncloseai" data-features="full"></div></code></pre>
|
||||
|
||||
<p><strong>Chat Only:</strong> Just the AI chat interface</p>
|
||||
<pre><code class="html"><div class="uncloseai" data-features="chat"></div></code></pre>
|
||||
|
||||
<p><strong>Specific Features:</strong> Choose which buttons to include</p>
|
||||
<pre><code class="html"><div class="uncloseai" data-features="tts,upload,read"></div>
|
||||
<div class="uncloseai" data-features="tts"></div></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>
|
||||
|
|
|
|||
640
uncloseai.js
640
uncloseai.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue