diff --git a/app/api/process-document/route.ts b/app/api/process-document/route.ts index 54a392d..a60a152 100644 --- a/app/api/process-document/route.ts +++ b/app/api/process-document/route.ts @@ -1,6 +1,7 @@ import { NextResponse, type NextRequest } from "next/server"; import pdf from "pdf-parse"; // ✅ Import correct import mammoth from "mammoth"; +import { PresidioAnalyzerResult } from "@/app/config/entityLabels"; export async function POST(req: NextRequest) { console.log("🔍 Début du traitement de la requête"); @@ -8,6 +9,9 @@ export async function POST(req: NextRequest) { try { const formData = await req.formData(); const file = formData.get("file") as File | null; + const category = (formData.get("category") as string) || "pii"; // Récupérer la catégorie + + console.log("📊 Catégorie sélectionnée:", category); // ✅ Validation améliorée du fichier if (!file) { return NextResponse.json( @@ -169,9 +173,11 @@ export async function POST(req: NextRequest) { const analyzerConfig = { text: fileContent, language: "fr", + mode: category, // Ajouter le mode basé sur la catégorie }; console.log("🔍 Appel à Presidio Analyzer..."); + console.log("📊 Configuration:", analyzerConfig); // ✅ Définir l'URL AVANT de l'utiliser const presidioAnalyzerUrl = @@ -209,6 +215,7 @@ export async function POST(req: NextRequest) { const anonymizerConfig = { text: fileContent, analyzer_results: analyzerResults, + mode: category, // Ajouter le mode pour l'anonymizer aussi }; console.log("🔍 Appel à Presidio Anonymizer..."); @@ -236,77 +243,33 @@ export async function POST(req: NextRequest) { const anonymizerResult = await anonymizeResponse.json(); console.log("✅ Anonymisation réussie."); - // 🔧 NOUVELLE FONCTION SIMPLIFIÉE pour extraire les valeurs de remplacement - // Ajouter cette interface au début du fichier - interface AnalyzerResult { - entity_type: string; - start: number; - end: number; - score: number; - } - - // Puis modifier la fonction - const extractReplacementValues = ( - originalText: string, - anonymizedText: string, - analyzerResults: AnalyzerResult[] - ) => { - const replacementMap: Record = {}; - - // Trier les entités par position - const sortedResults = [...analyzerResults].sort( - (a, b) => a.start - b.start - ); - - // Pour chaque entité, trouver son remplacement dans le texte anonymisé - let searchOffset = 0; - - sortedResults.forEach((result) => { - const originalValue = originalText.substring( - result.start, - result.end - ); - - // Chercher le prochain remplacement [XXX] après la position courante - const replacementPattern = /\[[^\]]+\]/g; - replacementPattern.lastIndex = searchOffset; - const match = replacementPattern.exec(anonymizedText); - - if (match) { - replacementMap[originalValue] = match[0]; - searchOffset = match.index + match[0].length; - console.log( - `✅ Mapping positionnel: "${originalValue}" -> "${match[0]}"` - ); - } else { - // Fallback - const fallbackValue = `[${result.entity_type.toUpperCase()}]`; - replacementMap[originalValue] = fallbackValue; - console.log( - `⚠️ Fallback: "${originalValue}" -> "${fallbackValue}"` - ); - } - }); - - console.log("🔧 Mapping final:", replacementMap); - return replacementMap; - }; - - const replacementValues = extractReplacementValues( - fileContent, - anonymizerResult.anonymized_text, - analyzerResults + // 🎯 SOLUTION SIMPLIFIÉE : Utiliser directement le texte anonymisé de Presidio + console.log( + "✅ Texte anonymisé reçu de Presidio:", + anonymizerResult.anonymized_text ); - // 🔍 AJOUT D'UN LOG POUR DÉBOGUER - console.log("🔧 Valeurs de remplacement extraites:", replacementValues); + // Créer un mapping simple basé sur les entités détectées + const replacementValues: Record = {}; + + analyzerResults.forEach((result: PresidioAnalyzerResult) => { + const originalValue = fileContent.substring(result.start, result.end); + const replacementValue = result.entity_type; // ✅ CORRECTION : Utiliser entity_type au lieu de [ENTITY_TYPE] + replacementValues[originalValue] = replacementValue; + + console.log( + `📝 Mapping créé: "${originalValue}" -> "${replacementValue}"` + ); + }); const result = { - text: fileContent, - anonymizedText: anonymizerResult.anonymized_text, + text: fileContent, // Texte original pour référence + anonymizedText: anonymizerResult.anonymized_text, // Texte déjà anonymisé par Presidio piiCount: analyzerResults.length, analyzerResults: analyzerResults, - replacementValues: replacementValues, // Utiliser les nouvelles valeurs + replacementValues: replacementValues, + // 🎯 NOUVEAU : Indiquer qu'on utilise directement le texte de Presidio + usePresidioText: true, }; return NextResponse.json(result, { status: 200 }); diff --git a/app/components/AnonymizationInterface.tsx b/app/components/AnonymizationInterface.tsx index 9d16e89..b784ddd 100644 --- a/app/components/AnonymizationInterface.tsx +++ b/app/components/AnonymizationInterface.tsx @@ -1,4 +1,4 @@ -import { CheckCircle } from "lucide-react"; +import { CheckCircle, Info } from "lucide-react"; interface AnonymizationInterfaceProps { isProcessing: boolean; @@ -17,103 +17,181 @@ export const AnonymizationInterface = ({ const anonymizedTypes = new Set(); - if (outputText.includes("")) { - anonymizedTypes.add("Prénoms"); - anonymizedTypes.add("Noms de famille"); - anonymizedTypes.add("Noms complets"); + // PII - Données personnelles + if (outputText.includes("[PERSONNE]")) { + anonymizedTypes.add("Noms et prénoms"); } - - // EMAIL_ADDRESS -> Adresses e-mail - if (outputText.includes("")) { - anonymizedTypes.add("Adresses e-mail"); - } - - // PHONE_NUMBER -> Numéros de téléphone - if (outputText.includes("")) { - anonymizedTypes.add("Numéros de téléphone"); - } - - // 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"); - } - - // BE_ADDRESS -> aussi Adresses - if (outputText.includes("")) { - anonymizedTypes.add("Adresses"); - } - - // DATE -> Dates - if (outputText.includes("") || outputText.includes("")) { + if (outputText.includes("[DATE]")) { anonymizedTypes.add("Dates"); } - - // IBAN -> Coordonnées bancaires (au lieu de Numéros d'ID) - if (outputText.includes("")) { - anonymizedTypes.add("Coordonnées bancaires"); + if (outputText.includes("[ADRESSE_EMAIL]")) { + anonymizedTypes.add("Adresses e-mail"); + } + if ( + outputText.includes("[TELEPHONE_FRANCAIS]") || + outputText.includes("[TELEPHONE_BELGE]") || + outputText.includes("[TELEPHONE]") + ) { + anonymizedTypes.add("Numéros de téléphone"); + } + if ( + outputText.includes("[ADRESSE_FRANCAISE]") || + outputText.includes("[ADRESSE_BELGE]") || + outputText.includes("[ADRESSE]") + ) { + anonymizedTypes.add("Adresses postales"); + } + if (outputText.includes("[LOCATION]")) { + anonymizedTypes.add("Lieux géographiques"); + } + if ( + outputText.includes("[CARTE_IDENTITE_FRANCAISE]") || + outputText.includes("[CARTE_IDENTITE_BELGE]") || + outputText.includes("[PASSEPORT_FRANCAIS]") || + outputText.includes("[PASSEPORT_BELGE]") || + outputText.includes("[PERMIS_CONDUIRE_FRANCAIS]") + ) { + anonymizedTypes.add("Documents d'identité"); + } + if (outputText.includes("[NUMERO_SECURITE_SOCIALE_FRANCAIS]")) { + anonymizedTypes.add("Numéros de sécurité sociale"); + } + if (outputText.includes("[BIOMETRIC_DATA]")) { + anonymizedTypes.add("Données biométriques"); + } + if (outputText.includes("[HEALTH_DATA]")) { + anonymizedTypes.add("Données de santé"); + } + if ( + outputText.includes("[SEXUAL_ORIENTATION]") || + outputText.includes("[POLITICAL_OPINIONS]") + ) { + anonymizedTypes.add("Données sensibles RGPD"); } - // CREDIT_CARD -> aussi Coordonnées bancaires (au lieu de Valeurs numériques) - if (outputText.includes("")) { - anonymizedTypes.add("Coordonnées bancaires"); + // Données financières + if ( + outputText.includes("[IBAN]") || + outputText.includes("[COMPTE_BANCAIRE_FRANCAIS]") + ) { + anonymizedTypes.add("Comptes bancaires"); + } + if (outputText.includes("[CREDIT_CARD]")) { + anonymizedTypes.add("Cartes de crédit"); + } + if (outputText.includes("[MONTANT_FINANCIER]")) { + anonymizedTypes.add("Montants financiers"); + } + if (outputText.includes("[NUMERO_FISCAL_FRANCAIS]")) { + anonymizedTypes.add("Numéros fiscaux"); + } + if (outputText.includes("[RGPD_FINANCIAL_DATA]")) { + anonymizedTypes.add("Données financières RGPD"); } - // NRP -> Numéros d'ID - if (outputText.includes("")) { - anonymizedTypes.add("Numéros d'ID"); + // Business - Données d'entreprise + if (outputText.includes("[ORGANISATION]")) { + anonymizedTypes.add("Noms d'organisations"); + } + if ( + outputText.includes("[SIRET_SIREN_FRANCAIS]") || + outputText.includes("[SOCIETE_FRANCAISE]") || + outputText.includes("[SOCIETE_BELGE]") + ) { + anonymizedTypes.add("Entreprises et sociétés"); + } + if ( + outputText.includes("[TVA_FRANCAISE]") || + outputText.includes("[TVA_BELGE]") + ) { + anonymizedTypes.add("Numéros de TVA"); + } + if ( + outputText.includes("[NUMERO_ENTREPRISE_BELGE]") || + outputText.includes("[REGISTRE_NATIONAL_BELGE]") + ) { + anonymizedTypes.add("Identifiants d'entreprise"); + } + if (outputText.includes("[SECRET_COMMERCIAL]")) { + anonymizedTypes.add("Secrets commerciaux"); + } + if (outputText.includes("[REFERENCE_CONTRAT]")) { + anonymizedTypes.add("Références de contrats"); + } + if (outputText.includes("[MARKET_SHARE]")) { + anonymizedTypes.add("Parts de marché"); + } + if ( + outputText.includes("[ID_PROFESSIONNEL_BELGE]") || + outputText.includes("[DONNEES_PROFESSIONNELLES]") + ) { + anonymizedTypes.add("Identifiants professionnels"); } - // BE_PRO_ID -> Numéros d'ID - if (outputText.includes("")) { - anonymizedTypes.add("Numéros d'ID"); + // Données techniques + if (outputText.includes("[ADRESSE_IP]")) { + anonymizedTypes.add("Adresses IP"); } - - // BE_ENTERPRISE_NUMBER -> Numéros d'ID - if (outputText.includes("")) { - anonymizedTypes.add("Numéros d'ID"); + if (outputText.includes("[URL_IDENTIFIANT]")) { + anonymizedTypes.add("URLs et identifiants web"); } - - // URL -> Noms de domaine - if (outputText.includes("")) { - anonymizedTypes.add("Noms de domaine"); + if (outputText.includes("[CLE_API_SECRETE]")) { + anonymizedTypes.add("Clés API secrètes"); } - - // CREDIT_CARD -> Coordonnées bancaires (supprimer la duplication) - if (outputText.includes("")) { - anonymizedTypes.add("Coordonnées bancaires"); + if (outputText.includes("[IDENTIFIANT_PERSONNEL]")) { + anonymizedTypes.add("Identifiants personnels"); } - - // IP_ADDRESS -> Valeurs numériques - if (outputText.includes("")) { - anonymizedTypes.add("Valeurs numériques"); + if (outputText.includes("[LOCALISATION_GPS]")) { + anonymizedTypes.add("Coordonnées GPS"); } - - // BE_VAT -> Valeurs numériques - if (outputText.includes("")) { - anonymizedTypes.add("Valeurs numériques"); + if (outputText.includes("[TITRE_CIVILITE]")) { + anonymizedTypes.add("Titres de civilité"); } return anonymizedTypes; }; - // Structure exacte de SupportedDataTypes (récupérée dynamiquement) + // Structure mise à jour avec les vrais types de données const supportedDataStructure = [ { - items: ["Prénoms", "Numéros de téléphone", "Noms de domaine"], + items: [ + "Noms et prénoms", + "Numéros de téléphone", + "URLs et identifiants web", + ], }, { - items: ["Noms de famille", "Adresses", "Dates"], + items: ["Adresses postales", "Lieux géographiques", "Dates"], }, { - items: ["Noms complets", "Numéros d'ID", "Coordonnées bancaires"], + items: ["Documents d'identité", "Comptes bancaires", "Cartes de crédit"], }, { - items: ["Adresses e-mail", "Valeurs monétaires", "Texte personnalisé"], + items: ["Adresses e-mail", "Montants financiers", "Adresses IP"], + }, + { + items: [ + "Noms d'organisations", + "Entreprises et sociétés", + "Numéros de TVA", + ], + }, + { + items: [ + "Parts de marché", + "Secrets commerciaux", + "Références de contrats", + ], + }, + { + items: [ + "Données biométriques", + "Données de santé", + "Données sensibles RGPD", + ], + }, + { + items: ["Clés API secrètes", "Coordonnées GPS", "Titres de civilité"], }, ]; @@ -158,33 +236,61 @@ export const AnonymizationInterface = ({ const anonymizedTypes = getAnonymizedDataTypes(); return ( -
-
- -

