Compare commits
5 Commits
a0e033b7eb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
050474e95b | ||
|
|
130929b756 | ||
|
|
d7d3a3c7e9 | ||
|
|
0360e1ca9f | ||
|
|
3a84da5c74 |
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse, type NextRequest } from "next/server";
|
import { NextResponse, type NextRequest } from "next/server";
|
||||||
import pdf from "pdf-parse"; // ✅ Import correct
|
import pdf from "pdf-parse"; // ✅ Import correct
|
||||||
import mammoth from "mammoth";
|
import mammoth from "mammoth";
|
||||||
|
import { PresidioAnalyzerResult } from "@/app/config/entityLabels";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
console.log("🔍 Début du traitement de la requête");
|
console.log("🔍 Début du traitement de la requête");
|
||||||
@@ -8,6 +9,9 @@ export async function POST(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const formData = await req.formData();
|
const formData = await req.formData();
|
||||||
const file = formData.get("file") as File | null;
|
const file = formData.get("file") as File | null;
|
||||||
|
const category = (formData.get("category") as string) || "pii"; // Récupérer la catégorie
|
||||||
|
|
||||||
|
console.log("📊 Catégorie sélectionnée:", category);
|
||||||
// ✅ Validation améliorée du fichier
|
// ✅ Validation améliorée du fichier
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -169,14 +173,16 @@ export async function POST(req: NextRequest) {
|
|||||||
const analyzerConfig = {
|
const analyzerConfig = {
|
||||||
text: fileContent,
|
text: fileContent,
|
||||||
language: "fr",
|
language: "fr",
|
||||||
|
mode: category, // Ajouter le mode basé sur la catégorie
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔍 Appel à Presidio Analyzer...");
|
console.log("🔍 Appel à Presidio Analyzer...");
|
||||||
|
console.log("📊 Configuration:", analyzerConfig);
|
||||||
|
|
||||||
// ✅ Définir l'URL AVANT de l'utiliser
|
// ✅ Définir l'URL AVANT de l'utiliser
|
||||||
const presidioAnalyzerUrl =
|
const presidioAnalyzerUrl =
|
||||||
"http://analyzer.151.80.20.211.sslip.io/analyze";
|
"http://analyzer.151.80.20.211.sslip.io/analyze";
|
||||||
|
// "http://localhost:5001/analyze";
|
||||||
try {
|
try {
|
||||||
const analyzeResponse = await fetch(presidioAnalyzerUrl, {
|
const analyzeResponse = await fetch(presidioAnalyzerUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -209,11 +215,13 @@ export async function POST(req: NextRequest) {
|
|||||||
const anonymizerConfig = {
|
const anonymizerConfig = {
|
||||||
text: fileContent,
|
text: fileContent,
|
||||||
analyzer_results: analyzerResults,
|
analyzer_results: analyzerResults,
|
||||||
|
mode: category, // Ajouter le mode pour l'anonymizer aussi
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔍 Appel à Presidio Anonymizer...");
|
console.log("🔍 Appel à Presidio Anonymizer...");
|
||||||
const presidioAnonymizerUrl =
|
const presidioAnonymizerUrl =
|
||||||
"http://analyzer.151.80.20.211.sslip.io/anonymize";
|
"http://analyzer.151.80.20.211.sslip.io/anonymize";
|
||||||
|
// "http://localhost:5001/anonymize";
|
||||||
|
|
||||||
const anonymizeResponse = await fetch(presidioAnonymizerUrl, {
|
const anonymizeResponse = await fetch(presidioAnonymizerUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -235,92 +243,33 @@ export async function POST(req: NextRequest) {
|
|||||||
const anonymizerResult = await anonymizeResponse.json();
|
const anonymizerResult = await anonymizeResponse.json();
|
||||||
console.log("✅ Anonymisation réussie.");
|
console.log("✅ Anonymisation réussie.");
|
||||||
|
|
||||||
// 🔧 NOUVELLE FONCTION SIMPLIFIÉE pour extraire les valeurs de remplacement
|
// 🎯 SOLUTION SIMPLIFIÉE : Utiliser directement le texte anonymisé de Presidio
|
||||||
// Ajouter cette interface au début du fichier
|
console.log(
|
||||||
interface AnalyzerResult {
|
"✅ Texte anonymisé reçu de Presidio:",
|
||||||
entity_type: string;
|
anonymizerResult.anonymized_text
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
score: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Puis modifier la fonction
|
|
||||||
const extractReplacementValues = (
|
|
||||||
originalText: string,
|
|
||||||
anonymizedText: string,
|
|
||||||
analyzerResults: AnalyzerResult[]
|
|
||||||
) => {
|
|
||||||
const replacementMap: Record<string, string> = {};
|
|
||||||
|
|
||||||
// Approche simple : comparer caractère par caractère
|
|
||||||
let originalIndex = 0;
|
|
||||||
let anonymizedIndex = 0;
|
|
||||||
|
|
||||||
// Trier les résultats par position
|
|
||||||
const sortedResults = [...analyzerResults].sort(
|
|
||||||
(a, b) => a.start - b.start
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const result of sortedResults) {
|
|
||||||
const originalValue = originalText.substring(
|
|
||||||
result.start,
|
|
||||||
result.end
|
|
||||||
);
|
|
||||||
|
|
||||||
// Avancer jusqu'à la position de l'entité dans le texte original
|
|
||||||
while (originalIndex < result.start) {
|
|
||||||
originalIndex++;
|
|
||||||
anonymizedIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maintenant on est au début de l'entité
|
|
||||||
// Dans le texte anonymisé, on doit avoir un remplacement qui commence par '['
|
|
||||||
if (anonymizedText[anonymizedIndex] === "[") {
|
|
||||||
// Trouver la fin du remplacement (le ']')
|
|
||||||
let endBracket = anonymizedIndex;
|
|
||||||
while (
|
|
||||||
endBracket < anonymizedText.length &&
|
|
||||||
anonymizedText[endBracket] !== "]"
|
|
||||||
) {
|
|
||||||
endBracket++;
|
|
||||||
}
|
|
||||||
endBracket++; // Inclure le ']'
|
|
||||||
|
|
||||||
const replacementValue = anonymizedText.substring(
|
|
||||||
anonymizedIndex,
|
|
||||||
endBracket
|
|
||||||
);
|
|
||||||
replacementMap[originalValue] = replacementValue;
|
|
||||||
|
|
||||||
// Avancer les index
|
|
||||||
originalIndex = result.end;
|
|
||||||
anonymizedIndex = endBracket;
|
|
||||||
} else {
|
|
||||||
// Si pas de '[', avancer normalement
|
|
||||||
originalIndex = result.end;
|
|
||||||
anonymizedIndex += result.end - result.start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🔧 Valeurs de remplacement extraites:", replacementMap);
|
|
||||||
return replacementMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
const replacementValues = extractReplacementValues(
|
|
||||||
fileContent,
|
|
||||||
anonymizerResult.anonymized_text,
|
|
||||||
analyzerResults
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔍 AJOUT D'UN LOG POUR DÉBOGUER
|
// Créer un mapping simple basé sur les entités détectées
|
||||||
console.log("🔧 Valeurs de remplacement extraites:", replacementValues);
|
const replacementValues: Record<string, string> = {};
|
||||||
|
|
||||||
|
analyzerResults.forEach((result: PresidioAnalyzerResult) => {
|
||||||
|
const originalValue = fileContent.substring(result.start, result.end);
|
||||||
|
const replacementValue = result.entity_type; // ✅ CORRECTION : Utiliser entity_type au lieu de [ENTITY_TYPE]
|
||||||
|
replacementValues[originalValue] = replacementValue;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`📝 Mapping créé: "${originalValue}" -> "${replacementValue}"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
text: fileContent,
|
text: fileContent, // Texte original pour référence
|
||||||
anonymizedText: anonymizerResult.anonymized_text,
|
anonymizedText: anonymizerResult.anonymized_text, // Texte déjà anonymisé par Presidio
|
||||||
piiCount: analyzerResults.length,
|
piiCount: analyzerResults.length,
|
||||||
analyzerResults: analyzerResults,
|
analyzerResults: analyzerResults,
|
||||||
replacementValues: replacementValues, // Utiliser les nouvelles valeurs
|
replacementValues: replacementValues,
|
||||||
|
// 🎯 NOUVEAU : Indiquer qu'on utilise directement le texte de Presidio
|
||||||
|
usePresidioText: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json(result, { status: 200 });
|
return NextResponse.json(result, { status: 200 });
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CheckCircle } from "lucide-react";
|
import { CheckCircle, Info } from "lucide-react";
|
||||||
|
|
||||||
interface AnonymizationInterfaceProps {
|
interface AnonymizationInterfaceProps {
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
@@ -17,106 +17,181 @@ export const AnonymizationInterface = ({
|
|||||||
|
|
||||||
const anonymizedTypes = new Set<string>();
|
const anonymizedTypes = new Set<string>();
|
||||||
|
|
||||||
if (outputText.includes("<PERSONNE>")) {
|
// PII - Données personnelles
|
||||||
anonymizedTypes.add("Prénoms");
|
if (outputText.includes("[PERSONNE]")) {
|
||||||
anonymizedTypes.add("Noms de famille");
|
anonymizedTypes.add("Noms et prénoms");
|
||||||
anonymizedTypes.add("Noms complets");
|
|
||||||
}
|
}
|
||||||
|
if (outputText.includes("[DATE]")) {
|
||||||
// EMAIL_ADDRESS -> Adresses e-mail
|
|
||||||
if (outputText.includes("<EMAIL_ADDRESS>")) {
|
|
||||||
anonymizedTypes.add("Adresses e-mail");
|
|
||||||
}
|
|
||||||
|
|
||||||
// PHONE_NUMBER -> Numéros de téléphone
|
|
||||||
if (outputText.includes("<PHONE_NUMBER>")) {
|
|
||||||
anonymizedTypes.add("Numéros de téléphone");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// BE_ADDRESS -> aussi Adresses
|
|
||||||
if (outputText.includes("<BE_ADDRESS>")) {
|
|
||||||
anonymizedTypes.add("Adresses");
|
|
||||||
}
|
|
||||||
|
|
||||||
// FLEXIBLE_DATE ou DATE_TIME -> Dates
|
|
||||||
if (
|
|
||||||
outputText.includes("<FLEXIBLE_DATE>") ||
|
|
||||||
outputText.includes("<DATE_TIME>")
|
|
||||||
) {
|
|
||||||
anonymizedTypes.add("Dates");
|
anonymizedTypes.add("Dates");
|
||||||
}
|
}
|
||||||
|
if (outputText.includes("[ADRESSE_EMAIL]")) {
|
||||||
// IBAN -> Coordonnées bancaires (au lieu de Numéros d'ID)
|
anonymizedTypes.add("Adresses e-mail");
|
||||||
if (outputText.includes("<IBAN>")) {
|
}
|
||||||
anonymizedTypes.add("Coordonnées bancaires");
|
if (
|
||||||
|
outputText.includes("[TELEPHONE_FRANCAIS]") ||
|
||||||
|
outputText.includes("[TELEPHONE_BELGE]") ||
|
||||||
|
outputText.includes("[TELEPHONE]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Numéros de téléphone");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
outputText.includes("[ADRESSE_FRANCAISE]") ||
|
||||||
|
outputText.includes("[ADRESSE_BELGE]") ||
|
||||||
|
outputText.includes("[ADRESSE]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Adresses postales");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[LOCATION]")) {
|
||||||
|
anonymizedTypes.add("Lieux géographiques");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
outputText.includes("[CARTE_IDENTITE_FRANCAISE]") ||
|
||||||
|
outputText.includes("[CARTE_IDENTITE_BELGE]") ||
|
||||||
|
outputText.includes("[PASSEPORT_FRANCAIS]") ||
|
||||||
|
outputText.includes("[PASSEPORT_BELGE]") ||
|
||||||
|
outputText.includes("[PERMIS_CONDUIRE_FRANCAIS]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Documents d'identité");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[NUMERO_SECURITE_SOCIALE_FRANCAIS]")) {
|
||||||
|
anonymizedTypes.add("Numéros de sécurité sociale");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[BIOMETRIC_DATA]")) {
|
||||||
|
anonymizedTypes.add("Données biométriques");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[HEALTH_DATA]")) {
|
||||||
|
anonymizedTypes.add("Données de santé");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
outputText.includes("[SEXUAL_ORIENTATION]") ||
|
||||||
|
outputText.includes("[POLITICAL_OPINIONS]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Données sensibles RGPD");
|
||||||
}
|
}
|
||||||
|
|
||||||
// CREDIT_CARD -> aussi Coordonnées bancaires (au lieu de Valeurs numériques)
|
// Données financières
|
||||||
if (outputText.includes("<CREDIT_CARD>")) {
|
if (
|
||||||
anonymizedTypes.add("Coordonnées bancaires");
|
outputText.includes("[IBAN]") ||
|
||||||
|
outputText.includes("[COMPTE_BANCAIRE_FRANCAIS]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Comptes bancaires");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[CREDIT_CARD]")) {
|
||||||
|
anonymizedTypes.add("Cartes de crédit");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[MONTANT_FINANCIER]")) {
|
||||||
|
anonymizedTypes.add("Montants financiers");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[NUMERO_FISCAL_FRANCAIS]")) {
|
||||||
|
anonymizedTypes.add("Numéros fiscaux");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[RGPD_FINANCIAL_DATA]")) {
|
||||||
|
anonymizedTypes.add("Données financières RGPD");
|
||||||
}
|
}
|
||||||
|
|
||||||
// NRP -> Numéros d'ID
|
// Business - Données d'entreprise
|
||||||
if (outputText.includes("<NRP>")) {
|
if (outputText.includes("[ORGANISATION]")) {
|
||||||
anonymizedTypes.add("Numéros d'ID");
|
anonymizedTypes.add("Noms d'organisations");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
outputText.includes("[SIRET_SIREN_FRANCAIS]") ||
|
||||||
|
outputText.includes("[SOCIETE_FRANCAISE]") ||
|
||||||
|
outputText.includes("[SOCIETE_BELGE]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Entreprises et sociétés");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
outputText.includes("[TVA_FRANCAISE]") ||
|
||||||
|
outputText.includes("[TVA_BELGE]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Numéros de TVA");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
outputText.includes("[NUMERO_ENTREPRISE_BELGE]") ||
|
||||||
|
outputText.includes("[REGISTRE_NATIONAL_BELGE]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Identifiants d'entreprise");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[SECRET_COMMERCIAL]")) {
|
||||||
|
anonymizedTypes.add("Secrets commerciaux");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[REFERENCE_CONTRAT]")) {
|
||||||
|
anonymizedTypes.add("Références de contrats");
|
||||||
|
}
|
||||||
|
if (outputText.includes("[MARKET_SHARE]")) {
|
||||||
|
anonymizedTypes.add("Parts de marché");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
outputText.includes("[ID_PROFESSIONNEL_BELGE]") ||
|
||||||
|
outputText.includes("[DONNEES_PROFESSIONNELLES]")
|
||||||
|
) {
|
||||||
|
anonymizedTypes.add("Identifiants professionnels");
|
||||||
}
|
}
|
||||||
|
|
||||||
// BE_PRO_ID -> Numéros d'ID
|
// Données techniques
|
||||||
if (outputText.includes("<BE_PRO_ID>")) {
|
if (outputText.includes("[ADRESSE_IP]")) {
|
||||||
anonymizedTypes.add("Numéros d'ID");
|
anonymizedTypes.add("Adresses IP");
|
||||||
}
|
}
|
||||||
|
if (outputText.includes("[URL_IDENTIFIANT]")) {
|
||||||
// BE_ENTERPRISE_NUMBER -> Numéros d'ID
|
anonymizedTypes.add("URLs et identifiants web");
|
||||||
if (outputText.includes("<BE_ENTERPRISE_NUMBER>")) {
|
|
||||||
anonymizedTypes.add("Numéros d'ID");
|
|
||||||
}
|
}
|
||||||
|
if (outputText.includes("[CLE_API_SECRETE]")) {
|
||||||
// URL -> Noms de domaine
|
anonymizedTypes.add("Clés API secrètes");
|
||||||
if (outputText.includes("<URL>")) {
|
|
||||||
anonymizedTypes.add("Noms de domaine");
|
|
||||||
}
|
}
|
||||||
|
if (outputText.includes("[IDENTIFIANT_PERSONNEL]")) {
|
||||||
// CREDIT_CARD -> Coordonnées bancaires (supprimer la duplication)
|
anonymizedTypes.add("Identifiants personnels");
|
||||||
if (outputText.includes("<CREDIT_CARD>")) {
|
|
||||||
anonymizedTypes.add("Coordonnées bancaires");
|
|
||||||
}
|
}
|
||||||
|
if (outputText.includes("[LOCALISATION_GPS]")) {
|
||||||
// IP_ADDRESS -> Valeurs numériques
|
anonymizedTypes.add("Coordonnées GPS");
|
||||||
if (outputText.includes("<IP_ADDRESS>")) {
|
|
||||||
anonymizedTypes.add("Valeurs numériques");
|
|
||||||
}
|
}
|
||||||
|
if (outputText.includes("[TITRE_CIVILITE]")) {
|
||||||
// BE_VAT -> Valeurs numériques
|
anonymizedTypes.add("Titres de civilité");
|
||||||
if (outputText.includes("<BE_VAT>")) {
|
|
||||||
anonymizedTypes.add("Valeurs numériques");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return anonymizedTypes;
|
return anonymizedTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Structure exacte de SupportedDataTypes (récupérée dynamiquement)
|
// Structure mise à jour avec les vrais types de données
|
||||||
const supportedDataStructure = [
|
const supportedDataStructure = [
|
||||||
{
|
{
|
||||||
items: ["Prénoms", "Numéros de téléphone", "Noms de domaine"],
|
items: [
|
||||||
|
"Noms et prénoms",
|
||||||
|
"Numéros de téléphone",
|
||||||
|
"URLs et identifiants web",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
items: ["Noms de famille", "Adresses", "Dates"],
|
items: ["Adresses postales", "Lieux géographiques", "Dates"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
items: ["Noms complets", "Numéros d'ID", "Coordonnées bancaires"],
|
items: ["Documents d'identité", "Comptes bancaires", "Cartes de crédit"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
items: ["Adresses e-mail", "Valeurs monétaires", "Texte personnalisé"],
|
items: ["Adresses e-mail", "Montants financiers", "Adresses IP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
"Noms d'organisations",
|
||||||
|
"Entreprises et sociétés",
|
||||||
|
"Numéros de TVA",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
"Parts de marché",
|
||||||
|
"Secrets commerciaux",
|
||||||
|
"Références de contrats",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
"Données biométriques",
|
||||||
|
"Données de santé",
|
||||||
|
"Données sensibles RGPD",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: ["Clés API secrètes", "Coordonnées GPS", "Titres de civilité"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -161,33 +236,61 @@ export const AnonymizationInterface = ({
|
|||||||
const anonymizedTypes = getAnonymizedDataTypes();
|
const anonymizedTypes = getAnonymizedDataTypes();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-3 mb-4">
|
{/* Instructions Panel */}
|
||||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
<h4 className="text-sm font-semibold text-green-700">
|
<div className="flex items-start gap-3">
|
||||||
Anonymisation terminée avec succès
|
<Info className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||||
</h4>
|
<div className="text-sm text-blue-800">
|
||||||
</div>
|
<p className="font-medium mb-2">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
|
Instructions d'utilisation :
|
||||||
{supportedDataStructure.map((column, columnIndex) => (
|
</p>
|
||||||
<div key={columnIndex} className="flex flex-col space-y-2">
|
<ul className="space-y-1 text-blue-700">
|
||||||
{column.items.map((item, itemIndex) => {
|
<li>• Survolez les mots pour les mettre en évidence</li>
|
||||||
const isAnonymized = anonymizedTypes.has(item);
|
<li>
|
||||||
return (
|
• Cliquez pour sélectionner un mot, Ctrl/CMD (ou Shift) +
|
||||||
<span
|
clic.
|
||||||
key={itemIndex}
|
</li>
|
||||||
className={
|
<li>• Faites clic droit pour ouvrir le menu contextuel</li>
|
||||||
isAnonymized
|
<li>• Modifiez les labels et couleurs selon vos besoins</li>
|
||||||
? "text-green-700 font-medium"
|
<li>
|
||||||
: "text-gray-400"
|
• Utilisez "Toutes les occurrences" pour appliquer à
|
||||||
}
|
tous les mots similaires
|
||||||
>
|
</li>
|
||||||
{isAnonymized ? "✓" : "•"} {item}
|
</ul>
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bloc vert existant */}
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
||||||
|
<div className="flex items-center space-x-3 mb-4">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-600" />
|
||||||
|
<h4 className="text-sm font-semibold text-green-700">
|
||||||
|
Anonymisation terminée avec succès
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
|
||||||
|
{supportedDataStructure.map((column, columnIndex) => (
|
||||||
|
<div key={columnIndex} className="flex flex-col space-y-2">
|
||||||
|
{column.items.map((item, itemIndex) => {
|
||||||
|
const isAnonymized = anonymizedTypes.has(item);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={itemIndex}
|
||||||
|
className={
|
||||||
|
isAnonymized
|
||||||
|
? "text-green-700 font-medium"
|
||||||
|
: "text-gray-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isAnonymized ? "✓" : "•"} {item}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,17 +13,19 @@ interface ProcessDocumentResponse {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props du hook
|
// Props du hook - Renommer pour correspondre à l'utilisation
|
||||||
interface AnonymizationLogicProps {
|
interface UseAnonymizationProps {
|
||||||
setOutputText: (text: string) => void;
|
setOutputText: (text: string) => void;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
setEntityMappings: (mappings: EntityMapping[]) => void;
|
setEntityMappings: (mappings: EntityMapping[]) => void;
|
||||||
|
setAnonymizedText?: (text: string) => void; // Nouveau paramètre optionnel
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOUVEAU: Définir les types pour le paramètre de anonymizeData
|
// NOUVEAU: Définir les types pour le paramètre de anonymizeData
|
||||||
interface AnonymizeDataParams {
|
interface AnonymizeDataParams {
|
||||||
file?: File | null;
|
file?: File | null;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
category?: string; // Ajouter le paramètre catégorie
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,10 +36,11 @@ export const useAnonymization = ({
|
|||||||
setOutputText,
|
setOutputText,
|
||||||
setError,
|
setError,
|
||||||
setEntityMappings,
|
setEntityMappings,
|
||||||
}: AnonymizationLogicProps) => {
|
setAnonymizedText,
|
||||||
|
}: UseAnonymizationProps) => {
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
const anonymizeData = async ({ file, text }: AnonymizeDataParams) => {
|
const anonymizeData = async ({ file, text, category = 'pii' }: AnonymizeDataParams) => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setEntityMappings([]);
|
setEntityMappings([]);
|
||||||
@@ -46,6 +49,10 @@ export const useAnonymization = ({
|
|||||||
try {
|
try {
|
||||||
// ÉTAPE 1: Construire le FormData ici pour garantir le bon format
|
// ÉTAPE 1: Construire le FormData ici pour garantir le bon format
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Ajouter la catégorie au FormData
|
||||||
|
formData.append('category', category);
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
} else if (text) {
|
} else if (text) {
|
||||||
@@ -74,17 +81,22 @@ export const useAnonymization = ({
|
|||||||
const replacementValues = data.replacementValues || {}; // Récupérer les valeurs de remplacement
|
const replacementValues = data.replacementValues || {}; // Récupérer les valeurs de remplacement
|
||||||
|
|
||||||
// 🔍 AJOUT DES CONSOLE.LOG POUR DÉBOGUER
|
// 🔍 AJOUT DES CONSOLE.LOG POUR DÉBOGUER
|
||||||
console.log("📊 Données reçues de Presidio:", {
|
console.log("📊 Réponse de l'API:", {
|
||||||
originalTextLength: originalText.length,
|
originalTextLength: originalText.length,
|
||||||
presidioResultsCount: presidioResults.length,
|
presidioResultsCount: presidioResults.length,
|
||||||
presidioResults: presidioResults,
|
presidioResults: presidioResults,
|
||||||
replacementValues: replacementValues,
|
replacementValues: replacementValues,
|
||||||
replacementValuesKeys: Object.keys(replacementValues),
|
replacementValuesKeys: Object.keys(replacementValues),
|
||||||
replacementValuesEntries: Object.entries(replacementValues)
|
replacementValuesEntries: Object.entries(replacementValues),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ÉTAPE 2 : Passer le texte ORIGINAL à l'état de sortie.
|
// ÉTAPE 2 : Utiliser le texte ANONYMISÉ de Presidio au lieu du texte original
|
||||||
setOutputText(originalText);
|
setOutputText(data.anonymizedText || originalText);
|
||||||
|
|
||||||
|
// NOUVEAU : Stocker le texte anonymisé de Presidio séparément
|
||||||
|
if (setAnonymizedText && data.anonymizedText) {
|
||||||
|
setAnonymizedText(data.anonymizedText);
|
||||||
|
}
|
||||||
|
|
||||||
// ÉTAPE 3 : Créer le tableau de mapping avec la nouvelle structure
|
// ÉTAPE 3 : Créer le tableau de mapping avec la nouvelle structure
|
||||||
const sortedResults = [...presidioResults].sort(
|
const sortedResults = [...presidioResults].sort(
|
||||||
@@ -96,13 +108,13 @@ export const useAnonymization = ({
|
|||||||
for (const result of sortedResults) {
|
for (const result of sortedResults) {
|
||||||
const { entity_type, start, end } = result;
|
const { entity_type, start, end } = result;
|
||||||
const detectedText = originalText.substring(start, end);
|
const detectedText = originalText.substring(start, end);
|
||||||
|
|
||||||
// 🔍 CONSOLE.LOG POUR CHAQUE ENTITÉ
|
// 🔍 CONSOLE.LOG POUR CHAQUE ENTITÉ
|
||||||
console.log(`🔍 Entité détectée:`, {
|
console.log(`🔍 Entité détectée:`, {
|
||||||
entity_type,
|
entity_type,
|
||||||
detectedText,
|
detectedText,
|
||||||
replacementFromMap: replacementValues[detectedText],
|
replacementFromMap: replacementValues[detectedText],
|
||||||
fallback: `[${entity_type}]`
|
fallback: `[${entity_type}]`,
|
||||||
});
|
});
|
||||||
|
|
||||||
mappings.push({
|
mappings.push({
|
||||||
@@ -110,12 +122,14 @@ export const useAnonymization = ({
|
|||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
text: detectedText,
|
text: detectedText,
|
||||||
replacementValue: replacementValues[detectedText] || `[${entity_type}]`,
|
replacementValue: replacementValues[detectedText],
|
||||||
displayName: replacementValues[detectedText] || `[${entity_type}]`, // Ajouter cette ligne
|
displayName: replacementValues[detectedText]
|
||||||
|
? replacementValues[detectedText].replace(/[\[\]]/g, "")
|
||||||
|
: entity_type,
|
||||||
customColor: undefined,
|
customColor: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔍 CONSOLE.LOG FINAL DES MAPPINGS
|
// 🔍 CONSOLE.LOG FINAL DES MAPPINGS
|
||||||
console.log("📋 Mappings créés:", mappings);
|
console.log("📋 Mappings créés:", mappings);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import { Trash2, Check, RotateCcw } from "lucide-react";
|
import { Trash2, Check, RotateCcw } from "lucide-react";
|
||||||
import { COLOR_PALETTE, type ColorOption } from "../config/colorPalette";
|
import { COLOR_PALETTE, type ColorOption } from "../config/colorPalette";
|
||||||
// import { EntityMapping } from "../config/entityLabels"; // SUPPRIMER cette ligne
|
import { EntityMapping } from "../config/entityLabels";
|
||||||
|
|
||||||
interface ContextMenuProps {
|
interface ContextMenuProps {
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
@@ -12,11 +12,8 @@ interface ContextMenuProps {
|
|||||||
wordIndices: number[];
|
wordIndices: number[];
|
||||||
};
|
};
|
||||||
existingLabels: string[];
|
existingLabels: string[];
|
||||||
// entityMappings: EntityMapping[]; // SUPPRIMER cette ligne
|
entityMappings?: EntityMapping[];
|
||||||
onApplyLabel: (
|
onApplyLabel: (displayName: string, applyToAll?: boolean) => void;
|
||||||
displayName: string,
|
|
||||||
applyToAll?: boolean
|
|
||||||
) => void;
|
|
||||||
onApplyColor: (
|
onApplyColor: (
|
||||||
color: string,
|
color: string,
|
||||||
colorName: string,
|
colorName: string,
|
||||||
@@ -31,15 +28,16 @@ const colorOptions: ColorOption[] = COLOR_PALETTE;
|
|||||||
export const ContextMenu: React.FC<ContextMenuProps> = ({
|
export const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||||
contextMenu,
|
contextMenu,
|
||||||
existingLabels,
|
existingLabels,
|
||||||
// entityMappings, // SUPPRIMER cette ligne
|
entityMappings,
|
||||||
onApplyLabel,
|
onApplyLabel,
|
||||||
onApplyColor,
|
onApplyColor,
|
||||||
onRemoveLabel,
|
onRemoveLabel,
|
||||||
getCurrentColor,
|
getCurrentColor,
|
||||||
}) => {
|
}) => {
|
||||||
const [customLabel, setCustomLabel] = useState("");
|
|
||||||
const [showNewLabelInput, setShowNewLabelInput] = useState(false);
|
const [showNewLabelInput, setShowNewLabelInput] = useState(false);
|
||||||
|
const [newLabelValue, setNewLabelValue] = useState("");
|
||||||
const [showColorPalette, setShowColorPalette] = useState(false);
|
const [showColorPalette, setShowColorPalette] = useState(false);
|
||||||
|
const [tempSelectedColor, setTempSelectedColor] = useState('');
|
||||||
const [applyToAll, setApplyToAll] = useState(false);
|
const [applyToAll, setApplyToAll] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -58,16 +56,27 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
if (customLabel.trim()) {
|
if (newLabelValue.trim()) {
|
||||||
console.log(
|
console.log(
|
||||||
"Application du label personnalisé:",
|
"Application du label personnalisé:",
|
||||||
customLabel.trim(),
|
newLabelValue.trim(),
|
||||||
"À toutes les occurrences:",
|
"À toutes les occurrences:",
|
||||||
applyToAll
|
applyToAll
|
||||||
);
|
);
|
||||||
onApplyLabel(customLabel.trim(), applyToAll); // CORRIGER: 2 paramètres seulement
|
|
||||||
setCustomLabel("");
|
// Appliquer d'abord le label
|
||||||
|
onApplyLabel(newLabelValue.trim(), applyToAll);
|
||||||
|
|
||||||
|
// Puis appliquer la couleur temporaire si elle existe
|
||||||
|
if (tempSelectedColor) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onApplyColor(tempSelectedColor, 'Couleur personnalisée', applyToAll);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewLabelValue("");
|
||||||
setShowNewLabelInput(false);
|
setShowNewLabelInput(false);
|
||||||
|
setTempSelectedColor(''); // Reset de la couleur temporaire
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,7 +86,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log("Annulation du nouveau label");
|
console.log("Annulation du nouveau label");
|
||||||
setShowNewLabelInput(false);
|
setShowNewLabelInput(false);
|
||||||
setCustomLabel("");
|
setNewLabelValue("");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fonction pour empêcher la propagation des événements
|
// Fonction pour empêcher la propagation des événements
|
||||||
@@ -149,36 +158,47 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
|
|
||||||
<div className="h-6 w-px bg-gray-300 flex-shrink-0"></div>
|
<div className="h-6 w-px bg-gray-300 flex-shrink-0"></div>
|
||||||
|
|
||||||
{/* Labels existants */}
|
{/* Labels existants - toujours visible */}
|
||||||
{existingLabels.length > 0 && (
|
<div className="flex-shrink-0">
|
||||||
<>
|
<select
|
||||||
<div className="flex-shrink-0">
|
onChange={(e) => {
|
||||||
<select
|
e.stopPropagation();
|
||||||
onChange={(e) => {
|
if (e.target.value) {
|
||||||
e.stopPropagation();
|
const selectedDisplayName = e.target.value;
|
||||||
if (e.target.value) {
|
console.log("📋 Label sélectionné:", selectedDisplayName);
|
||||||
const selectedDisplayName = e.target.value; // displayName
|
|
||||||
// CORRECTION: Plus besoin de chercher entity_type !
|
// Appliquer d'abord le label
|
||||||
onApplyLabel(selectedDisplayName, applyToAll);
|
onApplyLabel(selectedDisplayName, applyToAll);
|
||||||
}
|
|
||||||
}}
|
// Puis appliquer la couleur temporaire si elle existe
|
||||||
onClick={(e) => e.stopPropagation()}
|
if (tempSelectedColor) {
|
||||||
className="text-xs border border-gray-300 rounded px-2 py-1 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
setTimeout(() => {
|
||||||
defaultValue=""
|
onApplyColor(tempSelectedColor, 'Couleur personnalisée', applyToAll);
|
||||||
>
|
}, 100);
|
||||||
<option value="" disabled>
|
}
|
||||||
Chosi
|
|
||||||
</option>
|
// Reset du select et de la couleur temporaire
|
||||||
{existingLabels.map((label) => (
|
e.target.value = "";
|
||||||
<option key={label} value={label}>
|
setTempSelectedColor('');
|
||||||
{label}
|
}
|
||||||
</option>
|
}}
|
||||||
))}
|
onClick={(e) => e.stopPropagation()}
|
||||||
</select>
|
className="text-xs border border-blue-300 rounded px-2 py-1 bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-[120px] cursor-pointer hover:bg-blue-100 transition-colors"
|
||||||
</div>
|
defaultValue=""
|
||||||
<div className="h-6 w-px bg-gray-300 flex-shrink-0"></div>
|
>
|
||||||
</>
|
<option value="" disabled className="text-gray-500">
|
||||||
)}
|
{existingLabels.length > 0
|
||||||
|
? `📋 Labels (${existingLabels.length})`
|
||||||
|
: "📋 Aucun label"}
|
||||||
|
</option>
|
||||||
|
{existingLabels.map((label) => (
|
||||||
|
<option key={label} value={label} className="text-gray-800">
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="h-6 w-px bg-gray-300 flex-shrink-0"></div>
|
||||||
|
|
||||||
{/* Nouveau label */}
|
{/* Nouveau label */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
@@ -210,10 +230,10 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={customLabel}
|
value={newLabelValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setCustomLabel(e.target.value);
|
setNewLabelValue(e.target.value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -235,7 +255,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleApplyCustomLabel}
|
onClick={handleApplyCustomLabel}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
disabled={!customLabel.trim()}
|
disabled={!newLabelValue.trim()}
|
||||||
className="px-1 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500"
|
className="px-1 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
title="Appliquer le label"
|
title="Appliquer le label"
|
||||||
>
|
>
|
||||||
@@ -263,7 +283,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="w-5 h-5 rounded-full border-2 border-gray-300 cursor-pointer hover:border-gray-400 transition-all"
|
className="w-5 h-5 rounded-full border-2 border-gray-300 cursor-pointer hover:border-gray-400 transition-all"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: getCurrentColor(contextMenu.selectedText),
|
backgroundColor: tempSelectedColor || getCurrentColor(contextMenu.selectedText),
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -276,21 +296,36 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
|
|
||||||
{showColorPalette && (
|
{showColorPalette && (
|
||||||
<div className="flex items-center space-x-1 bg-gray-50 p-1 rounded border absolute z-10 mt-1 left-0">
|
<div className="flex items-center space-x-1 bg-gray-50 p-1 rounded border absolute z-10 mt-1 left-0">
|
||||||
{colorOptions.map((color) => (
|
{colorOptions.map((color) => {
|
||||||
<button
|
return (
|
||||||
key={color.value}
|
<button
|
||||||
type="button"
|
key={color.value}
|
||||||
onClick={(e) => {
|
type="button"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
onApplyColor(color.value, color.name, applyToAll);
|
e.stopPropagation();
|
||||||
setShowColorPalette(false);
|
|
||||||
}}
|
// Vérifier si le texte a déjà un mapping (modification)
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
const existingMapping = entityMappings?.find(mapping =>
|
||||||
className="w-4 h-4 rounded-full border-2 border-gray-300 cursor-pointer hover:border-gray-400 transition-all"
|
mapping.text === contextMenu.selectedText
|
||||||
style={{ backgroundColor: color.value }}
|
);
|
||||||
title={color.name}
|
|
||||||
/>
|
if (existingMapping) {
|
||||||
))}
|
// MODIFICATION : Appliquer directement la couleur
|
||||||
|
onApplyColor(color.value, color.name, applyToAll);
|
||||||
|
} else {
|
||||||
|
// CRÉATION : Juste stocker la couleur temporaire
|
||||||
|
setTempSelectedColor(color.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowColorPalette(false);
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="w-4 h-4 rounded-full border-2 border-gray-300 cursor-pointer hover:border-gray-400 transition-all"
|
||||||
|
style={{ backgroundColor: color.value }}
|
||||||
|
title={color.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { generateAnonymizedText } from "@/app/utils/generateAnonymizedText";
|
|
||||||
import { EntityMapping } from "@/app/config/entityLabels";
|
import { EntityMapping } from "@/app/config/entityLabels";
|
||||||
|
|
||||||
interface DownloadActionsProps {
|
interface DownloadActionsProps {
|
||||||
outputText: string;
|
outputText: string;
|
||||||
entityMappings?: EntityMapping[];
|
entityMappings?: EntityMapping[];
|
||||||
|
anonymizedText?: string; // Nouveau paramètre pour le texte déjà anonymisé par Presidio
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDownloadActions = ({
|
export const useDownloadActions = ({
|
||||||
outputText,
|
outputText,
|
||||||
entityMappings = [],
|
anonymizedText, // Texte déjà anonymisé par Presidio
|
||||||
}: DownloadActionsProps) => {
|
}: DownloadActionsProps) => {
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
const anonymizedText = generateAnonymizedText(outputText, entityMappings);
|
// Toujours utiliser le texte anonymisé de Presidio
|
||||||
navigator.clipboard.writeText(anonymizedText);
|
const textToCopy = anonymizedText || outputText;
|
||||||
|
navigator.clipboard.writeText(textToCopy);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadText = () => {
|
const downloadText = () => {
|
||||||
const anonymizedText = generateAnonymizedText(outputText, entityMappings);
|
// Utiliser le texte anonymisé de Presidio si disponible, sinon fallback sur outputText
|
||||||
const blob = new Blob([anonymizedText], { type: "text/plain" });
|
const textToDownload = anonymizedText || outputText;
|
||||||
|
const blob = new Blob([textToDownload], { type: "text/plain" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const EntityMappingTable = ({ mappings }: EntityMappingTableProps) => {
|
|||||||
return {
|
return {
|
||||||
...mapping,
|
...mapping,
|
||||||
entityNumber: entityCounts[entityType],
|
entityNumber: entityCounts[entityType],
|
||||||
displayName: mapping.displayName || mapping.replacementValue || `[${entityType}]`,
|
displayName: mapping.entity_type,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
import { SampleTextComponent } from "./SampleTextComponent";
|
import { SampleTextComponent } from "./SampleTextComponent";
|
||||||
import { SupportedDataTypes } from "./SupportedDataTypes";
|
import { SupportedDataTypes } from "./SupportedDataTypes";
|
||||||
import { AnonymizationInterface } from "./AnonymizationInterface";
|
import { AnonymizationInterface } from "./AnonymizationInterface";
|
||||||
import { highlightEntities } from "../utils/highlightEntities";
|
|
||||||
import { useState } from "react";
|
import { InteractiveTextEditor } from "./InteractiveTextEditor";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { EntityMapping } from "../config/entityLabels"; // Importer l'interface unifiée
|
import { EntityMapping } from "../config/entityLabels"; // Importer l'interface unifiée
|
||||||
|
|
||||||
// Supprimer l'interface locale EntityMapping (lignes 15-21)
|
// Supprimer l'interface locale EntityMapping (lignes 15-21)
|
||||||
@@ -21,7 +22,7 @@ interface FileUploadComponentProps {
|
|||||||
sourceText: string;
|
sourceText: string;
|
||||||
setSourceText: (text: string) => void;
|
setSourceText: (text: string) => void;
|
||||||
setUploadedFile: (file: File | null) => void;
|
setUploadedFile: (file: File | null) => void;
|
||||||
onAnonymize?: () => void;
|
onAnonymize?: (category?: string) => void;
|
||||||
isProcessing?: boolean;
|
isProcessing?: boolean;
|
||||||
canAnonymize?: boolean;
|
canAnonymize?: boolean;
|
||||||
isLoadingFile?: boolean;
|
isLoadingFile?: boolean;
|
||||||
@@ -32,6 +33,7 @@ interface FileUploadComponentProps {
|
|||||||
isExampleLoaded?: boolean;
|
isExampleLoaded?: boolean;
|
||||||
setIsExampleLoaded?: (loaded: boolean) => void;
|
setIsExampleLoaded?: (loaded: boolean) => void;
|
||||||
entityMappings?: EntityMapping[];
|
entityMappings?: EntityMapping[];
|
||||||
|
onMappingsUpdate?: (mappings: EntityMapping[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadComponent = ({
|
export const FileUploadComponent = ({
|
||||||
@@ -50,8 +52,10 @@ export const FileUploadComponent = ({
|
|||||||
downloadText,
|
downloadText,
|
||||||
setIsExampleLoaded,
|
setIsExampleLoaded,
|
||||||
entityMappings,
|
entityMappings,
|
||||||
|
onMappingsUpdate,
|
||||||
}: FileUploadComponentProps) => {
|
}: FileUploadComponentProps) => {
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState("pii");
|
||||||
|
|
||||||
// Fonction pour valider le type de fichier
|
// Fonction pour valider le type de fichier
|
||||||
const isValidFileType = (file: File) => {
|
const isValidFileType = (file: File) => {
|
||||||
@@ -173,7 +177,7 @@ export const FileUploadComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bloc résultat anonymisé */}
|
{/* Bloc résultat anonymisé - MODE INTERACTIF */}
|
||||||
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
|
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
|
||||||
<div className="bg-green-50 border-b border-green-200 px-4 sm:px-6 py-4">
|
<div className="bg-green-50 border-b border-green-200 px-4 sm:px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -183,7 +187,7 @@ export const FileUploadComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs sm:text-sm text-green-600">
|
<p className="text-xs sm:text-sm text-green-600">
|
||||||
Document anonymisé
|
DOCUMENT ANONYMISÉ MODE INTERACTIF
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,12 +219,208 @@ export const FileUploadComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<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">
|
<InteractiveTextEditor
|
||||||
{highlightEntities(
|
text={sourceText}
|
||||||
sourceText || "Aucun contenu à afficher", // Utiliser sourceText au lieu de outputText
|
entityMappings={entityMappings || []}
|
||||||
entityMappings || [] // Fournir un tableau vide par défaut
|
onUpdateMapping={(
|
||||||
)}
|
originalValue,
|
||||||
</div>
|
newLabel,
|
||||||
|
entityType,
|
||||||
|
applyToAllOccurrences,
|
||||||
|
customColor,
|
||||||
|
wordStart,
|
||||||
|
wordEnd
|
||||||
|
) => {
|
||||||
|
if (onMappingsUpdate && entityMappings) {
|
||||||
|
console.log("🔄 Mise à jour mapping:", {
|
||||||
|
originalValue,
|
||||||
|
newLabel,
|
||||||
|
entityType,
|
||||||
|
applyToAllOccurrences,
|
||||||
|
customColor,
|
||||||
|
wordStart,
|
||||||
|
wordEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedMappings: EntityMapping[];
|
||||||
|
|
||||||
|
if (applyToAllOccurrences) {
|
||||||
|
// CORRECTION: Créer des mappings pour toutes les occurrences dans le texte
|
||||||
|
const existingMappingsForOtherTexts =
|
||||||
|
entityMappings.filter(
|
||||||
|
(mapping) => mapping.text !== originalValue
|
||||||
|
);
|
||||||
|
|
||||||
|
const newMappings: EntityMapping[] = [];
|
||||||
|
let searchIndex = 0;
|
||||||
|
|
||||||
|
// Chercher toutes les occurrences dans le texte source
|
||||||
|
while (true) {
|
||||||
|
const foundIndex = sourceText.indexOf(
|
||||||
|
originalValue,
|
||||||
|
searchIndex
|
||||||
|
);
|
||||||
|
if (foundIndex === -1) break;
|
||||||
|
|
||||||
|
// Vérifier que c'est une occurrence valide (limites de mots)
|
||||||
|
const isValidBoundary =
|
||||||
|
(foundIndex === 0 ||
|
||||||
|
!/\w/.test(sourceText[foundIndex - 1])) &&
|
||||||
|
(foundIndex + originalValue.length ===
|
||||||
|
sourceText.length ||
|
||||||
|
!/\w/.test(
|
||||||
|
sourceText[foundIndex + originalValue.length]
|
||||||
|
));
|
||||||
|
|
||||||
|
if (isValidBoundary) {
|
||||||
|
newMappings.push({
|
||||||
|
text: originalValue,
|
||||||
|
entity_type: entityType,
|
||||||
|
start: foundIndex,
|
||||||
|
end: foundIndex + originalValue.length,
|
||||||
|
displayName: newLabel,
|
||||||
|
customColor: customColor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchIndex = foundIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMappings = [
|
||||||
|
...existingMappingsForOtherTexts,
|
||||||
|
...newMappings,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Logique existante pour une seule occurrence
|
||||||
|
if (
|
||||||
|
wordStart !== undefined &&
|
||||||
|
wordEnd !== undefined
|
||||||
|
) {
|
||||||
|
const targetMapping = entityMappings.find(
|
||||||
|
(mapping) =>
|
||||||
|
mapping.start === wordStart &&
|
||||||
|
mapping.end === wordEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetMapping) {
|
||||||
|
updatedMappings = entityMappings.map(
|
||||||
|
(mapping) => {
|
||||||
|
if (
|
||||||
|
mapping.start === wordStart &&
|
||||||
|
mapping.end === wordEnd
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...mapping,
|
||||||
|
displayName: newLabel,
|
||||||
|
entity_type: entityType,
|
||||||
|
customColor: customColor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const newMapping: EntityMapping = {
|
||||||
|
text: originalValue,
|
||||||
|
entity_type: entityType,
|
||||||
|
start: wordStart,
|
||||||
|
end: wordEnd,
|
||||||
|
displayName: newLabel,
|
||||||
|
customColor: customColor,
|
||||||
|
};
|
||||||
|
updatedMappings = [...entityMappings, newMapping];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: logique existante
|
||||||
|
const existingMappingIndex =
|
||||||
|
entityMappings.findIndex(
|
||||||
|
(mapping) => mapping.text === originalValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingMappingIndex !== -1) {
|
||||||
|
updatedMappings = entityMappings.map(
|
||||||
|
(mapping, index) => {
|
||||||
|
if (index === existingMappingIndex) {
|
||||||
|
return {
|
||||||
|
...mapping,
|
||||||
|
displayName: newLabel,
|
||||||
|
entity_type: entityType,
|
||||||
|
customColor: customColor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const foundIndex =
|
||||||
|
sourceText.indexOf(originalValue);
|
||||||
|
if (foundIndex !== -1) {
|
||||||
|
const newMapping: EntityMapping = {
|
||||||
|
text: originalValue,
|
||||||
|
entity_type: entityType,
|
||||||
|
start: foundIndex,
|
||||||
|
end: foundIndex + originalValue.length,
|
||||||
|
displayName: newLabel,
|
||||||
|
customColor: customColor,
|
||||||
|
};
|
||||||
|
updatedMappings = [
|
||||||
|
...entityMappings,
|
||||||
|
newMapping,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
updatedMappings = entityMappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"✅ Mappings mis à jour:",
|
||||||
|
updatedMappings.length
|
||||||
|
);
|
||||||
|
onMappingsUpdate(
|
||||||
|
updatedMappings.sort((a, b) => a.start - b.start)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRemoveMapping={(originalValue, applyToAll) => {
|
||||||
|
if (onMappingsUpdate && entityMappings) {
|
||||||
|
console.log("🗑️ Suppression mapping:", {
|
||||||
|
originalValue,
|
||||||
|
applyToAll,
|
||||||
|
});
|
||||||
|
|
||||||
|
let filteredMappings: EntityMapping[];
|
||||||
|
|
||||||
|
if (applyToAll) {
|
||||||
|
// Supprimer toutes les occurrences
|
||||||
|
filteredMappings = entityMappings.filter(
|
||||||
|
(mapping) => mapping.text !== originalValue
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Supprimer seulement la première occurrence
|
||||||
|
const firstIndex = entityMappings.findIndex(
|
||||||
|
(mapping) => mapping.text === originalValue
|
||||||
|
);
|
||||||
|
if (firstIndex !== -1) {
|
||||||
|
filteredMappings = entityMappings.filter(
|
||||||
|
(_, index) => index !== firstIndex
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
filteredMappings = entityMappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"✅ Mappings après suppression:",
|
||||||
|
filteredMappings.length
|
||||||
|
);
|
||||||
|
onMappingsUpdate(
|
||||||
|
filteredMappings.sort((a, b) => a.start - b.start)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,12 +486,37 @@ export const FileUploadComponent = ({
|
|||||||
{/* Boutons d'action - Responsive mobile */}
|
{/* Boutons d'action - Responsive mobile */}
|
||||||
{canAnonymize && !isLoadingFile && (
|
{canAnonymize && !isLoadingFile && (
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4">
|
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4">
|
||||||
|
{/* Sélecteur de catégorie - NOUVEAU */}
|
||||||
|
{onAnonymize && !outputText && (
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<label className="text-xs font-medium text-gray-700 text-center">
|
||||||
|
Catégorie d'anonymisation
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedCategory}
|
||||||
|
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#f7ab6e] focus:border-[#f7ab6e] bg-white"
|
||||||
|
>
|
||||||
|
<option value="pii">🔒 PII (Données Personnelles)</option>
|
||||||
|
<option value="business">🏢 Business (Données Métier)</option>
|
||||||
|
<option value="pii_business">
|
||||||
|
🔒🏢 PII + Business (Tout)
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Bouton Anonymiser - seulement si pas encore anonymisé */}
|
{/* Bouton Anonymiser - seulement si pas encore anonymisé */}
|
||||||
{onAnonymize && !outputText && (
|
{onAnonymize && !outputText && (
|
||||||
<button
|
<button
|
||||||
onClick={onAnonymize}
|
onClick={() => onAnonymize?.(selectedCategory)}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing || !sourceText.trim()}
|
||||||
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"
|
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"
|
||||||
|
: "Saisissez du texte pour anonymiser"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
@@ -422,13 +647,16 @@ export const FileUploadComponent = ({
|
|||||||
Type de données :
|
Type de données :
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<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">
|
<select
|
||||||
<option>
|
value={selectedCategory}
|
||||||
Informations Personnellement Identifiables (PII)
|
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||||
</option>
|
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 disabled style={{ color: "lightgray" }}>
|
>
|
||||||
PII + Données Business (En développement)
|
<option value="pii">🔒 PII (Données Personnelles)</option>
|
||||||
|
<option value="business">
|
||||||
|
🏢 Business (Données Métier)
|
||||||
</option>
|
</option>
|
||||||
|
<option value="pii_business">🔒🏢 PII + Business </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
|
<svg
|
||||||
@@ -444,7 +672,7 @@ export const FileUploadComponent = ({
|
|||||||
|
|
||||||
{/* Bouton Anonymiser */}
|
{/* Bouton Anonymiser */}
|
||||||
<button
|
<button
|
||||||
onClick={onAnonymize}
|
onClick={() => onAnonymize?.(selectedCategory)}
|
||||||
disabled={isProcessing || !sourceText.trim()}
|
disabled={isProcessing || !sourceText.trim()}
|
||||||
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"
|
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={
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ export const InstructionsPanel: React.FC = () => {
|
|||||||
<ul className="space-y-1 text-blue-700">
|
<ul className="space-y-1 text-blue-700">
|
||||||
<li>• Survolez les mots pour les mettre en évidence</li>
|
<li>• Survolez les mots pour les mettre en évidence</li>
|
||||||
<li>
|
<li>
|
||||||
• Cliquez pour sélectionner un mot, Crtl + clic pour plusieurs
|
• Cliquez pour sélectionner un mot, Ctrl/CMD (ou Shift) + clic.
|
||||||
mots
|
|
||||||
</li>
|
</li>
|
||||||
<li>• Faites clic droit pour ouvrir le menu contextuel</li>
|
<li>• Faites clic droit pour ouvrir le menu contextuel</li>
|
||||||
<li>• Modifiez les labels et couleurs selon vos besoins</li>
|
<li>• Modifiez les labels et couleurs selon vos besoins</li>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useContextMenu } from "./hooks/useContextMenu";
|
|||||||
import { useColorMapping } from "./hooks/useColorMapping";
|
import { useColorMapping } from "./hooks/useColorMapping";
|
||||||
import { TextDisplay } from "./TextDisplay";
|
import { TextDisplay } from "./TextDisplay";
|
||||||
import { ContextMenu } from "./ContextMenu";
|
import { ContextMenu } from "./ContextMenu";
|
||||||
import { InstructionsPanel } from "./InstructionsPanel";
|
|
||||||
|
|
||||||
interface InteractiveTextEditorProps {
|
interface InteractiveTextEditorProps {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -15,9 +14,11 @@ interface InteractiveTextEditorProps {
|
|||||||
newLabel: string,
|
newLabel: string,
|
||||||
entityType: string,
|
entityType: string,
|
||||||
applyToAllOccurrences?: boolean,
|
applyToAllOccurrences?: boolean,
|
||||||
customColor?: string // Ajouter ce paramètre
|
customColor?: string,
|
||||||
|
wordStart?: number,
|
||||||
|
wordEnd?: number
|
||||||
) => void;
|
) => void;
|
||||||
onRemoveMapping?: (originalValue: string) => void;
|
onRemoveMapping?: (originalValue: string, applyToAll?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
||||||
@@ -53,7 +54,10 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
// Support multi-sélection avec Ctrl, Cmd et Shift
|
||||||
|
const isMultiSelect = event.ctrlKey || event.metaKey || event.shiftKey;
|
||||||
|
|
||||||
|
if (isMultiSelect) {
|
||||||
setSelectedWords((prev) => {
|
setSelectedWords((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
if (newSet.has(index)) {
|
if (newSet.has(index)) {
|
||||||
@@ -95,8 +99,6 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative">
|
<div ref={containerRef} className="relative">
|
||||||
<InstructionsPanel />
|
|
||||||
|
|
||||||
<TextDisplay
|
<TextDisplay
|
||||||
words={words}
|
words={words}
|
||||||
text={text}
|
text={text}
|
||||||
|
|||||||
@@ -39,16 +39,16 @@ export const ResultPreviewComponent = ({
|
|||||||
const filteredMappings = entityMappings.filter(
|
const filteredMappings = entityMappings.filter(
|
||||||
(mapping) => mapping.text !== originalValue
|
(mapping) => mapping.text !== originalValue
|
||||||
);
|
);
|
||||||
|
|
||||||
const newMappings: EntityMapping[] = [];
|
const newMappings: EntityMapping[] = [];
|
||||||
|
|
||||||
if (applyToAllOccurrences) {
|
if (applyToAllOccurrences) {
|
||||||
// Appliquer à toutes les occurrences
|
// Appliquer à toutes les occurrences
|
||||||
let searchIndex = 0;
|
let searchIndex = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
const foundIndex = sourceText.indexOf(originalValue, searchIndex);
|
const foundIndex = sourceText.indexOf(originalValue, searchIndex);
|
||||||
if (foundIndex === -1) break;
|
if (foundIndex === -1) break;
|
||||||
|
|
||||||
if (isValidEntityBoundary(foundIndex, sourceText, originalValue)) {
|
if (isValidEntityBoundary(foundIndex, sourceText, originalValue)) {
|
||||||
newMappings.push({
|
newMappings.push({
|
||||||
text: originalValue,
|
text: originalValue,
|
||||||
@@ -68,7 +68,7 @@ export const ResultPreviewComponent = ({
|
|||||||
const targetMapping = entityMappings.find(
|
const targetMapping = entityMappings.find(
|
||||||
(mapping) => mapping.start === wordStart && mapping.end === wordEnd
|
(mapping) => mapping.start === wordStart && mapping.end === wordEnd
|
||||||
);
|
);
|
||||||
|
|
||||||
if (targetMapping) {
|
if (targetMapping) {
|
||||||
// Mettre à jour le mapping existant spécifique
|
// Mettre à jour le mapping existant spécifique
|
||||||
const updatedMappings = entityMappings.map((m) => {
|
const updatedMappings = entityMappings.map((m) => {
|
||||||
@@ -82,7 +82,7 @@ export const ResultPreviewComponent = ({
|
|||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMappingsUpdate?.(updatedMappings);
|
onMappingsUpdate?.(updatedMappings);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -101,7 +101,7 @@ export const ResultPreviewComponent = ({
|
|||||||
const existingMapping = entityMappings.find(
|
const existingMapping = entityMappings.find(
|
||||||
(mapping) => mapping.text === originalValue
|
(mapping) => mapping.text === originalValue
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingMapping) {
|
if (existingMapping) {
|
||||||
const updatedMappings = entityMappings.map((m) => {
|
const updatedMappings = entityMappings.map((m) => {
|
||||||
if (
|
if (
|
||||||
@@ -117,7 +117,7 @@ export const ResultPreviewComponent = ({
|
|||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMappingsUpdate?.(updatedMappings);
|
onMappingsUpdate?.(updatedMappings);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -138,7 +138,7 @@ export const ResultPreviewComponent = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifier le parent avec les nouveaux mappings
|
// Notifier le parent avec les nouveaux mappings
|
||||||
const allMappings = [...filteredMappings, ...newMappings];
|
const allMappings = [...filteredMappings, ...newMappings];
|
||||||
const uniqueMappings = allMappings.filter(
|
const uniqueMappings = allMappings.filter(
|
||||||
@@ -148,7 +148,7 @@ export const ResultPreviewComponent = ({
|
|||||||
(m) => m.start === mapping.start && m.end === mapping.end
|
(m) => m.start === mapping.start && m.end === mapping.end
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
onMappingsUpdate?.(uniqueMappings.sort((a, b) => a.start - b.start));
|
onMappingsUpdate?.(uniqueMappings.sort((a, b) => a.start - b.start));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ export const ResultPreviewComponent = ({
|
|||||||
<div className="flex-1 p-4 overflow-hidden">
|
<div className="flex-1 p-4 overflow-hidden">
|
||||||
<InteractiveTextEditor
|
<InteractiveTextEditor
|
||||||
text={sourceText}
|
text={sourceText}
|
||||||
entityMappings={entityMappings} // Utiliser entityMappings du parent au lieu de mappings
|
entityMappings={entityMappings}
|
||||||
onUpdateMapping={handleUpdateMapping}
|
onUpdateMapping={handleUpdateMapping}
|
||||||
onRemoveMapping={handleRemoveMapping}
|
onRemoveMapping={handleRemoveMapping}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Le contrat de prestation signé le 3 janvier 2024 prévoyait un montant de 75 00
|
|||||||
|
|
||||||
**Témoins clés :**
|
**Témoins clés :**
|
||||||
|
|
||||||
- Dr. Marie Claes (expert-comptable, n° IEC: 567890)
|
- Dr. Marie (expert-comptable, n° IEC: 567890)
|
||||||
- M. Pieter Van Der Berg (consultant IT, email: p.vanderberg@itconsult.be)
|
- M. Pieter Van Der Berg (consultant IT, email: p.vanderberg@itconsult.be)
|
||||||
|
|
||||||
**Données sensibles :**
|
**Données sensibles :**
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const TextDisplay: React.FC<TextDisplayProps> = ({
|
|||||||
const isHovered = hoveredWord === index;
|
const isHovered = hoveredWord === index;
|
||||||
|
|
||||||
let className =
|
let className =
|
||||||
"inline-block cursor-pointer transition-all duration-200 px-1 py-0.5 rounded-sm ";
|
"inline-block cursor-pointer transition-all duration-200 rounded-sm ";
|
||||||
let backgroundColor = "transparent";
|
let backgroundColor = "transparent";
|
||||||
|
|
||||||
if (word.isEntity) {
|
if (word.isEntity) {
|
||||||
@@ -70,11 +70,24 @@ export const TextDisplay: React.FC<TextDisplayProps> = ({
|
|||||||
className={className}
|
className={className}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
|
userSelect: "none",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onWordHover(index)}
|
onMouseEnter={() => onWordHover(index)}
|
||||||
onMouseLeave={() => onWordHover(null)}
|
onMouseLeave={() => onWordHover(null)}
|
||||||
onClick={(e) => onWordClick(index, e)}
|
onClick={(e) => {
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
onWordClick(index, e);
|
||||||
|
}}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
title={
|
title={
|
||||||
word.isEntity
|
word.isEntity
|
||||||
? `Entité: ${word.entityType} (Original: ${word.text})`
|
? `Entité: ${word.entityType} (Original: ${word.text})`
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const useColorMapping = (entityMappings: EntityMapping[]) => {
|
|||||||
const getCurrentColor = useCallback(
|
const getCurrentColor = useCallback(
|
||||||
(selectedText: string): string => {
|
(selectedText: string): string => {
|
||||||
if (!selectedText || !entityMappings) {
|
if (!selectedText || !entityMappings) {
|
||||||
return COLOR_PALETTE[0].value;
|
return '#e5e7eb'; // Couleur grise par défaut au lieu du bleu
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chercher le mapping correspondant au texte sélectionné
|
// Chercher le mapping correspondant au texte sélectionné
|
||||||
@@ -39,8 +39,8 @@ export const useColorMapping = (entityMappings: EntityMapping[]) => {
|
|||||||
return generateColorFromName(mapping.entity_type).value;
|
return generateColorFromName(mapping.entity_type).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Générer une couleur basée sur le texte
|
// Retourner gris par défaut si aucun mapping
|
||||||
return generateColorFromName(selectedText).value;
|
return '#e5e7eb';
|
||||||
},
|
},
|
||||||
[entityMappings]
|
[entityMappings]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { EntityMapping } from "@/app/config/entityLabels";
|
import { EntityMapping } from "@/app/config/entityLabels";
|
||||||
import { Word } from "./useTextParsing"; // AJOUTER cet import
|
import { Word } from "./useTextParsing";
|
||||||
|
|
||||||
interface ContextMenuState {
|
interface ContextMenuState {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -12,7 +12,7 @@ interface ContextMenuState {
|
|||||||
|
|
||||||
interface UseContextMenuProps {
|
interface UseContextMenuProps {
|
||||||
entityMappings: EntityMapping[];
|
entityMappings: EntityMapping[];
|
||||||
words: Word[]; // Maintenant le type Word est reconnu
|
words: Word[];
|
||||||
onUpdateMapping: (
|
onUpdateMapping: (
|
||||||
originalValue: string,
|
originalValue: string,
|
||||||
newLabel: string,
|
newLabel: string,
|
||||||
@@ -29,7 +29,7 @@ interface UseContextMenuProps {
|
|||||||
|
|
||||||
export const useContextMenu = ({
|
export const useContextMenu = ({
|
||||||
entityMappings,
|
entityMappings,
|
||||||
words, // Paramètre ajouté
|
words,
|
||||||
onUpdateMapping,
|
onUpdateMapping,
|
||||||
onRemoveMapping,
|
onRemoveMapping,
|
||||||
getCurrentColor,
|
getCurrentColor,
|
||||||
@@ -43,6 +43,10 @@ export const useContextMenu = ({
|
|||||||
wordIndices: [],
|
wordIndices: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Référence pour tracker les mappings précédents
|
||||||
|
const previousMappingsRef = useRef<EntityMapping[]>([]);
|
||||||
|
const previousLabelsRef = useRef<string[]>([]);
|
||||||
|
|
||||||
const closeContextMenu = useCallback(() => {
|
const closeContextMenu = useCallback(() => {
|
||||||
setContextMenu((prev) => ({ ...prev, visible: false }));
|
setContextMenu((prev) => ({ ...prev, visible: false }));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -54,26 +58,120 @@ export const useContextMenu = ({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getExistingLabels = useCallback(() => {
|
// OPTIMISATION INTELLIGENTE: Ne log que les changements
|
||||||
|
const existingLabels = useMemo(() => {
|
||||||
const uniqueLabels = new Set<string>();
|
const uniqueLabels = new Set<string>();
|
||||||
|
const newMappings: EntityMapping[] = [];
|
||||||
|
const changedMappings: EntityMapping[] = [];
|
||||||
|
const removedMappings: EntityMapping[] = [];
|
||||||
|
|
||||||
|
// Détecter les changements
|
||||||
|
const previousMap = new Map(
|
||||||
|
previousMappingsRef.current.map((m) => [m.text, m])
|
||||||
|
);
|
||||||
|
const currentMap = new Map(entityMappings.map((m) => [m.text, m]));
|
||||||
|
|
||||||
|
// Nouveaux mappings
|
||||||
entityMappings.forEach((mapping) => {
|
entityMappings.forEach((mapping) => {
|
||||||
uniqueLabels.add(mapping.displayName || mapping.entity_type); // Utiliser displayName
|
if (!previousMap.has(mapping.text)) {
|
||||||
|
newMappings.push(mapping);
|
||||||
|
} else {
|
||||||
|
const previous = previousMap.get(mapping.text)!;
|
||||||
|
if (JSON.stringify(previous) !== JSON.stringify(mapping)) {
|
||||||
|
changedMappings.push(mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return Array.from(uniqueLabels).sort();
|
|
||||||
|
// Mappings supprimés
|
||||||
|
previousMappingsRef.current.forEach((mapping) => {
|
||||||
|
if (!currentMap.has(mapping.text)) {
|
||||||
|
removedMappings.push(mapping);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logger seulement les changements
|
||||||
|
if (newMappings.length > 0) {
|
||||||
|
console.log("🆕 Nouveaux mappings détectés:", newMappings.length);
|
||||||
|
newMappings.forEach((mapping) => {
|
||||||
|
console.log("📋 Nouveau mapping:", {
|
||||||
|
text: mapping.text,
|
||||||
|
displayName: mapping.displayName,
|
||||||
|
entity_type: mapping.entity_type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedMappings.length > 0) {
|
||||||
|
console.log("🔄 Mappings modifiés:", changedMappings.length);
|
||||||
|
changedMappings.forEach((mapping) => {
|
||||||
|
console.log("📝 Mapping modifié:", {
|
||||||
|
text: mapping.text,
|
||||||
|
displayName: mapping.displayName,
|
||||||
|
entity_type: mapping.entity_type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedMappings.length > 0) {
|
||||||
|
console.log("🗑️ Mappings supprimés:", removedMappings.length);
|
||||||
|
removedMappings.forEach((mapping) => {
|
||||||
|
console.log("❌ Mapping supprimé:", {
|
||||||
|
text: mapping.text,
|
||||||
|
displayName: mapping.displayName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traitement de tous les mappings pour les labels
|
||||||
|
entityMappings.forEach((mapping) => {
|
||||||
|
if (
|
||||||
|
mapping.displayName &&
|
||||||
|
typeof mapping.displayName === "string" &&
|
||||||
|
mapping.displayName.trim().length > 0
|
||||||
|
) {
|
||||||
|
// Accepter tous les displayName non vides, pas seulement ceux avec crochets
|
||||||
|
uniqueLabels.add(mapping.displayName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = Array.from(uniqueLabels).sort();
|
||||||
|
|
||||||
|
// Logger seulement si les labels ont changé
|
||||||
|
const previousLabels = previousLabelsRef.current;
|
||||||
|
if (JSON.stringify(previousLabels) !== JSON.stringify(result)) {
|
||||||
|
console.log("🎯 Labels mis à jour:", {
|
||||||
|
ajoutés: result.filter((l) => !previousLabels.includes(l)),
|
||||||
|
supprimés: previousLabels.filter((l) => !result.includes(l)),
|
||||||
|
total: result.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour les références
|
||||||
|
previousMappingsRef.current = [...entityMappings];
|
||||||
|
previousLabelsRef.current = [...result];
|
||||||
|
|
||||||
|
return result;
|
||||||
}, [entityMappings]);
|
}, [entityMappings]);
|
||||||
|
|
||||||
// CORRECTION: Accepter displayName comme premier paramètre
|
const getExistingLabels = useCallback(() => {
|
||||||
|
return existingLabels;
|
||||||
|
}, [existingLabels]);
|
||||||
|
|
||||||
const applyLabel = useCallback(
|
const applyLabel = useCallback(
|
||||||
(displayName: string, applyToAll?: boolean) => {
|
(displayName: string, applyToAll?: boolean) => {
|
||||||
if (!contextMenu.selectedText) return;
|
if (!contextMenu.selectedText) return;
|
||||||
|
|
||||||
const originalText = contextMenu.selectedText;
|
const originalText = contextMenu.selectedText;
|
||||||
const firstWordIndex = contextMenu.wordIndices[0];
|
const selectedIndices = contextMenu.wordIndices;
|
||||||
|
|
||||||
// Calculer les vraies coordonnées start/end du mot cliqué
|
// Calculer les positions de début et fin pour tous les mots sélectionnés
|
||||||
const clickedWord = words[firstWordIndex];
|
const sortedIndices = selectedIndices.sort((a, b) => a - b);
|
||||||
const wordStart = clickedWord?.start;
|
const firstWord = words[sortedIndices[0]];
|
||||||
const wordEnd = clickedWord?.end;
|
const lastWord = words[sortedIndices[sortedIndices.length - 1]];
|
||||||
|
|
||||||
|
const wordStart = firstWord?.start;
|
||||||
|
const wordEnd = lastWord?.end;
|
||||||
|
|
||||||
const existingMapping = entityMappings.find(
|
const existingMapping = entityMappings.find(
|
||||||
(m) => m.text === originalText
|
(m) => m.text === originalText
|
||||||
@@ -82,14 +180,23 @@ export const useContextMenu = ({
|
|||||||
existingMapping?.entity_type ||
|
existingMapping?.entity_type ||
|
||||||
displayName.replace(/[\[\]]/g, "").toUpperCase();
|
displayName.replace(/[\[\]]/g, "").toUpperCase();
|
||||||
|
|
||||||
|
console.log("🏷️ Application de label:", {
|
||||||
|
text: originalText,
|
||||||
|
label: displayName,
|
||||||
|
entityType,
|
||||||
|
applyToAll,
|
||||||
|
wordIndices: selectedIndices,
|
||||||
|
positions: { start: wordStart, end: wordEnd },
|
||||||
|
});
|
||||||
|
|
||||||
onUpdateMapping(
|
onUpdateMapping(
|
||||||
originalText,
|
originalText,
|
||||||
displayName,
|
displayName,
|
||||||
entityType,
|
entityType,
|
||||||
applyToAll,
|
applyToAll,
|
||||||
undefined, // customColor
|
undefined,
|
||||||
wordStart, // vraies coordonnées start
|
wordStart,
|
||||||
wordEnd // vraies coordonnées end
|
wordEnd
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedWords(new Set());
|
setSelectedWords(new Set());
|
||||||
@@ -97,7 +204,7 @@ export const useContextMenu = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
contextMenu,
|
contextMenu,
|
||||||
words, // NOUVEAU
|
words,
|
||||||
entityMappings,
|
entityMappings,
|
||||||
onUpdateMapping,
|
onUpdateMapping,
|
||||||
closeContextMenu,
|
closeContextMenu,
|
||||||
@@ -105,59 +212,71 @@ export const useContextMenu = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// CORRECTION: Accepter applyToAll comme paramètre
|
|
||||||
const applyColorDirectly = useCallback(
|
const applyColorDirectly = useCallback(
|
||||||
(color: string, colorName: string, applyToAll?: boolean) => {
|
(color: string, colorName: string, applyToAll?: boolean) => {
|
||||||
if (!contextMenu.selectedText) return;
|
if (!contextMenu.selectedText) return;
|
||||||
|
|
||||||
const existingMapping = entityMappings.find(
|
const existingMapping = entityMappings.find(
|
||||||
(mapping) => mapping.text === contextMenu.selectedText
|
(mapping) => mapping.text === contextMenu.selectedText
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("useContextMenu - applyColorDirectly:", {
|
console.log("🎨 Application de couleur:", {
|
||||||
|
text: contextMenu.selectedText,
|
||||||
color,
|
color,
|
||||||
colorName,
|
colorName,
|
||||||
applyToAll,
|
applyToAll,
|
||||||
existingMapping,
|
existingMapping: !!existingMapping,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingMapping) {
|
if (existingMapping) {
|
||||||
|
// MODIFICATION : Appliquer directement la couleur pour un label existant
|
||||||
onUpdateMapping(
|
onUpdateMapping(
|
||||||
contextMenu.selectedText,
|
contextMenu.selectedText,
|
||||||
existingMapping.displayName || existingMapping.entity_type, // Utiliser displayName
|
existingMapping.displayName || existingMapping.entity_type,
|
||||||
existingMapping.entity_type,
|
existingMapping.entity_type,
|
||||||
applyToAll,
|
applyToAll,
|
||||||
color
|
color
|
||||||
);
|
);
|
||||||
|
setSelectedWords(new Set());
|
||||||
|
closeContextMenu();
|
||||||
} else {
|
} else {
|
||||||
|
// CRÉATION : Créer un nouveau label avec la couleur
|
||||||
|
// Utiliser le texte sélectionné comme nom de label par défaut
|
||||||
|
const defaultLabel = contextMenu.selectedText.toUpperCase();
|
||||||
|
|
||||||
|
console.log("🆕 Création d'un nouveau label avec couleur:", {
|
||||||
|
text: contextMenu.selectedText,
|
||||||
|
label: defaultLabel,
|
||||||
|
color,
|
||||||
|
applyToAll
|
||||||
|
});
|
||||||
|
|
||||||
onUpdateMapping(
|
onUpdateMapping(
|
||||||
contextMenu.selectedText,
|
contextMenu.selectedText,
|
||||||
"CUSTOM_LABEL",
|
defaultLabel,
|
||||||
"CUSTOM_LABEL",
|
defaultLabel,
|
||||||
applyToAll,
|
applyToAll,
|
||||||
color
|
color
|
||||||
);
|
);
|
||||||
|
setSelectedWords(new Set());
|
||||||
|
closeContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedWords(new Set());
|
|
||||||
closeContextMenu();
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
contextMenu.selectedText,
|
contextMenu.selectedText,
|
||||||
entityMappings, // Ajouter cette dépendance
|
entityMappings,
|
||||||
onUpdateMapping,
|
onUpdateMapping,
|
||||||
closeContextMenu,
|
closeContextMenu,
|
||||||
setSelectedWords,
|
setSelectedWords,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// CORRECTION: Accepter applyToAll comme paramètre
|
|
||||||
const removeLabel = useCallback(
|
const removeLabel = useCallback(
|
||||||
(applyToAll?: boolean) => {
|
(applyToAll?: boolean) => {
|
||||||
if (!contextMenu.selectedText || !onRemoveMapping) return;
|
if (!contextMenu.selectedText || !onRemoveMapping) return;
|
||||||
|
|
||||||
console.log("useContextMenu - removeLabel:", {
|
console.log("🗑️ Suppression de label:", {
|
||||||
selectedText: contextMenu.selectedText,
|
text: contextMenu.selectedText,
|
||||||
applyToAll,
|
applyToAll,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,7 +292,6 @@ export const useContextMenu = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Gestion des clics en dehors du menu
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (contextMenu.visible) {
|
if (contextMenu.visible) {
|
||||||
|
|||||||
@@ -46,27 +46,22 @@ export const useTextParsing = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utiliser displayName au lieu de entity_type
|
// Utiliser displayName directement SANS fallback
|
||||||
// Ligne 45 - Ajouter du debug
|
const anonymizedText = mapping.displayName;
|
||||||
console.log("useTextParsing - mapping:", {
|
|
||||||
text: mapping.text,
|
|
||||||
displayName: mapping.displayName,
|
|
||||||
entity_type: mapping.entity_type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const anonymizedText =
|
// Ne créer le segment que si displayName existe
|
||||||
mapping.displayName || `[${mapping.entity_type.toUpperCase()}]`;
|
if (anonymizedText) {
|
||||||
|
segments.push({
|
||||||
segments.push({
|
text: mapping.text,
|
||||||
text: mapping.text,
|
displayText: anonymizedText,
|
||||||
displayText: anonymizedText,
|
start: mapping.start,
|
||||||
start: mapping.start,
|
end: mapping.end,
|
||||||
end: mapping.end,
|
isEntity: true,
|
||||||
isEntity: true,
|
entityType: mapping.entity_type,
|
||||||
entityType: mapping.entity_type,
|
entityIndex: mappingIndex,
|
||||||
entityIndex: mappingIndex,
|
mapping: mapping,
|
||||||
mapping: mapping,
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
currentIndex = mapping.end; // CORRECTION: utiliser 'end'
|
currentIndex = mapping.end; // CORRECTION: utiliser 'end'
|
||||||
});
|
});
|
||||||
|
|||||||
26
app/page.tsx
26
app/page.tsx
@@ -7,7 +7,7 @@ import { ProgressBar } from "./components/ProgressBar";
|
|||||||
import { useFileHandler } from "./components/FileHandler";
|
import { useFileHandler } from "./components/FileHandler";
|
||||||
import { useAnonymization } from "./components/AnonymizationLogic";
|
import { useAnonymization } from "./components/AnonymizationLogic";
|
||||||
import { useDownloadActions } from "./components/DownloadActions";
|
import { useDownloadActions } from "./components/DownloadActions";
|
||||||
import { ResultPreviewComponent } from "./components/ResultPreviewComponent";
|
|
||||||
import { EntityMapping } from "./config/entityLabels"; // Importer l'interface unifiée
|
import { EntityMapping } from "./config/entityLabels"; // Importer l'interface unifiée
|
||||||
|
|
||||||
// Supprimer l'interface locale EntityMapping (lignes 12-18)
|
// Supprimer l'interface locale EntityMapping (lignes 12-18)
|
||||||
@@ -15,6 +15,7 @@ import { EntityMapping } from "./config/entityLabels"; // Importer l'interface u
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [sourceText, setSourceText] = useState("");
|
const [sourceText, setSourceText] = useState("");
|
||||||
const [outputText, setOutputText] = useState("");
|
const [outputText, setOutputText] = useState("");
|
||||||
|
const [anonymizedText, setAnonymizedText] = useState(""); // Nouveau state pour le texte anonymisé de Presidio
|
||||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoadingFile, setIsLoadingFile] = useState(false);
|
const [isLoadingFile, setIsLoadingFile] = useState(false);
|
||||||
@@ -60,16 +61,18 @@ export default function Home() {
|
|||||||
setOutputText,
|
setOutputText,
|
||||||
setError,
|
setError,
|
||||||
setEntityMappings,
|
setEntityMappings,
|
||||||
|
setAnonymizedText, // Passer la fonction pour stocker le texte anonymisé
|
||||||
});
|
});
|
||||||
|
|
||||||
const { copyToClipboard, downloadText } = useDownloadActions({
|
const { copyToClipboard, downloadText } = useDownloadActions({
|
||||||
outputText,
|
outputText,
|
||||||
entityMappings,
|
entityMappings,
|
||||||
|
anonymizedText, // Passer le texte anonymisé de Presidio
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fonction wrapper pour appeler anonymizeData avec les bonnes données
|
// Fonction wrapper pour appeler anonymizeData avec les bonnes données
|
||||||
const handleAnonymize = () => {
|
const handleAnonymize = (category?: string) => {
|
||||||
anonymizeData({ file: uploadedFile, text: sourceText });
|
anonymizeData({ file: uploadedFile, text: sourceText, category });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -102,26 +105,11 @@ export default function Home() {
|
|||||||
isExampleLoaded={isExampleLoaded}
|
isExampleLoaded={isExampleLoaded}
|
||||||
setIsExampleLoaded={setIsExampleLoaded}
|
setIsExampleLoaded={setIsExampleLoaded}
|
||||||
entityMappings={entityMappings}
|
entityMappings={entityMappings}
|
||||||
|
onMappingsUpdate={handleMappingsUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Interactive Text Editor - Nouveau composant pour l'édition interactive */}
|
|
||||||
{outputText && (
|
|
||||||
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
|
|
||||||
<div className="p-1 sm:p-3">
|
|
||||||
<ResultPreviewComponent
|
|
||||||
outputText={outputText}
|
|
||||||
sourceText={sourceText}
|
|
||||||
copyToClipboard={copyToClipboard}
|
|
||||||
downloadText={downloadText}
|
|
||||||
entityMappings={entityMappings}
|
|
||||||
onMappingsUpdate={handleMappingsUpdate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Entity Mapping Table - Seulement si outputText existe */}
|
{/* Entity Mapping Table - Seulement si outputText existe */}
|
||||||
{outputText && (
|
{outputText && (
|
||||||
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
|
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
|
||||||
|
|||||||
@@ -1,5 +1,59 @@
|
|||||||
import { EntityMapping } from "@/app/config/entityLabels";
|
import { EntityMapping } from "@/app/config/entityLabels";
|
||||||
|
|
||||||
|
// Fonction améliorée pour résoudre les chevauchements d'entités
|
||||||
|
const resolveOverlaps = (mappings: EntityMapping[]): EntityMapping[] => {
|
||||||
|
if (mappings.length <= 1) return mappings;
|
||||||
|
|
||||||
|
// Trier par position de début, puis par score/longueur
|
||||||
|
const sorted = [...mappings].sort((a, b) => {
|
||||||
|
if (a.start !== b.start) return a.start - b.start;
|
||||||
|
// En cas d'égalité, privilégier l'entité la plus longue
|
||||||
|
return (b.end - b.start) - (a.end - a.start);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved: EntityMapping[] = [];
|
||||||
|
|
||||||
|
for (const current of sorted) {
|
||||||
|
// Vérifier si cette entité chevauche avec une entité déjà acceptée
|
||||||
|
let hasConflict = false;
|
||||||
|
|
||||||
|
for (const existing of resolved) {
|
||||||
|
// Détecter tout type de chevauchement
|
||||||
|
const overlap = (
|
||||||
|
(current.start >= existing.start && current.start < existing.end) ||
|
||||||
|
(current.end > existing.start && current.end <= existing.end) ||
|
||||||
|
(current.start <= existing.start && current.end >= existing.end)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overlap) {
|
||||||
|
hasConflict = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasConflict) {
|
||||||
|
resolved.push(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved.sort((a, b) => a.start - b.start);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonction pour nettoyer et valider les mappings
|
||||||
|
const cleanMappings = (mappings: EntityMapping[], originalText: string): EntityMapping[] => {
|
||||||
|
return mappings.filter(mapping => {
|
||||||
|
// Vérifier que les indices sont valides
|
||||||
|
if (mapping.start < 0 || mapping.end < 0) return false;
|
||||||
|
if (mapping.start >= originalText.length) return false;
|
||||||
|
if (mapping.end > originalText.length) return false;
|
||||||
|
if (mapping.start >= mapping.end) return false;
|
||||||
|
|
||||||
|
// Vérifier que le texte correspond
|
||||||
|
const extractedText = originalText.slice(mapping.start, mapping.end);
|
||||||
|
return extractedText === mapping.text;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const generateAnonymizedText = (
|
export const generateAnonymizedText = (
|
||||||
originalText: string,
|
originalText: string,
|
||||||
mappings: EntityMapping[]
|
mappings: EntityMapping[]
|
||||||
@@ -8,20 +62,30 @@ export const generateAnonymizedText = (
|
|||||||
return originalText;
|
return originalText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trier les mappings par position de début
|
// Nettoyer et valider les mappings
|
||||||
const sortedMappings = [...mappings].sort((a, b) => a.start - b.start);
|
const cleanedMappings = cleanMappings(mappings, originalText);
|
||||||
|
|
||||||
|
// Résoudre les chevauchements
|
||||||
|
const resolvedMappings = resolveOverlaps(cleanedMappings);
|
||||||
|
|
||||||
let result = "";
|
let result = "";
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
|
|
||||||
for (const mapping of sortedMappings) {
|
for (const mapping of resolvedMappings) {
|
||||||
|
// Sécurité supplémentaire
|
||||||
|
if (mapping.start < lastIndex) continue;
|
||||||
|
|
||||||
// Ajouter le texte avant l'entité
|
// Ajouter le texte avant l'entité
|
||||||
result += originalText.slice(lastIndex, mapping.start);
|
result += originalText.slice(lastIndex, mapping.start);
|
||||||
|
|
||||||
// Utiliser displayName comme dans le tableau de mapping
|
// Utiliser la valeur de remplacement appropriée
|
||||||
result += mapping.displayName;
|
let replacement = mapping.replacementValue;
|
||||||
|
|
||||||
// Mettre à jour la position
|
if (!replacement) {
|
||||||
|
replacement = mapping.displayName || `[${mapping.entity_type}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += replacement;
|
||||||
lastIndex = mapping.end;
|
lastIndex = mapping.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export const highlightEntities = (
|
|||||||
|
|
||||||
// Créer et ajouter le badge stylisé pour l'entité
|
// Créer et ajouter le badge stylisé pour l'entité
|
||||||
const colorOption = generateColorFromName(entity_type);
|
const colorOption = generateColorFromName(entity_type);
|
||||||
const displayText = mapping.displayName || `[${entity_type.toUpperCase()}]`;
|
const displayText = entity_type;
|
||||||
|
|
||||||
parts.push(
|
parts.push(
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
Reference in New Issue
Block a user