Skip to content

Conversation

@DeveloperAmrit
Copy link

@DeveloperAmrit DeveloperAmrit commented Jan 6, 2026

Fixes #905

Relevant Changes

  • Made backend handler for detecting duplicate images
  • Made backend handler for detecting best shot out of duplicates
  • Made a duplicates page in frontend to show duplicates and best shot

Additional changes

  • Tweaked a minor bug in backend/app/database/images.py which was causing AI tagging and image upload to stop at 0%

Video - https://res.cloudinary.com/db6cpegxg/video/upload/v1767806671/Screen_Recording_2026-01-07_at_10.48.35_PM_bfxczl.mov

Summary by CodeRabbit

  • New Features

    • Added duplicate image detection and management system with a new Duplicates section accessible from the sidebar
    • Users can view grouped duplicate images, browse them in an interactive gallery viewer, and select duplicates for deletion
    • Automatic identification of best-quality images within duplicate groups
  • Chores

    • Added new dependency for image processing capabilities

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions github-actions bot added backend enhancement New feature or request frontend labels Jan 6, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

This PR implements duplicate image detection and "best shot" selection. It adds perceptual hashing to the database layer, introduces image similarity analysis using ORB features and dHash clustering in a new utility module, and provides REST API endpoints and a comprehensive UI for viewing duplicate image groups and deleting selected duplicates.

Changes

Cohort / File(s) Summary
Database Enhancement
backend/app/database/images.py
Added phash field to ImageRecord schema; persisted in bulk insert/upsert operations. Introduced two new retrieval functions: db_get_all_images_with_phash() to fetch all images with non-null perceptual hash, and db_get_images_by_ids() to retrieve image paths and thumbnails by ID list.
Image Processing Utilities
backend/app/utils/duplicate_detector.py, backend/app/utils/images.py
New module duplicate_detector.py implements visual duplicate detection: sharpness scoring via Laplacian variance, geometric similarity using ORB feature matching, perceptual hashing, best-shot selection by sharpness/file-size, and greedy image grouping by dHash similarity with geometric verification. Modified images.py to compute and persist phash when preparing image records.
Backend API Layer
backend/app/routes/duplicates.py, backend/main.py, backend/requirements.txt
New router module with two endpoints: GET /duplicates/ to retrieve grouped duplicate images with best-shot indicators, and POST /duplicates/delete to delete specified images and their files. Registered router in FastAPI app. Added ImageHash==4.3.1 dependency.
Frontend Route Configuration
frontend/src/constants/routes.ts, frontend/src/api/apiEndpoints.ts
Added ROUTES.DUPLICATES constant and new duplicatesEndpoints object exposing getDuplicates and deleteDuplicates API paths.
Frontend Navigation & Pages
frontend/src/components/Navigation/Sidebar/AppSidebar.tsx, frontend/src/pages/DuplicatePage/DuplicatePage.tsx, frontend/src/routes/AppRoutes.tsx
Added Duplicates menu item to sidebar. New DuplicatePage component displays duplicate image groups in grid layout, supports checkbox-based selection, per-image thumbnail preview with modal gallery viewer (keyboard navigation with arrow keys), and delete confirmation dialog. Registered route in AppRoutes.
Documentation
docs/backend/backend_python/openapi.json
Updated OpenAPI schema with two new Duplicates endpoints: GET and POST paths with request/response schemas and error handling definitions.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Frontend UI
    participant API as Backend API
    participant Detector as Image Processor
    participant DB as Database
    participant FS as File System

    rect rgb(100, 150, 200)
    Note over User,FS: Duplicate Detection & Retrieval Flow
    User->>Frontend: Request duplicates
    Frontend->>API: GET /duplicates/
    API->>DB: db_get_all_images_with_phash()
    DB-->>API: Images with phash
    API->>Detector: group_similar_images(images)
    Detector->>Detector: Compute dHash for each<br/>Estimate sharpness<br/>Cluster by hash proximity
    Detector->>Detector: Geometric verification<br/>of similar pairs
    Detector->>Detector: identify_best_shot()<br/>per group
    Detector-->>API: Grouped images + best_shot_id
    API-->>Frontend: Duplicate groups
    Frontend-->>User: Display groups with thumbnails
    end

    rect rgb(150, 100, 100)
    Note over User,FS: Deletion Flow
    User->>Frontend: Select images & confirm delete
    Frontend->>API: POST /duplicates/delete<br/>(image_ids)
    API->>DB: db_get_images_by_ids(ids)
    DB-->>API: Image records with paths
    API->>DB: Delete records
    API->>FS: Delete image files<br/>& thumbnails
    FS-->>API: File deletion complete
    API-->>Frontend: {deleted_count, deleted_files_count}
    Frontend->>Frontend: Refresh duplicate groups
    Frontend-->>User: Updated view
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement, UI, frontend