- Anonymisation terminée avec succès -

-
-
- {supportedDataStructure.map((column, columnIndex) => ( -
- {column.items.map((item, itemIndex) => { - const isAnonymized = anonymizedTypes.has(item); - return ( - - {isAnonymized ? "✓" : "•"} {item} - - ); - })} +
+ {/* Instructions Panel */} +
+
+ +
+

+ Instructions d'utilisation : +

+
    +
  • • Survolez les mots pour les mettre en Ă©vidence
  • +
  • + • Cliquez pour sĂ©lectionner un mot, Ctrl/CMD (ou Shift) + + clic. +
  • +
  • • Faites clic droit pour ouvrir le menu contextuel
  • +
  • • Modifiez les labels et couleurs selon vos besoins
  • +
  • + • Utilisez "Toutes les occurrences" pour appliquer Ă  + tous les mots similaires +
  • +
- ))} +
+
+ + {/* Bloc vert existant */} +
+
+ +

+ Anonymisation terminée avec succès +

+
+
+ {supportedDataStructure.map((column, columnIndex) => ( +
+ {column.items.map((item, itemIndex) => { + const isAnonymized = anonymizedTypes.has(item); + return ( + + {isAnonymized ? "✓" : "•"} {item} + + ); + })} +
+ ))} +
); diff --git a/app/components/AnonymizationLogic.tsx b/app/components/AnonymizationLogic.tsx index 4c2e076..040e78d 100644 --- a/app/components/AnonymizationLogic.tsx +++ b/app/components/AnonymizationLogic.tsx @@ -13,17 +13,19 @@ interface ProcessDocumentResponse { error?: string; } -// Props du hook -interface AnonymizationLogicProps { +// Props du hook - Renommer pour correspondre à l'utilisation +interface UseAnonymizationProps { setOutputText: (text: string) => void; setError: (error: string | null) => void; setEntityMappings: (mappings: EntityMapping[]) => void; + setAnonymizedText?: (text: string) => void; // Nouveau paramètre optionnel } // NOUVEAU: Définir les types pour le paramètre de anonymizeData interface AnonymizeDataParams { file?: File | null; text?: string; + category?: string; // Ajouter le paramètre catégorie } /** @@ -34,10 +36,11 @@ export const useAnonymization = ({ setOutputText, setError, setEntityMappings, -}: AnonymizationLogicProps) => { + setAnonymizedText, +}: UseAnonymizationProps) => { const [isProcessing, setIsProcessing] = useState(false); - const anonymizeData = async ({ file, text }: AnonymizeDataParams) => { + const anonymizeData = async ({ file, text, category = 'pii' }: AnonymizeDataParams) => { setIsProcessing(true); setError(null); setEntityMappings([]); @@ -46,6 +49,10 @@ export const useAnonymization = ({ try { // ÉTAPE 1: Construire le FormData ici pour garantir le bon format const formData = new FormData(); + + // Ajouter la catégorie au FormData + formData.append('category', category); + if (file) { formData.append("file", file); } else if (text) { @@ -74,7 +81,7 @@ export const useAnonymization = ({ const replacementValues = data.replacementValues || {}; // Récupérer les valeurs de remplacement // 🔍 AJOUT DES CONSOLE.LOG POUR DÉBOGUER - console.log("📊 Données reçues de Presidio:", { + console.log("📊 Réponse de l'API:", { originalTextLength: originalText.length, presidioResultsCount: presidioResults.length, presidioResults: presidioResults, @@ -83,8 +90,13 @@ export const useAnonymization = ({ replacementValuesEntries: Object.entries(replacementValues), }); - // ÉTAPE 2 : Passer le texte ORIGINAL à l'état de sortie. - setOutputText(originalText); + // ÉTAPE 2 : Utiliser le texte ANONYMISÉ de Presidio au lieu du texte original + setOutputText(data.anonymizedText || originalText); + + // NOUVEAU : Stocker le texte anonymisé de Presidio séparément + if (setAnonymizedText && data.anonymizedText) { + setAnonymizedText(data.anonymizedText); + } // ÉTAPE 3 : Créer le tableau de mapping avec la nouvelle structure const sortedResults = [...presidioResults].sort( @@ -111,7 +123,9 @@ export const useAnonymization = ({ end: end, text: detectedText, replacementValue: replacementValues[detectedText], - displayName: replacementValues[detectedText], + displayName: replacementValues[detectedText] + ? replacementValues[detectedText].replace(/[\[\]]/g, "") + : entity_type, customColor: undefined, }); } diff --git a/app/components/ContextMenu.tsx b/app/components/ContextMenu.tsx index e717028..8176a6b 100644 --- a/app/components/ContextMenu.tsx +++ b/app/components/ContextMenu.tsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useEffect } from "react"; import { Trash2, Check, RotateCcw } from "lucide-react"; import { COLOR_PALETTE, type ColorOption } from "../config/colorPalette"; -// import { EntityMapping } from "../config/entityLabels"; // SUPPRIMER cette ligne +import { EntityMapping } from "../config/entityLabels"; interface ContextMenuProps { contextMenu: { @@ -12,7 +12,7 @@ interface ContextMenuProps { wordIndices: number[]; }; existingLabels: string[]; - // entityMappings: EntityMapping[]; // SUPPRIMER cette ligne + entityMappings?: EntityMapping[]; onApplyLabel: (displayName: string, applyToAll?: boolean) => void; onApplyColor: ( color: string, @@ -28,15 +28,16 @@ const colorOptions: ColorOption[] = COLOR_PALETTE; export const ContextMenu: React.FC = ({ contextMenu, existingLabels, - // entityMappings, // SUPPRIMER cette ligne + entityMappings, onApplyLabel, onApplyColor, onRemoveLabel, getCurrentColor, }) => { - const [customLabel, setCustomLabel] = useState(""); const [showNewLabelInput, setShowNewLabelInput] = useState(false); + const [newLabelValue, setNewLabelValue] = useState(""); const [showColorPalette, setShowColorPalette] = useState(false); + const [tempSelectedColor, setTempSelectedColor] = useState(''); const [applyToAll, setApplyToAll] = useState(false); const menuRef = useRef(null); const inputRef = useRef(null); @@ -55,16 +56,27 @@ export const ContextMenu: React.FC = ({ e.preventDefault(); e.stopPropagation(); } - if (customLabel.trim()) { + if (newLabelValue.trim()) { console.log( "Application du label personnalisé:", - customLabel.trim(), + newLabelValue.trim(), "À toutes les occurrences:", applyToAll ); - onApplyLabel(customLabel.trim(), applyToAll); // CORRIGER: 2 paramètres seulement - setCustomLabel(""); + + // Appliquer d'abord le label + onApplyLabel(newLabelValue.trim(), applyToAll); + + // Puis appliquer la couleur temporaire si elle existe + if (tempSelectedColor) { + setTimeout(() => { + onApplyColor(tempSelectedColor, 'Couleur personnalisée', applyToAll); + }, 100); + } + + setNewLabelValue(""); setShowNewLabelInput(false); + setTempSelectedColor(''); // Reset de la couleur temporaire } }; @@ -74,7 +86,7 @@ export const ContextMenu: React.FC = ({ e.stopPropagation(); console.log("Annulation du nouveau label"); setShowNewLabelInput(false); - setCustomLabel(""); + setNewLabelValue(""); }; // Fonction pour empêcher la propagation des événements @@ -146,36 +158,47 @@ export const ContextMenu: React.FC = ({
- {/* Labels existants */} - {existingLabels.length > 0 && ( - <> -
- -
-
- - )} + {/* Labels existants - toujours visible */} +
+ +
+
{/* Nouveau label */}
@@ -207,10 +230,10 @@ export const ContextMenu: React.FC = ({ { e.stopPropagation(); - setCustomLabel(e.target.value); + setNewLabelValue(e.target.value); }} onKeyDown={(e) => { e.stopPropagation(); @@ -232,7 +255,7 @@ export const ContextMenu: React.FC = ({ type="button" onClick={handleApplyCustomLabel} onMouseDown={(e) => e.stopPropagation()} - disabled={!customLabel.trim()} + disabled={!newLabelValue.trim()} className="px-1 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500" title="Appliquer le label" > @@ -260,7 +283,7 @@ export const ContextMenu: React.FC = ({ type="button" className="w-5 h-5 rounded-full border-2 border-gray-300 cursor-pointer hover:border-gray-400 transition-all" style={{ - backgroundColor: getCurrentColor(contextMenu.selectedText), + backgroundColor: tempSelectedColor || getCurrentColor(contextMenu.selectedText), }} onClick={(e) => { e.stopPropagation(); @@ -273,21 +296,36 @@ export const ContextMenu: React.FC = ({ {showColorPalette && (
- {colorOptions.map((color) => ( -
)}
diff --git a/app/components/DownloadActions.tsx b/app/components/DownloadActions.tsx index 162f184..9c810e4 100644 --- a/app/components/DownloadActions.tsx +++ b/app/components/DownloadActions.tsx @@ -1,23 +1,25 @@ -import { generateAnonymizedText } from "@/app/utils/generateAnonymizedText"; import { EntityMapping } from "@/app/config/entityLabels"; interface DownloadActionsProps { outputText: string; entityMappings?: EntityMapping[]; + anonymizedText?: string; // Nouveau paramètre pour le texte déjà anonymisé par Presidio } export const useDownloadActions = ({ outputText, - entityMappings = [], + anonymizedText, // Texte déjà anonymisé par Presidio }: DownloadActionsProps) => { const copyToClipboard = () => { - const anonymizedText = generateAnonymizedText(outputText, entityMappings); - navigator.clipboard.writeText(anonymizedText); + // Toujours utiliser le texte anonymisé de Presidio + const textToCopy = anonymizedText || outputText; + navigator.clipboard.writeText(textToCopy); }; const downloadText = () => { - const anonymizedText = generateAnonymizedText(outputText, entityMappings); - const blob = new Blob([anonymizedText], { type: "text/plain" }); + // Utiliser le texte anonymisé de Presidio si disponible, sinon fallback sur outputText + const textToDownload = anonymizedText || outputText; + const blob = new Blob([textToDownload], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; diff --git a/app/components/EntityMappingTable.tsx b/app/components/EntityMappingTable.tsx index b7afba2..253ceac 100644 --- a/app/components/EntityMappingTable.tsx +++ b/app/components/EntityMappingTable.tsx @@ -40,7 +40,7 @@ export const EntityMappingTable = ({ mappings }: EntityMappingTableProps) => { return { ...mapping, entityNumber: entityCounts[entityType], - displayName: mapping.displayName || mapping.replacementValue || `[${entityType}]`, + displayName: mapping.entity_type, }; }); diff --git a/app/components/FileUploadComponent.tsx b/app/components/FileUploadComponent.tsx index 4274b29..a99ec28 100644 --- a/app/components/FileUploadComponent.tsx +++ b/app/components/FileUploadComponent.tsx @@ -9,8 +9,9 @@ import { import { SampleTextComponent } from "./SampleTextComponent"; import { SupportedDataTypes } from "./SupportedDataTypes"; import { AnonymizationInterface } from "./AnonymizationInterface"; -import { highlightEntities } from "../utils/highlightEntities"; -import { useState } from "react"; + +import { InteractiveTextEditor } from "./InteractiveTextEditor"; +import React, { useState } from "react"; import { EntityMapping } from "../config/entityLabels"; // Importer l'interface unifiée // Supprimer l'interface locale EntityMapping (lignes 15-21) @@ -21,7 +22,7 @@ interface FileUploadComponentProps { sourceText: string; setSourceText: (text: string) => void; setUploadedFile: (file: File | null) => void; - onAnonymize?: () => void; + onAnonymize?: (category?: string) => void; isProcessing?: boolean; canAnonymize?: boolean; isLoadingFile?: boolean; @@ -32,6 +33,7 @@ interface FileUploadComponentProps { isExampleLoaded?: boolean; setIsExampleLoaded?: (loaded: boolean) => void; entityMappings?: EntityMapping[]; + onMappingsUpdate?: (mappings: EntityMapping[]) => void; } export const FileUploadComponent = ({ @@ -50,8 +52,10 @@ export const FileUploadComponent = ({ downloadText, setIsExampleLoaded, entityMappings, + onMappingsUpdate, }: FileUploadComponentProps) => { const [isDragOver, setIsDragOver] = useState(false); + const [selectedCategory, setSelectedCategory] = useState("pii"); // Fonction pour valider le type de fichier const isValidFileType = (file: File) => { @@ -173,7 +177,7 @@ export const FileUploadComponent = ({
- {/* Bloc résultat anonymisé */} + {/* Bloc résultat anonymisé - MODE INTERACTIF */}
@@ -183,7 +187,7 @@ export const FileUploadComponent = ({

- Document anonymisé + DOCUMENT ANONYMISÉ MODE INTERACTIF

@@ -215,12 +219,208 @@ export const FileUploadComponent = ({
-
- {highlightEntities( - sourceText || "Aucun contenu à afficher", // Utiliser sourceText au lieu de outputText - entityMappings || [] // Fournir un tableau vide par défaut - )} -
+ { + if (onMappingsUpdate && entityMappings) { + console.log("🔄 Mise à jour mapping:", { + originalValue, + newLabel, + entityType, + applyToAllOccurrences, + customColor, + wordStart, + wordEnd, + }); + + let updatedMappings: EntityMapping[]; + + if (applyToAllOccurrences) { + // CORRECTION: Créer des mappings pour toutes les occurrences dans le texte + const existingMappingsForOtherTexts = + entityMappings.filter( + (mapping) => mapping.text !== originalValue + ); + + const newMappings: EntityMapping[] = []; + let searchIndex = 0; + + // Chercher toutes les occurrences dans le texte source + while (true) { + const foundIndex = sourceText.indexOf( + originalValue, + searchIndex + ); + if (foundIndex === -1) break; + + // Vérifier que c'est une occurrence valide (limites de mots) + const isValidBoundary = + (foundIndex === 0 || + !/\w/.test(sourceText[foundIndex - 1])) && + (foundIndex + originalValue.length === + sourceText.length || + !/\w/.test( + sourceText[foundIndex + originalValue.length] + )); + + if (isValidBoundary) { + newMappings.push({ + text: originalValue, + entity_type: entityType, + start: foundIndex, + end: foundIndex + originalValue.length, + displayName: newLabel, + customColor: customColor, + }); + } + + searchIndex = foundIndex + 1; + } + + updatedMappings = [ + ...existingMappingsForOtherTexts, + ...newMappings, + ]; + } else { + // Logique existante pour une seule occurrence + if ( + wordStart !== undefined && + wordEnd !== undefined + ) { + const targetMapping = entityMappings.find( + (mapping) => + mapping.start === wordStart && + mapping.end === wordEnd + ); + + if (targetMapping) { + updatedMappings = entityMappings.map( + (mapping) => { + if ( + mapping.start === wordStart && + mapping.end === wordEnd + ) { + return { + ...mapping, + displayName: newLabel, + entity_type: entityType, + customColor: customColor, + }; + } + return mapping; + } + ); + } else { + const newMapping: EntityMapping = { + text: originalValue, + entity_type: entityType, + start: wordStart, + end: wordEnd, + displayName: newLabel, + customColor: customColor, + }; + updatedMappings = [...entityMappings, newMapping]; + } + } else { + // Fallback: logique existante + const existingMappingIndex = + entityMappings.findIndex( + (mapping) => mapping.text === originalValue + ); + + if (existingMappingIndex !== -1) { + updatedMappings = entityMappings.map( + (mapping, index) => { + if (index === existingMappingIndex) { + return { + ...mapping, + displayName: newLabel, + entity_type: entityType, + customColor: customColor, + }; + } + return mapping; + } + ); + } else { + const foundIndex = + sourceText.indexOf(originalValue); + if (foundIndex !== -1) { + const newMapping: EntityMapping = { + text: originalValue, + entity_type: entityType, + start: foundIndex, + end: foundIndex + originalValue.length, + displayName: newLabel, + customColor: customColor, + }; + updatedMappings = [ + ...entityMappings, + newMapping, + ]; + } else { + updatedMappings = entityMappings; + } + } + } + } + + console.log( + "✅ Mappings mis à jour:", + updatedMappings.length + ); + onMappingsUpdate( + updatedMappings.sort((a, b) => a.start - b.start) + ); + } + }} + onRemoveMapping={(originalValue, applyToAll) => { + if (onMappingsUpdate && entityMappings) { + console.log("🗑️ Suppression mapping:", { + originalValue, + applyToAll, + }); + + let filteredMappings: EntityMapping[]; + + if (applyToAll) { + // Supprimer toutes les occurrences + filteredMappings = entityMappings.filter( + (mapping) => mapping.text !== originalValue + ); + } else { + // Supprimer seulement la première occurrence + const firstIndex = entityMappings.findIndex( + (mapping) => mapping.text === originalValue + ); + if (firstIndex !== -1) { + filteredMappings = entityMappings.filter( + (_, index) => index !== firstIndex + ); + } else { + filteredMappings = entityMappings; + } + } + + console.log( + "✅ Mappings après suppression:", + filteredMappings.length + ); + onMappingsUpdate( + filteredMappings.sort((a, b) => a.start - b.start) + ); + } + }} + />
@@ -286,12 +486,37 @@ export const FileUploadComponent = ({ {/* Boutons d'action - Responsive mobile */} {canAnonymize && !isLoadingFile && (
+ {/* Sélecteur de catégorie - NOUVEAU */} + {onAnonymize && !outputText && ( +
+ + +
+ )} + {/* Bouton Anonymiser - seulement si pas encore anonymisé */} {onAnonymize && !outputText && (