n8n anonyme

This commit is contained in:
nBiqoz
2025-06-13 12:57:08 +02:00
parent 386950b630
commit 7fe5523d80
7 changed files with 1726 additions and 132 deletions

View File

@@ -0,0 +1,42 @@
// components/MarkdownModal.tsx
import { X } from "lucide-react";
interface HtmlModalProps {
content: string | null;
onClose: () => void;
}
export default function MarkdownModal({ content, onClose }: HtmlModalProps) {
if (!content) return null;
return (
<div
className="fixed inset-0 bg-black bg-opacity-70 z-50 flex justify-center items-center p-4"
onClick={onClose}
>
<div
className="bg-[#061717] border-4 border-white shadow-[10px_10px_0_0_black] w-full max-w-3xl h-[80vh] flex flex-col relative"
onClick={(e) => e.stopPropagation()}
>
<header className="flex items-center justify-between p-4 border-b-4 border-white">
<h3 className="text-lg font-black text-white uppercase">
Aperçu du Document
</h3>
<button
onClick={onClose}
className="p-2 bg-[#F7AB6E] 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"
>
<X className="h-5 w-5" />
</button>
</header>
{/* Ce conteneur stylise le HTML propre qu'il reçoit */}
<div
className="preview-content p-6 overflow-y-auto w-full h-full"
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
</div>
);
}

View File

@@ -2,121 +2,96 @@
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* app/globals.css */
/* ======================================================= */
/* == STYLES MANUELS POUR L'APERÇU DU DOCUMENT DANS LA MODALE == */
/* ======================================================= */
/* Conteneur principal qui définit les styles par défaut */
.preview-content {
color: #d1d5db; /* Texte gris clair par défaut */
font-size: 16px;
line-height: 1.6;
}
body {
font-family: var(--font-sans);
/* --- TITRES --- */
.preview-content h1,
.preview-content h2,
.preview-content h3,
.preview-content h4 {
color: #f7ab6e; /* Couleur orange pour tous les titres */
font-weight: 700; /* Gras */
margin-top: 1.5em;
margin-bottom: 0.8em;
line-height: 1.2;
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
.preview-content h1 {
font-size: 2.5rem; /* Très grand */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
.preview-content h2 {
font-size: 2rem; /* Grand */
padding-bottom: 0.4em;
border-bottom: 1px solid rgba(255, 255, 255, 0.2); /* Ligne de séparation */
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
.preview-content h3 {
font-size: 1.5rem; /* Moyen */
}
.preview-content h4 {
font-size: 1.25rem; /* Petit */
}
/* --- TEXTE --- */
.preview-content p {
margin-bottom: 1em;
}
.preview-content strong {
color: #ffffff; /* Texte en gras bien blanc pour ressortir */
font-weight: 700;
}
/* --- LISTES À PUCES --- */
.preview-content ul {
list-style-type: disc; /* Activer les puces */
margin-left: 1.5em; /* Indentation */
margin-bottom: 1em;
}
.preview-content li {
margin-bottom: 0.5em;
padding-left: 0.5em;
}
/* Styliser la couleur de la puce elle-même ! */
.preview-content li::marker {
color: #f7ab6e;
}
/* --- TABLEAUX --- */
.preview-content table {
width: 100%;
margin-top: 1.5em;
border-collapse: collapse;
}
.preview-content th,
.preview-content td {
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 0.75em 1em;
text-align: left;
}
.preview-content th {
background-color: rgba(
255,
255,
255,
0.05
); /* Fond légèrement différent pour les en-têtes */
font-weight: 700;
color: #f7ab6e;
}

View File

@@ -8,14 +8,19 @@ 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
User, AtSign, MapPin, Cake, Home as HomeIcon, Venus, Phone, Building, Fingerprint, CreditCard, Check,
Eye // <-- Icône ajoutée
} from "lucide-react";
import MarkdownModal from "./components/MarkdownModal"; // <-- Composant de la modale ajouté
// Interfaces et Données
// === Interfaces et Données ===
// L'interface est mise à jour pour inclure le contenu texte pour l'aperçu
interface ProcessedFile {
id: string; name: string; status: 'processing' | 'completed' | 'error';
timestamp: Date; originalSize?: string; processedSize?: string;
piiCount?: number; errorMessage?: string; processedBlob?: Blob;
textContent?: string; // <-- Ajouté pour stocker le contenu du fichier
}
interface AnonymizationOptions { [key: string]: boolean; }
@@ -44,6 +49,8 @@ export default function Home() {
const [anonymizationOptions, setAnonymizationOptions] = useState<AnonymizationOptions>(
piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {})
);
// Nouvel état pour gérer la modale
const [modalContent, setModalContent] = useState<string | null>(null);
// === Fonctions utilitaires et Handlers ===
const handleOptionChange = (id: string) => setAnonymizationOptions(prev => ({ ...prev, [id]: !prev[id] }));
@@ -55,7 +62,27 @@ export default function Home() {
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(); };
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();
};
// Nouvelle fonction pour gérer l'ouverture de l'aperçu
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: <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]' }; } };
// === Fonction principale pour l'appel à n8n ===
@@ -75,9 +102,7 @@ export default function Home() {
try {
const formData = new FormData();
// On attache le fichier binaire
formData.append('file', file);
// On attache les options cochées sous forme de chaîne JSON
formData.append('options', JSON.stringify(anonymizationOptions));
setProgress(30);
@@ -91,9 +116,13 @@ export default function Home() {
const processedBlob = await response.blob();
const piiCount = parseInt(response.headers.get('X-Pii-Count') || '0', 10);
// On lit le contenu du blob en texte pour pouvoir l'afficher
const textContent = await processedBlob.text();
setProgress(90);
setHistory(prev => prev.map(item => item.id === fileId ? { ...item, status: 'completed', processedSize: formatFileSize(processedBlob.size), piiCount, processedBlob } : item));
// On met à jour l'historique avec toutes les informations, y compris le contenu texte
setHistory(prev => prev.map(item => item.id === fileId ? { ...item, status: 'completed', processedSize: formatFileSize(processedBlob.size), piiCount, processedBlob, textContent } : item));
setProgress(100);
} catch (err) {
@@ -127,7 +156,16 @@ export default function Home() {
) : (
history.map((item) => {
const status = getStatusInfo(item);
return (<div key={item.id} className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 transition-all"><div className="flex items-start justify-between"><div className="flex items-center gap-3 min-w-0"><div 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}`}>{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' && (<Button onClick={() => handleDownload(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>);
return (<div key={item.id} className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 transition-all"><div className="flex items-start justify-between"><div className="flex items-center gap-3 min-w-0"><div 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}`}>{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">
{/* Le nouveau bouton "Aperçu" est ajouté ici */}
{item.status === 'completed' && item.textContent && (
<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={() => handleDownload(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>
@@ -169,6 +207,12 @@ export default function Home() {
</div>
</div>
</main>
{/* Le composant de la modale est rendu ici, en dehors du flux principal */}
<MarkdownModal
content={modalContent}
onClose={() => setModalContent(null)}
/>
</div>
);
}

1532
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,11 +22,14 @@
"pdfjs-dist": "^5.3.31",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^20",
"@types/pdfjs-dist": "^2.10.377",
"@types/react": "^19",

View File

@@ -1,11 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};

19
tailwind.config.ts Normal file
View File

@@ -0,0 +1,19 @@
// tailwind.config.ts
import type { Config } from "tailwindcss";
import typography from "@tailwindcss/typography";
const config: Config = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [
typography, // C'est ici qu'on active le plugin pour la classe 'prose'
],
};
export default config;