diff --git a/frontend/src/api/faces.ts b/frontend/src/api/faces.ts index a7fbe17..766f5ab 100644 --- a/frontend/src/api/faces.ts +++ b/frontend/src/api/faces.ts @@ -156,6 +156,8 @@ export const facesApi = { date_processed_to?: string sort_by?: 'quality' | 'date_taken' | 'date_added' sort_dir?: 'asc' | 'desc' + tag_names?: string + match_all?: boolean }): Promise => { const response = await apiClient.get('/api/v1/faces/unidentified', { params, diff --git a/frontend/src/pages/AutoMatch.tsx b/frontend/src/pages/AutoMatch.tsx index 338655a..8afa4ab 100644 --- a/frontend/src/pages/AutoMatch.tsx +++ b/frontend/src/pages/AutoMatch.tsx @@ -2,10 +2,12 @@ import { useState, useEffect, useMemo } from 'react' import facesApi, { AutoMatchResponse, AutoMatchPersonItem, AutoMatchFaceItem } from '../api/faces' import peopleApi from '../api/people' import { apiClient } from '../api/client' +import { useDeveloperMode } from '../context/DeveloperModeContext' const DEFAULT_TOLERANCE = 0.6 export default function AutoMatch() { + const { isDeveloperMode } = useDeveloperMode() const [tolerance, setTolerance] = useState(DEFAULT_TOLERANCE) const [autoAcceptThreshold, setAutoAcceptThreshold] = useState(70) const [isActive, setIsActive] = useState(false) @@ -19,7 +21,6 @@ export default function AutoMatch() { const [saving, setSaving] = useState(false) const [hasNoResults, setHasNoResults] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) - const [showHelpTooltip, setShowHelpTooltip] = useState(false) const currentPerson = useMemo(() => { const activePeople = filteredPeople.length > 0 ? filteredPeople : people @@ -260,20 +261,22 @@ export default function AutoMatch() { > {isRefreshing ? 'Refreshing...' : '🔄 Refresh'} -
- - setTolerance(parseFloat(e.target.value) || 0)} - disabled={busy} - className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" - /> - (lower = stricter matching) -
+ {isDeveloperMode && ( +
+ + setTolerance(parseFloat(e.target.value) || 0)} + disabled={busy} + className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" + /> + (lower = stricter matching) +
+ )}
-
- - {showHelpTooltip && ( -
-
-
Auto-Match Criteria:
-
-
Face Pose:
-
- • Reference face: Frontal or tilted (not profile) -
- • Match face: Frontal or tilted (not profile) -
-
-
-
Similarity Threshold:
-
- • Minimum: {autoAcceptThreshold}% similarity -
- • Only matches ≥ {autoAcceptThreshold}% will be auto-accepted -
-
-
- Note: Profile faces are excluded for better accuracy -
-
-
-
- )} +
+ {isDeveloperMode && ( +
+ + setAutoAcceptThreshold(parseInt(e.target.value) || 70)} + disabled={busy || hasNoResults} + className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" + /> + % (min similarity)
-
-
- - setAutoAcceptThreshold(parseInt(e.target.value) || 70)} - disabled={busy || hasNoResults} - className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" - /> - % (min similarity) -
+ )}
- ℹ️ Auto-Match Criteria: Only frontal or tilted faces (not profile) with similarity ≥ {autoAcceptThreshold}% will be auto-accepted. Click the info icon for details. + ℹ️ Auto-Match Criteria: Only faces with similarity higher than 70% will be auto-matched. Profile faces are excluded for better accuracy.
@@ -574,11 +527,6 @@ export default function AutoMatch() { )} - {!isActive && ( -
-

Click "Start Auto-Match" to begin matching unidentified faces with identified people.

-
- )} ) } diff --git a/frontend/src/pages/Identify.tsx b/frontend/src/pages/Identify.tsx index 2c93216..440be6e 100644 --- a/frontend/src/pages/Identify.tsx +++ b/frontend/src/pages/Identify.tsx @@ -2,11 +2,14 @@ import { useEffect, useMemo, useState, useRef } from 'react' import facesApi, { FaceItem, SimilarFaceItem } from '../api/faces' import peopleApi, { Person } from '../api/people' import { apiClient } from '../api/client' +import tagsApi, { TagResponse } from '../api/tags' +import { useDeveloperMode } from '../context/DeveloperModeContext' type SortBy = 'quality' | 'date_taken' | 'date_added' type SortDir = 'asc' | 'desc' export default function Identify() { + const { isDeveloperMode } = useDeveloperMode() const [faces, setFaces] = useState([]) const [, setTotal] = useState(0) const [pageSize, setPageSize] = useState(50) @@ -33,8 +36,11 @@ export default function Identify() { const [dob, setDob] = useState('') const [busy, setBusy] = useState(false) const [imageLoading, setImageLoading] = useState(false) - const [filtersCollapsed, setFiltersCollapsed] = useState(false) + const [filtersCollapsed, setFiltersCollapsed] = useState(true) const [loadingFaces, setLoadingFaces] = useState(false) + const [availableTags, setAvailableTags] = useState([]) + const [selectedTags, setSelectedTags] = useState([]) + const [tagsExpanded, setTagsExpanded] = useState(false) // Store form data per face ID (matching desktop behavior) const [faceFormData, setFaceFormData] = useState 0 ? selectedTags.join(', ') : undefined, + match_all: false, // Default to match any tag }) // Apply unique faces filter if enabled @@ -200,6 +208,15 @@ export default function Identify() { setPeople(res.items) } + const loadTags = async () => { + try { + const res = await tagsApi.list() + setAvailableTags(res.items) + } catch (error) { + console.error('Error loading tags:', error) + } + } + const loadSimilar = async (faceId: number) => { if (!compareEnabled) { setSimilar([]) @@ -220,6 +237,7 @@ export default function Identify() { initialLoadRef.current = true loadFaces() loadPeople() + loadTags() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -232,6 +250,14 @@ export default function Identify() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [uniqueFacesOnly]) + // Reload when pageSize changes (immediate reload) + useEffect(() => { + if (initialLoadRef.current) { + loadFaces() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageSize]) + useEffect(() => { if (currentFace) { setImageLoading(true) // Show loading indicator when face changes @@ -398,20 +424,33 @@ export default function Identify() {
{/* Left: Controls and current face */}
- {/* Unique Faces Checkbox - Outside Filters */} + {/* Unique Faces Checkbox and Batch Size - Outside Filters */}
- -

- Hide duplicates with ≥60% match confidence -

+
+
+ +

+ Hide duplicates with ≥60% match confidence +

+
+
+ + +
+
@@ -437,15 +476,6 @@ export default function Identify() { onChange={(e) => setMinQuality(parseFloat(e.target.value))} className="w-full" />
{(minQuality * 100).toFixed(0)}%
-
- - -
setDateFrom(e.target.value)} @@ -474,6 +504,51 @@ export default function Identify() {
+
+
+

With Tags

+ +
+ {tagsExpanded && ( +
+
+ + +
+

+ Hold Ctrl/Cmd to select multiple tags +

+
+ )} +
{/* Pose mode display */} - {currentFace.pose_mode && ( + {isDeveloperMode && currentFace.pose_mode && (
Pose: {currentFace.pose_mode}
@@ -738,7 +813,7 @@ export default function Identify() {
{/* Pose mode */} - {s.pose_mode && ( + {isDeveloperMode && s.pose_mode && (
{s.pose_mode}
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index ee656d5..e8d3f49 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -523,9 +523,6 @@ export default function Search() { {tagsExpanded && (
-