"use client"; import { useState, useCallback } from "react"; import Image from "next/image"; 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, User, AtSign, MapPin, Cake, Home as HomeIcon, Venus, Phone, Building, Fingerprint, CreditCard, Check, Eye, } from "lucide-react"; import MarkdownModal from "./components/MarkdownModal"; // === Interfaces et Données === interface ProcessedFile { id: string; name: string; status: "processing" | "completed" | "error"; timestamp: Date; originalSize?: string; processedSize?: string; piiCount?: number; errorMessage?: string; processedBlob?: Blob; // textContent est maintenant un tableau de strings, une par page. textContent?: string[]; } interface AnonymizationOptions { [key: string]: boolean; } const piiOptions = [ { id: "name", label: "Nom & Prénom", icon: User }, { id: "email", label: "Email", icon: AtSign }, { id: "location", label: "Lieu", icon: MapPin }, { id: "birthdate", label: "Date de naissance", icon: Cake }, { id: "address", label: "Adresse", icon: HomeIcon }, { id: "gender", label: "Genre / Sexe", icon: Venus }, { id: "phone", label: "Téléphone", icon: Phone }, { id: "organization", label: "Entreprise", icon: Building }, { id: "idNumber", label: "N° Identification", icon: Fingerprint }, { id: "financial", label: "Données financières", icon: CreditCard }, ]; export default function Home() { const [file, setFile] = useState(null); const [isProcessing, setIsProcessing] = useState(false); const [progress, setProgress] = useState(0); const [isDragOver, setIsDragOver] = useState(false); const [history, setHistory] = useState([]); const [error, setError] = useState(null); const [anonymizationOptions, setAnonymizationOptions] = useState( piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {}) ); // L'état de la modale attend maintenant un tableau de strings ou null const [modalContent, setModalContent] = useState(null); const handleOptionChange = (id: string) => setAnonymizationOptions((prev) => ({ ...prev, [id]: !prev[id] })); const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files?.length) { setFile(event.target.files[0]); setError(null); } }; const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); if (e.dataTransfer.files?.length) { setFile(e.dataTransfer.files[0]); setError(null); } }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); const formatFileSize = (bytes: number): string => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; const clearFile = () => { setFile(null); setError(null); }; const clearHistory = () => setHistory([]); const removeFromHistory = (id: string) => setHistory((prev) => prev.filter((item) => item.id !== id)); const handleDownload = (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"); a.href = url; a.download = `anonymized_${ fileToDownload.name.split(".")[0] || "file" }.txt`; a.click(); URL.revokeObjectURL(url); a.remove(); }; // Met à jour le contenu de la modale avec le tableau de pages const handlePreview = (id: string) => { const fileToPreview = history.find((item) => item.id === id); if (fileToPreview?.textContent) { setModalContent(fileToPreview.textContent); } }; const getStatusInfo = (item: ProcessedFile) => { switch (item.status) { case "completed": return { icon: , color: "bg-[#061717]", }; case "error": return { icon: , color: "bg-[#F7AB6E]", }; default: return { icon: (
), color: "bg-[#061717]", }; } }; // === Fonction principale mise à jour pour gérer le format JSON === const processFile = async () => { if (!file) return; const n8nWebhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL; if (!n8nWebhookUrl) { setError("L'URL du service est manquante. Contactez l'administrateur."); 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); setProgress(10); try { const formData = new FormData(); formData.append("file", file); formData.append("options", JSON.stringify(anonymizationOptions)); setProgress(30); const response = await fetch(n8nWebhookUrl, { method: "POST", body: formData, }); setProgress(70); if (!response.ok) { const errorResult = await response .json() .catch(() => ({ error: `Erreur serveur [${response.status}]` })); throw new Error(errorResult.error || `Échec du traitement.`); } // On parse la réponse JSON au lieu de lire un blob const result = await response.json(); // Validation de la structure de la réponse if ( !result.anonymizedDocument || !Array.isArray(result.anonymizedDocument.pages) ) { throw new Error( "Format de réponse invalide du service d'anonymisation." ); } const textContent: string[] = result.anonymizedDocument.pages; const piiCount: number = result.anonymizedDocument.piiCount || 0; // On crée un blob pour le téléchargement .txt en joignant les pages const fullText = textContent.join("\n\n--- Page Suivante ---\n\n"); const processedBlob = new Blob([fullText], { type: "text/plain;charset=utf-8", }); setProgress(90); // On met à jour l'historique avec le tableau de pages setHistory((prev) => prev.map((item) => item.id === fileId ? { ...item, status: "completed", processedSize: formatFileSize(processedBlob.size), piiCount, processedBlob, textContent, } : 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); } }; // === Rendu du composant (JSX - inchangé) === return (

LeCercle.IA

Anonymisation • RGPD • Sécurisé

{file ? ( ) : ( )}
{file ? (

{file.name}

{formatFileSize(file.size)}

) : (

Glissez votre document

Ou cliquez ici

)}

Options d'Anonymisation

{piiOptions.map((option) => ( ))}
{isProcessing && (
Traitement... {Math.round(progress)}%
)} {error && (

{error}

)}
{file && !isProcessing && ( )}
{[ { icon: Shield, title: "RGPD", subtitle: "Conforme" }, { icon: Clock, title: "Rapide", subtitle: "Local" }, { icon: Lock, title: "Sécurisé", subtitle: "Sans Serveur" }, ].map((item, index) => (

{item.title}

{item.subtitle}

))}
setModalContent(null)} />
); }