new interactive

This commit is contained in:
Biqoz
2025-09-15 19:05:59 +02:00
parent 130929b756
commit 050474e95b
16 changed files with 746 additions and 330 deletions

View File

@@ -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&apos;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={