Compare commits

..

10 Commits

Author SHA1 Message Date
Biqoz 4f9067f69d maj node 2026-01-29 01:23:53 +01:00
Biqoz 253ad0498b maj node 2026-01-29 01:20:03 +01:00
Biqoz 90b73080e0 clean presidio 2026-01-29 00:19:50 +01:00
Biqoz 050474e95b new interactive 2025-09-15 19:05:59 +02:00
nBiqoz 130929b756 ok 2025-09-12 16:54:40 +02:00
nBiqoz d7d3a3c7e9 mac multi select 2025-09-12 13:28:39 +02:00
nBiqoz 0360e1ca9f logs good, replacement good 2025-09-07 18:01:14 +02:00
nBiqoz 3a84da5c74 select 2025-09-07 16:41:22 +02:00
nBiqoz a0e033b7eb fr 2025-09-07 13:39:30 +02:00
nBiqoz f3c2cb6ff5 fr 2025-09-07 13:36:00 +02:00
23 changed files with 1536 additions and 858 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(npm install:*)",
"Bash(npx next:*)",
"Bash(node:*)",
"Bash(npm view:*)",
"Bash(npm info:*)",
"Bash(npm ls:*)",
"Bash(npm outdated:*)"
]
}
}
+30 -69
View File
@@ -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,80 +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> = {};
// Créer une copie du texte anonymisé pour le traitement
const workingText = anonymizedText; // ✅ Changé de 'let' à 'const'
// Supprimer workingOriginal car elle n'est jamais utilisée
// Trier les résultats par position (du plus grand au plus petit pour éviter les décalages)
const sortedResults = [...analyzerResults].sort(
(a, b) => b.start - a.start
);
for (const result of sortedResults) {
const originalValue = originalText.substring(
result.start,
result.end
);
// Extraire les parties avant et après l'entité dans le texte original
const beforeOriginal = originalText.substring(0, result.start);
const afterOriginal = originalText.substring(result.end);
// Trouver les mêmes parties dans le texte anonymisé
const beforeIndex = workingText.indexOf(beforeOriginal);
const afterIndex = workingText.lastIndexOf(afterOriginal);
if (beforeIndex !== -1 && afterIndex !== -1) {
// Extraire la valeur de remplacement entre ces deux parties
const startPos = beforeIndex + beforeOriginal.length;
const endPos = afterIndex;
const replacementValue = workingText.substring(startPos, endPos);
// Vérifier que c'est bien un remplacement (commence par [ et finit par ])
if (
replacementValue.startsWith("[") &&
replacementValue.endsWith("]")
) {
replacementMap[originalValue] = replacementValue;
}
}
}
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 });
+203 -100
View File
@@ -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&apos;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 &quot;Toutes les occurrences&quot; 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>
); );
+27 -13
View File
@@ -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);
+97 -62
View File
@@ -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>
+24 -6
View File
@@ -1,23 +1,41 @@
import { generateAnonymizedText } from "@/app/utils/generateAnonymizedText";
import { EntityMapping } from "@/app/config/entityLabels"; import { EntityMapping } from "@/app/config/entityLabels";
import { generateAnonymizedText } from "@/app/utils/generateAnonymizedText";
interface DownloadActionsProps { interface DownloadActionsProps {
outputText: string; outputText: string;
entityMappings?: EntityMapping[]; entityMappings?: EntityMapping[];
anonymizedText?: string;
sourceText?: string; // Ajouter le texte source
} }
export const useDownloadActions = ({ export const useDownloadActions = ({
outputText, outputText,
entityMappings = [], entityMappings,
anonymizedText,
sourceText, // Nouveau paramètre
}: DownloadActionsProps) => { }: DownloadActionsProps) => {
const copyToClipboard = () => { const copyToClipboard = () => {
const anonymizedText = generateAnonymizedText(outputText, entityMappings); // Utiliser les mappings mis à jour pour générer le texte final
navigator.clipboard.writeText(anonymizedText); let textToCopy = anonymizedText || outputText;
if (sourceText && entityMappings && entityMappings.length > 0) {
// Générer le texte avec les labels modifiés manuellement
textToCopy = generateAnonymizedText(sourceText, entityMappings);
}
navigator.clipboard.writeText(textToCopy);
}; };
const downloadText = () => { const downloadText = () => {
const anonymizedText = generateAnonymizedText(outputText, entityMappings); // Utiliser les mappings mis à jour pour générer le texte final
const blob = new Blob([anonymizedText], { type: "text/plain" }); let textToDownload = anonymizedText || outputText;
if (sourceText && entityMappings && entityMappings.length > 0) {
// Générer le texte avec les labels modifiés manuellement
textToDownload = generateAnonymizedText(sourceText, entityMappings);
}
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;
+168 -5
View File
@@ -12,9 +12,135 @@ import { EntityMapping } from "../config/entityLabels";
interface EntityMappingTableProps { interface EntityMappingTableProps {
mappings: EntityMapping[]; mappings: EntityMapping[];
selectedCategory?: string;
} }
export const EntityMappingTable = ({ mappings }: EntityMappingTableProps) => { // Fonction pour filtrer les entités selon la catégorie
const filterMappingsByCategory = (
mappings: EntityMapping[],
category: string = "pii_business"
): EntityMapping[] => {
if (category === "pii_business") {
return mappings; // Tout afficher
}
// Définir les entités PII (Données personnelles)
const piiEntities = new Set([
// Données personnelles de base
"PERSONNE",
"PERSON",
"DATE",
"DATE_TIME",
"EMAIL_ADDRESS",
"ADRESSE_EMAIL",
"PHONE_NUMBER",
"TELEPHONE",
"CREDIT_CARD",
"IBAN",
"ADRESSE_IP",
// Adresses personnelles
"ADRESSE",
"ADRESSE_FRANCAISE",
"ADRESSE_BELGE",
"LOCATION",
// Téléphones personnels
"TELEPHONE_FRANCAIS",
"TELEPHONE_BELGE",
// Documents d'identité personnels
"NUMERO_SECURITE_SOCIALE_FRANCAIS",
"REGISTRE_NATIONAL_BELGE",
"CARTE_IDENTITE_FRANCAISE",
"CARTE_IDENTITE_BELGE",
"PASSEPORT_FRANCAIS",
"PASSEPORT_BELGE",
"PERMIS_CONDUIRE_FRANCAIS",
// Données financières personnelles
"COMPTE_BANCAIRE_FRANCAIS",
// Données sensibles RGPD
"HEALTH_DATA",
"DONNEES_SANTE",
"SEXUAL_ORIENTATION",
"ORIENTATION_SEXUELLE",
"POLITICAL_OPINIONS",
"OPINIONS_POLITIQUES",
"BIOMETRIC_DATA",
"DONNEES_BIOMETRIQUES",
"RGPD_FINANCIAL_DATA",
"DONNEES_FINANCIERES_RGPD",
// Identifiants personnels
"IDENTIFIANT_PERSONNEL",
]);
// Définir les entités Business (Données d'entreprise)
const businessEntities = new Set([
// Organisations et sociétés
"ORGANISATION",
"ORGANIZATION",
"SOCIETE_FRANCAISE",
"SOCIETE_BELGE",
// Identifiants fiscaux et d'entreprise
"TVA_FRANCAISE",
"TVA_BELGE",
"NUMERO_FISCAL_FRANCAIS",
"SIRET_SIREN_FRANCAIS",
"NUMERO_ENTREPRISE_BELGE",
// Identifiants professionnels
"ID_PROFESSIONNEL_BELGE",
// Données commerciales
"MARKET_SHARE",
"SECRET_COMMERCIAL",
"REFERENCE_CONTRAT",
"MONTANT_FINANCIER",
// Données techniques d'entreprise
"CLE_API_SECRETE",
]);
// Définir les entités mixtes (PII + Business)
const mixedEntities = new Set([
// Données pouvant être personnelles ou professionnelles
"TITRE_CIVILITE",
"DONNEES_PROFESSIONNELLES",
"LOCALISATION_GPS",
"URL_IDENTIFIANT",
]);
if (category === "pii") {
// Inclure PII + mixtes
const allowedEntities = new Set([...piiEntities, ...mixedEntities]);
return mappings.filter((mapping) =>
allowedEntities.has(mapping.entity_type)
);
}
if (category === "business") {
// Inclure Business + mixtes
const allowedEntities = new Set([...businessEntities, ...mixedEntities]);
return mappings.filter((mapping) =>
allowedEntities.has(mapping.entity_type)
);
}
// Par défaut, retourner tous les mappings
return mappings;
};
export const EntityMappingTable = ({
mappings,
selectedCategory = "pii_business",
}: EntityMappingTableProps) => {
// Filtrer les mappings selon la catégorie sélectionnée
const filteredMappings = filterMappingsByCategory(mappings, selectedCategory);
if (!mappings || mappings.length === 0) { if (!mappings || mappings.length === 0) {
return ( return (
<Card className="mt-8"> <Card className="mt-8">
@@ -32,23 +158,60 @@ export const EntityMappingTable = ({ mappings }: EntityMappingTableProps) => {
); );
} }
// Créer un compteur pour chaque type d'entité if (filteredMappings.length === 0) {
const categoryNames = {
pii: "PII (Données Personnelles)",
business: "Business (Données Métier)",
pii_business: "PII + Business",
};
return (
<Card className="mt-8">
<CardHeader>
<CardTitle className="text-lg font-medium text-[#092727]">
Entités détectées -{" "}
{categoryNames[selectedCategory as keyof typeof categoryNames] ||
"Toutes"}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-500 text-center py-8">
Aucune entité de type &quot;
{categoryNames[selectedCategory as keyof typeof categoryNames] ||
"sélectionné"}
&quot; détectée dans le document.
</p>
</CardContent>
</Card>
);
}
// Créer un compteur pour chaque type d'entité (sur les mappings filtrés)
const entityCounts: { [key: string]: number } = {}; const entityCounts: { [key: string]: number } = {};
const mappingsWithNumbers = mappings.map((mapping) => { const mappingsWithNumbers = filteredMappings.map((mapping) => {
const entityType = mapping.entity_type; const entityType = mapping.entity_type;
entityCounts[entityType] = (entityCounts[entityType] || 0) + 1; entityCounts[entityType] = (entityCounts[entityType] || 0) + 1;
return { return {
...mapping, ...mapping,
entityNumber: entityCounts[entityType], entityNumber: entityCounts[entityType],
displayName: mapping.displayName || mapping.replacementValue || `[${entityType}]`, displayName: mapping.entity_type,
}; };
}); });
const categoryNames = {
pii: "PII (Données Personnelles)",
business: "Business (Données Métier)",
pii_business: "PII + Business",
};
return ( return (
<Card className="mt-8"> <Card className="mt-8">
<CardHeader> <CardHeader>
<CardTitle className="text-lg font-medium text-[#092727]"> <CardTitle className="text-lg font-medium text-[#092727]">
Entités détectées ({mappings.length}) Entités détectées -{" "}
{categoryNames[selectedCategory as keyof typeof categoryNames] ||
"Toutes"}{" "}
({filteredMappings.length}/{mappings.length})
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
+254 -24
View File
@@ -9,19 +9,20 @@ 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)
interface FileUploadComponentProps { interface FileUploadComponentProps {
uploadedFile: File | null; uploadedFile: File | null;
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void; handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
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;
@@ -29,9 +30,11 @@ interface FileUploadComponentProps {
outputText?: string; outputText?: string;
copyToClipboard?: () => void; copyToClipboard?: () => void;
downloadText?: () => void; downloadText?: () => void;
isExampleLoaded?: boolean;
setIsExampleLoaded?: (loaded: boolean) => void; setIsExampleLoaded?: (loaded: boolean) => void;
entityMappings?: EntityMapping[]; entityMappings?: EntityMapping[];
onMappingsUpdate?: (mappings: EntityMapping[]) => void;
selectedCategory?: string;
setSelectedCategory?: (category: string) => void;
} }
export const FileUploadComponent = ({ export const FileUploadComponent = ({
@@ -50,8 +53,13 @@ export const FileUploadComponent = ({
downloadText, downloadText,
setIsExampleLoaded, setIsExampleLoaded,
entityMappings, entityMappings,
onMappingsUpdate,
selectedCategory = "pii",
setSelectedCategory,
}: FileUploadComponentProps) => { }: FileUploadComponentProps) => {
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
// Remove the duplicate local state declarations:
// 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 +181,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 +191,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 +223,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 +490,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&apos;anonymisation
</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory?.(e.target.value)}
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 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 +651,14 @@ 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> <option value="business">🏢 Business (Données Métier)</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 +674,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={
+1 -2
View File
@@ -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>
+66 -36
View File
@@ -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> = ({
@@ -27,11 +28,10 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
onRemoveMapping, onRemoveMapping,
}) => { }) => {
const [selectedWords, setSelectedWords] = useState<Set<number>>(new Set()); const [selectedWords, setSelectedWords] = useState<Set<number>>(new Set());
const [hoveredWord, setHoveredWord] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { words } = useTextParsing(text, entityMappings); const { words } = useTextParsing(text, entityMappings);
const { getCurrentColor } = useColorMapping(entityMappings); // CORRECTION: Passer entityMappings const { getCurrentColor } = useColorMapping(entityMappings);
const { const {
contextMenu, contextMenu,
showContextMenu, showContextMenu,
@@ -41,7 +41,7 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
getExistingLabels, getExistingLabels,
} = useContextMenu({ } = useContextMenu({
entityMappings, entityMappings,
words, // NOUVEAU: passer les mots words,
onUpdateMapping, onUpdateMapping,
onRemoveMapping, onRemoveMapping,
getCurrentColor, getCurrentColor,
@@ -49,12 +49,10 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
}); });
const handleWordClick = useCallback( const handleWordClick = useCallback(
(index: number, event: React.MouseEvent) => { (index: number | null, event: React.MouseEvent) => {
event.preventDefault(); if (index !== null) {
event.stopPropagation(); event.preventDefault();
setSelectedWords(prev => {
if (event.ctrlKey || event.metaKey) {
setSelectedWords((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);
if (newSet.has(index)) { if (newSet.has(index)) {
newSet.delete(index); newSet.delete(index);
@@ -63,55 +61,87 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
} }
return newSet; return newSet;
}); });
} else {
setSelectedWords(new Set([index]));
} }
}, },
[] []
); );
const handleContextMenu = useCallback( const handleContainerContextMenu = useCallback(
(event: React.MouseEvent) => { (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
if (selectedWords.size === 0) return;
// Priorité à la sélection de texte libre
const selection = window.getSelection();
if (selection && selection.toString().trim()) {
const selectedText = selection.toString().trim();
showContextMenu({
x: event.clientX,
y: event.clientY,
selectedText,
wordIndices: [], // Sélection libre, pas d'indices de mots
});
return;
}
// Fallback sur la sélection par mots si pas de sélection libre
if (selectedWords.size > 0) {
const selectedText = Array.from(selectedWords)
.sort((a, b) => a - b)
.map((index) => {
const word = words[index];
return word?.isEntity ? word.text : word?.text;
})
.filter(Boolean)
.join(" ");
const selectedText = Array.from(selectedWords) showContextMenu({
.map((index) => { x: event.clientX,
const word = words[index]; y: event.clientY,
return word?.isEntity ? word.text : word?.text; selectedText,
}) wordIndices: Array.from(selectedWords),
.filter(Boolean) });
.join(" "); }
showContextMenu({
x: event.clientX,
y: event.clientY,
selectedText,
wordIndices: Array.from(selectedWords),
});
}, },
[selectedWords, words, showContextMenu] [selectedWords, words, showContextMenu]
); );
const handleClearSelection = useCallback(() => {
setSelectedWords(new Set());
// Effacer la sélection de texte native
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
}, []);
return ( return (
<div ref={containerRef} className="relative"> <div ref={containerRef} className="relative">
<InstructionsPanel /> <div className="mb-2">
<button
onClick={handleClearSelection}
className="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded text-sm"
>
Effacer la sélection
</button>
{selectedWords.size > 0 && (
<span className="ml-2 text-sm text-gray-600">
{selectedWords.size} mot(s) sélectionné(s)
</span>
)}
</div>
<TextDisplay <TextDisplay
words={words} words={words}
text={text} text={text}
selectedWords={selectedWords} selectedWords={selectedWords}
hoveredWord={hoveredWord} onContextMenu={handleContainerContextMenu}
onWordClick={handleWordClick} onWordClick={handleWordClick}
onContextMenu={handleContextMenu}
onWordHover={setHoveredWord}
/> />
{contextMenu.visible && ( {contextMenu.visible && (
<ContextMenu <ContextMenu
contextMenu={contextMenu} contextMenu={contextMenu}
existingLabels={getExistingLabels()} existingLabels={getExistingLabels()}
// entityMappings={entityMappings} // SUPPRIMER cette ligne
onApplyLabel={applyLabel} onApplyLabel={applyLabel}
onApplyColor={applyColorDirectly} onApplyColor={applyColorDirectly}
onRemoveLabel={removeLabel} onRemoveLabel={removeLabel}
+10 -10
View File
@@ -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}
/> />
+1 -1
View File
@@ -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 :**
+16 -29
View File
@@ -6,44 +6,34 @@ interface TextDisplayProps {
words: Word[]; words: Word[];
text: string; text: string;
selectedWords: Set<number>; selectedWords: Set<number>;
hoveredWord: number | null;
onWordClick: (index: number, event: React.MouseEvent) => void;
onContextMenu: (event: React.MouseEvent) => void; onContextMenu: (event: React.MouseEvent) => void;
onWordHover: (index: number | null) => void; onWordClick: (index: number | null, event: React.MouseEvent) => void;
} }
export const TextDisplay: React.FC<TextDisplayProps> = ({ export const TextDisplay: React.FC<TextDisplayProps> = ({
words, words,
text, text,
selectedWords, selectedWords,
hoveredWord,
onWordClick,
onContextMenu, onContextMenu,
onWordHover, onWordClick,
}) => { }) => {
const renderWord = (word: Word, index: number) => { const renderWord = (word: Word, index: number) => {
const isSelected = selectedWords.has(index); const isSelected = selectedWords.has(index);
const isHovered = hoveredWord === index;
let className = let className = "inline-block transition-all duration-200 rounded-sm cursor-pointer select-text ";
"inline-block cursor-pointer transition-all duration-200 px-1 py-0.5 rounded-sm ";
let backgroundColor = "transparent"; let backgroundColor = "transparent";
if (word.isEntity) { if (word.isEntity) {
// Couleur personnalisée ou générée - Niveau 200
if (word.mapping?.customColor) { if (word.mapping?.customColor) {
backgroundColor = word.mapping.customColor; backgroundColor = word.mapping.customColor;
} else if (word.mapping?.displayName) { } else if (word.mapping?.displayName) {
// Utiliser generateColorFromName pour la cohérence
backgroundColor = generateColorFromName(word.mapping.displayName).value; backgroundColor = generateColorFromName(word.mapping.displayName).value;
} else if (word.entityType) { } else if (word.entityType) {
backgroundColor = generateColorFromName(word.entityType).value; backgroundColor = generateColorFromName(word.entityType).value;
} else { } else {
// Couleur par défaut si aucune information disponible
backgroundColor = generateColorFromName("default").value; backgroundColor = generateColorFromName("default").value;
} }
// Utiliser la classe CSS appropriée
if (word.mapping?.displayName) { if (word.mapping?.displayName) {
const colorClass = generateColorFromName(word.mapping.displayName); const colorClass = generateColorFromName(word.mapping.displayName);
className += `${colorClass.bgClass} ${colorClass.textClass} border `; className += `${colorClass.bgClass} ${colorClass.textClass} border `;
@@ -53,42 +43,39 @@ export const TextDisplay: React.FC<TextDisplayProps> = ({
} }
} }
// Gestion du survol et sélection - Couleurs claires
if (isSelected) { if (isSelected) {
className += "ring-2 ring-blue-400 "; className += "ring-2 ring-gray-400 bg-gray-100 ";
} else if (isHovered) { } else {
if (!word.isEntity) { className += "hover:bg-yellow-100 ";
className += "bg-gray-200 ";
backgroundColor = "#E5E7EB"; // gray-200
}
} }
className += "brightness-95 "; className += "brightness-95 ";
return ( return (
<span <span
key={index} key={index}
data-word-index={index}
className={className} className={className}
style={{ style={{
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
}} }}
onMouseEnter={() => onWordHover(index)} onClick={(event) => onWordClick(index, event)}
onMouseLeave={() => onWordHover(null)}
onClick={(e) => onWordClick(index, e)}
onContextMenu={onContextMenu}
title={ title={
word.isEntity word.isEntity
? `Entité: ${word.entityType} (Original: ${word.text})` ? `Entité: ${word.entityType} (Original: ${word.text})`
: "Cliquez pour sélectionner" : "Sélectionnez librement le texte ou cliquez sur les mots"
} }
> >
{word.displayText}{" "} {word.displayText}
</span> </span>
); );
}; };
return ( return (
<div className="p-4 bg-white border border-gray-200 rounded-lg min-h-[300px] leading-relaxed text-sm"> <div
<div className="whitespace-pre-wrap"> className="p-4 bg-white border border-gray-200 rounded-lg min-h-[300px] leading-relaxed text-sm select-text"
onContextMenu={onContextMenu}
>
<div className="whitespace-pre-wrap select-text">
{words.map((word, index) => { {words.map((word, index) => {
const nextWord = words[index + 1]; const nextWord = words[index + 1];
const spaceBetween = nextWord const spaceBetween = nextWord
@@ -98,7 +85,7 @@ export const TextDisplay: React.FC<TextDisplayProps> = ({
return ( return (
<React.Fragment key={index}> <React.Fragment key={index}>
{renderWord(word, index)} {renderWord(word, index)}
<span>{spaceBetween}</span> <span className="select-text">{spaceBetween}</span>
</React.Fragment> </React.Fragment>
); );
})} })}
+3 -3
View File
@@ -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]
); );
+152 -34
View File
@@ -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) {
+15 -20
View File
@@ -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'
}); });
+24 -30
View File
@@ -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,11 +15,14 @@ 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("");
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);
const [entityMappings, setEntityMappings] = useState<EntityMapping[]>([]); const [entityMappings, setEntityMappings] = useState<EntityMapping[]>([]);
const [isExampleLoaded, setIsExampleLoaded] = useState(false); // Remove this unused state variable:
// const [isExampleLoaded, setIsExampleLoaded] = useState(false);
const [selectedCategory, setSelectedCategory] = useState("pii");
const progressSteps = ["Téléversement", "Prévisualisation", "Anonymisation"]; const progressSteps = ["Téléversement", "Prévisualisation", "Anonymisation"];
@@ -37,7 +40,8 @@ export default function Home() {
setError(null); setError(null);
setIsLoadingFile(false); setIsLoadingFile(false);
setEntityMappings([]); setEntityMappings([]);
setIsExampleLoaded(false); // Remove this line: setIsExampleLoaded(false);
setSelectedCategory("pii");
}; };
// Fonction pour mettre à jour les mappings depuis l'éditeur interactif // Fonction pour mettre à jour les mappings depuis l'éditeur interactif
@@ -60,17 +64,21 @@ 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,
sourceText, // Ajouter le texte source
}); });
// Fonction wrapper pour appeler anonymizeData avec les bonnes données // Fonction wrapper pour appeler anonymizeData avec les bonnes données
const handleAnonymize = () => { // Remove unused function or update the onAnonymize prop
anonymizeData({ file: uploadedFile, text: sourceText }); // const handleAnonymize = (category?: string) => {
}; // anonymizeData({ file: uploadedFile, text: sourceText, category });
// };
return ( return (
<div className="min-h-screen w-full overflow-hidden"> <div className="min-h-screen w-full overflow-hidden">
@@ -88,45 +96,31 @@ export default function Home() {
sourceText={sourceText} sourceText={sourceText}
setSourceText={setSourceText} setSourceText={setSourceText}
setUploadedFile={setUploadedFile} setUploadedFile={setUploadedFile}
onAnonymize={handleAnonymize} onAnonymize={(category: string) => anonymizeData({ file: uploadedFile, text: sourceText, category })}
isProcessing={isProcessing} isProcessing={isProcessing}
canAnonymize={ canAnonymize={!!sourceText.trim()}
uploadedFile !== null ||
Boolean(sourceText && sourceText.trim())
}
isLoadingFile={isLoadingFile} isLoadingFile={isLoadingFile}
onRestart={handleRestart} onRestart={handleRestart}
outputText={outputText} outputText={outputText}
copyToClipboard={copyToClipboard} copyToClipboard={copyToClipboard}
downloadText={downloadText} downloadText={downloadText}
isExampleLoaded={isExampleLoaded} // Remove this line: setIsExampleLoaded={setIsExampleLoaded}
setIsExampleLoaded={setIsExampleLoaded}
entityMappings={entityMappings} entityMappings={entityMappings}
onMappingsUpdate={handleMappingsUpdate}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
/> />
</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">
<div className="p-1 sm:p-3"> <div className="p-1 sm:p-3">
<EntityMappingTable mappings={entityMappings} /> <EntityMappingTable
mappings={entityMappings}
selectedCategory={selectedCategory}
/>
</div> </div>
</div> </div>
)} )}
+77 -7
View File
@@ -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,36 @@ 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) {
// Priorité : displayName modifié > displayName original > entity_type
replacement = mapping.displayName || `[${mapping.entity_type}]`;
// Si displayName ne contient pas de crochets, les ajouter
if (mapping.displayName && !mapping.displayName.startsWith('[')) {
replacement = `[${mapping.displayName}]`;
}
}
result += replacement;
lastIndex = mapping.end; lastIndex = mapping.end;
} }
+2 -2
View File
@@ -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}
+2
View File
@@ -7,6 +7,8 @@ import { type Configuration } from "webpack";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
// ... autres configurations // ... autres configurations
turbopack: {},
webpack: (config: Configuration, { isServer }: { isServer: boolean }) => { webpack: (config: Configuration, { isServer }: { isServer: boolean }) => {
// ---- DÉBUT DE LA CORRECTION ---- // ---- DÉBUT DE LA CORRECTION ----
+327 -398
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -2,6 +2,9 @@
"name": "anonyme", "name": "anonyme",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"engines": {
"node": ">=20.9.0"
},
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
@@ -17,10 +20,10 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^3.0.1", "jspdf": "^4.0.0",
"lucide-react": "^0.514.0", "lucide-react": "^0.514.0",
"mammoth": "^1.9.1", "mammoth": "^1.9.1",
"next": "15.3.3", "next": "^16.1.6",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"react": "^19.0.0", "react": "^19.0.0",
+19 -5
View File
@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -19,9 +23,19 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
"./*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }