Spaces:
Running
Running
| 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 = ` | |
| <div class="message-avatar">${avatar}</div> | |
| <div class="message-content">${formatContent(content)}</div> | |
| `; | |
| 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, '<') | |
| .replace(/>/g, '>'); | |
| // Format code blocks | |
| formatted = formatted.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => { | |
| return `<pre><code class="language-${lang}">${escapeHtml(code.trim())}</code></pre>`; | |
| }); | |
| // Format inline code | |
| formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>'); | |
| // Format bold text | |
| formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); | |
| // Format line breaks | |
| formatted = formatted.replace(/\n/g, '<br>'); | |
| 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 = ` | |
| <div class="message-avatar">⚠️</div> | |
| <div class="message-content" style="color: var(--error-color);"> | |
| <strong>Error:</strong> ${error} | |
| </div> | |
| `; | |
| 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(); | |
| </arg_value>=== 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 |