sloneckity commited on
Commit
32482ae
·
1 Parent(s): 0605276

Update frontend files (script.js, style.css, index.html)

Browse files
Files changed (3) hide show
  1. static/script.js +243 -19
  2. static/style.css +85 -9
  3. 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
- // Optionally update log with snippets from data.log if desired during processing
303
- // logStatus(`[Progress ${data.progress}%] ${data.log || data.status}`);
 
 
 
 
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
- imageInfo.innerHTML = `
590
- <i class="ri-image-line"></i> ${result.filename}
591
- <br>
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
- currentZoomLevel = 1;
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 updateZoom() {
659
  if (previewImage.src) {
660
- previewImage.style.transform = `scale(${currentZoomLevel})`;
661
  }
662
  }
663
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
  zoomInBtn.addEventListener('click', () => {
665
- if (currentZoomLevel < MAX_ZOOM) {
666
- currentZoomLevel += 0.25;
667
- updateZoom();
 
 
 
 
 
 
668
  }
669
  });
670
 
 
671
  zoomOutBtn.addEventListener('click', () => {
672
- if (currentZoomLevel > MIN_ZOOM) {
673
- currentZoomLevel -= 0.25;
674
- updateZoom();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- object-fit: contain;
190
- transition: transform 0.2s ease;
 
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-color: var(--text-color);
412
  color: white;
413
- font-size: 0.75rem;
414
- border-radius: 0.25rem;
415
- white-space: nowrap;
416
- opacity: 0;
 
 
 
417
  pointer-events: none;
418
- transition: opacity 0.15s ease;
 
 
 
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" data-tooltip="Higher values mean more confident detections">
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
- <span id="progress-percentage">0%</span>
 
 
 
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>