This commit is contained in:
nBiqoz
2025-08-04 00:14:55 +02:00
parent b1de50cbc2
commit ad92302461
8 changed files with 371 additions and 222 deletions

View File

@@ -17,108 +17,97 @@ export const AnonymizationInterface = ({
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>")) {
anonymizedTypes.add("Prénoms");
anonymizedTypes.add("Noms de famille");
anonymizedTypes.add("Noms complets");
}
// Emails (EMAIL_ADDRESS)
// EMAIL_ADDRESS -> Adresses e-mail
if (outputText.includes("<EMAIL_ADDRESS>")) {
anonymizedTypes.add("Adresses e-mail");
}
// Téléphones (PHONE_NUMBER)
// PHONE_NUMBER -> Numéros de téléphone
if (outputText.includes("<PHONE_NUMBER>")) {
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>")) {
anonymizedTypes.add("Adresses");
}
// IBAN (IBAN)
if (outputText.includes("<IBAN>")) {
anonymizedTypes.add("Numéros d'ID"); // Ou créer une nouvelle catégorie "IBAN"
// BE_ADDRESS -> aussi Adresses
if (outputText.includes("<BE_ADDRESS>")) {
anonymizedTypes.add("Adresses");
}
// Organisations (ORGANIZATION)
if (outputText.includes("<ORGANIZATION>")) {
anonymizedTypes.add("Noms de domaine"); // Ou adapter selon vos besoins
}
// Dates personnalisées (CUSTOM_DATE)
if (outputText.includes("<CUSTOM_DATE>")) {
// FLEXIBLE_DATE ou DATE_TIME -> Dates
if (
outputText.includes("<FLEXIBLE_DATE>") ||
outputText.includes("<DATE_TIME>")
) {
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>")) {
anonymizedTypes.add("Numéros d'ID");
}
// ✅ ANCIENS PATTERNS (pour compatibilité)
// Noms (anciens patterns [Nom1], [Nom2]...)
if (outputText.includes("[Nom1]") || outputText.includes("[Nom")) {
anonymizedTypes.add("Prénoms");
anonymizedTypes.add("Noms de famille");
anonymizedTypes.add("Noms complets");
}
// Emails (anciens patterns)
if (outputText.includes("[Email1]") || outputText.includes("[Email")) {
anonymizedTypes.add("Adresses e-mail");
}
// Téléphones (anciens patterns)
if (
outputText.includes("[Téléphone1]") ||
outputText.includes("[Téléphone")
) {
anonymizedTypes.add("Numéros de téléphone");
}
// Adresses (anciens patterns)
if (outputText.includes("[Adresse1]") || outputText.includes("[Adresse")) {
anonymizedTypes.add("Adresses");
}
// Numéros d'ID / Sécurité sociale (anciens patterns)
if (
outputText.includes("[NuméroSS1]") ||
outputText.includes("[NuméroSS") ||
outputText.includes("[ID")
) {
anonymizedTypes.add("Numéros d'ID");
}
// Valeurs monétaires
if (outputText.includes("[Montant") || /\[\d+[€$]\]/.test(outputText)) {
anonymizedTypes.add("Valeurs monétaires");
}
// Noms de domaine
if (outputText.includes("[Domaine") || /\[.*\.com\]/.test(outputText)) {
// URL -> Noms de domaine
if (outputText.includes("<URL>")) {
anonymizedTypes.add("Noms de domaine");
}
// Valeurs numériques
if (
/\[\d+\]/.test(outputText) &&
!outputText.includes("[Téléphone") &&
!outputText.includes("[Montant")
) {
// CREDIT_CARD -> Coordonnées bancaires (supprimer la duplication)
if (outputText.includes("<CREDIT_CARD>")) {
anonymizedTypes.add("Coordonnées bancaires");
}
// 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");
}
// Texte personnalisé (si du texte a été modifié mais pas avec les patterns spécifiques)
if (sourceText !== outputText && anonymizedTypes.size === 0) {
anonymizedTypes.add("Texte personnalisé");
// BE_VAT -> Valeurs numériques
if (outputText.includes("<BE_VAT>")) {
anonymizedTypes.add("Valeurs numériques");
}
return anonymizedTypes;
@@ -133,7 +122,7 @@ export const AnonymizationInterface = ({
items: ["Noms de famille", "Adresses", "Dates"],
},
{
items: ["Noms complets", "Numéros d'ID", "Valeurs numériques"],
items: ["Noms complets", "Numéros d'ID", "Coordonnées bancaires"],
},
{
items: ["Adresses e-mail", "Valeurs monétaires", "Texte personnalisé"],

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import { patterns } from "@/app/utils/highlightEntities";
interface EntityMapping {
originalValue: string;
@@ -8,25 +9,18 @@ interface EntityMapping {
endIndex: number;
}
// Nouvelle interface pour les résultats de Presidio Analyzer
// L'API retourne des objets avec snake_case
interface PresidioAnalyzerResult {
entity_type: string;
start: number;
end: number;
score: number;
analysis_explanation?: {
recognizer: string;
pattern_name?: string;
pattern?: string;
validation_result?: boolean;
};
}
// Interface pour la réponse de l'API
// La réponse de l'API utilise camelCase pour les clés principales
interface ProcessDocumentResponse {
text?: string;
text?: string; // Texte original en cas de fallback
anonymizedText?: string;
piiCount?: number;
analyzerResults?: PresidioAnalyzerResult[];
error?: string;
}
@@ -66,101 +60,105 @@ export const useAnonymization = ({
setEntityMappings([]);
try {
console.log("🚀 Début anonymisation avec Presidio");
const formData = new FormData();
if (uploadedFile) {
console.log("📁 Traitement fichier:", {
name: uploadedFile.name,
type: uploadedFile.type,
size: uploadedFile.size
});
formData.append("file", uploadedFile);
} else {
console.log("📝 Traitement texte saisi");
const textBlob = new Blob([textToProcess], { type: "text/plain" });
const textFile = new File([textBlob], "input.txt", { type: "text/plain" });
const textFile = new File([textBlob], "input.txt", {
type: "text/plain",
});
formData.append("file", textFile);
}
console.log("🔍 Appel à /api/process-document avec Presidio...");
console.log("📦 FormData préparée:", Array.from(formData.entries()));
const response = await fetch("/api/process-document", {
method: "POST",
body: formData,
});
console.log("📡 Réponse reçue:", {
ok: response.ok,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
});
if (!response.ok) {
let errorMessage = `Erreur HTTP: ${response.status}`;
try {
const responseText = await response.text();
console.log("📄 Contenu de l'erreur:", responseText);
if (responseText.trim()) {
try {
const errorData = JSON.parse(responseText);
if (errorData.error) {
errorMessage = errorData.error;
console.log("✅ Message détaillé récupéré:", errorMessage);
}
} catch (jsonError) {
console.error("❌ Erreur parsing JSON:", jsonError); // ✅ Utiliser la variable
console.error("❌ Réponse non-JSON:", responseText);
errorMessage = `Erreur ${response.status}: Réponse invalide du serveur`;
}
}
} catch (readError) {
console.error("❌ Impossible de lire la réponse:", readError);
const errorData = await response.json();
if (errorData.error) errorMessage = errorData.error;
} catch {
/* Ignore */
}
throw new Error(errorMessage);
}
const data: ProcessDocumentResponse = await response.json();
console.log("📊 Réponse API:", data);
if (data.error) {
throw new Error(data.error);
}
if (data.anonymizedText) {
console.log("✅ Anonymisation réussie avec Presidio");
// Utiliser camelCase pour les propriétés de la réponse principale
if (data.anonymizedText && data.analyzerResults) {
setOutputText(data.anonymizedText);
// Extraire les mappings depuis les résultats Presidio (plus d'erreur 'any')
if (data.analyzerResults && data.text) {
const mappings: EntityMapping[] = data.analyzerResults.map(
(entity: PresidioAnalyzerResult, index: number) => ({
originalValue: data.text!.substring(entity.start, entity.end),
anonymizedValue: `[${entity.entity_type}${index + 1}]`,
entityType: entity.entity_type,
startIndex: entity.start,
endIndex: entity.end,
})
);
setEntityMappings(mappings);
console.log("📋 Entités détectées:", mappings.length);
console.log("🔍 Détails des entités:", mappings);
}
const entityTypeMap = new Map<string, string>();
patterns.forEach((p) => {
const match = p.regex.toString().match(/<([A-Z_]+)>/);
if (match && match[1]) {
entityTypeMap.set(match[1], p.label);
}
});
// 1. Compter les occurrences de chaque tag d'entité dans le texte anonymisé
const tagCounts = new Map<string, number>();
data.analyzerResults.forEach((result) => {
const tag = `<${result.entity_type}>`;
if (!tagCounts.has(result.entity_type)) {
const count = (
data.anonymizedText?.match(new RegExp(tag, "g")) || []
).length;
tagCounts.set(result.entity_type, count);
}
});
const seen = new Set<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) {
console.log(
"⚠️ Fallback: Presidio non disponible, texte original retourné"
);
setOutputText(data.text);
setError("Presidio temporairement indisponible. Texte non anonymisé.");
}
} catch (error) {
console.error("❌ Erreur anonymisation complète:", error);
setError(
error instanceof Error
? error.message

View File

@@ -11,6 +11,14 @@ import { SupportedDataTypes } from "./SupportedDataTypes";
import { AnonymizationInterface } from "./AnonymizationInterface";
import { highlightEntities } from "../utils/highlightEntities";
interface EntityMapping {
originalValue: string;
anonymizedValue: string;
entityType: string;
startIndex: number;
endIndex: number;
}
interface FileUploadComponentProps {
uploadedFile: File | null;
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
@@ -26,8 +34,9 @@ interface FileUploadComponentProps {
outputText?: string;
copyToClipboard?: () => void;
downloadText?: () => void;
isExampleLoaded?: boolean; // NOUVEAU
setIsExampleLoaded?: (loaded: boolean) => void; // NOUVEAU
isExampleLoaded?: boolean;
setIsExampleLoaded?: (loaded: boolean) => void;
entityMappings?: EntityMapping[]; // Ajouter cette prop
}
export const FileUploadComponent = ({
@@ -45,7 +54,8 @@ export const FileUploadComponent = ({
outputText,
copyToClipboard,
downloadText,
setIsExampleLoaded, // NOUVEAU - Ajouté ici
setIsExampleLoaded,
entityMappings, // Ajouter cette prop ici
}: FileUploadComponentProps) => {
// On passe en preview seulement si :
// 1. Un fichier est uploadé OU
@@ -133,7 +143,8 @@ export const FileUploadComponent = ({
<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">
{highlightEntities(
outputText || "Aucun contenu à afficher"
outputText || "Aucun contenu à afficher",
entityMappings // Ajouter les mappings ici
)}
</div>
</div>
@@ -206,7 +217,7 @@ export const FileUploadComponent = ({
<button
onClick={onAnonymize}
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 ? (
<>
@@ -288,20 +299,22 @@ export const FileUploadComponent = ({
</div>
{/* 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
</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
</p>
{/* 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">
{/* 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 */}
{!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&nbsp;</span>
<SampleTextComponent
setSourceText={setSourceText}
@@ -312,7 +325,6 @@ export const FileUploadComponent = ({
/>
</div>
)}
<textarea
value={sourceText}
onChange={(e) => setSourceText(e.target.value)}
@@ -330,21 +342,29 @@ export const FileUploadComponent = ({
</div>
{/* 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 */}
<div className="flex flex-col w-full sm:w-auto">
<label className="text-xs text-gray-500 mb-1">Type de données :</label>
<div className="flex flex-col w-full">
<label className="text-xs text-gray-500 mb-1">
Type de données :
</label>
<div className="relative">
<select
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>
<option disabled style={{ color: 'lightgray' }}>
<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">
<option>
Informations Personnellement Identifiables (PII)
</option>
<option disabled style={{ color: "lightgray" }}>
PII + Données Business (En développement)
</option>
</select>
<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>
@@ -353,7 +373,7 @@ export const FileUploadComponent = ({
<button
onClick={onAnonymize}
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={
sourceText.trim()
? "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">
<label className="flex flex-col items-center justify-center cursor-pointer group p-3 sm:p-4 h-full min-h-[200px]">
{/* 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" />
</div>
{/* 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
</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
</p>
{/* File Info */}
<div className="flex flex-col items-center gap-1 text-xs text-[#092727] opacity-60">
<span>📄 Fichiers TXT, PDF</span>
<span>Max 5MB</span>
<div className="flex s items-center gap-1 text-xs text-[#092727] opacity-60">
<span>📄 Fichiers TXT, PDF</span> - <span>Max 5MB</span>
</div>
{/* Hidden Input */}

View File

@@ -27,8 +27,8 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
return (
<div className="w-full max-w-2xl mx-auto mb-4 px-2">
<div className="flex items-center justify-center">
<div className="flex items-center w-full max-w-md">
<div className="flex items-start justify-center">
<div className="flex items-start w-full max-w-md">
{steps.map((step, index) => {
const stepNumber = index + 1;
const isCompleted = stepNumber < currentStep;
@@ -37,7 +37,10 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
return (
<React.Fragment key={index}>
{/* 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
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
@@ -50,7 +53,7 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
{getStepIcon(stepNumber, isCompleted)}
</div>
<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
? "text-[#092727]"
: "text-gray-500"
@@ -58,24 +61,19 @@ export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
style={{
wordBreak: "break-word",
hyphens: "auto",
minWidth: "60px", // Assure un espace minimum
maxWidth: "120px", // Empêche de devenir trop large
}}
>
{step === "Anonymisation" ? (
<>
<span className="hidden sm:inline">Anonymisation</span>
<span className="sm:hidden">Anonym.</span>
</>
) : (
step
)}
{step}
</span>
</div>
{/* Connector Line */}
{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
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
? "bg-[#f7ab6e]"
: "bg-gray-200"

View File

@@ -1,11 +1,20 @@
import { Copy, Download, AlertTriangle } from "lucide-react";
import { ReactNode } from "react";
interface EntityMapping {
originalValue: string;
anonymizedValue: string;
entityType: string;
startIndex: number;
endIndex: number;
}
interface ResultPreviewComponentProps {
outputText: string;
copyToClipboard: () => void;
downloadText: () => void;
highlightEntities: (text: string) => ReactNode;
highlightEntities: (text: string, mappings?: EntityMapping[]) => ReactNode;
entityMappings?: EntityMapping[];
}
export const ResultPreviewComponent = ({
@@ -13,6 +22,7 @@ export const ResultPreviewComponent = ({
copyToClipboard,
downloadText,
highlightEntities,
entityMappings,
}: ResultPreviewComponentProps) => {
if (!outputText) return null;
@@ -48,7 +58,7 @@ export const ResultPreviewComponent = ({
<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="leading-relaxed">
{highlightEntities(outputText)}
{highlightEntities(outputText, entityMappings)}
</div>
</div>
</div>

View File

@@ -18,7 +18,7 @@ export const SupportedDataTypes = () => {
<div className="flex flex-col space-y-2">
<span> Noms complets</span>
<span> Numéros d&apos;ID</span>
<span> Valeurs numériques</span>
<span> Coordonnées bancaires</span>
</div>
<div className="flex flex-col space-y-2">
<span> Adresses e-mail</span>

View File

@@ -107,8 +107,9 @@ export default function Home() {
outputText={outputText}
copyToClipboard={copyToClipboard}
downloadText={downloadText}
isExampleLoaded={isExampleLoaded} // NOUVEAU
setIsExampleLoaded={setIsExampleLoaded} // NOUVEAU
isExampleLoaded={isExampleLoaded}
setIsExampleLoaded={setIsExampleLoaded}
entityMappings={entityMappings} // Ajouter cette ligne
/>
</div>
</div>

View File

@@ -1,57 +1,191 @@
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;
// Patterns pour les différents types d'entités Presidio
const patterns = [
// ✅ Patterns Presidio existants
{ regex: /<PERSON>/g, className: "bg-blue-200 text-blue-800", label: "Personne" },
{ 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
{ 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: "Tél. BE" },
{ 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é)
{ regex: /\[([^\]]+)\]/g, className: "bg-[#f7ab6e] text-[#092727]", label: "Anonymisé" },
];
const replacements: Array<{
start: number;
end: number;
element: ReactNode;
}> = [];
const replacements: Array<{ start: number; end: number; 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
patterns.forEach((pattern, patternIndex) => {
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
let match;
while ((match = regex.exec(text)) !== null) {
const start = match.index;
const end = match.index + match[0].length;
// Vérifier qu'il n'y a pas de chevauchement avec des remplacements existants
const hasOverlap = replacements.some(r =>
(start >= r.start && start < r.end) || (end > r.start && end <= r.end)
const hasOverlap = replacements.some(
(r) =>
(start >= r.start && start < r.end) || (end > r.start && end <= r.end)
);
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 = (
<span
key={`${patternIndex}-${start}`}
className={`${pattern.className} px-2 py-1 rounded-md font-medium text-xs inline-block mx-0.5 shadow-sm border`}
title={`${pattern.label} anonymisé`}
className={`${displayClass} px-2 py-1 rounded-md font-medium text-xs inline-block mx-0.5 shadow-sm border`}
title={`${displayLabel} anonymisé`}
>
{match[0]}
{displayLabel}
</span>
);
replacements.push({ start, end, element });
}
}
@@ -73,10 +207,10 @@ export const highlightEntities = (text: string): ReactNode => {
if (replacement.start > lastIndex) {
parts.push(text.slice(lastIndex, replacement.start));
}
// Ajouter l'élément de remplacement
parts.push(replacement.element);
lastIndex = replacement.end;
});
@@ -86,4 +220,4 @@ export const highlightEntities = (text: string): ReactNode => {
}
return parts;
};
};