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>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> | |