From 89a63cbf577bf19caba00bc9827ae3fc1ef01a4c Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Wed, 12 Nov 2025 12:13:19 -0500 Subject: [PATCH] feat: Add Help page and enhance user navigation in PunimTag application This commit introduces a new Help page to the PunimTag application, providing users with detailed guidance on various features and workflows. The navigation has been updated to include the Help page, improving accessibility to support resources. Additionally, the user guide has been refined to remove outdated workflow examples, ensuring clarity and relevance. The Dashboard page has also been streamlined for a cleaner interface. Documentation has been updated to reflect these changes. --- docs/USER_GUIDE.md | 13 +- frontend/src/App.tsx | 2 + frontend/src/components/Layout.tsx | 13 +- frontend/src/pages/Dashboard.tsx | 1 - frontend/src/pages/Help.tsx | 520 +++++++++++++++++++++++++++++ frontend/src/pages/Identify.tsx | 180 ++++++++-- frontend/src/pages/Search.tsx | 12 + frontend/src/pages/Tags.tsx | 54 ++- 8 files changed, 749 insertions(+), 46 deletions(-) create mode 100644 frontend/src/pages/Help.tsx diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 87e1004..750759d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -117,11 +117,9 @@ The application uses a **left sidebar navigation** with the following pages: **Features**: - **Folder Selection**: Browse and select folders containing photos -- **File Upload**: Drag-and-drop or click to upload individual files - **Recursive Scanning**: Option to scan subfolders recursively - **Duplicate Detection**: Automatically detects and skips duplicate photos - **Real-time Progress**: Live progress tracking during import -- **EXIF Extraction**: Automatically extracts date taken and metadata **How to Use**: @@ -457,7 +455,7 @@ Note: Tags are case insensitive!********* **Purpose**: Configure application settings and preferences --- -## Workflow Examples + ### Complete Workflow: Import and Identify Photos @@ -490,15 +488,6 @@ Note: Tags are case insensitive!********* - Find specific photos - Assign tags for organization -### Quick Workflow: Just Process New Photos - -1. **Scan** → Import new photos -2. **Process** → Detect faces -3. **Identify** → Manually identify any remaining faces -4. **Auto-Match** → Automatically match to existing people -5. **Tag Photos** → Tag photos for easy future search - - --- **Last Updated**: October 2025 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1155b2e..1f9d3f5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,7 @@ import Modify from './pages/Modify' import Tags from './pages/Tags' import FacesMaintenance from './pages/FacesMaintenance' import Settings from './pages/Settings' +import Help from './pages/Help' import Layout from './components/Layout' function PrivateRoute({ children }: { children: React.ReactNode }) { @@ -44,6 +45,7 @@ function AppRoutes() { } /> } /> } /> + } /> ) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index f8aa0d7..5cdfb0f 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -6,26 +6,29 @@ export default function Layout() { const { username, logout } = useAuth() const navItems = [ - { path: '/', label: 'Dashboard', icon: '📊' }, { path: '/scan', label: 'Scan', icon: '🗂️' }, { path: '/process', label: 'Process', icon: '⚙️' }, { path: '/search', label: 'Search', icon: '🔍' }, { path: '/identify', label: 'Identify', icon: '👤' }, { path: '/auto-match', label: 'Auto-Match', icon: '🤖' }, { path: '/modify', label: 'Modify', icon: '✏️' }, - { path: '/tags', label: 'Tags', icon: '🏷️' }, + { path: '/tags', label: 'Tag', icon: '🏷️' }, { path: '/faces-maintenance', label: 'Faces Maintenance', icon: '🔧' }, { path: '/settings', label: 'Settings', icon: '⚙️' }, + { path: '/help', label: 'Help', icon: '📚' }, ] return (
{/* Top bar */}
-
+
-
-

PunimTag

+
+ + 🏠 +

PunimTag

