userisuser's picture
Deploy MiniCPM-V 4.6 Gradio Server demo
ecb8ee5
<!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>MiniCPM-V | OpenBMB</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
:root {
--bg: #0A0C10;
--blue: #3B5BFF;
--cyan: #27D4EA;
--text: #FFFFFF;
--text-muted: #6E7681;
--glass: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.1);
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg);
color: var(--text);
height: 100vh;
margin: 0;
display: flex;
flex-direction: column;
overflow: hidden; /* Prevent body scroll */
}
h1, h2, h3 { font-family: 'Outfit', sans-serif; }
.chat-scroll-area {
flex: 1;
overflow-y: auto;
padding-bottom: 120px; /* Space for floating input */
-webkit-overflow-scrolling: touch;
}
/* Modern Scrollbar */
.chat-scroll-area::-webkit-scrollbar {
width: 5px;
}
.chat-scroll-area::-webkit-scrollbar-track {
background: transparent;
}
.chat-scroll-area::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.message-bubble {
max-width: 85%;
animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
background: linear-gradient(135deg, var(--blue), var(--cyan));
color: #FFFFFF;
box-shadow: 0 10px 30px rgba(59, 91, 255, 0.2);
}
.bot-message {
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
}
.typing-dot {
width: 4px;
height: 4px;
background: var(--cyan);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.3); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
.input-pill {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
transition: all 0.3s ease;
}
.input-pill:focus-within {
border-color: var(--blue);
box-shadow: 0 0 30px rgba(59, 91, 255, 0.1);
}
.logo-glow {
filter: drop-shadow(0 0 10px rgba(39, 212, 234, 0.3));
}
.send-btn {
background: linear-gradient(135deg, var(--blue), var(--cyan));
transition: transform 0.2s ease, opacity 0.2s ease;
}
.send-btn:active { transform: scale(0.95); }
#user-input::placeholder { color: #555; }
</style>
</head>
<body>
<!-- Minimalist Header -->
<header class="h-20 flex items-center justify-between px-6 md:px-12 shrink-0 z-50">
<div class="flex items-center gap-4">
<img src="https://cdn-avatars.huggingface.co/v1/production/uploads/1670387859384-633fe7784b362488336bbfad.png"
alt="OpenBMB" class="w-10 h-10 logo-glow">
<div>
<h1 class="text-xl font-bold tracking-tight">MiniCPM-V</h1>
<p class="text-[10px] text-muted uppercase tracking-[0.2em] font-medium">By OpenBMB</p>
</div>
</div>
<div class="hidden md:flex items-center gap-2 text-[10px] font-bold text-muted uppercase tracking-widest">
<span class="w-1.5 h-1.5 rounded-full bg-[#27D4EA] animate-pulse"></span>
Vision System Online
</div>
</header>
<!-- Chat Messages Scroll Area -->
<main id="chat-messages" class="chat-scroll-area px-4 md:px-0">
<div class="max-w-3xl mx-auto space-y-8 pt-4">
<!-- Bot Greeting -->
<div class="flex gap-4 items-start">
<div class="bot-message p-6 rounded-3xl rounded-tl-none message-bubble shadow-2xl">
<p class="text-white/90 leading-relaxed text-[15px]">
Welcome to <span class="font-bold text-[#27D4EA]">MiniCPM-V 4.6</span>.
I can analyze images and videos with high efficiency.
<br><br>
Drop a file below to begin.
</p>
</div>
</div>
</div>
</main>
<!-- Floating Input Bar -->
<div class="fixed bottom-0 left-0 right-0 p-6 md:p-10 pointer-events-none">
<div class="max-w-3xl mx-auto pointer-events-auto">
<!-- Media Preview -->
<div id="preview-container" class="hidden mb-6 animate-in">
<div class="relative inline-block group">
<img id="image-preview" src="" class="h-36 w-auto rounded-3xl border border-white/20 shadow-2xl hidden object-cover" />
<video id="video-preview" class="h-36 w-auto rounded-3xl border border-white/20 shadow-2xl hidden object-cover" muted loop></video>
<button id="cancel-file" class="absolute -top-3 -right-3 bg-white text-black rounded-full p-2 shadow-xl hover:bg-neutral-200 transition-all">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
</div>
<!-- Pill Input -->
<div class="input-pill rounded-[2.5rem] p-2 flex items-end gap-2 pr-3 shadow-2xl">
<div class="flex items-center">
<input type="file" id="file-input" class="hidden" accept="image/*,video/*">
<button id="upload-trigger" class="p-4 text-white/30 hover:text-[#27D4EA] transition-colors">
<i data-lucide="paperclip" class="w-6 h-6"></i>
</button>
</div>
<textarea id="user-input" rows="1" placeholder="Type your message..."
class="flex-1 bg-transparent border-none focus:ring-0 text-white py-4 px-1 resize-none max-h-40 scrollbar-none text-[16px] leading-relaxed"></textarea>
<button id="send-btn" class="send-btn w-12 h-12 text-white rounded-full flex items-center justify-center disabled:opacity-20 disabled:grayscale group shrink-0 mb-1">
<i data-lucide="arrow-up" class="w-5 h-5 group-hover:scale-110 transition-transform" id="send-icon"></i>
<i data-lucide="loader-2" class="w-5 h-5 animate-spin hidden" id="loading-icon"></i>
</button>
</div>
</div>
</div>
<script type="module">
import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
lucide.createIcons();
const chatMessages = document.getElementById('chat-messages');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const fileInput = document.getElementById('file-input');
const uploadTrigger = document.getElementById('upload-trigger');
const previewContainer = document.getElementById('preview-container');
const imagePreview = document.getElementById('image-preview');
const videoPreview = document.getElementById('video-preview');
const cancelFile = document.getElementById('cancel-file');
const sendIcon = document.getElementById('send-icon');
const loadingIcon = document.getElementById('loading-icon');
let selectedFile = null;
let client = null;
async function init() {
try {
client = await Client.connect(window.location.origin);
} catch (err) { console.error("Gradio Connection Error", err); }
}
init();
// UI Interactions
uploadTrigger.onclick = () => fileInput.click();
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
selectedFile = file;
previewContainer.classList.remove('hidden');
const url = URL.createObjectURL(file);
if (file.type.startsWith('image/')) {
imagePreview.src = url;
imagePreview.classList.remove('hidden');
videoPreview.classList.add('hidden');
} else {
videoPreview.src = url;
videoPreview.classList.remove('hidden');
imagePreview.classList.add('hidden');
videoPreview.play();
}
}
};
cancelFile.onclick = () => {
selectedFile = null;
fileInput.value = '';
previewContainer.classList.add('hidden');
imagePreview.src = '';
videoPreview.src = '';
};
function appendMessage(role, text, mediaUrl = null, mediaType = null) {
const div = document.createElement('div');
div.className = `flex gap-4 items-start ${role === 'user' ? 'flex-row-reverse' : ''}`;
let mediaHtml = '';
if (mediaUrl) {
if (mediaType.startsWith('image')) {
mediaHtml = `<img src="${mediaUrl}" class="max-w-xs md:max-w-md rounded-3xl mb-4 border border-white/10" />`;
} else {
mediaHtml = `<video src="${mediaUrl}" controls class="max-w-xs md:max-w-md rounded-3xl mb-4 border border-white/10"></video>`;
}
}
const bubbleClass = role === 'user' ? 'user-message' : 'bot-message';
div.innerHTML = `
<div class="${bubbleClass} p-6 rounded-[2rem] ${role === 'user' ? 'rounded-tr-none' : 'rounded-tl-none'} message-bubble shadow-xl">
${mediaHtml}
<p class="leading-relaxed text-[15px] whitespace-pre-wrap font-medium">${text}</p>
</div>
`;
// Get the inner container
const container = chatMessages.querySelector('.max-w-3xl');
container.appendChild(div);
// Smooth scroll to bottom
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
}
async function sendMessage() {
const text = userInput.value.trim();
if (!text && !selectedFile) return;
const content = text;
const file = selectedFile;
userInput.value = '';
userInput.style.height = 'auto';
const fileUrl = file ? URL.createObjectURL(file) : null;
const fileType = file ? file.type : null;
appendMessage('user', content, fileUrl, fileType);
cancelFile.click();
sendIcon.classList.add('hidden');
loadingIcon.classList.remove('hidden');
sendBtn.disabled = true;
const thinkingId = 'think-' + Date.now();
const thinkingDiv = document.createElement('div');
thinkingDiv.id = thinkingId;
thinkingDiv.className = 'flex gap-4 items-start';
thinkingDiv.innerHTML = `
<div class="bot-message p-6 rounded-[2rem] rounded-tl-none message-bubble flex items-center gap-4">
<div class="flex gap-1.5">
<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>
</div>
</div>
`;
const container = chatMessages.querySelector('.max-w-3xl');
container.appendChild(thinkingDiv);
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
try {
let fileData = file ? handle_file(file) : null;
const result = await client.predict("/predict", {
message: content,
file: fileData,
downsample_mode: "16x"
});
document.getElementById(thinkingId).remove();
appendMessage('bot', result.data);
} catch (err) {
document.getElementById(thinkingId).remove();
appendMessage('bot', "The system encountered an error. Please check your file format and try again.");
} finally {
sendIcon.classList.remove('hidden');
loadingIcon.classList.add('hidden');
sendBtn.disabled = false;
}
}
sendBtn.onclick = sendMessage;
userInput.onkeydown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
</script>
</body>
</html>