Suggested reviewers

  • rahulharpal1603

Poem

🐰 A bunny hops through photos, spotting duplicates with glee,
Finds the sharpest sunset (the best shot, naturally!),
ORB features dance, dHash clusters align,
"Delete the blurry ones!" the user says with a sign,
Now galleries shine, duplicates gone—a hoppy design! 🌅✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main feature: implementing duplicate detection and best shot selection.
Linked Issues check ✅ Passed All core objectives from issue #905 are met: duplicate detection via phash and geometric similarity [905], best shot selection via sharpness metrics [905], and frontend UI for duplicates display [905].
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue #905; no extraneous modifications detected beyond duplicate/best shot detection and presentation.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI Agents
In @backend/app/routes/duplicates.py:
- Around line 70-72: The handler in delete_duplicates currently logs the
exception message and returns str(e) in the HTTPException detail, which may leak
sensitive internals; change the code to log the full exception internally (use
logger.error(..., exc_info=True) or similar) and raise
HTTPException(status_code=500, detail="Internal server error") or another
generic, non-sensitive message instead of str(e), keeping the logger call
referencing the delete_duplicates context and the raised HTTPException for the
same error path.

In @backend/app/utils/images.py:
- Around line 22-24: There are two logger definitions: logger =
get_logger(__name__) and later logger = logging.getLogger(__name__), which
shadows the custom get_logger; remove the redundant logging.getLogger assignment
so the module consistently uses logger from get_logger(__name__) (ensure any
imports still used like get_logger remain and that references to logger
elsewhere in the file use the single definition).

In @backend/requirements.txt:
- Line 74: Update the ImageHash dependency line in requirements.txt from
"ImageHash==4.3.1" to "ImageHash==4.3.2": locate the ImageHash entry (the
"ImageHash==4.3.1" token) and change the pinned version to 4.3.2, then run your
dependency install/lock workflow (pip install -r requirements.txt or regenerate
lockfile) to ensure the new version is applied.

In @frontend/src/pages/DuplicatePage/DuplicatePage.tsx:
- Around line 200-202: The filename extraction using img.path.split('/').pop()
is brittle on Windows; change it to a cross-platform splitter or utility (e.g.,
replace the split with img.path.split(/[/\\]/).pop() or create a
getFilename(path) helper used where img.path is rendered at the two spots in
DuplicatePage) so backslashes are handled correctly and both locations (the
img.path usage around the thumbnail captions) use the same robust function.
- Around line 97-116: The effect uses navigateImage but doesn't include it in
the dependency array; wrap navigateImage in useCallback (so it preserves
identity unless its own dependencies change) and then add navigateImage to the
useEffect dependency array alongside viewingImage and groups; ensure the
useCallback's dependency list includes any values navigateImage uses (e.g.,
viewingImage, groups, setViewingImage) to avoid stale closures.
- Around line 208-262: The image preview modal JSX (the block that checks
viewingImage and renders the overlay, including handlers handlePrev/handleNext
and the close button that calls setViewingImage) is currently inside the
groups.map() callback and is therefore duplicated and nested incorrectly; move
that entire modal block so it lives once at the component root (outside the
groups.map), remove the stray closing </div> that currently closes the group
container incorrectly, and ensure the modal still references viewingImage,
convertFileSrc(viewingImage.path), handlePrev, handleNext and setViewingImage so
its behavior is unchanged.
🧹 Nitpick comments (6)
frontend/src/pages/DuplicatePage/DuplicatePage.tsx (1)

8-13: Consider reusing the existing Image interface.

A similar Image interface exists in frontend/src/types/Media.ts. Consider extending or reusing it to maintain type consistency across the codebase, adding the phash field if needed.

🔎 Proposed approach
import { Image as BaseImage } from '@/types/Media';

