akhaliq HF Staff commited on
Commit
96cb7bd
·
1 Parent(s): 803abce

refactor: update UI layout with improved mobile-responsive drawer, custom typography, and modernized glassmorphism components

Browse files
Files changed (1) hide show
  1. index.html +233 -194
index.html CHANGED
@@ -2,229 +2,274 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>MiniCPM-V 4.6 | Next-Gen Multimodal AI</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
  <script src="https://unpkg.com/lucide@latest"></script>
10
  <style>
11
  :root {
12
- --glass-bg: rgba(17, 24, 39, 0.7);
13
- --glass-border: rgba(255, 255, 255, 0.1);
14
  --accent: #6366f1;
15
- --accent-glow: rgba(99, 102, 241, 0.3);
 
 
16
  }
17
 
18
  body {
19
  font-family: 'Inter', sans-serif;
20
- background-color: #030712;
21
- color: #f3f4f6;
22
- overflow-x: hidden;
 
23
  }
24
 
 
 
25
  .glass {
26
- background: var(--glass-bg);
27
- backdrop-filter: blur(12px);
 
28
  border: 1px solid var(--glass-border);
29
  }
30
 
31
  .chat-container {
32
- height: calc(100vh - 180px);
33
- scrollbar-width: thin;
34
- scrollbar-color: var(--glass-border) transparent;
35
- }
36
-
37
- .chat-container::-webkit-scrollbar {
38
- width: 6px;
39
  }
 
40
 
41
- .chat-container::-webkit-scrollbar-thumb {
42
- background: var(--glass-border);
43
- border-radius: 10px;
44
  }
45
 
46
- .message-anim {
47
- animation: slideUp 0.3s ease-out forwards;
 
48
  }
49
 
50
- @keyframes slideUp {
51
- from { opacity: 0; transform: translateY(10px); }
52
- to { opacity: 1; transform: translateY(0); }
53
  }
54
 
55
- .gradient-text {
56
- background: linear-gradient(135deg, #818cf8, #c084fc);
57
- -webkit-background-clip: text;
58
- -webkit-text-fill-color: transparent;
59
  }
60
 
61
- .glow-button {
62
- transition: all 0.3s ease;
 
63
  }
64
 
65
- .glow-button:hover {
66
- box-shadow: 0 0 20px var(--accent-glow);
67
- transform: translateY(-1px);
 
 
 
68
  }
 
 
69
 
70
- .file-preview-container {
71
- position: relative;
72
- display: inline-block;
73
  }
74
 
75
- .remove-file {
76
- position: absolute;
77
- top: -8px;
78
- right: -8px;
79
- background: #ef4444;
80
- border-radius: 50%;
81
- padding: 2px;
82
- cursor: pointer;
83
- display: none;
84
  }
85
 
86
- .file-preview-container:hover .remove-file {
87
- display: block;
88
  }
89
 
90
- #loading-spinner {
91
- display: none;
 
 
 
 
 
 
 
 
 
 
92
  }
93
  </style>
94
  </head>
95
- <body class="min-h-screen flex flex-col">
96
- <!-- Header -->
97
- <header class="h-16 glass fixed top-0 w-full z-50 flex items-center justify-between px-6 border-b border-white/5">
98
- <div class="flex items-center gap-3">
99
- <div class="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
100
- <i data-lucide="zap" class="w-5 h-5 text-white"></i>
 
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
- <h1 class="text-xl font-bold tracking-tight gradient-text">MiniCPM-V 4.6</h1>
103
- </div>
104
- <div class="flex items-center gap-6 text-sm font-medium text-gray-400">
105
- <a href="#" class="hover:text-white transition-colors">Docs</a>
106
- <a href="#" class="hover:text-white transition-colors">GitHub</a>
107
- <div class="h-4 w-[1px] bg-white/10"></div>
108
- <button class="glass px-4 py-1.5 rounded-full text-xs border border-white/10 hover:bg-white/5 transition-all">
109
- v4.6.0-stable
110
- </button>
111
- </div>
112
- </header>
113
-
114
- <!-- Sidebar -->
115
- <aside class="fixed left-0 top-16 w-64 h-full glass border-r border-white/5 p-4 hidden md:block">
116
- <div class="mb-8">
117
- <h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">Mode Settings</h2>
118
- <div class="space-y-4">
119
- <div>
120
- <label class="text-xs text-gray-400 mb-2 block">Downsample Mode</label>
121
- <select id="downsample-mode" class="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500">
122
- <option value="16x">16x (Fast)</option>
123
- <option value="4x">4x (Finer Detail)</option>
124
  </select>
