4NK_IA_front/src/App.tsx
4NK IA 78d4310137 feat: ajout déduplication des entités extraites
- Déduplication déterministe des identités, adresses, dates, entreprises, signatures et références
- Implémentation dans src/services/ruleNer.ts et src/services/backendApi.ts
- Clés de normalisation: prénom+nom, rue+CP+ville, nom+SIRET, type+valeur
- Test ciblé tests/deduplication.test.ts pour valider la fonctionnalité
- Documentation complète dans docs/deduplication_entites.md
- Correction des tests existants (supertest, extractEntitiesFromText)
- Compilation validée et services opérationnels
2025-09-19 13:29:39 +00:00

171 lines
6.3 KiB
TypeScript

import { useEffect, useCallback, useRef } from 'react'
import './App.css'
import { AppRouter } from './router'
import { useAppDispatch, useAppSelector } from './store'
import { loadFolderResults, setBootstrapped, setCurrentFolderHash, setPollingInterval, stopPolling, setCurrentFolderName, createDefaultFolderThunk } from './store/documentSlice'
import { usePerformance } from './hooks/usePerformance'
import { useAccessibility } from './hooks/useAccessibility'
import './styles/accessibility.css'
export default function App() {
const dispatch = useAppDispatch()
const { documents, bootstrapped, currentFolderHash, folderResults, hasPending, pollingInterval } =
useAppSelector((state) => state.document)
const visibilityRef = useRef<boolean>(typeof document !== 'undefined' ? !document.hidden : true)
// Hooks d'optimisation
const { startRenderTimer, endRenderTimer } = usePerformance()
const { announceToScreenReader } = useAccessibility()
// Bootstrap au démarrage de l'application avec système de dossiers
useEffect(() => {
startRenderTimer()
console.log('🔍 [APP] useEffect déclenché:', {
documentsLength: documents.length,
bootstrapped,
currentFolderHash,
folderResultsLength: folderResults.length,
isDev: import.meta.env.DEV,
})
// Récupérer le hash du dossier depuis l'URL
const urlParams = new URLSearchParams(window.location.search)
const urlFolderHash = urlParams.get('hash')
console.log('🔍 [APP] Hash du dossier depuis URL:', urlFolderHash)
const initializeFolder = async () => {
try {
let folderHash = urlFolderHash || currentFolderHash
// Si un hash est passé dans l'URL, le prioriser et l'enregistrer
if (urlFolderHash && urlFolderHash !== currentFolderHash) {
dispatch(setCurrentFolderHash(urlFolderHash))
}
// Si aucun hash n'est disponible, demander le dossier par défaut au backend
if (!folderHash) {
const res = await dispatch(createDefaultFolderThunk()).unwrap()
folderHash = res.folderHash
dispatch(setCurrentFolderHash(folderHash))
console.log('📌 [APP] Dossier par défaut créé/récupéré:', folderHash)
}
// Charger les résultats du dossier
console.log('📁 [APP] Chargement des résultats du dossier:', folderHash)
await dispatch(loadFolderResults(folderHash)).unwrap()
// Marquer le bootstrap comme terminé
dispatch(setBootstrapped(true))
endRenderTimer()
announceToScreenReader('Application chargée avec succès')
console.log('🎉 [APP] Bootstrap terminé avec le dossier:', folderHash)
} catch (error) {
console.error("❌ [APP] Erreur lors de l'initialisation du dossier:", error)
}
}
// Ne pas refaire le bootstrap si déjà fait
if (bootstrapped) {
console.log('⏭️ [APP] Bootstrap déjà effectué, dossier:', currentFolderHash)
return
}
initializeFolder()
}, [dispatch, bootstrapped, currentFolderHash, folderResults.length, documents.length, startRenderTimer, endRenderTimer, announceToScreenReader])
// Listener pour appliquer le fallback de nom de dossier côté store
useEffect(() => {
const handler = (e: Event) => {
const name = (e as CustomEvent<string>).detail
if (typeof name === 'string' && name.length > 0) {
dispatch(setCurrentFolderName(name))
}
}
window.addEventListener('4nk:setFolderName', handler as EventListener)
return () => window.removeEventListener('4nk:setFolderName', handler as EventListener)
}, [dispatch])
// Fonction pour démarrer le polling
const startPolling = useCallback(
(folderHash: string) => {
console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash)
let pollCount = 0
const maxPolls = 30 // Maximum d'itérations
const tick = () => {
if (pollCount >= maxPolls) {
console.log('⏹️ [APP] Arrêt du polling - limite atteinte')
dispatch(stopPolling())
return
}
if (!visibilityRef.current) {
// Onglet caché: replanifier sans requête
const hiddenDelay = 20000
console.log('⏸️ [APP] Onglet caché, report du polling de', hiddenDelay, 'ms')
const t = setTimeout(tick, hiddenDelay)
dispatch(setPollingInterval(t as any))
return
}
pollCount += 1
console.log(`🔄 [APP] Polling #${pollCount}`)
dispatch(loadFolderResults(folderHash))
// Backoff exponentiel doux basé sur le nombre d'itérations
const base = 12000
const factor = Math.min(4, Math.pow(2, Math.floor(pollCount / 5)))
const delay = base * factor
const t = setTimeout(tick, delay)
dispatch(setPollingInterval(t as any))
}
const t0 = setTimeout(tick, 0)
dispatch(setPollingInterval(t0 as any))
},
[dispatch],
)
// Fonction pour arrêter le polling
const stopPollingCallback = useCallback(() => {
console.log('⏹️ [APP] Arrêt du polling')
dispatch(stopPolling())
}, [dispatch])
// Gestion du polling basé sur l'état hasPending
useEffect(() => {
// Ne démarrer le polling que si on n'a encore jamais chargé ce dossier
// et seulement quand le backend indique des pending
if (hasPending && currentFolderHash && !pollingInterval) {
console.log('🔄 [APP] Démarrage du polling - fichiers en cours détectés')
startPolling(currentFolderHash)
} else if (!hasPending && pollingInterval) {
console.log('⏹️ [APP] Arrêt du polling - tous les fichiers traités')
stopPollingCallback()
}
}, [hasPending, currentFolderHash, pollingInterval, startPolling, stopPollingCallback])
// Pause/reprise du polling selon visibilité de la page
useEffect(() => {
const onVis = () => {
visibilityRef.current = !document.hidden
console.log('[APP] Visibilité changée, visible =', visibilityRef.current)
}
document.addEventListener('visibilitychange', onVis)
return () => document.removeEventListener('visibilitychange', onVis)
}, [])
// Nettoyage au démontage du composant
useEffect(() => {
return () => {
if (pollingInterval) {
clearInterval(pollingInterval)
}
}
}, [pollingInterval])
return <AppRouter />
}