Spaces:
Running
Running
| class ChatbotApp { | |
| constructor() { | |
| this.generator = null; | |
| this.messages = [ | |
| { role: "system", content: "You are a helpful assistant. Be concise and friendly." } | |
| ]; | |
| this.isGenerating = false; | |
| this.init(); | |
| } | |
| async init() { | |
| this.setupElements(); | |
| this.setupEventListeners(); | |
| await this.loadModel(); | |
| } | |
| setupElements() { | |
| this.chatMessages = document.getElementById('chatMessages'); | |
| this.messageInput = document.getElementById('messageInput'); | |
| this.sendButton = document.getElementById('sendButton'); | |
| this.status = document.getElementById('status'); | |
| this.loadingIndicator = document.getElementById('loadingIndicator'); | |
| this.progressFill = document.getElementById('progressFill'); | |
| this.progressText = document.getElementById('progressText'); | |
| this.charCount = document.getElementById('charCount'); | |
| } | |
| setupEventListeners() { | |
| // Send button click | |
| this.sendButton.addEventListener('click', () => this.sendMessage()); | |
| // Enter key to send (Shift+Enter for new line) | |
| this.messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.sendMessage(); | |
| } | |
| }); | |
| // Character count | |
| this.messageInput.addEventListener('input', () => { | |
| const length = this.messageInput.value.length; | |
| this.charCount.textContent = `${length} / 1000`; | |
| // Auto-resize textarea | |
| this.messageInput.style.height = 'auto'; | |
| this.messageInput.style.height = Math.min(this.messageInput.scrollHeight, 120) + 'px'; | |
| }); | |
| // Start worker for model loading | |
| this.setupWorker(); | |
| } | |
| setupWorker() { | |
| // Create worker code as a blob | |
| const workerCode = ` | |
| self.addEventListener('message', async (e) => { | |
| if (e.data.type === 'loadModel') { | |
| try { | |
| // Simulate progress updates | |
| self.postMessage({ type: 'progress', progress: 10 }); | |
| const { pipeline, TextStreamer } = await | |
| import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.1/dist/transformers.min.js'); | |
| self.postMessage({ type: 'progress', progress: 30 }); | |
| const generator = await pipeline( | |
| "text-generation", | |
| "onnx-community/gemma-3-270m-it-ONNX", | |
| { | |
| dtype: "fp32", | |
| progress_callback: (progress) => { | |
| if (progress.status === 'downloading') { | |
| const percent = Math.round((progress.loaded / progress.total) * 100); | |
| self.postMessage({ type: 'progress', progress: 30 + (percent * 0.6) }); | |
| } else if (progress.status === 'ready') { | |
| self.postMessage({ type: 'progress', progress: 100 }); | |
| } | |
| } | |
| } | |
| ); | |
| self.postMessage({ type: 'modelLoaded', generator: generator }); | |
| } catch (error) { | |
| self.postMessage({ type: 'error', error: error.message }); | |
| } | |
| } | |
| }); | |
| `; | |
| const blob = new Blob([workerCode], { type: 'application/javascript' }); | |
| this.worker = new Worker(URL.createObjectURL(blob)); | |
| this.worker.addEventListener('message', (e) => { | |
| switch(e.data.type) { | |
| case 'progress': | |
| this.updateProgress(e.data.progress); | |
| break; | |
| case 'modelLoaded': | |
| this.onModelLoaded(e.data.generator); | |
| break; | |
| case 'error': | |
| this.onError(e.data.error); | |
| break; | |
| } | |
| }); | |
| } | |
| async loadModel() { | |
| this.status.textContent = 'Downloading model...'; | |
| this.loadingIndicator.classList.remove('hidden'); | |
| this.worker.postMessage({ type: 'loadModel' }); | |
| } | |
| updateProgress(progress) { | |
| this.progressFill.style.width = `${progress}%`; | |
| this.progressText.textContent = `${Math.round(progress)}%`; | |
| } | |
| onModelLoaded(generator) { | |
| this.generator = generator; | |
| this.loadingIndicator.classList.add('hidden'); | |
| this.status.textContent = 'Ready'; | |
| this.messageInput.disabled = false; | |
| this.sendButton.disabled = false; | |
| this.messageInput.focus(); | |
| } | |
| onError(error) { | |
| this.loadingIndicator.classList.add('hidden'); | |
| this.status.textContent = 'Error loading model'; | |
| this.addMessage('system', `Error: ${error.message}. Please refresh the page to try again.`); | |
| } | |
| async sendMessage() { | |
| const message = this.messageInput.value.trim(); | |
| if (!message || this.isGenerating || !this.generator) return; | |
| // Add user message | |
| this.addMessage('user', message); | |
| this.messages.push({ role: "user", content: message }); | |
| // Clear input | |
| this.messageInput.value = ''; | |
| this.messageInput.style.height = 'auto'; | |
| this.charCount.textContent = '0 / 1000'; | |
| // Disable input | |
| this.isGenerating = true; | |
| this.messageInput.disabled = true; | |
| this.sendButton.disabled = true; | |
| this.status.textContent = 'Thinking...'; | |
| // Add AI message with typing indicator | |
| const aiMessageId = this.addMessage('ai', '', true); | |
| try { | |
| // Generate response with streaming | |
| const streamer = new TextStreamer(this.generator.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (text) => { | |
| this.updateMessage(aiMessageId, text); | |
| } | |
| }); | |
| const output = await this.generator(this.messages, { | |
| max_new_tokens: 512, | |
| do_sample: false, | |
| streamer: streamer, | |
| }); | |
| // Add the AI response to messages | |
| const aiResponse = output[0].generated_text.at(-1).content; | |
| this.messages.push({ role: "assistant", content: aiResponse }); | |
| } catch (error) { | |
| this.updateMessage(aiMessageId, 'Sorry, I encountered an error. Please try again.'); | |
| console.error('Generation error:', error); | |
| } finally { | |
| // Re-enable input | |
| this.isGenerating = false; | |
| this.messageInput.disabled = false; | |
| this.sendButton.disabled = false; | |
| this.status.textContent = 'Ready'; | |
| this.messageInput.focus(); | |
| } | |
| } | |
| addMessage(role, content, isTyping = false) { | |
| const messageId = 'msg-' + Date.now(); | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${role}`; | |
| messageDiv.id = messageId; | |
| const avatar = document.createElement('div'); | |
| avatar.className = role === 'user' ? 'user-avatar' : 'ai-avatar'; | |
| avatar.innerHTML = role === 'user' | |
| ? '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> | |
| <circle cx="12" cy="7" r="4"></circle> | |
| </svg>' | |
| : '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z" /> | |
| <path d="M12 8v4" /> | |
| <circle cx="12" cy="16" r="1" /> | |
| </svg>'; | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = 'message-content'; | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'message-bubble'; | |
| if (isTyping) { | |
| bubble.innerHTML = '<div class="typing-indicator"> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| </div>'; | |
| } else { | |
| bubble.textContent = content; | |
| } | |
| const time = document.createElement('div'); | |
| time.className = 'message-time'; | |
| time.textContent = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); | |
| messageContent.appendChild(bubble); | |
| messageContent.appendChild(time); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(messageContent); | |
| this.chatMessages.appendChild(messageDiv); | |
| this.chatMessages.scrollTop = this.chatMessages.scrollHeight; | |
| return messageId; | |
| } | |
| updateMessage(messageId, content) { | |
| const messageElement = document.getElementById(messageId); | |
| if (messageElement) { | |
| const bubble = messageElement.querySelector('.message-bubble'); | |
| bubble.textContent = content; | |
| this.chatMessages.scrollTop = this.chatMessages.scrollHeight; | |
| } | |
| } | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new ChatbotApp(); | |
| }); |