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'; | |
| }); | |
| // Initialize worker | |
| this.setupWorker(); | |
| } | |
| setupWorker() { | |
| // Create worker from separate file | |
| this.worker = new Worker('worker.js'); | |
| 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}. 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 transformers.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(); | |
| }); |