Files
Anonyme/app/components/FileUploadComponent.tsx
2025-08-01 14:30:25 +02:00

427 lines
19 KiB
TypeScript

import {
Upload,
FileText,
AlertTriangle,
Shield,
Copy,
Download,
} from "lucide-react";
import { SampleTextComponent } from "./SampleTextComponent";
import { SupportedDataTypes } from "./SupportedDataTypes";
import { AnonymizationInterface } from "./AnonymizationInterface";
import { highlightEntities } from "../utils/highlightEntities";
interface FileUploadComponentProps {
uploadedFile: File | null;
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => 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;
copyToClipboard?: () => void;
downloadText?: () => void;
isExampleLoaded?: boolean; // NOUVEAU
setIsExampleLoaded?: (loaded: boolean) => void; // NOUVEAU
}
export const FileUploadComponent = ({
uploadedFile,
handleFileChange,
sourceText,
setSourceText,
setUploadedFile,
setFileContent,
onAnonymize,
isProcessing = false,
canAnonymize = false,
isLoadingFile = false,
onRestart,
outputText,
copyToClipboard,
downloadText,
setIsExampleLoaded, // NOUVEAU - Ajouté ici
}: FileUploadComponentProps) => {
// On passe en preview seulement si :
// 1. Un fichier est uploadé OU
// 2. On a un résultat d'anonymisation
// (On retire isExampleLoaded pour permettre l'édition du texte d'exemple)
if (uploadedFile || outputText) {
return (
<div className="w-full flex flex-col space-y-6">
{/* Si on a un résultat, afficher 2 blocs côte à côte */}
{outputText ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Preview du document original */}
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
<div className="bg-orange-50 border-b border-orange-200 px-4 sm:px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<FileText className="h-4 w-4 sm:h-5 sm:w-5 text-orange-600" />
</div>
<div className="min-w-0 flex-1">
{uploadedFile ? (
<p className="text-xs sm:text-sm text-orange-600 truncate">
{uploadedFile.name} {" "}
{(uploadedFile.size / 1024).toFixed(1)} KB
</p>
) : (
<p className="text-xs sm:text-sm text-orange-600">
Demo - Exemple de texte
</p>
)}
</div>
</div>
</div>
</div>
<div className="p-1">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4 max-h-72 overflow-y-auto overflow-x-hidden">
<pre className="text-xs sm:text-sm text-gray-700 whitespace-pre-wrap font-mono break-words overflow-wrap-anywhere">
{sourceText || "Aucun contenu à afficher"}
</pre>
</div>
</div>
</div>
{/* Bloc résultat anonymisé */}
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
<div className="bg-green-50 border-b border-green-200 px-4 sm:px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-green-100 rounded-lg flex items-center justify-center">
<Shield className="h-4 w-4 sm:h-5 sm:w-5 text-green-600" />
</div>
<div className="min-w-0 flex-1">
<p className="text-xs sm:text-sm text-green-600">
Document anonymisé
</p>
</div>
</div>
{/* Boutons d'action */}
<div className="flex items-center gap-2">
{copyToClipboard && (
<button
onClick={copyToClipboard}
className="p-2 text-green-600 hover:text-green-700 hover:bg-green-100 rounded-lg transition-colors duration-200"
title="Copier le texte"
>
<Copy className="h-4 w-4" />
</button>
)}
{downloadText && (
<button
onClick={downloadText}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded-lg text-xs font-medium transition-colors duration-200 flex items-center space-x-1"
title="Télécharger le fichier"
>
<Download className="h-3 w-3" />
<span className="hidden sm:inline">Télécharger</span>
</button>
)}
</div>
</div>
</div>
<div className="p-1">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4 max-h-72 overflow-y-auto overflow-x-hidden">
<div className="text-xs sm:text-sm text-gray-700 whitespace-pre-wrap break-words overflow-wrap-anywhere leading-relaxed">
{highlightEntities(
outputText || "Aucun contenu à afficher"
)}
</div>
</div>
</div>
</div>
</div>
) : (
/* Preview normal quand pas de résultat */
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden">
<div className="bg-orange-50 border-b border-orange-200 px-4 sm:px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<FileText className="h-4 w-4 sm:h-5 sm:w-5 text-orange-600" />
</div>
<div className="min-w-0 flex-1">
{uploadedFile ? (
<p className="text-xs sm:text-sm text-orange-600 truncate">
{uploadedFile.name} {" "}
{(uploadedFile.size / 1024).toFixed(1)} KB
</p>
) : (
<p className="text-xs sm:text-sm text-orange-600">
Demo - Exemple de texte
</p>
)}
</div>
</div>
</div>
</div>
<div className="p-2 ">
{/* Zone de texte avec limite de hauteur et scroll */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4 max-h-48 overflow-y-auto overflow-x-hidden">
{isLoadingFile ? (
<div className="flex items-center justify-center py-8">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-5 w-5 sm:h-6 sm:w-6 border-b-2 border-[#f7ab6e]"></div>
<span className="text-xs sm:text-sm text-gray-600">
Chargement du fichier en cours...
</span>
</div>
</div>
) : (
<pre className="text-xs sm:text-sm text-gray-700 whitespace-pre-wrap font-mono break-words overflow-wrap-anywhere">
{sourceText || "Aucun contenu à afficher"}
</pre>
)}
</div>
{/* Disclaimer déplacé en dessous du texte */}
<div className="mt-4">
<div className="flex items-start gap-2 p-3 bg-[#f7ab6e] bg-opacity-10 border border-[#f7ab6e] border-opacity-30 rounded-lg">
<AlertTriangle className="h-4 w-4 text-[#f7ab6e] mt-0.5 flex-shrink-0" />
<p className="text-[10px] sm:text-[11px] text-[#092727] leading-relaxed">
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>
)}
{/* Boutons d'action - Responsive mobile */}
{canAnonymize && !isLoadingFile && (
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4">
{/* Bouton Anonymiser en premier */}
{onAnonymize && (
<button
onClick={onAnonymize}
disabled={isProcessing}
className="w-full sm:w-auto bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 disabled:opacity-50 disabled:cursor-not-allowed text-white px-6 py-3 rounded-lg text-sm font-medium transition-colors duration-300 flex items-center justify-center space-x-3"
>
{isProcessing ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Anonymisation en cours...</span>
</>
) : (
<>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<span>Anonymiser mes données</span>
</>
)}
</button>
)}
{/* Bouton Recommencer */}
{onRestart && (
<button
onClick={onRestart}
className="w-full sm:w-auto bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg text-sm font-medium transition-colors duration-300 flex items-center justify-center space-x-2"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<span>Recommencer</span>
</button>
)}
</div>
)}
{/* Affichage conditionnel : Interface d'anonymisation OU Types de données supportées */}
{isProcessing || outputText ? (
<AnonymizationInterface
isProcessing={isProcessing}
outputText={outputText}
sourceText={sourceText}
/>
) : (
<SupportedDataTypes />
)}
</div>
);
}
return (
<div className="w-full flex flex-col space-y-3">
{/* Deux colonnes côte à côte */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Colonne gauche - Zone de texte */}
<div className="border-2 border-dashed border-[#092727] rounded-xl bg-gray-50 hover:bg-gray-100 hover:border-[#0a3030] transition-all duration-300">
<div className="p-3 sm:p-4">
{/* Header avec icône */}
<div className="flex items-center justify-center mb-2">
<div className="w-8 h-8 bg-[#f7ab6e] rounded-full flex items-center justify-center">
<FileText className="h-4 w-4 text-white" />
</div>
</div>
{/* Titre */}
<h3 className="text-sm font-semibold text-[#092727] mb-1 text-center">
Saisissez votre texte
</h3>
<p className="text-xs text-[#092727] opacity-80 mb-2 text-center">
Tapez ou collez votre texte ici
</p>
{/* Zone de texte éditable */}
<div className="relative border-2 border-gray-200 rounded-lg bg-white focus-within:border-[#f7ab6e] focus-within:ring-1 focus-within:ring-[#f7ab6e]/20 transition-all duration-300">
{/* Zone pour le texte - SANS overflow */}
<div className="h-40 p-2 pb-6 relative"> {/* Ajout de pb-6 pour le compteur */}
{/* Placeholder personnalisé avec lien cliquable */}
{!sourceText && (
<div className="absolute inset-2 text-gray-400 text-xs leading-relaxed pointer-events-none">
<span>Commencez à taper du texte, ou&nbsp;</span>
<SampleTextComponent
setSourceText={setSourceText}
setFileContent={setFileContent}
setUploadedFile={setUploadedFile}
setIsExampleLoaded={setIsExampleLoaded}
variant="link"
/>
</div>
)}
<textarea
value={sourceText}
onChange={(e) => setSourceText(e.target.value)}
placeholder="" // Placeholder vide car on utilise le custom
className="w-full h-full border-none outline-none resize-none text-[#092727] text-xs leading-relaxed bg-transparent overflow-y-auto"
style={{
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
}}
/>
{/* Compteur de caractères en bas à gauche */}
<div className="absolute bottom-1 left-2 text-gray-400 text-xs pointer-events-none">
{sourceText.length} caractères
</div>
</div>
{/* Barre du bas avec sélecteur et bouton */}
<div className="flex items-center justify-between p-2 border-t border-gray-200 bg-gray-50">
{/* Sélecteur de type d'anonymisation */}
<div className="relative">
<select
className="appearance-none bg-white border border-gray-300 text-gray-700 text-xs rounded-md pl-3 pr-8 py-2 focus:outline-none focus:ring-1 focus:ring-[#f7ab6e] focus:border-[#f7ab6e] transition-colors duration-200"
>
<option>Information Personnellement Identifiable (PII)</option>
<option disabled style={{ color: 'lightgray' }}>
PII + Données Business (En développement)
</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
</div>
</div>
{/* Bouton Anonymiser */}
<button
onClick={onAnonymize}
disabled={isProcessing || !sourceText.trim()}
className="bg-[#f7ab6e] hover:bg-[#f7ab6e]/90 disabled:opacity-50 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg text-xs font-medium transition-colors duration-300 flex items-center space-x-2 shadow-sm"
title={
sourceText.trim()
? "Anonymiser les données"
: "Saisissez du texte pour anonymiser"
}
>
{isProcessing ? (
<>
<div className="animate-spin rounded-full h-3 w-3 border-b border-white"></div>
<span>Traitement...</span>
</>
) : (
<>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<span>Anonymisez les données</span>
</>
)}
</button>
</div>
</div>
</div>
</div>
{/* Colonne droite - Zone upload */}
<div className="border-2 border-dashed border-[#092727] rounded-xl bg-gray-50 hover:bg-gray-100 hover:border-[#0a3030] transition-all duration-300">
<label className="flex flex-col items-center justify-center cursor-pointer group p-3 sm:p-4 h-full min-h-[200px]">
{/* Upload Icon */}
<div className="w-10 h-10 bg-[#092727] group-hover:bg-[#0a3030] rounded-full flex items-center justify-center mb-3 transition-colors duration-300">
<Upload className="h-5 w-5 text-white" />
</div>
{/* Titre */}
<h3 className="text-sm font-semibold text-[#092727] mb-1 group-hover:text-[#0a3030] transition-colors duration-300 text-center">
Déposez votre fichier ici
</h3>
<p className="text-xs text-[#092727] opacity-80 mb-3 text-center group-hover:opacity-90 transition-opacity duration-300">
ou cliquez pour sélectionner
</p>
{/* File Info */}
<div className="flex flex-col items-center gap-1 text-xs text-[#092727] opacity-60">
<span>📄 Fichiers TXT, PDF</span>
<span>Max 5MB</span>
</div>
{/* Hidden Input */}
<input
type="file"
onChange={handleFileChange}
accept=".txt,.pdf"
className="hidden"
/>
</label>
</div>
</div>
{/* Supported Data Types */}
<SupportedDataTypes />
</div>
);
};