interface DuplicateImage extends Pick<BaseImage, 'id' | 'path' | 'thumbnailPath'> {
  phash: string;
}
docs/backend/backend_python/openapi.json (1)

1334-1382: Consider adding maxItems to the request body array.

The request accepts an unbounded array of image IDs. While this is a low-risk internal API, adding a reasonable maxItems limit (e.g., 1000) would provide some protection against accidental or malicious large payloads.

backend/app/routes/duplicates.py (1)

25-25: Hardcoded threshold differs from module constant.

The threshold is hardcoded as 5 here, but HASH_THRESHOLD in duplicate_detector.py is 8. Consider using the constant for consistency, or document why a different value is intentional.

🔎 Proposed fix
+from app.utils.duplicate_detector import identify_best_shot, group_similar_images, HASH_THRESHOLD
 ...
-        groups = group_similar_images(all_images, threshold=5)
+        groups = group_similar_images(all_images, threshold=HASH_THRESHOLD)

Or if 5 is intentional for stricter matching, add a comment explaining the choice.

backend/app/utils/duplicate_detector.py (3)

1-10: Unused import: json.

The json module is imported but never used in this file.

🔎 Proposed fix
 import imagehash
 from PIL import Image
 import os
-import json
 import cv2

80-91: Use context manager for PIL Image.

The image is opened but not explicitly closed, which could lead to resource leaks when processing many images.

🔎 Proposed fix
 def calculate_phash(image_path: str) -> Optional[str]:
     """
     Calculate perceptual hash for an image.
     """
     try:
-        img = Image.open(image_path)
-        # phash is generally good for finding duplicates including resized/compressed ones
-        hash_obj = imagehash.phash(img)
-        return str(hash_obj)
+        with Image.open(image_path) as img:
+            # phash is generally good for finding duplicates including resized/compressed ones
+            hash_obj = imagehash.phash(img)
+            return str(hash_obj)
     except Exception as e:
         logger.error(f"Error calculating pHash for {image_path}: {e}")
         return None

124-145: Clean up confusing comments and use context manager for PIL Image.

The comments at lines 125-130 discuss whether to use pHash or dHash but don't reach a clear conclusion. The code then computes dHash regardless. Additionally, the PIL Image should use a context manager.

🔎 Proposed fix
     for img in images:
-        if img.get('phash'): # We are technically using the pHash from DB if available, or calculating on fly
-             # If we want to switch to dHash strictly we might need to re-compute.
-             # For now let's reuse the stored hash as a first pass filter if possible,
-             # OR strictly compute dHash now for better burst mode detection.
-             # Given the context, let's calculate dHash on fly for high accuracy as requested.
-             pass
-        
         path = img.get('path')
         if not path or not os.path.exists(path):
             continue
             
         try:
-             # Calculate dHash on the fly for heavy logic mode
-             pil_img = Image.open(path)
-             dhash = imagehash.dhash(pil_img, hash_size=HASH_SIZE)
-             img['hash_obj'] = dhash
-             # Compute sharpness now to save time later
-             img['sharpness_score'] = get_image_sharpness(path)
-             processed_images.append(img)
+             # Calculate dHash on-the-fly for burst mode detection accuracy
+             with Image.open(path) as pil_img:
+                 dhash = imagehash.dhash(pil_img, hash_size=HASH_SIZE)
+                 img['hash_obj'] = dhash
+             # Compute sharpness now to save time later
+             img['sharpness_score'] = get_image_sharpness(path)
+             processed_images.append(img)
         except Exception as e:
              logger.warning(f"Error processing image {path}: {e}")
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bdf1d3 and ad449b2.

📒 Files selected for processing (12)
  • backend/app/database/images.py
  • backend/app/routes/duplicates.py
  • backend/app/utils/duplicate_detector.py
  • backend/app/utils/images.py
  • backend/main.py
  • backend/requirements.txt
  • docs/backend/backend_python/openapi.json
  • frontend/src/api/apiEndpoints.ts
  • frontend/src/components/Navigation/Sidebar/AppSidebar.tsx
  • frontend/src/constants/routes.ts
  • frontend/src/pages/DuplicatePage/DuplicatePage.tsx
  • frontend/src/routes/AppRoutes.tsx
