Sprint C#14: Auto-Match progress bar, elapsed time, and cancel.
This commit is contained in:
@@ -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?: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user