finalyse
This commit is contained in:
@@ -17,108 +17,97 @@ export const AnonymizationInterface = ({
|
|||||||
|
|
||||||
const anonymizedTypes = new Set<string>();
|
const anonymizedTypes = new Set<string>();
|
||||||
|
|
||||||
// ✅ 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("<PERSON>")) {
|
if (outputText.includes("<PERSON>")) {
|
||||||
anonymizedTypes.add("Prénoms");
|
anonymizedTypes.add("Prénoms");
|
||||||
anonymizedTypes.add("Noms de famille");
|
anonymizedTypes.add("Noms de famille");
|
||||||
anonymizedTypes.add("Noms complets");
|
anonymizedTypes.add("Noms complets");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emails (EMAIL_ADDRESS)
|
// EMAIL_ADDRESS -> Adresses e-mail
|
||||||
if (outputText.includes("<EMAIL_ADDRESS>")) {
|
if (outputText.includes("<EMAIL_ADDRESS>")) {
|
||||||
anonymizedTypes.add("Adresses e-mail");
|
anonymizedTypes.add("Adresses e-mail");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Téléphones (PHONE_NUMBER)
|
// PHONE_NUMBER -> Numéros de téléphone
|
||||||
if (outputText.includes("<PHONE_NUMBER>")) {
|
if (outputText.includes("<PHONE_NUMBER>")) {
|
||||||
anonymizedTypes.add("Numéros de téléphone");
|
anonymizedTypes.add("Numéros de téléphone");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adresses (LOCATION)
|
// BE_PHONE_NUMBER -> aussi Numéros de téléphone
|
||||||
|
if (outputText.includes("<BE_PHONE_NUMBER>")) {
|
||||||
|
anonymizedTypes.add("Numéros de téléphone");
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOCATION -> Adresses
|
||||||
if (outputText.includes("<LOCATION>")) {
|
if (outputText.includes("<LOCATION>")) {
|
||||||
anonymizedTypes.add("Adresses");
|
anonymizedTypes.add("Adresses");
|
||||||
}
|
}
|
||||||
|
|
||||||
// IBAN (IBAN)
|
// BE_ADDRESS -> aussi Adresses
|
||||||
if (outputText.includes("<IBAN>")) {
|
if (outputText.includes("<BE_ADDRESS>")) {
|
||||||
anonymizedTypes.add("Numéros d'ID"); // Ou créer une nouvelle catégorie "IBAN"
|
anonymizedTypes.add("Adresses");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Organisations (ORGANIZATION)
|
// FLEXIBLE_DATE ou DATE_TIME -> Dates
|
||||||
if (outputText.includes("<ORGANIZATION>")) {
|
if (
|
||||||
anonymizedTypes.add("Noms de domaine"); // Ou adapter selon vos besoins
|
outputText.includes("<FLEXIBLE_DATE>") ||
|
||||||
}
|
outputText.includes("<DATE_TIME>")
|
||||||
|
) {
|
||||||
// Dates personnalisées (CUSTOM_DATE)
|
|
||||||
if (outputText.includes("<CUSTOM_DATE>")) {
|
|
||||||
anonymizedTypes.add("Dates");
|
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("<IBAN>")) {
|
||||||
|
anonymizedTypes.add("Coordonnées bancaires");
|
||||||
|
}
|
||||||
|
|
||||||
|
// CREDIT_CARD -> aussi Coordonnées bancaires (au lieu de Valeurs numériques)
|
||||||
|
if (outputText.includes("<CREDIT_CARD>")) {
|
||||||
|
anonymizedTypes.add("Coordonnées bancaires");
|
||||||
|
}
|
||||||
|
|
||||||
|
// NRP -> Numéros d'ID
|
||||||
|
if (outputText.includes("<NRP>")) {
|
||||||
|
anonymizedTypes.add("Numéros d'ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
// BE_PRO_ID -> Numéros d'ID
|
||||||
|
if (outputText.includes("<BE_PRO_ID>")) {
|
||||||
|
anonymizedTypes.add("Numéros d'ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
// BE_ENTERPRISE_NUMBER -> Numéros d'ID
|
||||||
if (outputText.includes("<BE_ENTERPRISE_NUMBER>")) {
|
if (outputText.includes("<BE_ENTERPRISE_NUMBER>")) {
|
||||||
anonymizedTypes.add("Numéros d'ID");
|
anonymizedTypes.add("Numéros d'ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ANCIENS PATTERNS (pour compatibilité)
|
// URL -> Noms de domaine
|
||||||
|
if (outputText.includes("<URL>")) {
|
||||||
// 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)) {
|
|
||||||
anonymizedTypes.add("Noms de domaine");
|
anonymizedTypes.add("Noms de domaine");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valeurs numériques
|
// CREDIT_CARD -> Coordonnées bancaires (supprimer la duplication)
|
||||||
if (
|
if (outputText.includes("<CREDIT_CARD>")) {
|
||||||
/\[\d+\]/.test(outputText) &&
|
anonymizedTypes.add("Coordonnées bancaires");
|
||||||
!outputText.includes("[Téléphone") &&
|
}
|
||||||
!outputText.includes("[Montant")
|
|
||||||
) {
|
// Supprimer cette ligne dupliquée :
|
||||||
|
// if (outputText.includes("<CREDIT_CARD>")) {
|
||||||
|
// anonymizedTypes.add("Valeurs numériques");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// IP_ADDRESS -> Valeurs numériques
|
||||||
|
if (outputText.includes("<IP_ADDRESS>")) {
|
||||||
anonymizedTypes.add("Valeurs numériques");
|
anonymizedTypes.add("Valeurs numériques");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Texte personnalisé (si du texte a été modifié mais pas avec les patterns spécifiques)
|
// BE_VAT -> Valeurs numériques
|
||||||
if (sourceText !== outputText && anonymizedTypes.size === 0) {
|
if (outputText.includes("<BE_VAT>")) {
|
||||||
anonymizedTypes.add("Texte personnalisé");
|
anonymizedTypes.add("Valeurs numériques");
|
||||||
}
|
}
|
||||||
|
|
||||||
return anonymizedTypes;
|
return anonymizedTypes;
|
||||||
@@ -133,7 +122,7 @@ export const AnonymizationInterface = ({
|
|||||||
items: ["Noms de famille", "Adresses", "Dates"],
|
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é"],
|
items: ["Adresses e-mail", "Valeurs monétaires", "Texte personnalisé"],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { patterns } from "@/app/utils/highlightEntities";
|
||||||
|
|
||||||
interface EntityMapping {
|
interface EntityMapping {
|
||||||
originalValue: string;
|
originalValue: string;
|
||||||
@@ -8,25 +9,18 @@ interface EntityMapping {
|
|||||||
endIndex: number;
|
endIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nouvelle interface pour les résultats de Presidio Analyzer
|
// L'API retourne des objets avec snake_case
|
||||||
interface PresidioAnalyzerResult {
|
interface PresidioAnalyzerResult {
|
||||||
entity_type: string;
|
entity_type: string;
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
score: 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 {
|
interface ProcessDocumentResponse {
|
||||||
text?: string;
|
text?: string; // Texte original en cas de fallback
|
||||||
anonymizedText?: string;
|
anonymizedText?: string;
|
||||||
piiCount?: number;
|
|
||||||
analyzerResults?: PresidioAnalyzerResult[];
|
analyzerResults?: PresidioAnalyzerResult[];
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
@@ -66,101 +60,105 @@ export const useAnonymization = ({
|
|||||||
setEntityMappings([]);
|
setEntityMappings([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("🚀 Début anonymisation avec Presidio");
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
if (uploadedFile) {
|
if (uploadedFile) {
|
||||||
console.log("📁 Traitement fichier:", {
|
|
||||||
name: uploadedFile.name,
|
|
||||||
type: uploadedFile.type,
|
|
||||||
size: uploadedFile.size
|
|
||||||
});
|
|
||||||
formData.append("file", uploadedFile);
|
formData.append("file", uploadedFile);
|
||||||
} else {
|
} else {
|
||||||
console.log("📝 Traitement texte saisi");
|
|
||||||
const textBlob = new Blob([textToProcess], { type: "text/plain" });
|
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);
|
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", {
|
const response = await fetch("/api/process-document", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
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) {
|
if (!response.ok) {
|
||||||
let errorMessage = `Erreur HTTP: ${response.status}`;
|
let errorMessage = `Erreur HTTP: ${response.status}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const responseText = await response.text();
|
const errorData = await response.json();
|
||||||
console.log("📄 Contenu de l'erreur:", responseText);
|
if (errorData.error) errorMessage = errorData.error;
|
||||||
|
} catch {
|
||||||
if (responseText.trim()) {
|
/* Ignore */
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: ProcessDocumentResponse = await response.json();
|
const data: ProcessDocumentResponse = await response.json();
|
||||||
console.log("📊 Réponse API:", data);
|
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.anonymizedText) {
|
// Utiliser camelCase pour les propriétés de la réponse principale
|
||||||
console.log("✅ Anonymisation réussie avec Presidio");
|
if (data.anonymizedText && data.analyzerResults) {
|
||||||
setOutputText(data.anonymizedText);
|
setOutputText(data.anonymizedText);
|
||||||
|
|
||||||
// Extraire les mappings depuis les résultats Presidio (plus d'erreur 'any')
|
const entityTypeMap = new Map<string, string>();
|
||||||
if (data.analyzerResults && data.text) {
|
patterns.forEach((p) => {
|
||||||
const mappings: EntityMapping[] = data.analyzerResults.map(
|
const match = p.regex.toString().match(/<([A-Z_]+)>/);
|
||||||
(entity: PresidioAnalyzerResult, index: number) => ({
|
if (match && match[1]) {
|
||||||
originalValue: data.text!.substring(entity.start, entity.end),
|
entityTypeMap.set(match[1], p.label);
|
||||||
anonymizedValue: `[${entity.entity_type}${index + 1}]`,
|
}
|
||||||
entityType: entity.entity_type,
|
});
|
||||||
startIndex: entity.start,
|
|
||||||
endIndex: entity.end,
|
// 1. Compter les occurrences de chaque tag d'entité dans le texte anonymisé
|
||||||
})
|
const tagCounts = new Map<string, number>();
|
||||||
);
|
data.analyzerResults.forEach((result) => {
|
||||||
setEntityMappings(mappings);
|
const tag = `<${result.entity_type}>`;
|
||||||
console.log("📋 Entités détectées:", mappings.length);
|
if (!tagCounts.has(result.entity_type)) {
|
||||||
console.log("🔍 Détails des entités:", mappings);
|
const count = (
|
||||||
}
|
data.anonymizedText?.match(new RegExp(tag, "g")) || []
|
||||||
|
).length;
|
||||||
|
tagCounts.set(result.entity_type, count);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const uniqueMappings: EntityMapping[] = [];
|
||||||
|
const addedCounts = new Map<string, number>();
|
||||||
|
|
||||||
|
// 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) {
|
} else if (data.text) {
|
||||||
console.log(
|
|
||||||
"⚠️ Fallback: Presidio non disponible, texte original retourné"
|
|
||||||
);
|
|
||||||
setOutputText(data.text);
|
setOutputText(data.text);
|
||||||
setError("Presidio temporairement indisponible. Texte non anonymisé.");
|
setError("Presidio temporairement indisponible. Texte non anonymisé.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erreur anonymisation complète:", error);
|
|
||||||
setError(
|
setError(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import { SupportedDataTypes } from "./SupportedDataTypes";
|
|||||||
import { AnonymizationInterface } from "./AnonymizationInterface";
|
import { AnonymizationInterface } from "./AnonymizationInterface";
|
||||||
import { highlightEntities } from "../utils/highlightEntities";
|
import { highlightEntities } from "../utils/highlightEntities";
|
||||||
|
|
||||||
|
interface EntityMapping {
|
||||||
|
originalValue: string;
|
||||||
|
anonymizedValue: string;
|
||||||
|
entityType: string;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface FileUploadComponentProps {
|
interface FileUploadComponentProps {
|
||||||
uploadedFile: File | null;
|
uploadedFile: File | null;
|
||||||
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
@@ -26,8 +34,9 @@ interface FileUploadComponentProps {
|
|||||||
outputText?: string;
|
outputText?: string;
|
||||||
copyToClipboard?: () => void;
|
copyToClipboard?: () => void;
|
||||||
downloadText?: () => void;
|
downloadText?: () => void;
|
||||||
isExampleLoaded?: boolean; // NOUVEAU
|
isExampleLoaded?: boolean;
|
||||||
setIsExampleLoaded?: (loaded: boolean) => void; // NOUVEAU
|
setIsExampleLoaded?: (loaded: boolean) => void;
|
||||||
|
entityMappings?: EntityMapping[]; // Ajouter cette prop
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadComponent = ({
|
export const FileUploadComponent = ({
|
||||||
@@ -45,7 +54,8 @@ export const FileUploadComponent = ({
|
|||||||
outputText,
|
outputText,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
downloadText,
|
downloadText,
|
||||||
setIsExampleLoaded, // NOUVEAU - Ajouté ici
|
setIsExampleLoaded,
|
||||||
|
entityMappings, // Ajouter cette prop ici
|
||||||
}: FileUploadComponentProps) => {
|
}: FileUploadComponentProps) => {
|
||||||
// On passe en preview seulement si :
|
// On passe en preview seulement si :
|
||||||
// 1. Un fichier est uploadé OU
|
// 1. Un fichier est uploadé OU
|
||||||
@@ -133,7 +143,8 @@ export const FileUploadComponent = ({
|
|||||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4 max-h-72 overflow-y-auto overflow-x-hidden">
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4 max-h-72 overflow-y-auto overflow-x-hidden">
|
||||||
<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
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +217,7 @@ export const FileUploadComponent = ({
|
|||||||
<button
|
<button
|
||||||
onClick={onAnonymize}
|
onClick={onAnonymize}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
className="w-full sm:w-auto bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 disabled:opacity-50 disabled:cursor-not-allowed text-white px-6 py-3 rounded-lg text-sm font-medium transition-colors duration-300 flex items-center justify-center space-x-3"
|
className="w-full sm:w-auto bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 text-black px-6 py-3 rounded-lg text-sm font-medium transition-colors duration-300 flex items-center justify-center space-x-3 disabled:bg-gray-300 disabled:text-gray-800 disabled:font-bold disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
@@ -288,20 +299,22 @@ export const FileUploadComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Titre */}
|
{/* Titre */}
|
||||||
<h3 className="text-sm font-semibold text-[#092727] mb-1 text-center">
|
<h3 className="text-lg font-semibold text-[#092727] mb-1 text-center">
|
||||||
Saisissez votre texte
|
Saisissez votre texte
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-[#092727] opacity-80 mb-2 text-center">
|
<p className="text-md text-[#092727] opacity-80 mb-2 text-center">
|
||||||
Tapez ou collez votre texte ici
|
Tapez ou collez votre texte ici
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Zone de texte éditable */}
|
{/* Zone de texte éditable */}
|
||||||
<div className="relative border-2 border-gray-200 rounded-lg bg-white focus-within:border-[#f7ab6e] focus-within:ring-1 focus-within:ring-[#f7ab6e]/20 transition-all duration-300">
|
<div className="relative border-2 border-gray-200 rounded-lg bg-white focus-within:border-[#f7ab6e] focus-within:ring-1 focus-within:ring-[#f7ab6e]/20 transition-all duration-300">
|
||||||
{/* Zone pour le texte - SANS overflow */}
|
{/* Zone pour le texte - SANS overflow */}
|
||||||
<div className="h-40 p-2 pb-6 relative"> {/* Ajout de pb-6 pour le compteur */}
|
<div className="h-40 p-2 pb-6 relative">
|
||||||
|
{" "}
|
||||||
|
{/* Ajout de pb-6 pour le compteur */}
|
||||||
{/* Placeholder personnalisé avec lien cliquable */}
|
{/* Placeholder personnalisé avec lien cliquable */}
|
||||||
{!sourceText && (
|
{!sourceText && (
|
||||||
<div className="absolute inset-2 text-gray-400 text-xs leading-relaxed pointer-events-none">
|
<div className="absolute inset-2 text-gray-400 text-md leading-relaxed pointer-events-none">
|
||||||
<span>Commencez à taper du texte, ou </span>
|
<span>Commencez à taper du texte, ou </span>
|
||||||
<SampleTextComponent
|
<SampleTextComponent
|
||||||
setSourceText={setSourceText}
|
setSourceText={setSourceText}
|
||||||
@@ -312,7 +325,6 @@ export const FileUploadComponent = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
value={sourceText}
|
value={sourceText}
|
||||||
onChange={(e) => setSourceText(e.target.value)}
|
onChange={(e) => setSourceText(e.target.value)}
|
||||||
@@ -330,21 +342,29 @@ export const FileUploadComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Barre du bas avec sélecteur et bouton */}
|
{/* Barre du bas avec sélecteur et bouton */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between p-2 border-t border-gray-200 bg-gray-50 space-y-2 sm:space-y-0">
|
<div className="flex flex-col p-2 border-t border-gray-200 bg-gray-50 space-y-2">
|
||||||
{/* Sélecteur de type d'anonymisation */}
|
{/* Sélecteur de type d'anonymisation */}
|
||||||
<div className="flex flex-col w-full sm:w-auto">
|
<div className="flex flex-col w-full">
|
||||||
<label className="text-xs text-gray-500 mb-1">Type de données :</label>
|
<label className="text-xs text-gray-500 mb-1">
|
||||||
|
Type de données :
|
||||||
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select className="w-full appearance-none bg-white border border-gray-300 text-gray-700 text-xs rounded-md pl-3 pr-8 py-2 focus:outline-none focus:ring-1 focus:ring-[#f7ab6e] focus:border-[#f7ab6e] transition-colors duration-200">
|
||||||
className="w-full sm:w-auto appearance-none bg-white border border-gray-300 text-gray-700 text-xs rounded-md pl-3 pr-8 py-2 focus:outline-none focus:ring-1 focus:ring-[#f7ab6e] focus:border-[#f7ab6e] transition-colors duration-200"
|
<option>
|
||||||
>
|
Informations Personnellement Identifiables (PII)
|
||||||
<option>Informations Personnellement Identifiables (PII)</option>
|
</option>
|
||||||
<option disabled style={{ color: 'lightgray' }}>
|
<option disabled style={{ color: "lightgray" }}>
|
||||||
PII + Données Business (En développement)
|
PII + Données Business (En développement)
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||||
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
|
<svg
|
||||||
|
className="fill-current h-4 w-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,7 +373,7 @@ export const FileUploadComponent = ({
|
|||||||
<button
|
<button
|
||||||
onClick={onAnonymize}
|
onClick={onAnonymize}
|
||||||
disabled={isProcessing || !sourceText.trim()}
|
disabled={isProcessing || !sourceText.trim()}
|
||||||
className="w-full sm:w-auto bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 disabled:opacity-50 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg text-xs font-medium transition-colors duration-300 flex items-center justify-center space-x-2 shadow-sm"
|
className="w-full bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 text-black px-4 py-2 rounded-lg text-xs font-medium transition-colors duration-300 flex items-center justify-center space-x-2 shadow-sm disabled:bg-gray-300 disabled:text-gray-800 disabled:font-bold disabled:cursor-not-allowed"
|
||||||
title={
|
title={
|
||||||
sourceText.trim()
|
sourceText.trim()
|
||||||
? "Anonymiser les données"
|
? "Anonymiser les données"
|
||||||
@@ -393,22 +413,21 @@ export const FileUploadComponent = ({
|
|||||||
<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 border-[#092727] rounded-xl bg-gray-50 hover:bg-gray-100 hover:border-[#0a3030] transition-all duration-300">
|
||||||
<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-[#092727] group-hover:bg-[#0a3030] rounded-full flex items-center justify-center mb-3 transition-colors duration-300">
|
<div className="w-10 h-10 bg-[#f7ab6e] rounded-full flex items-center justify-center mb-3 transition-colors duration-300">
|
||||||
<Upload className="h-5 w-5 text-white" />
|
<Upload className="h-5 w-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Titre */}
|
{/* Titre */}
|
||||||
<h3 className="text-sm font-semibold text-[#092727] mb-1 group-hover:text-[#0a3030] transition-colors duration-300 text-center">
|
<h3 className="text-lg font-semibold text-[#092727] mb-1 group-hover:text-[#0a3030] transition-colors duration-300 text-center">
|
||||||
Déposez votre fichier ici
|
Déposez votre fichier ici
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-[#092727] opacity-80 mb-3 text-center group-hover:opacity-90 transition-opacity duration-300">
|
<p className="text-sm text-[#092727] opacity-80 mb-3 text-center group-hover:opacity-90 transition-opacity duration-300">
|
||||||
ou cliquez pour sélectionner
|
ou cliquez pour sélectionner
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* File Info */}
|
{/* File Info */}
|
||||||
<div className="flex flex-col items-center gap-1 text-xs text-[#092727] opacity-60">
|
<div className="flex s items-center gap-1 text-xs text-[#092727] opacity-60">
|
||||||
<span>📄 Fichiers TXT, PDF</span>
|
<span>📄 Fichiers TXT, PDF</span> - <span>Max 5MB</span>
|
||||||
<span>Max 5MB</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hidden Input */}
|
{/* Hidden Input */}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl mx-auto mb-4 px-2">
|
<div className="w-full max-w-2xl mx-auto mb-4 px-2">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-start justify-center">
|
||||||
<div className="flex items-center w-full max-w-md">
|
<div className="flex items-start w-full max-w-md">
|
||||||
{steps.map((step, index) => {
|
{steps.map((step, index) => {
|
||||||
const stepNumber = index + 1;
|
const stepNumber = index + 1;
|
||||||
const isCompleted = stepNumber < currentStep;
|
const isCompleted = stepNumber < currentStep;
|
||||||
@@ -37,7 +37,10 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
|
|||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
{/* Step Circle and Label */}
|
{/* Step Circle and Label */}
|
||||||
<div className="flex flex-col items-center">
|
<div
|
||||||
|
className="flex flex-col items-center text-center"
|
||||||
|
style={{ flex: "0 0 auto" }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-5 h-5 sm:w-6 sm:h-6 rounded-full flex items-center justify-center font-medium text-xs transition-all duration-300 ${
|
className={`w-5 h-5 sm:w-6 sm:h-6 rounded-full flex items-center justify-center font-medium text-xs transition-all duration-300 ${
|
||||||
isCompleted
|
isCompleted
|
||||||
@@ -50,7 +53,7 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
|
|||||||
{getStepIcon(stepNumber, isCompleted)}
|
{getStepIcon(stepNumber, isCompleted)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`mt-1 text-[8px] sm:text-[10px] font-medium text-center max-w-[60px] sm:max-w-none leading-tight ${
|
className={`mt-1 text-sm font-medium leading-tight ${
|
||||||
isCompleted || isCurrent
|
isCompleted || isCurrent
|
||||||
? "text-[#092727]"
|
? "text-[#092727]"
|
||||||
: "text-gray-500"
|
: "text-gray-500"
|
||||||
@@ -58,24 +61,19 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
|
|||||||
style={{
|
style={{
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
hyphens: "auto",
|
hyphens: "auto",
|
||||||
|
minWidth: "60px", // Assure un espace minimum
|
||||||
|
maxWidth: "120px", // Empêche de devenir trop large
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{step === "Anonymisation" ? (
|
{step}
|
||||||
<>
|
|
||||||
<span className="hidden sm:inline">Anonymisation</span>
|
|
||||||
<span className="sm:hidden">Anonym.</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
step
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connector Line */}
|
{/* Connector Line */}
|
||||||
{index < steps.length - 1 && (
|
{index < steps.length - 1 && (
|
||||||
<div className="flex-1 flex items-center justify-center px-2 sm:px-4">
|
<div className="flex-1 flex items-center justify-center px-1 sm:px-2 mt-2.5 sm:mt-3">
|
||||||
<div
|
<div
|
||||||
className={`h-0.5 w-full max-w-[40px] sm:max-w-[60px] transition-all duration-300 ${
|
className={`h-0.5 w-full transition-all duration-300 ${
|
||||||
stepNumber < currentStep
|
stepNumber < currentStep
|
||||||
? "bg-[#f7ab6e]"
|
? "bg-[#f7ab6e]"
|
||||||
: "bg-gray-200"
|
: "bg-gray-200"
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { Copy, Download, AlertTriangle } from "lucide-react";
|
import { Copy, Download, AlertTriangle } from "lucide-react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface EntityMapping {
|
||||||
|
originalValue: string;
|
||||||
|
anonymizedValue: string;
|
||||||
|
entityType: string;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface ResultPreviewComponentProps {
|
interface ResultPreviewComponentProps {
|
||||||
outputText: string;
|
outputText: string;
|
||||||
copyToClipboard: () => void;
|
copyToClipboard: () => void;
|
||||||
downloadText: () => void;
|
downloadText: () => void;
|
||||||
highlightEntities: (text: string) => ReactNode;
|
highlightEntities: (text: string, mappings?: EntityMapping[]) => ReactNode;
|
||||||
|
entityMappings?: EntityMapping[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResultPreviewComponent = ({
|
export const ResultPreviewComponent = ({
|
||||||
@@ -13,6 +22,7 @@ export const ResultPreviewComponent = ({
|
|||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
downloadText,
|
downloadText,
|
||||||
highlightEntities,
|
highlightEntities,
|
||||||
|
entityMappings,
|
||||||
}: ResultPreviewComponentProps) => {
|
}: ResultPreviewComponentProps) => {
|
||||||
if (!outputText) return null;
|
if (!outputText) return null;
|
||||||
|
|
||||||
@@ -48,7 +58,7 @@ export const ResultPreviewComponent = ({
|
|||||||
<div className="flex-1 p-4 overflow-hidden">
|
<div className="flex-1 p-4 overflow-hidden">
|
||||||
<div className="h-full min-h-[300px] text-[#092727] whitespace-pre-wrap overflow-y-auto">
|
<div className="h-full min-h-[300px] text-[#092727] whitespace-pre-wrap overflow-y-auto">
|
||||||
<div className="leading-relaxed">
|
<div className="leading-relaxed">
|
||||||
{highlightEntities(outputText)}
|
{highlightEntities(outputText, entityMappings)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const SupportedDataTypes = () => {
|
|||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<span>• Noms complets</span>
|
<span>• Noms complets</span>
|
||||||
<span>• Numéros d'ID</span>
|
<span>• Numéros d'ID</span>
|
||||||
<span>• Valeurs numériques</span>
|
<span>• Coordonnées bancaires</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<span>• Adresses e-mail</span>
|
<span>• Adresses e-mail</span>
|
||||||
|
|||||||
@@ -107,8 +107,9 @@ export default function Home() {
|
|||||||
outputText={outputText}
|
outputText={outputText}
|
||||||
copyToClipboard={copyToClipboard}
|
copyToClipboard={copyToClipboard}
|
||||||
downloadText={downloadText}
|
downloadText={downloadText}
|
||||||
isExampleLoaded={isExampleLoaded} // NOUVEAU
|
isExampleLoaded={isExampleLoaded}
|
||||||
setIsExampleLoaded={setIsExampleLoaded} // NOUVEAU
|
setIsExampleLoaded={setIsExampleLoaded}
|
||||||
|
entityMappings={entityMappings} // Ajouter cette ligne
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,31 +1,131 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export const highlightEntities = (text: string): ReactNode => {
|
export const patterns = [
|
||||||
|
{
|
||||||
|
regex: /<PERSON>/g,
|
||||||
|
className: "bg-blue-200 text-blue-800",
|
||||||
|
label: "Personne",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<EMAIL_ADDRESS>/g,
|
||||||
|
className: "bg-green-200 text-green-800",
|
||||||
|
label: "Adresse Email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<PHONE_NUMBER>/g,
|
||||||
|
className: "bg-purple-200 text-purple-800",
|
||||||
|
label: "N° de Téléphone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<LOCATION>/g,
|
||||||
|
className: "bg-red-200 text-red-800",
|
||||||
|
label: "Lieu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<IBAN>/g,
|
||||||
|
className: "bg-yellow-200 text-yellow-800",
|
||||||
|
label: "IBAN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<ORGANIZATION>/g,
|
||||||
|
className: "bg-indigo-200 text-indigo-800",
|
||||||
|
label: "Organisation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<FLEXIBLE_DATE>/g,
|
||||||
|
className: "bg-pink-200 text-pink-800",
|
||||||
|
label: "Date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<BE_ADDRESS>/g,
|
||||||
|
className: "bg-cyan-200 text-cyan-800",
|
||||||
|
label: "Adresse (BE)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<BE_PHONE_NUMBER>/g,
|
||||||
|
className: "bg-violet-200 text-violet-800",
|
||||||
|
label: "N° de Tél. (BE)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<CREDIT_CARD>/g,
|
||||||
|
className: "bg-orange-200 text-orange-800",
|
||||||
|
label: "Carte de Crédit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<URL>/g,
|
||||||
|
className: "bg-teal-200 text-teal-800",
|
||||||
|
label: "URL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<IP_ADDRESS>/g,
|
||||||
|
className: "bg-gray-300 text-gray-900",
|
||||||
|
label: "Adresse IP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<DATE_TIME>/g,
|
||||||
|
className: "bg-pink-300 text-pink-900",
|
||||||
|
label: "Date & Heure",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<NRP>/g,
|
||||||
|
className: "bg-red-300 text-red-900",
|
||||||
|
label: "N° Registre National",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<BE_VAT>/g,
|
||||||
|
className: "bg-yellow-300 text-yellow-900",
|
||||||
|
label: "TVA (BE)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<BE_ENTERPRISE_NUMBER>/g,
|
||||||
|
className: "bg-lime-200 text-lime-800",
|
||||||
|
label: "N° d'entreprise (BE)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /<BE_PRO_ID>/g,
|
||||||
|
className: "bg-emerald-200 text-emerald-800",
|
||||||
|
label: "ID Pro (BE)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface EntityMapping {
|
||||||
|
originalValue: string;
|
||||||
|
anonymizedValue: string;
|
||||||
|
entityType: string;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const highlightEntities = (
|
||||||
|
text: string,
|
||||||
|
entityMappings?: EntityMapping[]
|
||||||
|
): ReactNode => {
|
||||||
if (!text) return text;
|
if (!text) return text;
|
||||||
|
|
||||||
// Patterns pour les différents types d'entités Presidio
|
const replacements: Array<{
|
||||||
const patterns = [
|
start: number;
|
||||||
// ✅ Patterns Presidio existants
|
end: number;
|
||||||
{ regex: /<PERSON>/g, className: "bg-blue-200 text-blue-800", label: "Personne" },
|
element: ReactNode;
|
||||||
{ regex: /<EMAIL_ADDRESS>/g, className: "bg-green-200 text-green-800", label: "Email" },
|
}> = [];
|
||||||
{ regex: /<PHONE_NUMBER>/g, className: "bg-purple-200 text-purple-800", label: "Téléphone" },
|
|
||||||
{ regex: /<LOCATION>/g, className: "bg-red-200 text-red-800", label: "Lieu" },
|
|
||||||
{ regex: /<IBAN>/g, className: "bg-yellow-200 text-yellow-800", label: "IBAN" },
|
|
||||||
{ regex: /<ORGANIZATION>/g, className: "bg-indigo-200 text-indigo-800", label: "Organisation" },
|
|
||||||
|
|
||||||
// 🆕 Patterns spécifiques détectés dans votre texte
|
// Si on a des mappings, on les utilise pour créer un mapping des valeurs anonymisées
|
||||||
{ regex: /<FLEXIBLE_DATE>/g, className: "bg-pink-200 text-pink-800", label: "Date" },
|
const anonymizedValueMap = new Map<
|
||||||
{ regex: /<BE_ADDRESS>/g, className: "bg-cyan-200 text-cyan-800", label: "Adresse BE" },
|
string,
|
||||||
{ regex: /<BE_PHONE_NUMBER>/g, className: "bg-violet-200 text-violet-800", label: "Tél. BE" },
|
{ label: string; className: string }
|
||||||
{ regex: /<BE_ENTERPRISE_NUMBER>/g, className: "bg-orange-200 text-orange-800", label: "N° Entreprise BE" },
|
>();
|
||||||
{ regex: /<BE_PRO_ID>/g, className: "bg-emerald-200 text-emerald-800", label: "ID Professionnel BE" },
|
|
||||||
{ regex: /<IP_ADDRESS>/g, className: "bg-slate-200 text-slate-800", label: "Adresse IP" },
|
|
||||||
|
|
||||||
// Anciens patterns (pour compatibilité)
|
if (entityMappings) {
|
||||||
{ regex: /\[([^\]]+)\]/g, className: "bg-[#f7ab6e] text-[#092727]", label: "Anonymisé" },
|
entityMappings.forEach((mapping) => {
|
||||||
];
|
// Trouver le pattern correspondant au type d'entité
|
||||||
|
const pattern = patterns.find((p) => p.label === mapping.entityType);
|
||||||
const replacements: Array<{ start: number; end: number; element: ReactNode }> = [];
|
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) => {
|
||||||
@@ -37,18 +137,52 @@ export const highlightEntities = (text: string): ReactNode => {
|
|||||||
const end = match.index + match[0].length;
|
const end = match.index + match[0].length;
|
||||||
|
|
||||||
// Vérifier qu'il n'y a pas de chevauchement avec des remplacements existants
|
// Vérifier qu'il n'y a pas de chevauchement avec des remplacements existants
|
||||||
const hasOverlap = replacements.some(r =>
|
const hasOverlap = replacements.some(
|
||||||
(start >= r.start && start < r.end) || (end > r.start && end <= r.end)
|
(r) =>
|
||||||
|
(start >= r.start && start < r.end) || (end > r.start && end <= r.end)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasOverlap) {
|
if (!hasOverlap) {
|
||||||
|
// Chercher si on a un mapping pour cette entité
|
||||||
|
let displayLabel = pattern.label;
|
||||||
|
const displayClass = pattern.className; // Changé de 'let' à 'const'
|
||||||
|
|
||||||
|
if (entityMappings) {
|
||||||
|
// Compter les occurrences précédentes du même type pour déterminer le numéro
|
||||||
|
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(
|
||||||
|
(mapping) => mapping.entityType === entityType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingMapping) {
|
||||||
|
// Utiliser le compteur pour déterminer le bon numéro avec des crochets
|
||||||
|
const entityCount = previousMatches + 1;
|
||||||
|
displayLabel = `${entityType} [${entityCount}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const element = (
|
const element = (
|
||||||
<span
|
<span
|
||||||
key={`${patternIndex}-${start}`}
|
key={`${patternIndex}-${start}`}
|
||||||
className={`${pattern.className} px-2 py-1 rounded-md font-medium text-xs inline-block mx-0.5 shadow-sm border`}
|
className={`${displayClass} px-2 py-1 rounded-md font-medium text-xs inline-block mx-0.5 shadow-sm border`}
|
||||||
title={`${pattern.label} anonymisé`}
|
title={`${displayLabel} anonymisé`}
|
||||||
>
|
>
|
||||||
{match[0]}
|
{displayLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user