🧰 Additional context used
🧬 Code graph analysis (6)
frontend/src/pages/DuplicatePage/DuplicatePage.tsx (2)
frontend/src/types/Media.ts (1)
  • Image (13-23)
frontend/src/api/apiEndpoints.ts (1)
  • duplicatesEndpoints (34-37)
frontend/src/routes/AppRoutes.tsx (2)
frontend/src/constants/routes.ts (1)
  • ROUTES (1-13)
frontend/src/pages/DuplicatePage/DuplicatePage.tsx (1)
  • DuplicatePage (20-266)
backend/app/utils/duplicate_detector.py (1)
frontend/src/types/Media.ts (1)
  • Image (13-23)
backend/app/routes/duplicates.py (2)
backend/app/database/images.py (3)
  • db_get_all_images_with_phash (424-457)
  • db_delete_images_by_ids (367-399)
  • db_get_images_by_ids (459-472)
backend/app/utils/duplicate_detector.py (2)
  • identify_best_shot (93-114)
  • group_similar_images (116-185)
frontend/src/components/Navigation/Sidebar/AppSidebar.tsx (1)
frontend/src/constants/routes.ts (1)
  • ROUTES (1-13)
backend/app/utils/images.py (1)
backend/app/utils/duplicate_detector.py (1)
  • calculate_phash (80-91)
🪛 Checkov (3.2.334)
docs/backend/backend_python/openapi.json

[medium] 1321-1327: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (14)
backend/main.py (1)

28-28: LGTM! Duplicates router properly integrated.

The duplicates router is correctly imported and registered following the established pattern of other routers in the application.

Also applies to: 134-134

frontend/src/routes/AppRoutes.tsx (1)

12-12: LGTM! Duplicates route properly configured.

The new duplicates route is correctly implemented following the existing routing pattern, properly nested within the Layout wrapper for authenticated pages.

Also applies to: 27-27

frontend/src/constants/routes.ts (1)

12-12: LGTM! Route constant properly defined.

The DUPLICATES route constant follows the established naming convention and is properly integrated into the ROUTES object.

frontend/src/components/Navigation/Sidebar/AppSidebar.tsx (1)

19-19: LGTM! Sidebar menu item properly implemented.

The Duplicates menu item is correctly added with appropriate icon (Copy from lucide-react) and follows the established pattern for sidebar navigation items.

Also applies to: 56-56

frontend/src/api/apiEndpoints.ts (1)

33-37: LGTM!

The new duplicatesEndpoints object follows the established pattern and aligns with the backend API routes.

backend/app/utils/images.py (1)

169-179: LGTM!

The phash integration is well-placed after successful thumbnail generation. The nullable phash field handles cases where hash computation fails gracefully.

backend/app/database/images.py (5)

30-30: LGTM!

The phash field is correctly typed as Union[str, None] to match the nullable database column.


68-68: Verify migration strategy for existing images.

New images will have phash computed during insertion, but existing images in the database will have phash = NULL. The duplicates feature will only detect duplicates among images with non-null phash values.

Consider adding a migration or background task to compute phash for existing images, or document this limitation.


102-112: LGTM!

The INSERT statement correctly includes phash, and the ON CONFLICT clause properly updates the phash value during upserts while preserving the conditional isTagged logic.


424-457: LGTM!

The function correctly retrieves all images with non-null phash values, has proper error handling, and ensures connection cleanup in the finally block.


459-472: LGTM!

The function follows established patterns with parameterized queries, proper error handling, and connection cleanup.

docs/backend/backend_python/openapi.json (1)

1308-1332: LGTM!

The GET /duplicates/ endpoint is correctly documented. The response schema could benefit from a more detailed definition (like other endpoints use $ref to named schemas), but the current auto-generated schema is functional.

backend/app/routes/duplicates.py (1)

11-39: LGTM for the endpoint structure.

The endpoint correctly fetches images, groups them, identifies best shots, and returns structured results with proper error handling.

backend/app/utils/duplicate_detector.py (1)

116-185: Algorithm is well-designed with appropriate safeguards.

The greedy clustering approach with hash-based fast filtering and ORB verification for accuracy is a good balance. The sharpness pre-sort ensures high-quality images become cluster representatives. Error handling and cleanup of temporary attributes are properly implemented.

