From ad92302461e7143ec796c2d2b984a5c641fc25e8 Mon Sep 17 00:00:00 2001 From: nBiqoz Date: Mon, 4 Aug 2025 00:14:55 +0200 Subject: [PATCH] finalyse --- app/components/AnonymizationInterface.tsx | 127 ++++++------- app/components/AnonymizationLogic.tsx | 142 ++++++++------- app/components/FileUploadComponent.tsx | 71 +++++--- app/components/ProgressBar.tsx | 26 ++- app/components/ResultPreviewComponent.tsx | 14 +- app/components/SupportedDataTypes.tsx | 2 +- app/page.tsx | 5 +- app/utils/highlightEntities.tsx | 206 ++++++++++++++++++---- 8 files changed, 371 insertions(+), 222 deletions(-) diff --git a/app/components/AnonymizationInterface.tsx b/app/components/AnonymizationInterface.tsx index c607f10..ed764fa 100644 --- a/app/components/AnonymizationInterface.tsx +++ b/app/components/AnonymizationInterface.tsx @@ -17,108 +17,97 @@ export const AnonymizationInterface = ({ const anonymizedTypes = new Set(); - // ✅ NOUVEAUX PATTERNS PRESIDIO + // Correspondance exacte avec les patterns de highlightEntities.tsx - // Noms (PERSON) + // PERSON -> Prénoms/Noms + // PERSON -> Prénoms, Noms de famille ET Noms complets if (outputText.includes("")) { anonymizedTypes.add("Prénoms"); anonymizedTypes.add("Noms de famille"); anonymizedTypes.add("Noms complets"); } - // Emails (EMAIL_ADDRESS) + // EMAIL_ADDRESS -> Adresses e-mail if (outputText.includes("")) { anonymizedTypes.add("Adresses e-mail"); } - // Téléphones (PHONE_NUMBER) + // PHONE_NUMBER -> Numéros de téléphone if (outputText.includes("")) { anonymizedTypes.add("Numéros de téléphone"); } - // Adresses (LOCATION) + // BE_PHONE_NUMBER -> aussi Numéros de téléphone + if (outputText.includes("")) { + anonymizedTypes.add("Numéros de téléphone"); + } + + // LOCATION -> Adresses if (outputText.includes("")) { anonymizedTypes.add("Adresses"); } - // IBAN (IBAN) - if (outputText.includes("")) { - anonymizedTypes.add("Numéros d'ID"); // Ou créer une nouvelle catégorie "IBAN" + // BE_ADDRESS -> aussi Adresses + if (outputText.includes("")) { + anonymizedTypes.add("Adresses"); } - // Organisations (ORGANIZATION) - if (outputText.includes("")) { - anonymizedTypes.add("Noms de domaine"); // Ou adapter selon vos besoins - } - - // Dates personnalisées (CUSTOM_DATE) - if (outputText.includes("")) { + // FLEXIBLE_DATE ou DATE_TIME -> Dates + if ( + outputText.includes("") || + outputText.includes("") + ) { anonymizedTypes.add("Dates"); } - // Numéros d'entreprise belges (BE_ENTERPRISE_NUMBER) + // IBAN -> Coordonnées bancaires (au lieu de Numéros d'ID) + if (outputText.includes("")) { + anonymizedTypes.add("Coordonnées bancaires"); + } + + // CREDIT_CARD -> aussi Coordonnées bancaires (au lieu de Valeurs numériques) + if (outputText.includes("")) { + anonymizedTypes.add("Coordonnées bancaires"); + } + + // NRP -> Numéros d'ID + if (outputText.includes("")) { + anonymizedTypes.add("Numéros d'ID"); + } + + // BE_PRO_ID -> Numéros d'ID + if (outputText.includes("")) { + anonymizedTypes.add("Numéros d'ID"); + } + + // BE_ENTERPRISE_NUMBER -> Numéros d'ID if (outputText.includes("")) { anonymizedTypes.add("Numéros d'ID"); } - // ✅ ANCIENS PATTERNS (pour compatibilité) - - // Noms (anciens patterns [Nom1], [Nom2]...) - if (outputText.includes("[Nom1]") || outputText.includes("[Nom")) { - anonymizedTypes.add("Prénoms"); - anonymizedTypes.add("Noms de famille"); - anonymizedTypes.add("Noms complets"); - } - - // Emails (anciens patterns) - if (outputText.includes("[Email1]") || outputText.includes("[Email")) { - anonymizedTypes.add("Adresses e-mail"); - } - - // Téléphones (anciens patterns) - if ( - outputText.includes("[Téléphone1]") || - outputText.includes("[Téléphone") - ) { - anonymizedTypes.add("Numéros de téléphone"); - } - - // Adresses (anciens patterns) - if (outputText.includes("[Adresse1]") || outputText.includes("[Adresse")) { - anonymizedTypes.add("Adresses"); - } - - // Numéros d'ID / Sécurité sociale (anciens patterns) - if ( - outputText.includes("[NuméroSS1]") || - outputText.includes("[NuméroSS") || - outputText.includes("[ID") - ) { - anonymizedTypes.add("Numéros d'ID"); - } - - // Valeurs monétaires - if (outputText.includes("[Montant") || /\[\d+[€$]\]/.test(outputText)) { - anonymizedTypes.add("Valeurs monétaires"); - } - - // Noms de domaine - if (outputText.includes("[Domaine") || /\[.*\.com\]/.test(outputText)) { + // URL -> Noms de domaine + if (outputText.includes("")) { anonymizedTypes.add("Noms de domaine"); } - // Valeurs numériques - if ( - /\[\d+\]/.test(outputText) && - !outputText.includes("[Téléphone") && - !outputText.includes("[Montant") - ) { + // CREDIT_CARD -> Coordonnées bancaires (supprimer la duplication) + if (outputText.includes("")) { + anonymizedTypes.add("Coordonnées bancaires"); + } + + // Supprimer cette ligne dupliquée : + // if (outputText.includes("")) { + // anonymizedTypes.add("Valeurs numériques"); + // } + + // IP_ADDRESS -> Valeurs numériques + if (outputText.includes("")) { anonymizedTypes.add("Valeurs numériques"); } - // Texte personnalisé (si du texte a été modifié mais pas avec les patterns spécifiques) - if (sourceText !== outputText && anonymizedTypes.size === 0) { - anonymizedTypes.add("Texte personnalisé"); + // BE_VAT -> Valeurs numériques + if (outputText.includes("")) { + anonymizedTypes.add("Valeurs numériques"); } return anonymizedTypes; @@ -133,7 +122,7 @@ export const AnonymizationInterface = ({ items: ["Noms de famille", "Adresses", "Dates"], }, { - items: ["Noms complets", "Numéros d'ID", "Valeurs numériques"], + items: ["Noms complets", "Numéros d'ID", "Coordonnées bancaires"], }, { items: ["Adresses e-mail", "Valeurs monétaires", "Texte personnalisé"], diff --git a/app/components/AnonymizationLogic.tsx b/app/components/AnonymizationLogic.tsx index 74c1417..d4b61c5 100644 --- a/app/components/AnonymizationLogic.tsx +++ b/app/components/AnonymizationLogic.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { patterns } from "@/app/utils/highlightEntities"; interface EntityMapping { originalValue: string; @@ -8,25 +9,18 @@ interface EntityMapping { endIndex: number; } -// Nouvelle interface pour les résultats de Presidio Analyzer +// L'API retourne des objets avec snake_case interface PresidioAnalyzerResult { entity_type: string; start: number; end: number; score: number; - analysis_explanation?: { - recognizer: string; - pattern_name?: string; - pattern?: string; - validation_result?: boolean; - }; } -// Interface pour la réponse de l'API +// La réponse de l'API utilise camelCase pour les clés principales interface ProcessDocumentResponse { - text?: string; + text?: string; // Texte original en cas de fallback anonymizedText?: string; - piiCount?: number; analyzerResults?: PresidioAnalyzerResult[]; error?: string; } @@ -66,101 +60,105 @@ export const useAnonymization = ({ setEntityMappings([]); try { - console.log("🚀 Début anonymisation avec Presidio"); - const formData = new FormData(); - if (uploadedFile) { - console.log("📁 Traitement fichier:", { - name: uploadedFile.name, - type: uploadedFile.type, - size: uploadedFile.size - }); formData.append("file", uploadedFile); } else { - console.log("📝 Traitement texte saisi"); const textBlob = new Blob([textToProcess], { type: "text/plain" }); - const textFile = new File([textBlob], "input.txt", { type: "text/plain" }); + const textFile = new File([textBlob], "input.txt", { + type: "text/plain", + }); formData.append("file", textFile); } - console.log("🔍 Appel à /api/process-document avec Presidio..."); - console.log("📦 FormData préparée:", Array.from(formData.entries())); - const response = await fetch("/api/process-document", { method: "POST", body: formData, }); - console.log("📡 Réponse reçue:", { - ok: response.ok, - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()) - }); - if (!response.ok) { let errorMessage = `Erreur HTTP: ${response.status}`; - try { - const responseText = await response.text(); - console.log("📄 Contenu de l'erreur:", responseText); - - if (responseText.trim()) { - try { - const errorData = JSON.parse(responseText); - if (errorData.error) { - errorMessage = errorData.error; - console.log("✅ Message détaillé récupéré:", errorMessage); - } - } catch (jsonError) { - console.error("❌ Erreur parsing JSON:", jsonError); // ✅ Utiliser la variable - console.error("❌ Réponse non-JSON:", responseText); - errorMessage = `Erreur ${response.status}: Réponse invalide du serveur`; - } - } - } catch (readError) { - console.error("❌ Impossible de lire la réponse:", readError); + const errorData = await response.json(); + if (errorData.error) errorMessage = errorData.error; + } catch { + /* Ignore */ } - throw new Error(errorMessage); } const data: ProcessDocumentResponse = await response.json(); - console.log("📊 Réponse API:", data); if (data.error) { throw new Error(data.error); } - if (data.anonymizedText) { - console.log("✅ Anonymisation réussie avec Presidio"); + // Utiliser camelCase pour les propriétés de la réponse principale + if (data.anonymizedText && data.analyzerResults) { setOutputText(data.anonymizedText); - // Extraire les mappings depuis les résultats Presidio (plus d'erreur 'any') - if (data.analyzerResults && data.text) { - const mappings: EntityMapping[] = data.analyzerResults.map( - (entity: PresidioAnalyzerResult, index: number) => ({ - originalValue: data.text!.substring(entity.start, entity.end), - anonymizedValue: `[${entity.entity_type}${index + 1}]`, - entityType: entity.entity_type, - startIndex: entity.start, - endIndex: entity.end, - }) - ); - setEntityMappings(mappings); - console.log("📋 Entités détectées:", mappings.length); - console.log("🔍 Détails des entités:", mappings); - } + const entityTypeMap = new Map(); + patterns.forEach((p) => { + const match = p.regex.toString().match(/<([A-Z_]+)>/); + if (match && match[1]) { + entityTypeMap.set(match[1], p.label); + } + }); + + // 1. Compter les occurrences de chaque tag d'entité dans le texte anonymisé + const tagCounts = new Map(); + data.analyzerResults.forEach((result) => { + const tag = `<${result.entity_type}>`; + if (!tagCounts.has(result.entity_type)) { + const count = ( + data.anonymizedText?.match(new RegExp(tag, "g")) || [] + ).length; + tagCounts.set(result.entity_type, count); + } + }); + + const seen = new Set(); + const uniqueMappings: EntityMapping[] = []; + const addedCounts = new Map(); + + // 2. N'ajouter que les entités réellement anonymisées avec un compteur + data.analyzerResults + .sort((a, b) => a.start - b.start) // Trier par ordre d'apparition + .forEach((result) => { + const entityType = result.entity_type; + const maxCount = tagCounts.get(entityType) || 0; + const currentCount = addedCounts.get(entityType) || 0; + + if (currentCount < maxCount) { + const originalValue = textToProcess.substring( + result.start, + result.end + ); + const frenchLabel = entityTypeMap.get(entityType) || entityType; + const uniqueKey = `${frenchLabel}|${originalValue}`; + + if (!seen.has(uniqueKey)) { + const newCount = (addedCounts.get(entityType) || 0) + 1; + addedCounts.set(entityType, newCount); + + uniqueMappings.push({ + entityType: frenchLabel, + originalValue: originalValue, + anonymizedValue: `${frenchLabel} [${newCount}]`, + startIndex: result.start, + endIndex: result.end, + }); + seen.add(uniqueKey); + } + } + }); + + setEntityMappings(uniqueMappings); } else if (data.text) { - console.log( - "⚠️ Fallback: Presidio non disponible, texte original retourné" - ); setOutputText(data.text); setError("Presidio temporairement indisponible. Texte non anonymisé."); } } catch (error) { - console.error("❌ Erreur anonymisation complète:", error); setError( error instanceof Error ? error.message diff --git a/app/components/FileUploadComponent.tsx b/app/components/FileUploadComponent.tsx index dcb8c87..524534c 100644 --- a/app/components/FileUploadComponent.tsx +++ b/app/components/FileUploadComponent.tsx @@ -11,6 +11,14 @@ import { SupportedDataTypes } from "./SupportedDataTypes"; import { AnonymizationInterface } from "./AnonymizationInterface"; import { highlightEntities } from "../utils/highlightEntities"; +interface EntityMapping { + originalValue: string; + anonymizedValue: string; + entityType: string; + startIndex: number; + endIndex: number; +} + interface FileUploadComponentProps { uploadedFile: File | null; handleFileChange: (e: React.ChangeEvent) => void; @@ -26,8 +34,9 @@ interface FileUploadComponentProps { outputText?: string; copyToClipboard?: () => void; downloadText?: () => void; - isExampleLoaded?: boolean; // NOUVEAU - setIsExampleLoaded?: (loaded: boolean) => void; // NOUVEAU + isExampleLoaded?: boolean; + setIsExampleLoaded?: (loaded: boolean) => void; + entityMappings?: EntityMapping[]; // Ajouter cette prop } export const FileUploadComponent = ({ @@ -45,7 +54,8 @@ export const FileUploadComponent = ({ outputText, copyToClipboard, downloadText, - setIsExampleLoaded, // NOUVEAU - Ajouté ici + setIsExampleLoaded, + entityMappings, // Ajouter cette prop ici }: FileUploadComponentProps) => { // On passe en preview seulement si : // 1. Un fichier est uploadé OU @@ -133,7 +143,8 @@ export const FileUploadComponent = ({
{highlightEntities( - outputText || "Aucun contenu à afficher" + outputText || "Aucun contenu à afficher", + entityMappings // Ajouter les mappings ici )}
@@ -206,7 +217,7 @@ export const FileUploadComponent = ({