Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Qwen2-VL Chat</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; | |
| background: #f8fafc; | |
| height: 100vh; | |
| overflow: hidden; | |
| color: #1e293b; | |
| } | |
| .app-container { | |
| display: flex; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| /* Left Sidebar - Chat List */ | |
| .chat-sidebar { | |
| width: 280px; | |
| background: #ffffff; | |
| border-right: 1px solid #e2e8f0; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| } | |
| .sidebar-header { | |
| padding: 20px; | |
| border-bottom: 1px solid #e2e8f0; | |
| background: #f8fafc; | |
| } | |
| .user-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 16px; | |
| } | |
| .user-avatar { | |
| width: 40px; | |
| height: 40px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: 600; | |
| font-size: 16px; | |
| } | |
| .user-details h3 { | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: #1e293b; | |
| } | |
| .user-details p { | |
| font-size: 12px; | |
| color: #64748b; | |
| margin-top: 2px; | |
| } | |
| .new-chat-btn { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .new-chat-btn:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); | |
| } | |
| .chat-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 12px; | |
| } | |
| .chat-item { | |
| padding: 12px 16px; | |
| margin-bottom: 4px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| position: relative; | |
| group: 1; | |
| } | |
| .chat-item:hover { | |
| background: #f1f5f9; | |
| } | |
| .chat-item.active { | |
| background: #e0e7ff; | |
| border: 1px solid #c7d2fe; | |
| } | |
| .chat-item-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 4px; | |
| } | |
| .chat-title { | |
| font-size: 14px; | |
| font-weight: 500; | |
| color: #1e293b; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| max-width: 180px; | |
| } | |
| .chat-menu { | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| position: relative; | |
| } | |
| .chat-item:hover .chat-menu { | |
| opacity: 1; | |
| } | |
| .chat-menu-btn { | |
| width: 24px; | |
| height: 24px; | |
| border: none; | |
| background: none; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #64748b; | |
| } | |
| .chat-menu-btn:hover { | |
| background: #e2e8f0; | |
| } | |
| .chat-meta { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 12px; | |
| color: #64748b; | |
| } | |
| /* Main Chat Area */ | |
| .chat-main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: #ffffff; | |
| } | |
| .chat-header { | |
| padding: 20px 24px; | |
| border-bottom: 1px solid #e2e8f0; | |
| background: #ffffff; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .chat-title-section h1 { | |
| font-size: 20px; | |
| font-weight: 600; | |
| color: #1e293b; | |
| margin-bottom: 4px; | |
| } | |
| .chat-subtitle { | |
| font-size: 14px; | |
| color: #64748b; | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 12px; | |
| } | |
| .header-btn { | |
| width: 40px; | |
| height: 40px; | |
| border: none; | |
| background: #f8fafc; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| color: #64748b; | |
| } | |
| .header-btn:hover { | |
| background: #e2e8f0; | |
| color: #1e293b; | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| padding: 24px; | |
| overflow-y: auto; | |
| scroll-behavior: smooth; | |
| } | |
| .welcome-message { | |
| text-align: center; | |
| padding: 80px 20px; | |
| color: #64748b; | |
| } | |
| .welcome-icon { | |
| font-size: 64px; | |
| margin-bottom: 24px; | |
| } | |
| .welcome-message h2 { | |
| font-size: 28px; | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| color: #1e293b; | |
| } | |
| .welcome-message p { | |
| font-size: 16px; | |
| line-height: 1.6; | |
| } | |
| .message { | |
| margin-bottom: 24px; | |
| display: flex; | |
| gap: 16px; | |
| max-width: 800px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .message.user { | |
| flex-direction: row-reverse; | |
| } | |
| .message-avatar { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| flex-shrink: 0; | |
| } | |
| .message.user .message-avatar { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .message.assistant .message-avatar { | |
| background: #f1f5f9; | |
| color: #64748b; | |
| } | |
| .message-content { | |
| flex: 1; | |
| background: #f8fafc; | |
| padding: 16px 20px; | |
| border-radius: 16px; | |
| font-size: 15px; | |
| line-height: 1.6; | |
| word-wrap: break-word; | |
| white-space: pre-wrap; | |
| } | |
| .message.user .message-content { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .message.assistant .message-content { | |
| background: #f8fafc; | |
| color: #1e293b; | |
| border: 1px solid #e2e8f0; | |
| } | |
| .message-image { | |
| max-width: 300px; | |
| border-radius: 12px; | |
| margin-bottom: 12px; | |
| display: block; | |
| } | |
| .message-time { | |
| font-size: 12px; | |
| color: rgba(255, 255, 255, 0.7); | |
| margin-top: 8px; | |
| } | |
| .message.assistant .message-time { | |
| color: #94a3b8; | |
| } | |
| /* Input Area */ | |
| .chat-input-area { | |
| padding: 24px; | |
| background: #ffffff; | |
| border-top: 1px solid #e2e8f0; | |
| } | |
| .input-container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 12px; | |
| background: #f8fafc; | |
| border: 2px solid #e2e8f0; | |
| border-radius: 16px; | |
| padding: 16px; | |
| transition: all 0.2s ease; | |
| } | |
| .input-container:focus-within { | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| .image-upload-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .image-upload-btn { | |
| width: 44px; | |
| height: 44px; | |
| border: none; | |
| background: #ffffff; | |
| border: 2px solid #e2e8f0; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| color: #64748b; | |
| } | |
| .image-upload-btn:hover { | |
| border-color: #667eea; | |
| color: #667eea; | |
| } | |
| .image-upload-btn.has-image { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-color: #667eea; | |
| color: white; | |
| } | |
| .image-preview { | |
| display: none; | |
| } | |
| .image-preview.has-image { | |
| display: block; | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| border: 2px solid #e2e8f0; | |
| } | |
| .image-preview img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .text-input-section { | |
| flex: 1; | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 12px; | |
| } | |
| #messageInput { | |
| flex: 1; | |
| border: none; | |
| outline: none; | |
| resize: none; | |
| font-size: 15px; | |
| line-height: 1.6; | |
| padding: 8px 0; | |
| background: transparent; | |
| font-family: inherit; | |
| max-height: 120px; | |
| min-height: 24px; | |
| color: #1e293b; | |
| } | |
| #messageInput::placeholder { | |
| color: #94a3b8; | |
| } | |
| .send-btn { | |
| width: 44px; | |
| height: 44px; | |
| border: none; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .send-btn:hover:not(:disabled) { | |
| transform: translateY(-1px); | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3); | |
| } | |
| .send-btn:disabled { | |
| background: #e2e8f0; | |
| color: #94a3b8; | |
| cursor: not-allowed; | |
| } | |
| /* Right Sidebar - Settings */ | |
| .settings-sidebar { | |
| width: 320px; | |
| background: #ffffff; | |
| border-left: 1px solid #e2e8f0; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .settings-header { | |
| padding: 20px 24px; | |
| border-bottom: 1px solid #e2e8f0; | |
| background: #f8fafc; | |
| } | |
| .settings-header h2 { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: #1e293b; | |
| margin-bottom: 4px; | |
| } | |
| .settings-subtitle { | |
| font-size: 14px; | |
| color: #64748b; | |
| } | |
| .settings-content { | |
| padding: 24px; | |
| overflow-y: auto; | |
| } | |
| .setting-group { | |
| margin-bottom: 24px; | |
| } | |
| .setting-group:last-child { | |
| margin-bottom: 0; | |
| } | |
| .setting-label { | |
| display: block; | |
| font-size: 14px; | |
| font-weight: 500; | |
| margin-bottom: 12px; | |
| color: #374151; | |
| } | |
| .setting-description { | |
| font-size: 12px; | |
| color: #64748b; | |
| margin-bottom: 12px; | |
| line-height: 1.4; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .slider-container input[type="range"] { | |
| flex: 1; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: #e2e8f0; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| .slider-container input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| cursor: pointer; | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); | |
| } | |
| .slider-container input[type="range"]::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| cursor: pointer; | |
| border: none; | |
| } | |
| .slider-value { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #1e293b; | |
| min-width: 50px; | |
| text-align: center; | |
| background: #f1f5f9; | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| border: 1px solid #e2e8f0; | |
| } | |
| /* Loading Indicator */ | |
| .loading-indicator { | |
| display: none; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 16px 24px; | |
| background: #f8fafc; | |
| color: #64748b; | |
| font-size: 14px; | |
| border-top: 1px solid #e2e8f0; | |
| } | |
| .loading-indicator.active { | |
| display: flex; | |
| } | |
| .loading-dots { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #94a3b8; | |
| animation: bounce 1.4s ease-in-out infinite both; | |
| } | |
| .dot:nth-child(1) { animation-delay: -0.32s; } | |
| .dot:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes bounce { | |
| 0%, 80%, 100% { | |
| transform: scale(0.8); | |
| opacity: 0.5; | |
| } | |
| 40% { | |
| transform: scale(1); | |
| opacity: 1; | |
| } | |
| } | |
| /* Custom scrollbar */ | |
| .chat-messages::-webkit-scrollbar, | |
| .chat-list::-webkit-scrollbar, | |
| .settings-content::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .chat-messages::-webkit-scrollbar-track, | |
| .chat-list::-webkit-scrollbar-track, | |
| .settings-content::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .chat-messages::-webkit-scrollbar-thumb, | |
| .chat-list::-webkit-scrollbar-thumb, | |
| .settings-content::-webkit-scrollbar-thumb { | |
| background: rgba(0, 0, 0, 0.2); | |
| border-radius: 3px; | |
| } | |
| .chat-messages::-webkit-scrollbar-thumb:hover, | |
| .chat-list::-webkit-scrollbar-thumb:hover, | |
| .settings-content::-webkit-scrollbar-thumb:hover { | |
| background: rgba(0, 0, 0, 0.3); | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 1024px) { | |
| .settings-sidebar { | |
| display: none; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .chat-sidebar { | |
| width: 260px; | |
| } | |
| .app-container { | |
| overflow-x: auto; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Left Sidebar - Chat List --> | |
| <div class="chat-sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="user-info"> | |
| <div class="user-avatar">AT</div> | |
| <div class="user-details"> | |
| <h3>ayush-thakur02</h3> | |
| <p>2025-06-03 10:57 UTC</p> | |
| </div> | |
| </div> | |
| <button class="new-chat-btn" onclick="createNewChat()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| New Chat | |
| </button> | |
| </div> | |
| <div class="chat-list" id="chatList"> | |
| <!-- Chat items will be loaded here --> | |
| </div> | |
| </div> | |
| <!-- Main Chat Area --> | |
| <div class="chat-main"> | |
| <div class="chat-header"> | |
| <div class="chat-title-section"> | |
| <h1 id="currentChatTitle">Qwen2-VL Assistant</h1> | |
| <p class="chat-subtitle">Vision-Language AI Assistant</p> | |
| </div> | |
| <div class="header-actions"> | |
| <button class="header-btn" onclick="toggleSidebar('chat')" title="Toggle Chat List"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <line x1="3" y1="6" x2="21" y2="6"></line> | |
| <line x1="3" y1="12" x2="21" y2="12"></line> | |
| <line x1="3" y1="18" x2="21" y2="18"></line> | |
| </svg> | |
| </button> | |
| <button class="header-btn" onclick="toggleSidebar('settings')" title="Toggle Settings"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1 1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="chat-messages" id="chatMessages"> | |
| <div class="welcome-message"> | |
| <div class="welcome-icon">🤖</div> | |
| <h2>Welcome to Qwen2-VL</h2> | |
| <p>Upload an image and ask me anything about it!<br>I can analyze, describe, and answer questions about visual content.</p> | |
| </div> | |
| </div> | |
| <div class="loading-indicator" id="loadingIndicator"> | |
| <div class="loading-dots"> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| </div> | |
| <span>Generating response...</span> | |
| </div> | |
| <div class="chat-input-area"> | |
| <form id="chatForm" class="chat-form"> | |
| <div class="input-container"> | |
| <div class="image-upload-section"> | |
| <input type="file" id="imageInput" name="image" accept="image/*" hidden> | |
| <button type="button" class="image-upload-btn" onclick="document.getElementById('imageInput').click()"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> | |
| <circle cx="8.5" cy="8.5" r="1.5"></circle> | |
| <path d="M21 15l-5-5L5 21"></path> | |
| </svg> | |
| </button> | |
| <div class="image-preview" id="imagePreview"></div> | |
| </div> | |
| <div class="text-input-section"> | |
| <textarea | |
| id="messageInput" | |
| name="prompt" | |
| placeholder="Ask me anything about your image..." | |
| rows="1" | |
| required | |
| ></textarea> | |
| <button type="submit" class="send-btn" id="sendBtn" disabled> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <line x1="22" y1="2" x2="11" y2="13"></line> | |
| <polygon points="22,2 15,22 11,13 2,9"></polygon> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Right Sidebar - Settings --> | |
| <div class="settings-sidebar"> | |
| <div class="settings-header"> | |
| <h2>Generation Settings</h2> | |
| <p class="settings-subtitle">Fine-tune AI responses</p> | |
| </div> | |
| <div class="settings-content"> | |
| <div class="setting-group"> | |
| <label class="setting-label" for="temperature">Temperature</label> | |
| <p class="setting-description">Controls randomness. Lower values make responses more focused and deterministic.</p> | |
| <div class="slider-container"> | |
| <input type="range" id="temperature" min="0.1" max="1.5" step="0.1" value="0.7"> | |
| <span class="slider-value">0.7</span> | |
| </div> | |
| </div> | |
| <div class="setting-group"> | |
| <label class="setting-label" for="top_p">Top-p (Nucleus Sampling)</label> | |
| <p class="setting-description">Controls diversity. Lower values focus on more probable tokens.</p> | |
| <div class="slider-container"> | |
| <input type="range" id="top_p" min="0.1" max="1.0" step="0.05" value="0.9"> | |
| <span class="slider-value">0.9</span> | |
| </div> | |
| </div> | |
| <div class="setting-group"> | |
| <label class="setting-label" for="max_tokens">Max Tokens</label> | |
| <p class="setting-description">Maximum length of the response. Higher values allow longer responses.</p> | |
| <div class="slider-container"> | |
| <input type="range" id="max_tokens" min="32" max="2048" step="32" value="1024"> | |
| <span class="slider-value">1024</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Global variables | |
| let currentChatId = null; | |
| let chats = []; | |
| // DOM Elements | |
| const chatForm = document.getElementById('chatForm'); | |
| const messageInput = document.getElementById('messageInput'); | |
| const imageInput = document.getElementById('imageInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const loadingIndicator = document.getElementById('loadingIndicator'); | |
| const imageUploadBtn = document.querySelector('.image-upload-btn'); | |
| const imagePreview = document.getElementById('imagePreview'); | |
| const chatList = document.getElementById('chatList'); | |
| const currentChatTitle = document.getElementById('currentChatTitle'); | |
| // Settings elements | |
| const temperatureSlider = document.getElementById('temperature'); | |
| const topPSlider = document.getElementById('top_p'); | |
| const maxTokensSlider = document.getElementById('max_tokens'); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', function() { | |
| setupEventListeners(); | |
| setupSliders(); | |
| loadChats(); | |
| createNewChat(); | |
| }); | |
| function setupEventListeners() { | |
| chatForm.addEventListener('submit', handleSubmit); | |
| messageInput.addEventListener('input', validateInput); | |
| imageInput.addEventListener('change', handleImageSelect); | |
| messageInput.addEventListener('input', autoResizeTextarea); | |
| messageInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| if (!sendBtn.disabled) { | |
| chatForm.dispatchEvent(new Event('submit')); | |
| } | |
| } | |
| }); | |
| } | |
| function setupSliders() { | |
| const sliders = [temperatureSlider, topPSlider, maxTokensSlider]; | |
| sliders.forEach(slider => { | |
| const valueSpan = slider.parentElement.querySelector('.slider-value'); | |
| slider.addEventListener('input', function() { | |
| valueSpan.textContent = this.value; | |
| }); | |
| }); | |
| } | |
| function validateInput() { | |
| const hasText = messageInput.value.trim().length > 0; | |
| const hasImage = imageInput.files.length > 0; | |
| sendBtn.disabled = !(hasText && hasImage); | |
| } | |
| function handleImageSelect(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| imagePreview.innerHTML = `<img src="${e.target.result}" alt="Preview">`; | |
| imagePreview.classList.add('has-image'); | |
| imageUploadBtn.classList.add('has-image'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } else { | |
| imagePreview.innerHTML = ''; | |
| imagePreview.classList.remove('has-image'); | |
| imageUploadBtn.classList.remove('has-image'); | |
| } | |
| validateInput(); | |
| } | |
| function autoResizeTextarea() { | |
| messageInput.style.height = 'auto'; | |
| messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + 'px'; | |
| } | |
| async function handleSubmit(e) { | |
| e.preventDefault(); | |
| const formData = new FormData(); | |
| const messageText = messageInput.value.trim(); | |
| const imageFile = imageInput.files[0]; | |
| if (!messageText || !imageFile) return; | |
| // Ensure we have a current chat | |
| if (!currentChatId) { | |
| await createNewChat(); | |
| } | |
| formData.append('prompt', messageText); | |
| formData.append('image', imageFile); | |
| formData.append('temperature', temperatureSlider.value); | |
| formData.append('top_p', topPSlider.value); | |
| formData.append('max_tokens', maxTokensSlider.value); | |
| formData.append('chat_id', currentChatId); | |
| // Add user message to chat | |
| addMessage('user', messageText, URL.createObjectURL(imageFile)); | |
| // Clear form | |
| messageInput.value = ''; | |
| imageInput.value = ''; | |
| imagePreview.innerHTML = ''; | |
| imagePreview.classList.remove('has-image'); | |
| imageUploadBtn.classList.remove('has-image'); | |
| sendBtn.disabled = true; | |
| autoResizeTextarea(); | |
| showLoading(); | |
| try { | |
| const response = await fetch('/infer', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| hideLoading(); | |
| if (data.response) { | |
| addMessage('assistant', data.response); | |
| loadChats(); // Refresh chat list | |
| } else { | |
| addMessage('assistant', 'Sorry, I encountered an error processing your request.'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| hideLoading(); | |
| addMessage('assistant', 'Sorry, there was an error connecting to the server.'); | |
| } | |
| } | |
| function addMessage(sender, text, imageSrc = null) { | |
| const welcomeMessage = chatMessages.querySelector('.welcome-message'); | |
| if (welcomeMessage) { | |
| welcomeMessage.remove(); | |
| } | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${sender}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.textContent = sender === 'user' ? '👤' : '🤖'; | |
| const content = document.createElement('div'); | |
| content.className = 'message-content'; | |
| if (imageSrc && sender === 'user') { | |
| const img = document.createElement('img'); | |
| img.src = imageSrc; | |
| img.className = 'message-image'; | |
| content.appendChild(img); | |
| } | |
| const textDiv = document.createElement('div'); | |
| textDiv.textContent = text; | |
| content.appendChild(textDiv); | |
| const timeDiv = document.createElement('div'); | |
| timeDiv.className = 'message-time'; | |
| timeDiv.textContent = new Date().toLocaleTimeString(); | |
| content.appendChild(timeDiv); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(content); | |
| chatMessages.appendChild(messageDiv); | |
| scrollToBottom(); | |
| } | |
| function showLoading() { | |
| loadingIndicator.classList.add('active'); | |
| scrollToBottom(); | |
| } | |
| function hideLoading() { | |
| loadingIndicator.classList.remove('active'); | |
| } | |
| function scrollToBottom() { | |
| setTimeout(() => { | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| }, 100); | |
| } | |
| // Chat management functions | |
| async function createNewChat() { | |
| try { | |
| const response = await fetch('/api/chats', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| const newChat = await response.json(); | |
| currentChatId = newChat.id; | |
| // Clear current chat | |
| chatMessages.innerHTML = ` | |
| <div class="welcome-message"> | |
| <div class="welcome-icon">🤖</div> | |
| <h2>Welcome to Qwen2-VL</h2> | |
| <p>Upload an image and ask me anything about it!<br>I can analyze, describe, and answer questions about visual content.</p> | |
| </div> | |
| `; | |
| currentChatTitle.textContent = newChat.title; | |
| loadChats(); | |
| } catch (error) { | |
| console.error('Error creating new chat:', error); | |
| } | |
| } | |
| async function loadChats() { | |
| try { | |
| const response = await fetch('/api/chats'); | |
| chats = await response.json(); | |
| renderChatList(); | |
| } catch (error) { | |
| console.error('Error loading chats:', error); | |
| } | |
| } | |
| function renderChatList() { | |
| chatList.innerHTML = ''; | |
| chats.forEach(chat => { | |
| const chatItem = document.createElement('div'); | |
| chatItem.className = `chat-item ${chat.id === currentChatId ? 'active' : ''}`; | |
| chatItem.onclick = () => loadChat(chat.id); | |
| chatItem.innerHTML = ` | |
| <div class="chat-item-header"> | |
| <div class="chat-title">${chat.title}</div> | |
| <div class="chat-menu"> | |
| <button class="chat-menu-btn" onclick="event.stopPropagation(); deleteChat('${chat.id}')"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <polyline points="3,6 5,6 21,6"></polyline> | |
| <path d="m19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="chat-meta"> | |
| <span>${chat.message_count} messages</span> | |
| <span>${new Date(chat.updated_at).toLocaleDateString()}</span> | |
| </div> | |
| `; | |
| chatList.appendChild(chatItem); | |
| }); | |
| } | |
| async function loadChat(chatId) { | |
| try { | |
| const response = await fetch(`/api/chats/${chatId}`); | |
| const chat = await response.json(); | |
| currentChatId = chatId; | |
| currentChatTitle.textContent = chat.title; | |
| // Clear and load messages | |
| chatMessages.innerHTML = ''; | |
| if (chat.messages.length === 0) { | |
| chatMessages.innerHTML = ` | |
| <div class="welcome-message"> | |
| <div class="welcome-icon">🤖</div> | |
| <h2>Welcome to Qwen2-VL</h2> | |
| <p>Upload an image and ask me anything about it!<br>I can analyze, describe, and answer questions about visual content.</p> | |
| </div> | |
| `; | |
| } else { | |
| chat.messages.forEach(message => { | |
| if (message.role === 'user') { | |
| const imageUrl = `data:image/jpeg;base64,${message.image}`; | |
| addMessage('user', message.content, imageUrl); | |
| } else { | |
| addMessage('assistant', message.content); | |
| } | |
| }); | |
| } | |
| renderChatList(); | |
| scrollToBottom(); | |
| } catch (error) { | |
| console.error('Error loading chat:', error); | |
| } | |
| } | |
| async function deleteChat(chatId) { | |
| if (!confirm('Are you sure you want to delete this chat?')) return; | |
| try { | |
| await fetch(`/api/chats/${chatId}`, { | |
| method: 'DELETE' | |
| }); | |
| if (chatId === currentChatId) { | |
| await createNewChat(); | |
| } | |
| loadChats(); | |
| } catch (error) { | |
| console.error('Error deleting chat:', error); | |
| } | |
| } | |
| function toggleSidebar(type) { | |
| // This would handle mobile responsive behavior | |
| // For now, it's just a placeholder | |
| console.log(`Toggle ${type} sidebar`); | |
| } | |
| </script> | |
| </body> | |
| </html> |