+
{username} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 623d85e..6428ab1 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,7 +1,6 @@ export default function Dashboard() { return (
-

Dashboard

Welcome to PunimTag!

diff --git a/frontend/src/pages/Help.tsx b/frontend/src/pages/Help.tsx new file mode 100644 index 0000000..d80bc27 --- /dev/null +++ b/frontend/src/pages/Help.tsx @@ -0,0 +1,520 @@ +import { useState } from 'react' + +type PageId = 'overview' | 'scan' | 'process' | 'identify' | 'auto-match' | 'search' | 'modify' | 'tags' | 'faces-maintenance' + +export default function Help() { + const [currentPage, setCurrentPage] = useState('overview') + + const renderPageContent = () => { + switch (currentPage) { + case 'overview': + return + case 'scan': + return setCurrentPage('overview')} /> + case 'process': + return setCurrentPage('overview')} /> + case 'identify': + return setCurrentPage('overview')} /> + case 'auto-match': + return setCurrentPage('overview')} /> + case 'search': + return setCurrentPage('overview')} /> + case 'modify': + return setCurrentPage('overview')} /> + case 'tags': + return setCurrentPage('overview')} /> + case 'faces-maintenance': + return setCurrentPage('overview')} /> + default: + return + } + } + + return ( +

+

📚 Help

+ {renderPageContent()} +
+ ) +} + +function NavigationOverview({ onPageClick }: { onPageClick: (page: PageId) => void }) { + const navItems = [ + { id: 'scan' as PageId, icon: '🗂️', label: 'Scan', description: 'Import photos from folders or upload files' }, + { id: 'process' as PageId, icon: '⚙️', label: 'Process', description: 'Detect and process faces in photos' }, + { id: 'identify' as PageId, icon: '👤', label: 'Identify', description: 'Manually identify people in faces' }, + { id: 'auto-match' as PageId, icon: '🤖', label: 'Auto-Match', description: 'Automatically match similar faces to previously identified faces' }, + { id: 'search' as PageId, icon: '🔍', label: 'Search', description: 'Search and filter photos' }, + { id: 'modify' as PageId, icon: '✏️', label: 'Modify', description: 'Edit person information' }, + { id: 'tags' as PageId, icon: '🏷️', label: 'Tags', description: 'Tag photos and manage photo tags' }, + { id: 'faces-maintenance' as PageId, icon: '🔧', label: 'Faces Maintenance', description: 'Manage face data' }, + ] + + return ( +
+

Navigation Overview

+

+ The application uses a left sidebar navigation with the following pages. Click on any page to learn more about it: +

+
+ {navItems.map((item) => ( + + ))} +
+
+ ) +} + +function PageHelpLayout({ title, onBack, children }: { title: string; onBack: () => void; children: React.ReactNode }) { + return ( +
+
+ +

{title}

+
+ {children} +
+ ) +} + +function ScanPageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Import photos into your collection from folders or upload files

+
+
+

Features

+
    +
  • Folder Selection: Browse and select folders containing photos
  • +
  • Recursive Scanning: Option to scan subfolders recursively (enabled by default)
  • +
  • Duplicate Detection: Automatically detects and skips duplicate photos
  • +
  • Real-time Progress: Live progress tracking during import
  • +
  • EXIF Extraction: Automatically extracts date taken and metadata
  • +
+
+
+

How to Use

+

Folder Scan:

+
    +
  1. Click "Browse Folder" button
  2. +
  3. Select a folder containing photos
  4. +
  5. Toggle "Recursive" if you want to include subfolders (enabled by default)
  6. +
  7. Click "Start Scan" button
  8. +
  9. Monitor progress in the progress bar
  10. +
  11. View results (photos added, existing photos skipped)
  12. +
+
+
+

What Happens

+
    +
  • Photos are copied to data/uploads directory
  • +
  • EXIF metadata is extracted (date taken, orientation, etc.)
  • +
  • Duplicate detection by checksum
  • +
  • Photos are added to database
  • +
  • Faces are NOT detected yet (use Process page for that)
  • +
+
+
+

Tips

+
    +
  • Large folders may take time - be patient!
  • +
+
+
+
+ ) +} + +function ProcessPageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Detect faces in imported photos and generate face encodings

+
+
+

Features

+
    +
  • Face detection method used - retinaface - Best accuracy, medium speed
  • +
  • Face recognition model used - ArcFace - Best accuracy, medium speed
  • +
  • Batch Size: Configure how many photos to process at once
  • +
  • Real-time Progress: Live progress tracking
  • +
  • Job Cancellation: Stop processing if needed
  • +
