diff --git a/frontend/src/api/faces.ts b/frontend/src/api/faces.ts index b1b3d58..63498ac 100644 --- a/frontend/src/api/faces.ts +++ b/frontend/src/api/faces.ts @@ -168,6 +168,7 @@ export interface MaintenanceFaceItem { quality_score: number person_id: number | null person_name: string | null + excluded: boolean } export interface MaintenanceFacesResponse { @@ -272,6 +273,8 @@ export const facesApi = { page_size?: number min_quality?: number max_quality?: number + excluded_filter?: 'all' | 'excluded' | 'included' + identified_filter?: 'all' | 'identified' | 'unidentified' }): Promise => { const response = await apiClient.get('/api/v1/faces/maintenance', { params, diff --git a/frontend/src/pages/FacesMaintenance.tsx b/frontend/src/pages/FacesMaintenance.tsx index a076ece..ec60867 100644 --- a/frontend/src/pages/FacesMaintenance.tsx +++ b/frontend/src/pages/FacesMaintenance.tsx @@ -2,18 +2,23 @@ import { useEffect, useState, useMemo } from 'react' import facesApi, { MaintenanceFaceItem } from '../api/faces' import { apiClient } from '../api/client' -type SortColumn = 'person_name' | 'quality' +type SortColumn = 'person_name' | 'quality' | 'photo_path' | 'excluded' type SortDir = 'asc' | 'desc' +type ExcludedFilter = 'all' | 'excluded' | 'included' +type IdentifiedFilter = 'all' | 'identified' | 'unidentified' export default function FacesMaintenance() { const [faces, setFaces] = useState([]) const [total, setTotal] = useState(0) const [pageSize, setPageSize] = useState(50) const [minQuality, setMinQuality] = useState(0.0) - const [maxQuality, setMaxQuality] = useState(0.45) + const [maxQuality, setMaxQuality] = useState(1.0) + const [excludedFilter, setExcludedFilter] = useState('all') + const [identifiedFilter, setIdentifiedFilter] = useState('all') const [selectedFaces, setSelectedFaces] = useState>(new Set()) const [loading, setLoading] = useState(false) const [deleting, setDeleting] = useState(false) + const [excluding, setExcluding] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [sortColumn, setSortColumn] = useState(null) const [sortDir, setSortDir] = useState('asc') @@ -26,6 +31,8 @@ export default function FacesMaintenance() { page_size: pageSize, min_quality: minQuality, max_quality: maxQuality, + excluded_filter: excludedFilter, + identified_filter: identifiedFilter, }) setFaces(res.items) setTotal(res.total) @@ -41,7 +48,7 @@ export default function FacesMaintenance() { useEffect(() => { loadFaces() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageSize, minQuality, maxQuality]) + }, [pageSize, minQuality, maxQuality, excludedFilter, identifiedFilter]) const toggleSelection = (faceId: number) => { setSelectedFaces(prev => { @@ -88,6 +95,14 @@ export default function FacesMaintenance() { aVal = a.quality_score bVal = b.quality_score break + case 'photo_path': + aVal = a.photo_path + bVal = b.photo_path + break + case 'excluded': + aVal = a.excluded ? 1 : 0 + bVal = b.excluded ? 1 : 0 + break } if (typeof aVal === 'string') { @@ -127,12 +142,33 @@ export default function FacesMaintenance() { } } + const handleExclude = async () => { + if (selectedFaces.size === 0) { + alert('Please select at least one face to exclude.') + return + } + setExcluding(true) + try { + const faceIds = Array.from(selectedFaces) + // Exclude each selected face + await Promise.all(faceIds.map(faceId => facesApi.setExcluded(faceId, true))) + // Reload faces after exclusion + await loadFaces() + alert(`Successfully excluded ${selectedFaces.size} face(s)`) + } catch (error) { + console.error('Error excluding faces:', error) + alert('Error excluding faces. Please try again.') + } finally { + setExcluding(false) + } + } + return (
{/* Controls */}
-
+
{/* Quality Range Selector */}
+ {/* Excluded Faces Filter */} +
+ + +
+ + {/* Identified Filter */} +
+ + +
+ {/* Batch Size */}