Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Chat Assistant</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Markdown Parser --> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <!-- Code Highlighting --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <!-- Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| gray: { | |
| 750: '#2d2d35', | |
| 850: '#1a1a20', | |
| 950: '#0d0d10', | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #565869; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #acacbe; | |
| } | |
| /* Typing Animation */ | |
| .typing-dot { | |
| animation: typing 1.4s infinite ease-in-out both; | |
| } | |
| .typing-dot:nth-child(1) { animation-delay: -0.32s; } | |
| .typing-dot:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes typing { | |
| 0%, 80%, 100% { transform: scale(0); } | |
| 40% { transform: scale(1); } | |
| } | |
| /* Markdown Styles */ | |
| .prose pre { | |
| background-color: #0d0d10; | |
| border-radius: 0.5rem; | |
| padding: 1rem; | |
| margin: 1rem 0; | |
| overflow-x: auto; | |
| } | |
| .prose code { | |
| background-color: rgba(255,255,255,0.1); | |
| padding: 0.2rem 0.4rem; | |
| border-radius: 0.25rem; | |
| font-family: monospace; | |
| font-size: 0.9em; | |
| } | |
| .prose pre code { | |
| background-color: transparent; | |
| padding: 0; | |
| border-radius: 0; | |
| color: inherit; | |
| } | |
| .prose p { | |
| margin-bottom: 1rem; | |
| } | |
| .prose p:last-child { | |
| margin-bottom: 0; | |
| } | |
| .prose ul { | |
| list-style-type: disc; | |
| padding-left: 1.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .prose ol { | |
| list-style-type: decimal; | |
| padding-left: 1.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-800 text-gray-100 h-screen flex overflow-hidden font-sans antialiased"> | |
| <!-- Sidebar --> | |
| <div id="sidebar" class="bg-gray-900 w-[260px] flex-shrink-0 flex flex-col transition-all duration-300 -ml-[260px] md:ml-0 border-r border-white/10 z-20 absolute md:relative h-full"> | |
| <!-- New Chat Button --> | |
| <div class="p-3 flex-shrink-0"> | |
| <button onclick="startNewChat()" class="flex items-center gap-3 px-3 py-3 w-full text-sm text-white border border-white/20 rounded-md hover:bg-gray-800 transition-colors text-left"> | |
| <i class="fa-solid fa-plus"></i> | |
| New chat | |
| </button> | |
| </div> | |
| <!-- History List --> | |
| <div class="flex-1 overflow-y-auto px-3 py-2 space-y-2" id="history-list"> | |
| <!-- History items will be injected here --> | |
| <div class="text-xs font-medium text-gray-500 px-3 py-2">Today</div> | |
| </div> | |
| <!-- User / Settings --> | |
| <div class="p-3 border-t border-white/10"> | |
| <button onclick="toggleSettings()" class="flex items-center gap-3 px-3 py-3 w-full text-sm text-white hover:bg-gray-800 rounded-md transition-colors text-left"> | |
| <i class="fa-solid fa-gear"></i> | |
| Settings | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex flex-col h-full relative bg-gray-800"> | |
| <!-- Mobile Header --> | |
| <div class="md:hidden flex items-center p-4 border-b border-white/10 bg-gray-800 text-gray-100 z-10"> | |
| <button onclick="toggleSidebar()" class="text-gray-300 hover:text-white"> | |
| <i class="fa-solid fa-bars text-xl"></i> | |
| </button> | |
| <span class="ml-4 font-medium">New chat</span> | |
| </div> | |
| <!-- Chat Area --> | |
| <div class="flex-1 overflow-y-auto scroll-smooth" id="chat-container"> | |
| <!-- Welcome Screen --> | |
| <div id="welcome-screen" class="h-full flex flex-col items-center justify-center p-8 text-center"> | |
| <div class="bg-white/10 p-4 rounded-full mb-6"> | |
| <i class="fa-solid fa-robot text-4xl text-white"></i> | |
| </div> | |
| <h2 class="text-2xl font-semibold mb-2">How can I help you today?</h2> | |
| </div> | |
| <!-- Messages will be injected here --> | |
| <div id="messages-list" class="flex flex-col pb-32 pt-4"></div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="absolute bottom-0 left-0 w-full bg-gradient-to-t from-gray-800 via-gray-800 to-transparent pt-10 pb-6 px-4"> | |
| <div class="max-w-3xl mx-auto"> | |
| <div class="relative flex items-end gap-2 bg-[#40414f] border border-black/10 dark:border-gray-900/50 rounded-xl shadow-xs overflow-hidden ring-offset-2 focus-within:ring-2 ring-blue-500/50"> | |
| <textarea | |
| id="user-input" | |
| rows="1" | |
| class="w-full max-h-[200px] py-3 pl-4 pr-12 bg-transparent border-0 text-white placeholder-gray-400 focus:ring-0 resize-none overflow-y-auto" | |
| placeholder="Message ChatGPT..." | |
| oninput="this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'" | |
| onkeydown="handleEnter(event)" | |
| ></textarea> | |
| <button | |
| onclick="sendMessage()" | |
| id="send-btn" | |
| class="absolute right-2 bottom-2 p-2 rounded-md text-gray-400 hover:bg-black/50 hover:text-white disabled:opacity-40 disabled:hover:bg-transparent transition-colors" | |
| disabled | |
| > | |
| <i class="fa-solid fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| <div class="text-center mt-2"> | |
| <p class="text-xs text-gray-500">AI can make mistakes. Consider checking important information.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Modal --> | |
| <div id="settings-modal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center p-4"> | |
| <div class="bg-gray-900 rounded-xl w-full max-w-md border border-white/10 shadow-2xl transform transition-all scale-95 opacity-0" id="settings-content"> | |
| <div class="flex justify-between items-center p-4 border-b border-white/10"> | |
| <h3 class="text-lg font-medium">Settings</h3> | |
| <button onclick="toggleSettings()" class="text-gray-400 hover:text-white"> | |
| <i class="fa-solid fa-xmark"></i> | |
| </button> | |
| </div> | |
| <div class="p-6 space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Hugging Face API Key</label> | |
| <input type="password" id="api-key-input" class="w-full bg-gray-800 border border-white/10 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="hf_..."> | |
| <p class="text-xs text-gray-500 mt-1">Required for the chatbot to function.</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Model ID</label> | |
| <input type="text" id="model-input" class="w-full bg-gray-800 border border-white/10 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" value="mistralai/Mistral-7B-Instruct-v0.2"> | |
| <p class="text-xs text-gray-500 mt-1">e.g., mistralai/Mistral-7B-Instruct-v0.2, HuggingFaceH4/zephyr-7b-beta</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">System Prompt</label> | |
| <textarea id="system-prompt-input" rows="3" class="w-full bg-gray-800 border border-white/10 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500">You are a helpful AI assistant.</textarea> | |
| </div> | |
| </div> | |
| <div class="p-4 border-t border-white/10 flex justify-end"> | |
| <button onclick="saveSettings()" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md transition-colors">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- Configuration --- | |
| const DEFAULT_MODEL = "mistralai/Mistral-7B-Instruct-v0.2"; | |
| let CONFIG = { | |
| apiKey: localStorage.getItem('hf_api_key') || '', | |
| model: localStorage.getItem('hf_model') || DEFAULT_MODEL, | |
| systemPrompt: localStorage.getItem('hf_system_prompt') || "You are a helpful AI assistant." | |
| }; | |
| // --- State --- | |
| let messages = []; | |
| let isGenerating = false; | |
| // --- Initialization --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Load settings into inputs | |
| document.getElementById('api-key-input').value = CONFIG.apiKey; | |
| document.getElementById('model-input').value = CONFIG.model; | |
| document.getElementById('system-prompt-input').value = CONFIG.systemPrompt; | |
| // Check if API key is missing | |
| if (!CONFIG.apiKey) { | |
| toggleSettings(); | |
| } | |
| // Input listener | |
| const input = document.getElementById('user-input'); | |
| input.addEventListener('input', () => { | |
| const btn = document.getElementById('send-btn'); | |
| btn.disabled = input.value.trim() === ''; | |
| if (input.value.trim() !== '') { | |
| btn.classList.add('bg-green-600', 'text-white'); | |
| btn.classList.remove('text-gray-400'); | |
| } else { | |
| btn.classList.remove('bg-green-600', 'text-white'); | |
| btn.classList.add('text-gray-400'); | |
| } | |
| }); | |
| }); | |
| // --- UI Functions --- | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| sidebar.classList.toggle('-ml-[260px]'); | |
| } | |
| function toggleSettings() { | |
| const modal = document.getElementById('settings-modal'); | |
| const content = document.getElementById('settings-content'); | |
| if (modal.classList.contains('hidden')) { | |
| modal.classList.remove('hidden'); | |
| // Small delay for animation | |
| setTimeout(() => { | |
| content.classList.remove('scale-95', 'opacity-0'); | |
| content.classList.add('scale-100', 'opacity-100'); | |
| }, 10); | |
| } else { | |
| content.classList.remove('scale-100', 'opacity-100'); | |
| content.classList.add('scale-95', 'opacity-0'); | |
| setTimeout(() => { | |
| modal.classList.add('hidden'); | |
| }, 300); | |
| } | |
| } | |
| function saveSettings() { | |
| const newKey = document.getElementById('api-key-input').value.trim(); | |
| const newModel = document.getElementById('model-input').value.trim(); | |
| const newPrompt = document.getElementById('system-prompt-input').value.trim(); | |
| if (newKey) { | |
| localStorage.setItem('hf_api_key', newKey); | |
| CONFIG.apiKey = newKey; | |
| } | |
| if (newModel) { | |
| localStorage.setItem('hf_model', newModel); | |
| CONFIG.model = newModel; | |
| } | |
| if (newPrompt) { | |
| localStorage.setItem('hf_system_prompt', newPrompt); | |
| CONFIG.systemPrompt = newPrompt; | |
| } | |
| toggleSettings(); | |
| alert('Settings saved!'); | |
| } | |
| function handleEnter(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| function startNewChat() { | |
| messages = []; | |
| document.getElementById('messages-list').innerHTML = ''; | |
| document.getElementById('welcome-screen').classList.remove('hidden'); | |
| document.getElementById('user-input').value = ''; | |
| document.getElementById('user-input').style.height = 'auto'; | |
| } | |
| function appendMessage(role, content) { | |
| const list = document.getElementById('messages-list'); | |
| const welcome = document.getElementById('welcome-screen'); | |
| if (!welcome.classList.contains('hidden')) { | |
| welcome.classList.add('hidden'); | |
| } | |
| const isUser = role === 'user'; | |
| const avatar = isUser | |
| ? '<div class="w-8 h-8 bg-gray-600 rounded-sm flex items-center justify-center"><i class="fa-solid fa-user text-white"></i></div>' | |
| : '<div class="w-8 h-8 bg-green-500 rounded-sm flex items-center justify-center"><i class="fa-solid fa-robot text-white"></i></div>'; | |
| const bgClass = isUser ? '' : 'bg-[#444654]'; | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = `w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${bgClass}`; | |
| // Render Markdown | |
| const renderedContent = marked.parse(content); | |
| msgDiv.innerHTML = ` | |
| <div class="text-base gap-4 md:gap-6 md:max-w-3xl lg:max-w-[40rem] xl:max-w-[48rem] p-4 md:py-6 flex lg:px-0 m-auto"> | |
| <div class="flex-shrink-0 flex flex-col relative items-end"> | |
| ${avatar} | |
| </div> | |
| <div class="relative flex-1 prose prose-invert max-w-none break-words"> | |
| ${renderedContent} | |
| </div> | |
| </div> | |
| `; | |
| list.appendChild(msgDiv); | |
| // Highlight code blocks | |
| msgDiv.querySelectorAll('pre code').forEach((block) => { | |
| hljs.highlightElement(block); | |
| }); | |
| // Scroll to bottom | |
| const container = document.getElementById('chat-container'); | |
| container.scrollTop = container.scrollHeight; | |
| return msgDiv; | |
| } | |
| function showTypingIndicator() { | |
| const list = document.getElementById('messages-list'); | |
| const div = document.createElement('div'); | |
| div.id = 'typing-indicator'; | |
| div.className = 'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 bg-[#444654]'; | |
| div.innerHTML = ` | |
| <div class="text-base gap-4 md:gap-6 md:max-w-3xl lg:max-w-[40rem] xl:max-w-[48rem] p-4 md:py-6 flex lg:px-0 m-auto"> | |
| <div class="flex-shrink-0 flex flex-col relative items-end"> | |
| <div class="w-8 h-8 bg-green-500 rounded-sm flex items-center justify-center"><i class="fa-solid fa-robot text-white"></i></div> | |
| </div> | |
| <div class="relative flex-1 flex items-center gap-1"> | |
| <div class="w-2 h-2 bg-gray-400 rounded-full typing-dot"></div> | |
| <div class="w-2 h-2 bg-gray-400 rounded-full typing-dot"></div> | |
| <div class="w-2 h-2 bg-gray-400 rounded-full typing-dot"></div> | |
| </div> | |
| </div> | |
| `; | |
| list.appendChild(div); | |
| const container = document.getElementById('chat-container'); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| function removeTypingIndicator() { | |
| const indicator = document.getElementById('typing-indicator'); | |
| if (indicator) indicator.remove(); | |
| } | |
| // --- API Interaction --- | |
| async function sendMessage() { | |
| if (isGenerating) return; | |
| const input = document.getElementById('user-input'); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| if (!CONFIG.apiKey) { | |
| alert('Please set your Hugging Face API Key in settings first.'); | |
| toggleSettings(); | |
| return; | |
| } | |
| // UI Updates | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| document.getElementById('send-btn').disabled = true; | |
| document.getElementById('send-btn').classList.remove('bg-green-600', 'text-white'); | |
| // Add User Message | |
| messages.push({ role: 'user', content: text }); | |
| appendMessage('user', text); | |
| // Show Loading | |
| isGenerating = true; | |
| showTypingIndicator(); | |
| try { | |
| // Prepare prompt | |
| let prompt = "<s>"; | |
| for (let i = 0; i < messages.length; i++) { | |
| const msg = messages[i]; | |
| if (msg.role === 'user') { | |
| if (i === 0) { | |
| prompt += `[INST] ${CONFIG.systemPrompt}\n\n${msg.content} [/INST]`; | |
| } else { | |
| prompt += `[INST] ${msg.content} [/INST]`; | |
| } | |
| } else { | |
| prompt += ` ${msg.content} </s>`; | |
| } | |
| } | |
| const response = await fetch(`https://api-inference.huggingface.co/models/${CONFIG.model}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${CONFIG.apiKey}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| inputs: prompt, | |
| parameters: { | |
| max_new_tokens: 1024, | |
| temperature: 0.7, | |
| top_p: 0.95, | |
| return_full_text: false, | |
| stream: false | |
| } | |
| }) | |
| }); | |
| if (!response.ok) { | |
| let errorMsg = `API Error: ${response.status} ${response.statusText}`; | |
| try { | |
| const errorData = await response.json(); | |
| if (errorData.error) { | |
| errorMsg += `\nDetails: ${errorData.error}`; | |
| } | |
| } catch (e) { | |
| // Ignore JSON parse error | |
| } | |
| throw new Error(errorMsg); | |
| } | |
| const data = await response.json(); | |
| let botText = ''; | |
| if (Array.isArray(data) && data.length > 0) { | |
| botText = data[0].generated_text; | |
| } else if (data.generated_text) { | |
| botText = data.generated_text; | |
| } else { | |
| botText = "Error: No response from model."; | |
| } | |
| removeTypingIndicator(); | |
| messages.push({ role: 'assistant', content: botText }); | |
| appendMessage('assistant', botText); | |
| } catch (error) { | |
| console.error(error); | |
| removeTypingIndicator(); | |
| appendMessage('assistant', `**Error:** ${error.message}\n\n**Troubleshooting:**\n1. Check your API Key in Settings.\n2. Ensure your token has 'Read' permissions.\n3. Try a different model ID (e.g., 'HuggingFaceH4/zephyr-7b-beta').`); | |
| } finally { | |
| isGenerating = false; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |