Spaces:
Running
Running
| // Main application logic for Apple Style Chatbot | |
| import { pipeline, TextStreamer } from 'https://cdn.jsdelivr.net/npm/@huggingface/[email protected]'; | |
| class AppleChatbot { | |
| constructor() { | |
| this.generator = null; | |
| this.isGenerating = false; | |
| this.messages = []; | |
| this.settings = { | |
| maxTokens: 512, | |
| temperature: 0.7, | |
| streaming: true | |
| }; | |
| this.init(); | |
| } | |
| async init() { | |
| // Initialize UI elements | |
| this.loadingScreen = document.getElementById('loadingScreen'); | |
| this.chatContainer = document.getElementById('chatContainer'); | |
| this.inputArea = document.getElementById('inputArea'); | |
| this.messagesList = document.getElementById('messagesList'); | |
| this.messageInput = document.getElementById('messageInput'); | |
| this.sendBtn = document.getElementById('sendBtn'); | |
| this.clearBtn = document.getElementById('clearBtn'); | |
| this.settingsBtn = document.getElementById('settingsBtn'); | |
| this.progressFill = document.getElementById('progressFill'); | |
| this.loadingStatus = document.getElementById('loadingStatus'); | |
| this.charCount = document.getElementById('charCount'); | |
| // Setup event listeners | |
| this.setupEventListeners(); | |
| // Load model | |
| await this.loadModel(); | |
| } | |
| setupEventListeners() { | |
| // Send button | |
| this.sendBtn.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', (e) => { | |
| const length = e.target.value.length; | |
| this.charCount.textContent = `${length} / 1000`; | |
| this.sendBtn.disabled = length === 0 || this.isGenerating; | |
| // Auto-resize textarea | |
| e.target.style.height = 'auto'; | |
| e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; | |
| }); | |
| // Clear button | |
| this.clearBtn.addEventListener('click', () => this.clearChat()); | |
| // Settings | |
| this.settingsBtn.addEventListener('click', () => this.openSettings()); | |
| document.getElementById('closeSettings').addEventListener('click', () => this.closeSettings()); | |
| document.getElementById('saveSettings').addEventListener('click', () => this.saveSettings()); | |
| // Settings inputs | |
| document.getElementById('maxTokens').addEventListener('input', (e) => { | |
| document.getElementById('maxTokensValue').textContent = e.target.value; | |
| }); | |
| document.getElementById('temperature').addEventListener('input', (e) => { | |
| document.getElementById('temperatureValue').textContent = e.target.value; | |
| }); | |
| // Close modal on backdrop click | |
| document.getElementById('settingsModal').addEventListener('click', (e) => { | |
| if (e.target.id === 'settingsModal') { | |
| this.closeSettings(); | |
| } | |
| }); | |
| } | |
| async loadModel() { | |
| try { | |
| this.updateProgress(10, 'Downloading model...'); | |
| // Initialize the text generation pipeline with progress callback | |
| this.generator = await pipeline( | |
| 'text-generation', | |
| 'onnx-community/gemma-3-270m-it-ONNX', | |
| { | |
| dtype: 'fp32', | |
| progress_callback: (progress) => { | |
| const percentage = Math.round(progress.progress * 100); | |
| const status = progress.status || 'Loading...'; | |
| this.updateProgress(percentage, status); | |
| } | |
| } | |
| ); | |
| this.updateProgress(100, 'Model ready!'); | |
| // Hide loading screen and show chat | |
| setTimeout(() => { | |
| this.loadingScreen.style.display = 'none'; | |
| this.chatContainer.style.display = 'block'; | |
| this.inputArea.style.display = 'block'; | |
| this.messageInput.focus(); | |
| }, 500); | |
| } catch (error) { | |
| console.error('Error loading model:', error); | |
| this.showError('Failed to load AI model. Please refresh the page and try again.'); | |
| } | |
| } | |
| updateProgress(progress, status) { | |
| this.progressFill.style.width = `${progress}%`; | |
| this.loadingStatus.textContent = status; | |
| } | |
| async sendMessage() { | |
| const text = this.messageInput.value.trim(); | |
| if (!text || this.isGenerating || !this.generator) return; | |
| // Add user message | |
| this.addMessage('user', text); | |
| this.messages.push({ role: 'user', content: text }); | |
| // Clear input | |
| this.messageInput.value = ''; | |
| this.charCount.textContent = '0 / 1000'; | |
| this.sendBtn.disabled = true; | |
| this.messageInput.style.height = 'auto'; | |
| // Show typing indicator | |
| const typingMessage = this.showTypingIndicator(); | |
| try { | |
| this.isGenerating = true; | |
| // Generate response | |
| const response = await this.generateResponse(); | |
| // Remove typing indicator | |
| typingMessage.remove(); | |
| // Add assistant response | |
| this.addMessage('assistant', response); | |
| this.messages.push({ role: 'assistant', content: response }); | |
| } catch (error) { | |
| console.error('Error generating response:', error); | |
| typingMessage.remove(); | |
| this.addMessage('assistant', 'Sorry, I encountered an error while generating a response. Please try again.'); | |
| } finally { | |
| this.isGenerating = false; | |
| this.sendBtn.disabled = false; | |
| this.messageInput.focus(); | |
| } | |
| } | |
| async generateResponse() { | |
| if (!this.generator) throw new Error('Generator not initialized'); | |
| const generationParams = { | |
| max_new_tokens: this.settings.maxTokens, | |
| do_sample: this.settings.temperature > 0, | |
| temperature: this.settings.temperature, | |
| }; | |
| // Create prompt from messages | |
| const prompt = this.createPrompt(); | |
| // Generate response | |
| const output = await this.generator(prompt, generationParams); | |
| // Extract the assistant's response | |
| const generatedText = output[0].generated_text; | |
| const response = generatedText.replace(prompt, '').trim(); | |
| return response || 'I apologize, but I couldn\'t generate a response.'; | |
| } | |
| createPrompt() { | |
| // Create a simple prompt from the conversation history | |
| let prompt = ''; | |
| for (const msg of this.messages) { | |
| if (msg.role === 'user') { | |
| prompt += `User: ${msg.content}\n`; | |
| } else { | |
| prompt += `Assistant: ${msg.content}\n`; | |
| } | |
| } | |
| prompt += 'Assistant: '; | |
| return prompt; | |
| } | |
| addMessage(role, content) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${role}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.innerHTML = role === 'user' | |
| ? '<svg width="24" height="24" 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" /><circle cx="12" cy="7" r="4" /></svg>' | |
| : '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7V17L12 22L22 17V7L12 2Z" /></svg>'; | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = 'message-content'; | |
| const messageText = document.createElement('div'); | |
| messageText.className = 'message-text'; | |
| messageText.textContent = content; | |
| const messageTime = document.createElement('div'); | |
| messageTime.className = 'message-time'; | |
| messageTime.textContent = this.getCurrentTime(); | |
| messageContent.appendChild(messageText); | |
| messageContent.appendChild(messageTime); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(messageContent); | |
| this.messagesList.appendChild(messageDiv); | |
| // Scroll to bottom | |
| this.chatContainer.scrollTop = this.chatContainer.scrollHeight; | |
| } | |
| showTypingIndicator() { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message assistant'; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7V17L12 22L22 17V7L12 2Z" /></svg>'; | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = 'message-content'; | |
| const typingIndicator = document.createElement('div'); | |
| typingIndicator.className = 'message-text typing-indicator'; | |
| typingIndicator.innerHTML = ` | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| `; | |
| const messageTime = document.createElement('div'); | |
| messageTime.className = 'message-time'; | |
| messageTime.textContent = 'Typing...'; | |
| messageContent.appendChild(typingIndicator); | |
| messageContent.appendChild(messageTime); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(messageContent); | |
| this.messagesList.appendChild(messageDiv); | |
| this.chatContainer.scrollTop = this.chatContainer.scrollHeight; | |
| return messageDiv; | |
| } | |
| getCurrentTime() { | |
| const now = new Date(); | |
| const hours = now.getHours().toString().padStart(2, '0'); | |
| const minutes = now.getMinutes().toString().padStart(2, '0'); | |
| return `${hours}:${minutes}`; | |
| } | |
| clearChat() { | |
| if (confirm('Are you sure you want to clear all messages?')) { | |
| // Keep only the welcome message | |
| this.messagesList.innerHTML = ` | |
| <div class="message assistant"> | |
| <div class="message-avatar"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2L2 7V17L12 22L22 17V7L12 2Z" /> | |
| </svg> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-text"> | |
| Hello! I'm your AI assistant powered by Gemma-3. How can I help you today? | |
| </div> | |
| <div class="message-time">${this.getCurrentTime()}</div> | |
| </div> | |
| </div> | |
| `; | |
| this.messages = []; | |
| this.messageInput.focus(); | |
| } | |
| } | |
| openSettings() { | |
| document.getElementById('settingsModal').classList.add('active'); | |
| document.getElementById('maxTokens').value = this.settings.maxTokens; | |
| document.getElementById('maxTokensValue').textContent = this.settings.maxTokens; | |
| document.getElementById('temperature').value = this.settings.temperature; | |
| document.getElementById('temperatureValue').textContent = this.settings.temperature; | |
| document.getElementById('streaming').checked = this.settings.streaming; | |
| } | |
| closeSettings() { | |
| document.getElementById('settingsModal').classList.remove('active'); | |
| } | |
| saveSettings() { | |
| this.settings.maxTokens = parseInt(document.getElementById('maxTokens').value); | |
| this.settings.temperature = parseFloat(document.getElementById('temperature').value); | |
| this.settings.streaming = document.getElementById('streaming').checked; | |
| this.closeSettings(); | |
| } | |
| showError(message) { | |
| this.loadingStatus.textContent = message; | |
| this.loadingStatus.style.color = 'var(--error)'; | |
| } | |
| } | |
| // Initialize the chatbot when the page loads | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new AppleChatbot(); | |
| }); |