+
+
+

How to Use

+
    +
  1. Optionally set Batch Size (leave empty for default)
  2. +
  3. Click "Start Processing" button
  4. +
  5. Monitor progress: +
      +
    • Progress bar shows overall completion
    • +
    • Photo count shows photos processed
    • +
    • Face count shows faces detected and stored
    • +
    +
  6. +
  7. Wait for completion or click "Stop Processing" to cancel
  8. +
+
+
+

What Happens

+
    +
  • DeepFace analyzes each unprocessed photo
  • +
  • Faces are detected and located
  • +
  • 512-dimensional face encodings are generated
  • +
  • Face metadata is stored (confidence, quality, location)
  • +
  • Photos are marked as processed
  • +
+
+
+

Tips

+
    +
  • You can cancel and resume later
  • +
+
+
+
+ ) +} + +function IdentifyPageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Manually identify people in detected faces

+
+
+

Features

+
    +
  • Face Navigation: Browse through unidentified faces
  • +
  • Person Creation: Create new person records
  • +
  • Similar Faces Panel: View similar faces for comparison
  • +
  • Confidence Display: See match confidence percentages
  • +
  • Date Filtering: Filter faces by date taken or processed
  • +
  • Unique Faces Filter: Hide duplicate faces of same person
  • +
  • Face Information: View face metadata (confidence, quality, detector/model)
  • +
+
+
+

How to Use

+
+

Basic Identification:

+
    +
  1. Navigate to Identify page
  2. +
  3. View the current face on the left panel
  4. +
  5. Select existing person from the drop down list or create new person below
  6. +
  7. Enter person information: +
      +
    • First Name (required)
    • +
    • Last Name (required)
    • +
    • Middle Name (optional)
    • +
    • Maiden Name (optional)
    • +
    • Date of Birth (required)
    • +
    +
  8. +
  9. Click "Identify" button to identify the face
  10. +
+
+
+

Using Similar Faces:

+
    +
  1. Toggle "Compare" checkbox to show similar faces
  2. +
  3. View similar faces in the right panel
  4. +
  5. See confidence percentages (color-coded)
  6. +
  7. Select similar faces to bulk identify with the current left face
  8. +
  9. Click "Identify" button to bulk identify left and all selected on the right faces to that person.
  10. +
+
+
+
+

Confidence Colors

+
    +
  • 🟢 80%+ = Very High (Almost Certain)
  • +
  • 🟡 70%+ = High (Likely Match)
  • +
  • 🟠 60%+ = Medium (Possible Match)
  • +
  • 🔴 50%+ = Low (Questionable)
  • +
  • <50% = Very Low (Unlikely)
  • +
+
+
+

Tips

+
    +
  • Use similar faces to identify groups of photos
  • +
  • Date filtering helps focus on specific time periods
  • +
  • Unique faces filter reduces clutter
  • +
  • Confidence scores help prioritize identification
  • +
+
+
+
+ ) +} + +function AutoMatchPageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Automatically match unidentified faces to identified people

+
+
+

Features

+
    +
  • Person-Centric View: Shows identified person on left, matches on right
  • +
  • Checkbox Selection: Select which faces to identify
  • +
  • Confidence Display: Color-coded match confidence
  • +
  • Batch Identification: Identify multiple faces at once
  • +
  • Navigation: Move between different people
  • +
+
+
+

How to Use

+
+

Automatic Match Workflow:

+
    +
  1. Navigate to Auto-Match page
  2. +
  3. Click Run Auto-Match button
  4. +
  5. All unidentified faces will be matched to identified faces based on the following criteria: +
      +
    • Similarity higher than 70%
    • +
    • Picture quality higher than 50%
    • +
    • Profile faces are excluded for better accuracy
    • +
    +
  6. +
+
+
+

Manual Match Workflow:

+
    +
  1. Navigate to Auto-Match page
  2. +
  3. Faces load automatically on page load
  4. +
  5. View identified person on the left panel
  6. +
  7. View matching unidentified faces on the right panel
  8. +
  9. Check boxes next to faces you want to identify
  10. +
  11. Click "Save changes for [Person Name]" button
  12. +
  13. Use "Next" and "Back" buttons to navigate between people
  14. +
+
+
+
+

Tips

+
    +
  • Review matches carefully before saving
  • +
  • Use confidence scores to guide decisions
  • +
  • You can correct mistakes by going back and unchecking
  • +
  • High confidence matches (>70%) are usually accurate
  • +
+
+
+
+ ) +} + +function SearchPageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Search and filter photos by various criteria

+
+
+

Features

+
    +
  • People Filter: Filter by identified people
  • +
  • Date Filter: Filter by date taken or date added
  • +
  • Tag Filter: Filter by photo tags
  • +
  • Folder Filter: Filter by source folder
  • +
  • Photo Grid: Virtualized grid of matching photos
  • +
  • Pagination: Navigate through search results
  • +
  • Select All: Select all photos in current results
  • +
+
+
+

How to Use

+

Basic Search:

+
    +
  1. Navigate to Search page
  2. +
  3. Use filter dropdowns to set criteria
  4. +
  5. Click "Search" or filters apply automatically
  6. +
  7. View matching photos in the grid
  8. +
  9. Click on photos to view details
  10. +
  11. Use "Select All" to select all photos in results
  12. +
  13. Use "Tag selected photos" to tag multiple photos at once
  14. +
+
+
+

Tips

+
    +
  • Combine filters for precise searches
  • +
  • Use date ranges to find photos from specific periods
  • +
  • Tag filtering helps find themed photos
  • +
  • People filter is most useful after identification
  • +
+
+
+
+ ) +} + +function ModifyPageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Edit person information and manage person records

+
+
+

Features

+
    +
  • Person Selection: Choose person to edit
  • +
  • Information Editing: Update names and date of birth
  • +
  • Face Management: View and manage person's faces
  • +
  • Person Deletion: Remove person records (with confirmation)
  • +
+
+
+

How to Use

+

Editing Person Information:

+
    +
  1. Navigate to Modify page
  2. +
  3. Select person from dropdown
  4. +
  5. Edit information fields (First Name, Last Name, Middle Name, Maiden Name, Date of Birth)
  6. +
  7. Click "Save Changes" button
  8. +
+
+
+

Tips

+
    +
  • Use this to correct mistakes in identification
  • +
  • Update names if you learn more information
  • +
  • Be careful with deletion - it's permanent
  • +
+
+
+
+ ) +} + +function TagsPageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Manage photo tags and tag-photo relationships

+
+
+

Features

+
    +
  • Tag List: View all existing tags
  • +
  • Tag Creation: Create new tags
  • +
  • Tag Editing: Edit tag names
  • +
  • Tag Deletion: Remove tags
  • +
  • Photo-Tag Linkage: Assign tags to photos
  • +
+
+
+

How to Use

+

Creating Tags:

+
    +
  1. Navigate to Tags page
  2. +
  3. Click "Manage Tags" button
  4. +
  5. Enter new tag name
  6. +
  7. Click "Add tag" button
  8. +
+

Assigning Tags to Photos:

+
    +
  • Select photos from Tags page (or from Search page)
  • +
  • Tags can be assigned to multiple photos at once by selecting multiple photos and clicking "Tag Selected Photos" button
  • +
  • Tags can be assigned to all photos in a specific folder at once by clicking the linkage icon next to the folder name
  • +
  • Tags can be assigned to a single photo by clicking the linkage icon on the right of each photo
  • +
+
+
+

Tips

+
    +
  • Use descriptive tag names
  • +
  • Create tags for events, locations, themes
  • +
  • Tags help organize and find photos later
  • +
  • Note: Tags are case insensitive!
  • +