Note: For very large image collections, the O(n × clusters × ORB_cost) complexity could become slow. Consider adding a comment about expected performance characteristics or adding batch size limits if this becomes an issue in production.

Comment on lines +70 to +72
except Exception as e:
logger.error(f"Error in delete_duplicates: {e}")
raise HTTPException(status_code=500, detail=str(e))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid exposing raw exception details in API response.

Using str(e) in the error detail could expose internal paths, database errors, or other sensitive information to API consumers.

🔎 Proposed fix
     except Exception as e:
         logger.error(f"Error in delete_duplicates: {e}")
-        raise HTTPException(status_code=500, detail=str(e))
+        raise HTTPException(status_code=500, detail="Failed to delete images")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
logger.error(f"Error in delete_duplicates: {e}")
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
logger.error(f"Error in delete_duplicates: {e}")
raise HTTPException(status_code=500, detail="Failed to delete images")
🤖 Prompt for AI Agents
In @backend/app/routes/duplicates.py around lines 70 - 72, The handler in
delete_duplicates currently logs the exception message and returns str(e) in the
HTTPException detail, which may leak sensitive internals; change the code to log
the full exception internally (use logger.error(..., exc_info=True) or similar)
and raise HTTPException(status_code=500, detail="Internal server error") or
another generic, non-sensitive message instead of str(e), keeping the logger
call referencing the delete_duplicates context and the raised HTTPException for
the same error path.

Comment on lines +22 to 24
from app.utils.duplicate_detector import calculate_phash

logger = get_logger(__name__)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate logger definition detected.

Line 24 defines logger = get_logger(__name__), but line 30 redefines it as logger = logging.getLogger(__name__). This shadows the first definition and may cause inconsistent logging behavior if get_logger provides custom formatting or handlers.

🔎 Proposed fix

Remove the duplicate definition at line 30:

 logger = get_logger(__name__)
 
 
 # GPS EXIF tag constant
 GPS_INFO_TAG = 34853
 
-logger = logging.getLogger(__name__)
-

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @backend/app/utils/images.py around lines 22 - 24, There are two logger
definitions: logger = get_logger(__name__) and later logger =
logging.getLogger(__name__), which shadows the custom get_logger; remove the
redundant logging.getLogger assignment so the module consistently uses logger
from get_logger(__name__) (ensure any imports still used like get_logger remain
and that references to logger elsewhere in the file use the single definition).

psutil>=5.9.5
pytest-asyncio>=1.0.0
setuptools==66.1.1
ImageHash==4.3.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check ImageHash package version information and security advisories

echo "=== Checking available ImageHash versions (last 10) ==="
curl -s https://pypi.org/pypi/ImageHash/json | jq -r '.releases | keys[]' | sort -V | tail -10

echo -e "\n=== Latest version ==="
curl -s https://pypi.org/pypi/ImageHash/json | jq -r '.info.version'

echo -e "\n=== Release date of version 4.3.1 ==="
curl -s https://pypi.org/pypi/ImageHash/4.3.1/json | jq -r '.releases' 2>/dev/null || curl -s https://pypi.org/pypi/ImageHash/json | jq -r '.releases."4.3.1"[0].upload_time' 2>/dev/null

echo -e "\n=== Check if version 4.3.1 exists ==="
curl -s https://pypi.org/pypi/ImageHash/4.3.1/json | jq -r '.info.version' 2>/dev/null

Repository: AOSSIE-Org/PictoPy

Length of output: 288


🌐 Web query:

ImageHash security vulnerabilities CVE

💡 Result:

Short answer: There are no public CVEs that specifically name the Python ImageHash package. Snyk’s vulnerability DB lists no direct vulnerabilities for ImageHash, and PyPI shows the project but no CVE entries. [1][2]

Context / related notes:

  • Image-processing libraries commonly do have CVEs (e.g., ImageMagick and others), so risks often come from image decoders or native libs you use alongside ImageHash (not ImageHash itself). Examples: ImageMagick CVEs tracked by Ubuntu, and a recent stb_image CVE. [3][4]

If you want, I can:

  • search more exhaustively (NVD, GitHub advisories, OSS‑DB) and return any CVEs that mention ImageHash by name; or
  • scan a project that uses ImageHash for known-vulnerable dependencies. Which would you prefer?

🌐 Web query:

ImageHash package security advisories

