version pre
This commit is contained in:
917
app/page.tsx
917
app/page.tsx
@@ -1,536 +1,485 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import { Upload, Download, Copy, AlertTriangle } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Upload,
|
|
||||||
FileText,
|
|
||||||
ShieldCheck,
|
|
||||||
Download,
|
|
||||||
Trash2,
|
|
||||||
AlertCircle,
|
|
||||||
X,
|
|
||||||
Zap,
|
|
||||||
Lock,
|
|
||||||
Shield,
|
|
||||||
Clock,
|
|
||||||
Eye,
|
|
||||||
TestTube,
|
|
||||||
} from "lucide-react";
|
|
||||||
import PresidioModal from "./components/PresidioModal";
|
|
||||||
|
|
||||||
export type PageObject = {
|
export type PageObject = {
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ProcessedFile {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
status: "processing" | "completed" | "error";
|
|
||||||
timestamp: Date;
|
|
||||||
originalSize?: string;
|
|
||||||
processedSize?: string;
|
|
||||||
piiCount?: number;
|
|
||||||
errorMessage?: string;
|
|
||||||
processedBlob?: Blob;
|
|
||||||
anonymizedText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
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 [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const [history, setHistory] = useState<ProcessedFile[]>([]);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showPresidioModal, setShowPresidioModal] = useState(false);
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null); // Nouveau state pour le fichier
|
||||||
const [anonymizedResult, setAnonymizedResult] = useState<{
|
const [fileContent, setFileContent] = useState<string>(""); // Nouveau state pour le contenu
|
||||||
text: string;
|
|
||||||
piiCount: number;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files?.length) {
|
if (e.target.files?.length) {
|
||||||
setFile(e.target.files[0]);
|
const selectedFile = e.target.files[0];
|
||||||
|
setUploadedFile(selectedFile); // Stocker le fichier
|
||||||
|
setSourceText(""); // EFFACER le texte existant quand on upload un fichier
|
||||||
setError(null);
|
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 handleDrop = useCallback((e: React.DragEvent) => {
|
const loadSampleText = () => {
|
||||||
e.preventDefault();
|
const sampleText = `Bonjour,
|
||||||
setIsDragOver(false);
|
|
||||||
if (e.dataTransfer.files?.length) {
|
|
||||||
setFile(e.dataTransfer.files[0]);
|
|
||||||
setError(null);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
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.
|
||||||
e.preventDefault();
|
|
||||||
setIsDragOver(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragLeave = useCallback(() => {
|
Mon adresse est 123 rue de la Paix, 75001 Paris.
|
||||||
setIsDragOver(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
Mon numéro de sécurité sociale est 1 85 03 75 123 456 78.
|
||||||
if (bytes === 0) return "0 Bytes";
|
|
||||||
const k = 1024;
|
Cordialement,
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
Jean Dupont`;
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
setSourceText(sampleText);
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearFile = () => {
|
const anonymizeData = async () => {
|
||||||
setFile(null);
|
// 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);
|
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 clearHistory = () => setHistory([]);
|
const copyToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(outputText);
|
||||||
|
};
|
||||||
|
|
||||||
const removeFromHistory = (id: string) =>
|
const downloadText = () => {
|
||||||
setHistory((prev) => prev.filter((item) => item.id !== id));
|
const blob = new Blob([outputText], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
const handleDownloadTxt = (id: string) => {
|
|
||||||
const fileToDownload = history.find((item) => item.id === id);
|
|
||||||
if (!fileToDownload?.processedBlob) return;
|
|
||||||
const url = URL.createObjectURL(fileToDownload.processedBlob);
|
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `anonymized_${fileToDownload.name
|
a.download = "texte-anonymise.txt";
|
||||||
.split(".")
|
|
||||||
.slice(0, -1)
|
|
||||||
.join(".")}.txt`;
|
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreview = (id: string) => {
|
|
||||||
const fileToPreview = history.find((item) => item.id === id);
|
|
||||||
if (fileToPreview?.anonymizedText && fileToPreview.piiCount !== undefined) {
|
|
||||||
setAnonymizedResult({
|
|
||||||
text: fileToPreview.anonymizedText,
|
|
||||||
piiCount: fileToPreview.piiCount,
|
|
||||||
});
|
|
||||||
setShowPresidioModal(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusInfo = (item: ProcessedFile) => {
|
|
||||||
switch (item.status) {
|
|
||||||
case "completed":
|
|
||||||
return {
|
|
||||||
icon: <ShieldCheck className="h-4 w-4 text-white" />,
|
|
||||||
color: "bg-[#061717]",
|
|
||||||
};
|
|
||||||
case "error":
|
|
||||||
return {
|
|
||||||
icon: <AlertCircle className="h-4 w-4 text-white" />,
|
|
||||||
color: "bg-[#F7AB6E]",
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
icon: (
|
|
||||||
<div className="h-2 w-2 rounded-full bg-[#F7AB6E] animate-pulse" />
|
|
||||||
),
|
|
||||||
color: "bg-[#061717]",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadTestDocument = () => {
|
|
||||||
try {
|
|
||||||
// Créer un document de test avec des données PII fictives
|
|
||||||
const testContent = `Rapport pour la société belge "Solution Globale SPRL" (BCE : BE 0987.654.321).
|
|
||||||
Contact principal : M. Luc Dubois, né le 15/03/1975.
|
|
||||||
Son numéro de registre national est le 75.03.15-123.45.
|
|
||||||
Adresse : Avenue des Arts 56, 1000 Bruxelles.
|
|
||||||
Téléphone : +32 470 12 34 56. Email : luc.dubois@solutionglobale.be.
|
|
||||||
Le paiement de la facture a été effectué par carte VISA 4979 1234 5678 9012.
|
|
||||||
Le remboursement sera versé sur le compte IBAN BE12 3456 7890 1234, code SWIFT : GEBABEBB.`;
|
|
||||||
|
|
||||||
const testBlob = new Blob([testContent], { type: "text/plain" });
|
|
||||||
const testFile = new File([testBlob], "document-test.txt", {
|
|
||||||
type: "text/plain",
|
|
||||||
});
|
|
||||||
|
|
||||||
setFile(testFile);
|
|
||||||
setError(null);
|
|
||||||
} catch {
|
|
||||||
setError("Erreur lors du chargement du document de test");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const processFile = async () => {
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
setIsProcessing(true);
|
|
||||||
setProgress(0);
|
|
||||||
setError(null);
|
|
||||||
const fileId = `${Date.now()}-${file.name}`;
|
|
||||||
|
|
||||||
setHistory((prev) => [
|
|
||||||
{
|
|
||||||
id: fileId,
|
|
||||||
name: file.name,
|
|
||||||
status: "processing",
|
|
||||||
timestamp: new Date(),
|
|
||||||
originalSize: formatFileSize(file.size),
|
|
||||||
},
|
|
||||||
...prev,
|
|
||||||
]);
|
|
||||||
|
|
||||||
setFile(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
setProgress(25);
|
|
||||||
|
|
||||||
const response = await fetch("/api/process-document", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
setProgress(75);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
result.error || "Une erreur est survenue lors du traitement."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { anonymizedText, piiCount } = result;
|
|
||||||
|
|
||||||
const processedBlob = new Blob([anonymizedText], {
|
|
||||||
type: "text/plain;charset=utf-8",
|
|
||||||
});
|
|
||||||
|
|
||||||
setHistory((prev) =>
|
|
||||||
prev.map((item) =>
|
|
||||||
item.id === fileId
|
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
status: "completed",
|
|
||||||
processedSize: formatFileSize(processedBlob.size),
|
|
||||||
piiCount,
|
|
||||||
processedBlob,
|
|
||||||
anonymizedText,
|
|
||||||
}
|
|
||||||
: item
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setProgress(100);
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage =
|
|
||||||
err instanceof Error
|
|
||||||
? err.message
|
|
||||||
: "Une erreur inconnue est survenue.";
|
|
||||||
setError(errorMessage);
|
|
||||||
setHistory((prev) =>
|
|
||||||
prev.map((item) =>
|
|
||||||
item.id === fileId ? { ...item, status: "error", errorMessage } : item
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
setTimeout(() => setProgress(0), 1000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen bg-[#061717] flex flex-col md:flex-row overflow-hidden">
|
<div className="min-h-screen bg-[#092727]">
|
||||||
<aside className="w-full md:w-80 md:flex-shrink-0 bg-[#061717] border-b-4 md:border-b-0 md:border-r-4 border-white flex flex-col shadow-[4px_0_0_0_black]">
|
{" "}
|
||||||
<div className="p-4 border-b-4 border-white bg-[#F7AB6E] shadow-[0_4px_0_0_black] flex items-center justify-between">
|
{/* Fond vert foncé */}
|
||||||
<div className="flex items-center gap-3">
|
{/* Header */}
|
||||||
<Image
|
<div className="bg-[#092727] border-b border-white border-opacity-20">
|
||||||
src="/logo.svg"
|
{" "}
|
||||||
alt="LeCercle.IA Logo"
|
{/* Header vert foncé avec bordure blanche */}
|
||||||
width={32}
|
<div className="max-w-6xl mx-auto px-4 py-6">
|
||||||
height={32}
|
<h1 className="text-3xl font-bold text-white text-center mb-2">
|
||||||
/>
|
{" "}
|
||||||
<h2 className="text-lg font-black text-white uppercase tracking-wide">
|
{/* Titre en blanc */}
|
||||||
Historique
|
OUTIL D'ANONYMISATION DE DONNÉES
|
||||||
</h2>
|
</h1>
|
||||||
</div>
|
|
||||||
{history.length > 0 && (
|
|
||||||
<Button
|
|
||||||
onClick={clearHistory}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="bg-[#061717] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
</div>
|
||||||
{history.length > 0 ? (
|
{/* Main Content */}
|
||||||
history.map((item) => {
|
<div className="max-w-6xl mx-auto px-4 py-8">
|
||||||
const status = getStatusInfo(item);
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
return (
|
{/* Left Panel - Source */}
|
||||||
<div
|
<div className="space-y-4">
|
||||||
key={item.id}
|
{/* Tabs */}
|
||||||
className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 transition-all"
|
<div className="flex border-b border-white border-opacity-20">
|
||||||
>
|
<button
|
||||||
<div className="flex items-start justify-between">
|
onClick={() => setActiveTab("text")}
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||||
<div
|
activeTab === "text"
|
||||||
className={`flex-shrink-0 w-7 h-7 border-2 border-white shadow-[2px_2px_0_0_black] flex items-center justify-center ${status.color}`}
|
? "border-[#f7ab6e] bg-[#f7ab6e] text-[#092727]"
|
||||||
>
|
: "border-transparent text-white hover:text-[#f7ab6e]"
|
||||||
{status.icon}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p
|
|
||||||
className="text-sm font-black text-white uppercase truncate"
|
|
||||||
title={item.name}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</p>
|
|
||||||
{item.status === "completed" && (
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<span className="text-xs font-bold text-white/70">
|
|
||||||
{item.originalSize} → {item.processedSize}
|
|
||||||
</span>
|
|
||||||
<Badge className="bg-[#F7AB6E] text-white border-2 border-white shadow-[2px_2px_0_0_black] font-black uppercase text-[10px] px-1.5 py-0">
|
|
||||||
{item.piiCount} PII
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.status === "error" && (
|
|
||||||
<p className="text-xs font-bold text-[#F7AB6E] mt-1 uppercase">
|
|
||||||
Erreur
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{item.status === "processing" && (
|
|
||||||
<p className="text-xs font-bold text-[#F7AB6E] mt-1 uppercase">
|
|
||||||
Traitement...
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 ml-2">
|
|
||||||
{item.status === "completed" && item.anonymizedText && (
|
|
||||||
<Button
|
|
||||||
onClick={() => handlePreview(item.id)}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 bg-[#061717] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
|
||||||
>
|
|
||||||
<Eye className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{item.status === "completed" && (
|
|
||||||
<Button
|
|
||||||
onClick={() => handleDownloadTxt(item.id)}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 bg-[#061717] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
|
||||||
>
|
|
||||||
<Download className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
onClick={() => removeFromHistory(item.id)}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 bg-[#F7AB6E] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-10 px-4 flex flex-col justify-center h-full">
|
|
||||||
<div className="w-14 h-14 bg-[#F7AB6E] border-4 border-white shadow-[6px_6px_0_0_black] mx-auto mb-4 flex items-center justify-center">
|
|
||||||
<FileText className="h-7 w-7 text-white" />
|
|
||||||
</div>
|
|
||||||
<p className="text-white font-black text-base uppercase">
|
|
||||||
Aucun Document
|
|
||||||
</p>
|
|
||||||
<p className="text-white/70 font-bold mt-1 text-xs">
|
|
||||||
Vos fichiers apparaîtront ici.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main className="flex-1 flex flex-col items-center justify-center p-4 md:p-6 bg-[#061717] overflow-y-auto">
|
|
||||||
<div className="w-full max-w-xl flex flex-col ">
|
|
||||||
<header className="text-center ">
|
|
||||||
<div className="inline-block p-2 bg-[#F7AB6E] border-1 border-white shadow-[6px_6px_0_0_black] mb-3">
|
|
||||||
<Lock className="h-8 w-8 text-white" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl md:text-4xl font-black text-white uppercase tracking-tighter mb-1">
|
|
||||||
LeCercle.IA
|
|
||||||
</h1>
|
|
||||||
<p className="text-base md:text-lg font-bold text-white/90 uppercase tracking-wider">
|
|
||||||
Anonymisation • RGPD • Sécurisé
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
<Card className="flex-grow my-4 bg-[#061717] border-4 border-white shadow-[10px_10px_0_0_black] flex flex-col">
|
|
||||||
<CardContent className="p-4 md:p-6 space-y-4 flex-grow flex flex-col">
|
|
||||||
<div
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
className={`relative border-4 border-dashed p-7 text-center transition-all duration-200 ${
|
|
||||||
isDragOver
|
|
||||||
? "border-[#F7AB6E] bg-white/5"
|
|
||||||
: file
|
|
||||||
? "border-[#F7AB6E] bg-white/5 border-solid"
|
|
||||||
: "border-white hover:border-[#F7AB6E] hover:bg-white/5"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Input
|
Texte Source
|
||||||
type="file"
|
</button>
|
||||||
onChange={handleFileChange}
|
<button
|
||||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
disabled
|
||||||
accept=".txt,.json,.csv,.pdf,.docx"
|
className="px-4 py-2 font-medium border-b-2 transition-colors border-transparent text-white opacity-50 cursor-not-allowed relative group"
|
||||||
/>
|
>
|
||||||
<div className="flex flex-col items-center justify-center space-y-4">
|
Saisir URL
|
||||||
<div
|
<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">
|
||||||
className={`w-14 h-14 border-4 border-white shadow-[5px_5px_0_0_black] flex items-center justify-center transition-all duration-200 ${
|
À venir
|
||||||
file ? "bg-[#F7AB6E]" : "bg-[#061717]"
|
</span>
|
||||||
}`}
|
</button>
|
||||||
>
|
</div>
|
||||||
{file ? (
|
|
||||||
<FileText className="h-7 w-7 text-white" />
|
|
||||||
) : (
|
|
||||||
<Upload className="h-7 w-7 text-white" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{file ? (
|
|
||||||
<div>
|
|
||||||
<p className="text-base font-black text-white uppercase">
|
|
||||||
{file.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-bold text-white/70">
|
|
||||||
{formatFileSize(file.size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<p className="text-lg font-black text-white uppercase tracking-wide">
|
|
||||||
Glissez votre document
|
|
||||||
</p>
|
|
||||||
<p className="text-base font-bold text-white/70 uppercase mt-1">
|
|
||||||
Ou cliquez ici
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bouton pour charger un document de test */}
|
{/* Content Area - même taille que droite */}
|
||||||
<div className="flex-grow flex items-center justify-center">
|
<div className="border border-white border-opacity-20 rounded-md bg-[#092727] h-[500px] flex flex-col">
|
||||||
<Button
|
{/* Zone de contenu - prend tout l'espace disponible */}
|
||||||
onClick={loadTestDocument}
|
<div className="flex-1 overflow-hidden">
|
||||||
disabled={isProcessing}
|
{activeTab === "text" ? (
|
||||||
className="bg-[#061717] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1 h-12 px-6 text-base font-black uppercase tracking-wide disabled:opacity-50"
|
<div className="h-full">
|
||||||
>
|
{/* Zone de texte - prend TOUT l'espace */}
|
||||||
<TestTube className="h-5 w-5 mr-2" />
|
<div className="h-full relative">
|
||||||
Charger Document de Test
|
{/* Placeholder conditionnel : affiché seulement si pas de texte ET pas de fichier uploadé */}
|
||||||
</Button>
|
{sourceText === "" && !uploadedFile && (
|
||||||
</div>
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
{isProcessing && (
|
{/* Affichage du fichier uploadé dans la zone de texte */}
|
||||||
<div className="space-y-2">
|
{uploadedFile && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="absolute top-3 left-3 right-3 text-white text-sm pointer-events-none z-10">
|
||||||
<span className="text-base font-black text-white uppercase">
|
<div className="inline-flex items-center gap-2 bg-[#f7ab6e] rounded-md p-3 max-w-fit">
|
||||||
Traitement...
|
<Upload className="h-4 w-4 text-[#092727]" />
|
||||||
</span>
|
<span className="text-[#092727] text-sm font-medium">
|
||||||
<span className="text-lg font-black text-[#F7AB6E]">
|
{uploadedFile.name}
|
||||||
{Math.round(progress)}%
|
</span>
|
||||||
</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>
|
||||||
<div className="h-4 bg-[#061717] border-2 border-white shadow-[3px_3px_0_0_black]">
|
) : (
|
||||||
<div
|
<div className="h-full p-4">
|
||||||
className="h-full bg-[#F7AB6E] transition-all duration-300"
|
<input
|
||||||
style={{ width: `${progress}%` }}
|
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>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<div className="flex items-start gap-3 p-3 bg-[#F7AB6E] border-2 border-white shadow-[4px_4px_0_0_black]">
|
|
||||||
<AlertCircle className="h-5 w-5 text-white flex-shrink-0 mt-0.5" />
|
|
||||||
<p className="text-sm font-bold text-white uppercase">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-stretch gap-3 pt-2">
|
|
||||||
<Button
|
|
||||||
onClick={processFile}
|
|
||||||
disabled={!file || isProcessing}
|
|
||||||
className="flex-1 bg-[#F7AB6E] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1 h-12 text-base font-black uppercase tracking-wide disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isProcessing ? (
|
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-4 border-white border-t-transparent" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Zap className="h-5 w-5 mr-2" />
|
|
||||||
Anonymiser
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
{file && !isProcessing && (
|
|
||||||
<Button
|
|
||||||
onClick={clearFile}
|
|
||||||
className="h-12 w-12 p-0 bg-[#061717] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1"
|
|
||||||
>
|
|
||||||
<X className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
{/* Bouton télécharger fichier - au-dessus du disclaimer */}
|
||||||
<div className="grid grid-cols-3 gap-3 pb-4">
|
<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">
|
||||||
{ icon: Shield, title: "RGPD", subtitle: "Conforme" },
|
<Upload className="h-4 w-4 text-[#f7ab6e] group-hover:text-white transition-colors" />
|
||||||
{ icon: Clock, title: "Rapide", subtitle: "Local" },
|
Téléverser votre fichier
|
||||||
{ icon: Lock, title: "Sécurisé", subtitle: "Sans Serveur" },
|
<input
|
||||||
].map((item, index) => (
|
type="file"
|
||||||
<div
|
onChange={handleFileChange}
|
||||||
key={index}
|
accept=".txt,.pdf,.docx,.doc"
|
||||||
className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 text-center"
|
className="hidden"
|
||||||
>
|
/>
|
||||||
<div className="w-8 h-8 bg-[#F7AB6E] border-2 border-white shadow-[2px_2px_0_0_black] mx-auto mb-2 flex items-center justify-center">
|
</label>
|
||||||
<item.icon className="h-4 w-4 text-white" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-black text-white uppercase">
|
|
||||||
{item.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs font-bold text-white/70 uppercase">
|
|
||||||
{item.subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
|
||||||
{showPresidioModal && (
|
{/* Anonymize Button */}
|
||||||
<PresidioModal
|
<div className="flex justify-center mt-8">
|
||||||
anonymizedText={anonymizedResult?.text || null}
|
<button
|
||||||
piiCount={anonymizedResult?.piiCount || 0}
|
onClick={anonymizeData}
|
||||||
onClose={() => setShowPresidioModal(false)}
|
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>
|
</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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user