Sprint C#14: Auto-Match progress bar, elapsed time, and cancel.

This commit is contained in:
2026-08-04 22:03:35 -04:00
parent 7e92afe7c5
commit a2a3c7e9c2
2 changed files with 57 additions and 4 deletions
+9 -2
View File
@@ -278,8 +278,15 @@ export const facesApi = {
const response = await apiClient.post<BatchUnmatchResponse>('/api/v1/faces/batch-unmatch', payload)
return response.data
},
autoMatch: async (request: AutoMatchRequest): Promise<AutoMatchResponse> => {
const response = await apiClient.post<AutoMatchResponse>('/api/v1/faces/auto-match', request)
autoMatch: async (
request: AutoMatchRequest,
options?: { signal?: AbortSignal }
): Promise<AutoMatchResponse> => {
const response = await apiClient.post<AutoMatchResponse>(
'/api/v1/faces/auto-match',
request,
{ signal: options?.signal }
)
return response.data
},
getAutoMatchPeople: async (params?: {
+48 -2
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useMemo, useRef } from 'react'
import axios from 'axios'
import facesApi, {
AutoMatchPersonSummary,
AutoMatchFaceItem
@@ -33,6 +34,8 @@ export default function AutoMatch() {
const [selectedFaces, setSelectedFaces] = useState<Record<number, boolean>>({})
const [originalSelectedFaces, setOriginalSelectedFaces] = useState<Record<number, boolean>>({})
const [busy, setBusy] = useState(false)
const [runElapsedSec, setRunElapsedSec] = useState(0)
const autoMatchAbortRef = useRef<AbortController | null>(null)
const [saving, setSaving] = useState(false)
const [hasNoResults, setHasNoResults] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -41,6 +44,25 @@ export default function AutoMatch() {
const STATE_KEY = 'automatch_state'
const SETTINGS_KEY = 'automatch_settings'
useEffect(() => {
if (!busy) {
setRunElapsedSec(0)
return
}
const started = Date.now()
const timer = window.setInterval(() => {
setRunElapsedSec(Math.floor((Date.now() - started) / 1000))
}, 1000)
return () => window.clearInterval(timer)
}, [busy])
const cancelAutoMatch = () => {
autoMatchAbortRef.current?.abort()
autoMatchAbortRef.current = null
setBusy(false)
showToast('Auto-match cancelled', 'info')
}
// Track if initial load has happened
const initialLoadRef = useRef(false)
// Track if settings have been loaded from sessionStorage
@@ -501,13 +523,15 @@ export default function AutoMatch() {
}
setBusy(true)
const controller = new AbortController()
autoMatchAbortRef.current = controller
try {
const response = await facesApi.autoMatch({
tolerance: RUN_AUTO_MATCH_TOLERANCE, // Use 0.5 for Run auto-match button (stricter)
auto_accept: true,
auto_accept_threshold: autoAcceptThreshold,
use_distance_based_thresholds: true // Enable distance-based thresholds for Run auto-match button
})
}, { signal: controller.signal })
// Show summary if auto-accept was performed
if (response.auto_accepted) {
@@ -533,7 +557,6 @@ export default function AutoMatch() {
setPeople([])
setFilteredPeople([])
setIsActive(false)
setBusy(false)
return
}
@@ -545,9 +568,13 @@ export default function AutoMatch() {
setOriginalSelectedFaces({})
setIsActive(true)
} catch (error) {
if (axios.isCancel(error)) {
return
}
console.error('Auto-match failed:', error)
showToast('Failed to start auto-match. Please try again.', 'error')
} finally {
autoMatchAbortRef.current = null
setBusy(false)
}
}
@@ -796,6 +823,15 @@ export default function AutoMatch() {
>
{busy ? 'Processing...' : hasNoResults ? 'No Matches Available' : '🚀 Run Auto-Match'}
</button>
{busy && (
<button
type="button"
onClick={cancelAutoMatch}
className="px-3 py-2 text-sm border border-red-300 text-red-700 rounded hover:bg-red-50"
>
Cancel
</button>
)}
</div>
{isDeveloperMode && (
<div className="flex items-center gap-2">
@@ -814,6 +850,16 @@ export default function AutoMatch() {
</div>
)}
</div>
{busy && (
<div className="mt-3" role="status" aria-live="polite">
<div className="h-2 w-full overflow-hidden rounded bg-gray-200">
<div className="h-full w-1/3 animate-pulse rounded bg-blue-600" />
</div>
<p className="mt-1 text-sm text-gray-600">
Running auto-match across the library {runElapsedSec}s elapsed
</p>
</div>
)}
<div className="mt-2 text-xs text-gray-600 bg-blue-50 border border-blue-200 rounded p-2">
<span className="font-medium"> Auto-Match Criteria:</span> Only faces with similarity higher than 85% and picture quality higher than 50% will be auto-matched. Profile faces are excluded for better accuracy.
</div>