glisser
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}]`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user