125
  </div>
126
- </div>
 
 
 
 
 
 
 
 
127
  </div>
128
 
129
- <div>
130
- <h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">Quick Actions</h2>
131
- <button class="w-full text-left px-3 py-2 text-sm text-gray-400 hover:text-white hover:bg-white/5 rounded-lg transition-all flex items-center gap-3">
132
- <i data-lucide="image" class="w-4 h-4"></i> Image Analysis
133
- </button>
134
- <button class="w-full text-left px-3 py-2 text-sm text-gray-400 hover:text-white hover:bg-white/5 rounded-lg transition-all flex items-center gap-3">
135
- <i data-lucide="video" class="w-4 h-4"></i> Video Understanding
136
- </button>
137
  </div>
138
  </aside>
139
 
140
- <!-- Main Chat Area -->
141
- <main class="flex-1 mt-16 md:ml-64 p-4 md:p-8 flex flex-col">
142
- <div id="chat-messages" class="chat-container space-y-6 pb-24 overflow-y-auto">
143
- <!-- Welcome Message -->
144
- <div class="flex gap-4 max-w-3xl mx-auto items-start message-anim">
145
- <div class="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center shrink-0 border border-indigo-500/30">
146
- <i data-lucide="bot" class="w-4 h-4 text-indigo-400"></i>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  </div>
148
- <div class="glass p-5 rounded-2xl rounded-tl-none border border-white/5">
149
- <p class="text-gray-200 leading-relaxed">
150
- Hello! I am **MiniCPM-V 4.6**, an ultra-efficient multimodal assistant. I can help you understand images and videos with high precision.
151
- <br><br>
152
- Try uploading an image or a video to get started!
153
  </p>
154
  </div>
155
  </div>
156
  </div>
157
 
158
- <!-- Input Section -->
159
- <div class="fixed bottom-0 left-0 md:left-64 right-0 p-4 bg-gradient-to-t from-[#030712] via-[#030712] to-transparent">
160
- <div class="max-w-4xl mx-auto glass rounded-2xl p-2 border border-white/10 shadow-2xl">
161
- <div id="preview-area" class="px-4 py-2 hidden">
162
- <div class="file-preview-container">
163
- <img id="image-preview" src="" class="h-20 w-auto rounded-lg border border-white/10 hidden" />
164
- <video id="video-preview" class="h-20 w-auto rounded-lg border border-white/10 hidden" muted loop></video>
165
- <div id="remove-file-btn" class="remove-file"><i data-lucide="x" class="w-3 h-3 text-white"></i></div>
 
 
 
166
  </div>
167
  </div>
168
-
169
- <div class="flex items-end gap-2 px-2 pb-1 pt-1">
170
- <button id="upload-btn" class="p-3 text-gray-400 hover:text-white hover:bg-white/5 rounded-xl transition-all">
171
- <i data-lucide="paperclip" class="w-5 h-5"></i>
172
- </button>
173
- <input type="file" id="file-input" class="hidden" accept="image/*,video/*">
174
-
175
- <textarea id="user-input" rows="1" placeholder="Ask anything about the media..." class="flex-1 bg-transparent border-none focus:ring-0 text-white placeholder-gray-500 py-3 resize-none max-h-48 scrollbar-none" oninput="this.style.height = ''; this.style.height = this.scrollHeight + 'px'"></textarea>
176
-
177
- <button id="send-btn" class="bg-indigo-600 hover:bg-indigo-500 text-white p-3 rounded-xl glow-button flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed">
178
- <i data-lucide="arrow-up" class="w-5 h-5" id="send-icon"></i>
179
- <i data-lucide="loader-2" class="w-5 h-5 animate-spin hidden" id="loading-spinner"></i>
180
- </button>
 
 
 
 
 
