version nice

This commit is contained in:
nBiqoz
2025-07-26 21:39:49 +02:00
parent aa19bb82a0
commit 5ba4fdc450
16 changed files with 1462 additions and 517 deletions

View File

@@ -0,0 +1,181 @@
import { CheckCircle } from "lucide-react";
interface AnonymizationInterfaceProps {
isProcessing: boolean;
outputText?: string;
sourceText?: string;
}
export const AnonymizationInterface = ({
isProcessing,
outputText,
sourceText,
}: AnonymizationInterfaceProps) => {
// Fonction pour détecter quels types de données ont été anonymisés
const getAnonymizedDataTypes = () => {
if (!outputText || !sourceText) return new Set();
const anonymizedTypes = new Set<string>();
// Détecter les patterns d'anonymisation dans le texte de sortie
// Noms (Prénoms, Noms de famille, Noms complets)
if (outputText.includes("[Nom1]") || outputText.includes("[Nom")) {
anonymizedTypes.add("Prénoms");
anonymizedTypes.add("Noms de famille");
anonymizedTypes.add("Noms complets");
}
// Emails
if (outputText.includes("[Email1]") || outputText.includes("[Email")) {
anonymizedTypes.add("Adresses e-mail");
}
// Téléphones
if (
outputText.includes("[Téléphone1]") ||
outputText.includes("[Téléphone")
) {
anonymizedTypes.add("Numéros de téléphone");
}
// Adresses
if (outputText.includes("[Adresse1]") || outputText.includes("[Adresse")) {
anonymizedTypes.add("Adresses");
}
// Numéros d'ID / Sécurité sociale
if (
outputText.includes("[NuméroSS1]") ||
outputText.includes("[NuméroSS") ||
outputText.includes("[ID")
) {
anonymizedTypes.add("Numéros d'ID");
}
// Dates
if (
outputText.includes("[Date") ||
/\[\d{2}\/\d{2}\/\d{4}\]/.test(outputText)
) {
anonymizedTypes.add("Dates");
}
// Valeurs monétaires
if (outputText.includes("[Montant") || /\[\d+[€$]\]/.test(outputText)) {
anonymizedTypes.add("Valeurs monétaires");
}
// Noms de domaine
if (outputText.includes("[Domaine") || /\[.*\.com\]/.test(outputText)) {
anonymizedTypes.add("Noms de domaine");
}
// Valeurs numériques
if (
/\[\d+\]/.test(outputText) &&
!outputText.includes("[Téléphone") &&
!outputText.includes("[Montant")
) {
anonymizedTypes.add("Valeurs numériques");
}
// Texte personnalisé (si du texte a été modifié mais pas avec les patterns spécifiques)
if (sourceText !== outputText && anonymizedTypes.size === 0) {
anonymizedTypes.add("Texte personnalisé");
}
return anonymizedTypes;
};
// Structure exacte de SupportedDataTypes (récupérée dynamiquement)
const supportedDataStructure = [
{
items: ["Prénoms", "Numéros de téléphone", "Noms de domaine"],
},
{
items: ["Noms de famille", "Adresses", "Dates"],
},
{
items: ["Noms complets", "Numéros d'ID", "Valeurs numériques"],
},
{
items: ["Adresses e-mail", "Valeurs monétaires", "Texte personnalisé"],
},
];
if (isProcessing) {
return (
<div className="bg-gray-50 border border-gray-200 rounded-xl p-6">
<div className="flex items-center justify-center space-x-3 mb-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-500"></div>
<h4 className="text-sm font-semibold text-gray-700">
Anonymisation en cours...
</h4>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full animate-pulse"></div>
<span className="text-xs text-gray-600">Analyse du contenu</span>
</div>
<div className="flex items-center space-x-2">
<div
className="w-2 h-2 bg-gray-500 rounded-full animate-pulse"
style={{ animationDelay: "0.5s" }}
></div>
<span className="text-xs text-gray-600">
Détection des données sensibles
</span>
</div>
<div className="flex items-center space-x-2">
<div
className="w-2 h-2 bg-gray-500 rounded-full animate-pulse"
style={{ animationDelay: "1s" }}
></div>
<span className="text-xs text-gray-600">
Application de l&apos;anonymisation
</span>
</div>
</div>
</div>
);
}
if (outputText) {
const anonymizedTypes = getAnonymizedDataTypes();
return (
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
<div className="flex items-center space-x-3 mb-4">
<CheckCircle className="h-5 w-5 text-green-600" />
<h4 className="text-sm font-semibold text-green-700">
Anonymisation terminée avec succès
</h4>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
{supportedDataStructure.map((column, columnIndex) => (
<div key={columnIndex} className="flex flex-col space-y-2">
{column.items.map((item, itemIndex) => {
const isAnonymized = anonymizedTypes.has(item);
return (
<span
key={itemIndex}
className={
isAnonymized
? "text-green-700 font-medium"
: "text-gray-400"
}
>
{isAnonymized ? "✓" : "•"} {item}
</span>
);
})}
</div>
))}
</div>
</div>
);
}
return null;
};

