Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>MiniCPM5-1B | OpenBMB</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/contrib/auto-render.min.js"></script> | |
| <style> | |
| :root { | |
| --bg: #ffffff; | |
| --blue: #4f46e5; | |
| --cyan: #6366f1; | |
| --text: #1e293b; | |
| --text-muted: #64748b; | |
| --glass: rgba(0, 0, 0, 0.02); | |
| --glass-border: #e2e8f0; | |
| --accent: #4f46e5; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg); | |
| color: var(--text); | |
| height: 100vh; | |
| margin: 0; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .chat-scroll-area { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding-bottom: 140px; | |
| -webkit-overflow-scrolling: touch; | |
| scroll-behavior: smooth; | |
| } | |
| .chat-scroll-area::-webkit-scrollbar { width: 4px; } | |
| .chat-scroll-area::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; } | |
| .message-bubble { | |
| max-width: 85%; | |
| padding: 15px; | |
| animation: fadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(12px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .user-message { | |
| background: linear-gradient(135deg, var(--blue), var(--cyan)); | |
| color: #ffffff; | |
| box-shadow: 0 4px 14px rgba(79, 70, 229, 0.2); | |
| border-radius: 20px 20px 4px 20px; | |
| } | |
| .bot-message { | |
| background: #f8fafc; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 20px 20px 20px 4px; | |
| } | |
| .thinking-block { | |
| background: #eef2ff; | |
| border-left: 3px solid var(--accent); | |
| padding: 12px 16px; | |
| margin-bottom: 12px; | |
| border-radius: 4px 12px 12px 4px; | |
| font-size: 14px; | |
| color: var(--text-muted); | |
| font-style: italic; | |
| } | |
| .typing-dot { | |
| width: 4px; height: 4px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| animation: bounce 1.4s infinite ease-in-out; | |
| } | |
| @keyframes bounce { | |
| 0%, 80%, 100% { transform: scale(0.3); opacity: 0.4; } | |
| 40% { transform: scale(1); opacity: 1; } | |
| } | |
| .input-pill { | |
| background: #ffffff; | |
| border: 1px solid var(--glass-border); | |
| transition: all 0.3s ease; | |
| } | |
| .input-pill:focus-within { | |
| border-color: rgba(79, 70, 229, 0.4); | |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); | |
| } | |
| .logo-glow { | |
| filter: drop-shadow(0 0 8px rgba(99, 102, 241, 0.25)); | |
| } | |
| .send-btn { | |
| background: linear-gradient(135deg, var(--blue), var(--cyan)); | |
| transition: all 0.3s ease; | |
| } | |
| .send-btn:hover:not(:disabled) { transform: scale(1.05); filter: brightness(1.05); } | |
| .settings-panel { | |
| background: rgba(255, 255, 255, 0.98); | |
| backdrop-filter: blur(20px); | |
| border-left: 1px solid var(--glass-border); | |
| transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .control-slider { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 4px; | |
| background: #e2e8f0; | |
| border-radius: 2px; | |
| outline: none; | |
| } | |
| .control-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 12px; height: 12px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .toggle-switch { | |
| width: 36px; height: 20px; | |
| background: #e2e8f0; | |
| border-radius: 10px; | |
| position: relative; | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| } | |
| .toggle-switch.active { background: var(--accent); } | |
| .toggle-switch::after { | |
| content: ''; | |
| position: absolute; | |
| top: 2px; left: 2px; | |
| width: 16px; height: 16px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: transform 0.3s; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.15); | |
| } | |
| .toggle-switch.active::after { transform: translateX(16px); } | |
| .bot-message pre { | |
| background: #f1f5f9; | |
| border: 1px solid #e2e8f0; | |
| border-radius: 10px; | |
| padding: 12px; | |
| overflow-x: auto; | |
| } | |
| .bot-message code { color: #4f46e5; } | |
| .thinking-status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 10px; | |
| font-weight: 700; | |
| letter-spacing: 0.12em; | |
| padding: 6px 14px; | |
| border-radius: 100px; | |
| border: 1px solid; | |
| transition: all 0.25s ease; | |
| } | |
| .thinking-status-badge.thinking-on { | |
| color: #15803d; | |
| background: #f0fdf4; | |
| border-color: #bbf7d0; | |
| } | |
| .thinking-status-badge.thinking-on .status-dot { | |
| background: #22c55e; | |
| animation: pulse-dot 2s infinite; | |
| } | |
| .thinking-status-badge.thinking-off { | |
| color: #64748b; | |
| background: #f1f5f9; | |
| border-color: #e2e8f0; | |
| } | |
| .thinking-status-badge.thinking-off .status-dot { | |
| background: #94a3b8; | |
| animation: none; | |
| } | |
| .thinking-status-badge .status-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| } | |
| @keyframes pulse-dot { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.45; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="h-16 flex items-center justify-between px-6 md:px-10 shrink-0 z-50 border-b border-slate-200 bg-white"> | |
| <div class="flex items-center gap-3"> | |
| <img src="https://cdn-avatars.huggingface.co/v1/production/uploads/1670387859384-633fe7784b362488336bbfad.png" | |
| alt="OpenBMB" class="w-10 h-10 logo-glow rounded-lg"> | |
| <div> | |
| <h1 class="text-lg font-bold text-slate-800">MiniCPM5-1B</h1> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-[0.2em] font-semibold">By OpenBMB</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div id="thinking-status-badge" class="thinking-status-badge thinking-on" title="Thinking mode for next message"> | |
| <span class="status-dot"></span> | |
| <span id="thinking-status-label">THINKING</span> | |
| </div> | |
| <button id="toggle-settings" class="p-2 rounded-xl hover:bg-slate-100 text-slate-500 hover:text-slate-800 transition-all"> | |
| <i data-lucide="sliders-horizontal" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <div id="settings-panel" class="fixed top-0 right-0 h-full w-72 z-[100] translate-x-full settings-panel p-6 flex flex-col gap-6 shadow-[-12px_0_40px_rgba(0,0,0,0.08)]"> | |
| <div class="flex items-center justify-between"> | |
| <h2 class="text-base font-bold text-slate-800">Settings</h2> | |
| <button id="close-settings" class="text-slate-400 hover:text-slate-700"> | |
| <i data-lucide="x" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-5"> | |
| <div class="flex items-center justify-between"> | |
| <span class="text-sm font-medium text-slate-600">Thinking</span> | |
| <div id="thinking-toggle" class="toggle-switch active"></div> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Temperature</span> | |
| <span id="temp-val">0.9</span> | |
| </div> | |
| <input type="range" id="temp-slider" min="0" max="1" step="0.05" value="0.9" class="control-slider"> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Top-p</span> | |
| <span id="p-val">0.95</span> | |
| </div> | |
| <input type="range" id="p-slider" min="0" max="1" step="0.01" value="0.95" class="control-slider"> | |
| </div> | |
| <button onclick="clearHistory()" class="w-full py-3 rounded-xl bg-red-50 border border-red-200 text-red-600 text-sm font-bold hover:bg-red-100 transition-all flex items-center justify-center gap-2 mt-4"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| Clear History | |
| </button> | |
| </div> | |
| </div> | |
| <main id="chat-messages" class="chat-scroll-area px-4 flex-1"> | |
| <div class="max-w-3xl mx-auto space-y-6 pt-6 pb-32" id="chat-container"> | |
| <div class="flex gap-3 items-start"> | |
| <div class="bot-message message-bubble shadow-sm"> | |
| <p class="text-slate-700 leading-relaxed text-[15px]"> | |
| Hello! I'm <strong>MiniCPM5-1B</strong>. How can I help you today? | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <div class="fixed bottom-0 left-0 right-0 p-4 md:p-8 pointer-events-none z-50"> | |
| <div class="max-w-3xl mx-auto pointer-events-auto"> | |
| <div class="input-pill rounded-[2rem] p-2 flex items-end shadow-lg"> | |
| <textarea id="user-input" placeholder="Ask MiniCPM5..." rows="1" | |
| class="flex-1 bg-transparent border-none focus:ring-0 text-slate-800 placeholder-slate-400 py-3 px-3 resize-none max-h-40 leading-relaxed font-medium outline-none"></textarea> | |
| <button id="send-btn" class="send-btn w-11 h-11 text-white rounded-full flex items-center justify-center shrink-0 mb-0.5 mr-1"> | |
| <i data-lucide="arrow-up" class="w-5 h-5" id="send-icon"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| lucide.createIcons(); | |
| const chatContainer = document.getElementById('chat-container'); | |
| const chatScrollArea = document.getElementById('chat-messages'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const settingsPanel = document.getElementById('settings-panel'); | |
| const toggleSettings = document.getElementById('toggle-settings'); | |
| const closeSettings = document.getElementById('close-settings'); | |
| const thinkingToggle = document.getElementById('thinking-toggle'); | |
| const tempSlider = document.getElementById('temp-slider'); | |
| const pSlider = document.getElementById('p-slider'); | |
| let client = null; | |
| let chatHistory = []; | |
| let currentJob = null; | |
| let isSettingsOpen = false; | |
| const THINK_CLOSE = '</think>'; | |
| const thinkingStatusBadge = document.getElementById('thinking-status-badge'); | |
| const thinkingStatusLabel = document.getElementById('thinking-status-label'); | |
| function updateThinkingStatusBadge() { | |
| const on = thinkingToggle.classList.contains('active'); | |
| thinkingStatusBadge.classList.toggle('thinking-on', on); | |
| thinkingStatusBadge.classList.toggle('thinking-off', !on); | |
| thinkingStatusLabel.textContent = on ? 'THINKING' : 'NOTHINK'; | |
| thinkingStatusBadge.title = on ? 'Thinking enabled for next message' : 'Thinking disabled for next message'; | |
| } | |
| async function init() { | |
| try { | |
| client = await Client.connect(window.location.origin, { events: ["data", "status"] }); | |
| } catch (err) { | |
| console.error("Gradio connection error", err); | |
| } | |
| } | |
| init(); | |
| function renderMath(el) { | |
| if (window.renderMathInElement) { | |
| renderMathInElement(el, { | |
| delimiters: [ | |
| {left: '$$', right: '$$', display: true}, | |
| {left: '$', right: '$', display: false}, | |
| ], | |
| throwOnError: false | |
| }); | |
| } | |
| } | |
| function splitThinking(fullText, thinkingEnabled) { | |
| const text = fullText.replace('<think>', '').replace('<|im_end|>', ''); | |
| if (!thinkingEnabled) { | |
| return { thinking: '', answer: text.trim() }; | |
| } | |
| const pos = text.indexOf(THINK_CLOSE); | |
| if (pos === -1) { | |
| return { thinking: text.trim(), answer: '' }; | |
| } | |
| return { | |
| thinking: text.slice(0, pos).trim(), | |
| answer: text.slice(pos + THINK_CLOSE.length).trim() | |
| }; | |
| } | |
| function appendMessage(role, text = '') { | |
| const div = document.createElement('div'); | |
| div.className = `flex gap-3 items-start ${role === 'user' ? 'flex-row-reverse' : ''}`; | |
| const bubbleClass = role === 'user' ? 'user-message' : 'bot-message'; | |
| div.innerHTML = ` | |
| <div class="${bubbleClass} message-bubble shadow-sm"> | |
| <div class="thinking-container hidden"></div> | |
| <div class="content-container leading-relaxed text-[15px]">${role === 'user' ? escapeHtml(text) : marked.parse(text)}</div> | |
| </div> | |
| `; | |
| chatContainer.appendChild(div); | |
| if (role === 'bot') renderMath(div.querySelector('.content-container')); | |
| chatScrollArea.scrollTo({ top: chatScrollArea.scrollHeight, behavior: 'smooth' }); | |
| return div; | |
| } | |
| function escapeHtml(s) { | |
| return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| } | |
| function updateBotMessage(div, fullText) { | |
| const thinkingContainer = div.querySelector('.thinking-container'); | |
| const contentContainer = div.querySelector('.content-container'); | |
| const thinkingEnabled = thinkingToggle.classList.contains('active'); | |
| const { thinking, answer } = splitThinking(fullText, thinkingEnabled); | |
| if (thinking) { | |
| thinkingContainer.classList.remove('hidden'); | |
| thinkingContainer.innerHTML = `<div class="thinking-block">${marked.parse(thinking)}</div>`; | |
| } else { | |
| thinkingContainer.classList.add('hidden'); | |
| thinkingContainer.innerHTML = ''; | |
| } | |
| if (answer) { | |
| contentContainer.innerHTML = marked.parse(answer); | |
| } else if (thinkingEnabled && thinking) { | |
| contentContainer.innerHTML = '<div class="flex gap-1.5 py-1"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>'; | |
| } else { | |
| contentContainer.innerHTML = ''; | |
| } | |
| renderMath(thinkingContainer); | |
| renderMath(contentContainer); | |
| chatScrollArea.scrollTo({ top: chatScrollArea.scrollHeight, behavior: 'smooth' }); | |
| return answer; | |
| } | |
| async function sendMessage() { | |
| const text = userInput.value.trim(); | |
| if (!text) return; | |
| userInput.value = ''; | |
| userInput.style.height = 'auto'; | |
| appendMessage('user', text); | |
| sendBtn.disabled = true; | |
| const botDiv = appendMessage('bot', ''); | |
| const contentContainer = botDiv.querySelector('.content-container'); | |
| contentContainer.innerHTML = '<div class="flex gap-1.5 py-2"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>'; | |
| let isStopped = false; | |
| sendBtn.onclick = () => { | |
| if (currentJob) { | |
| currentJob.cancel(); | |
| isStopped = true; | |
| resetSendBtn(); | |
| } | |
| }; | |
| try { | |
| currentJob = client.submit("/predict", { | |
| message: text, | |
| history: chatHistory, | |
| thinking_mode: thinkingToggle.classList.contains('active'), | |
| temperature: parseFloat(tempSlider.value), | |
| top_p: parseFloat(pSlider.value), | |
| }); | |
| let finalAnswer = ""; | |
| for await (const msg of currentJob) { | |
| if (isStopped) break; | |
| if (msg.type === "data" && msg.data) { | |
| finalAnswer = updateBotMessage(botDiv, msg.data[0]); | |
| } else if (msg.type === "status" && msg.stage === "complete") { | |
| break; | |
| } else if (msg.type === "status" && msg.stage === "error") { | |
| throw new Error(msg.message || "Generation failed"); | |
| } | |
| } | |
| if (!isStopped && finalAnswer) { | |
| chatHistory.push([text, finalAnswer]); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| if (!isStopped) { | |
| contentContainer.innerHTML = '<p class="text-red-500">Error: please try again.</p>'; | |
| } | |
| } finally { | |
| resetSendBtn(); | |
| currentJob = null; | |
| } | |
| } | |
| function resetSendBtn() { | |
| sendBtn.disabled = false; | |
| sendBtn.onclick = sendMessage; | |
| } | |
| window.clearHistory = function() { | |
| chatHistory = []; | |
| chatContainer.innerHTML = ` | |
| <div class="flex gap-3 items-start"> | |
| <div class="bot-message message-bubble shadow-sm"> | |
| <p class="text-slate-700 leading-relaxed text-[15px]">History cleared. How can I help you?</p> | |
| </div> | |
| </div> | |
| `; | |
| toggleSettingsSidebar(false); | |
| }; | |
| function toggleSettingsSidebar(open) { | |
| isSettingsOpen = open; | |
| settingsPanel.classList.toggle('translate-x-full', !open); | |
| settingsPanel.classList.toggle('translate-x-0', open); | |
| } | |
| toggleSettings.onclick = (e) => { e.stopPropagation(); toggleSettingsSidebar(true); }; | |
| closeSettings.onclick = () => toggleSettingsSidebar(false); | |
| document.addEventListener('click', (e) => { | |
| if (isSettingsOpen && !settingsPanel.contains(e.target) && !toggleSettings.contains(e.target)) { | |
| toggleSettingsSidebar(false); | |
| } | |
| }); | |
| thinkingToggle.onclick = () => { | |
| thinkingToggle.classList.toggle('active'); | |
| if (chatHistory.length > 0) { | |
| if (confirm("Changing Thinking mode will clear conversation history. Continue?")) { | |
| clearHistory(); | |
| updateThinkingStatusBadge(); | |
| } else { | |
| thinkingToggle.classList.toggle('active'); | |
| } | |
| } else { | |
| updateThinkingStatusBadge(); | |
| } | |
| }; | |
| updateThinkingStatusBadge(); | |
| tempSlider.oninput = () => document.getElementById('temp-val').textContent = tempSlider.value; | |
| pSlider.oninput = () => document.getElementById('p-val').textContent = pSlider.value; | |
| sendBtn.onclick = sendMessage; | |
| userInput.onkeydown = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }; | |
| userInput.oninput = () => { | |
| userInput.style.height = 'auto'; | |
| userInput.style.height = userInput.scrollHeight + 'px'; | |
| }; | |
| </script> | |
| </body> | |
| </html> | |