finalyse
This commit is contained in:
@@ -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é"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 </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 */}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SupportedDataTypes = () => {
|
||||
<div className="flex flex-col space-y-2">
|
||||
<span>• Noms complets</span>
|
||||
<span>• Numéros d'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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user