clean presidio

This commit is contained in:
Biqoz
2026-01-29 00:19:50 +01:00
parent 050474e95b
commit 90b73080e0
11 changed files with 667 additions and 516 deletions

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:*)"
]
}
}

View File

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

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,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 &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 {
@@ -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>

View File

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

View File

@@ -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;
const selectedText = Array.from(selectedWords) // Priorité à la sélection de texte libre
.map((index) => { const selection = window.getSelection();
const word = words[index]; if (selection && selection.toString().trim()) {
return word?.isEntity ? word.text : word?.text; const selectedText = selection.toString().trim();
}) showContextMenu({
.filter(Boolean) x: event.clientX,
.join(" "); y: event.clientY,
selectedText,
wordIndices: [], // Sélection libre, pas d'indices de mots
});
return;
}
showContextMenu({ // Fallback sur la sélection par mots si pas de sélection libre
x: event.clientX, if (selectedWords.size > 0) {
y: event.clientY, const selectedText = Array.from(selectedWords)
selectedText, .sort((a, b) => a - b)
wordIndices: Array.from(selectedWords), .map((index) => {
}); const word = words[index];
return word?.isEntity ? word.text : word?.text;
})
.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}

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 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>
); );
})} })}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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"
]
} }