Spaces:
Sleeping
Sleeping
Commit
·
32482ae
1
Parent(s):
0605276
Update frontend files (script.js, style.css, index.html)
Browse files- static/script.js +243 -19
- static/style.css +85 -9
- templates/index.html +7 -4
static/script.js
CHANGED
|
@@ -13,6 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 13 |
const progress = document.getElementById('progress');
|
| 14 |
const progressText = document.getElementById('progress-text');
|
| 15 |
const progressPercentage = document.getElementById('progress-percentage');
|
|
|
|
| 16 |
const statusOutput = document.getElementById('status-output');
|
| 17 |
const clearLogBtn = document.getElementById('clear-log');
|
| 18 |
const resultsTableBody = document.querySelector('.results-table tbody');
|
|
@@ -25,6 +26,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 25 |
const exportCsvBtn = document.getElementById('export-csv');
|
| 26 |
const exportImagesBtn = document.getElementById('export-images');
|
| 27 |
const inputModeHelp = document.getElementById('input-mode-help');
|
|
|
|
| 28 |
|
| 29 |
let currentResults = [];
|
| 30 |
let currentImageIndex = -1;
|
|
@@ -34,6 +36,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 34 |
const MIN_ZOOM = 0.5;
|
| 35 |
let progressInterval = null; // Interval timer for polling
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
// Pagination and sorting variables
|
| 38 |
const RESULTS_PER_PAGE = 10;
|
| 39 |
let currentPage = 1;
|
|
@@ -282,6 +291,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 282 |
clearInterval(progressInterval); // Clear any existing timer
|
| 283 |
}
|
| 284 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
progressInterval = setInterval(async () => {
|
| 286 |
try {
|
| 287 |
const response = await fetch(`/progress/${jobId}`);
|
|
@@ -299,8 +318,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 299 |
|
| 300 |
// Update UI based on status
|
| 301 |
updateProgress(data.progress || 0, data.status);
|
| 302 |
-
|
| 303 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
if (data.status === 'success') {
|
| 306 |
clearInterval(progressInterval);
|
|
@@ -344,9 +367,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 344 |
if (isLoading) {
|
| 345 |
startProcessingBtn.innerHTML = '<i class="ri-loader-4-line"></i> Processing...';
|
| 346 |
document.body.classList.add('processing');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
} else {
|
| 348 |
startProcessingBtn.innerHTML = '<i class="ri-play-line"></i> Start Processing';
|
| 349 |
document.body.classList.remove('processing');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
}
|
| 351 |
}
|
| 352 |
|
|
@@ -354,6 +385,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 354 |
progress.value = value;
|
| 355 |
progressPercentage.textContent = `${value}%`;
|
| 356 |
progressText.textContent = message;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
}
|
| 358 |
|
| 359 |
function logStatus(message) {
|
|
@@ -586,11 +622,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 586 |
const imageUrl = `/results/${currentJobId}/${result.annotated_filename}`;
|
| 587 |
previewImage.src = imageUrl;
|
| 588 |
previewImage.alt = result.filename;
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
<i class="ri-egg-line"></i> ${result.num_eggs} eggs detected
|
| 593 |
-
`;
|
| 594 |
|
| 595 |
// Enable zoom controls
|
| 596 |
zoomInBtn.disabled = false;
|
|
@@ -613,10 +647,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 613 |
rows.forEach(row => {
|
| 614 |
if (parseInt(row.dataset.originalIndex) === index) {
|
| 615 |
row.classList.add('selected');
|
| 616 |
-
// Scroll the row into view if needed
|
| 617 |
-
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| 618 |
}
|
| 619 |
});
|
|
|
|
|
|
|
|
|
|
| 620 |
} else {
|
| 621 |
clearPreview();
|
| 622 |
}
|
|
@@ -631,8 +666,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 631 |
previewImage.alt = 'No image selected';
|
| 632 |
imageInfo.textContent = 'Select an image from the results to view';
|
| 633 |
currentImageIndex = -1;
|
| 634 |
-
|
| 635 |
-
updateZoom();
|
| 636 |
|
| 637 |
// Disable controls
|
| 638 |
prevBtn.disabled = true;
|
|
@@ -640,6 +674,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 640 |
zoomInBtn.disabled = true;
|
| 641 |
zoomOutBtn.disabled = true;
|
| 642 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
// Image Navigation
|
| 645 |
prevBtn.addEventListener('click', () => {
|
|
@@ -655,26 +709,136 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 655 |
});
|
| 656 |
|
| 657 |
// Zoom Controls
|
| 658 |
-
function
|
| 659 |
if (previewImage.src) {
|
| 660 |
-
previewImage.style.transform = `scale(${currentZoomLevel})`;
|
| 661 |
}
|
| 662 |
}
|
| 663 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
zoomInBtn.addEventListener('click', () => {
|
| 665 |
-
if (currentZoomLevel < MAX_ZOOM) {
|
| 666 |
-
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
}
|
| 669 |
});
|
| 670 |
|
|
|
|
| 671 |
zoomOutBtn.addEventListener('click', () => {
|
| 672 |
-
if (currentZoomLevel > MIN_ZOOM) {
|
| 673 |
-
|
| 674 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
}
|
| 676 |
});
|
| 677 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
// Export Handlers
|
| 679 |
exportCsvBtn.addEventListener('click', () => {
|
| 680 |
if (!currentJobId) return;
|
|
@@ -687,6 +851,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 687 |
window.location.href = `/export_images/${currentJobId}`;
|
| 688 |
});
|
| 689 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
// Initialize
|
| 691 |
updateUploadState();
|
| 692 |
logStatus('Application ready');
|
|
|
|
| 13 |
const progress = document.getElementById('progress');
|
| 14 |
const progressText = document.getElementById('progress-text');
|
| 15 |
const progressPercentage = document.getElementById('progress-percentage');
|
| 16 |
+
const imageCounter = document.getElementById('image-counter');
|
| 17 |
const statusOutput = document.getElementById('status-output');
|
| 18 |
const clearLogBtn = document.getElementById('clear-log');
|
| 19 |
const resultsTableBody = document.querySelector('.results-table tbody');
|
|
|
|
| 26 |
const exportCsvBtn = document.getElementById('export-csv');
|
| 27 |
const exportImagesBtn = document.getElementById('export-images');
|
| 28 |
const inputModeHelp = document.getElementById('input-mode-help');
|
| 29 |
+
const imageContainer = document.getElementById('image-container');
|
| 30 |
|
| 31 |
let currentResults = [];
|
| 32 |
let currentImageIndex = -1;
|
|
|
|
| 36 |
const MIN_ZOOM = 0.5;
|
| 37 |
let progressInterval = null; // Interval timer for polling
|
| 38 |
|
| 39 |
+
// Panning variables
|
| 40 |
+
let isPanning = false;
|
| 41 |
+
let startPanX = 0;
|
| 42 |
+
let startPanY = 0;
|
| 43 |
+
let currentPanX = 0;
|
| 44 |
+
let currentPanY = 0;
|
| 45 |
+
|
| 46 |
// Pagination and sorting variables
|
| 47 |
const RESULTS_PER_PAGE = 10;
|
| 48 |
let currentPage = 1;
|
|
|
|
| 291 |
clearInterval(progressInterval); // Clear any existing timer
|
| 292 |
}
|
| 293 |
|
| 294 |
+
const totalImages = Array.from(fileInput.files).filter(file => {
|
| 295 |
+
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/tif'];
|
| 296 |
+
if (inputMode.value === 'folder') {
|
| 297 |
+
return allowedTypes.includes(file.type) &&
|
| 298 |
+
file.webkitRelativePath &&
|
| 299 |
+
!file.webkitRelativePath.startsWith('.');
|
| 300 |
+
}
|
| 301 |
+
return allowedTypes.includes(file.type);
|
| 302 |
+
}).length;
|
| 303 |
+
|
| 304 |
progressInterval = setInterval(async () => {
|
| 305 |
try {
|
| 306 |
const response = await fetch(`/progress/${jobId}`);
|
|
|
|
| 318 |
|
| 319 |
// Update UI based on status
|
| 320 |
updateProgress(data.progress || 0, data.status);
|
| 321 |
+
|
| 322 |
+
// Update image counter based on progress percentage
|
| 323 |
+
if (data.status === 'running' || data.status === 'starting') {
|
| 324 |
+
const processedImages = Math.floor((data.progress / 90) * totalImages); // 90 is the max progress before completion
|
| 325 |
+
imageCounter.textContent = `${processedImages} of ${totalImages} images`;
|
| 326 |
+
}
|
| 327 |
|
| 328 |
if (data.status === 'success') {
|
| 329 |
clearInterval(progressInterval);
|
|
|
|
| 367 |
if (isLoading) {
|
| 368 |
startProcessingBtn.innerHTML = '<i class="ri-loader-4-line"></i> Processing...';
|
| 369 |
document.body.classList.add('processing');
|
| 370 |
+
// Disable input changes during processing
|
| 371 |
+
inputMode.disabled = true;
|
| 372 |
+
fileInput.disabled = true;
|
| 373 |
+
confidenceSlider.disabled = true;
|
| 374 |
} else {
|
| 375 |
startProcessingBtn.innerHTML = '<i class="ri-play-line"></i> Start Processing';
|
| 376 |
document.body.classList.remove('processing');
|
| 377 |
+
// Re-enable inputs after processing
|
| 378 |
+
inputMode.disabled = false;
|
| 379 |
+
fileInput.disabled = false;
|
| 380 |
+
confidenceSlider.disabled = false;
|
| 381 |
}
|
| 382 |
}
|
| 383 |
|
|
|
|
| 385 |
progress.value = value;
|
| 386 |
progressPercentage.textContent = `${value}%`;
|
| 387 |
progressText.textContent = message;
|
| 388 |
+
|
| 389 |
+
// Clear the image counter when not processing
|
| 390 |
+
if (message === 'Ready to process' || message === 'Processing complete' || message.startsWith('Error')) {
|
| 391 |
+
imageCounter.textContent = '';
|
| 392 |
+
}
|
| 393 |
}
|
| 394 |
|
| 395 |
function logStatus(message) {
|
|
|
|
| 622 |
const imageUrl = `/results/${currentJobId}/${result.annotated_filename}`;
|
| 623 |
previewImage.src = imageUrl;
|
| 624 |
previewImage.alt = result.filename;
|
| 625 |
+
|
| 626 |
+
// Update image info with the new function
|
| 627 |
+
updateImageInfo();
|
|
|
|
|
|
|
| 628 |
|
| 629 |
// Enable zoom controls
|
| 630 |
zoomInBtn.disabled = false;
|
|
|
|
| 647 |
rows.forEach(row => {
|
| 648 |
if (parseInt(row.dataset.originalIndex) === index) {
|
| 649 |
row.classList.add('selected');
|
|
|
|
|
|
|
| 650 |
}
|
| 651 |
});
|
| 652 |
+
|
| 653 |
+
// Reset panning when a new image is displayed
|
| 654 |
+
resetPanZoom();
|
| 655 |
} else {
|
| 656 |
clearPreview();
|
| 657 |
}
|
|
|
|
| 666 |
previewImage.alt = 'No image selected';
|
| 667 |
imageInfo.textContent = 'Select an image from the results to view';
|
| 668 |
currentImageIndex = -1;
|
| 669 |
+
resetPanZoom();
|
|
|
|
| 670 |
|
| 671 |
// Disable controls
|
| 672 |
prevBtn.disabled = true;
|
|
|
|
| 674 |
zoomInBtn.disabled = true;
|
| 675 |
zoomOutBtn.disabled = true;
|
| 676 |
}
|
| 677 |
+
|
| 678 |
+
function resetPanZoom() {
|
| 679 |
+
// Reset zoom and pan values
|
| 680 |
+
currentZoomLevel = 1;
|
| 681 |
+
currentPanX = 0;
|
| 682 |
+
currentPanY = 0;
|
| 683 |
+
|
| 684 |
+
// Reset transform directly
|
| 685 |
+
if (previewImage.src) {
|
| 686 |
+
previewImage.style.transform = 'none';
|
| 687 |
+
|
| 688 |
+
// Force a reflow to ensure the transform is actually reset
|
| 689 |
+
void previewImage.offsetWidth;
|
| 690 |
+
|
| 691 |
+
// Then apply the default transform
|
| 692 |
+
updateImageTransform();
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
updatePanCursorState();
|
| 696 |
+
}
|
| 697 |
|
| 698 |
// Image Navigation
|
| 699 |
prevBtn.addEventListener('click', () => {
|
|
|
|
| 709 |
});
|
| 710 |
|
| 711 |
// Zoom Controls
|
| 712 |
+
function updateImageTransform() {
|
| 713 |
if (previewImage.src) {
|
| 714 |
+
previewImage.style.transform = `translate(${currentPanX}px, ${currentPanY}px) scale(${currentZoomLevel})`;
|
| 715 |
}
|
| 716 |
}
|
| 717 |
|
| 718 |
+
// Unified zoom function for both buttons and wheel
|
| 719 |
+
function zoomImage(newZoom, zoomX, zoomY) {
|
| 720 |
+
if (newZoom >= MIN_ZOOM && newZoom <= MAX_ZOOM) {
|
| 721 |
+
const oldZoom = currentZoomLevel;
|
| 722 |
+
|
| 723 |
+
// If zooming out to minimum, just reset everything
|
| 724 |
+
if (newZoom === MIN_ZOOM) {
|
| 725 |
+
currentZoomLevel = MIN_ZOOM;
|
| 726 |
+
currentPanX = 0;
|
| 727 |
+
currentPanY = 0;
|
| 728 |
+
} else {
|
| 729 |
+
// Calculate zoom ratio
|
| 730 |
+
const zoomRatio = newZoom / oldZoom;
|
| 731 |
+
|
| 732 |
+
// Get the image and container dimensions
|
| 733 |
+
const containerRect = imageContainer.getBoundingClientRect();
|
| 734 |
+
const imgRect = previewImage.getBoundingClientRect();
|
| 735 |
+
|
| 736 |
+
// Calculate the center of the image
|
| 737 |
+
const imgCenterX = imgRect.width / 2;
|
| 738 |
+
const imgCenterY = imgRect.height / 2;
|
| 739 |
+
|
| 740 |
+
// Calculate the point to zoom relative to
|
| 741 |
+
const relativeX = zoomX - (imgRect.left - containerRect.left);
|
| 742 |
+
const relativeY = zoomY - (imgRect.top - containerRect.top);
|
| 743 |
+
|
| 744 |
+
// Update zoom level
|
| 745 |
+
currentZoomLevel = newZoom;
|
| 746 |
+
|
| 747 |
+
// Adjust pan to maintain the zoom point position
|
| 748 |
+
currentPanX = currentPanX + (imgCenterX - relativeX) * (zoomRatio - 1);
|
| 749 |
+
currentPanY = currentPanY + (imgCenterY - relativeY) * (zoomRatio - 1);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
updateImageTransform();
|
| 753 |
+
updatePanCursorState();
|
| 754 |
+
updateImageInfo();
|
| 755 |
+
}
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
// Zoom in button handler
|
| 759 |
zoomInBtn.addEventListener('click', () => {
|
| 760 |
+
if (currentZoomLevel < MAX_ZOOM && previewImage.src) {
|
| 761 |
+
const containerRect = imageContainer.getBoundingClientRect();
|
| 762 |
+
const imgRect = previewImage.getBoundingClientRect();
|
| 763 |
+
|
| 764 |
+
// Calculate the center point of the image
|
| 765 |
+
const centerX = (imgRect.left - containerRect.left) + imgRect.width / 2;
|
| 766 |
+
const centerY = (imgRect.top - containerRect.top) + imgRect.height / 2;
|
| 767 |
+
|
| 768 |
+
zoomImage(currentZoomLevel + 0.25, centerX, centerY);
|
| 769 |
}
|
| 770 |
});
|
| 771 |
|
| 772 |
+
// Zoom out button handler
|
| 773 |
zoomOutBtn.addEventListener('click', () => {
|
| 774 |
+
if (currentZoomLevel > MIN_ZOOM && previewImage.src) {
|
| 775 |
+
const containerRect = imageContainer.getBoundingClientRect();
|
| 776 |
+
const imgRect = previewImage.getBoundingClientRect();
|
| 777 |
+
|
| 778 |
+
// Calculate the center point of the image
|
| 779 |
+
const centerX = (imgRect.left - containerRect.left) + imgRect.width / 2;
|
| 780 |
+
const centerY = (imgRect.top - containerRect.top) + imgRect.height / 2;
|
| 781 |
+
|
| 782 |
+
zoomImage(currentZoomLevel - 0.25, centerX, centerY);
|
| 783 |
+
}
|
| 784 |
+
});
|
| 785 |
+
|
| 786 |
+
// Mouse wheel zoom uses cursor position
|
| 787 |
+
imageContainer.addEventListener('wheel', (e) => {
|
| 788 |
+
if (previewImage.src) {
|
| 789 |
+
e.preventDefault();
|
| 790 |
+
|
| 791 |
+
// Get mouse position relative to container
|
| 792 |
+
const rect = imageContainer.getBoundingClientRect();
|
| 793 |
+
const mouseX = e.clientX - rect.left;
|
| 794 |
+
const mouseY = e.clientY - rect.top;
|
| 795 |
+
|
| 796 |
+
// Calculate zoom direction and factor
|
| 797 |
+
const zoomDirection = e.deltaY < 0 ? 1 : -1;
|
| 798 |
+
const zoomFactor = 0.1;
|
| 799 |
+
|
| 800 |
+
// Calculate and apply new zoom level
|
| 801 |
+
const newZoom = currentZoomLevel + (zoomDirection * zoomFactor);
|
| 802 |
+
zoomImage(newZoom, mouseX, mouseY);
|
| 803 |
+
}
|
| 804 |
+
});
|
| 805 |
+
|
| 806 |
+
// Panning event listeners
|
| 807 |
+
imageContainer.addEventListener('mousedown', (e) => {
|
| 808 |
+
if (e.button === 0 && previewImage.src && currentZoomLevel > 1) { // Left mouse button and zoomed in
|
| 809 |
+
isPanning = true;
|
| 810 |
+
startPanX = e.clientX - currentPanX;
|
| 811 |
+
startPanY = e.clientY - currentPanY;
|
| 812 |
+
imageContainer.classList.add('panning');
|
| 813 |
+
e.preventDefault(); // Prevent image dragging behavior
|
| 814 |
+
}
|
| 815 |
+
});
|
| 816 |
+
|
| 817 |
+
window.addEventListener('mousemove', (e) => {
|
| 818 |
+
if (isPanning) {
|
| 819 |
+
currentPanX = e.clientX - startPanX;
|
| 820 |
+
currentPanY = e.clientY - startPanY;
|
| 821 |
+
updateImageTransform();
|
| 822 |
+
}
|
| 823 |
+
});
|
| 824 |
+
|
| 825 |
+
window.addEventListener('mouseup', () => {
|
| 826 |
+
if (isPanning) {
|
| 827 |
+
isPanning = false;
|
| 828 |
+
imageContainer.classList.remove('panning');
|
| 829 |
}
|
| 830 |
});
|
| 831 |
|
| 832 |
+
// Function to update cursor state based on zoom level
|
| 833 |
+
function updatePanCursorState() {
|
| 834 |
+
if (currentZoomLevel > 1) {
|
| 835 |
+
imageContainer.classList.add('can-pan');
|
| 836 |
+
} else {
|
| 837 |
+
imageContainer.classList.remove('can-pan');
|
| 838 |
+
imageContainer.classList.remove('panning');
|
| 839 |
+
}
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
// Export Handlers
|
| 843 |
exportCsvBtn.addEventListener('click', () => {
|
| 844 |
if (!currentJobId) return;
|
|
|
|
| 851 |
window.location.href = `/export_images/${currentJobId}`;
|
| 852 |
});
|
| 853 |
|
| 854 |
+
// Add keyboard controls for panning (arrow keys)
|
| 855 |
+
window.addEventListener('keydown', (e) => {
|
| 856 |
+
if (previewImage.src && currentZoomLevel > 1) {
|
| 857 |
+
const PAN_AMOUNT = 30; // pixels to pan per key press
|
| 858 |
+
|
| 859 |
+
switch (e.key) {
|
| 860 |
+
case 'ArrowLeft':
|
| 861 |
+
currentPanX += PAN_AMOUNT;
|
| 862 |
+
updateImageTransform();
|
| 863 |
+
e.preventDefault();
|
| 864 |
+
break;
|
| 865 |
+
case 'ArrowRight':
|
| 866 |
+
currentPanX -= PAN_AMOUNT;
|
| 867 |
+
updateImageTransform();
|
| 868 |
+
e.preventDefault();
|
| 869 |
+
break;
|
| 870 |
+
case 'ArrowUp':
|
| 871 |
+
currentPanY += PAN_AMOUNT;
|
| 872 |
+
updateImageTransform();
|
| 873 |
+
e.preventDefault();
|
| 874 |
+
break;
|
| 875 |
+
case 'ArrowDown':
|
| 876 |
+
currentPanY -= PAN_AMOUNT;
|
| 877 |
+
updateImageTransform();
|
| 878 |
+
e.preventDefault();
|
| 879 |
+
break;
|
| 880 |
+
case 'Home':
|
| 881 |
+
// Reset pan position but keep zoom
|
| 882 |
+
currentPanX = 0;
|
| 883 |
+
currentPanY = 0;
|
| 884 |
+
updateImageTransform();
|
| 885 |
+
e.preventDefault();
|
| 886 |
+
break;
|
| 887 |
+
}
|
| 888 |
+
}
|
| 889 |
+
});
|
| 890 |
+
|
| 891 |
+
// Add some instructions to the image info when zoomed in
|
| 892 |
+
function updateImageInfo() {
|
| 893 |
+
if (!currentResults[currentImageIndex]) return;
|
| 894 |
+
|
| 895 |
+
const result = currentResults[currentImageIndex];
|
| 896 |
+
let infoText = `
|
| 897 |
+
<i class="ri-image-line"></i> ${result.filename}
|
| 898 |
+
<br>
|
| 899 |
+
<i class="ri-egg-line"></i> ${result.num_eggs} eggs detected
|
| 900 |
+
`;
|
| 901 |
+
|
| 902 |
+
if (currentZoomLevel > 1) {
|
| 903 |
+
infoText += `
|
| 904 |
+
<br>
|
| 905 |
+
<small style="color: var(--text-muted);">
|
| 906 |
+
<i class="ri-drag-move-line"></i> Click and drag to pan or use arrow keys
|
| 907 |
+
</small>
|
| 908 |
+
`;
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
imageInfo.innerHTML = infoText;
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
// Initialize
|
| 915 |
updateUploadState();
|
| 916 |
logStatus('Application ready');
|
static/style.css
CHANGED
|
@@ -181,13 +181,17 @@ textarea:focus {
|
|
| 181 |
align-items: center;
|
| 182 |
justify-content: center;
|
| 183 |
height: 400px;
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
.image-preview img {
|
|
|
|
| 187 |
max-width: 100%;
|
| 188 |
max-height: 100%;
|
| 189 |
-
|
| 190 |
-
transition: transform 0.
|
|
|
|
| 191 |
}
|
| 192 |
|
| 193 |
.image-controls {
|
|
@@ -197,6 +201,15 @@ textarea:focus {
|
|
| 197 |
justify-content: center;
|
| 198 |
}
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
.image-info {
|
| 201 |
margin: 1rem 0;
|
| 202 |
padding: 0.75rem;
|
|
@@ -395,6 +408,63 @@ progress::-moz-progress-bar {
|
|
| 395 |
justify-content: center;
|
| 396 |
}
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
/* Tooltips */
|
| 399 |
[data-tooltip] {
|
| 400 |
position: relative;
|
|
@@ -407,15 +477,21 @@ progress::-moz-progress-bar {
|
|
| 407 |
bottom: 100%;
|
| 408 |
left: 50%;
|
| 409 |
transform: translateX(-50%);
|
| 410 |
-
padding: 0.5rem;
|
| 411 |
-
background
|
| 412 |
color: white;
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
| 417 |
pointer-events: none;
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
| 419 |
}
|
| 420 |
|
| 421 |
[data-tooltip]:hover::after {
|
|
|
|
| 181 |
align-items: center;
|
| 182 |
justify-content: center;
|
| 183 |
height: 400px;
|
| 184 |
+
cursor: default;
|
| 185 |
+
user-select: none;
|
| 186 |
}
|
| 187 |
|
| 188 |
.image-preview img {
|
| 189 |
+
object-fit: contain;
|
| 190 |
max-width: 100%;
|
| 191 |
max-height: 100%;
|
| 192 |
+
transform-origin: center;
|
| 193 |
+
transition: transform 0.1s ease-out;
|
| 194 |
+
display: block;
|
| 195 |
}
|
| 196 |
|
| 197 |
.image-controls {
|
|
|
|
| 201 |
justify-content: center;
|
| 202 |
}
|
| 203 |
|
| 204 |
+
/* Panning cursor states */
|
| 205 |
+
.image-preview.can-pan {
|
| 206 |
+
cursor: grab;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.image-preview.panning {
|
| 210 |
+
cursor: grabbing;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
.image-info {
|
| 214 |
margin: 1rem 0;
|
| 215 |
padding: 0.75rem;
|
|
|
|
| 408 |
justify-content: center;
|
| 409 |
}
|
| 410 |
|
| 411 |
+
/* Processing State */
|
| 412 |
+
body.processing .progress-container {
|
| 413 |
+
position: relative;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
@keyframes spin {
|
| 417 |
+
to { transform: rotate(360deg); }
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.processing #start-processing i {
|
| 421 |
+
animation: spin 1s linear infinite;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.progress-container::before {
|
| 425 |
+
content: '';
|
| 426 |
+
position: absolute;
|
| 427 |
+
top: 0;
|
| 428 |
+
left: 0;
|
| 429 |
+
right: 0;
|
| 430 |
+
height: 2px;
|
| 431 |
+
background: linear-gradient(90deg,
|
| 432 |
+
var(--primary-color) 0%,
|
| 433 |
+
var(--primary-hover) 50%,
|
| 434 |
+
var(--primary-color) 100%);
|
| 435 |
+
background-size: 200% 100%;
|
| 436 |
+
animation: shimmer 2s infinite linear;
|
| 437 |
+
opacity: 0;
|
| 438 |
+
transition: opacity 0.3s ease;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
body.processing .progress-container::before {
|
| 442 |
+
opacity: 1;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
@keyframes shimmer {
|
| 446 |
+
to { background-position: 200% 0; }
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
/* Update progress bar styles for processing state */
|
| 450 |
+
body.processing progress::-webkit-progress-value {
|
| 451 |
+
background: linear-gradient(90deg,
|
| 452 |
+
var(--primary-color) 0%,
|
| 453 |
+
var(--primary-hover) 50%,
|
| 454 |
+
var(--primary-color) 100%);
|
| 455 |
+
background-size: 200% 100%;
|
| 456 |
+
animation: shimmer 2s infinite linear;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
body.processing progress::-moz-progress-bar {
|
| 460 |
+
background: linear-gradient(90deg,
|
| 461 |
+
var(--primary-color) 0%,
|
| 462 |
+
var(--primary-hover) 50%,
|
| 463 |
+
var(--primary-color) 100%);
|
| 464 |
+
background-size: 200% 100%;
|
| 465 |
+
animation: shimmer 2s infinite linear;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
/* Tooltips */
|
| 469 |
[data-tooltip] {
|
| 470 |
position: relative;
|
|
|
|
| 477 |
bottom: 100%;
|
| 478 |
left: 50%;
|
| 479 |
transform: translateX(-50%);
|
| 480 |
+
padding: 0.5rem 1rem;
|
| 481 |
+
background: rgba(0, 0, 0, 0.8);
|
| 482 |
color: white;
|
| 483 |
+
border-radius: 0.375rem;
|
| 484 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 485 |
+
font-size: 0.875rem;
|
| 486 |
+
white-space: normal;
|
| 487 |
+
text-align: center;
|
| 488 |
+
max-width: 300px;
|
| 489 |
+
width: max-content;
|
| 490 |
pointer-events: none;
|
| 491 |
+
opacity: 0;
|
| 492 |
+
transition: opacity 0.2s ease;
|
| 493 |
+
z-index: 1000;
|
| 494 |
+
margin-bottom: 0.5rem;
|
| 495 |
}
|
| 496 |
|
| 497 |
[data-tooltip]:hover::after {
|
templates/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
<i class="ri-microscope-line"></i>
|
| 13 |
NemaQuant
|
| 14 |
<small style="display: block; font-size: 1rem; font-weight: normal; color: var(--text-muted);">
|
| 15 |
-
Automated Nematode Egg Detection
|
| 16 |
</small>
|
| 17 |
</h1>
|
| 18 |
|
|
@@ -48,9 +48,9 @@
|
|
| 48 |
<div class="card compact">
|
| 49 |
<h2><i class="ri-settings-4-line"></i> Processing</h2>
|
| 50 |
<div class="form-group">
|
| 51 |
-
<label for="confidence-threshold"
|
| 52 |
Confidence Threshold
|
| 53 |
-
<i class="ri-information-line"></i>
|
| 54 |
</label>
|
| 55 |
<div class="range-with-value">
|
| 56 |
<input type="range" id="confidence-threshold" name="confidence-threshold"
|
|
@@ -62,7 +62,10 @@
|
|
| 62 |
<div class="progress-container">
|
| 63 |
<div class="progress-info">
|
| 64 |
<span id="progress-text">Ready to process</span>
|
| 65 |
-
<
|
|
|
|
|
|
|
|
|
|
| 66 |
</div>
|
| 67 |
<progress id="progress" value="0" max="100"></progress>
|
| 68 |
</div>
|
|
|
|
| 12 |
<i class="ri-microscope-line"></i>
|
| 13 |
NemaQuant
|
| 14 |
<small style="display: block; font-size: 1rem; font-weight: normal; color: var(--text-muted);">
|
| 15 |
+
Automated Nematode Egg Detection and Counting
|
| 16 |
</small>
|
| 17 |
</h1>
|
| 18 |
|
|
|
|
| 48 |
<div class="card compact">
|
| 49 |
<h2><i class="ri-settings-4-line"></i> Processing</h2>
|
| 50 |
<div class="form-group">
|
| 51 |
+
<label for="confidence-threshold">
|
| 52 |
Confidence Threshold
|
| 53 |
+
<i class="ri-information-line" data-tooltip="Recommended range: 0.5-0.7; higher values reduce false detections but may miss eggs, lower values catch more eggs but may include false positives"></i>
|
| 54 |
</label>
|
| 55 |
<div class="range-with-value">
|
| 56 |
<input type="range" id="confidence-threshold" name="confidence-threshold"
|
|
|
|
| 62 |
<div class="progress-container">
|
| 63 |
<div class="progress-info">
|
| 64 |
<span id="progress-text">Ready to process</span>
|
| 65 |
+
<div>
|
| 66 |
+
<span id="image-counter" style="margin-right: 1rem;"></span>
|
| 67 |
+
<span id="progress-percentage">0%</span>
|
| 68 |
+
</div>
|
| 69 |
</div>
|
| 70 |
<progress id="progress" value="0" max="100"></progress>
|
| 71 |
</div>
|