import { pipeline, TextStreamer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers"; class AppleChatBot { constructor() { this.generator = null; this.isGenerating = false; this.messages = [ { role: "system", content: "You are a helpful assistant. Be concise and friendly in your responses." } ]; this.init(); } async init() { this.setupEventListeners(); await this.loadModel(); } setupEventListeners() { const messageInput = document.getElementById('messageInput'); const sendButton = document.getElementById('sendButton'); // Handle input changes messageInput.addEventListener('input', () => { this.updateCharCount(); this.autoResizeTextarea(); sendButton.disabled = !messageInput.value.trim() || this.isGenerating; }); // Handle Enter key messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); // Handle send button sendButton.addEventListener('click', () => this.sendMessage()); } updateCharCount() { const messageInput = document.getElementById('messageInput'); const charCount = document.getElementById('charCount'); charCount.textContent = `${messageInput.value.length} / 2000`; } autoResizeTextarea() { const textarea = document.getElementById('messageInput'); textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; } async loadModel() { try { // Show loading overlay const loadingOverlay = document.getElementById('loadingOverlay'); const progressFill = document.getElementById('progressFill'); const progressText = document.getElementById('progressText'); const modelStatus = document.getElementById('modelStatus'); const statusDot = modelStatus.querySelector('.status-dot'); const statusText = modelStatus.querySelector('.status-text'); // Update status statusText.textContent = 'Loading model...'; statusDot.className = 'status-dot loading'; // Create 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); progressFill.style.width = `${percentage}%`; progressText.textContent = `${percentage}%`; if (progress.status === 'downloading') { statusText.textContent = `Downloading... ${percentage}%`; } else if (progress.status === 'ready') { statusText.textContent = 'Processing model...'; } } } ); // Hide loading overlay loadingOverlay.style.opacity = '0'; setTimeout(() => { loadingOverlay.style.display = 'none'; }, 300); // Update status to ready statusText.textContent = 'Ready'; statusDot.className = 'status-dot ready'; document.getElementById('sendButton').disabled = false; } catch (error) { console.error('Error loading model:', error); const loadingOverlay = document.getElementById('loadingOverlay'); const loadingContent = loadingOverlay.querySelector('.loading-content'); loadingContent.innerHTML = `

Failed to Load Model

There was an error loading the AI model. Please refresh the page and try again.

`; } } async sendMessage() { if (this.isGenerating || !this.generator) return; const messageInput = document.getElementById('messageInput'); const message = messageInput.value.trim(); if (!message) return; // Add user message to history this.messages.push({ role: "user", content: message }); this.addMessageToUI(message, 'user'); // Clear input messageInput.value = ''; this.updateCharCount(); this.autoResizeTextarea(); // Disable send button this.isGenerating = true; document.getElementById('sendButton').disabled = true; // Show typing indicator this.showTypingIndicator(); try { // Create text streamer for real-time output const streamer = new TextStreamer(this.generator.tokenizer, { skip_prompt: true, skip_special_tokens: true, callback_function: (text) => { this.updateStreamingMessage(text); } }); // Generate response const output = await this.generator(this.messages, { max_new_tokens: 512, do_sample: false, temperature: 0.7, streamer: streamer }); // Get the assistant's response const assistantMessage = output[0].generated_text.at(-1).content; this.messages.push({ role: "assistant", content: assistantMessage }); } catch (error) { console.error('Error generating response:', error); this.addMessageToUI('Sorry, I encountered an error. Please try again.', 'assistant error'); } finally { // Hide typing indicator and re-enable send button this.hideTypingIndicator(); this.isGenerating = false; document.getElementById('sendButton').disabled = false; } } showTypingIndicator() { const typingIndicator = document.getElementById('typingIndicator'); typingIndicator.style.display = 'block'; // Create a placeholder message for streaming this.streamingMessageElement = this.addMessageToUI('', 'assistant', true); } hideTypingIndicator() { const typingIndicator = document.getElementById('typingIndicator'); typingIndicator.style.display = 'none'; } updateStreamingMessage(text) { if (this.streamingMessageElement) { const messageContent = this.streamingMessageElement.querySelector('.message-text'); if (messageContent) { messageContent.textContent = text; this.scrollToBottom(); } } } addMessageToUI(content, role, isStreaming = false) { const chatMessages = document.getElementById('chatMessages'); const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}`; const messageContent = document.createElement('div'); messageContent.className = 'message-content'; if (role === 'assistant') { messageContent.innerHTML = `
AI Assistant
${isStreaming ? '' : this.formatMessage(content)}
`; } else if (role === 'user') { messageContent.innerHTML = `
You
${this.formatMessage(content)}
`; } else { messageContent.innerHTML = `

${content}

`; } messageDiv.appendChild(messageContent); chatMessages.appendChild(messageDiv); // Smooth scroll to bottom this.scrollToBottom(); // Add entrance animation requestAnimationFrame(() => { messageDiv.style.opacity = '1'; messageDiv.style.transform = 'translateY(0)'; }); return messageDiv; } formatMessage(content) { // Basic markdown-like formatting return content .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/`(.*?)`/g, '$1') .replace(/\n/g, '
'); } scrollToBottom() { const chatMessages = document.getElementById('chatMessages'); chatMessages.scrollTop = chatMessages.scrollHeight; } } // Initialize the chatbot when the page loads document.addEventListener('DOMContentLoaded', () => { new AppleChatBot(); });