Files
Anonyme/app/page.tsx
2025-07-26 00:12:57 +02:00

486 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { Upload, Download, Copy, AlertTriangle } from "lucide-react";
export type PageObject = {
pageNumber: number;
htmlContent: string;
};
export default function Home() {
const [sourceText, setSourceText] = useState<string>("");
const [outputText, setOutputText] = useState<string>("");
const [activeTab, setActiveTab] = useState<"text" | "url">("text");
const [urlInput, setUrlInput] = useState<string>("");
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [uploadedFile, setUploadedFile] = useState<File | null>(null); // Nouveau state pour le fichier
const [fileContent, setFileContent] = useState<string>(""); // Nouveau state pour le contenu
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
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);
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 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);
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);
}
};
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 (
<div className="min-h-screen bg-[#092727]">
{" "}
{/* Fond vert foncé */}
{/* Header */}
<div className="bg-[#092727] border-b border-white border-opacity-20">
{" "}
{/* Header vert foncé avec bordure blanche */}
<div className="max-w-6xl mx-auto px-4 py-6">
<h1 className="text-3xl font-bold text-white text-center mb-2">
{" "}
{/* Titre en blanc */}
OUTIL D&apos;ANONYMISATION DE DONNÉES
</h1>
</div>
</div>
{/* Main Content */}
<div className="max-w-6xl mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Panel - Source */}
<div className="space-y-4">
{/* Tabs */}
<div className="flex border-b border-white border-opacity-20">
<button
onClick={() => setActiveTab("text")}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === "text"
? "border-[#f7ab6e] bg-[#f7ab6e] text-[#092727]"
: "border-transparent text-white hover:text-[#f7ab6e]"
}`}
>
Texte Source
</button>
<button
disabled
className="px-4 py-2 font-medium border-b-2 transition-colors border-transparent text-white opacity-50 cursor-not-allowed relative group"
>
Saisir URL
<span className="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-[#f7ab6e] text-[#092727] text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
À venir
</span>
</button>
</div>
{/* Content Area - même taille que droite */}
<div className="border border-white border-opacity-20 rounded-md bg-[#092727] h-[500px] flex flex-col">
{/* Zone de contenu - prend tout l'espace disponible */}
<div className="flex-1 overflow-hidden">
{activeTab === "text" ? (
<div className="h-full">
{/* Zone de texte - prend TOUT l'espace */}
<div className="h-full relative">
{/* Placeholder conditionnel : affiché seulement si pas de texte ET pas de fichier uploadé */}
{sourceText === "" && !uploadedFile && (
<div className="absolute top-3 left-3 text-white text-opacity-60 text-sm pointer-events-none z-10">
Tapez votre texte ici ou{" "}
<span
onClick={loadSampleText}
className="text-[#f7ab6e] hover:text-[#f7ab6e] hover:opacity-80 underline cursor-pointer pointer-events-auto"
>
essayez un texte d&apos;exemple
</span>
</div>
)}
{/* Affichage du fichier uploadé dans la zone de texte */}
{uploadedFile && (
<div className="absolute top-3 left-3 right-3 text-white text-sm pointer-events-none z-10">
<div className="inline-flex items-center gap-2 bg-[#f7ab6e] rounded-md p-3 max-w-fit">
<Upload className="h-4 w-4 text-[#092727]" />
<span className="text-[#092727] text-sm font-medium">
{uploadedFile.name}
</span>
<span className="text-[#092727] text-opacity-80 text-xs">
({(uploadedFile.size / 1024).toFixed(1)} KB)
</span>
<button
onClick={() => {
setUploadedFile(null);
setFileContent("");
setSourceText("");
const fileInput = document.querySelector(
'input[type="file"]'
) as HTMLInputElement;
if (fileInput) fileInput.value = "";
}}
className="ml-2 text-[#092727] hover:text-white transition-colors p-1 hover:bg-[#092727] hover:bg-opacity-20 rounded pointer-events-auto"
title="Supprimer le fichier"
>
×
</button>
</div>
</div>
)}
<textarea
value={sourceText}
onChange={(e) => setSourceText(e.target.value)}
className="w-full h-full border-none outline-none resize-none text-white bg-transparent overflow-y-auto p-3 placeholder-white placeholder-opacity-60"
/>
</div>
</div>
) : (
<div className="h-full p-4">
<input
type="url"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
placeholder="Entrez l'URL du site web à anonymiser"
className="w-full p-3 border border-white border-opacity-20 rounded-lg outline-none focus:border-[#f7ab6e] bg-transparent text-white placeholder-white placeholder-opacity-60"
/>
</div>
)}
</div>
{/* Bouton télécharger fichier - au-dessus du disclaimer */}
<div className="px-4 pb-2 flex-shrink-0">
<label className="flex items-center gap-2 px-4 py-2 text-[#f7ab6e] hover:bg-[#f7ab6e] hover:text-white rounded-lg font-medium cursor-pointer transition-colors text-sm w-fit group">
<Upload className="h-4 w-4 text-[#f7ab6e] group-hover:text-white transition-colors" />
Téléverser votre fichier
<input
type="file"
onChange={handleFileChange}
accept=".txt,.pdf,.docx,.doc"
className="hidden"
/>
</label>
</div>
{/* Disclaimer - tout en bas */}
<div className="p-4 rounded-b-md flex-shrink-0">
<div className="flex items-start gap-2 p-2 bg-[#f7ab6e] bg-opacity-20 rounded-md text-[#092727]">
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<p className=" text-sm">
Cet outil IA peut ne pas détecter toutes les informations
sensibles. Vérifiez le résultat avant de le partager.
</p>
</div>
</div>
</div>
</div>
{/* Right Panel - Output */}
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-white border-opacity-20 pb-2">
<h3 className="text-lg font-medium text-white">
Texte de Sortie
</h3>
<div className="flex items-center gap-2">
<button
onClick={copyToClipboard}
disabled={!outputText}
className="p-2 text-white hover:text-[#f7ab6e] disabled:opacity-50"
title="Copier"
>
<Copy className="h-4 w-4" />
</button>
<button
onClick={downloadText}
disabled={!outputText}
className="p-2 text-white hover:text-[#f7ab6e] disabled:opacity-50"
title="Télécharger"
>
<Download className="h-4 w-4" />
</button>
</div>
</div>
{/* Content Area - même taille que gauche */}
<div className="border border-white border-opacity-20 rounded-md bg-[#092727] min-h-[500px] max-h-[500px] flex flex-col">
{/* Zone de contenu - 90% */}
<div className="flex-1 p-4 overflow-hidden">
<div className="h-full min-h-[350px] max-h-[350px] text-white whitespace-pre-wrap overflow-y-auto">
{outputText ? (
<div className="leading-relaxed">
{highlightEntities(outputText)}
</div>
) : (
"Le texte anonymisé apparaîtra ici..."
)}
</div>
</div>
{/* Disclaimer - 10% intégré dans le même cadre */}
<div className="p-4">
<div className="flex items-start gap-2 p-2 [#092727] bg-[#f7ab6e] bg-opacity-20 rounded-md">
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<p className="text-sm">
Vérifiez le résultat expurgé pour vous assurer que toutes
les informations privées sont supprimées et éviter une
divulgation accidentelle.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Anonymize Button */}
<div className="flex justify-center mt-8">
<button
onClick={anonymizeData}
disabled={isProcessing || (!sourceText.trim() && !uploadedFile)}
className="bg-[#f7ab6e] hover:bg-[#f7ab6e] hover:opacity-80 text-[#092727] px-8 py-3 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{isProcessing
? "Anonymisation en cours..."
: "Anonymiser mes données"}
</button>
</div>
{/* Error Message */}
{error && (
<div className="mt-4 p-4 bg-[#f7ab6e] bg-opacity-20 border border-[#f7ab6e] rounded-lg">
<p className="text-[#092727] text-sm">{error}</p>
</div>
)}
</div>
</div>
);
}
// Fonction pour mettre en évidence les entités anonymisées
const highlightEntities = (text: string) => {
if (!text) return text;
// Pattern pour détecter les entités anonymisées entre crochets
const entityPattern = /\[([^\]]+)\]/g;
const parts = [];
let lastIndex = 0;
let match;
while ((match = entityPattern.exec(text)) !== null) {
// Ajouter le texte avant l'entité
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
// Ajouter l'entité avec fond orange
parts.push(
<span
key={match.index}
className="bg-[#f7ab6e] text-[#092727] px-1 py-0.5 rounded font-medium"
>
{match[0]}
</span>
);
lastIndex = match.index + match[0].length;
}
// Ajouter le reste du texte
if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}
return parts.length > 0 ? parts : text;
};