181
  </div>
182
  </div>
183
- <p class="text-[10px] text-center text-gray-600 mt-2">MiniCPM-V 4.6 may produce inaccurate information about people, places, or facts.</p>
184
  </div>
185
  </main>
186
 
187
  <script type="module">
188
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
189
 
190
- // Initialize Lucide icons
191
  lucide.createIcons();
192
 
 
193
  const chatMessages = document.getElementById('chat-messages');
194
  const userInput = document.getElementById('user-input');
195
  const sendBtn = document.getElementById('send-btn');
196
  const fileInput = document.getElementById('file-input');
197
- const uploadBtn = document.getElementById('upload-btn');
198
- const previewArea = document.getElementById('preview-area');
199
  const imagePreview = document.getElementById('image-preview');
200
  const videoPreview = document.getElementById('video-preview');
201
- const removeFileBtn = document.getElementById('remove-file-btn');
 
 
 
202
  const downsampleMode = document.getElementById('downsample-mode');
203
  const sendIcon = document.getElementById('send-icon');
204
- const loadingSpinner = document.getElementById('loading-spinner');
205
 
206
  let selectedFile = null;
207
  let client = null;
208
 
209
- async function initClient() {
 
210
  try {
211
  client = await Client.connect(window.location.origin);
212
- console.log("Gradio Client Connected");
213
- } catch (error) {
214
- console.error("Failed to connect to Gradio backend:", error);
215
  }
216
  }
 
217
 
218
- initClient();
219
-
220
- uploadBtn.onclick = () => fileInput.click();
 
 
 
 
 
 
221
 
 
 