View File

@@ -0,0 +1,178 @@
import { useState } from "react";
interface EntityMapping {
originalValue: string;
anonymizedValue: string;
entityType: string;
startIndex: number;
endIndex: number;
}
interface AnonymizationLogicProps {
sourceText: string;
fileContent: string;
uploadedFile: File | null;
setOutputText: (text: string) => void;
setError: (error: string | null) => void;
setEntityMappings: (mappings: EntityMapping[]) => void;
}
export const useAnonymization = ({
sourceText,
fileContent,
uploadedFile,
setOutputText,
setError,
setEntityMappings,
}: AnonymizationLogicProps) => {
const [isProcessing, setIsProcessing] = useState(false);
const anonymizeData = async () => {
const textToProcess = sourceText || fileContent || "";
if (!textToProcess.trim()) {
setError(
"Veuillez saisir du texte à anonymiser ou télécharger un fichier"
);
return;
}
setIsProcessing(true);
setError(null);
setOutputText("");
setEntityMappings([]);
try {
if (
uploadedFile &&
uploadedFile.type === "application/pdf" &&
!fileContent
) {
const formData = new FormData();
formData.append("file", uploadedFile);
const response = await fetch("/api/process-document", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("Erreur lors du traitement du PDF");
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
if (data.anonymizedText) {
setOutputText(data.anonymizedText);
// TODO: Extraire les mappings depuis les résultats Presidio
setIsProcessing(false);
return;
}
}
await new Promise((resolve) => setTimeout(resolve, 1500));
// Simulation des mappings pour le fallback
const mappings: EntityMapping[] = [];
let anonymized = textToProcess;
// Noms
const nameMatches = textToProcess.matchAll(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g);
let nameCounter = 1;
for (const match of nameMatches) {
const replacement = `[Nom${nameCounter}]`;
mappings.push({
originalValue: match[0],
anonymizedValue: replacement,
entityType: "PERSON",
startIndex: match.index!,
endIndex: match.index! + match[0].length
});
anonymized = anonymized.replace(match[0], replacement);
nameCounter++;
}
// Téléphones
const phoneMatches = textToProcess.matchAll(/\b0[1-9](?:[\s.-]?\d{2}){4}\b/g);
let phoneCounter = 1;
for (const match of phoneMatches) {
const replacement = `[Téléphone${phoneCounter}]`;
mappings.push({
originalValue: match[0],
anonymizedValue: replacement,
entityType: "PHONE_NUMBER",
startIndex: match.index!,
endIndex: match.index! + match[0].length
});
anonymized = anonymized.replace(match[0], replacement);
phoneCounter++;
}
// Emails
const emailMatches = textToProcess.matchAll(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g);
let emailCounter = 1;
for (const match of emailMatches) {
const replacement = `[Email${emailCounter}]`;
mappings.push({
originalValue: match[0],
anonymizedValue: replacement,
entityType: "EMAIL_ADDRESS",
startIndex: match.index!,
endIndex: match.index! + match[0].length
});
anonymized = anonymized.replace(match[0], replacement);
emailCounter++;
}
// Adresses
const addressMatches = textToProcess.matchAll(/\b\d{1,3}\s+[a-zA-Z\s]+,\s*\d{5}\s+[a-zA-Z\s]+\b/g);
let addressCounter = 1;
for (const match of addressMatches) {
const replacement = `[Adresse${addressCounter}]`;
mappings.push({
originalValue: match[0],
anonymizedValue: replacement,
entityType: "LOCATION",
startIndex: match.index!,
endIndex: match.index! + match[0].length
});
anonymized = anonymized.replace(match[0], replacement);
addressCounter++;
}
// Numéros de sécurité sociale
const ssnMatches = textToProcess.matchAll(/\b\d\s\d{2}\s\d{2}\s\d{2}\s\d{3}\s\d{3}\s\d{2}\b/g);
let ssnCounter = 1;
for (const match of ssnMatches) {
const replacement = `[NuméroSS${ssnCounter}]`;
mappings.push({
originalValue: match[0],
anonymizedValue: replacement,
entityType: "FR_NIR",
startIndex: match.index!,
endIndex: match.index! + match[0].length
});
anonymized = anonymized.replace(match[0], replacement);
ssnCounter++;
}
setOutputText(anonymized);
setEntityMappings(mappings);
} catch (error) {
console.error("Erreur anonymisation:", error);
setError(
error instanceof Error
? error.message
: "Erreur lors de l'anonymisation"
);
} finally {
setIsProcessing(false);
}
};
return { anonymizeData, isProcessing };
};

View File

@@ -0,0 +1,48 @@
import React from "react";
import { FileText } from "lucide-react";
interface DocumentPreviewProps {
uploadedFile: File | null;
fileContent: string;
sourceText: string;
}
export const DocumentPreview: React.FC<DocumentPreviewProps> = ({
uploadedFile,
fileContent,
sourceText,
}) => {
if (!uploadedFile && (!sourceText || !sourceText.trim())) {
return null;
}
return (
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
<div className="bg-orange-50 border-b border-orange-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<FileText className="h-5 w-5 text-orange-600" />
</div>
<div>
{uploadedFile && (
<p className="text-sm text-orange-600">
{uploadedFile.name} {(uploadedFile.size / 1024).toFixed(1)}{" "}
KB
</p>
)}
</div>
</div>
</div>
</div>
<div className="p-6">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 max-h-64 overflow-y-auto">
<pre className="text-sm text-gray-700 whitespace-pre-wrap font-mono">
{sourceText || fileContent || "Aucun contenu à afficher"}
</pre>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,23 @@
interface DownloadActionsProps {
outputText: string;
}
export const useDownloadActions = ({ outputText }: DownloadActionsProps) => {
const copyToClipboard = () => {
navigator.clipboard.writeText(outputText);
};
const downloadText = () => {
const blob = new Blob([outputText], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "texte-anonymise.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return { copyToClipboard, downloadText };
};

View File

@@ -0,0 +1,135 @@
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface EntityMapping {
originalValue: string;
anonymizedValue: string;
entityType: string;
startIndex: number;
endIndex: number;
}
interface EntityMappingTableProps {
mappings: EntityMapping[];
}
export const EntityMappingTable = ({ mappings }: EntityMappingTableProps) => {
if (!mappings || mappings.length === 0) {
return (
<Card className="mt-6">
<CardHeader className="pb-4">
<CardTitle className="text-base sm:text-lg font-medium text-[#092727]">
Tableau de mapping des entités
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-500 text-center py-4 text-sm">
Aucune entité sensible détectée dans le texte.
</p>
</CardContent>
</Card>
);
}
return (
<Card className="mt-6">
<CardHeader className="pb-4">
<CardTitle className="text-base sm:text-lg font-medium text-[#092727]">
Tableau de mapping des entités ({mappings.length} entité
{mappings.length > 1 ? "s" : ""} anonymisée
{mappings.length > 1 ? "s" : ""})
</CardTitle>
</CardHeader>
<CardContent className="px-2 sm:px-6">
{/* Version mobile : Cards empilées */}
<div className="block sm:hidden space-y-4">
{mappings.map((mapping, index) => (
<div
key={index}
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-600">
Type d&apos;entité
</span>
<Badge
variant="outline"
className="bg-[#f7ab6e] bg-opacity-20 text-[#092727] border-[#f7ab6e] text-xs"
>
{mapping.entityType}
</Badge>
</div>
<div className="space-y-2">
<div>
<span className="text-xs font-medium text-gray-600 block mb-1">
Valeur originale
</span>
<div className="font-mono text-xs bg-red-50 text-red-700 p-2 rounded border break-all">
{mapping.originalValue}
</div>
</div>
<div>
<span className="text-xs font-medium text-gray-600 block mb-1">
Valeur anonymisée
</span>
<div className="font-mono text-xs bg-green-50 text-green-700 p-2 rounded border break-all">
{mapping.anonymizedValue}
</div>
</div>
</div>
</div>
</div>
))}
</div>
{/* Version desktop : Table classique */}
<div className="hidden sm:block overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="font-semibold text-[#092727] min-w-[120px]">
Type d&apos;entité
</TableHead>
<TableHead className="font-semibold text-[#092727] min-w-[150px]">
Valeur originale
</TableHead>
<TableHead className="font-semibold text-[#092727] min-w-[150px]">
Valeur anonymisée
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mappings.map((mapping, index) => (
<TableRow key={index} className="hover:bg-gray-50">
<TableCell className="py-4">
<Badge
variant="outline"
className="bg-[#f7ab6e] bg-opacity-20 text-[#092727] border-[#f7ab6e]"
>
{mapping.entityType}
</Badge>
</TableCell>
<TableCell className="font-mono text-sm bg-red-50 text-red-700 py-4 max-w-[200px] break-all">
{mapping.originalValue}
</TableCell>
<TableCell className="font-mono text-sm bg-green-50 text-green-700 py-4 max-w-[200px] break-all">
{mapping.anonymizedValue}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,92 @@
interface FileHandlerProps {
setUploadedFile: (file: File | null) => void;
setSourceText: (text: string) => void;
setFileContent: (content: string) => void;
setError: (error: string | null) => void;
setIsLoadingFile?: (loading: boolean) => void; // Ajouter cette propriété
}
export const useFileHandler = ({
setUploadedFile,
setSourceText,
setFileContent,
setError,
setIsLoadingFile,
}: FileHandlerProps) => {
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (!file) return;
setUploadedFile(file);
setError(null);
setSourceText("");
setFileContent("");
if (file.type === "text/plain") {
try {
const text = await file.text();
setFileContent(text);
setSourceText(text);
} catch {
setError("Erreur lors de la lecture du fichier texte");
setUploadedFile(null);
}
} else if (file.type === "application/pdf") {
// Activer le loader immédiatement pour les PDF
setIsLoadingFile?.(true);
try {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/process-document", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const extractedText = data.text || data.anonymizedText || "";
if (!extractedText || extractedText.trim().length === 0) {
throw new Error(
"Le fichier PDF ne contient pas de texte extractible"
);
}
setFileContent(extractedText);
setSourceText(extractedText);
} catch (error) {
console.error("Erreur PDF:", error);
setError(
error instanceof Error
? error.message
: "Erreur lors de la lecture du fichier PDF"
);
setUploadedFile(null);
setFileContent("");
setSourceText("");
} finally {
// Désactiver le loader une fois terminé
setIsLoadingFile?.(false);
}
} else {
setError(
"Type de fichier non supporté. Veuillez utiliser un fichier TXT ou PDF."
);
setUploadedFile(null);
}
};
return { handleFileChange };
};

View File

@@ -0,0 +1,220 @@
import { Upload, FileText, AlertTriangle } from "lucide-react";
import { SampleTextComponent } from "./SampleTextComponent";
import { SupportedDataTypes } from "./SupportedDataTypes";
import { AnonymizationInterface } from "./AnonymizationInterface";
interface FileUploadComponentProps {
uploadedFile: File | null;
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
sourceText: string;
setSourceText: (text: string) => void;
setUploadedFile: (file: File | null) => void;
setFileContent: (content: string) => void;
onAnonymize?: () => void;
isProcessing?: boolean;
canAnonymize?: boolean;
isLoadingFile?: boolean;
onRestart?: () => void;
outputText?: string;
}
export const FileUploadComponent = ({
uploadedFile,
handleFileChange,
sourceText,
setSourceText,
setUploadedFile,
setFileContent,
onAnonymize,
isProcessing = false,
canAnonymize = false,
isLoadingFile = false,
onRestart,
outputText,
}: FileUploadComponentProps) => {
// Si un fichier est uploadé ou qu'il y a du texte d'exemple, on affiche le preview
if (uploadedFile || (sourceText && sourceText.trim())) {
return (
<div className="w-full flex flex-col space-y-6">
{/* Preview du document avec en-tête simple */}
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
<div className="bg-orange-50 border-b border-orange-200 px-4 sm:px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<FileText className="h-4 w-4 sm:h-5 sm:w-5 text-orange-600" />
</div>
<div className="min-w-0 flex-1">
{uploadedFile ? (
<p className="text-xs sm:text-sm text-orange-600 truncate">
{uploadedFile.name} {" "}
{(uploadedFile.size / 1024).toFixed(1)} KB
</p>
) : (
<p className="text-xs sm:text-sm text-orange-600">
Demo - Exemple de texte
</p>
)}
</div>
</div>
</div>
</div>
<div className="p-4 sm:p-6">
{/* Zone de texte avec limite de hauteur et scroll */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4 max-h-48 overflow-y-auto">
{isLoadingFile ? (
<div className="flex items-center justify-center py-8">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-5 w-5 sm:h-6 sm:w-6 border-b-2 border-[#f7ab6e]"></div>
<span className="text-xs sm:text-sm text-gray-600">
Chargement du fichier en cours...
</span>
</div>
</div>
) : (
<pre className="text-xs sm:text-sm text-gray-700 whitespace-pre-wrap font-mono">
{sourceText || "Aucun contenu à afficher"}
</pre>
)}
</div>
{/* Disclaimer déplacé en dessous du texte */}
<div className="mt-4">
<div className="flex items-start gap-2 p-3 bg-[#f7ab6e] bg-opacity-10 border border-[#f7ab6e] border-opacity-30 rounded-lg">
<AlertTriangle className="h-4 w-4 text-[#f7ab6e] mt-0.5 flex-shrink-0" />
<p className="text-[10px] sm:text-[11px] text-[#092727] leading-relaxed">
Cet outil IA peut ne pas détecter toutes les informations
sensibles.
<br />
Vérifiez le résultat avant de le partager.
</p>
</div>
</div>
</div>
</div>
{/* 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">
{/* Bouton Anonymiser en premier */}
{onAnonymize && (
<button
onClick={onAnonymize}
disabled={isProcessing}
className="w-full sm:w-auto bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 disabled:opacity-50 disabled:cursor-not-allowed text-white px-6 py-3 rounded-lg text-sm font-medium transition-colors duration-300 flex items-center justify-center space-x-3"
>
{isProcessing ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Anonymisation en cours...</span>
</>
) : (
<>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<span>Anonymiser mes données</span>
</>
)}
</button>
)}
{/* Bouton Recommencer */}
{onRestart && (
<button
onClick={onRestart}
className="w-full sm:w-auto bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg text-sm font-medium transition-colors duration-300 flex items-center justify-center space-x-2"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<span>Recommencer</span>
</button>
)}
</div>
)}
{/* Affichage conditionnel : Interface d'anonymisation OU Types de données supportées */}
{isProcessing || outputText ? (
<AnonymizationInterface
isProcessing={isProcessing}
outputText={outputText}
sourceText={sourceText}
/>
) : (
<SupportedDataTypes />
)}
</div>
);
}
// Si pas de fichier ni de texte, on affiche la zone de drop
return (
<div className="w-full flex flex-col space-y-5">
{/* Drop Zone - Responsive */}
<label className="flex flex-col items-center justify-center cursor-pointer group transition-all duration-300 border-2 border-dashed border-[#092727] rounded-xl bg-gray-50 hover:bg-gray-100 hover:border-[#0a3030] p-6 sm:p-8">
{/* Upload Icon */}
<div className="w-12 h-12 sm:w-16 sm:h-16 bg-[#f7ab6e] group-hover:bg-[#f7ab6e]/75 rounded-full flex items-center justify-center mb-4 transition-colors duration-300">
<Upload className="h-6 w-6 sm:h-8 sm:w-8 text-white transition-colors duration-300" />
</div>
{/* Main Text */}
<h3 className="text-base sm:text-lg font-semibold text-[#092727] mb-2 group-hover:text-[#0a3030] transition-colors duration-300 text-center">
Déposer votre fichier ici
</h3>
<p className="text-sm sm:text-base text-[#092727] opacity-80 mb-4 text-center group-hover:opacity-90 transition-opacity duration-300">
ou cliquez pour sélectionner un fichier
</p>
{/* File Info */}
<div className="flex flex-col sm:flex-row items-center gap-2 text-xs sm:text-sm text-[#092727] opacity-70">
<span>📄 Fichiers TXT, PDF</span>
<span className="hidden sm:inline"></span>
<span>Max 5MB</span>
</div>
{/* Hidden Input */}
<input
type="file"
onChange={handleFileChange}
accept=".txt,.pdf"
className="hidden"
/>
</label>
{/* Bouton d'exemple repositionné juste en dessous */}
<div className="flex justify-center">
<SampleTextComponent
setSourceText={setSourceText}
setFileContent={setFileContent}
setUploadedFile={setUploadedFile}
variant="button"
/>
</div>
{/* Supported Data Types */}
<SupportedDataTypes />
</div>
);
};

View File

@@ -0,0 +1,92 @@
import { Check, Upload, Eye, Shield } from "lucide-react";
interface ProgressBarProps {
currentStep: number;
steps: string[];
}
export const ProgressBar = ({ currentStep, steps }: ProgressBarProps) => {
// Icônes pour chaque étape
const getStepIcon = (stepNumber: number, isCompleted: boolean) => {
if (isCompleted) {
return <Check className="h-3 w-3" />;
}
switch (stepNumber) {
case 1:
return <Upload className="h-3 w-3" />;
case 2:
return <Eye className="h-3 w-3" />;
case 3:
return <Shield className="h-3 w-3" />;
default:
return stepNumber;
}
};
return (
<div className="w-full max-w-2xl mx-auto mb-4 px-2">
<div className="flex items-center justify-center">
<div className="flex items-center justify-between w-full max-w-xs sm:max-w-md">
{steps.map((step, index) => {
const stepNumber = index + 1;
const isCompleted = stepNumber < currentStep;
const isCurrent = stepNumber === currentStep;
return (
<div key={index} className="flex items-center">
{/* Step Circle */}
<div className="flex flex-col items-center">
<div
className={`w-5 h-5 sm:w-6 sm:h-6 rounded-full flex items-center justify-center font-medium text-xs transition-all duration-300 ${
isCompleted
? "bg-[#f7ab6e] text-white"
: isCurrent
? "bg-[#f7ab6e] text-white ring-2 ring-[#f7ab6e] ring-opacity-20"
: "bg-gray-200 text-gray-500"
}`}
>
{getStepIcon(stepNumber, isCompleted)}
</div>
<span
className={`mt-1 text-[8px] sm:text-[10px] font-medium text-center max-w-[60px] sm:max-w-none leading-tight ${
isCompleted || isCurrent
? "text-[#092727]"
: "text-gray-500"
}`}
style={{
wordBreak: 'break-word',
hyphens: 'auto'
}}
>
{step === "Anonymisation" ? (
<>
<span className="hidden sm:inline">Anonymisation</span>
<span className="sm:hidden">Anonym.</span>
</>
) : (
step
)}
</span>
</div>
{/* Connector Line */}
{index < steps.length - 1 && (
<div className="flex-1 mx-2 sm:mx-4">
<div
className={`h-0.5 w-6 sm:w-12 transition-all duration-300 ${
stepNumber < currentStep
? "bg-[#f7ab6e]"
: "bg-gray-200"
}`}
/>
</div>
)}
</div>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,67 @@
import { Copy, Download, AlertTriangle } from "lucide-react";
import { ReactNode } from "react";
interface ResultPreviewComponentProps {
outputText: string;
copyToClipboard: () => void;
downloadText: () => void;
highlightEntities: (text: string) => ReactNode;
}
export const ResultPreviewComponent = ({
outputText,
copyToClipboard,
downloadText,
highlightEntities,
}: ResultPreviewComponentProps) => {
if (!outputText) return null;
return (
<div className="mt-8 space-y-4">
<div className="flex items-center justify-between border-b border-[#f7ab6e] border-opacity-30 pb-2">
<h3 className="text-lg font-medium text-[#092727]">
Document anonymisé
</h3>
<div className="flex items-center gap-2">
<button
onClick={copyToClipboard}
disabled={!outputText}
className="p-2 text-[#092727] hover:text-[#f7ab6e] disabled:opacity-50"
title="Copier"
>
<Copy className="h-4 w-4" />
</button>
<button
onClick={downloadText}
disabled={!outputText}
className="p-2 text-[#092727] hover:text-[#f7ab6e] disabled:opacity-50"
title="Télécharger"
>
<Download className="h-4 w-4" />
</button>
</div>
</div>
<div className="border border-[#f7ab6e] border-opacity-30 rounded-lg bg-white min-h-[400px] flex flex-col">
<div className="flex-1 p-4 overflow-hidden">
<div className="h-full min-h-[300px] text-[#092727] whitespace-pre-wrap overflow-y-auto">
<div className="leading-relaxed">
{highlightEntities(outputText)}
</div>
</div>
</div>
<div className="p-4 border-t border-[#f7ab6e] border-opacity-30">
<div className="flex items-start gap-2 p-2 bg-[#f7ab6e] bg-opacity-10 rounded-md">
<AlertTriangle className="h-4 w-4 text-[#f7ab6e] mt-0.5 flex-shrink-0" />
<p className="text-sm text-[#092727]">
Vérifiez le résultat pour vous assurer que toutes les informations
privées sont supprimées et éviter une divulgation accidentelle.
</p>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,46 @@
interface SampleTextComponentProps {
setSourceText: (text: string) => void;
setFileContent: (content: string) => void;
setUploadedFile: (file: File | null) => void;
variant?: "default" | "button"; // Ajouter une variante
}
export const SampleTextComponent = ({
setSourceText,
setFileContent,
setUploadedFile,
}: SampleTextComponentProps) => {
const loadSampleText = () => {
const sampleText = `Date : 15 mars 2025
Dans le cadre du litige opposant Madame Els Vandermeulen (née le 12/08/1978, demeurant 45 Avenue Louise, 1050 Ixelles, Tel: 02/456.78.90) à Monsieur Karel Derycke, gérant de la SPRL DigitalConsult (BCE: 0123.456.789), nous analysons les éléments suivants :
**Contexte financier :**
Le contrat de prestation signé le 3 janvier 2024 prévoyait un montant de 75 000 € HTVA. Les virements effectués depuis le compte IBAN BE68 5390 0754 7034 (BNP Paribas Fortis) vers le compte bénéficiaire BE71 0961 2345 6789 montrent des irrégularités.
**Témoins clés :**
- Dr. Marie Claes (expert-comptable, n° IEC: 567890)
- M. Pieter Van Der Berg (consultant IT, email: p.vanderberg@itconsult.be)
**Données sensibles :**
Le serveur compromis contenait 12 000 dossiers clients avec numéros de registre national. Lincident du 28 février 2024 a exposé les données personnelles stockées sur ladresse IP 10.0.0.45 dans les bureaux situés Rue de la Loi 200, 1040 Etterbeek.
Coordonnées bancaires : BE43 0017 5555 5557 (CBC Banque)
TVA intracommunautaire : BE0987.654.321`;
setSourceText(sampleText);
setFileContent(sampleText);
setUploadedFile(null);
};
return (
<>
<button
onClick={loadSampleText}
className="bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 cursor-pointer text-white px-6 py-3 rounded-lg text-sm font-medium transition-colors duration-300"
>
Essayez avec un texte d&apos;exemple
</button>
</>
);
};

View File

@@ -0,0 +1,31 @@
export const SupportedDataTypes = () => {
return (
<div className="bg-gray-50 border border-gray-200 rounded-xl p-6">
<h4 className="text-sm font-semibold text-[#092727] mb-4">
Types de données supportées :
</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs text-[#092727] opacity-80">
<div className="flex flex-col space-y-2">
<span> Prénoms</span>
<span> Numéros de téléphone</span>
<span> Noms de domaine</span>
</div>
<div className="flex flex-col space-y-2">
<span> Noms de famille</span>
<span> Adresses</span>
<span> Dates</span>
</div>
<div className="flex flex-col space-y-2">
<span> Noms complets</span>
<span> Numéros d&apos;ID</span>
<span> Valeurs numériques</span>
</div>
<div className="flex flex-col space-y-2">
<span> Adresses e-mail</span>
<span> Valeurs monétaires</span>
<span> Texte personnalisé</span>
</div>
</div>
</div>
);
};