Spaces:
Running
Running
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f5f5f7; | |
| --bg-tertiary: #e8e8ed; | |
| --text-primary: #1d1d1f; | |
| --text-secondary: #86868b; | |
| --user-msg-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --ai-msg-bg: #f5f5f7; | |
| --border-color: #d2d2d7; | |
| --shadow: 0 4px 20px rgba(0, 0, 0, 0.08); | |
| --shadow-hover: 0 8px 30px rgba(0, 0, 0, 0.12); | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| overflow: hidden; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| background: var(--bg-primary); | |
| } | |
| .header { | |
| background: var(--bg-primary); | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 1rem 2rem; | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .logo h1 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| background: var(--primary-gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .built-with { | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| font-size: 0.875rem; | |
| padding: 0.5rem 1rem; | |
| border-radius: 20px; | |
| transition: all 0.3s ease; | |
| } | |
| .built-with:hover { | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| } | |
| .chat-container { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* Loading Screen */ | |
| .loading-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: var(--bg-primary); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| } | |
| .loading-content { | |
| text-align: center; | |
| padding: 2rem; | |
| max-width: 400px; | |
| } | |
| .spinner { | |
| width: 60px; | |
| height: 60px; | |
| margin: 0 auto 2rem; | |
| border: 4px solid var(--bg-tertiary); | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .loading-content h2 { | |
| font-size: 1.5rem; | |
| margin-bottom: 0.5rem; | |
| font-weight: 600; | |
| } | |
| .loading-content p { | |
| color: var(--text-secondary); | |
| margin-bottom: 1.5rem; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background: var(--bg-tertiary); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-bottom: 1rem; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: var(--primary-gradient); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .loading-details { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| min-height: 1.5rem; | |
| } | |
| /* Chat Interface */ | |
| .chat-interface { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .messages-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 2rem; | |
| scroll-behavior: smooth; | |
| } | |
| .messages-container::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .messages-container::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .messages-container::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 4px; | |
| } | |
| .messages-container::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-secondary); | |
| } | |
| .welcome-message { | |
| text-align: center; | |
| padding: 3rem 1rem; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| } | |
| .welcome-message h2 { | |
| font-size: 2rem; | |
| margin-bottom: 0.5rem; | |
| font-weight: 600; | |
| } | |
| .welcome-message p { | |
| color: var(--text-secondary); | |
| margin-bottom: 2rem; | |
| } | |
| .suggestions { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.75rem; | |
| justify-content: center; | |
| } | |
| .suggestion-btn { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| padding: 0.75rem 1.25rem; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 0.875rem; | |
| transition: all 0.3s ease; | |
| color: var(--text-primary); | |
| } | |
| .suggestion-btn:hover { | |
| background: var(--bg-tertiary); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow); | |
| } | |
| .message { | |
| display: flex; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .message.user { | |
| flex-direction: row-reverse; | |
| } | |
| .message-avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| font-size: 1.25rem; | |
| } | |
| .message.user .message-avatar { | |
| background: var(--user-msg-bg); | |
| color: white; | |
| } | |
| .message.ai .message-avatar { | |
| background: var(--bg-secondary); | |
| } | |
| .message-content { | |
| max-width: 70%; | |
| padding: 1rem 1.25rem; | |
| border-radius: 20px; | |
| word-wrap: break-word; | |
| } | |
| .message.user .message-content { | |
| background: var(--user-msg-bg); | |
| color: white; | |
| border-bottom-right-radius: 4px; | |
| } | |
| .message.ai .message-content { | |
| background: var(--ai-msg-bg); | |
| border-bottom-left-radius: 4px; | |
| } | |
| .message-content p { | |
| margin: 0; | |
| white-space: pre-wrap; | |
| } | |
| .typing-indicator { | |
| display: flex; | |
| gap: 0.25rem; | |
| padding: 0.5rem 0; | |
| } | |
| .typing-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--text-secondary); | |
| 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% { | |
| opacity: 0.3; | |
| transform: translateY(0); | |
| } | |
| 30% { | |
| opacity: 1; | |
| transform: translateY(-4px); | |
| } | |
| } | |
| /* Input Container */ | |
| .input-container { | |
| padding: 1.5rem 2rem 2rem; | |
| background: var(--bg-primary); | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| gap: 0.75rem; | |
| background: var(--bg-secondary); | |
| border-radius: 24px; | |
| padding: 0.75rem 1rem; | |
| border: 2px solid transparent; | |
| transition: all 0.3s ease; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: #667eea; | |
| background: var(--bg-primary); | |
| box-shadow: var(--shadow); | |
| } | |
| #userInput { | |
| flex: 1; | |
| border: none; | |
| background: transparent; | |
| font-size: 1rem; | |
| resize: none; | |
| outline: none; | |
| font-family: inherit; | |
| max-height: 120px; | |
| color: var(--text-primary); | |
| } | |
| #userInput::placeholder { | |
| color: var(--text-secondary); | |
| } | |
| .send-btn { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| border: none; | |
| background: var(--primary-gradient); | |
| color: white; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s ease; | |
| flex-shrink: 0; | |
| } | |
| .send-btn:hover:not(:disabled) { | |
| transform: scale(1.05); | |
| box-shadow: var(--shadow); | |
| } | |
| .send-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .input-info { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 0.5rem; | |
| padding: 0 0.5rem; | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| .model-info { | |
| font-weight: 500; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .header { | |
| padding: 1rem; | |
| } | |
| .logo h1 { | |
| font-size: 1.25rem; | |
| } | |
| .messages-container { | |
| padding: 1rem; | |
| } | |
| .message-content { | |
| max-width: 85%; | |
| } | |
| .input-container { | |
| padding: 1rem; | |
| } | |
| .suggestions { | |
| flex-direction: column; | |
| } | |
| .suggestion-btn { | |
| width: 100%; | |
| } | |
| } | |
| === app.js === | |
| import { pipeline, TextStreamer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.1.2"; | |
| 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.currentStreamingMessage = null; | |
| this.initElements(); | |
| this.attachEventListeners(); | |
| this.initModel(); | |
| } | |
| initElements() { | |
| this.loadingScreen = document.getElementById('loadingScreen'); | |
| this.chatInterface = document.getElementById('chatInterface'); | |
| this.messagesContainer = document.getElementById('messagesContainer'); | |
| this.userInput = document.getElementById('userInput'); | |
| this.sendBtn = document.getElementById('sendBtn'); | |
| this.charCount = document.getElementById('charCount'); | |
| this.loadingStatus = document.getElementById('loadingStatus'); | |
| this.progressFill = document.getElementById('progressFill'); | |
| this.loadingDetails = document.getElementById('loadingDetails'); | |
| } | |
| attachEventListeners() { | |
| this.sendBtn.addEventListener('click', () => this.handleSend()); | |
| this.userInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.handleSend(); | |
| } | |
| }); | |
| this.userInput.addEventListener('input', () => { | |
| this.updateCharCount(); | |
| this.autoResizeTextarea(); | |
| this.updateSendButton(); | |
| }); | |
| // Suggestion buttons | |
| document.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('suggestion-btn')) { | |
| const prompt = e.target.dataset.prompt; | |
| this.userInput.value = prompt; | |
| this.updateCharCount(); | |
| this.updateSendButton(); | |
| this.userInput.focus(); | |
| } | |
| }); | |
| } | |
| async initModel() { | |
| try { | |
| this.updateLoadingStatus('Loading AI 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 === 'progress') { | |
| const percentage = Math.round((progress.loaded / progress.total) * 100); | |
| this.updateLoadingStatus( | |
| `Downloading model: ${progress.file}`, | |
| percentage, | |
| `${this.formatBytes(progress.loaded)} / ${this.formatBytes(progress.total)}` | |
| ); | |
| } else if (progress.status === 'done') { | |
| this.updateLoadingStatus('Model loaded successfully!', 100); | |
| } | |
| } | |
| } | |
| ); | |
| // Model loaded successfully | |
| setTimeout(() => { | |
| this.loadingScreen.style.display = 'none'; | |
| this.chatInterface.style.display = 'flex'; | |
| this.userInput.focus(); | |
| }, 500); | |
| } catch (error) { | |
| console.error('Error loading model:', error); | |
| this.updateLoadingStatus('Error loading model. Please refresh the page.', 0); | |
| this.loadingDetails.textContent = error.message; | |
| } | |
| } | |
| updateLoadingStatus(status, progress, details = '') { | |
| this.loadingStatus.textContent = status; | |
| this.progressFill.style.width = `${progress}%`; | |
| this.loadingDetails.textContent = details; | |
| } | |
| formatBytes(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | |
| } | |
| updateCharCount() { | |
| const count = this.userInput.value.length; | |
| this.charCount.textContent = `${count}/500`; | |
| } | |
| autoResizeTextarea() { | |
| this.userInput.style.height = 'auto'; | |
| this.userInput.style.height = Math.min(this.userInput.scrollHeight, 120) + 'px'; | |
| } | |
| updateSendButton() { | |
| const hasText = this.userInput.value.trim().length > 0; | |
| this.sendBtn.disabled = !hasText || this.isGenerating; | |
| } | |
| async handleSend() { | |
| const message = this.userInput.value.trim(); | |
| if (!message || this.isGenerating) return; | |
| // Add user message | |
| this.addMessage(message, 'user'); | |
| this.messages.push({ role: "user", content: message }); | |
| // Clear input | |
| this.userInput.value = ''; | |
| this.updateCharCount(); | |
| this.updateSendButton(); | |
| this.userInput.style.height = 'auto'; | |
| // Show typing indicator | |
| const typingIndicator = this.addTypingIndicator(); | |
| this.isGenerating = true; | |
| try { | |
| // Create AI message container | |
| const aiMessageDiv = this.createMessageElement('', 'ai'); | |
| typingIndicator.remove(); | |
| this.messagesContainer.appendChild(aiMessageDiv); | |
| const contentDiv = aiMessageDiv.querySelector('.message-content p'); | |
| let fullResponse = ''; | |
| // Generate response with streaming | |
| const output = await this.generator(this.messages, { | |
| max_new_tokens: 512, | |
| do_sample: false, | |
| temperature: 0.7, | |
| top_p: 0.9, | |
| streamer: new TextStreamer(this.generator.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (text) => { | |
| fullResponse += text; | |
| contentDiv.textContent = fullResponse; | |
| this.scrollToBottom(); | |
| }, | |
| }), | |
| }); | |
| // Get final response | |
| const finalResponse = output[0].generated_text.at(-1).content; | |
| contentDiv.textContent = finalResponse; | |
| // Add to message history | |
| this.messages.push({ role: "assistant", content: finalResponse }); | |
| } catch (error) { | |
| console.error('Error generating response:', error); | |
| typingIndicator.remove(); | |
| this.addMessage('Sorry, I encountered an error. Please try again.', 'ai'); | |
| } finally { | |
| this.isGenerating = false; | |
| this.updateSendButton(); | |
| this.scrollToBottom(); | |
| } | |
| } | |
| addMessage(content, role) { | |
| const messageDiv = this.createMessageElement(content, role); | |
| // Remove welcome message if it exists | |
| const welcomeMsg = this.messagesContainer.querySelector('.welcome-message'); | |
| if (welcomeMsg) { | |
| welcomeMsg.remove(); | |
| } | |
| this.messagesContainer.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| } | |
| createMessageElement(content, role) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${role}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.textContent = role === 'user' ? '👤' : '🤖'; | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'message-content'; | |
| const textP = document.createElement('p'); | |
| textP.textContent = content; | |
| contentDiv.appendChild(textP); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(contentDiv); | |
| return messageDiv; | |
| } | |
| addTypingIndicator() { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message ai'; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.textContent = '🤖'; | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'message-content'; | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.className = 'typing-indicator'; | |
| typingDiv.innerHTML = '<div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div>'; | |
| contentDiv.appendChild(typingDiv); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(contentDiv); | |
| this.messagesContainer.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| return messageDiv; | |
| } | |
| scrollToBottom() { | |
| this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; | |
| } | |
| } | |
| // Initialize the app when DOM is ready | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new ChatApp(); | |
| }); |