MiniCPM5-1B-Demo / index.html
mac
update rewritten ui: small padding
e0cc803
<!DOCTYPE html>
<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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
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>