Spaces:
Running
Running
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary-color: #007AFF; | |
| --primary-hover: #0051D5; | |
| --background: #FFFFFF; | |
| --surface: #F2F2F7; | |
| --surface-hover: #E5E5EA; | |
| --text-primary: #000000; | |
| --text-secondary: #6D6D80; | |
| --border-color: #C6C6C8; | |
| --success: #34C759; | |
| --error: #FF3B30; | |
| --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15); | |
| --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.2); | |
| --radius-sm: 8px; | |
| --radius-md: 12px; | |
| --radius-lg: 16px; | |
| --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: var(--background); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| max-width: 100%; | |
| } | |
| /* Header */ | |
| .header { | |
| background: var(--background); | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 12px 20px; | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| background: rgba(255, 255, 255, 0.72); | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo { | |
| color: var(--primary-color); | |
| } | |
| .header h1 { | |
| font-size: 20px; | |
| font-weight: 600; | |
| letter-spacing: -0.5px; | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .status-indicator::before { | |
| content: ''; | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--error); | |
| } | |
| .status-indicator.online::before { | |
| background: var(--success); | |
| } | |
| .built-with { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| transition: var(--transition); | |
| } | |
| .built-with:hover { | |
| color: var(--primary-color); | |
| } | |
| /* Loading Screen */ | |
| .loading-screen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: var(--background); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| transition: opacity 0.3s ease; | |
| } | |
| .loading-content { | |
| text-align: center; | |
| max-width: 320px; | |
| } | |
| .spinner { | |
| width: 48px; | |
| height: 48px; | |
| margin: 0 auto 24px; | |
| border: 3px solid var(--border-color); | |
| border-top-color: var(--primary-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .loading-content h2 { | |
| font-size: 24px; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| } | |
| .loading-content p { | |
| color: var(--text-secondary); | |
| margin-bottom: 24px; | |
| font-size: 15px; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background: var(--surface); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-bottom: 8px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: var(--primary-color); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .progress-text { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| /* Chat Container */ | |
| .chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| background: var(--surface); | |
| padding: 20px; | |
| } | |
| .chat-container::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .chat-container::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .chat-container::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 3px; | |
| } | |
| .chat-messages { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .welcome-message { | |
| text-align: center; | |
| padding: 40px 20px; | |
| } | |
| .welcome-message h2 { | |
| font-size: 28px; | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| } | |
| .welcome-message p { | |
| color: var(--text-secondary); | |
| font-size: 16px; | |
| line-height: 1.5; | |
| } | |
| .message { | |
| display: flex; | |
| gap: 12px; | |
| animation: slideIn 0.3s ease; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .message.user { | |
| flex-direction: row-reverse; | |
| } | |
| .message-avatar { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| font-weight: 600; | |
| flex-shrink: 0; | |
| } | |
| .message.user .message-avatar { | |
| background: var(--primary-color); | |
| color: white; | |
| } | |
| .message.assistant .message-avatar { | |
| background: var(--surface-hover); | |
| color: var(--text-primary); | |
| } | |
| .message-bubble { | |
| max-width: 70%; | |
| padding: 12px 16px; | |
| border-radius: var(--radius-md); | |
| position: relative; | |
| } | |
| .message.user .message-bubble { | |
| background: var(--primary-color); | |
| color: white; | |
| border-bottom-right-radius: 4px; | |
| } | |
| .message.assistant .message-bubble { | |
| background: var(--background); | |
| border-bottom-left-radius: 4px; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .message-content { | |
| font-size: 15px; | |
| line-height: 1.5; | |
| word-wrap: break-word; | |
| } | |
| .typing-indicator { | |
| display: flex; | |
| gap: 4px; | |
| padding: 8px 0; | |
| } | |
| .typing-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--text-secondary); | |
| border-radius: 50%; | |
| animation: typing 1.4s infinite; | |
| } | |
| .typing-dot:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .typing-dot:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| @keyframes typing { | |
| 0%, 60%, 100% { | |
| transform: translateY(0); | |
| opacity: 0.5; | |
| } | |
| 30% { | |
| transform: translateY(-10px); | |
| opacity: 1; | |
| } | |
| } | |
| /* Input Area */ | |
| .input-area { | |
| background: var(--background); | |
| border-top: 1px solid var(--border-color); | |
| padding: 16px 20px; | |
| } | |
| .input-container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| gap: 12px; | |
| align-items: flex-end; | |
| } | |
| .message-input { | |
| flex: 1; | |
| background: var(--surface); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius-md); | |
| padding: 12px 16px; | |
| font-size: 15px; | |
| font-family: inherit; | |
| resize: none; | |
| outline: none; | |
| transition: var(--transition); | |
| min-height: 44px; | |
| max-height: 120px; | |
| line-height: 1.5; | |
| } | |
| .message-input:focus { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); | |
| } | |
| .send-button { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| border: none; | |
| color: white; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition); | |
| flex-shrink: 0; | |
| } | |
| .send-button:hover:not(:disabled) { | |
| background: var(--primary-hover); | |
| transform: scale(1.05); | |
| } | |
| .send-button:disabled { | |
| background: var(--border-color); | |
| cursor: not-allowed; | |
| opacity: 0.6; | |
| } | |
| .input-footer { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 8px; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| /* Utility Classes */ | |
| .hidden { | |
| display: none ; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .header { | |
| padding: 10px 16px; | |
| } | |
| .header h1 { | |
| font-size: 18px; | |
| } | |
| .built-with { | |
| display: none; | |
| } | |
| .chat-container { | |
| padding: 16px; | |
| } | |
| .message-bubble { | |
| max-width: 85%; | |
| } | |
| .input-area { | |
| padding: 12px 16px; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .welcome-message h2 { | |
| font-size: 24px; | |
| } | |
| .message-bubble { | |
| max-width: 90%; | |
| } | |
| } | |
| === app.js === | |
| import { pipeline, TextStreamer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.1"; | |
| class AppleStyleChatbot { | |
| constructor() { | |
| this.generator = null; | |
| this.messages = []; | |
| this.isGenerating = false; | |
| this.initElements(); | |
| this.initEventListeners(); | |
| this.loadModel(); | |
| } | |
| initElements() { | |
| // Loading elements | |
| this.loadingScreen = document.getElementById('loading-screen'); | |
| this.loadingText = document.getElementById('loading-text'); | |
| this.progressFill = document.getElementById('progress-fill'); | |
| this.progressText = document.getElementById('progress-text'); | |
| // Chat elements | |
| this.chatContainer = document.getElementById('chat-container'); | |
| this.chatMessages = document.getElementById('chat-messages'); | |
| this.inputArea = document.getElementById('input-area'); | |
| this.messageInput = document.getElementById('message-input'); | |
| this.sendButton = document.getElementById('send-button'); | |
| this.charCount = document.getElementById('char-count'); | |
| this.modelStatus = document.getElementById('model-status'); | |
| // Remove welcome message when first user message is sent | |
| this.welcomeMessage = null; | |
| } | |
| initEventListeners() { | |
| // 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(); | |
| } | |
| }); | |
| // Auto-resize textarea | |
| this.messageInput.addEventListener('input', () => { | |
| this.autoResizeTextarea(); | |
| this.updateCharCount(); | |
| this.updateSendButton(); | |
| }); | |
| // Focus input when clicking anywhere in input area | |
| this.inputArea.addEventListener('click', (e) => { | |
| if (e.target.closest('.message-input')) return; | |
| this.messageInput.focus(); | |
| }); | |
| } | |
| async loadModel() { | |
| try { | |
| this.updateLoadingStatus('Downloading model files...'); | |
| this.updateProgress(0); | |
| // Create 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) => { | |
| if (progress.status === 'progress') { | |
| const percent = Math.round(progress.progress * 100); | |
| this.updateProgress(percent); | |
| if (progress.file) { | |
| this.updateLoadingStatus(`Loading ${progress.file}...`); | |
| } | |
| } | |
| } | |
| } | |
| ); | |
| this.updateLoadingStatus('Initializing model...'); | |
| this.updateProgress(100); | |
| // Show chat interface | |
| setTimeout(() => { | |
| this.loadingScreen.style.opacity = '0'; | |
| setTimeout(() => { | |
| this.loadingScreen.classList.add('hidden'); | |
| this.chatContainer.classList.remove('hidden'); | |
| this.inputArea.classList.remove('hidden'); | |
| this.modelStatus.classList.add('online'); | |
| this.modelStatus.textContent = 'Online'; | |
| this.messageInput.focus(); | |
| }, 300); | |
| }, 500); | |
| } catch (error) { | |
| console.error('Failed to load model:', error); | |
| this.showError('Failed to load AI model. Please refresh the page and try again.'); | |
| } | |
| } | |
| updateLoadingStatus(text) { | |
| this.loadingText.textContent = text; | |
| } | |
| updateProgress(percent) { | |
| this.progressFill.style.width = `${percent}%`; | |
| this.progressText.textContent = `${percent}%`; | |
| } | |
| showError(message) { | |
| this.loadingText.textContent = message; | |
| this.loadingText.style.color = 'var(--error)'; | |
| setTimeout(() => { | |
| this.loadingScreen.innerHTML = ` | |
| <div class="loading-content"> | |
| <h2 style="color: var(--error);">Error</h2> | |
| <p>${message}</p> | |
| <button onclick="location.reload()" style=" | |
| margin-top: 20px; | |
| padding: 12px 24px; | |
| background: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: var(--radius-md); | |
| font-size: 15px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| ">Reload</button> | |
| </div> | |
| `; | |
| }, 100); | |
| } | |
| async sendMessage() { | |
| const message = this.messageInput.value.trim(); | |
| if (!message || this.isGenerating) return; | |
| // Remove welcome message on first user message | |
| if (this.welcomeMessage === null) { | |
| const welcomeEl = document.querySelector('.welcome-message'); | |
| if (welcomeEl) { | |
| welcomeEl.style.opacity = '0'; | |
| setTimeout(() => welcomeEl.remove(), 300); | |
| } | |
| this.welcomeMessage = false; | |
| } | |
| // Add user message | |
| this.addMessage(message, 'user'); | |
| this.messageInput.value = ''; | |
| this.autoResizeTextarea(); | |
| this.updateCharCount(); | |
| this.updateSendButton(); | |
| // Disable input during generation | |
| this.isGenerating = true; | |
| this.sendButton.disabled = true; | |
| this.messageInput.disabled = true; | |
| // Add typing indicator | |
| const typingId = this.addTypingIndicator(); | |
| try { | |
| // Update messages array with conversation history | |
| this.messages.push({ role: "user", content: message }); | |
| // Keep only last 10 messages to avoid context limit | |
| const contextMessages = this.messages.slice(-10); | |
| // Generate response with streaming | |
| const responseText = await this.generateResponse(contextMessages); | |
| // Remove typing indicator and add assistant message | |
| this.removeTypingIndicator(typingId); | |
| this.addMessage(responseText, 'assistant'); | |
| // Add to message history | |
| this.messages.push({ role: "assistant", content: responseText }); | |
| } catch (error) { | |
| console.error('Generation error:', error); | |
| this.removeTypingIndicator(typingId); | |
| this.addMessage('Sorry, I encountered an error while generating a response. Please try again.', 'assistant', true); | |
| } finally { | |
| // Re-enable input | |
| this.isGenerating = false; | |
| this.sendButton.disabled = false; | |
| this.messageInput.disabled = false; | |
| this.messageInput.focus(); | |
| } | |
| } | |
| async generateResponse(messages) { | |
| let fullResponse = ''; | |
| try { | |
| // Create a custom streamer to capture the text | |
| const streamer = new TextStreamer(this.generator.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (text) => { | |
| fullResponse += text; | |
| return text; | |
| }, | |
| }); | |
| // Generate response | |
| const output = await generator(messages, { | |
| max_new_tokens: 512, | |
| do_sample: false, | |
| temperature: 0.7, | |
| top_p: 0.9, | |
| streamer: streamer, | |
| }); | |
| return output[0].generated_text.at(-1).content; | |
| } catch (error) { | |
| console.error('Generation error:', error); | |
| throw error; | |
| } | |
| } | |
| addMessage(text, role, isError = false) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${role}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.textContent = role === 'user' ? 'U' : 'A'; | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'message-bubble'; | |
| const content = document.createElement('div'); | |
| content.className = 'message-content'; | |
| if (isError) { | |
| content.style.color = 'var(--error)'; | |
| } | |
| content.textContent = text; | |
| bubble.appendChild(content); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(bubble); | |
| this.chatMessages.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| return messageDiv; | |
| } | |
| addTypingIndicator() { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message assistant'; | |
| messageDiv.id = `typing-${Date.now()}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.textContent = 'A'; | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'message-bubble'; | |
| const typing = document.createElement('div'); | |
| typing.className = 'typing-indicator'; | |
| typing.innerHTML = '<div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div>'; | |
| bubble.appendChild(typing); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(bubble); | |
| this.chatMessages.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| return messageDiv.id; | |
| } | |
| removeTypingIndicator(id) { | |
| const indicator = document.getElementById(id); | |
| if (indicator) { | |
| indicator.style.opacity = '0'; | |
| setTimeout(() => indicator.remove(), 300); | |
| } | |
| } | |
| autoResizeTextarea() { | |
| this.messageInput.style.height = 'auto'; | |
| this.messageInput.style.height = Math.min(this.messageInput.scrollHeight, 120) + 'px'; | |
| } | |
| updateCharCount() { | |
| const count = this.messageInput.value.length; | |
| this.charCount.textContent = `${count} / 2000`; | |
| } | |
| updateSendButton() { | |
| const hasText = this.messageInput.value.trim().length > 0; | |
| this.sendButton.disabled = !hasText || this.isGenerating; | |
| } | |
| scrollToBottom() { | |
| this.chatContainer.scrollTop = this.chatContainer.scrollHeight; | |
| } | |
| } | |
| // Initialize the chatbot when the page loads | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new AppleStyleChatbot(); | |
| }); | |
| // Fix for the generator reference in generateResponse | |
| let generator; | |
| pipeline( | |
| "text-generation", | |
| "onnx-community/gemma-3-270m-it-ONNX", | |
| { | |
| dtype: "fp32", | |
| } | |
| ).then(g => { | |
| generator = g; | |
| }); |