This commit is contained in:
nBiqoz
2025-08-09 15:23:20 +02:00
parent e4c735cdc4
commit 74e56c956c
3 changed files with 209 additions and 84 deletions

View File

@@ -117,36 +117,74 @@ export const useAnonymization = ({
} }
}); });
const seen = new Set<string>(); // 2. Créer un mapping basé sur l'ordre d'apparition dans le texte anonymisé
const uniqueMappings: EntityMapping[] = []; const uniqueMappings: EntityMapping[] = [];
const addedCounts = new Map<string, number>();
// 2. N'ajouter que les entités réellement anonymisées avec un compteur // Vérifier que les données nécessaires sont disponibles
data.analyzerResults if (!data.analyzerResults || !data.anonymizedText) {
.sort((a, b) => a.start - b.start) // Trier par ordre d'apparition setEntityMappings([]);
.forEach((result) => { return;
const entityType = result.entity_type; }
const maxCount = tagCounts.get(entityType) || 0;
const currentCount = addedCounts.get(entityType) || 0;
if (currentCount < maxCount) { const entityCounters = new Map<string, number>();
const originalValue = textToProcess.substring(
result.start, // Parcourir le texte anonymisé pour trouver les tags dans l'ordre
result.end const anonymizedText = data.anonymizedText;
); const allMatches: Array<{
match: RegExpMatchArray;
entityType: string;
position: number;
}> = [];
// Trouver tous les tags dans le texte anonymisé
patterns.forEach(pattern => {
const entityTypeKey = pattern.regex.toString().match(/<([A-Z_]+)>/)?.[1];
if (entityTypeKey) {
const regex = new RegExp(pattern.regex.source, 'g');
let match;
while ((match = regex.exec(anonymizedText)) !== null) {
allMatches.push({
match,
entityType: entityTypeKey,
position: match.index
});
}
}
});
// Trier par position dans le texte anonymisé
allMatches.sort((a, b) => a.position - b.position);
// Créer les mappings dans l'ordre d'apparition
const seen = new Set<string>();
allMatches.forEach(({ entityType }) => {
const frenchLabel = entityTypeMap.get(entityType) || entityType; const frenchLabel = entityTypeMap.get(entityType) || entityType;
const uniqueKey = `${frenchLabel}|${originalValue}`; const currentCount = (entityCounters.get(entityType) || 0) + 1;
entityCounters.set(entityType, currentCount);
// Trouver l'entité correspondante dans les résultats d'analyse
const correspondingResult = data.analyzerResults
?.filter(result => result.entity_type === entityType)
.find(result => {
const originalValue = textToProcess.substring(result.start, result.end);
const uniqueKey = `${frenchLabel}|${originalValue}|${currentCount}`;
return !seen.has(uniqueKey);
});
if (correspondingResult) {
const originalValue = textToProcess.substring(
correspondingResult.start,
correspondingResult.end
);
const uniqueKey = `${frenchLabel}|${originalValue}|${currentCount}`;
if (!seen.has(uniqueKey)) { if (!seen.has(uniqueKey)) {
const newCount = (addedCounts.get(entityType) || 0) + 1;
addedCounts.set(entityType, newCount);
uniqueMappings.push({ uniqueMappings.push({
entityType: frenchLabel, entityType: frenchLabel,
originalValue: originalValue, originalValue: originalValue,
anonymizedValue: `${frenchLabel} [${newCount}]`, anonymizedValue: `${frenchLabel} [${currentCount}]`,
startIndex: result.start, startIndex: correspondingResult.start,
endIndex: result.end, endIndex: correspondingResult.end,
}); });
seen.add(uniqueKey); seen.add(uniqueKey);
} }

View File

@@ -10,6 +10,7 @@ import { SampleTextComponent } from "./SampleTextComponent";
import { SupportedDataTypes } from "./SupportedDataTypes"; import { SupportedDataTypes } from "./SupportedDataTypes";
import { AnonymizationInterface } from "./AnonymizationInterface"; import { AnonymizationInterface } from "./AnonymizationInterface";
import { highlightEntities } from "../utils/highlightEntities"; import { highlightEntities } from "../utils/highlightEntities";
import { useState } from "react";
interface EntityMapping { interface EntityMapping {
originalValue: string; originalValue: string;
@@ -57,6 +58,86 @@ export const FileUploadComponent = ({
setIsExampleLoaded, setIsExampleLoaded,
entityMappings, // Ajouter cette prop ici entityMappings, // Ajouter cette prop ici
}: FileUploadComponentProps) => { }: FileUploadComponentProps) => {
const [isDragOver, setIsDragOver] = useState(false);
// Fonction pour valider le type de fichier
const isValidFileType = (file: File) => {
const allowedTypes = ["text/plain", "application/pdf"];
const allowedExtensions = [".txt", ".pdf"];
return (
allowedTypes.includes(file.type) ||
allowedExtensions.some((ext) => file.name.toLowerCase().endsWith(ext))
);
};
// Gestionnaires de glisser-déposer
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Vérifier si on quitte vraiment la zone de drop
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
const file = files[0];
// Vérifier le type de fichier
if (!isValidFileType(file)) {
alert(
"Type de fichier non supporté. Veuillez sélectionner un fichier PDF ou TXT."
);
return;
}
// Vérifier la taille (5MB max)
if (file.size > 5 * 1024 * 1024) {
alert("Le fichier est trop volumineux. Taille maximale : 5MB.");
return;
}
const syntheticEvent = {
target: { files: [file] },
} as unknown as React.ChangeEvent<HTMLInputElement>;
handleFileChange(syntheticEvent);
}
};
// Gestionnaire de changement de fichier modifié pour valider le type
const handleFileChangeWithValidation = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const file = e.target.files?.[0];
if (file && !isValidFileType(file)) {
alert(
"Type de fichier non supporté. Veuillez sélectionner un fichier PDF ou TXT."
);
e.target.value = ""; // Reset l'input
return;
}
handleFileChange(e);
};
// On passe en preview seulement si : // On passe en preview seulement si :
// 1. Un fichier est uploadé OU // 1. Un fichier est uploadé OU
// 2. On a un résultat d'anonymisation // 2. On a un résultat d'anonymisation
@@ -144,7 +225,7 @@ export const FileUploadComponent = ({
<div className="text-xs sm:text-sm text-gray-700 whitespace-pre-wrap break-words overflow-wrap-anywhere leading-relaxed"> <div className="text-xs sm:text-sm text-gray-700 whitespace-pre-wrap break-words overflow-wrap-anywhere leading-relaxed">
{highlightEntities( {highlightEntities(
outputText || "Aucun contenu à afficher", outputText || "Aucun contenu à afficher",
entityMappings // Ajouter les mappings ici entityMappings
)} )}
</div> </div>
</div> </div>
@@ -410,30 +491,59 @@ export const FileUploadComponent = ({
</div> </div>
{/* Colonne droite - Zone upload */} {/* Colonne droite - Zone upload */}
<div className="border-2 border-dashed border-[#092727] rounded-xl bg-gray-50 hover:bg-gray-100 hover:border-[#0a3030] transition-all duration-300"> <div
className={`border-2 border-dashed rounded-xl transition-all duration-300 ${
isDragOver
? "border-[#f7ab6e] bg-[#f7ab6e]/10 scale-105"
: "border-[#092727] bg-gray-50 hover:bg-gray-100 hover:border-[#0a3030]"
}`}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<label className="flex flex-col items-center justify-center cursor-pointer group p-3 sm:p-4 h-full min-h-[200px]"> <label className="flex flex-col items-center justify-center cursor-pointer group p-3 sm:p-4 h-full min-h-[200px]">
{/* Upload Icon */} {/* Upload Icon */}
<div className="w-10 h-10 bg-[#f7ab6e] rounded-full flex items-center justify-center mb-3 transition-colors duration-300"> <div
className={`w-10 h-10 rounded-full flex items-center justify-center mb-3 transition-colors duration-300 ${
isDragOver ? "bg-[#f7ab6e] scale-110" : "bg-[#f7ab6e]"
}`}
>
<Upload className="h-5 w-5 text-white" /> <Upload className="h-5 w-5 text-white" />
</div> </div>
{/* Titre */} {/* Titre */}
<h3 className="text-lg font-semibold text-[#092727] mb-1 group-hover:text-[#0a3030] transition-colors duration-300 text-center"> <h3
Déposez votre fichier ici className={`text-lg font-semibold mb-1 transition-colors duration-300 text-center ${
isDragOver
? "text-[#f7ab6e]"
: "text-[#092727] group-hover:text-[#0a3030]"
}`}
>
{isDragOver
? "Déposez votre fichier"
: "Déposez votre fichier ici"}
</h3> </h3>
<p className="text-sm text-[#092727] opacity-80 mb-3 text-center group-hover:opacity-90 transition-opacity duration-300"> <p
className={`text-sm mb-3 text-center transition-opacity duration-300 ${
isDragOver
? "text-[#f7ab6e] opacity-90"
: "text-[#092727] opacity-80 group-hover:opacity-90"
}`}
>
ou cliquez pour sélectionner ou cliquez pour sélectionner
</p> </p>
{/* File Info */} {/* File Info */}
<div className="flex s items-center gap-1 text-xs text-[#092727] opacity-60"> <div className="flex items-center gap-1 text-xs text-[#092727] opacity-60">
<span>📄 Fichiers TXT, PDF</span> - <span>Max 5MB</span> <span>📄 Fichiers PDF et TXT uniquement</span> -{" "}
<span>Max 5MB</span>
</div> </div>
{/* Hidden Input */} {/* Hidden Input */}
<input <input
type="file" type="file"
onChange={handleFileChange} onChange={handleFileChangeWithValidation}
accept=".txt,.pdf" accept=".txt,.pdf"
className="hidden" className="hidden"
/> />

View File

@@ -108,29 +108,11 @@ export const highlightEntities = (
element: ReactNode; element: ReactNode;
}> = []; }> = [];
// Si on a des mappings, on les utilise pour créer un mapping des valeurs anonymisées
const anonymizedValueMap = new Map<
string,
{ label: string; className: string }
>();
if (entityMappings) {
entityMappings.forEach((mapping) => {
// Trouver le pattern correspondant au type d'entité
const pattern = patterns.find((p) => p.label === mapping.entityType);
if (pattern) {
anonymizedValueMap.set(mapping.anonymizedValue, {
label: mapping.anonymizedValue,
className: pattern.className,
});
}
});
}
// Trouver toutes les correspondances // Trouver toutes les correspondances
patterns.forEach((pattern, patternIndex) => { patterns.forEach((pattern, patternIndex) => {
const regex = new RegExp(pattern.regex.source, pattern.regex.flags); const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
let match; let match;
let matchCount = 0; // Compteur pour ce type d'entité
while ((match = regex.exec(text)) !== null) { while ((match = regex.exec(text)) !== null) {
const start = match.index; const start = match.index;
@@ -143,36 +125,31 @@ export const highlightEntities = (
); );
if (!hasOverlap) { if (!hasOverlap) {
// Chercher si on a un mapping pour cette entité matchCount++; // Incrémenter le compteur pour ce type
let displayLabel = pattern.label; let displayLabel = pattern.label;
const displayClass = pattern.className; // Changé de 'let' à 'const' const displayClass = pattern.className;
if (entityMappings) { if (entityMappings) {
// Compter les occurrences précédentes du même type pour déterminer le numéro // Chercher le mapping correspondant à cette position et ce type
const entityType = pattern.label;
const previousMatches = replacements.filter((r) => {
// Correction du type 'any' en utilisant une interface plus spécifique
const element = r.element as React.ReactElement<{
className?: string;
}>;
const prevPattern = patterns.find(
(p) =>
p.className ===
element?.props?.className?.split(" ")[0] +
" " +
element?.props?.className?.split(" ")[1]
);
return prevPattern?.label === entityType;
}).length;
const matchingMapping = entityMappings.find( const matchingMapping = entityMappings.find(
(mapping) => mapping.entityType === entityType (mapping) => mapping.entityType === pattern.label
); );
if (matchingMapping) { if (matchingMapping) {
// Utiliser le compteur pour déterminer le bon numéro avec des crochets // Utiliser directement la valeur anonymisée du mapping
const entityCount = previousMatches + 1; // qui correspond à cette occurrence (basée sur l'ordre d'apparition)
displayLabel = `${entityType} [${entityCount}]`; const entityType = pattern.label;
const mappingsOfThisType = entityMappings.filter(
(m) => m.entityType === entityType
);
// Prendre le mapping correspondant à cette occurrence
if (mappingsOfThisType[matchCount - 1]) {
displayLabel = mappingsOfThisType[matchCount - 1].anonymizedValue;
} else {
// Fallback si pas de mapping trouvé
displayLabel = `${entityType} [${matchCount}]`;
}
} }
} }