💡 Result:

Short answer: The ImageHash project itself has no published security advisories; its GitHub security page shows no advisories and no SECURITY.md. However, ImageHash depends on image libraries (notably Pillow), which have had several CVEs (e.g., CVE-2022-22817, CVE-2021-28675). You should track Pillow (and other imaging deps) for security fixes and keep ImageHash and its dependencies up to date. [1][2][3][4]

Sources:

  • ImageHash GitHub security overview (no advisories). [1]
  • Snyk package page for ImageHash (no direct vulnerabilities recorded). [2]
  • CVE-2022-22817 (Pillow ImageMath.eval arbitrary expression). [3]
  • CVE-2021-28675 (Pillow PSD plugin issue). [4]

Update ImageHash to version 4.3.2 (latest available).

Version 4.3.1 is valid and free of known vulnerabilities, but version 4.3.2 is now available. ImageHash itself has no published CVEs; however, its dependency on Pillow means security updates to image libraries should be monitored and kept current.

🤖 Prompt for AI Agents
In @backend/requirements.txt at line 74, Update the ImageHash dependency line in
requirements.txt from "ImageHash==4.3.1" to "ImageHash==4.3.2": locate the
ImageHash entry (the "ImageHash==4.3.1" token) and change the pinned version to
4.3.2, then run your dependency install/lock workflow (pip install -r
requirements.txt or regenerate lockfile) to ensure the new version is applied.

Comment on lines +97 to +116
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!viewingImage) return;

switch (e.key) {
case 'ArrowLeft':
navigateImage('prev');
break;
case 'ArrowRight':
navigateImage('next');
break;
case 'Escape':
setViewingImage(null);
break;
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [viewingImage, groups]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

navigateImage should be in the useEffect dependency array.

The navigateImage function is called inside the effect but not listed as a dependency. This can cause stale closure issues where viewingImage or groups values become outdated.

🔎 Proposed fix

Wrap navigateImage in useCallback and add it to dependencies:

+import React, { useEffect, useState, useCallback } from 'react';
...
-  const navigateImage = (direction: 'prev' | 'next') => {
+  const navigateImage = useCallback((direction: 'prev' | 'next') => {
     if (!viewingImage) return;
     for (const group of groups) {
       const idx = group.images.findIndex(img => img.id === viewingImage.id);
       if (idx !== -1) {
         let newIdx;
         if (direction === 'prev') {
           newIdx = idx === 0 ? group.images.length - 1 : idx - 1;
         } else {
           newIdx = (idx + 1) % group.images.length;
         }
         setViewingImage(group.images[newIdx]);
         return;
       }
     }
-  };
+  }, [viewingImage, groups]);

   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
       ...
     };
     window.addEventListener('keydown', handleKeyDown);
     return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [viewingImage, groups]);
+  }, [viewingImage, navigateImage]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @frontend/src/pages/DuplicatePage/DuplicatePage.tsx around lines 97 - 116,
The effect uses navigateImage but doesn't include it in the dependency array;
wrap navigateImage in useCallback (so it preserves identity unless its own
dependencies change) and then add navigateImage to the useEffect dependency
array alongside viewingImage and groups; ensure the useCallback's dependency
list includes any values navigateImage uses (e.g., viewingImage, groups,
setViewingImage) to avoid stale closures.

Comment on lines +200 to +202
<div className="absolute bottom-0 left-0 right-0 bg-black/60 p-2 text-xs text-white truncate">
{img.path.split('/').pop()}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Path splitting may fail on Windows.

Using path.split('/').pop() assumes Unix-style paths. On Windows, paths use backslashes and this will return the entire path string instead of the filename.

🔎 Proposed fix

Use a cross-platform approach:

-  {img.path.split('/').pop()}
+  {img.path.split(/[\\/]/).pop()}

Or extract to a utility function for reuse at lines 201 and 240.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="absolute bottom-0 left-0 right-0 bg-black/60 p-2 text-xs text-white truncate">
{img.path.split('/').pop()}
</div>
<div className="absolute bottom-0 left-0 right-0 bg-black/60 p-2 text-xs text-white truncate">
{img.path.split(/[\\/]/).pop()}
</div>
🤖 Prompt for AI Agents
In @frontend/src/pages/DuplicatePage/DuplicatePage.tsx around lines 200 - 202,
The filename extraction using img.path.split('/').pop() is brittle on Windows;
change it to a cross-platform splitter or utility (e.g., replace the split with
img.path.split(/[/\\]/).pop() or create a getFilename(path) helper used where
img.path is rendered at the two spots in DuplicatePage) so backslashes are
handled correctly and both locations (the img.path usage around the thumbnail
captions) use the same robust function.

