Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Clarity AI | Medical Analysis</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://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| * { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| body { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .glass-card { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| transition: all 0.2s ease; | |
| } | |
| .glass-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| transition: all 0.2s ease; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); | |
| } | |
| .user-message { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .bot-message { | |
| background: rgba(248, 250, 252, 0.95); | |
| color: #334155; | |
| border: 1px solid rgba(226, 232, 240, 0.5); | |
| } | |
| .upload-zone { | |
| transition: all 0.2s ease; | |
| border: 2px dashed rgba(255, 255, 255, 0.4); | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| .upload-zone:hover { | |
| border-color: rgba(255, 255, 255, 0.7); | |
| background: rgba(255, 255, 255, 0.15); | |
| transform: scale(1.01); | |
| } | |
| .upload-zone.dragover { | |
| border-color: #ffd700; | |
| background: rgba(255, 215, 0, 0.1); | |
| } | |
| .chat-container { | |
| height: calc(100vh - 120px); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| min-height: 0; | |
| } | |
| .main-container { | |
| height: calc(100vh - 80px); | |
| overflow-y: auto; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(102, 126, 234, 0.6); | |
| border-radius: 10px; | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .glassmorphism-header { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(20px); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .progress-bar { | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| transition: width 1s ease; | |
| } | |
| .typing-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| margin: 0 1px; | |
| animation: typing 1s infinite ease-in-out; | |
| } | |
| .typing-dot:nth-child(1) { animation-delay: 0s; } | |
| .typing-dot:nth-child(2) { animation-delay: 0.2s; } | |
| .typing-dot:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes typing { | |
| 0%, 60%, 100% { transform: scale(0.8); opacity: 0.5; } | |
| 30% { transform: scale(1); opacity: 1; } | |
| } | |
| </style> | |
| </head> | |
| <body class="antialiased text-gray-800"> | |
| <!-- Header --> | |
| <header class="glassmorphism-header h-20 flex-shrink-0 z-50"> | |
| <nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full flex justify-between items-center"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="bg-gradient-to-r from-white to-gray-100 p-2 rounded-xl shadow-lg"> | |
| <svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path> | |
| </svg> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-bold text-white">Clarity AI</h1> | |
| <p class="text-xs text-white/80">Medical Analysis</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <!-- Public app: no auth controls --> | |
| </div> | |
| </nav> | |
| </header> | |
| <!-- Main Container --> | |
| <main class="main-container"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
| <!-- Upload Section --> | |
| <section class="glass-card p-6 mb-6 fade-in"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800">Medical Image Analysis</h2> | |
| <div class="hidden sm:block text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">Step 1</div> | |
| </div> | |
| <form id="upload-form" class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <!-- Model Selection --> | |
| <div> | |
| <label for="model-select" class="block text-lg font-semibold text-gray-700 mb-3">Choose Model</label> | |
| <select id="model-select" name="model" class="w-full p-3 text-lg border-2 border-gray-200 focus:ring-2 focus:ring-purple-500 focus:border-purple-500 rounded-xl transition-all"> | |
| <option value="Brain Tumor">🧠 Brain Tumor</option> | |
| <option value="Kvasir">🔬 Gastrointestinal</option> | |
| <option value="Pneumonia">🫁 Pneumonia</option> | |
| <option value="Skin Cancer">🩺 Skin Cancer</option> | |
| <option value="Tuberculosis">🦠 Tuberculosis</option> | |
| </select> | |
| </div> | |
| <!-- Image Upload --> | |
| <div> | |
| <label class="block text-lg font-semibold text-gray-700 mb-3">Upload Image</label> | |
| <div id="upload-container"> | |
| <!-- Image Preview --> | |
| <div id="image-preview-container" class="hidden mb-4"> | |
| <div class="relative rounded-xl overflow-hidden shadow-lg"> | |
| <img id="image-preview" src="#" alt="Preview" class="w-full h-40 object-cover"> | |
| <button type="button" id="remove-image" class="absolute top-2 right-2 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 transition-colors">×</button> | |
| </div> | |
| </div> | |
| <!-- Upload Zone --> | |
| <div id="upload-zone" class="upload-zone rounded-xl p-8 text-center cursor-pointer"> | |
| <svg class="mx-auto h-12 w-12 text-white/80 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path> | |
| </svg> | |
| <p class="text-lg font-medium text-white mb-2">Drop image here or click</p> | |
| <p class="text-white/70 text-sm">JPG, PNG, GIF up to 10MB</p> | |
| </div> | |
| <input id="file-upload-hidden" name="file" type="file" accept="image/*" class="hidden"/> | |
| </div> | |
| <!-- Examples Gallery --> | |
| <div class="mt-6"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <h3 class="text-lg font-semibold text-gray-700">Or pick an example</h3> | |
| <button type="button" id="refresh-examples" class="text-sm text-indigo-700 hover:underline">Refresh</button> | |
| </div> | |
| <div id="examples-container" class="grid grid-cols-2 sm:grid-cols-3 gap-3"></div> | |
| <p id="examples-empty" class="text-sm text-gray-500 hidden">No examples found.</p> | |
| </div> | |
| </div> | |
| <!-- Submit Button --> | |
| <div class="lg:col-span-2"> | |
| <button type="submit" class="w-full btn-primary font-bold py-4 px-6 rounded-xl text-lg flex items-center justify-center"> | |
| <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path> | |
| </svg> | |
| Analyze Image | |
| </button> | |
| </div> | |
| </form> | |
| </section> | |
| <!-- Results Section --> | |
| <section id="results-section" class="hidden"> | |
| <div class="grid grid-cols-1 xl:grid-cols-3 gap-6"> | |
| <!-- Results Panel --> | |
| <div class="xl:col-span-2 glass-card p-6 fade-in"> | |
| <h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> | |
| <svg class="w-5 h-5 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 11-6 0 3 3 0 016 0z"></path> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path> | |
| </svg> | |
| Analysis Results | |
| </h2> | |
| <div id="results-content" class="space-y-6"> | |
| <div id="prediction-output"></div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <h3 class="font-semibold mb-3 text-gray-700 text-center">Original Image</h3> | |
| <img id="original-image" src="" class="w-full h-auto rounded-lg shadow-md"> | |
| </div> | |
| <div> | |
| <h3 class="font-semibold mb-3 text-gray-700 text-center">AI Focus Map</h3> | |
| <div id="gradcam-container"> | |
| <img id="gradcam-image" src="" class="w-full h-auto rounded-lg shadow-md hidden"> | |
| <div id="gradcam-placeholder" class="flex items-center justify-center h-32 bg-gray-100 rounded-lg text-gray-500"> | |
| <div class="text-center"> | |
| <svg class="w-8 h-8 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |
| </svg> | |
| <p class="text-sm">Not Available</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Panel --> | |
| <div id="chat-card" class="glass-card opacity-50 pointer-events-none transition-all duration-300 fade-in"> | |
| <div class="chat-container p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-xl font-bold text-gray-800 flex items-center"> | |
| <svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path> | |
| </svg> | |
| AI Assistant | |
| </h2> | |
| <div id="chat-status" class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full text-xs">Waiting</div> | |
| </div> | |
| <div class="chat-messages mb-4 p-3 bg-gray-50 rounded-xl border space-y-3"> | |
| <div id="chat-messages"> | |
| <div class="bot-message p-3 rounded-xl max-w-[90%]"> | |
| <div class="flex items-center mb-2"> | |
| <div class="w-2 h-2 bg-yellow-400 rounded-full mr-2"></div> | |
| <span class="text-xs text-gray-500">SYSTEM</span> | |
| </div> | |
| Run an analysis to activate the AI assistant. | |
| </div> | |
| </div> | |
| </div> | |
| <div id="predefined-prompts-container" class="flex flex-wrap gap-2 mb-4 empty:hidden"></div> | |
| <form id="chat-form" class="flex bg-white rounded-xl shadow-sm overflow-hidden"> | |
| <input type="text" id="chat-input" class="flex-1 p-3 focus:outline-none" placeholder="Ask about results..." disabled> | |
| <button type="submit" id="chat-submit" class="btn-primary px-4 flex items-center rounded-none" disabled> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path> | |
| </svg> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </main> | |
| <script> | |
| // Element references | |
| const uploadForm = document.getElementById('upload-form'); | |
| const fileUploadHidden = document.getElementById('file-upload-hidden'); | |
| const uploadZone = document.getElementById('upload-zone'); | |
| const imagePreviewContainer = document.getElementById('image-preview-container'); | |
| const imagePreview = document.getElementById('image-preview'); | |
| const removeImageBtn = document.getElementById('remove-image'); | |
| const resultsSection = document.getElementById('results-section'); | |
| const predictionOutput = document.getElementById('prediction-output'); | |
| const originalImage = document.getElementById('original-image'); | |
| const gradcamImage = document.getElementById('gradcam-image'); | |
| const gradcamPlaceholder = document.getElementById('gradcam-placeholder'); | |
| const chatCard = document.getElementById('chat-card'); | |
| const chatStatus = document.getElementById('chat-status'); | |
| const chatMessages = document.getElementById('chat-messages'); | |
| const chatInput = document.getElementById('chat-input'); | |
| const chatSubmit = document.getElementById('chat-submit'); | |
| const chatForm = document.getElementById('chat-form'); | |
| const promptsContainer = document.getElementById('predefined-prompts-container'); | |
| const modelSelect = document.getElementById('model-select'); | |
| const examplesContainer = document.getElementById('examples-container'); | |
| const examplesEmpty = document.getElementById('examples-empty'); | |
| const refreshExamplesBtn = document.getElementById('refresh-examples'); | |
| let predictionContext = null; | |
| let chatHistory = []; | |
| let cachedExamples = []; | |
| // File upload handling | |
| uploadZone.addEventListener('click', () => fileUploadHidden.click()); | |
| uploadZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.add('dragover'); | |
| }); | |
| uploadZone.addEventListener('dragleave', () => { | |
| uploadZone.classList.remove('dragover'); | |
| }); | |
| uploadZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| fileUploadHidden.files = files; | |
| handleFileSelect(files[0]); | |
| } | |
| }); | |
| fileUploadHidden.addEventListener('change', function() { | |
| if (this.files[0]) { | |
| handleFileSelect(this.files[0]); | |
| } | |
| }); | |
| removeImageBtn.addEventListener('click', () => { | |
| imagePreviewContainer.classList.add('hidden'); | |
| uploadZone.style.display = 'block'; | |
| fileUploadHidden.value = ''; | |
| }); | |
| function handleFileSelect(file) { | |
| if (!file.type.startsWith('image/')) { | |
| alert('Please select an image file.'); | |
| return; | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| alert('File size must be less than 10MB.'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| imagePreview.src = e.target.result; | |
| imagePreviewContainer.classList.remove('hidden'); | |
| uploadZone.style.display = 'none'; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| uploadForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| if (!fileUploadHidden.files[0]) { | |
| alert("Please select an image file first."); | |
| return; | |
| } | |
| const submitButton = uploadForm.querySelector('button[type="submit"]'); | |
| const originalText = submitButton.innerHTML; | |
| submitButton.disabled = true; | |
| submitButton.innerHTML = ` | |
| <div class="flex items-center"> | |
| <div class="flex space-x-1 mr-2"> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| </div> | |
| Analyzing... | |
| </div> | |
| `; | |
| // Reset states | |
| resultsSection.classList.add('hidden'); | |
| chatCard.classList.add('opacity-50', 'pointer-events-none'); | |
| chatStatus.textContent = 'Analyzing'; | |
| chatStatus.className = 'bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs'; | |
| chatInput.disabled = true; | |
| chatSubmit.disabled = true; | |
| chatMessages.innerHTML = ` | |
| <div class="bot-message p-3 rounded-xl max-w-[90%]"> | |
| <div class="flex items-center mb-2"> | |
| <div class="flex space-x-1 mr-2"> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| </div> | |
| <span class="text-xs text-gray-500">ANALYZING</span> | |
| </div> | |
| Processing your medical image... | |
| </div> | |
| `; | |
| predictionContext = null; | |
| chatHistory = []; | |
| promptsContainer.innerHTML = ''; | |
| try { | |
| const formData = new FormData(uploadForm); | |
| const response = await fetch('/predict', { method: 'POST', body: formData }); | |
| const data = await response.json(); | |
| if (data.error) { | |
| alert(`Error: ${data.error}`); | |
| return; | |
| } | |
| predictionContext = data; | |
| resultsSection.classList.remove('hidden'); | |
| predictionOutput.innerHTML = ` | |
| <div class="bg-gradient-to-r from-indigo-50 to-purple-50 p-6 rounded-xl border border-indigo-200"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <span class="text-sm font-semibold text-indigo-600 bg-indigo-100 px-3 py-1 rounded-full">${data.model_used}</span> | |
| <span class="text-2xl font-bold text-gray-900">${(data.confidence * 100).toFixed(1)}%</span> | |
| </div> | |
| <h3 class="text-2xl font-bold text-gray-900 mb-3">${data.prediction}</h3> | |
| <div class="bg-gray-200 rounded-full h-3 overflow-hidden"> | |
| <div class="progress-bar h-3 rounded-full" style="width: ${(data.confidence * 100).toFixed(0)}%"></div> | |
| </div> | |
| </div> | |
| `; | |
| originalImage.src = `${data.original_image}?t=${new Date().getTime()}`; | |
| if (data.gradcam_image) { | |
| gradcamImage.src = `${data.gradcam_image}?t=${new Date().getTime()}`; | |
| gradcamImage.classList.remove('hidden'); | |
| gradcamPlaceholder.classList.add('hidden'); | |
| } else { | |
| gradcamImage.classList.add('hidden'); | |
| gradcamPlaceholder.classList.remove('hidden'); | |
| } | |
| // Activate chat | |
| chatCard.classList.remove('opacity-50', 'pointer-events-none'); | |
| chatStatus.textContent = 'Active'; | |
| chatStatus.className = 'bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs'; | |
| chatMessages.innerHTML = ''; | |
| addMessage("Analysis complete! Ask me anything about the results.", 'bot'); | |
| chatInput.disabled = false; | |
| chatSubmit.disabled = false; | |
| if (data.predefined_prompts) { | |
| data.predefined_prompts.forEach(prompt => { | |
| const btn = document.createElement('button'); | |
| btn.textContent = prompt; | |
| btn.className = 'bg-indigo-100 text-indigo-700 text-sm px-3 py-1 rounded-full hover:bg-indigo-200 transition-all'; | |
| btn.onclick = () => { | |
| chatInput.value = prompt; | |
| chatForm.requestSubmit(); | |
| }; | |
| promptsContainer.appendChild(btn); | |
| }); | |
| } | |
| } catch (error) { | |
| alert('An unexpected error occurred.'); | |
| console.error("Prediction Error:", error); | |
| } finally { | |
| submitButton.disabled = false; | |
| submitButton.innerHTML = originalText; | |
| } | |
| }); | |
| chatForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const message = chatInput.value.trim(); | |
| if (!message || !predictionContext) return; | |
| addMessage(message, 'user'); | |
| chatHistory.push({ role: 'user', parts: [message] }); | |
| chatInput.value = ''; | |
| promptsContainer.innerHTML = ''; | |
| const thinkingMsg = addMessage('', 'bot'); | |
| thinkingMsg.innerHTML = ` | |
| <div class="flex items-center"> | |
| <div class="flex space-x-1 mr-2"> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| </div> | |
| <span class="text-sm text-gray-500">Thinking...</span> | |
| </div> | |
| `; | |
| try { | |
| const response = await fetch('/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message, context: predictionContext, history: chatHistory }) | |
| }); | |
| const data = await response.json(); | |
| thinkingMsg.remove(); | |
| if (data.error) { | |
| addMessage(`Error: ${data.error}`, 'bot'); | |
| chatHistory.pop(); | |
| } else { | |
| addMessage(data.response, 'bot'); | |
| chatHistory.push({ role: 'model', parts: [data.response] }); | |
| } | |
| } catch (error) { | |
| thinkingMsg.remove(); | |
| addMessage('Connection error. Please try again.', 'bot'); | |
| chatHistory.pop(); | |
| console.error("Chat Error:", error); | |
| } | |
| }); | |
| function addMessage(text, type) { | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = `p-3 rounded-xl max-w-[90%] ${type}-message mb-3`; | |
| if (type === 'user') { | |
| msgDiv.classList.add('ml-auto', 'text-right'); | |
| msgDiv.textContent = text; | |
| } else { | |
| msgDiv.classList.add('mr-auto'); | |
| if (text) { | |
| msgDiv.innerHTML = marked.parse(text); | |
| } | |
| } | |
| chatMessages.appendChild(msgDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| return msgDiv; | |
| } | |
| // Load example images when model changes | |
| async function loadExamples() { | |
| try { | |
| const res = await fetch('/example_images'); | |
| const data = await res.json(); | |
| cachedExamples = Array.isArray(data.images) ? data.images : []; | |
| renderExamples(); | |
| } catch (e) { | |
| console.error('Failed to load examples', e); | |
| cachedExamples = []; | |
| renderExamples(); | |
| } | |
| } | |
| function renderExamples() { | |
| examplesContainer.innerHTML = ''; | |
| if (!cachedExamples.length) { | |
| examplesEmpty.classList.remove('hidden'); | |
| return; | |
| } | |
| examplesEmpty.classList.add('hidden'); | |
| // Show up to 9 examples | |
| cachedExamples.slice(0, 9).forEach((url) => { | |
| const card = document.createElement('button'); | |
| card.type = 'button'; | |
| card.className = 'relative group rounded-xl overflow-hidden shadow hover:shadow-lg transition focus:outline-none focus:ring-2 focus:ring-indigo-500 text-left'; | |
| card.title = 'Use this example'; | |
| card.onclick = () => selectExample(url); | |
| const name = decodeURIComponent(url.split('/').pop() || 'example'); | |
| card.innerHTML = ` | |
| <img src="${url}" alt="${name}" class="w-full h-24 object-cover"> | |
| <div class="px-2 py-1 text-xs bg-white text-gray-700 truncate">${name}</div> | |
| `; | |
| examplesContainer.appendChild(card); | |
| }); | |
| } | |
| async function selectExample(url) { | |
| try { | |
| const resp = await fetch(url); | |
| const blob = await resp.blob(); | |
| const inferredName = url.split('/').pop() || 'example.png'; | |
| const file = new File([blob], inferredName, { type: blob.type || 'image/png' }); | |
| // Put into the hidden file input | |
| const dt = new DataTransfer(); | |
| dt.items.add(file); | |
| fileUploadHidden.files = dt.files; | |
| // Reuse existing preview logic | |
| handleFileSelect(file); | |
| } catch (e) { | |
| console.error('Failed to select example', e); | |
| alert('Unable to load example image.'); | |
| } | |
| } | |
| modelSelect.addEventListener('change', loadExamples); | |
| refreshExamplesBtn.addEventListener('click', loadExamples); | |
| // Initial load for default selection | |
| loadExamples(); | |
| // Smooth scrolling | |
| document.documentElement.style.scrollBehavior = 'smooth'; | |
| </script> | |
| </body> | |
| </html> |