486 lines
18 KiB
TypeScript
486 lines
18 KiB
TypeScript
"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'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'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;
|
||
};
|