Comment on lines +208 to +262
{/* Image Preview Modal */}
{viewingImage && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 p-4 animate-in fade-in duration-200"
onClick={() => setViewingImage(null)}
>
<div className="relative max-w-[95vw] max-h-[95vh] flex flex-col items-center justify-center" onClick={(e) => e.stopPropagation()}>
<div className="absolute top-2 left-2 z-50 bg-black/60 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm shadow-lg pointer-events-none">
{(() => {
const group = groups.find(g => g.images.some(i => i.id === viewingImage.id));
if (!group) return '';
const index = group.images.findIndex(i => i.id === viewingImage.id) + 1;
return `${index} / ${group.images.length}`;
})()}
</div>

{/* Navigation Buttons */}
<button
className="absolute left-2 md:-left-16 top-1/2 -translate-y-1/2 text-white hover:text-gray-300 transition-colors bg-white/10 hover:bg-white/20 p-2 rounded-full z-10"
onClick={handlePrev}
title="Previous Image"
>
<ChevronLeft size={32} />
</button>

<img
src={convertFileSrc(viewingImage.path)}
className="max-w-full max-h-[85vh] object-contain rounded-md shadow-2xl"
alt="Preview"
/>

<div className="mt-4 text-white text-center bg-black/60 px-4 py-2 rounded-full backdrop-blur-sm">
<p className="font-medium truncate max-w-2xl">{viewingImage.path.split('/').pop()}</p>
<p className="text-xs text-gray-300 truncate max-w-2xl">{viewingImage.path}</p>
</div>

<button
className="absolute right-2 md:-right-16 top-1/2 -translate-y-1/2 text-white hover:text-gray-300 transition-colors bg-white/10 hover:bg-white/20 p-2 rounded-full z-10"
onClick={handleNext}
title="Next Image"
>
<ChevronRight size={32} />
</button>

<button
className="absolute -top-12 right-0 md:-right-12 md:-top-0 text-white hover:text-gray-300 transition-colors bg-white/10 hover:bg-white/20 p-2 rounded-full"
onClick={() => setViewingImage(null)}
>
<X size={32} />
</button>
</div>
</div>
)}
</div>
))}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Modal is rendered inside the map loop causing incorrect DOM structure.

The image preview modal (lines 208-260) is rendered inside the groups.map() callback, which means:

  1. The modal is nested inside each group's <div> instead of at the component root
  2. It will be rendered multiple times (once per group)
  3. The closing </div> at line 261 closes the group container, not the space container
🔎 Proposed fix

Move the modal outside the map loop to render it once at the component root:

     <div className="space-y-8">
       {groups.map((group, index) => (
         <div key={index} className="bg-gray-800/50 p-4 rounded-lg border border-gray-700">
           ...
           <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
             {group.images.map((img) => {
               ...
             })}
           </div>
-
-      {/* Image Preview Modal */}
-      {viewingImage && (
-        <div ...>
-          ...
-        </div>
-      )}
         </div>
       ))}
     </div>
+
+    {/* Image Preview Modal */}
+    {viewingImage && (
+      <div 
+        className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 p-4 animate-in fade-in duration-200"
+        onClick={() => setViewingImage(null)}
+      >
+        ...
+      </div>
+    )}
   </div>
 );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @frontend/src/pages/DuplicatePage/DuplicatePage.tsx around lines 208 - 262,
The image preview modal JSX (the block that checks viewingImage and renders the
overlay, including handlers handlePrev/handleNext and the close button that
calls setViewingImage) is currently inside the groups.map() callback and is
therefore duplicated and nested incorrectly; move that entire modal block so it
lives once at the component root (outside the groups.map), remove the stray
closing </div> that currently closes the group container incorrectly, and ensure
the modal still references viewingImage, convertFileSrc(viewingImage.path),
handlePrev, handleNext and setViewingImage so its behavior is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Duplicate & "Best Shot" Detection

1 participant