Spaces:
Sleeping
Sleeping
| // 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 = `<td>${result.filename}</td><td>${result.num_eggs}</td>`; | |
| // 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 | |
| }); |