222
  fileInput.onchange = (e) => {
223
  const file = e.target.files[0];
224
  if (file) {
225
  selectedFile = file;
226
- previewArea.classList.remove('hidden');
227
-
228
  const url = URL.createObjectURL(file);
229
  if (file.type.startsWith('image/')) {
230
  imagePreview.src = url;
@@ -239,118 +284,112 @@
239
  }
240
  };
241
 
242
- removeFileBtn.onclick = () => {
243
  selectedFile = null;
244
  fileInput.value = '';
245
- previewArea.classList.add('hidden');
246
  imagePreview.src = '';
247
  videoPreview.src = '';
248
- videoPreview.pause();
249
  };
250
 
251
- function addMessage(role, content, fileUrl = null, fileType = null) {
252
  const div = document.createElement('div');
253
- div.className = `flex gap-4 max-w-3xl mx-auto items-start message-anim ${role === 'user' ? 'flex-row-reverse' : ''}`;
254
-
255
- const icon = role === 'user' ? 'user' : 'bot';
256
- const iconColor = role === 'user' ? 'gray' : 'indigo';
257
 
258
  let mediaHtml = '';
259
- if (fileUrl) {
260
- if (fileType.startsWith('image')) {
261
- mediaHtml = `<img src="${fileUrl}" class="max-w-xs rounded-lg mb-3 border border-white/10" />`;
262
  } else {
263
- mediaHtml = `<video src="${fileUrl}" controls class="max-w-xs rounded-lg mb-3 border border-white/10"></video>`;
264
  }
265
  }
266
 
 
 
 
 
267
  div.innerHTML = `
268
- <div class="w-8 h-8 rounded-full bg-${iconColor}-500/20 flex items-center justify-center shrink-0 border border-${iconColor}-500/30">
269
- <i data-lucide="${icon}" class="w-4 h-4 text-${iconColor}-400"></i>
270
  </div>
271
- <div class="glass p-5 rounded-2xl ${role === 'user' ? 'rounded-tr-none' : 'rounded-tl-none'} border border-white/5">
272
  ${mediaHtml}
273
- <div class="text-gray-200 leading-relaxed whitespace-pre-wrap">${content}</div>
274
  </div>
275
  `;
276
  chatMessages.appendChild(div);
277
  lucide.createIcons();
278
- chatMessages.scrollTop = chatMessages.scrollHeight;
279
  }
280
 
281
- async function handleSend() {
282
  const text = userInput.value.trim();
283
  if (!text && !selectedFile) return;
284
 
285
- const currentFile = selectedFile;
286
- const currentText = text;
287
- const currentMode = downsampleMode.value;
288
 
289
- // Clear input
290
  userInput.value = '';
291
  userInput.style.height = 'auto';
292
- const fileUrl = currentFile ? URL.createObjectURL(currentFile) : null;
293
- const fileType = currentFile ? currentFile.type : null;
294
 
295
- addMessage('user', currentText, fileUrl, fileType);
296
-
297
- // Show loading state
 
298
  sendIcon.classList.add('hidden');
299
- loadingSpinner.classList.remove('hidden');
300
  sendBtn.disabled = true;
301
 
302
- // Add thinking placeholder
 
303
  const thinkingDiv = document.createElement('div');
304
- thinkingDiv.className = 'flex gap-4 max-w-3xl mx-auto items-start message-anim';
305
- thinkingDiv.id = 'thinking-placeholder';
306
  thinkingDiv.innerHTML = `
307
- <div class="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center shrink-0 border border-indigo-500/30">
308
- <i data-lucide="bot" class="w-4 h-4 text-indigo-400"></i>
309
  </div>
310
- <div class="glass p-5 rounded-2xl rounded-tl-none border border-white/5">
311
- <div class="flex items-center gap-2 text-gray-400 italic">
312
- <i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i> MiniCPM is thinking...
313
  </div>
 
314
  </div>
315
  `;
316
  chatMessages.appendChild(thinkingDiv);
 
317
  lucide.createIcons();
318
- chatMessages.scrollTop = chatMessages.scrollHeight;
319
 
320
  try {
321
- let fileData = null;
322
- if (currentFile) {
323
- fileData = handle_file(currentFile);
324
- }
325
-
326
- console.log("Sending request to MiniCPM-V 4.6 backend...");
327
  const result = await client.predict("/predict", {
328
- message: currentText,
329
  file: fileData,
330
- downsample_mode: currentMode
331
  });
332
-
333
- // Remove placeholder and add actual message
334
- document.getElementById('thinking-placeholder')?.remove();
335
- addMessage('assistant', result.data);
336
- } catch (error) {
337
- console.error("Prediction failed:", error);
338
- document.getElementById('thinking-placeholder')?.remove();
339
- addMessage('assistant', "Sorry, I encountered an error while processing your request. Please check the Space logs.");
340
  } finally {
341
  sendIcon.classList.remove('hidden');
342
- loadingSpinner.classList.add('hidden');
343
  sendBtn.disabled = false;
344
- removeFileBtn.onclick(); // Reset preview
345
  }
346
  }
347
 
348
- sendBtn.onclick = handleSend;
349
-
350
  userInput.onkeydown = (e) => {
351
- if (e.key === 'Enter' && !e.shiftKey) {
352
  e.preventDefault();
353
- handleSend();
354
  }
355
  };
356
  </script>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>MiniCPM-V 4.6 | Next-Gen Multimodal AI</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
+ <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">
9
  <script src="https://unpkg.com/lucide@latest"></script>
10
  <style>
11
  :root {
 
 
12
  --accent: #6366f1;
13
+ --accent-dark: #4f46e5;
14
+ --glass: rgba(15, 23, 42, 0.7);
15
+ --glass-border: rgba(255, 255, 255, 0.08);
16
  }
17
 
18
  body {
19
  font-family: 'Inter', sans-serif;
20
+ background: radial-gradient(circle at top left, #1e1b4b, #0f172a, #020617);
21
+ color: #f8fafc;
22
+ height: 100vh;
23
+ overflow: hidden;
24
  }
25
 
26
+ h1, h2, h3 { font-family: 'Outfit', sans-serif; }
27
+
28
  .glass {
29
+ background: var(--glass);
30
+ backdrop-filter: blur(16px);
31
+ -webkit-backdrop-filter: blur(16px);
32
  border: 1px solid var(--glass-border);
33
  }
34
 
35
  .chat-container {
36
+ scrollbar-width: none;
37
+ -ms-overflow-style: none;
 
 
 
 
 
38
  }
39
+ .chat-container::-webkit-scrollbar { display: none; }
40
 
41
+ .message-bubble {
42
+ max-width: 85%;
43
+ transition: transform 0.2s ease;
44
  }
45
 
46
+ .user-message {
47
+ background: linear-gradient(135deg, var(--accent), var(--accent-dark));
48
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.2);
49
  }
50
 
51
+ .bot-message {
52
+ background: rgba(30, 41, 59, 0.5);
53
+ border: 1px solid var(--glass-border);
54
  }
55
 
56
+ .animate-in {
57
+ animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
 
 
58
  }
59
 
60
+ @keyframes slideUp {
61
+ from { opacity: 0; transform: translateY(20px); }
62
+ to { opacity: 1; transform: translateY(0); }
63
  }
64
 
65
+ .typing-dot {
66
+ width: 4px;
67
+ height: 4px;
68
+ background: #94a3b8;
69
+ border-radius: 50%;
70
+ animation: bounce 1.4s infinite ease-in-out;
71
  }
72
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
73
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
74
 
75
+ @keyframes bounce {
76
+ 0%, 80%, 100% { transform: scale(0); }
77
+ 40% { transform: scale(1.0); }
78
  }
79
 
80
+ .input-glow:focus-within {
81
+ box-shadow: 0 0 25px rgba(99, 102, 241, 0.15);
82
+ border-color: rgba(99, 102, 241, 0.4);
 
 
 
 
 
 
83
  }
84
 
85
+ .mobile-drawer {
86
+ transition: transform 0.3s ease-in-out;
87
  }
88
 
89
+ @media (max-width: 768px) {
90
+ .mobile-drawer {
91
+ position: fixed;
92
+ left: 0;
93
+ top: 0;
94
+ bottom: 0;
95
+ z-index: 100;
96
+ transform: translateX(-100%);
97
+ }
98
+ .mobile-drawer.active {
99
+ transform: translateX(0);
100
+ }
101
  }
102
  </style>
103
  </head>
104
+ <body class="flex">
105
+
106
+ <!-- Mobile Header -->
107
+ <div class="md:hidden fixed top-0 left-0 right-0 h-14 glass z-50 flex items-center justify-between px-4">
108
+ <button id="menu-toggle" class="p-2"><i data-lucide="menu" class="w-6 h-6"></i></button>
109
+ <div class="font-bold text-lg tracking-tight bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">MiniCPM-V</div>
110
+ <div class="w-10"></div>
111
+ </div>
112
+
113
+ <!-- Sidebar / Drawer -->
114
+ <aside id="sidebar" class="mobile-drawer w-72 h-screen glass border-r border-white/5 flex flex-col md:translate-x-0">
115
+ <div class="p-6">
116
+ <div class="flex items-center gap-3 mb-8">
117
+ <div class="w-10 h-10 bg-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-500/20">
118
+ <i data-lucide="sparkles" class="w-6 h-6 text-white"></i>
119
+ </div>
120
+ <h1 class="text-xl font-bold tracking-tight">MiniCPM-V <span class="text-xs opacity-50 block font-normal">v4.6 Ultra-Efficient</span></h1>
121
  </div>
122
+
123
+ <nav class="space-y-1">
124
+ <h2 class="text-[10px] uppercase tracking-widest text-slate-500 font-bold mb-4 ml-2">Configuration</h2>
125
+ <div class="p-3 rounded-xl bg-white/5 border border-white/5 mb-6">
126
+ <label class="text-[11px] text-slate-400 block mb-2 font-medium">Visual Precision</label>
127
+ <select id="downsample-mode" class="w-full bg-slate-900/50 border border-white/10 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 appearance-none cursor-pointer">
128
+ <option value="16x">🚀 16x (Fast)</option>
129
+ <option value="4x">💎 4x (Detailed)</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  </select>
131
  </div>
132
+
133
+ <h2 class="text-[10px] uppercase tracking-widest text-slate-500 font-bold mb-4 ml-2">Quick Access</h2>
134
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-slate-400 hover:text-white hover:bg-white/5 transition-all group">
135
+ <i data-lucide="book-open" class="w-4 h-4 group-hover:text-indigo-400"></i> Model Card
136
+ </a>
137
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-slate-400 hover:text-white hover:bg-white/5 transition-all group">
138
+ <i data-lucide="github" class="w-4 h-4 group-hover:text-indigo-400"></i> Repository
139
+ </a>
140
+ </nav>
141
  </div>
142
 
143
+ <div class="mt-auto p-6 border-t border-white/5">
144
+ <div class="flex items-center gap-3 p-3 rounded-xl bg-indigo-500/10 border border-indigo-500/20">
145
+ <i data-lucide="cpu" class="w-5 h-5 text-indigo-400"></i>
146
+ <div class="text-[11px]">
147
+ <span class="block text-indigo-300 font-bold">ZeroGPU Powered</span>
148
+ <span class="text-indigo-300/60">Dynamic Allocation</span>
149
+ </div>
150
+ </div>
151
  </div>
152
  </aside>
153
 
154
+ <!-- Overlay -->
155
+ <div id="overlay" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[90] hidden md:hidden"></div>
156
+
157
+ <!-- Main Content -->
158
+ <main class="flex-1 flex flex-col h-screen relative">
159
+ <!-- Desktop Header -->
160
+ <header class="hidden md:flex h-16 items-center justify-between px-8 border-b border-white/5">
161
+ <div class="text-sm text-slate-400 flex items-center gap-2">
162
+ <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> Systems Active
163
+ </div>
164
+ <div class="flex items-center gap-4">
165
+ <span class="text-xs px-3 py-1 rounded-full bg-white/5 border border-white/10 text-slate-400">openbmb/MiniCPM-V-4.6</span>
166
+ </div>
167
+ </header>
168
+
169
+ <!-- Chat History -->
170
+ <div id="chat-messages" class="flex-1 overflow-y-auto p-4 md:p-10 space-y-8 chat-container">
171
+ <!-- Bot Greeting -->
172
+ <div class="flex gap-4 items-start animate-in">
173
+ <div class="w-9 h-9 rounded-xl bg-indigo-600 flex items-center justify-center shrink-0 shadow-lg shadow-indigo-500/20">
174
+ <i data-lucide="bot" class="w-5 h-5 text-white"></i>
175
  </div>
176
+ <div class="bot-message p-5 rounded-2xl rounded-tl-none message-bubble shadow-xl">
177
+ <p class="text-slate-200 leading-relaxed text-[15px]">
178
+ Hi! I'm MiniCPM-V, your multimodal AI assistant. Upload an image or video, and I'll help you understand it in detail.
 
 
179
  </p>
180
  </div>
181
  </div>
182
  </div>
183
 
184
+ <!-- Input Area -->
185
+ <div class="p-4 md:p-8 relative">
186
+ <div class="max-w-4xl mx-auto">
187
+ <!-- Preview -->
188
+ <div id="preview-container" class="hidden mb-4 animate-in">
189
+ <div class="relative inline-block group">
190
+ <img id="image-preview" src="" class="h-32 w-auto rounded-2xl border-2 border-indigo-500/50 shadow-2xl hidden" />
191
+ <video id="video-preview" class="h-32 w-auto rounded-2xl border-2 border-indigo-500/50 shadow-2xl hidden" muted loop></video>
192
+ <button id="cancel-file" class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1.5 shadow-lg hover:bg-red-600 transition-colors">
193
+ <i data-lucide="x" class="w-4 h-4"></i>
194
+ </button>
195
  </div>
196
  </div>
197
+
198
+ <!-- Input Box -->
199
+ <div class="glass rounded-3xl p-2 input-glow border border-white/10 shadow-2xl transition-all duration-300">
200
+ <div class="flex items-end gap-2 pr-1">
201
+ <div class="flex items-center">
202
+ <input type="file" id="file-input" class="hidden" accept="image/*,video/*">
203
+ <button id="upload-trigger" class="p-3 text-slate-400 hover:text-white hover:bg-white/5 rounded-2xl transition-all">
204
+ <i data-lucide="plus-circle" class="w-6 h-6"></i>
205
+ </button>
206
+ </div>
207
+
208
+ <textarea id="user-input" rows="1" placeholder="Ask anything..." class="flex-1 bg-transparent border-none focus:ring-0 text-white placeholder-slate-500 py-3 px-2 resize-none max-h-40 scrollbar-none text-[15px]" oninput="this.style.height = ''; this.style.height = this.scrollHeight + 'px'"></textarea>
209
+
210
+ <button id="send-btn" class="w-12 h-12 bg-indigo-600 hover:bg-indigo-500 text-white rounded-2xl flex items-center justify-center transition-all disabled:opacity-30 disabled:cursor-not-allowed group">
211
+ <i data-lucide="arrow-up" class="w-5 h-5 group-hover:scale-110 transition-transform" id="send-icon"></i>
212
+ <i data-lucide="loader-2" class="w-5 h-5 animate-spin hidden" id="loading-icon"></i>
213
+ </button>
214
+ </div>
215
  </div>
216
  </div>
 
217
  </div>
218
  </main>
219
 
220
  <script type="module">
221
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
222
 
 
223
  lucide.createIcons();
224
 
225
+ // UI Selectors
226
  const chatMessages = document.getElementById('chat-messages');
227
  const userInput = document.getElementById('user-input');
228
  const sendBtn = document.getElementById('send-btn');
229
  const fileInput = document.getElementById('file-input');
230
+ const uploadTrigger = document.getElementById('upload-trigger');
231
+ const previewContainer = document.getElementById('preview-container');
232
  const imagePreview = document.getElementById('image-preview');
233
  const videoPreview = document.getElementById('video-preview');
234
+ const cancelFile = document.getElementById('cancel-file');
235
+ const sidebar = document.getElementById('sidebar');
236
+ const menuToggle = document.getElementById('menu-toggle');
237
+ const overlay = document.getElementById('overlay');
238
  const downsampleMode = document.getElementById('downsample-mode');
239
  const sendIcon = document.getElementById('send-icon');
240
+ const loadingIcon = document.getElementById('loading-icon');
241
 
242
  let selectedFile = null;
243
  let client = null;
244
 
245
+ // Init Gradio Client
246
+ async function init() {
247
  try {
248
  client = await Client.connect(window.location.origin);
249
+ console.log("Connected to Gradio Server");
250
+ } catch (err) {
251
+ console.error("Connection failed", err);
252
  }
253
  }
254
+ init();
255
 
256
+ // Sidebar logic
257
+ menuToggle.onclick = () => {
258
+ sidebar.classList.add('active');
259
+ overlay.classList.remove('hidden');
260
+ };
261
+ overlay.onclick = () => {
262
+ sidebar.classList.remove('active');
263
+ overlay.classList.add('hidden');
264
+ };
265
 
266
+ // File handling
267
+ uploadTrigger.onclick = () => fileInput.click();
268
  fileInput.onchange = (e) => {
269
  const file = e.target.files[0];
270
  if (file) {
271
  selectedFile = file;
272
+ previewContainer.classList.remove('hidden');
 
273
  const url = URL.createObjectURL(file);
274
  if (file.type.startsWith('image/')) {
275
  imagePreview.src = url;
 
284
  }
285
  };
286
 
287
+ cancelFile.onclick = () => {
288
  selectedFile = null;
289
  fileInput.value = '';
290
+ previewContainer.classList.add('hidden');
291
  imagePreview.src = '';
292
  videoPreview.src = '';
 
293
  };
294
 
295
+ function appendMessage(role, text, mediaUrl = null, mediaType = null) {
296
  const div = document.createElement('div');
297
+ div.className = `flex gap-4 items-start animate-in ${role === 'user' ? 'flex-row-reverse' : ''}`;
 
 
 
298
 
299
  let mediaHtml = '';
300
+ if (mediaUrl) {
301
+ if (mediaType.startsWith('image')) {
302
+ mediaHtml = `<img src="${mediaUrl}" class="max-w-xs md:max-w-md rounded-xl mb-3 border border-white/10" />`;
303
  } else {
304
+ mediaHtml = `<video src="${mediaUrl}" controls class="max-w-xs md:max-w-md rounded-xl mb-3 border border-white/10"></video>`;
305
  }
306
  }
307
 
308
+ const bubbleClass = role === 'user' ? 'user-message' : 'bot-message';
309
+ const icon = role === 'user' ? 'user' : 'bot';
310
+ const iconBg = role === 'user' ? 'bg-slate-700' : 'bg-indigo-600';
311
+
312
  div.innerHTML = `
313
+ <div class="w-9 h-9 rounded-xl ${iconBg} flex items-center justify-center shrink-0 shadow-lg">
314
+ <i data-lucide="${icon}" class="w-5 h-5 text-white"></i>
315
  </div>
316
+ <div class="${bubbleClass} p-5 rounded-2xl ${role === 'user' ? 'rounded-tr-none' : 'rounded-tl-none'} message-bubble shadow-xl">
317
  ${mediaHtml}
318
+ <p class="text-white leading-relaxed text-[15px] whitespace-pre-wrap">${text}</p>
319
  </div>
320
  `;
321
  chatMessages.appendChild(div);
322
  lucide.createIcons();
323
+ chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
324
  }
325
 
326
+ async function sendMessage() {
327
  const text = userInput.value.trim();
328
  if (!text && !selectedFile) return;
329
 
330
+ const content = text;
331
+ const file = selectedFile;
332
+ const mode = downsampleMode.value;
333
 
334
+ // Clear UI
335
  userInput.value = '';
336
  userInput.style.height = 'auto';
337
+ const fileUrl = file ? URL.createObjectURL(file) : null;
338
+ const fileType = file ? file.type : null;
339
 
340
+ appendMessage('user', content, fileUrl, fileType);
341
+ cancelFile.click();
342
+
343
+ // Loading state
344
  sendIcon.classList.add('hidden');
345
+ loadingIcon.classList.remove('hidden');
346
  sendBtn.disabled = true;
347
 
348
+ // Placeholder
349
+ const thinkingId = 'think-' + Date.now();
350
  const thinkingDiv = document.createElement('div');
351
+ thinkingDiv.id = thinkingId;
352
+ thinkingDiv.className = 'flex gap-4 items-start animate-in';
353
  thinkingDiv.innerHTML = `
354
+ <div class="w-9 h-9 rounded-xl bg-indigo-600 flex items-center justify-center shrink-0">
355
+ <i data-lucide="bot" class="w-5 h-5 text-white"></i>
356
  </div>
357
+ <div class="bot-message p-5 rounded-2xl rounded-tl-none message-bubble shadow-xl flex items-center gap-3">
358
+ <div class="flex gap-1">
359
+ <div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>
360
  </div>
361
+ <span class="text-sm text-slate-400 italic">Processing...</span>
362
  </div>
363
  `;
364
  chatMessages.appendChild(thinkingDiv);
365
+ chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
366
  lucide.createIcons();
 
367
 
368
  try {
369
+ let fileData = file ? handle_file(file) : null;
 
 
 
 
 
370
  const result = await client.predict("/predict", {
371
+ message: content,
372
  file: fileData,
373
+ downsample_mode: mode
374
  });
375
+
376
+ document.getElementById(thinkingId).remove();
377
+ appendMessage('bot', result.data);
378
+ } catch (err) {
379
+ document.getElementById(thinkingId).remove();
380
+ appendMessage('bot', "⚠️ Sorry, something went wrong. Please check your connection or try a different file.");
 
 
381
  } finally {
382
  sendIcon.classList.remove('hidden');
383
+ loadingIcon.classList.add('hidden');
384
  sendBtn.disabled = false;
 
385
  }
386
  }
387
 
388
+ sendBtn.onclick = sendMessage;
 
389
  userInput.onkeydown = (e) => {
390
+ if (e.key === 'Enter' && !e.shiftKey && window.innerWidth > 768) {
391
  e.preventDefault();
392
+ sendMessage();
393
  }
394
  };
395
  </script>