Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Qwen Chat - AI Assistant</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'], | |
| }, | |
| colors: { | |
| primary: { | |
| 50: '#f0f9ff', | |
| 100: '#e0f2fe', | |
| 200: '#bae6fd', | |
| 300: '#7dd3fc', | |
| 400: '#38bdf8', | |
| 500: '#0ea5e9', | |
| 600: '#0284c7', | |
| 700: '#0369a1', | |
| 800: '#075985', | |
| 900: '#0c4a6e', | |
| }, | |
| dark: { | |
| 800: '#1e293b', | |
| 900: '#0f172a', | |
| 950: '#020617', | |
| } | |
| }, | |
| animation: { | |
| 'fade-in': 'fadeIn 0.5s ease-out', | |
| 'slide-up': 'slideUp 0.3s ease-out', | |
| 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |
| 'typing': 'typing 1.5s infinite', | |
| }, | |
| keyframes: { | |
| fadeIn: { | |
| '0%': { opacity: '0' }, | |
| '100%': { opacity: '1' }, | |
| }, | |
| slideUp: { | |
| '0%': { transform: 'translateY(20px)', opacity: '0' }, | |
| '100%': { transform: 'translateY(0)', opacity: '1' }, | |
| }, | |
| typing: { | |
| '0%, 100%': { opacity: '1' }, | |
| '50%': { opacity: '0.3' }, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #475569; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #64748b; | |
| } | |
| /* Glassmorphism */ | |
| .glass { | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .glass-light { | |
| background: rgba(255, 255, 255, 0.03); | |
| backdrop-filter: blur(8px); | |
| -webkit-backdrop-filter: blur(8px); | |
| } | |
| /* Code Block Styling */ | |
| .code-block { | |
| background: #0d1117; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin: 12px 0; | |
| } | |
| .code-header { | |
| background: #161b22; | |
| padding: 8px 16px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid #30363d; | |
| } | |
| .code-content { | |
| padding: 16px; | |
| overflow-x: auto; | |
| font-family: 'Fira Code', 'Consolas', monospace; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| color: #e6edf3; | |
| } | |
| .code-content .keyword { color: #ff7b72; } | |
| .code-content .string { color: #a5d6ff; } | |
| .code-content .comment { color: #8b949e; font-style: italic; } | |
| .code-content .function { color: #d2a8ff; } | |
| .code-content .number { color: #79c0ff; } | |
| /* Typing Indicator */ | |
| .typing-dot { | |
| width: 6px; | |
| height: 6px; | |
| background: #38bdf8; | |
| border-radius: 50%; | |
| display: inline-block; | |
| 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; } | |
| /* Message Animations */ | |
| .message-appear { | |
| animation: slideUp 0.3s ease-out forwards; | |
| } | |
| /* Gradient Text */ | |
| .gradient-text { | |
| background: linear-gradient(135deg, #38bdf8 0%, #818cf8 50%, #c084fc 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| /* Input Glow */ | |
| .input-glow:focus-within { | |
| box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2), 0 0 20px rgba(56, 189, 248, 0.1); | |
| } | |
| /* Background Pattern */ | |
| .bg-pattern { | |
| background-image: | |
| radial-gradient(circle at 20% 50%, rgba(56, 189, 248, 0.08) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(192, 132, 252, 0.06) 0%, transparent 50%), | |
| radial-gradient(circle at 40% 20%, rgba(129, 140, 248, 0.05) 0%, transparent 50%); | |
| } | |
| /* Markdown Styles */ | |
| .markdown-body h1 { font-size: 1.5em; font-weight: 700; margin: 16px 0 8px; } | |
| .markdown-body h2 { font-size: 1.25em; font-weight: 600; margin: 14px 0 8px; } | |
| .markdown-body h3 { font-size: 1.1em; font-weight: 600; margin: 12px 0 6px; } | |
| .markdown-body p { margin: 8px 0; line-height: 1.7; } | |
| .markdown-body ul, .markdown-body ol { margin: 8px 0; padding-left: 20px; } | |
| .markdown-body li { margin: 4px 0; } | |
| .markdown-body strong { font-weight: 600; color: #f1f5f9; } | |
| .markdown-body em { font-style: italic; } | |
| .markdown-body blockquote { | |
| border-left: 3px solid #38bdf8; | |
| padding-left: 12px; | |
| margin: 12px 0; | |
| color: #94a3b8; | |
| font-style: italic; | |
| } | |
| .markdown-body pre { margin: 12px 0; } | |
| .markdown-body code { | |
| background: rgba(56, 189, 248, 0.1); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 0.9em; | |
| color: #7dd3fc; | |
| font-family: 'Fira Code', monospace; | |
| } | |
| .markdown-body pre code { | |
| background: none; | |
| padding: 0; | |
| color: inherit; | |
| } | |
| .markdown-body table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 12px 0; | |
| } | |
| .markdown-body th, .markdown-body td { | |
| border: 1px solid #334155; | |
| padding: 8px 12px; | |
| text-align: left; | |
| } | |
| .markdown-body th { | |
| background: rgba(56, 189, 248, 0.1); | |
| font-weight: 600; | |
| } | |
| .markdown-body a { | |
| color: #38bdf8; | |
| text-decoration: underline; | |
| } | |
| .markdown-body hr { | |
| border: none; | |
| border-top: 1px solid #334155; | |
| margin: 16px 0; | |
| } | |
| /* Tooltip */ | |
| .tooltip { | |
| position: relative; | |
| } | |
| .tooltip::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| padding: 4px 8px; | |
| background: #1e293b; | |
| color: #f8fafc; | |
| font-size: 12px; | |
| border-radius: 4px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.2s; | |
| margin-bottom: 4px; | |
| border: 1px solid #334155; | |
| } | |
| .tooltip:hover::after { | |
| opacity: 1; | |
| } | |
| /* Selection */ | |
| ::selection { | |
| background: rgba(56, 189, 248, 0.3); | |
| color: #f8fafc; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-dark-950 text-slate-200 font-sans h-screen overflow-hidden bg-pattern"> | |
| <!-- App Container --> | |
| <div class="flex h-full"> | |
| <!-- Sidebar --> | |
| <aside id="sidebar" class="w-72 bg-dark-900 border-r border-slate-800/50 flex flex-col transition-all duration-300 ease-in-out flex-shrink-0"> | |
| <!-- Sidebar Header --> | |
| <div class="p-4 border-b border-slate-800/50"> | |
| <div class="flex items-center gap-3 mb-4"> | |
| <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-sky-500 to-violet-500 flex items-center justify-center shadow-lg shadow-sky-500/20"> | |
| <i class="fas fa-robot text-white text-lg"></i> | |
| </div> | |
| <div> | |
| <h1 class="font-bold text-lg text-slate-100">Qwen Chat</h1> | |
| <p class="text-xs text-slate-400">AI Assistant</p> | |
| </div> | |
| </div> | |
| <button onclick="newChat()" class="w-full py-2.5 px-4 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700/50 text-slate-200 text-sm font-medium transition-all duration-200 flex items-center justify-center gap-2 hover:border-slate-600 group"> | |
| <i class="fas fa-plus text-sky-400 group-hover:scale-110 transition-transform"></i> | |
| New Chat | |
| </button> | |
| </div> | |
| <!-- Chat History --> | |
| <div class="flex-1 overflow-y-auto p-3 space-y-1" id="chatHistory"> | |
| <!-- History items will be populated by JS --> | |
| </div> | |
| <!-- Sidebar Footer --> | |
| <div class="p-4 border-t border-slate-800/50 space-y-3"> | |
| <!-- API Key Input --> | |
| <div class="relative"> | |
| <input | |
| type="password" | |
| id="apiKeyInput" | |
| placeholder="Enter HuggingFace Token" | |
| class="w-full bg-slate-800/50 border border-slate-700/50 rounded-lg px-3 py-2 text-xs text-slate-300 placeholder-slate-500 focus:outline-none focus:border-sky-500/50 focus:ring-1 focus:ring-sky-500/20 transition-all" | |
| > | |
| <button onclick="saveApiKey()" class="absolute right-2 top-1/2 -translate-y-1/2 text-sky-400 hover:text-sky-300 text-xs"> | |
| <i class="fas fa-save"></i> | |
| </button> | |
| </div> | |
| <div class="flex items-center justify-between text-xs text-slate-500"> | |
| <span class="flex items-center gap-1"> | |
| <i class="fas fa-shield-alt text-emerald-500"></i> | |
| Secure | |
| </span> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="hover:text-sky-400 transition-colors flex items-center gap-1"> | |
| Built with <span class="text-sky-400 font-medium">anycoder</span> | |
| <i class="fas fa-external-link-alt text-[10px]"></i> | |
| </a> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="flex-1 flex flex-col relative min-w-0"> | |
| <!-- Mobile Header --> | |
| <div class="lg:hidden flex items-center justify-between p-4 border-b border-slate-800/50 glass"> | |
| <button onclick="toggleSidebar()" class="text-slate-400 hover:text-slate-200"> | |
| <i class="fas fa-bars text-xl"></i> | |
| </button> | |
| <div class="flex items-center gap-2"> | |
| <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-sky-500 to-violet-500 flex items-center justify-center"> | |
| <i class="fas fa-robot text-white text-sm"></i> | |
| </div> | |
| <span class="font-semibold text-slate-200">Qwen Chat</span> | |
| </div> | |
| <div class="w-8"></div> | |
| </div> | |
| <!-- Chat Area --> | |
| <div id="chatContainer" class="flex-1 overflow-y-auto p-4 lg:p-8 space-y-6 scroll-smooth"> | |
| <!-- Welcome Screen --> | |
| <div id="welcomeScreen" class="flex flex-col items-center justify-center h-full text-center px-4 max-w-2xl mx-auto"> | |
| <div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-sky-500 to-violet-500 flex items-center justify-center mb-6 shadow-2xl shadow-sky-500/20 animate-pulse-slow"> | |
| <i class="fas fa-sparkles text-white text-3xl"></i> | |
| </div> | |
| <h2 class="text-3xl lg:text-4xl font-bold mb-3 gradient-text">How can I help you today?</h2> | |
| <p class="text-slate-400 mb-8 max-w-md">Powered by Qwen 2.5 - Alibaba's latest large language model. Ask me anything about coding, writing, analysis, or creative tasks.</p> | |
| <!-- Quick Suggestions --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 w-full max-w-lg"> | |
| <button onclick="sendQuickMessage('Explain quantum computing in simple terms')" class="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50 hover:border-sky-500/30 hover:bg-slate-800 transition-all text-left group"> | |
| <div class="w-8 h-8 rounded-lg bg-violet-500/20 flex items-center justify-center mb-3 group-hover:scale-110 transition-transform"> | |
| <i class="fas fa-atom text-violet-400"></i> | |
| </div> | |
| <p class="text-sm text-slate-300 font-medium">Explain quantum computing</p> | |
| <p class="text-xs text-slate-500 mt-1">in simple terms</p> | |
| </button> | |
| <button onclick="sendQuickMessage('Write a Python function to sort a list of dictionaries by multiple keys')" class="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50 hover:border-sky-500/30 hover:bg-slate-800 transition-all text-left group"> | |
| <div class="w-8 h-8 rounded-lg bg-sky-500/20 flex items-center justify-center mb-3 group-hover:scale-110 transition-transform"> | |
| <i class="fas fa-code text-sky-400"></i> | |
| </div> | |
| <p class="text-sm text-slate-300 font-medium">Write Python code</p> | |
| <p class="text-xs text-slate-500 mt-1">sort dictionaries by keys</p> | |
| </button> | |
| <button onclick="sendQuickMessage('Create a marketing strategy for a new eco-friendly product')" class="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50 hover:border-sky-500/30 hover:bg-slate-800 transition-all text-left group"> | |
| <div class="w-8 h-8 rounded-lg bg-emerald-500/20 flex items-center justify-center mb-3 group-hover:scale-110 transition-transform"> | |
| <i class="fas fa-leaf text-emerald-400"></i> | |
| </div> | |
| <p class="text-sm text-slate-300 font-medium">Marketing strategy</p> | |
| <p class="text-xs text-slate-500 mt-1">for eco-friendly product</p> | |
| </button> | |
| <button onclick="sendQuickMessage('Analyze the pros and cons of remote work for tech companies')" class="p-4 rounded-xl bg-slate-800/50 border border-slate-700/50 hover:border-sky-500/30 hover:bg-slate-800 transition-all text-left group"> | |
| <div class="w-8 h-8 rounded-lg bg-amber-500/20 flex items-center justify-center mb-3 group-hover:scale-110 transition-transform"> | |
| <i class="fas fa-chart-line text-amber-400"></i> | |
| </div> | |
| <p class="text-sm text-slate-300 font-medium">Analyze remote work</p> | |
| <p class="text-xs text-slate-500 mt-1">pros and cons for tech</p> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Messages will appear here --> | |
| <div id="messagesArea" class="hidden max-w-3xl mx-auto w-full space-y-6"></div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="p-4 lg:p-6 border-t border-slate-800/50 glass"> | |
| <div class="max-w-3xl mx-auto"> | |
| <!-- Model Selector --> | |
| <div class="flex items-center gap-2 mb-3 overflow-x-auto pb-1"> | |
| <button onclick="setModel('qwen2.5-72b-instruct')" class="model-btn active px-3 py-1.5 rounded-full text-xs font-medium bg-sky-500/20 text-sky-300 border border-sky-500/30 transition-all whitespace-nowrap" data-model="qwen2.5-72b-instruct"> | |
| <i class="fas fa-bolt mr-1"></i>Qwen 2.5 72B | |
| </button> | |
| <button onclick="setModel('qwen2.5-14b-instruct')" class="model-btn px-3 py-1.5 rounded-full text-xs font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:border-slate-600 transition-all whitespace-nowrap" data-model="qwen2.5-14b-instruct"> | |
| Qwen 2.5 14B | |
| </button> | |
| <button onclick="setModel('qwen2.5-7b-instruct')" class="model-btn px-3 py-1.5 rounded-full text-xs font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:border-slate-600 transition-all whitespace-nowrap" data-model="qwen2.5-7b-instruct"> | |
| Qwen 2.5 7B | |
| </button> | |
| <button onclick="setModel('qwen2.5-coder-32b-instruct')" class="model-btn px-3 py-1.5 rounded-full text-xs font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:border-slate-600 transition-all whitespace-nowrap" data-model="qwen2.5-coder-32b-instruct"> | |
| <i class="fas fa-code mr-1"></i>Coder 32B | |
| </button> | |
| </div> | |
| <!-- Input Box --> | |
| <div class="relative input-glow rounded-2xl bg-slate-800/80 border border-slate-700/50 transition-all duration-300"> | |
| <textarea | |
| id="messageInput" | |
| rows="1" | |
| placeholder="Message Qwen..." | |
| class="w-full bg-transparent text-slate-200 placeholder-slate-500 px-5 py-4 pr-14 resize-none focus:outline-none max-h-48 text-sm leading-relaxed" | |
| onkeydown="handleKeyDown(event)" | |
| oninput="autoResize(this)" | |
| ></textarea> | |
| <div class="absolute right-3 bottom-3 flex items-center gap-2"> | |
| <button onclick="clearInput()" class="p-2 text-slate-500 hover:text-slate-300 transition-colors rounded-lg hover:bg-slate-700/50"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| <button onclick="sendMessage()" id="sendBtn" class="p-2.5 bg-sky-500 hover:bg-sky-400 text-white rounded-xl transition-all duration-200 shadow-lg shadow-sky-500/20 hover:shadow-sky-500/30 disabled:opacity-50 disabled:cursor-not-allowed"> | |
| <i class="fas fa-paper-plane text-sm"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <p class="text-center text-xs text-slate-600 mt-2"> | |
| Qwen can make mistakes. Consider checking important information. | |
| </p> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div id="toast" class="fixed top-4 right-4 px-4 py-3 rounded-xl glass border border-slate-700/50 text-sm text-slate-200 transform translate-x-full transition-transform duration-300 z-50 flex items-center gap-2"> | |
| <i class="fas fa-check-circle text-emerald-400"></i> | |
| <span id="toastMessage">Notification</span> | |
| </div> | |
| <script> | |
| // State Management | |
| let currentModel = 'qwen2.5-72b-instruct'; | |
| let chatHistory = []; | |
| let currentChatId = null; | |
| let isGenerating = false; | |
| let abortController = null; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadApiKey(); | |
| loadChatHistory(); | |
| createNewChat(); | |
| autoResize(document.getElementById('messageInput')); | |
| }); | |
| // API Key Management | |
| function saveApiKey() { | |
| const key = document.getElementById('apiKeyInput').value.trim(); | |
| if (key) { | |
| localStorage.setItem('hf_token', key); | |
| showToast('API Key saved successfully'); | |
| } | |
| } | |
| function loadApiKey() { | |
| const key = localStorage.getItem('hf_token'); | |
| if (key) { | |
| document.getElementById('apiKeyInput').value = key; | |
| } | |
| } | |
| function getApiKey() { | |
| return localStorage.getItem('hf_token'); | |
| } | |
| // Chat History Management | |
| function loadChatHistory() { | |
| const saved = localStorage.getItem('chatHistory'); | |
| if (saved) { | |
| chatHistory = JSON.parse(saved); | |
| renderChatHistory(); | |
| } | |
| } | |
| function saveChatHistory() { | |
| localStorage.setItem('chatHistory', JSON.stringify(chatHistory)); | |
| } | |
| function renderChatHistory() { | |
| const container = document.getElementById('chatHistory'); | |
| container.innerHTML = chatHistory.map(chat => ` | |
| <div onclick="loadChat('${chat.id}')" class="group flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all duration-200 ${chat.id === currentChatId ? 'bg-sky-500/10 border border-sky-500/20' : 'hover:bg-slate-800/50 border border-transparent'}"> | |
| <div class="w-8 h-8 rounded-lg ${chat.id === currentChatId ? 'bg-sky-500/20' : 'bg-slate-800'} flex items-center justify-center flex-shrink-0"> | |
| <i class="fas fa-message ${chat.id === currentChatId ? 'text-sky-400' : 'text-slate-500'} text-xs"></i> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <p class="text-sm text-slate-300 truncate font-medium">${chat.title}</p> | |
| <p class="text-xs text-slate-500 truncate">${new Date(chat.timestamp).toLocaleDateString()}</p> | |
| </div> | |
| <button onclick="event.stopPropagation(); deleteChat('${chat.id}')" class="opacity-0 group-hover:opacity-100 p-1.5 text-slate-500 hover:text-red-400 transition-all rounded-lg hover:bg-red-500/10"> | |
| <i class="fas fa-trash-alt text-xs"></i> | |
| </button> | |
| </div> | |
| `).join(''); | |
| } | |
| function createNewChat() { | |
| currentChatId = Date.now().toString(); | |
| const newChat = { | |
| id: currentChatId, | |
| title: 'New Conversation', | |
| messages: [], | |
| timestamp: Date.now(), | |
| model: currentModel | |
| }; | |
| chatHistory.unshift(newChat); | |
| saveChatHistory(); | |
| renderChatHistory(); | |
| renderMessages(); | |
| document.getElementById('welcomeScreen').classList.remove('hidden'); | |
| document.getElementById('messagesArea').classList.add('hidden'); | |
| } | |
| function newChat() { | |
| createNewChat(); | |
| document.getElementById('messageInput').focus(); | |
| } | |
| function loadChat(id) { | |
| currentChatId = id; | |
| const chat = chatHistory.find(c => c.id === id); | |
| if (chat) { | |
| currentModel = chat.model || currentModel; | |
| updateModelButtons(); | |
| renderMessages(); | |
| renderChatHistory(); | |
| } | |
| } | |
| function deleteChat(id) { | |
| chatHistory = chatHistory.filter(c => c.id !== id); | |
| saveChatHistory(); | |
| renderChatHistory(); | |
| if (currentChatId === id) { | |
| createNewChat(); | |
| } | |
| } | |
| // Message Rendering | |
| function renderMessages() { | |
| const chat = chatHistory.find(c => c.id === currentChatId); | |
| const messagesArea = document.getElementById('messagesArea'); | |
| if (!chat || chat.messages.length === 0) { | |
| document.getElementById('welcomeScreen').classList.remove('hidden'); | |
| messagesArea.classList.add('hidden'); | |
| return; | |
| } | |
| document.getElementById('welcomeScreen').classList.add('hidden'); | |
| messagesArea.classList.remove('hidden'); | |
| messagesArea.innerHTML = chat.messages.map((msg, index) => ` | |
| <div class="message-appear flex gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}" style="animation-delay: ${index * 0.05}s"> | |
| <!-- Avatar --> | |
| <div class="flex-shrink-0"> | |
| ${msg.role === 'user' | |
| ? `<div class="w-8 h-8 rounded-full bg-gradient-to-br from-sky-500 to-violet-500 flex items-center justify-center"> | |
| <i class="fas fa-user text-white text-xs"></i> | |
| </div>` | |
| : `<div class="w-8 h-8 rounded-full bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center"> | |
| <i class="fas fa-robot text-white text-xs"></i> | |
| </div>` | |
| } | |
| </div> | |
| <!-- Message Content --> | |
| <div class="flex-1 ${msg.role === 'user' ? 'text-right' : ''} max-w-[85%] lg:max-w-[80%]"> | |
| <div class="inline-block text-left"> | |
| <div class="flex items-center gap-2 mb-1 ${msg.role === 'user' ? 'justify-end' : ''}"> | |
| <span class="text-xs font-medium ${msg.role === 'user' ? 'text-sky-400' : 'text-emerald-400'}"> | |
| ${msg.role === 'user' ? 'You' : 'Qwen'} | |
| </span> | |
| <span class="text-xs text-slate-600">${formatTime(msg.timestamp)}</span> | |
| </div> | |
| <div class="${msg.role === 'user' | |
| ? 'bg-sky-500/10 border border-sky-500/20 text-slate-200' | |
| : 'bg-slate-800/50 border border-slate-700/50 text-slate-300'} | |
| rounded-2xl px-5 py-3.5 text-sm leading-relaxed markdown-body"> | |
| ${msg.role === 'assistant' ? renderMarkdown(msg.content) : escapeHtml(msg.content)} | |
| </div> | |
| ${msg.role === 'assistant' ? ` | |
| <div class="flex items-center gap-2 mt-2"> | |
| <button onclick="copyMessage('${escapeJs(msg.content)}')" class="tooltip p-1.5 text-slate-500 hover:text-sky-400 transition-colors rounded-lg hover:bg-slate-800" data-tooltip="Copy"> | |
| <i class="fas fa-copy text-xs"></i> | |
| </button> | |
| <button onclick="regenerateMessage(${index})" class="tooltip p-1.5 text-slate-500 hover:text-sky-400 transition-colors rounded-lg hover:bg-slate-800" data-tooltip="Regenerate"> | |
| <i class="fas fa-rotate-right text-xs"></i> | |
| </button> | |
| <button onclick="likeMessage(${index})" class="tooltip p-1.5 text-slate-500 hover:text-emerald-400 transition-colors rounded-lg hover:bg-slate-800" data-tooltip="Like"> | |
| <i class="fas fa-thumbs-up text-xs"></i> | |
| </button> | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| // Scroll to bottom | |
| const container = document.getElementById('chatContainer'); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| // Markdown Renderer | |
| function renderMarkdown(text) { | |
| // Escape HTML first | |
| let html = escapeHtml(text); | |
| // Code blocks with language | |
| html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { | |
| const language = lang || 'text'; | |
| const highlightedCode = syntaxHighlight(code.trim()); | |
| return ` | |
| <div class="code-block"> | |
| <div class="code-header"> | |
| <span class="text-xs text-slate-400 font-mono">${language}</span> | |
| <button onclick="copyCode(this)" class="text-xs text-slate-500 hover:text-sky-400 transition-colors flex items-center gap-1"> | |
| <i class="fas fa-copy"></i> Copy | |
| </button> | |
| </div> | |
| <div class="code-content">${highlightedCode}</div> | |
| </div> | |
| `; | |
| }); | |
| // Inline code | |
| html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); | |
| // Headers | |
| html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>'); | |
| html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>'); | |
| html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>'); | |
| // Bold and italic | |
| html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>'); | |
| html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); | |
| html = html.replace(/\*(.*?)\*/g, '<em>$1</em>'); | |
| // Lists | |
| html = html.replace(/^- (.*$)/gm, '<li>$1</li>'); | |
| html = html.replace(/(<li>.*<<\/li>\n)+/g, '<ul>$&</ul>'); | |
| // Numbered lists | |
| html = html.replace(/^\d+\. (.*$)/gm, '<li>$1</li>'); | |
| html = html.replace(/(<li>.*<<\/li>\n)+/g, '<ol>$&</ol>'); | |
| // Links | |
| html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); | |
| // Blockquotes | |
| html = html.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>'); | |
| // Horizontal rules | |
| html = html.replace(/^---$/gm, '<hr>'); | |
| // Paragraphs (split by double newline) | |
| html = html.split('\n\n').map(p => { | |
| if (p.trim() && !p.startsWith('<')) { | |
| return `<p>${p.replace(/\n/g, '<br>')}</p>`; | |
| } | |
| return p; | |
| }).join(''); | |
| return html; | |
| } | |
| // Syntax Highlighting | |
| function syntaxHighlight(code) { | |
| // Simple syntax highlighting | |
| return code | |
| .replace(/\b(def|class|return|if|else|elif|for|while|import|from|as|try|except|with|lambda|yield|async|await|const|let|var|function|return|if|else|for|while|class|import|export|try|catch)\b/g, '<span class="keyword">$1</span>') | |
| .replace(/(".*?"|'.*?'|`.*?`)/g, '<span class="string">$1</span>') | |
| .replace(/\b(\d+)\b/g, '<span class="number">$1</span>') | |
| .replace(/(\w+)\(/g, '<span class="function">$1</span>(') | |
| .replace(/(#.*$|\/\/.*$)/gm, '<span class="comment">$1</span>'); | |
| } | |
| // Utility Functions | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function escapeJs(text) { | |
| return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, '\\n'); | |
| } | |
| function formatTime(timestamp) { | |
| return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| } | |
| // UI Interactions | |
| function setModel(model) { | |
| currentModel = model; | |
| updateModelButtons(); | |
| const chat = chatHistory.find(c => c.id === currentChatId); | |
| if (chat) { | |
| chat.model = model; | |
| saveChatHistory(); | |
| } | |
| } | |
| function updateModelButtons() { | |
| document.querySelectorAll('.model-btn').forEach(btn => { | |
| if (btn.dataset.model === currentModel) { | |
| btn.classList.add('bg-sky-500/20', 'text-sky-300', 'border-sky-500/30'); | |
| btn.classList.remove('bg-slate-800', 'text-slate-400', 'border-slate-700'); | |
| } else { | |
| btn.classList.remove('bg-sky-500/20', 'text-sky-300', 'border-sky-500/30'); | |
| btn.classList.add('bg-slate-800', 'text-slate-400', 'border-slate-700'); | |
| } | |
| }); | |
| } | |
| function autoResize(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = Math.min(textarea.scrollHeight, 192) + 'px'; | |
| } | |
| function handleKeyDown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| function clearInput() { | |
| const input = document.getElementById('messageInput'); | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| input.focus(); | |
| } | |
| function sendQuickMessage(text) { | |
| document.getElementById('messageInput').value = text; | |
| autoResize(document.getElementById('messageInput')); | |
| sendMessage(); | |
| } | |
| // Message Actions | |
| function copyMessage(content) { | |
| navigator.clipboard.writeText(content).then(() => { | |
| showToast('Copied to clipboard'); | |
| }); | |
| } | |
| function copyCode(btn) { | |
| const code = btn.closest('.code-block').querySelector('.code-content').textContent; | |
| navigator.clipboard.writeText(code).then(() => { | |
| const original = btn.innerHTML; | |
| btn.innerHTML = '<i class="fas fa-check"></i> Copied'; | |
| setTimeout(() => btn.innerHTML = original, 2000); | |
| }); | |
| } | |
| function likeMessage(index) { | |
| showToast('Thanks for your feedback!'); | |
| } | |
| function regenerateMessage(index) { | |
| const chat = chatHistory.find(c => c.id === currentChatId); | |
| if (chat && index > 0) { | |
| const userMessage = chat.messages[index - 1]; | |
| if (userMessage.role === 'user') { | |
| chat.messages.splice(index - 1, 2); | |
| saveChatHistory(); | |
| renderMessages(); | |
| sendMessageToAPI(userMessage.content); | |
| } | |
| } | |
| } | |
| // Send Message | |
| async function sendMessage() { | |
| const input = document.getElementById('messageInput'); | |
| const content = input.value.trim(); | |
| if (!content || isGenerating) return; | |
| // Check API key | |
| const apiKey = getApiKey(); | |
| if (!apiKey) { | |
| showToast('Please enter your HuggingFace token first', 'error'); | |
| document.getElementById('apiKeyInput').focus(); | |
| return; | |
| } | |
| // Add user message | |
| const chat = chatHistory.find(c => c.id === currentChatId); | |
| const userMessage = { | |
| role: 'user', | |
| content: content, | |
| timestamp: Date.now() | |
| }; | |
| chat.messages.push(userMessage); | |
| // Update title if first message | |
| if (chat.messages.length === 1) { | |
| chat.title = content.substring(0, 50) + (content.length > 50 ? '...' : ''); | |
| } | |
| saveChatHistory(); | |
| renderChatHistory(); | |
| renderMessages(); | |
| // Clear input | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| // Send to API | |
| await sendMessageToAPI(content); | |
| } | |
| // API Communication | |
| async function sendMessageToAPI(content) { | |
| isGenerating = true; | |
| const sendBtn = document.getElementById('sendBtn'); | |
| sendBtn.disabled = true; | |
| // Add typing indicator | |
| const chat = chatHistory.find(c => c.id === currentChatId); | |
| const assistantMessage = { | |
| role: 'assistant', | |
| content: '', | |
| timestamp: Date.now(), | |
| isTyping: true | |
| }; | |
| chat.messages.push(assistantMessage); | |
| renderMessages(); | |
| // Show typing animation | |
| const messagesArea = document.getElementById('messagesArea'); | |
| const lastMessage = messagesArea.lastElementChild; | |
| const contentDiv = lastMessage.querySelector('.markdown-body'); | |
| contentDiv.innerHTML = ` | |
| <div class="flex items-center gap-2 py-2"> | |
| <span class="typing-dot"></span> | |
| <span class="typing-dot" style="animation-delay: -0.16s"></span> | |
| <span class="typing-dot" style="animation-delay: -0.32s"></span> | |
| </div> | |
| `; | |
| try { | |
| abortController = new AbortController(); | |
| const response = await fetch('https://api-inference.huggingface.co/models/Qwen/' + currentModel, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${getApiKey()}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| inputs: formatConversation(chat.messages.slice(0, -1)), | |
| parameters: { | |
| max_new_tokens: 2048, | |
| temperature: 0.7, | |
| top_p: 0.9, | |
| return_full_text: false | |
| } | |
| }), | |
| signal: abortController.signal | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| // Remove typing indicator and add actual response | |
| assistantMessage.isTyping = false; | |
| if (Array.isArray(data) && data[0]?.generated_text) { | |
| assistantMessage.content = data[0].generated_text; | |
| } else if (data.generated_text) { | |
| assistantMessage.content = data.generated_text; | |
| } else { | |
| throw new Error('Unexpected response format'); | |
| } | |
| } catch (error) { | |
| if (error.name === 'AbortError') { | |
| assistantMessage.content = 'Generation stopped.'; | |
| } else { | |
| assistantMessage.content = `Error: ${error.message}. Please check your API token and try again.`; | |
| } | |
| assistantMessage.isTyping = false; | |
| } finally { | |
| isGenerating = false; | |
| sendBtn.disabled = false; | |
| abortController = null; | |
| saveChatHistory(); | |
| renderMessages(); | |
| } | |
| } | |
| function formatConversation(messages) { | |
| let prompt = ''; | |
| for (const msg of messages) { | |
| if (msg.role === 'user') { | |
| prompt += `|<|im_start|>user\n${msg.content}|<|im_end|>\n`; | |
| } else if (msg.role === 'assistant') { | |
| prompt += `|<|im_start|>assistant\n${msg.content}|<|im_end|>\n`; | |
| } | |
| } | |
| prompt += '|<|im_start|>assistant\n'; | |
| return prompt; | |
| } | |
| // Toast Notification | |
| function showToast(message, type = 'success') { | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| toastMessage.textContent = message; | |
| toast.querySelector('i').className = type === 'error' ? 'fas fa-exclamation-circle text-red-400' : 'fas fa-check-circle text-emerald-400'; | |
| toast.classList.remove('translate-x-full'); | |
| setTimeout(() => { | |
| toast.classList.add('translate-x-full'); | |
| }, 3000); | |
| } | |
| // Sidebar Toggle (Mobile) | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| sidebar.classList.toggle('-translate-x-full'); | |
| } | |
| // Stop Generation | |
| function stopGeneration() { | |
| if (abortController) { | |
| abortController.abort(); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |