import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.0'; // Configure environment env.allowLocalModels = false; // State let generator = null; let currentModel = null; let isVisionModel = false; let currentImage = null; let isGenerating = false; let conversationHistory = []; // DOM Elements const modelSelect = document.getElementById('model-select'); const loadModelBtn = document.getElementById('load-model-btn'); const loadingContainer = document.getElementById('loading-container'); const loadingStatus = document.getElementById('loading-status'); const progressBar = document.getElementById('progress-bar'); const progressText = document.getElementById('progress-text'); const chatContainer = document.getElementById('chat-container'); const chatMessages = document.getElementById('chat-messages'); const imageInput = document.getElementById('image-input'); const attachImageBtn = document.getElementById('attach-image-btn'); const imageUrlInput = document.getElementById('image-url-input'); const loadUrlBtn = document.getElementById('load-url-btn'); const imagePreviewContainer = document.getElementById('image-preview-container'); const imagePreview = document.getElementById('image-preview'); const removeImageBtn = document.getElementById('remove-image-btn'); const userInput = document.getElementById('user-input'); const sendBtn = document.getElementById('send-btn'); const errorContainer = document.getElementById('error-container'); const errorMessage = document.getElementById('error-message'); const dismissErrorBtn = document.getElementById('dismiss-error-btn'); // Settings const maxTokensSlider = document.getElementById('max-tokens'); const maxTokensValue = document.getElementById('max-tokens-value'); const temperatureSlider = document.getElementById('temperature'); const temperatureValue = document.getElementById('temperature-value'); const topPSlider = document.getElementById('top-p'); const topPValue = document.getElementById('top-p-value'); // Vision model identifiers const VISION_MODELS = ['SmolVLM', 'Fara', 'llava', 'vision']; function isVisionModelSelected(modelId) { return VISION_MODELS.some(vm => modelId.toLowerCase().includes(vm.toLowerCase())); } // Progress callback for model loading function progressCallback(progress) { if (progress.status === 'initiate') { loadingStatus.textContent = `Loading ${progress.file || 'model'}...`; } else if (progress.status === 'download') { loadingStatus.textContent = `Downloading ${progress.file || 'model'}...`; } else if (progress.status === 'progress') { const percent = Math.round(progress.progress || 0); progressBar.style.width = `${percent}%`; progressText.textContent = `${percent}%`; loadingStatus.textContent = `Downloading ${progress.file || 'model'}...`; } else if (progress.status === 'done') { loadingStatus.textContent = `Loaded ${progress.file || 'model'}`; } else if (progress.status === 'ready') { loadingStatus.textContent = 'Model ready!'; progressBar.style.width = '100%'; progressText.textContent = '100%'; } } // Load model async function loadModel() { const modelId = modelSelect.value; if (currentModel === modelId && generator) { showError('Model already loaded!'); return; } try { loadModelBtn.disabled = true; loadingContainer.classList.remove('hidden'); chatContainer.classList.add('hidden'); progressBar.style.width = '0%'; progressText.textContent = '0%'; loadingStatus.textContent = 'Initializing...'; isVisionModel = isVisionModelSelected(modelId); // Clean up previous model if (generator) { generator = null; } // Determine device - try WebGPU first let device = 'wasm'; if (navigator.gpu) { try { const adapter = await navigator.gpu.requestAdapter(); if (adapter) { device = 'webgpu'; loadingStatus.textContent = 'Using WebGPU acceleration...'; } } catch (e) { console.log('WebGPU not available, falling back to WASM'); } } loadingStatus.textContent = `Loading model on ${device.toUpperCase()}...`; // Create pipeline based on model type if (isVisionModel) { generator = await pipeline('image-text-to-text', modelId, { device: device, dtype: 'q4f16', progress_callback: progressCallback, }); } else { generator = await pipeline('text-generation', modelId, { device: device, dtype: 'q4f16', progress_callback: progressCallback, }); } currentModel = modelId; conversationHistory = []; // Update UI loadingContainer.classList.add('hidden'); chatContainer.classList.remove('hidden'); sendBtn.disabled = false; // Update attach button visibility attachImageBtn.style.display = isVisionModel ? 'block' : 'none'; imageUrlInput.style.display = isVisionModel ? 'block' : 'none'; loadUrlBtn.style.display = isVisionModel ? 'block' : 'none'; // Clear chat and show ready message chatMessages.innerHTML = `
`; } catch (error) { console.error('Error loading model:', error); showError(`Failed to load model: ${error.message}`); loadingContainer.classList.add('hidden'); } finally { loadModelBtn.disabled = false; } } // Handle image upload function handleImageUpload(file) { if (!file) return; const reader = new FileReader(); reader.onload = (e) => { currentImage = e.target.result; imagePreview.src = currentImage; imagePreviewContainer.classList.remove('hidden'); }; reader.readAsDataURL(file); } // Load image from URL async function loadImageFromUrl() { const url = imageUrlInput.value.trim(); if (!url) return; try { currentImage = url; imagePreview.src = url; imagePreviewContainer.classList.remove('hidden'); imageUrlInput.value = ''; } catch (error) { showError('Failed to load image from URL'); } } // Remove attached image function removeImage() { currentImage = null; imagePreview.src = ''; imagePreviewContainer.classList.add('hidden'); imageInput.value = ''; } // Add message to chat function addMessage(role, content, imageUrl = null) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}`; const label = document.createElement('div'); label.className = 'message-label'; label.textContent = role === 'user' ? 'You' : 'Assistant'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; if (imageUrl) { const img = document.createElement('img'); img.src = imageUrl; img.className = 'message-image'; contentDiv.appendChild(img); } const textSpan = document.createElement('span'); textSpan.textContent = content; contentDiv.appendChild(textSpan); messageDiv.appendChild(label); messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; return contentDiv; } // Add typing indicator function addTypingIndicator() { const messageDiv = document.createElement('div'); messageDiv.className = 'message assistant'; messageDiv.id = 'typing-indicator'; const label = document.createElement('div'); label.className = 'message-label'; label.textContent = 'Assistant'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; const typingDiv = document.createElement('div'); typingDiv.className = 'typing-indicator'; typingDiv.innerHTML = ''; contentDiv.appendChild(typingDiv); messageDiv.appendChild(label); messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; } // Remove typing indicator function removeTypingIndicator() { const indicator = document.getElementById('typing-indicator'); if (indicator) { indicator.remove(); } } // Send message async function sendMessage() { const text = userInput.value.trim(); if (!text || !generator || isGenerating) return; isGenerating = true; sendBtn.disabled = true; userInput.disabled = true; try { // Add user message addMessage('user', text, currentImage); // Clear input userInput.value = ''; const imageForMessage = currentImage; removeImage(); // Show typing indicator addTypingIndicator(); // Get generation settings const maxTokens = parseInt(maxTokensSlider.value); const temperature = parseFloat(temperatureSlider.value); const topP = parseFloat(topPSlider.value); let response; if (isVisionModel && imageForMessage) { // Vision model with image const messages = [ { role: 'user', content: [ { type: 'image', image: imageForMessage }, { type: 'text', text: text } ] } ]; const output = await generator(messages, { max_new_tokens: maxTokens, temperature: temperature, top_p: topP, do_sample: temperature > 0, }); response = output[0].generated_text.at(-1).content; } else if (isVisionModel) { // Vision model without image - text only const messages = [ { role: 'user', content: [ { type: 'text', text: text } ] } ]; const output = await generator(messages, { max_new_tokens: maxTokens, temperature: temperature, top_p: topP, do_sample: temperature > 0, }); response = output[0].generated_text.at(-1).content; } else { // Text-only model conversationHistory.push({ role: 'user', content: text }); const output = await generator(conversationHistory, { max_new_tokens: maxTokens, temperature: temperature, top_p: topP, do_sample: temperature > 0, }); const generatedMessages = output[0].generated_text; const assistantMessage = generatedMessages[generatedMessages.length - 1]; response = assistantMessage.content; conversationHistory.push({ role: 'assistant', content: response }); } // Remove typing indicator and add response removeTypingIndicator(); addMessage('assistant', response); } catch (error) { console.error('Error generating response:', error); removeTypingIndicator(); showError(`Generation error: ${error.message}`); } finally { isGenerating = false; sendBtn.disabled = false; userInput.disabled = false; userInput.focus(); } } // Show error function showError(message) { errorMessage.textContent = message; errorContainer.classList.remove('hidden'); } // Hide error function hideError() { errorContainer.classList.add('hidden'); } // Event Listeners loadModelBtn.addEventListener('click', loadModel); attachImageBtn.addEventListener('click', () => { if (isVisionModel) { imageInput.click(); } }); imageInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleImageUpload(e.target.files[0]); } }); loadUrlBtn.addEventListener('click', loadImageFromUrl); imageUrlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { loadImageFromUrl(); } }); removeImageBtn.addEventListener('click', removeImage); sendBtn.addEventListener('click', sendMessage); userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); dismissErrorBtn.addEventListener('click', hideError); // Settings sliders maxTokensSlider.addEventListener('input', () => { maxTokensValue.textContent = maxTokensSlider.value; }); temperatureSlider.addEventListener('input', () => { temperatureValue.textContent = temperatureSlider.value; }); topPSlider.addEventListener('input', () => { topPValue.textContent = topPSlider.value; }); // Drag and drop for images chatContainer.addEventListener('dragover', (e) => { if (isVisionModel) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; } }); chatContainer.addEventListener('drop', (e) => { if (isVisionModel) { e.preventDefault(); const files = e.dataTransfer.files; if (files.length > 0 && files[0].type.startsWith('image/')) { handleImageUpload(files[0]); } } }); // Initialize document.addEventListener('DOMContentLoaded', () => { // Hide image controls initially attachImageBtn.style.display = 'none'; imageUrlInput.style.display = 'none'; loadUrlBtn.style.display = 'none'; });