From 7945b084a47517ceb76f5e2d628a0db49e9126b5 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Tue, 4 Nov 2025 13:30:40 -0500 Subject: [PATCH] feat: Enhance navigation and filtering in AutoMatch and Identify components This commit introduces new navigation buttons in the AutoMatch component, allowing users to easily move between persons being matched. Additionally, the Identify component has been updated to include a collapsible filters section, improving the user interface for managing face identification. The unique faces filter is now enabled by default, enhancing the identification process. Documentation and tests have been updated to reflect these changes, ensuring a reliable user experience. --- frontend/src/pages/AutoMatch.tsx | 21 +++++++++ frontend/src/pages/Identify.tsx | 68 ++++++++++++++++++------------ src/core/search_stats.py | 4 ++ src/web/services/search_service.py | 2 + 4 files changed, 68 insertions(+), 27 deletions(-) diff --git a/frontend/src/pages/AutoMatch.tsx b/frontend/src/pages/AutoMatch.tsx index 81e2acf..aab1783 100644 --- a/frontend/src/pages/AutoMatch.tsx +++ b/frontend/src/pages/AutoMatch.tsx @@ -233,6 +233,27 @@ export default function AutoMatch() { {currentPerson && ( <>
+
+
+ Person {currentIndex + 1} of {activePeople.length} +
+
+ + +
+

👤 Person: {currentPerson.person_name}

📁 Photo: {currentPerson.reference_photo_filename} diff --git a/frontend/src/pages/Identify.tsx b/frontend/src/pages/Identify.tsx index 6657fbb..9f21da6 100644 --- a/frontend/src/pages/Identify.tsx +++ b/frontend/src/pages/Identify.tsx @@ -22,7 +22,7 @@ export default function Identify() { const [similar, setSimilar] = useState([]) const [compareEnabled, setCompareEnabled] = useState(true) const [selectedSimilar, setSelectedSimilar] = useState>({}) - const [uniqueFacesOnly, setUniqueFacesOnly] = useState(false) + const [uniqueFacesOnly, setUniqueFacesOnly] = useState(true) const [people, setPeople] = useState([]) const [personId, setPersonId] = useState(undefined) @@ -33,6 +33,7 @@ export default function Identify() { const [dob, setDob] = useState('') const [busy, setBusy] = useState(false) const [imageLoading, setImageLoading] = useState(false) + const [filtersCollapsed, setFiltersCollapsed] = useState(false) // Store form data per face ID (matching desktop behavior) const [faceFormData, setFaceFormData] = useState { const onKey = (e: KeyboardEvent) => { - if (e.key.toLowerCase() === 'j') { - setCurrentIdx((i) => Math.min(i + 1, Math.max(0, faces.length - 1))) - } else if (e.key.toLowerCase() === 'k') { - setCurrentIdx((i) => Math.max(i - 1, 0)) - } else if (e.key === 'Enter' && canIdentify) { + if (e.key === 'Enter' && canIdentify) { handleIdentify() } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) - }, [faces.length, canIdentify]) + }, [canIdentify]) const handleIdentify = async () => { if (!currentFace) return @@ -349,8 +346,23 @@ export default function Identify() {

{/* Left: Controls and current face */}
-
-
+
+
setFiltersCollapsed(!filtersCollapsed)}> +

Filters

+ +
+ {!filtersCollapsed && ( +
+
-
- -

- Hide duplicates with ≥60% match confidence -

-
+
+ +

+ Hide duplicates with ≥60% match confidence +

+
+
+ )}
{currentInfo}
- - + +
{!currentFace ? ( @@ -549,8 +563,8 @@ export default function Identify() { className={`px-3 py-2 rounded text-white ${canIdentify && !busy ? 'bg-indigo-600 hover:bg-indigo-700' : 'bg-gray-400 cursor-not-allowed'}`}> {busy ? 'Identifying...' : 'Identify (Enter)'} - - + +
diff --git a/src/core/search_stats.py b/src/core/search_stats.py index ae86673..0f07dbb 100644 --- a/src/core/search_stats.py +++ b/src/core/search_stats.py @@ -335,6 +335,8 @@ class SearchStats: def get_photos_without_faces(self) -> List[Tuple]: """Get photos that have no detected faces + Only includes processed photos (photos that have been processed for face detection). + Returns: List of tuples: (photo_path, filename) """ @@ -343,11 +345,13 @@ class SearchStats: with self.db.get_db_connection() as conn: cursor = conn.cursor() # Find photos that have no faces associated with them + # Only include processed photos cursor.execute(''' SELECT p.path, p.filename FROM photos p LEFT JOIN faces f ON p.id = f.photo_id WHERE f.photo_id IS NULL + AND p.processed = 1 ORDER BY p.filename ''') for row in cursor.fetchall(): diff --git a/src/web/services/search_service.py b/src/web/services/search_service.py index 7379072..d47b6f0 100644 --- a/src/web/services/search_service.py +++ b/src/web/services/search_service.py @@ -182,12 +182,14 @@ def get_photos_without_faces( ) -> Tuple[List[Photo], int]: """Get photos that have no detected faces. + Only includes processed photos (photos that have been processed for face detection). Matches desktop behavior exactly. """ query = ( db.query(Photo) .outerjoin(Face, Photo.id == Face.photo_id) .filter(Face.photo_id.is_(None)) + .filter(Photo.processed == True) # Only include processed photos ) # Apply folder filter if provided