Spaces:
Sleeping
Sleeping
| document.addEventListener('DOMContentLoaded', function () { | |
| // --- Globals & Element Selectors --- | |
| const sidebar = document.getElementById('sidebar'); | |
| const content = document.getElementById('content'); | |
| const sidebarCollapse = document.getElementById('sidebarCollapse'); | |
| const navLinks = document.querySelectorAll('.sidebar-nav .list-unstyled a'); | |
| const tabs = document.querySelectorAll('.content-tab'); | |
| // Diagnose Tab Elements | |
| const uploadArea = document.getElementById('upload-area'); | |
| const imageUploadInput = document.getElementById('image-upload'); | |
| const loadingSpinner = document.getElementById('loading-spinner'); | |
| const diagnosisResult = document.getElementById('diagnosis-result'); | |
| const imagePreview = document.getElementById('image-preview'); | |
| const diseaseName = document.getElementById('disease-name'); | |
| const healthStatus = document.getElementById('health-status'); | |
| const sourceModelBadge = document.getElementById('source-model-badge'); | |
| const detailedSymptoms = document.getElementById('detailed-symptoms'); | |
| const preventionMethods = document.getElementById('prevention-methods'); | |
| const smartAnalysis = document.getElementById('smart-analysis'); | |
| const diagnoseNewImageBtn = document.getElementById('diagnose-new-image-btn'); | |
| // Analysis Tab Elements | |
| const placeholderMsg = document.getElementById('placeholder-msg'); | |
| const analysisLoader = document.getElementById('analysis-loader'); | |
| const analysisDashboard = document.getElementById('analysis-dashboard'); | |
| // Chat Tab Elements | |
| const chatForm = document.getElementById('chat-form'); | |
| const chatInput = document.getElementById('chat-input'); | |
| const chatBox = document.getElementById('chat-box'); | |
| // Showcase Tab Elements | |
| const showcaseGrid = document.getElementById('showcase-grid'); | |
| const showcaseOverlay = document.getElementById('showcase-overlay'); | |
| const showcaseCard = document.getElementById('showcase-card'); | |
| const showcaseContent = document.getElementById('showcase-content'); | |
| const showcaseCloseBtn = document.getElementById('showcase-close-btn'); | |
| // --- Sidebar Functionality --- | |
| sidebarCollapse.addEventListener('click', () => { | |
| sidebar.classList.toggle('collapsed'); | |
| content.classList.toggle('full-width'); | |
| }); | |
| // --- Tab Navigation --- | |
| navLinks.forEach(link => { | |
| link.parentElement.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const tabId = link.parentElement.getAttribute('data-tab'); | |
| // Update active link | |
| document.querySelector('.sidebar-nav .list-unstyled .active').classList.remove('active'); | |
| link.parentElement.classList.add('active'); | |
| // Update active tab | |
| tabs.forEach(tab => { | |
| tab.classList.remove('active-tab'); | |
| }); | |
| const targetTab = document.getElementById(`${tabId}-tab`); | |
| targetTab.classList.add('active-tab'); | |
| // Smart Tab Listener: Analysis tab logic | |
| if (tabId === 'analysis') { | |
| // Check if imagePreview.src contains a valid image (not #) | |
| if (imagePreview.src && imagePreview.src !== '#') { | |
| // Only fetch if dashboard is not currently displayed or its src is empty | |
| if (analysisDashboard.style.display === 'none' || !analysisDashboard.src || analysisDashboard.src === window.location.href + '#') { | |
| placeholderMsg.style.display = 'none'; | |
| analysisLoader.style.display = 'block'; | |
| analysisDashboard.style.display = 'none'; | |
| fetch('/visualize') | |
| .then(response => { | |
| if (!response.ok) { | |
| return response.json().then(err => { | |
| throw new Error(err.error || `HTTP error! Status: ${response.status}`); | |
| }); | |
| } | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| if (data.graph) { | |
| // Inject the Base64 string into #analysis-dashboard | |
| analysisDashboard.src = data.graph; | |
| analysisDashboard.style.display = 'block'; | |
| placeholderMsg.style.display = 'none'; | |
| } else { | |
| throw new Error("No graph data received from server."); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Analysis Fetch Error:', error); | |
| placeholderMsg.innerHTML = `<p class="text-light">Error: ${error.message}</p>`; | |
| placeholderMsg.style.display = 'block'; | |
| analysisDashboard.style.display = 'none'; | |
| }) | |
| .finally(() => { | |
| // Hide loader | |
| analysisLoader.style.display = 'none'; | |
| }); | |
| } | |
| // If already visible and has content, do nothing, just make sure other elements are hidden | |
| else { | |
| placeholderMsg.style.display = 'none'; | |
| analysisLoader.style.display = 'none'; | |
| analysisDashboard.style.display = 'block'; // Ensure it's visible if already loaded | |
| } | |
| } else { | |
| // No valid image - show placeholder | |
| placeholderMsg.style.display = 'block'; | |
| analysisLoader.style.display = 'none'; | |
| analysisDashboard.style.display = 'none'; | |
| } | |
| } | |
| }); | |
| }); | |
| // --- Diagnose Tab: File Upload --- | |
| uploadArea.addEventListener('click', () => imageUploadInput.click()); | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('drag-over'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('drag-over'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('drag-over'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| imageUploadInput.files = files; | |
| handleImageUpload(files[0]); | |
| } | |
| }); | |
| imageUploadInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| handleImageUpload(e.target.files[0]); | |
| } | |
| }); | |
| // Event listener for the new "Diagnose New Image" button | |
| diagnoseNewImageBtn.addEventListener('click', () => { | |
| diagnosisResult.style.display = 'none'; // Hide results | |
| imagePreview.src = '#'; // Clear image preview | |
| // Clear previous data | |
| diseaseName.textContent = ''; | |
| healthStatus.textContent = ''; | |
| detailedSymptoms.innerHTML = ''; | |
| preventionMethods.innerHTML = ''; | |
| smartAnalysis.textContent = ''; | |
| sourceModelBadge.style.display = 'none'; | |
| // Reset analysis dashboard state | |
| analysisDashboard.style.display = 'none'; | |
| analysisDashboard.src = '#'; | |
| uploadArea.style.display = 'flex'; // Show upload area | |
| imageUploadInput.value = ''; // Clear file input | |
| }); | |
| async function handleImageUpload(file) { | |
| if (!file.type.startsWith('image/')) { | |
| alert('Please upload a valid image file.'); | |
| return; | |
| } | |
| // Show loading state | |
| loadingSpinner.style.display = 'block'; | |
| diagnosisResult.style.display = 'none'; | |
| uploadArea.style.display = 'none'; | |
| // Display image preview | |
| const reader = new FileReader(); | |
| reader.onload = e => imagePreview.src = e.target.result; | |
| reader.readAsDataURL(file); | |
| // Prepare form data and send to backend | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const response = await fetch('/diagnose', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! Status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| displayDiagnosis(data); | |
| } catch (error) { | |
| console.error('Diagnosis Error:', error); | |
| displayError('Failed to get diagnosis. Please ensure the backend is running and check the console.'); | |
| } finally { | |
| // Hide loading state and show results | |
| loadingSpinner.style.display = 'none'; | |
| diagnosisResult.style.display = 'block'; | |
| } | |
| } | |
| function displayError(message) { | |
| uploadArea.style.display = 'flex'; // Show upload area again | |
| diseaseName.textContent = 'Error'; | |
| healthStatus.textContent = message; | |
| detailedSymptoms.innerHTML = ''; | |
| preventionMethods.innerHTML = ''; | |
| smartAnalysis.textContent = ''; | |
| sourceModelBadge.style.display = 'none'; | |
| diagnosisResult.style.display = 'block'; | |
| } | |
| function displayDiagnosis(data) { | |
| // Format text with bullet points | |
| const formatBulletPoints = (text) => { | |
| if (!text) return ''; | |
| return '<ul>' + text.split('\\n').map(item => item.trim().startsWith('-') ? `<li>${item.substring(1).trim()}</li>` : `<li>${item.trim()}</li>`).join('') + '</ul>'; | |
| }; | |
| diseaseName.textContent = data.disease_name || 'N/A'; | |
| healthStatus.textContent = data.health_status || 'N/A'; | |
| smartAnalysis.textContent = data.smart_analysis || 'N/A'; | |
| detailedSymptoms.innerHTML = formatBulletPoints(data.detailed_symptoms); | |
| preventionMethods.innerHTML = formatBulletPoints(data.prevention_methods); | |
| // Update source model badge | |
| if (data.source_model) { | |
| sourceModelBadge.textContent = `Source: ${data.source_model}`; | |
| sourceModelBadge.className = 'badge'; // Reset class | |
| if (data.source_model === 'Keras + DB') { | |
| sourceModelBadge.classList.add('bg-success'); | |
| } else { | |
| sourceModelBadge.classList.add('bg-info'); | |
| } | |
| sourceModelBadge.style.display = 'inline-block'; | |
| } | |
| } | |
| // --- Chat Tab: Chat Functionality --- | |
| chatForm.addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| // Append user message immediately | |
| appendMessage(message, 'user'); | |
| chatInput.value = ''; // Clear input | |
| // Create a new message bubble for the bot's response and indicate loading | |
| const botDiv = appendMessage('', 'bot'); // Append '' initially | |
| // Add pulsing dots | |
| const thinkingIndicator = document.createElement('div'); | |
| thinkingIndicator.className = 'thinking-dots'; | |
| thinkingIndicator.innerHTML = '<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>'; | |
| botDiv.appendChild(thinkingIndicator); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| try { | |
| const response = await fetch('/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message: message }) | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`HTTP error! Status: ${response.status}, Message: ${errorText}`); | |
| } | |
| const botResponseText = await response.text(); // Get the full response text | |
| botDiv.innerHTML = ''; // Clear thinking indicator | |
| botDiv.innerHTML = marked.parse(botResponseText); // Use marked.parse for HTML rendering | |
| chatBox.scrollTop = chatBox.scrollHeight; // Scroll to the bottom | |
| } catch (error) { | |
| console.error('Bytez API Error:', error); | |
| botDiv.innerHTML = 'Sorry, I am having trouble connecting to the AI. Please try again later. ' + error.message; // Use innerHTML for error | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } | |
| }); // This closes the chatForm.addEventListener | |
| function appendMessage(message, sender) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `chat-message ${sender}`; | |
| if (sender === 'bot') { | |
| messageDiv.classList.add('bot-message'); // Add bot-message class for styling | |
| } | |
| messageDiv.textContent = message; | |
| chatBox.appendChild(messageDiv); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| return messageDiv; | |
| } | |
| // --- Model Showcase Tab: Interactive Expansion --- | |
| let activeCard = null; | |
| async function loadModelClasses() { | |
| if (!showcaseGrid) return; | |
| try { | |
| const response = await fetch('/api/classes'); | |
| if (!response.ok) throw new Error('Failed to fetch classes'); | |
| const classes = await response.json(); | |
| if (classes && !classes.error) { | |
| showcaseGrid.innerHTML = ''; // Clear previous | |
| classes.forEach(className => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'showcase-btn'; | |
| // The className is now the source of truth, e.g., "Apple Apple Scab" | |
| btn.dataset.classId = className; | |
| const title = document.createElement('span'); | |
| title.className = 'showcase-btn-title'; | |
| // The text content is the same as the ID | |
| title.textContent = className; | |
| btn.appendChild(title); | |
| btn.addEventListener('click', () => openShowcaseCard(btn)); | |
| showcaseGrid.appendChild(btn); | |
| }); | |
| } else { | |
| showcaseGrid.innerHTML = '<p class="text-danger">Could not load model classes.</p>'; | |
| } | |
| } catch (error) { | |
| console.error('Error fetching model classes:', error); | |
| showcaseGrid.innerHTML = '<p class="text-danger">Could not load model classes.</p>'; | |
| } | |
| } | |
| async function openShowcaseCard(button) { | |
| if (activeCard) return; | |
| activeCard = button; | |
| const classId = button.dataset.classId; | |
| const rect = button.getBoundingClientRect(); | |
| // 1. Show overlay | |
| showcaseOverlay.style.display = 'flex'; | |
| requestAnimationFrame(() => { | |
| showcaseOverlay.classList.add('visible'); | |
| }); | |
| // 2. Set initial card position and size | |
| showcaseCard.style.top = `${rect.top}px`; | |
| showcaseCard.style.left = `${rect.left}px`; | |
| showcaseCard.style.width = `${rect.width}px`; | |
| showcaseCard.style.height = `${rect.height}px`; | |
| // 3. Fetch data and prepare content | |
| showcaseContent.innerHTML = '<div class="spinner-border text-light" role="status"><span class="visually-hidden">Loading...</span></div>'; | |
| // Use encodeURIComponent to handle spaces and special characters in the classId | |
| const dataPromise = fetch(`/api/showcase/${encodeURIComponent(classId)}`).then(res => res.json()); | |
| // 4. Start expansion animation | |
| requestAnimationFrame(() => { | |
| showcaseCard.classList.add('expanding'); | |
| requestAnimationFrame(() => { | |
| showcaseCard.classList.add('expanded'); | |
| }); | |
| }); | |
| // 5. Populate content when data arrives | |
| try { | |
| const data = await dataPromise; | |
| if (data.error) throw new Error(data.error); | |
| showcaseContent.innerHTML = ` | |
| <h2>${data.title}</h2> | |
| <p>${data.short_description}</p> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <h3><i class="fa-solid fa-microscope me-2"></i>Quick Symptoms</h3> | |
| <ul>${data.quick_symptoms.map(s => `<li>${s}</li>`).join('')}</ul> | |
| </div> | |
| <div class="col-md-6"> | |
| <h3><i class="fa-solid fa-shield-alt me-2"></i>Fast Prevention</h3> | |
| <ul>${data.fast_prevention.map(p => `<li>${p}</li>`).join('')}</ul> | |
| </div> | |
| </div> | |
| `; | |
| } catch (error) { | |
| showcaseContent.innerHTML = `<p class="text-danger">Error: ${error.message}</p>`; | |
| } | |
| } | |
| function closeShowcaseCard() { | |
| if (!activeCard) return; | |
| const rect = activeCard.getBoundingClientRect(); | |
| // Reverse the animation | |
| showcaseCard.classList.remove('expanded'); | |
| // Reset to button's position - needs to be done after the transition starts | |
| setTimeout(() => { | |
| showcaseCard.style.top = `${rect.top}px`; | |
| showcaseCard.style.left = `${rect.left}px`; | |
| showcaseCard.style.width = `${rect.width}px`; | |
| showcaseCard.style.height = `${rect.height}px`; | |
| }, 0); | |
| showcaseOverlay.classList.remove('visible'); | |
| // Hide elements after transition | |
| showcaseCard.addEventListener('transitionend', () => { | |
| showcaseCard.classList.remove('expanding'); | |
| showcaseOverlay.style.display = 'none'; | |
| showcaseContent.innerHTML = ''; | |
| activeCard = null; | |
| }, { once: true }); | |
| } | |
| // Event listeners for closing the card | |
| showcaseCloseBtn.addEventListener('click', closeShowcaseCard); | |
| showcaseOverlay.addEventListener('click', (e) => { | |
| if (e.target === showcaseOverlay) { // Only if clicking the background itself | |
| closeShowcaseCard(); | |
| } | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape' && activeCard) { | |
| closeShowcaseCard(); | |
| } | |
| }); | |
| // --- Initializations --- | |
| loadModelClasses(); | |
| }); | |