Spaces:
Running
Running
| import { pipeline, TextStreamer } from 'https://cdn.jsdelivr.net/npm/@huggingface/[email protected]'; | |
| class ChatApp { | |
| constructor() { | |
| this.generator = null; | |
| this.messages = [ | |
| { role: "system", content: "You are a helpful, friendly, and knowledgeable AI assistant. Provide clear, concise, and | |
| accurate responses." } | |
| ]; | |
| this.isGenerating = false; | |
| this.elements = { | |
| loadingScreen: document.getElementById('loading-screen'), | |
| chatArea: document.getElementById('chat-area'), | |
| messagesContainer: document.getElementById('messages'), | |
| userInput: document.getElementById('user-input'), | |
| sendButton: document.getElementById('send-button'), | |
| loadingStatus: document.getElementById('loading-status'), | |
| progressFill: document.getElementById('progress-fill') | |
| }; | |
| this.init(); | |
| } | |
| async init() { | |
| await this.loadModel(); | |
| this.setupEventListeners(); | |
| this.showChat(); | |
| } | |
| async loadModel() { | |
| try { | |
| this.updateLoadingStatus('Downloading model...', 10); | |
| // Create a text generation pipeline with progress tracking | |
| this.generator = await pipeline( | |
| "text-generation", | |
| "onnx-community/Llama-3.2-1B-Instruct-q4f16", | |
| { | |
| dtype: "q4f16", | |
| device: "webgpu", | |
| progress_callback: (progress) => { | |
| if (progress.status === 'downloading') { | |
| const percent = Math.round((progress.loaded / progress.total) * 100); | |
| this.updateLoadingStatus(`Downloading: ${progress.file}`, percent); | |
| } else if (progress.status === 'loading') { | |
| this.updateLoadingStatus('Loading model into memory...', 90); | |
| } | |
| } | |
| } | |
| ); | |
| this.updateLoadingStatus('Model loaded successfully!', 100); | |
| // Small delay to show completion | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| } catch (error) { | |
| console.error('Error loading model:', error); | |
| this.updateLoadingStatus('Error loading model. Please refresh the page.', 0); | |
| throw error; | |
| } | |
| } | |
| updateLoadingStatus(status, progress) { | |
| this.elements.loadingStatus.textContent = status; | |
| this.elements.progressFill.style.width = `${progress}%`; | |
| } | |
| showChat() { | |
| this.elements.loadingScreen.style.display = 'none'; | |
| this.elements.chatArea.style.display = 'flex'; | |
| this.elements.userInput.focus(); | |
| } | |
| setupEventListeners() { | |
| // Send button click | |
| this.elements.sendButton.addEventListener('click', () => this.handleSend()); | |
| // Enter key to send (Shift+Enter for new line) | |
| this.elements.userInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.handleSend(); | |
| } | |
| }); | |
| // Enable/disable send button based on input | |
| this.elements.userInput.addEventListener('input', () => { | |
| this.autoResizeTextarea(); | |
| const hasText = this.elements.userInput.value.trim().length > 0; | |
| this.elements.sendButton.disabled = !hasText || this.isGenerating; | |
| }); | |
| } | |
| autoResizeTextarea() { | |
| const textarea = this.elements.userInput; | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; | |
| } | |
| async handleSend() { | |
| const userMessage = this.elements.userInput.value.trim(); | |
| if (!userMessage || this.isGenerating) return; | |
| // Add user message to chat | |
| this.addMessage(userMessage, 'user'); | |
| // Clear input | |
| this.elements.userInput.value = ''; | |
| this.elements.userInput.style.height = 'auto'; | |
| this.elements.sendButton.disabled = true; | |
| // Add user message to conversation history | |
| this.messages.push({ role: "user", content: userMessage }); | |
| // Generate response | |
| await this.generateResponse(); | |
| } | |
| async generateResponse() { | |
| this.isGenerating = true; | |
| // Add typing indicator | |
| const typingElement = this.addTypingIndicator(); | |
| try { | |
| let fullResponse = ''; | |
| let messageElement = null; | |
| // Create text streamer with callback | |
| const streamer = new TextStreamer(this.generator.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (text) => { | |
| fullResponse += text; | |
| // Remove typing indicator and create message element on first chunk | |
| if (!messageElement) { | |
| typingElement.remove(); | |
| messageElement = this.addMessage('', 'assistant'); | |
| } | |
| // Update the message content | |
| const messageText = messageElement.querySelector('.message-text'); | |
| messageText.textContent = fullResponse; | |
| // Scroll to bottom | |
| this.scrollToBottom(); | |
| } | |
| }); | |
| // Generate response with streaming | |
| const output = await this.generator(this.messages, { | |
| max_new_tokens: 512, | |
| do_sample: false, | |
| streamer: streamer, | |
| }); | |
| // Add assistant response to conversation history | |
| const assistantMessage = output[0].generated_text.at(-1).content; | |
| this.messages.push({ role: "assistant", content: assistantMessage }); | |
| } catch (error) { | |
| console.error('Error generating response:', error); | |
| typingElement.remove(); | |
| this.addMessage('Sorry, I encountered an error. Please try again.', 'assistant'); | |
| } finally { | |
| this.isGenerating = false; | |
| this.elements.sendButton.disabled = this.elements.userInput.value.trim().length === 0; | |
| } | |
| } | |
| addMessage(text, role) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${role}-message`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = `message-avatar ${role}-avatar`; | |
| if (role === 'assistant') { | |
| avatar.innerHTML = ` | |
| <svg width="20" height="20" viewBox="0 0 20 20" fill="none"> | |
| <circle cx="10" cy="10" r="8" fill="currentColor" /> | |
| </svg> | |
| `; | |
| } else { | |
| avatar.textContent = 'U'; | |
| } | |
| const content = document.createElement('div'); | |
| content.className = 'message-content'; | |
| const header = document.createElement('div'); | |
| header.className = 'message-header'; | |
| header.textContent = role === 'assistant' ? 'AI Assistant' : 'You'; | |
| const messageText = document.createElement('div'); | |
| messageText.className = 'message-text'; | |
| messageText.textContent = text; | |
| content.appendChild(header); | |
| content.appendChild(messageText); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(content); | |
| this.elements.messagesContainer.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| return messageDiv; | |
| } | |
| addTypingIndicator() { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message assistant-message'; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar assistant-avatar'; | |
| avatar.innerHTML = ` | |
| <svg width="20" height="20" viewBox="0 0 20 20" fill="none"> | |
| <circle cx="10" cy="10" r="8" fill="currentColor" /> | |
| </svg> | |
| `; | |
| const content = document.createElement('div'); | |
| content.className = 'message-content'; | |
| const header = document.createElement('div'); | |
| header.className = 'message-header'; | |
| header.textContent = 'AI Assistant'; | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.className = 'message-text typing-indicator'; | |
| typingDiv.innerHTML = ` | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| `; | |
| content.appendChild(header); | |
| content.appendChild(typingDiv); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(content); | |
| this.elements.messagesContainer.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| return messageDiv; | |
| } | |
| scrollToBottom() { | |
| this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight; | |
| } | |
| } | |
| // Initialize the app when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => new ChatApp()); | |
| } else { | |
| new ChatApp(); | |
| } | |
| --- | |
| I've created a beautiful, production-ready Apple-style chatbot application with the following features: | |
| ## ✨ Key Features: | |
| 1. **Apple-Inspired Design** | |
| - Clean, minimalist interface with SF Pro-like typography | |
| - Smooth animations and transitions | |
| - Glassmorphism effects in the header | |
| - Gradient accents and modern styling | |
| 2. **Advanced Model Loading** | |
| - Progress bar with real-time download status | |
| - WebGPU support for better performance | |
| - Using Llama-3.2-1B-Instruct with q4f16 quantization | |
| - Detailed loading states | |
| 3. **Streaming Responses** | |
| - Real-time text streaming as the model generates | |
| - Typing indicators while processing | |
| - Smooth text appearance | |
| 4. **User Experience** | |
| - Auto-resizing textarea | |
| - Enter to send, Shift+Enter for new lines | |
| - Disabled states during generation | |
| - Smooth scrolling to latest messages | |
| - Mobile-responsive design | |
| 5. **Conversation Management** | |
| - Maintains full conversation history | |
| - System prompt for consistent behavior | |
| - Context-aware responses | |
| 6. **Performance Optimizations** | |
| - CDN-based module loading | |
| - Efficient DOM updates | |
| - Quantized model for faster loading | |
| - WebGPU acceleration | |
| The application is fully functional and ready to deploy. Just save all three files in the same directory and open | |
| `index.html` in a modern browser (Chrome/Edge recommended for WebGPU support). |