clean presidio
This commit is contained in:
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal 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:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,40 @@
|
|||||||
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; // Nouveau paramètre pour le texte déjà anonymisé par Presidio
|
anonymizedText?: string;
|
||||||
|
sourceText?: string; // Ajouter le texte source
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDownloadActions = ({
|
export const useDownloadActions = ({
|
||||||
outputText,
|
outputText,
|
||||||
anonymizedText, // Texte déjà anonymisé par Presidio
|
entityMappings,
|
||||||
|
anonymizedText,
|
||||||
|
sourceText, // Nouveau paramètre
|
||||||
}: DownloadActionsProps) => {
|
}: DownloadActionsProps) => {
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
// Toujours utiliser le texte anonymisé de Presidio
|
// Utiliser les mappings mis à jour pour générer le texte final
|
||||||
const textToCopy = anonymizedText || outputText;
|
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);
|
navigator.clipboard.writeText(textToCopy);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadText = () => {
|
const downloadText = () => {
|
||||||
// Utiliser le texte anonymisé de Presidio si disponible, sinon fallback sur outputText
|
// Utiliser les mappings mis à jour pour générer le texte final
|
||||||
const textToDownload = anonymizedText || outputText;
|
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 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");
|
||||||
|
|||||||
@@ -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,9 +158,37 @@ 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 "
|
||||||
|
{categoryNames[selectedCategory as keyof typeof categoryNames] ||
|
||||||
|
"sélectionné"}
|
||||||
|
" 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 {
|
||||||
@@ -44,11 +198,20 @@ export const EntityMappingTable = ({ mappings }: EntityMappingTableProps) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import { EntityMapping } from "../config/entityLabels"; // Importer l'interface
|
|||||||
|
|
||||||
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?: (category?: string) => void;
|
onAnonymize?: (category: string) => void;
|
||||||
isProcessing?: boolean;
|
isProcessing?: boolean;
|
||||||
canAnonymize?: boolean;
|
canAnonymize?: boolean;
|
||||||
isLoadingFile?: boolean;
|
isLoadingFile?: boolean;
|
||||||
@@ -30,10 +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;
|
onMappingsUpdate?: (mappings: EntityMapping[]) => void;
|
||||||
|
selectedCategory?: string;
|
||||||
|
setSelectedCategory?: (category: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadComponent = ({
|
export const FileUploadComponent = ({
|
||||||
@@ -53,9 +54,12 @@ export const FileUploadComponent = ({
|
|||||||
setIsExampleLoaded,
|
setIsExampleLoaded,
|
||||||
entityMappings,
|
entityMappings,
|
||||||
onMappingsUpdate,
|
onMappingsUpdate,
|
||||||
|
selectedCategory = "pii",
|
||||||
|
setSelectedCategory,
|
||||||
}: FileUploadComponentProps) => {
|
}: FileUploadComponentProps) => {
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [selectedCategory, setSelectedCategory] = useState("pii");
|
// 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) => {
|
||||||
@@ -494,8 +498,8 @@ export const FileUploadComponent = ({
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
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"
|
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="pii">🔒 PII (Données Personnelles)</option>
|
||||||
<option value="business">🏢 Business (Données Métier)</option>
|
<option value="business">🏢 Business (Données Métier)</option>
|
||||||
@@ -649,13 +653,11 @@ export const FileUploadComponent = ({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
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"
|
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="pii">🔒 PII (Données Personnelles)</option>
|
||||||
<option value="business">
|
<option value="business">🏢 Business (Données Métier)</option>
|
||||||
🏢 Business (Données Métier)
|
|
||||||
</option>
|
|
||||||
<option value="pii_business">🔒🏢 PII + Business </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">
|
||||||
|
|||||||
@@ -28,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,
|
||||||
@@ -42,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,
|
||||||
@@ -50,15 +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 => {
|
||||||
// Support multi-sélection avec Ctrl, Cmd et Shift
|
|
||||||
const isMultiSelect = event.ctrlKey || event.metaKey || event.shiftKey;
|
|
||||||
|
|
||||||
if (isMultiSelect) {
|
|
||||||
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);
|
||||||
@@ -67,53 +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">
|
||||||
|
<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}
|
||||||
|
|||||||
@@ -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 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,55 +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,
|
||||||
userSelect: "none",
|
|
||||||
WebkitUserSelect: "none",
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => onWordHover(index)}
|
|
||||||
onMouseLeave={() => onWordHover(null)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
onWordClick(index, e);
|
|
||||||
}}
|
|
||||||
onContextMenu={onContextMenu}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
onClick={(event) => onWordClick(index, event)}
|
||||||
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
|
||||||
@@ -111,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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
36
app/page.tsx
36
app/page.tsx
@@ -15,12 +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(""); // Nouveau state pour le texte anonymisé de Presidio
|
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"];
|
||||||
|
|
||||||
@@ -38,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
|
||||||
@@ -67,13 +70,15 @@ export default function Home() {
|
|||||||
const { copyToClipboard, downloadText } = useDownloadActions({
|
const { copyToClipboard, downloadText } = useDownloadActions({
|
||||||
outputText,
|
outputText,
|
||||||
entityMappings,
|
entityMappings,
|
||||||
anonymizedText, // Passer le texte anonymisé de Presidio
|
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 = (category?: string) => {
|
// Remove unused function or update the onAnonymize prop
|
||||||
anonymizeData({ file: uploadedFile, text: sourceText, category });
|
// 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">
|
||||||
@@ -91,21 +96,19 @@ 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}
|
onMappingsUpdate={handleMappingsUpdate}
|
||||||
|
selectedCategory={selectedCategory}
|
||||||
|
setSelectedCategory={setSelectedCategory}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +117,10 @@ export default function Home() {
|
|||||||
{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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -82,7 +82,13 @@ export const generateAnonymizedText = (
|
|||||||
let replacement = mapping.replacementValue;
|
let replacement = mapping.replacementValue;
|
||||||
|
|
||||||
if (!replacement) {
|
if (!replacement) {
|
||||||
|
// Priorité : displayName modifié > displayName original > entity_type
|
||||||
replacement = mapping.displayName || `[${mapping.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;
|
result += replacement;
|
||||||
|
|||||||
725
package-lock.json
generated
725
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user