+
+
+
+ ) +} + +function FacesMaintenancePageHelp({ onBack }: { onBack: () => void }) { + return ( + +
+
+

Purpose

+

Remove unwanted faces - mainly due to low quality face detections

+
+
+

Features

+
    +
  • Face List: View all faces in database
  • +
  • Face Filtering: Filter quality
  • +
  • Face Deletion: Remove unwanted faces
  • +
  • Bulk Operations: Perform actions on multiple faces
  • +
+
+
+

How to Use

+

Viewing Faces:

+
    +
  1. Navigate to Faces Maintenance page
  2. +
  3. View list of all faces
  4. +
  5. See face thumbnails and metadata
  6. +
  7. Filter faces by quality (delete faces with low quality)
  8. +
+

Deleting Faces:

+
    +
  • Select faces to delete
  • +
  • Click "Delete Selected" button
  • +
  • Confirm deletion
  • +
  • ⚠️ Warning: Deletion is permanent
  • +
+
+
+

Tips

+
    +
  • Remove low-quality face detections
  • +
  • Regular maintenance keeps database clean
  • +
+
+
+
+ ) +} + diff --git a/frontend/src/pages/Identify.tsx b/frontend/src/pages/Identify.tsx index f88c868..6483e2d 100644 --- a/frontend/src/pages/Identify.tsx +++ b/frontend/src/pages/Identify.tsx @@ -31,6 +31,8 @@ export default function Identify() { // SessionStorage key for persisting settings (clears when tab/window closes) const SETTINGS_KEY = 'identify_settings' + // SessionStorage key for persisting page state (faces, current index, etc.) + const STATE_KEY = 'identify_state' const [people, setPeople] = useState([]) const [personId, setPersonId] = useState(undefined) @@ -63,15 +65,24 @@ export default function Identify() { const initialLoadRef = useRef(false) // Track if settings have been loaded from localStorage const [settingsLoaded, setSettingsLoaded] = useState(false) + // Track if state has been restored from sessionStorage + const [stateRestored, setStateRestored] = useState(false) + // Track if initial restoration is complete (prevents reload effects from firing during restoration) + const restorationCompleteRef = useRef(false) const canIdentify = useMemo(() => { return Boolean((personId && currentFace) || (firstName && lastName && dob && currentFace)) }, [personId, firstName, lastName, dob, currentFace]) - const loadFaces = async () => { + const loadFaces = async (clearState: boolean = false) => { setLoadingFaces(true) try { + // Clear saved state if explicitly requested (Refresh button) + if (clearState) { + sessionStorage.removeItem(STATE_KEY) + } + const res = await facesApi.getUnidentified({ page: 1, page_size: pageSize, @@ -96,6 +107,12 @@ export default function Identify() { setTotal(res.total) } setCurrentIdx(0) + // Clear form data when refreshing + if (clearState) { + setFaceFormData({}) + setSimilar([]) + setSelectedSimilar({}) + } } finally { setLoadingFaces(false) } @@ -266,6 +283,95 @@ export default function Identify() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + // Load state from sessionStorage on mount (faces, current index, similar, form data) + useEffect(() => { + try { + const saved = sessionStorage.getItem(STATE_KEY) + if (saved) { + const state = JSON.parse(saved) + if (state.faces && Array.isArray(state.faces) && state.faces.length > 0) { + setFaces(state.faces) + if (state.currentIdx !== undefined) { + setCurrentIdx(Math.min(state.currentIdx, state.faces.length - 1)) + } + if (state.similar && Array.isArray(state.similar)) { + setSimilar(state.similar) + } + if (state.faceFormData && typeof state.faceFormData === 'object') { + setFaceFormData(state.faceFormData) + } + if (state.selectedSimilar && typeof state.selectedSimilar === 'object') { + setSelectedSimilar(state.selectedSimilar) + } + // Mark that we restored state, so we don't reload + initialLoadRef.current = true + // Mark restoration as complete after state is restored + // Use a small delay to ensure all state updates have been processed + setTimeout(() => { + restorationCompleteRef.current = true + }, 50) + } + } + } catch (error) { + console.error('Error loading state from sessionStorage:', error) + } finally { + setStateRestored(true) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Save state to sessionStorage whenever it changes (but only after initial restore) + useEffect(() => { + if (!stateRestored) return // Don't save during initial restore + + try { + const state = { + faces, + currentIdx, + similar, + faceFormData, + selectedSimilar, + } + sessionStorage.setItem(STATE_KEY, JSON.stringify(state)) + } catch (error) { + console.error('Error saving state to sessionStorage:', error) + } + }, [faces, currentIdx, similar, faceFormData, selectedSimilar, stateRestored]) + + // Save state on unmount (when navigating away) - use refs to capture latest values + const facesRef = useRef(faces) + const currentIdxRef = useRef(currentIdx) + const similarRef = useRef(similar) + const faceFormDataRef = useRef(faceFormData) + const selectedSimilarRef = useRef(selectedSimilar) + + // Update refs whenever state changes + useEffect(() => { + facesRef.current = faces + currentIdxRef.current = currentIdx + similarRef.current = similar + faceFormDataRef.current = faceFormData + selectedSimilarRef.current = selectedSimilar + }, [faces, currentIdx, similar, faceFormData, selectedSimilar]) + + // Save state on unmount (when navigating away) + useEffect(() => { + return () => { + try { + const state = { + faces: facesRef.current, + currentIdx: currentIdxRef.current, + similar: similarRef.current, + faceFormData: faceFormDataRef.current, + selectedSimilar: selectedSimilarRef.current, + } + sessionStorage.setItem(STATE_KEY, JSON.stringify(state)) + } catch (error) { + console.error('Error saving state on unmount:', error) + } + } + }, []) + // Save settings to sessionStorage whenever they change (but only after initial load) useEffect(() => { if (!settingsLoaded) return // Don't save during initial load @@ -289,35 +395,50 @@ export default function Identify() { } }, [pageSize, minQuality, sortBy, sortDir, dateFrom, dateTo, uniqueFacesOnly, compareEnabled, selectedTags, settingsLoaded]) - // Initial load on mount (after settings are loaded) + // Initial load on mount (after settings and state are loaded) useEffect(() => { - if (!initialLoadRef.current && settingsLoaded) { + if (!initialLoadRef.current && settingsLoaded && stateRestored) { initialLoadRef.current = true - loadFaces() + // Only load if we didn't restore state (no faces means we need to load) + if (faces.length === 0) { + loadFaces() + // If we're loading fresh, mark restoration as complete immediately + restorationCompleteRef.current = true + } else { + // If state was restored, restorationCompleteRef is already set in the state restoration effect + // But ensure it's set in case state restoration didn't happen + if (!restorationCompleteRef.current) { + setTimeout(() => { + restorationCompleteRef.current = true + }, 50) + } + } loadPeople() loadTags() } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [settingsLoaded]) + }, [settingsLoaded, stateRestored]) // Reload when uniqueFacesOnly changes (immediate reload) + // But only if restoration is complete (prevents reload during initial restoration) useEffect(() => { - if (initialLoadRef.current) { + if (initialLoadRef.current && restorationCompleteRef.current) { loadFaces() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [uniqueFacesOnly]) // Reload when pageSize changes (immediate reload) + // But only if restoration is complete (prevents reload during initial restoration) useEffect(() => { - if (initialLoadRef.current) { + if (initialLoadRef.current && restorationCompleteRef.current) { loadFaces() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageSize]) useEffect(() => { - if (currentFace) { + if (currentFace && restorationCompleteRef.current) { setImageLoading(true) // Show loading indicator when face changes loadSimilar(currentFace.id) } @@ -416,15 +537,6 @@ export default function Identify() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentFace?.id]) // Only restore when face ID changes - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if (e.key === 'Enter' && canIdentify) { - handleIdentify() - } - } - window.addEventListener('keydown', onKey) - return () => window.removeEventListener('keydown', onKey) - }, [canIdentify]) const handleIdentify = async () => { if (!currentFace) return @@ -614,13 +726,23 @@ export default function Identify() { )}
- +
+ + +
)} @@ -632,6 +754,14 @@ export default function Identify() {
+
{!currentFace ? ( @@ -769,7 +899,7 @@ export default function Identify() { diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index 9143997..3fd1d64 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -200,6 +200,11 @@ export default function Search() { setSelectedPhotos(new Set()) } + const selectAll = () => { + const allPhotoIds = new Set(results.map(photo => photo.id)) + setSelectedPhotos(allPhotoIds) + } + const loadPhotoTags = async () => { if (selectedPhotos.size === 0) return @@ -593,6 +598,13 @@ export default function Search() { > Tag selected photos +