// Basic front-end logic document.addEventListener('DOMContentLoaded', () => { const inputMode = document.getElementById('input-mode'); const fileInput = document.getElementById('file-input'); const startProcessingBtn = document.getElementById('start-processing'); const confidenceSlider = document.getElementById('confidence-threshold'); const confidenceValue = document.getElementById('confidence-value'); const progress = document.getElementById('progress'); const progressText = document.getElementById('progress-text'); const processingStatus = document.getElementById('processing-status'); const statusOutput = document.getElementById('status-output'); const resultsTableBody = document.querySelector('#results-table tbody'); const previewImage = document.getElementById('preview-image'); const imageInfo = document.getElementById('image-info'); const prevBtn = document.getElementById('prev-image'); const nextBtn = document.getElementById('next-image'); const exportCsvBtn = document.getElementById('export-csv'); const exportImagesBtn = document.getElementById('export-images'); const zoomInBtn = document.getElementById('zoom-in'); const zoomOutBtn = document.getElementById('zoom-out'); let currentResults = []; let currentImageIndex = -1; let currentJobId = null; // Update confidence value display confidenceSlider.addEventListener('input', () => { confidenceValue.textContent = confidenceSlider.value; }); // Handle input mode change (single file vs multiple) inputMode.addEventListener('change', () => { if (inputMode.value === 'single') { fileInput.removeAttribute('multiple'); } else { fileInput.setAttribute('multiple', ''); } }); // --- Updated Start Processing Logic --- startProcessingBtn.addEventListener('click', async () => { const files = fileInput.files; if (!files || files.length === 0) { logStatus('Error: No files selected.'); alert('Please select one or more image files.'); return; } const mode = inputMode.value; if (mode === 'single' && files.length > 1) { logStatus('Error: Single File mode selected, but multiple files chosen.'); alert('Single File mode allows only one file. Please select one file or switch to Directory mode.'); return; } logStatus('Starting upload and processing...'); processingStatus.textContent = 'Uploading...'; startProcessingBtn.disabled = true; progress.value = 0; progressText.textContent = '0%'; resultsTableBody.innerHTML = ''; // Clear previous results clearPreview(); // Clear image preview const formData = new FormData(); for (const file of files) { formData.append('files', file); } formData.append('input_mode', mode); formData.append('confidence_threshold', confidenceSlider.value); // Basic progress simulation for upload (replace with XHR progress if needed) progress.value = 50; progressText.textContent = '50%'; processingStatus.textContent = 'Processing...'; try { const response = await fetch('/process', { method: 'POST', body: formData, }); progress.value = 100; progressText.textContent = '100%'; // First check if the response is valid const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { // Handle non-JSON response const textResponse = await response.text(); logStatus(`Error: Server returned non-JSON response: ${textResponse.substring(0, 200)}...`); processingStatus.textContent = 'Error: Server returned invalid format'; throw new Error('Server returned non-JSON response'); } // Now we can safely parse JSON const data = await response.json(); if (response.ok) { logStatus('Processing successful.'); processingStatus.textContent = 'Processing finished.'; currentJobId = data.job_id; // Store the job ID logStatus(`Job ID: ${currentJobId}`); if (data.log) { logStatus("--- Server Log ---"); logStatus(data.log); logStatus("--- End Log ---"); } if (data.error_files && data.error_files.length > 0) { logStatus(`Warning: Skipped invalid files: ${data.error_files.join(', ')}`); } displayResults(data.results || []); } else { logStatus(`Error: ${data.error || 'Unknown processing error.'}`); if (data.log) { logStatus("--- Server Log ---"); logStatus(data.log); logStatus("--- End Log ---"); } processingStatus.textContent = 'Error during processing.'; alert(`Processing failed: ${data.error || 'Unknown error'}`); } } catch (error) { console.error('Fetch Error:', error); logStatus(`Network or Server Error: ${error.message}`); processingStatus.textContent = 'Network Error.'; progress.value = 0; progressText.textContent = 'Error'; alert(`An error occurred while communicating with the server: ${error.message}`); } finally { startProcessingBtn.disabled = false; // Re-enable button } }); function logStatus(message) { statusOutput.value += message + '\n'; statusOutput.scrollTop = statusOutput.scrollHeight; // Auto-scroll } function displayResults(results) { currentResults = results; // Store results resultsTableBody.innerHTML = ''; // Clear existing results currentImageIndex = -1; // Reset index currentJobId = currentJobId; // Ensure job ID is current if (!results || results.length === 0) { logStatus("No results returned from processing."); clearPreview(); exportCsvBtn.disabled = true; exportImagesBtn.disabled = true; updateNavButtons(); // Ensure nav buttons are disabled return; // Exit early } results.forEach((result, index) => { const row = resultsTableBody.insertRow(); row.classList.add('result-row'); // Add class for styling/selection row.innerHTML = `${result.filename}${result.num_eggs}`; // Add data attributes for easy access row.dataset.index = index; row.dataset.filename = result.filename; row.dataset.annotatedFilename = result.annotated_filename; row.addEventListener('click', () => { displayImage(index); }); }); displayImage(0); // Show the first image initially // Enable export buttons IF there are results exportCsvBtn.disabled = !results.some(r => r.filename); // Enable if at least one result has a filename exportImagesBtn.disabled = !results.some(r => r.annotated_filename); // Enable if at least one result has an annotated image } function displayImage(index) { if (index < 0 || index >= currentResults.length || !currentJobId) { clearPreview(); return; } currentImageIndex = index; const result = currentResults[index]; if (result.annotated_filename) { const imageUrl = `/results/${currentJobId}/${result.annotated_filename}`; previewImage.src = imageUrl; previewImage.alt = `Annotated ${result.filename}`; imageInfo.textContent = `Filename: ${result.filename} - Eggs detected: ${result.num_eggs}`; logStatus(`Displaying image: ${result.annotated_filename}`); } else { // Handle cases where annotation failed or wasn't produced previewImage.src = ''; // Clear image previewImage.alt = 'Annotated image not available'; imageInfo.textContent = `Filename: ${result.filename} - Eggs detected: ${result.num_eggs} (Annotation N/A)`; logStatus(`Annotated image not available for: ${result.filename}`); } updateNavButtons(); // Highlight selected row document.querySelectorAll('.result-row').forEach(row => { row.classList.remove('selected'); }); const selectedRow = resultsTableBody.querySelector(`tr[data-index="${index}"]`); if (selectedRow) { selectedRow.classList.add('selected'); } } function clearPreview() { previewImage.src = ''; previewImage.alt = 'Annotated image preview'; imageInfo.textContent = 'Filename: - Eggs detected: -'; currentImageIndex = -1; updateNavButtons(); } function updateNavButtons() { prevBtn.disabled = currentImageIndex <= 0; nextBtn.disabled = currentImageIndex < 0 || currentImageIndex >= currentResults.length - 1; } prevBtn.addEventListener('click', () => { if (currentImageIndex > 0) { displayImage(currentImageIndex - 1); } }); nextBtn.addEventListener('click', () => { if (currentImageIndex < currentResults.length - 1) { displayImage(currentImageIndex + 1); } }); // --- Export Logic (Placeholders - requires backend implementation) --- exportCsvBtn.addEventListener('click', () => { if (!currentJobId) { alert("No job processed yet."); return; } // Construct the CSV download URL const csvFilename = `${currentJobId}_results.csv`; const downloadUrl = `/results/${currentJobId}/${csvFilename}`; logStatus(`Triggering CSV download: ${csvFilename}`); // Create a temporary link and click it const link = document.createElement('a'); link.href = downloadUrl; link.download = csvFilename; // Suggest filename to browser document.body.appendChild(link); link.click(); document.body.removeChild(link); }); exportImagesBtn.addEventListener('click', () => { if (!currentJobId) { alert("No job processed yet."); return; } // This is more complex. Ideally, the backend would provide a zip file. // For now, just log and maybe open the first image? logStatus('Image export clicked. Downloading individual images or a zip file is needed.'); alert('Image export functionality requires backend support to create a downloadable archive (zip file).'); // Example: trigger download of the currently viewed image if (currentImageIndex !== -1 && currentResults[currentImageIndex].annotated_filename) { const imgFilename = currentResults[currentImageIndex].annotated_filename; const downloadUrl = `/results/${currentJobId}/${imgFilename}`; const link = document.createElement('a'); link.href = downloadUrl; link.download = imgFilename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }); // --- Zoom Logic (Placeholder - requires a library or complex CSS/JS) --- zoomInBtn.addEventListener('click', () => { console.log('Zoom In clicked'); logStatus('Zoom functionality not yet implemented.'); alert('Zoom functionality is not yet implemented.'); }); zoomOutBtn.addEventListener('click', () => { console.log('Zoom Out clicked'); logStatus('Zoom functionality not yet implemented.'); alert('Zoom functionality is not yet implemented.'); }); // Initial setup if (inputMode.value === 'single') { fileInput.removeAttribute('multiple'); } logStatus('Application initialized. Ready for file selection.'); exportCsvBtn.disabled = true; exportImagesBtn.disabled = true; clearPreview(); // Ensure preview is cleared on load });