new interactive
This commit is contained in:
@@ -9,8 +9,9 @@ import {
|
||||
import { SampleTextComponent } from "./SampleTextComponent";
|
||||
import { SupportedDataTypes } from "./SupportedDataTypes";
|
||||
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
|
||||
|
||||
// Supprimer l'interface locale EntityMapping (lignes 15-21)
|
||||
@@ -21,7 +22,7 @@ interface FileUploadComponentProps {
|
||||
sourceText: string;
|
||||
setSourceText: (text: string) => void;
|
||||
setUploadedFile: (file: File | null) => void;
|
||||
onAnonymize?: () => void;
|
||||
onAnonymize?: (category?: string) => void;
|
||||
isProcessing?: boolean;
|
||||
canAnonymize?: boolean;
|
||||
isLoadingFile?: boolean;
|
||||
@@ -32,6 +33,7 @@ interface FileUploadComponentProps {
|
||||
isExampleLoaded?: boolean;
|
||||
setIsExampleLoaded?: (loaded: boolean) => void;
|
||||
entityMappings?: EntityMapping[];
|
||||
onMappingsUpdate?: (mappings: EntityMapping[]) => void;
|
||||
}
|
||||
|
||||
export const FileUploadComponent = ({
|
||||
@@ -50,8 +52,10 @@ export const FileUploadComponent = ({
|
||||
downloadText,
|
||||
setIsExampleLoaded,
|
||||
entityMappings,
|
||||
onMappingsUpdate,
|
||||
}: FileUploadComponentProps) => {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState("pii");
|
||||
|
||||
// Fonction pour valider le type de fichier
|
||||
const isValidFileType = (file: File) => {
|
||||
@@ -173,7 +177,7 @@ export const FileUploadComponent = ({
|
||||
</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-green-50 border-b border-green-200 px-4 sm:px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -183,7 +187,7 @@ export const FileUploadComponent = ({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs sm:text-sm text-green-600">
|
||||
Document anonymisé
|
||||
DOCUMENT ANONYMISÉ MODE INTERACTIF
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,12 +219,208 @@ export const FileUploadComponent = ({
|
||||
</div>
|
||||
<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="text-xs sm:text-sm text-gray-700 whitespace-pre-wrap break-words overflow-wrap-anywhere leading-relaxed">
|
||||
{highlightEntities(
|
||||
sourceText || "Aucun contenu à afficher", // Utiliser sourceText au lieu de outputText
|
||||
entityMappings || [] // Fournir un tableau vide par défaut
|
||||
)}
|
||||
</div>
|
||||
<InteractiveTextEditor
|
||||
text={sourceText}
|
||||
entityMappings={entityMappings || []}
|
||||
onUpdateMapping={(
|
||||
originalValue,
|
||||
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>
|
||||
@@ -286,12 +486,37 @@ export const FileUploadComponent = ({
|
||||
{/* Boutons d'action - Responsive mobile */}
|
||||
{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">
|
||||
{/* Sélecteur de catégorie - NOUVEAU */}
|
||||
{onAnonymize && !outputText && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-xs font-medium text-gray-700 text-center">
|
||||
Catégorie d'anonymisation
|
||||
</label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#f7ab6e] focus:border-[#f7ab6e] bg-white"
|
||||
>
|
||||
<option value="pii">🔒 PII (Données Personnelles)</option>
|
||||
<option value="business">🏢 Business (Données Métier)</option>
|
||||
<option value="pii_business">
|
||||
🔒🏢 PII + Business (Tout)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bouton Anonymiser - seulement si pas encore anonymisé */}
|
||||
{onAnonymize && !outputText && (
|
||||
<button
|
||||
onClick={onAnonymize}
|
||||
disabled={isProcessing}
|
||||
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"
|
||||
onClick={() => onAnonymize?.(selectedCategory)}
|
||||
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"
|
||||
title={
|
||||
sourceText.trim()
|
||||
? "Anonymiser les données"
|
||||
: "Saisissez du texte pour anonymiser"
|
||||
}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
@@ -422,13 +647,16 @@ export const FileUploadComponent = ({
|
||||
Type de données :
|
||||
</label>
|
||||
<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">
|
||||
<option>
|
||||
Informations Personnellement Identifiables (PII)
|
||||
</option>
|
||||
<option disabled style={{ color: "lightgray" }}>
|
||||
PII + Données Business (En développement)
|
||||
<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 </option>
|
||||
</select>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<svg
|
||||
@@ -444,7 +672,7 @@ export const FileUploadComponent = ({
|
||||
|
||||
{/* Bouton Anonymiser */}
|
||||
<button
|
||||
onClick={onAnonymize}
|
||||
onClick={() => onAnonymize?.(selectedCategory)}
|
||||
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"
|
||||
title={
|
||||
|
||||
Reference in New Issue
Block a user