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>(); 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é"],

View File

@@ -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,
})
);
setEntityMappings(mappings);
console.log("📋 Entités détectées:", mappings.length);
console.log("🔍 Détails des entités:", mappings);
} }
} else if (data.text) { });
console.log(
"⚠️ Fallback: Presidio non disponible, texte original retourné" // 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) {
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

View File

@@ -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&nbsp;</span> <span>Commencez à taper du texte, ou&nbsp;</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 */}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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&apos;ID</span> <span> Numéros d&apos;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>

View File

@@ -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>

View File

@@ -1,31 +1,131 @@
import { ReactNode } from 'react'; import { ReactNode } from "react";
export const highlightEntities = (text: string): ReactNode => { export const patterns = [
if (!text) return text; {
regex: /<PERSON>/g,
// Patterns pour les différents types d'entités Presidio className: "bg-blue-200 text-blue-800",
const patterns = [ label: "Personne",
// ✅ 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: /<EMAIL_ADDRESS>/g,
{ regex: /<PHONE_NUMBER>/g, className: "bg-purple-200 text-purple-800", label: "Téléphone" }, className: "bg-green-200 text-green-800",
{ regex: /<LOCATION>/g, className: "bg-red-200 text-red-800", label: "Lieu" }, label: "Adresse Email",
{ 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: /<PHONE_NUMBER>/g,
// 🆕 Patterns spécifiques détectés dans votre texte className: "bg-purple-200 text-purple-800",
{ regex: /<FLEXIBLE_DATE>/g, className: "bg-pink-200 text-pink-800", label: "Date" }, label: "N° de Téléphone",
{ 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: /<LOCATION>/g,
{ regex: /<BE_PRO_ID>/g, className: "bg-emerald-200 text-emerald-800", label: "ID Professionnel BE" }, className: "bg-red-200 text-red-800",
{ regex: /<IP_ADDRESS>/g, className: "bg-slate-200 text-slate-800", label: "Adresse IP" }, label: "Lieu",
},
// Anciens patterns (pour compatibilité) {
{ regex: /\[([^\]]+)\]/g, className: "bg-[#f7ab6e] text-[#092727]", label: "Anonymisé" }, 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)",
},
]; ];
const replacements: Array<{ start: number; end: number; element: ReactNode }> = []; interface EntityMapping {
originalValue: string;
anonymizedValue: string;
entityType: string;
startIndex: number;
endIndex: number;
}
export const highlightEntities = (
text: string,
entityMappings?: EntityMapping[]
): ReactNode => {
if (!text) return text;
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 // 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(
(r) =>
(start >= r.start && start < r.end) || (end > r.start && end <= r.end) (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>
); );