diff --git a/app/api/process-document/route.ts b/app/api/process-document/route.ts index bfebe7e..3a63077 100644 --- a/app/api/process-document/route.ts +++ b/app/api/process-document/route.ts @@ -19,15 +19,16 @@ export async function POST(req: NextRequest) { let fileContent = ""; const fileType = file.type; - // --- LOGIQUE D'EXTRACTION DE TEXTE (INCHANGÉE) --- + // --- LOGIQUE D'EXTRACTION DE TEXTE --- if (fileType === "application/pdf") { console.log("📄 Traitement PDF en cours..."); try { const buffer = Buffer.from(await file.arrayBuffer()); const data = await pdf(buffer); - fileContent = data.text; + fileContent = data.text || ""; console.log("✅ Extraction PDF réussie, longueur:", fileContent.length); } catch (pdfError) { + console.error("❌ Erreur PDF:", pdfError); return NextResponse.json( { error: `Erreur traitement PDF: ${ @@ -45,12 +46,13 @@ export async function POST(req: NextRequest) { try { const arrayBuffer = await file.arrayBuffer(); const result = await mammoth.extractRawText({ arrayBuffer }); - fileContent = result.value; + fileContent = result.value || ""; console.log( "✅ Extraction Word réussie, longueur:", fileContent.length ); } catch (wordError) { + console.error("❌ Erreur Word:", wordError); return NextResponse.json( { error: `Erreur traitement Word: ${ @@ -69,6 +71,7 @@ export async function POST(req: NextRequest) { fileContent.length ); } catch (textError) { + console.error("❌ Erreur texte:", textError); return NextResponse.json( { error: `Erreur lecture texte: ${ @@ -88,12 +91,19 @@ export async function POST(req: NextRequest) { ); } + // Vérifier si c'est juste pour l'extraction de texte (lecture simple) + const isSimpleExtraction = + req.headers.get("x-simple-extraction") === "true"; + + if (isSimpleExtraction) { + // Retourner juste le texte extrait + return NextResponse.json({ text: fileContent }, { status: 200 }); + } + // ========================================================== - // CONFIGURATION PRESIDIO ANALYZER (SIMPLIFIÉE) + // CONFIGURATION PRESIDIO ANALYZER (pour l'anonymisation complète) // ========================================================== - // Toute la configuration (recognizers, allow_list, etc.) est maintenant dans le default.yaml du service. - // L'API a juste besoin d'envoyer le texte et la langue. const analyzerConfig = { text: fileContent, language: "fr", @@ -104,70 +114,72 @@ export async function POST(req: NextRequest) { const presidioAnalyzerUrl = "http://analyzer.151.80.20.211.sslip.io/analyze"; - const analyzeResponse = await fetch(presidioAnalyzerUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(analyzerConfig), - }); + try { + const analyzeResponse = await fetch(presidioAnalyzerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(analyzerConfig), + }); - console.log("📊 Statut Analyzer:", analyzeResponse.status); - if (!analyzeResponse.ok) { - const errorBody = await analyzeResponse.text(); - return NextResponse.json( - { error: `Erreur Analyzer: ${errorBody}` }, - { status: 500 } - ); + console.log("📊 Statut Analyzer:", analyzeResponse.status); + if (!analyzeResponse.ok) { + const errorBody = await analyzeResponse.text(); + console.error("❌ Erreur Analyzer:", errorBody); + // Fallback: retourner juste le texte si Presidio n'est pas disponible + return NextResponse.json({ text: fileContent }, { status: 200 }); + } + + const analyzerResults = await analyzeResponse.json(); + console.log("✅ Analyzer a trouvé", analyzerResults.length, "entités."); + + // ========================================================================= + // CONFIGURATION PRESIDIO ANONYMIZER + // ========================================================================= + + const anonymizerConfig = { + text: fileContent, + analyzer_results: analyzerResults, + }; + + console.log("🔍 Appel à Presidio Anonymizer..."); + const presidioAnonymizerUrl = + "http://anonymizer.151.80.20.211.sslip.io/anonymize"; + + const anonymizeResponse = await fetch(presidioAnonymizerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(anonymizerConfig), + }); + + console.log("📊 Statut Anonymizer:", anonymizeResponse.status); + if (!anonymizeResponse.ok) { + const errorBody = await anonymizeResponse.text(); + console.error("❌ Erreur Anonymizer:", errorBody); + // Fallback: retourner juste le texte si Presidio n'est pas disponible + return NextResponse.json({ text: fileContent }, { status: 200 }); + } + + const anonymizerResult = await anonymizeResponse.json(); + console.log("✅ Anonymisation réussie."); + + const result = { + text: fileContent, + anonymizedText: anonymizerResult.text, + piiCount: analyzerResults.length, + }; + + return NextResponse.json(result, { status: 200 }); + } catch (presidioError) { + console.error("❌ Erreur Presidio:", presidioError); + // Fallback: retourner juste le texte extrait + return NextResponse.json({ text: fileContent }, { status: 200 }); } - - const analyzerResults = await analyzeResponse.json(); - console.log("✅ Analyzer a trouvé", analyzerResults.length, "entités."); - - // ========================================================================= - // CONFIGURATION PRESIDIO ANONYMIZER - // ========================================================================= - - // L'Anonymizer lira la config d'anonymisation du default.yaml de l'Analyzer - // ou vous pouvez définir des transformations spécifiques ici si besoin. - // Pour commencer, on envoie juste les résultats de l'analyse. - const anonymizerConfig = { - text: fileContent, - analyzer_results: analyzerResults, - }; - - console.log("🔍 Appel à Presidio Anonymizer..."); - const presidioAnonymizerUrl = - "http://anonymizer.151.80.20.211.sslip.io/anonymize"; - - const anonymizeResponse = await fetch(presidioAnonymizerUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(anonymizerConfig), - }); - - console.log("📊 Statut Anonymizer:", anonymizeResponse.status); - if (!anonymizeResponse.ok) { - const errorBody = await anonymizeResponse.text(); - return NextResponse.json( - { error: `Erreur Anonymizer: ${errorBody}` }, - { status: 500 } - ); - } - - const anonymizerResult = await anonymizeResponse.json(); - console.log("✅ Anonymisation réussie."); - - const result = { - anonymizedText: anonymizerResult.text, - piiCount: analyzerResults.length, - }; - - return NextResponse.json(result, { status: 200 }); } catch (err: unknown) { console.error("❌ Erreur générale:", err); return NextResponse.json( diff --git a/app/components/AnonymizationInterface.tsx b/app/components/AnonymizationInterface.tsx new file mode 100644 index 0000000..a6cb5f4 --- /dev/null +++ b/app/components/AnonymizationInterface.tsx @@ -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(); + + // 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 ( +
+
+
+

+ Anonymisation en cours... +

+
+
+
+
+ Analyse du contenu +
+
+
+ + Détection des données sensibles + +
+
+
+ + Application de l'anonymisation + +
+
+
+ ); + } + + if (outputText) { + const anonymizedTypes = getAnonymizedDataTypes(); + + return ( +
+
+ +

+ Anonymisation terminée avec succès +

+
+
+ {supportedDataStructure.map((column, columnIndex) => ( +
+ {column.items.map((item, itemIndex) => { + const isAnonymized = anonymizedTypes.has(item); + return ( + + {isAnonymized ? "✓" : "•"} {item} + + ); + })} +
+ ))} +
+
+ ); + } + + return null; +}; diff --git a/app/components/AnonymizationLogic.tsx b/app/components/AnonymizationLogic.tsx new file mode 100644 index 0000000..1bcab17 --- /dev/null +++ b/app/components/AnonymizationLogic.tsx @@ -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 }; +}; \ No newline at end of file diff --git a/app/components/DocumentPreview.tsx b/app/components/DocumentPreview.tsx new file mode 100644 index 0000000..2e15803 --- /dev/null +++ b/app/components/DocumentPreview.tsx @@ -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 = ({ + uploadedFile, + fileContent, + sourceText, +}) => { + if (!uploadedFile && (!sourceText || !sourceText.trim())) { + return null; + } + + return ( +
+
+
+
+
+ +
+
+ {uploadedFile && ( +

+ {uploadedFile.name} • {(uploadedFile.size / 1024).toFixed(1)}{" "} + KB +

+ )} +
+
+
+
+ +
+
+
+            {sourceText || fileContent || "Aucun contenu Ă  afficher"}
+          
+
+
+
+ ); +}; diff --git a/app/components/DownloadActions.tsx b/app/components/DownloadActions.tsx new file mode 100644 index 0000000..7524b09 --- /dev/null +++ b/app/components/DownloadActions.tsx @@ -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 }; +}; \ No newline at end of file diff --git a/app/components/EntityMappingTable.tsx b/app/components/EntityMappingTable.tsx new file mode 100644 index 0000000..6f520cd --- /dev/null +++ b/app/components/EntityMappingTable.tsx @@ -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 ( + + + + Tableau de mapping des entités + + + +

+ Aucune entité sensible détectée dans le texte. +

+
+
+ ); + } + + return ( + + + + Tableau de mapping des entités ({mappings.length} entité + {mappings.length > 1 ? "s" : ""} anonymisée + {mappings.length > 1 ? "s" : ""}) + + + + {/* Version mobile : Cards empilées */} +
+ {mappings.map((mapping, index) => ( +
+
+
+ + Type d'entité + + + {mapping.entityType} + +
+
+
+ + Valeur originale + +
+ {mapping.originalValue} +
+
+
+ + Valeur anonymisée + +
+ {mapping.anonymizedValue} +
+
+
+
+
+ ))} +
+ + {/* Version desktop : Table classique */} +
+ + + + + Type d'entité + + + Valeur originale + + + Valeur anonymisée + + + + + {mappings.map((mapping, index) => ( + + + + {mapping.entityType} + + + + {mapping.originalValue} + + + {mapping.anonymizedValue} + + + ))} + +
+
+
+
+ ); +}; diff --git a/app/components/FileHandler.tsx b/app/components/FileHandler.tsx new file mode 100644 index 0000000..4238675 --- /dev/null +++ b/app/components/FileHandler.tsx @@ -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 + ) => { + 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 }; +}; diff --git a/app/components/FileUploadComponent.tsx b/app/components/FileUploadComponent.tsx new file mode 100644 index 0000000..fed5d49 --- /dev/null +++ b/app/components/FileUploadComponent.tsx @@ -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) => 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 ( +
+ {/* Preview du document avec en-tĂŞte simple */} +
+
+
+
+
+ +
+
+ {uploadedFile ? ( +

+ {uploadedFile.name} •{" "} + {(uploadedFile.size / 1024).toFixed(1)} KB +

+ ) : ( +

+ Demo - Exemple de texte +

+ )} +
+
+
+
+ +
+ {/* Zone de texte avec limite de hauteur et scroll */} +
+ {isLoadingFile ? ( +
+
+
+ + Chargement du fichier en cours... + +
+
+ ) : ( +
+                  {sourceText || "Aucun contenu Ă  afficher"}
+                
+ )} +
+ + {/* Disclaimer déplacé en dessous du texte */} +
+
+ +

+ Cet outil IA peut ne pas détecter toutes les informations + sensibles. +
+ Vérifiez le résultat avant de le partager. +

+
+
+
+
+ + {/* Boutons d'action - Responsive mobile */} + {canAnonymize && !isLoadingFile && ( +
+ {/* Bouton Anonymiser en premier */} + {onAnonymize && ( + + )} + + {/* Bouton Recommencer */} + {onRestart && ( + + )} +
+ )} + + {/* Affichage conditionnel : Interface d'anonymisation OU Types de données supportées */} + {isProcessing || outputText ? ( + + ) : ( + + )} +
+ ); + } + + // Si pas de fichier ni de texte, on affiche la zone de drop + return ( +
+ {/* Drop Zone - Responsive */} + + + {/* Bouton d'exemple repositionné juste en dessous */} +
+ +
+ + {/* Supported Data Types */} + +
+ ); +}; diff --git a/app/components/ProgressBar.tsx b/app/components/ProgressBar.tsx new file mode 100644 index 0000000..c30b65e --- /dev/null +++ b/app/components/ProgressBar.tsx @@ -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 ; + } + + switch (stepNumber) { + case 1: + return ; + case 2: + return ; + case 3: + return ; + default: + return stepNumber; + } + }; + + return ( +
+
+
+ {steps.map((step, index) => { + const stepNumber = index + 1; + const isCompleted = stepNumber < currentStep; + const isCurrent = stepNumber === currentStep; + + return ( +
+ {/* Step Circle */} +
+
+ {getStepIcon(stepNumber, isCompleted)} +
+ + {step === "Anonymisation" ? ( + <> + Anonymisation + Anonym. + + ) : ( + step + )} + +
+ + {/* Connector Line */} + {index < steps.length - 1 && ( +
+
+
+ )} +
+ ); + })} +
+
+
+ ); +}; diff --git a/app/components/ResultPreviewComponent.tsx b/app/components/ResultPreviewComponent.tsx new file mode 100644 index 0000000..0cf21e7 --- /dev/null +++ b/app/components/ResultPreviewComponent.tsx @@ -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 ( +
+
+

+ Document anonymisé +

+
+ + + +
+
+ +
+
+
+
+ {highlightEntities(outputText)} +
+
+
+ +
+
+ +

+ Vérifiez le résultat pour vous assurer que toutes les informations + privées sont supprimées et éviter une divulgation accidentelle. +

+
+
+
+
+ ); +}; diff --git a/app/components/SampleTextComponent.tsx b/app/components/SampleTextComponent.tsx new file mode 100644 index 0000000..7a854ae --- /dev/null +++ b/app/components/SampleTextComponent.tsx @@ -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. L’incident du 28 février 2024 a exposé les données personnelles stockées sur l’adresse 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 ( + <> + + + ); +}; diff --git a/app/components/SupportedDataTypes.tsx b/app/components/SupportedDataTypes.tsx new file mode 100644 index 0000000..bc80623 --- /dev/null +++ b/app/components/SupportedDataTypes.tsx @@ -0,0 +1,31 @@ +export const SupportedDataTypes = () => { + return ( +
+

+ Types de données supportées : +

+
+
+ • Prénoms + • Numéros de téléphone + • Noms de domaine +
+
+ • Noms de famille + • Adresses + • Dates +
+
+ • Noms complets + • Numéros d'ID + • Valeurs numériques +
+
+ • Adresses e-mail + • Valeurs monétaires + • Texte personnalisé +
+
+
+ ); +}; diff --git a/app/layout.tsx b/app/layout.tsx index 75400df..228f7ec 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -25,7 +25,7 @@ export default function RootLayout({ return ( {children} diff --git a/app/page.tsx b/app/page.tsx index 7fb69d5..134401b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,485 +1,158 @@ "use client"; import { useState } from "react"; -import { Upload, Download, Copy, AlertTriangle } from "lucide-react"; +import { FileUploadComponent } from "./components/FileUploadComponent"; +import { ResultPreviewComponent } from "./components/ResultPreviewComponent"; +import { EntityMappingTable } from "./components/EntityMappingTable"; +import { ProgressBar } from "./components/ProgressBar"; +import { useFileHandler } from "./components/FileHandler"; +import { useAnonymization } from "./components/AnonymizationLogic"; +import { useDownloadActions } from "./components/DownloadActions"; +import { highlightEntities } from "./utils/highlightEntities"; -export type PageObject = { - pageNumber: number; - htmlContent: string; -}; +interface EntityMapping { + originalValue: string; + anonymizedValue: string; + entityType: string; + startIndex: number; + endIndex: number; +} export default function Home() { - const [sourceText, setSourceText] = useState(""); - const [outputText, setOutputText] = useState(""); - const [activeTab, setActiveTab] = useState<"text" | "url">("text"); - const [urlInput, setUrlInput] = useState(""); - const [isProcessing, setIsProcessing] = useState(false); + const [sourceText, setSourceText] = useState(""); + const [outputText, setOutputText] = useState(""); + const [uploadedFile, setUploadedFile] = useState(null); + const [fileContent, setFileContent] = useState(""); const [error, setError] = useState(null); - const [uploadedFile, setUploadedFile] = useState(null); // Nouveau state pour le fichier - const [fileContent, setFileContent] = useState(""); // Nouveau state pour le contenu + const [isLoadingFile, setIsLoadingFile] = useState(false); + const [entityMappings, setEntityMappings] = useState([]); - const handleFileChange = async (e: React.ChangeEvent) => { - if (e.target.files?.length) { - const selectedFile = e.target.files[0]; - setUploadedFile(selectedFile); // Stocker le fichier - setSourceText(""); // EFFACER le texte existant quand on upload un fichier - setError(null); + const progressSteps = ["Téléversement", "Prévisualisation", "Anonymisation"]; - try { - // Traitement des fichiers texte - juste lire le contenu - if ( - selectedFile.type === "text/plain" || - selectedFile.name.endsWith(".txt") - ) { - const reader = new FileReader(); - reader.onload = (e) => { - const content = e.target?.result as string; - setFileContent(content); // Stocker le contenu sans l'afficher - }; - reader.readAsText(selectedFile); - } - // Pour les PDF - juste stocker le fichier, pas de traitement automatique - else if ( - selectedFile.type === "application/pdf" || - selectedFile.name.endsWith(".pdf") - ) { - setFileContent(""); // Pas de contenu texte pour les PDF - } - // Traitement des fichiers Word (optionnel) - else if ( - selectedFile.name.endsWith(".docx") || - selectedFile.name.endsWith(".doc") - ) { - setError( - "Les fichiers Word ne sont pas encore supportés. Veuillez convertir en PDF ou texte." - ); - setUploadedFile(null); - return; - } else { - setError( - "Format de fichier non supporté. Veuillez utiliser un fichier PDF ou texte." - ); - setUploadedFile(null); - return; - } - } catch (error) { - setError( - error instanceof Error - ? error.message - : "Erreur lors du traitement du fichier" - ); - setUploadedFile(null); - } - } + const getCurrentStep = () => { + if (outputText) return 3; + if (uploadedFile || (sourceText && sourceText.trim())) return 2; + return 1; }; - const loadSampleText = () => { - const sampleText = `Bonjour, - -Je suis Jean Dupont, né le 15 mars 1985. Mon numéro de téléphone est le 06 12 34 56 78 et mon email est jean.dupont@email.com. - -Mon adresse est 123 rue de la Paix, 75001 Paris. - -Mon numéro de sécurité sociale est 1 85 03 75 123 456 78. - -Cordialement, -Jean Dupont`; - setSourceText(sampleText); - }; - - const anonymizeData = async () => { - // Si un fichier est uploadé, traiter le fichier - if (uploadedFile) { - if ( - uploadedFile.type === "application/pdf" || - uploadedFile.name.endsWith(".pdf") - ) { - setIsProcessing(true); - setError(null); - setOutputText(""); - - try { - 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 result = await response.json(); - if (result.anonymizedText) { - setOutputText(result.anonymizedText); - } else { - throw new Error(result.error || "Erreur lors de l'anonymisation"); - } - } catch (error) { - setError( - error instanceof Error - ? error.message - : "Erreur lors du traitement du fichier" - ); - } finally { - setIsProcessing(false); - } - return; - } else { - // Pour les fichiers texte, utiliser le contenu lu - if (!fileContent.trim()) { - setError("Le fichier ne contient pas de texte"); - return; - } - // Utiliser fileContent au lieu de sourceText pour l'anonymisation - const textToAnonymize = fileContent; - - setIsProcessing(true); - setError(null); - setOutputText(""); - - try { - await new Promise((resolve) => setTimeout(resolve, 1500)); - - const anonymized = textToAnonymize - .replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, "[Nom1]") - .replace(/\b0[1-9](?:[\s.-]?\d{2}){4}\b/g, "[Téléphone1]") - .replace( - /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, - "[Email1]" - ) - .replace( - /\b\d{1,3}\s+[a-zA-Z\s]+,\s*\d{5}\s+[a-zA-Z\s]+\b/g, - "[Adresse1]" - ) - .replace( - /\b\d\s\d{2}\s\d{2}\s\d{2}\s\d{3}\s\d{3}\s\d{2}\b/g, - "[NuméroSS1]" - ); - - setOutputText(anonymized); - } catch { - setError("Erreur lors de l'anonymisation"); - } finally { - setIsProcessing(false); - } - return; - } - } - - // Si pas de fichier, traiter le texte saisi manuellement - if (!sourceText.trim()) { - setError( - "Veuillez saisir du texte à anonymiser ou télécharger un fichier" - ); - return; - } - - setIsProcessing(true); - setError(null); + // Fonction pour recommencer (retourner à l'état initial) + const handleRestart = () => { + setSourceText(""); setOutputText(""); - - try { - await new Promise((resolve) => setTimeout(resolve, 1500)); - - const anonymized = sourceText - .replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, "[Nom1]") - .replace(/\b0[1-9](?:[\s.-]?\d{2}){4}\b/g, "[Téléphone1]") - .replace( - /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, - "[Email1]" - ) - .replace( - /\b\d{1,3}\s+[a-zA-Z\s]+,\s*\d{5}\s+[a-zA-Z\s]+\b/g, - "[Adresse1]" - ) - .replace( - /\b\d\s\d{2}\s\d{2}\s\d{2}\s\d{3}\s\d{3}\s\d{2}\b/g, - "[NuméroSS1]" - ); - - setOutputText(anonymized); - } catch { - setError("Erreur lors de l'anonymisation"); - } finally { - setIsProcessing(false); - } + setUploadedFile(null); + setFileContent(""); + setError(null); + setIsLoadingFile(false); + setEntityMappings([]); }; - const copyToClipboard = () => { - navigator.clipboard.writeText(outputText); - }; + // Hooks personnalisés pour la logique métier + const { handleFileChange } = useFileHandler({ + setUploadedFile, + setSourceText, + setFileContent, + setError, + setIsLoadingFile, // Passer le setter + }); - 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); - }; + const { anonymizeData, isProcessing } = useAnonymization({ + sourceText, + fileContent, + uploadedFile, + setOutputText, + setError, + setEntityMappings, + }); + + const { copyToClipboard, downloadText } = useDownloadActions({ outputText }); return ( -
- {" "} - {/* Fond vert foncé */} +
{/* Header */}
- {" "} - {/* Header vert foncé avec bordure blanche */} -
-

- {" "} - {/* Titre en blanc */} +
+

OUTIL D'ANONYMISATION DE DONNÉES

+

+ Protégez vos informations sensibles en quelques clics +

+ {/* Main Content */} -
-
- {/* Left Panel - Source */} -
- {/* Tabs */} -
- - -
+
+ {/* Progress Bar */} + - {/* Content Area - mĂŞme taille que droite */} -
- {/* Zone de contenu - prend tout l'espace disponible */} -
- {activeTab === "text" ? ( -
- {/* Zone de texte - prend TOUT l'espace */} -
- {/* Placeholder conditionnel : affiché seulement si pas de texte ET pas de fichier uploadé */} - {sourceText === "" && !uploadedFile && ( -
- Tapez votre texte ici ou{" "} - - essayez un texte d'exemple - -
- )} - - {/* Affichage du fichier uploadé dans la zone de texte */} - {uploadedFile && ( -
-
- - - {uploadedFile.name} - - - ({(uploadedFile.size / 1024).toFixed(1)} KB) - - -
-
- )} - -