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 { generateAnonymizedText } from "@/app/utils/generateAnonymizedText";
|
||||
|
||||
interface DownloadActionsProps {
|
||||
outputText: string;
|
||||
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 = ({
|
||||
outputText,
|
||||
anonymizedText, // Texte déjà anonymisé par Presidio
|
||||
entityMappings,
|
||||
anonymizedText,
|
||||
sourceText, // Nouveau paramètre
|
||||
}: DownloadActionsProps) => {
|
||||
const copyToClipboard = () => {
|
||||
// Toujours utiliser le texte anonymisé de Presidio
|
||||
const textToCopy = anonymizedText || outputText;
|
||||
// Utiliser les mappings mis à jour pour générer le texte final
|
||||
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 = () => {
|
||||
// Utiliser le texte anonymisé de Presidio si disponible, sinon fallback sur outputText
|
||||
const textToDownload = anonymizedText || outputText;
|
||||
// Utiliser les mappings mis à jour pour générer le texte final
|
||||
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 a = document.createElement("a");
|
||||
|
||||
@@ -12,9 +12,135 @@ import { EntityMapping } from "../config/entityLabels";
|
||||
|
||||
interface EntityMappingTableProps {
|
||||
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) {
|
||||
return (
|
||||
<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 mappingsWithNumbers = mappings.map((mapping) => {
|
||||
const mappingsWithNumbers = filteredMappings.map((mapping) => {
|
||||
const entityType = mapping.entity_type;
|
||||
entityCounts[entityType] = (entityCounts[entityType] || 0) + 1;
|
||||
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 (
|
||||
<Card className="mt-8">
|
||||
<CardHeader>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -18,11 +18,11 @@ import { EntityMapping } from "../config/entityLabels"; // Importer l'interface
|
||||
|
||||
interface FileUploadComponentProps {
|
||||
uploadedFile: File | null;
|
||||
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
sourceText: string;
|
||||
setSourceText: (text: string) => void;
|
||||
setUploadedFile: (file: File | null) => void;
|
||||
onAnonymize?: (category?: string) => void;
|
||||
onAnonymize?: (category: string) => void;
|
||||
isProcessing?: boolean;
|
||||
canAnonymize?: boolean;
|
||||
isLoadingFile?: boolean;
|
||||
@@ -30,10 +30,11 @@ interface FileUploadComponentProps {
|
||||
outputText?: string;
|
||||
copyToClipboard?: () => void;
|
||||
downloadText?: () => void;
|
||||
isExampleLoaded?: boolean;
|
||||
setIsExampleLoaded?: (loaded: boolean) => void;
|
||||
entityMappings?: EntityMapping[];
|
||||
onMappingsUpdate?: (mappings: EntityMapping[]) => void;
|
||||
selectedCategory?: string;
|
||||
setSelectedCategory?: (category: string) => void;
|
||||
}
|
||||
|
||||
export const FileUploadComponent = ({
|
||||
@@ -53,9 +54,12 @@ export const FileUploadComponent = ({
|
||||
setIsExampleLoaded,
|
||||
entityMappings,
|
||||
onMappingsUpdate,
|
||||
selectedCategory = "pii",
|
||||
setSelectedCategory,
|
||||
}: FileUploadComponentProps) => {
|
||||
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
|
||||
const isValidFileType = (file: File) => {
|
||||
@@ -494,8 +498,8 @@ export const FileUploadComponent = ({
|
||||
</label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#f7ab6e] focus:border-[#f7ab6e] bg-white"
|
||||
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>
|
||||
@@ -649,13 +653,11 @@ export const FileUploadComponent = ({
|
||||
<div className="relative">
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
<option value="pii_business">🔒🏢 PII + Business </option>
|
||||
</select>
|
||||
<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,
|
||||
}) => {
|
||||
const [selectedWords, setSelectedWords] = useState<Set<number>>(new Set());
|
||||
const [hoveredWord, setHoveredWord] = useState<number | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { words } = useTextParsing(text, entityMappings);
|
||||
const { getCurrentColor } = useColorMapping(entityMappings); // CORRECTION: Passer entityMappings
|
||||
const { getCurrentColor } = useColorMapping(entityMappings);
|
||||
const {
|
||||
contextMenu,
|
||||
showContextMenu,
|
||||
@@ -42,7 +41,7 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
||||
getExistingLabels,
|
||||
} = useContextMenu({
|
||||
entityMappings,
|
||||
words, // NOUVEAU: passer les mots
|
||||
words,
|
||||
onUpdateMapping,
|
||||
onRemoveMapping,
|
||||
getCurrentColor,
|
||||
@@ -50,15 +49,10 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
||||
});
|
||||
|
||||
const handleWordClick = useCallback(
|
||||
(index: number, event: React.MouseEvent) => {
|
||||
(index: number | null, event: React.MouseEvent) => {
|
||||
if (index !== null) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Support multi-sélection avec Ctrl, Cmd et Shift
|
||||
const isMultiSelect = event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
|
||||
if (isMultiSelect) {
|
||||
setSelectedWords((prev) => {
|
||||
setSelectedWords(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index);
|
||||
@@ -67,19 +61,32 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
} else {
|
||||
setSelectedWords(new Set([index]));
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
const handleContainerContextMenu = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
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;
|
||||
@@ -93,27 +100,48 @@ export const InteractiveTextEditor: React.FC<InteractiveTextEditorProps> = ({
|
||||
selectedText,
|
||||
wordIndices: Array.from(selectedWords),
|
||||
});
|
||||
}
|
||||
},
|
||||
[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 (
|
||||
<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
|
||||
words={words}
|
||||
text={text}
|
||||
selectedWords={selectedWords}
|
||||
hoveredWord={hoveredWord}
|
||||
onContextMenu={handleContainerContextMenu}
|
||||
onWordClick={handleWordClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onWordHover={setHoveredWord}
|
||||
/>
|
||||
|
||||
{contextMenu.visible && (
|
||||
<ContextMenu
|
||||
contextMenu={contextMenu}
|
||||
existingLabels={getExistingLabels()}
|
||||
// entityMappings={entityMappings} // SUPPRIMER cette ligne
|
||||
onApplyLabel={applyLabel}
|
||||
onApplyColor={applyColorDirectly}
|
||||
onRemoveLabel={removeLabel}
|
||||
|
||||
@@ -6,44 +6,34 @@ interface TextDisplayProps {
|
||||
words: Word[];
|
||||
text: string;
|
||||
selectedWords: Set<number>;
|
||||
hoveredWord: number | null;
|
||||
onWordClick: (index: number, 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> = ({
|
||||
words,
|
||||
text,
|
||||
selectedWords,
|
||||
hoveredWord,
|
||||
onWordClick,
|
||||
onContextMenu,
|
||||
onWordHover,
|
||||
onWordClick,
|
||||
}) => {
|
||||
const renderWord = (word: Word, index: number) => {
|
||||
const isSelected = selectedWords.has(index);
|
||||
const isHovered = hoveredWord === index;
|
||||
|
||||
let className =
|
||||
"inline-block cursor-pointer transition-all duration-200 rounded-sm ";
|
||||
let className = "inline-block transition-all duration-200 rounded-sm cursor-pointer select-text ";
|
||||
let backgroundColor = "transparent";
|
||||
|
||||
if (word.isEntity) {
|
||||
// Couleur personnalisée ou générée - Niveau 200
|
||||
if (word.mapping?.customColor) {
|
||||
backgroundColor = word.mapping.customColor;
|
||||
} else if (word.mapping?.displayName) {
|
||||
// Utiliser generateColorFromName pour la cohérence
|
||||
backgroundColor = generateColorFromName(word.mapping.displayName).value;
|
||||
} else if (word.entityType) {
|
||||
backgroundColor = generateColorFromName(word.entityType).value;
|
||||
} else {
|
||||
// Couleur par défaut si aucune information disponible
|
||||
backgroundColor = generateColorFromName("default").value;
|
||||
}
|
||||
|
||||
// Utiliser la classe CSS appropriée
|
||||
if (word.mapping?.displayName) {
|
||||
const colorClass = generateColorFromName(word.mapping.displayName);
|
||||
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) {
|
||||
className += "ring-2 ring-blue-400 ";
|
||||
} else if (isHovered) {
|
||||
if (!word.isEntity) {
|
||||
className += "bg-gray-200 ";
|
||||
backgroundColor = "#E5E7EB"; // gray-200
|
||||
}
|
||||
className += "ring-2 ring-gray-400 bg-gray-100 ";
|
||||
} else {
|
||||
className += "hover:bg-yellow-100 ";
|
||||
}
|
||||
|
||||
className += "brightness-95 ";
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
data-word-index={index}
|
||||
className={className}
|
||||
style={{
|
||||
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={
|
||||
word.isEntity
|
||||
? `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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white border border-gray-200 rounded-lg min-h-[300px] leading-relaxed text-sm">
|
||||
<div className="whitespace-pre-wrap">
|
||||
<div
|
||||
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) => {
|
||||
const nextWord = words[index + 1];
|
||||
const spaceBetween = nextWord
|
||||
@@ -111,7 +85,7 @@ export const TextDisplay: React.FC<TextDisplayProps> = ({
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{renderWord(word, index)}
|
||||
<span>{spaceBetween}</span>
|
||||
<span className="select-text">{spaceBetween}</span>
|
||||
</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() {
|
||||
const [sourceText, setSourceText] = 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 [error, setError] = useState<string | null>(null);
|
||||
const [isLoadingFile, setIsLoadingFile] = useState(false);
|
||||
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"];
|
||||
|
||||
@@ -38,7 +40,8 @@ export default function Home() {
|
||||
setError(null);
|
||||
setIsLoadingFile(false);
|
||||
setEntityMappings([]);
|
||||
setIsExampleLoaded(false);
|
||||
// Remove this line: setIsExampleLoaded(false);
|
||||
setSelectedCategory("pii");
|
||||
};
|
||||
|
||||
// Fonction pour mettre à jour les mappings depuis l'éditeur interactif
|
||||
@@ -67,13 +70,15 @@ export default function Home() {
|
||||
const { copyToClipboard, downloadText } = useDownloadActions({
|
||||
outputText,
|
||||
entityMappings,
|
||||
anonymizedText, // Passer le texte anonymisé de Presidio
|
||||
anonymizedText,
|
||||
sourceText, // Ajouter le texte source
|
||||
});
|
||||
|
||||
// Fonction wrapper pour appeler anonymizeData avec les bonnes données
|
||||
const handleAnonymize = (category?: string) => {
|
||||
anonymizeData({ file: uploadedFile, text: sourceText, category });
|
||||
};
|
||||
// Remove unused function or update the onAnonymize prop
|
||||
// const handleAnonymize = (category?: string) => {
|
||||
// anonymizeData({ file: uploadedFile, text: sourceText, category });
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full overflow-hidden">
|
||||
@@ -91,21 +96,19 @@ export default function Home() {
|
||||
sourceText={sourceText}
|
||||
setSourceText={setSourceText}
|
||||
setUploadedFile={setUploadedFile}
|
||||
onAnonymize={handleAnonymize}
|
||||
onAnonymize={(category: string) => anonymizeData({ file: uploadedFile, text: sourceText, category })}
|
||||
isProcessing={isProcessing}
|
||||
canAnonymize={
|
||||
uploadedFile !== null ||
|
||||
Boolean(sourceText && sourceText.trim())
|
||||
}
|
||||
canAnonymize={!!sourceText.trim()}
|
||||
isLoadingFile={isLoadingFile}
|
||||
onRestart={handleRestart}
|
||||
outputText={outputText}
|
||||
copyToClipboard={copyToClipboard}
|
||||
downloadText={downloadText}
|
||||
isExampleLoaded={isExampleLoaded}
|
||||
setIsExampleLoaded={setIsExampleLoaded}
|
||||
// Remove this line: setIsExampleLoaded={setIsExampleLoaded}
|
||||
entityMappings={entityMappings}
|
||||
onMappingsUpdate={handleMappingsUpdate}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +117,10 @@ export default function Home() {
|
||||
{outputText && (
|
||||
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
|
||||
<div className="p-1 sm:p-3">
|
||||
<EntityMappingTable mappings={entityMappings} />
|
||||
<EntityMappingTable
|
||||
mappings={entityMappings}
|
||||
selectedCategory={selectedCategory}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -82,7 +82,13 @@ export const generateAnonymizedText = (
|
||||
let replacement = mapping.replacementValue;
|
||||
|
||||
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;
|
||||
|
||||
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",
|
||||
"clsx": "^2.1.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.1",
|
||||
"jspdf": "^4.0.0",
|
||||
"lucide-react": "^0.514.0",
|
||||
"mammoth": "^1.9.1",
|
||||
"next": "15.3.3",
|
||||
"next": "^16.1.6",
|
||||
"patch-package": "^8.0.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -11,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -19,9 +23,19 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user