nemaquant / static /script.js
sloneckity's picture
Improve error handling in both frontend and backend
7954a1d
raw
history blame
12.5 kB
// 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
});