Plantoi / static /js /main.js
Al1Abdullah's picture
Deploy Plantoi: Update app, requirements, and model
47058bc
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();
});