import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.0'; // ============================================ // CONFIGURATION // ============================================ const CONFIG = { model: 'Xenova/Llama-3.2-3B-Instruct', // Quantized version for faster loading maxTokens: 512, temperature: 0.7, topP: 0.9, doSample: true, repetitionPenalty: 1.1 }; // ============================================ // DOM ELEMENTS // ============================================ const elements = { chatContainer: document.getElementById('chat-container'), messagesList: document.getElementById('messages-list'), userInput: document.getElementById('user-input'), sendBtn: document.getElementById('send-btn'), loadingIndicator: document.getElementById('loading-indicator'), loadingMessage: document.getElementById('loading-message'), progressBar: document.getElementById('progress-bar'), progressFill: document.getElementById('progress-fill'), welcomeScreen: document.getElementById('welcome-screen'), statusDot: document.querySelector('.status-dot'), statusText: document.querySelector('.status-text'), exampleBtns: document.querySelectorAll('.example-btn') }; // ============================================ // APPLICATION STATE // ============================================ let appState = { modelLoaded: false, isGenerating: false, messages: [] }; // ============================================ // WORKER HANDLERS // ============================================ let currentResponse = ''; let isWorkerBusy = false; // Handle worker messages worker.onmessage = (e) => { const { type, payload } = e.data; switch (type) { case 'loading': updateModelStatus('loading'); break; case 'progress': updateProgress(payload.progress, payload.message); break; case 'loaded': appState.modelLoaded = true; updateModelStatus('ready'); hideLoading(); hideWelcomeScreen(); break; case 'result': handleGenerationComplete(payload.result); break; case 'error': showError(payload.error); break; case 'cancelled': stopGeneration(); break; } }; // ============================================ // MODEL INITIALIZATION // ============================================ async function initializeModel() { try { showLoading('Initializing model...'); worker.postMessage({ type: 'load', payload: { model: CONFIG.model } }); } catch (error) { showError(`Failed to initialize model: ${error.message}`); } } // ============================================ // GENERATION FUNCTIONS // ============================================ async function sendMessage(message) { if (!appState.modelLoaded || isWorkerBusy) return; // Add user message to chat addMessage('user', message); appState.messages.push({ role: 'user', content: message }); // Clear input elements.userInput.value = ''; elements.userInput.style.height = 'auto'; updateSendButton(); // Start generation isWorkerBusy = true; currentResponse = ''; showLoading('Generating response...'); try { // Create prompt for the model const prompt = createPrompt(); worker.postMessage({ type: 'generate', payload: { prompt: prompt, maxTokens: CONFIG.maxTokens, temperature: CONFIG.temperature, topP: CONFIG.topP, doSample: CONFIG.doSample, repetitionPenalty: CONFIG.repetitionPenalty } }); } catch (error) { showError(`Generation failed: ${error.message}`); isWorkerBusy = false; } } function createPrompt() { // Convert messages to chat format if (appState.messages.length === 0) { return 'You are a helpful AI assistant. Please answer the user\'s question helpfully and accurately.'; } return appState.messages .map(msg => `${msg.role}: ${msg.content}`) .join('\n\n') + '\n\nassistant:'; } // ============================================ // MESSAGE HANDLING // ============================================ function addMessage(role, content) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}`; const avatar = role === 'user' ? '👤' : '🤖'; messageDiv.innerHTML = `
${avatar}
${formatContent(content)}
`; elements.messagesList.appendChild(messageDiv); scrollToBottom(); return messageDiv; } function handleGenerationComplete(generatedText) { // Add AI response to chat const messageDiv = addMessage('ai', ''); // Create content element for streaming const contentElement = messageDiv.querySelector('.message-content'); contentElement.classList.add('typing-cursor'); // Stream the response streamText(contentElement, generatedText) .then(() => { contentElement.classList.remove('typing-cursor'); appState.messages.push({ role: 'assistant', content: generatedText }); isWorkerBusy = false; hideLoading(); }) .catch(error => { showError(`Streaming failed: ${error.message}`); isWorkerBusy = false; hideLoading(); }); } async function streamText(element, text) { const words = text.split(' '); let currentText = ''; for (let i = 0; i < words.length; i++) { currentText += words[i] + ' '; element.innerHTML = formatContent(currentText); scrollToBottom(); // Add a small delay for more natural typing effect await new Promise(resolve => setTimeout(resolve, 10 + Math.random() * 20)); } } function formatContent(text) { // Simple text formatting // Escape HTML let formatted = text .replace(/&/g, '&') .replace(//g, '>'); // Format code blocks formatted = formatted.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => { return `
${escapeHtml(code.trim())}
`; }); // Format inline code formatted = formatted.replace(/`([^`]+)`/g, '$1'); // Format bold text formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '$1'); // Format line breaks formatted = formatted.replace(/\n/g, '
'); return formatted; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ============================================ // UI FUNCTIONS // ============================================ function showLoading(message) { elements.loadingIndicator.classList.remove('hidden'); elements.loadingMessage.textContent = message; elements.progressBar.classList.remove('hidden'); elements.progressFill.style.width = '0%'; } function hideLoading() { elements.loadingIndicator.classList.add('hidden'); elements.progressBar.classList.add('hidden'); } function updateProgress(progress, message) { elements.progressFill.style.width = `${progress}%`; if (message) { elements.loadingMessage.textContent = message; } } function updateModelStatus(status) { elements.statusDot.className = 'status-dot'; if (status === 'loading') { elements.statusDot.classList.add('loading'); elements.statusText.textContent = 'Loading model...'; } else if (status === 'ready') { elements.statusDot.classList.add('ready'); elements.statusText.textContent = 'Ready'; } } function showError(error) { console.error('Error:', error); hideLoading(); // Add error message to chat const messageDiv = document.createElement('div'); messageDiv.className = `message ai`; messageDiv.innerHTML = `
⚠️
Error: ${error}
`; elements.messagesList.appendChild(messageDiv); isWorkerBusy = false; } function scrollToBottom() { elements.chatContainer.scrollTop = elements.chatContainer.scrollHeight; } function hideWelcomeScreen() { elements.welcomeScreen.style.display = 'none'; } // ============================================ // EVENT LISTENERS // ============================================ function updateSendButton() { const hasText = elements.userInput.value.trim().length > 0; elements.sendBtn.disabled = !hasText || isWorkerBusy; } async function handleInput() { updateSendButton(); // Auto-resize textarea elements.userInput.style.height = 'auto'; elements.userInput.style.height = Math.min(elements.userInput.scrollHeight, 200) + 'px'; } async function handleSend() { const message = elements.userInput.value.trim(); if (message && !isWorkerBusy) { await sendMessage(message); } } // ============================================ // INITIALIZATION // ============================================ async function init() { // Configure environment env.allowLocalModels = false; env.useBrowserCache = true; // Initialize model await initializeModel(); // Setup event listeners elements.userInput.addEventListener('input', handleInput); elements.userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); elements.sendBtn.addEventListener('click', handleSend); // Setup example prompts elements.exampleBtns.forEach(btn => { btn.addEventListener('click', () => { elements.userInput.value = btn.dataset.prompt; handleInput(); elements.userInput.focus(); }); }); // Focus input on load elements.userInput.focus(); } // Start the application init(); === worker.js === // This file is handled as an inline blob in index.html for single-file deployment // The worker code is provided as a string in the index.html script tag