good presidio
This commit is contained in:
161
app/api/process-document/route.ts
Normal file
161
app/api/process-document/route.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { NextResponse, type NextRequest } from "next/server";
|
||||||
|
import pdf from "pdf-parse";
|
||||||
|
import mammoth from "mammoth";
|
||||||
|
import { type File } from "buffer";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const formData = await req.formData();
|
||||||
|
const file = formData.get("file") as File | null;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Aucun fichier reçu." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileContent = "";
|
||||||
|
const fileType = file.type;
|
||||||
|
|
||||||
|
if (fileType === "application/pdf") {
|
||||||
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
const data = await pdf(buffer);
|
||||||
|
fileContent = data.text;
|
||||||
|
} else if (
|
||||||
|
fileType ===
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
) {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const result = await mammoth.extractRawText({ arrayBuffer });
|
||||||
|
fileContent = result.value;
|
||||||
|
} else {
|
||||||
|
fileContent = await file.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzerConfig = {
|
||||||
|
text: fileContent,
|
||||||
|
language: "fr",
|
||||||
|
ad_hoc_recognizers: [
|
||||||
|
{
|
||||||
|
name: "BelgianNRNRecognizer",
|
||||||
|
supported_entity: "BE_NATIONAL_REGISTER_NUMBER",
|
||||||
|
supported_language: "fr",
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
name: "NRN_Pattern",
|
||||||
|
regex:
|
||||||
|
"\\b(?:[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01]))-?\\d{3}\\.?\\d{2}\\b",
|
||||||
|
score: 1.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context: ["registre national", "nrn", "niss"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BelgianEnterpriseRecognizer",
|
||||||
|
supported_entity: "BE_ENTERPRISE_NUMBER",
|
||||||
|
supported_language: "fr",
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
name: "BTW_Pattern",
|
||||||
|
regex: "\\bBE\\s?0\\d{3}\\.\\d{3}\\.\\d{3}\\b",
|
||||||
|
score: 0.95,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context: ["entreprise", "btw", "tva"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IBANRecognizer",
|
||||||
|
supported_entity: "IBAN",
|
||||||
|
supported_language: "fr",
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
name: "IBAN_Pattern",
|
||||||
|
regex: "\\b[A-Z]{2}\\d{2}\\s?(?:\\d{4}\\s?){4,7}\\d{1,4}\\b",
|
||||||
|
score: 0.95,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context: ["iban", "compte", "bancaire"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PhoneRecognizer",
|
||||||
|
supported_entity: "PHONE_NUMBER",
|
||||||
|
supported_language: "fr",
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
name: "Phone_Pattern",
|
||||||
|
regex:
|
||||||
|
"\\b(?:(?:\\+|00)(?:32|33|352)|0)\\s?[1-9](?:[\\s.-]?\\d{2}){3,4}\\b",
|
||||||
|
score: 0.8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context: ["téléphone", "tel", "mobile", "gsm"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmailRecognizer",
|
||||||
|
supported_entity: "EMAIL_ADDRESS",
|
||||||
|
supported_language: "fr",
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
name: "Email_Pattern",
|
||||||
|
regex: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b",
|
||||||
|
score: 1.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context: ["email", "courriel", "adresse électronique"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const presidioAnalyzerUrl =
|
||||||
|
"http://ocs00s000ssow8kssossocco.51.68.233.212.sslip.io/analyze";
|
||||||
|
|
||||||
|
const analyzeResponse = await fetch(presidioAnalyzerUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(analyzerConfig),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!analyzeResponse.ok) {
|
||||||
|
const errorBody = await analyzeResponse.text();
|
||||||
|
throw new Error(
|
||||||
|
`Erreur de l'analyseur Presidio (${analyzeResponse.status}): ${errorBody}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzerResults = await analyzeResponse.json();
|
||||||
|
|
||||||
|
const presidioAnonymizerUrl =
|
||||||
|
"http://r8gko4kcwwk4sso40cc0gkg8.51.68.233.212.sslip.io/anonymize";
|
||||||
|
|
||||||
|
const anonymizeResponse = await fetch(presidioAnonymizerUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: fileContent,
|
||||||
|
analyzer_results: analyzerResults,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!anonymizeResponse.ok) {
|
||||||
|
const errorBody = await anonymizeResponse.text();
|
||||||
|
throw new Error(
|
||||||
|
`Erreur de l'anonymiseur Presidio (${anonymizeResponse.status}): ${errorBody}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const anonymizerResult = await anonymizeResponse.json();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
anonymizedText: anonymizerResult.text,
|
||||||
|
piiCount: analyzerResults.length,
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage =
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: "Une erreur inconnue est survenue sur le serveur.";
|
||||||
|
console.error("API Error:", err);
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
// app/components/MarkdownModal.tsx
|
|
||||||
|
|
||||||
import { X, Download } from "lucide-react";
|
|
||||||
import { jsPDF } from "jspdf";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { type PageObject } from "../page"; // Importer le type depuis la page principale
|
|
||||||
|
|
||||||
interface HtmlModalProps {
|
|
||||||
content: PageObject[] | null; // Utilise le type PageObject importé
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moteur de rendu HTML vers PDF, robuste et récursif
|
|
||||||
const renderHtmlOnPdfPage = (pdf: jsPDF, htmlContent: string) => {
|
|
||||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
|
||||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
|
||||||
const margin = 15;
|
|
||||||
let cursorY = margin;
|
|
||||||
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.innerHTML = htmlContent;
|
|
||||||
|
|
||||||
const processNode = (
|
|
||||||
node: ChildNode,
|
|
||||||
currentStyle: { fontStyle: "normal" | "bold" | "italic"; fontSize: number }
|
|
||||||
) => {
|
|
||||||
if (cursorY > pageHeight - margin) {
|
|
||||||
pdf.addPage();
|
|
||||||
cursorY = margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStyle = { ...currentStyle };
|
|
||||||
let spacingAfter = 0;
|
|
||||||
|
|
||||||
switch (node.nodeName) {
|
|
||||||
case "H1":
|
|
||||||
nextStyle.fontSize = 22;
|
|
||||||
nextStyle.fontStyle = "bold";
|
|
||||||
spacingAfter = 8;
|
|
||||||
break;
|
|
||||||
case "H2":
|
|
||||||
nextStyle.fontSize = 18;
|
|
||||||
nextStyle.fontStyle = "bold";
|
|
||||||
spacingAfter = 6;
|
|
||||||
break;
|
|
||||||
case "H3":
|
|
||||||
nextStyle.fontSize = 14;
|
|
||||||
nextStyle.fontStyle = "bold";
|
|
||||||
spacingAfter = 4;
|
|
||||||
break;
|
|
||||||
case "P":
|
|
||||||
nextStyle.fontSize = 10;
|
|
||||||
nextStyle.fontStyle = "normal";
|
|
||||||
spacingAfter = 4;
|
|
||||||
break;
|
|
||||||
case "UL":
|
|
||||||
spacingAfter = 4;
|
|
||||||
break;
|
|
||||||
case "STRONG":
|
|
||||||
case "B":
|
|
||||||
nextStyle.fontStyle = "bold";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
|
|
||||||
pdf.setFontSize(nextStyle.fontSize);
|
|
||||||
pdf.setFont("Helvetica", nextStyle.fontStyle);
|
|
||||||
const textLines = node.textContent.split("\n");
|
|
||||||
textLines.forEach((line) => {
|
|
||||||
if (line.trim() === "") {
|
|
||||||
cursorY += nextStyle.fontSize * 0.4;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const splitText = pdf.splitTextToSize(line, pageWidth - margin * 2);
|
|
||||||
pdf.text(splitText, margin, cursorY);
|
|
||||||
cursorY += splitText.length * nextStyle.fontSize * 0.4;
|
|
||||||
});
|
|
||||||
} else if (node.nodeName === "UL") {
|
|
||||||
Array.from((node as HTMLUListElement).children).forEach((li) => {
|
|
||||||
const liText = `• ${li.textContent?.trim() || ""}`;
|
|
||||||
const lines = pdf.splitTextToSize(liText, pageWidth - margin * 2 - 5);
|
|
||||||
pdf.setFontSize(10);
|
|
||||||
pdf.setFont("Helvetica", "normal");
|
|
||||||
pdf.text(lines, margin + 5, cursorY);
|
|
||||||
cursorY += lines.length * 10 * 0.4 + 2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.childNodes.length > 0) {
|
|
||||||
node.childNodes.forEach((child) => processNode(child, nextStyle));
|
|
||||||
}
|
|
||||||
cursorY += spacingAfter;
|
|
||||||
};
|
|
||||||
|
|
||||||
container.childNodes.forEach((node) =>
|
|
||||||
processNode(node, { fontStyle: "normal", fontSize: 10 })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MarkdownModal({ content, onClose }: HtmlModalProps) {
|
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
|
||||||
if (!content) return null;
|
|
||||||
|
|
||||||
const handleDownloadPdf = async () => {
|
|
||||||
if (!content || content.length === 0) return;
|
|
||||||
setIsDownloading(true);
|
|
||||||
try {
|
|
||||||
const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" });
|
|
||||||
pdf.setFont("Helvetica", "normal");
|
|
||||||
|
|
||||||
// Mis à jour pour boucler sur les objets et passer le contenu HTML
|
|
||||||
content.forEach((page, index) => {
|
|
||||||
if (index > 0) pdf.addPage();
|
|
||||||
renderHtmlOnPdfPage(pdf, page.htmlContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
pdf.save("document_anonymise.pdf");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la génération du PDF:", error);
|
|
||||||
} finally {
|
|
||||||
setIsDownloading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mis à jour pour extraire le contenu HTML de chaque page avant de l'afficher
|
|
||||||
const previewHtml = content
|
|
||||||
.map((page) => page.htmlContent)
|
|
||||||
.join(
|
|
||||||
'<hr style="margin: 2.5rem 0; border-style: dashed; border-color: rgba(255,255,255,0.2);">'
|
|
||||||
);
|
|
||||||
|
|
||||||
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>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleDownloadPdf}
|
|
||||||
disabled={isDownloading}
|
|
||||||
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 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isDownloading ? (
|
|
||||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
||||||
) : (
|
|
||||||
<Download className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div
|
|
||||||
className="preview-content p-6 overflow-y-auto w-full h-full"
|
|
||||||
dangerouslySetInnerHTML={{ __html: previewHtml }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
155
app/components/PresidioModal.tsx
Normal file
155
app/components/PresidioModal.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { X, Download } from "lucide-react";
|
||||||
|
import { jsPDF } from "jspdf";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface PresidioModalProps {
|
||||||
|
anonymizedText: string | null;
|
||||||
|
piiCount: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderAnonymizedTextOnPdf = (pdf: jsPDF, anonymizedText: string) => {
|
||||||
|
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||||
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||||
|
const margin = 15;
|
||||||
|
let cursorY = margin;
|
||||||
|
|
||||||
|
const paragraphs = anonymizedText.split("\n\n").filter((p) => p.trim());
|
||||||
|
|
||||||
|
paragraphs.forEach((paragraph) => {
|
||||||
|
if (cursorY > pageHeight - margin - 20) {
|
||||||
|
pdf.addPage();
|
||||||
|
cursorY = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = paragraph.split(/(<[A-Z_]+>)/g);
|
||||||
|
|
||||||
|
parts.forEach((part) => {
|
||||||
|
if (part.match(/<[A-Z_]+>/)) {
|
||||||
|
pdf.setFontSize(10);
|
||||||
|
pdf.setFont("Helvetica", "bold");
|
||||||
|
pdf.setTextColor(255, 0, 0);
|
||||||
|
const lines = pdf.splitTextToSize(part, pageWidth - margin * 2);
|
||||||
|
pdf.text(lines, margin, cursorY);
|
||||||
|
cursorY += lines.length * 10 * 0.5;
|
||||||
|
} else if (part.trim()) {
|
||||||
|
pdf.setFontSize(10);
|
||||||
|
pdf.setFont("Helvetica", "normal");
|
||||||
|
pdf.setTextColor(0, 0, 0);
|
||||||
|
const lines = pdf.splitTextToSize(part, pageWidth - margin * 2);
|
||||||
|
pdf.text(lines, margin, cursorY);
|
||||||
|
cursorY += lines.length * 10 * 0.5;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cursorY += 8;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAnonymizedTextForPreview = (text: string): string => {
|
||||||
|
return text
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph) => {
|
||||||
|
const formattedParagraph = paragraph.replace(
|
||||||
|
/<([A-Z_]+)>/g,
|
||||||
|
'<span style="background-color: #ff4444; color: white; padding: 2px 6px; border-radius: 4px; font-weight: bold; font-size: 0.9em;"><$1></span>'
|
||||||
|
);
|
||||||
|
return `<p style="margin-bottom: 1rem; line-height: 1.6;">${formattedParagraph}</p>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PresidioModal({
|
||||||
|
anonymizedText,
|
||||||
|
piiCount,
|
||||||
|
onClose,
|
||||||
|
}: PresidioModalProps) {
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
|
||||||
|
if (!anonymizedText) return null;
|
||||||
|
|
||||||
|
const handleDownloadPdf = async () => {
|
||||||
|
setIsDownloading(true);
|
||||||
|
try {
|
||||||
|
const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" });
|
||||||
|
|
||||||
|
pdf.setFontSize(16);
|
||||||
|
pdf.setFont("Helvetica", "bold");
|
||||||
|
pdf.setTextColor(0, 0, 0);
|
||||||
|
pdf.text("Document Anonymisé par Presidio", 15, 20);
|
||||||
|
|
||||||
|
pdf.setFontSize(10);
|
||||||
|
pdf.setFont("Helvetica", "normal");
|
||||||
|
pdf.text(
|
||||||
|
`Données personnelles détectées et anonymisées : ${piiCount}`,
|
||||||
|
15,
|
||||||
|
30
|
||||||
|
);
|
||||||
|
|
||||||
|
pdf.setDrawColor(0, 0, 0);
|
||||||
|
pdf.line(15, 35, pdf.internal.pageSize.getWidth() - 15, 35);
|
||||||
|
|
||||||
|
pdf.setFontSize(10);
|
||||||
|
renderAnonymizedTextOnPdf(pdf, anonymizedText);
|
||||||
|
|
||||||
|
pdf.save("document_anonymise_presidio.pdf");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la génération du PDF:", error);
|
||||||
|
} finally {
|
||||||
|
setIsDownloading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewHtml = `
|
||||||
|
<div style="font-family: Arial, sans-serif; color: white;">
|
||||||
|
<h2 style="color: #F7AB6E; margin-bottom: 1rem;">Document Anonymisé par Presidio</h2>
|
||||||
|
<p style="color: #ccc; margin-bottom: 2rem; font-size: 0.9em;">
|
||||||
|
<strong>${piiCount}</strong> données personnelles détectées et anonymisées
|
||||||
|
</p>
|
||||||
|
<div style="border-top: 2px dashed rgba(255,255,255,0.3); padding-top: 1.5rem;">
|
||||||
|
${formatAnonymizedTextForPreview(anonymizedText)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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-4xl 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">
|
||||||
|
Document Anonymisé - {piiCount} données masquées
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadPdf}
|
||||||
|
disabled={isDownloading}
|
||||||
|
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 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||||
|
) : (
|
||||||
|
<Download className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
className="preview-content p-6 overflow-y-auto w-full h-full"
|
||||||
|
dangerouslySetInnerHTML={{ __html: previewHtml }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
app/page.tsx
96
app/page.tsx
@@ -31,15 +31,13 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
Eye,
|
Eye,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import MarkdownModal from "./components/MarkdownModal";
|
import PresidioModal from "./components/PresidioModal";
|
||||||
|
|
||||||
// Définition de la structure d'un objet page, exporté pour être réutilisé
|
|
||||||
export type PageObject = {
|
export type PageObject = {
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Interface mise à jour pour utiliser la nouvelle structure PageObject
|
|
||||||
interface ProcessedFile {
|
interface ProcessedFile {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -50,7 +48,7 @@ interface ProcessedFile {
|
|||||||
piiCount?: number;
|
piiCount?: number;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
processedBlob?: Blob;
|
processedBlob?: Blob;
|
||||||
textContent?: PageObject[];
|
anonymizedText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnonymizationOptions {
|
interface AnonymizationOptions {
|
||||||
@@ -58,7 +56,7 @@ interface AnonymizationOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const piiOptions = [
|
const piiOptions = [
|
||||||
{ id: "name", label: "Nom & Prénom", icon: User },
|
{ id: "name", label: "Nom & Prénom", icon: User },
|
||||||
{ id: "email", label: "Email", icon: AtSign },
|
{ id: "email", label: "Email", icon: AtSign },
|
||||||
{ id: "location", label: "Lieu", icon: MapPin },
|
{ id: "location", label: "Lieu", icon: MapPin },
|
||||||
{ id: "birthdate", label: "Date de naissance", icon: Cake },
|
{ id: "birthdate", label: "Date de naissance", icon: Cake },
|
||||||
@@ -81,17 +79,22 @@ export default function Home() {
|
|||||||
useState<AnonymizationOptions>(
|
useState<AnonymizationOptions>(
|
||||||
piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {})
|
piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {})
|
||||||
);
|
);
|
||||||
// CORRECTION : L'état de la modale attend maintenant la nouvelle structure de données
|
const [showPresidioModal, setShowPresidioModal] = useState(false);
|
||||||
const [modalContent, setModalContent] = useState<PageObject[] | null>(null);
|
const [anonymizedResult, setAnonymizedResult] = useState<{
|
||||||
|
text: string;
|
||||||
|
piiCount: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const handleOptionChange = (id: string) =>
|
const handleOptionChange = (id: string) =>
|
||||||
setAnonymizationOptions((prev) => ({ ...prev, [id]: !prev[id] }));
|
setAnonymizationOptions((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files?.length) {
|
if (e.target.files?.length) {
|
||||||
setFile(e.target.files[0]);
|
setFile(e.target.files[0]);
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
@@ -100,13 +103,16 @@ export default function Home() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragOver(true);
|
setIsDragOver(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDragLeave = useCallback(() => {
|
const handleDragLeave = useCallback(() => {
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
if (bytes === 0) return "0 Bytes";
|
if (bytes === 0) return "0 Bytes";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -114,11 +120,14 @@ export default function Home() {
|
|||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearFile = () => {
|
const clearFile = () => {
|
||||||
setFile(null);
|
setFile(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearHistory = () => setHistory([]);
|
const clearHistory = () => setHistory([]);
|
||||||
|
|
||||||
const removeFromHistory = (id: string) =>
|
const removeFromHistory = (id: string) =>
|
||||||
setHistory((prev) => prev.filter((item) => item.id !== id));
|
setHistory((prev) => prev.filter((item) => item.id !== id));
|
||||||
|
|
||||||
@@ -128,18 +137,24 @@ export default function Home() {
|
|||||||
const url = URL.createObjectURL(fileToDownload.processedBlob);
|
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_${
|
a.download = `anonymized_${fileToDownload.name
|
||||||
fileToDownload.name.split(".")[0] || "file"
|
.split(".")
|
||||||
}.txt`;
|
.slice(0, -1)
|
||||||
|
.join(".")}.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
a.remove();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreview = (id: string) => {
|
const handlePreview = (id: string) => {
|
||||||
const fileToPreview = history.find((item) => item.id === id);
|
const fileToPreview = history.find((item) => item.id === id);
|
||||||
if (fileToPreview?.textContent) {
|
if (fileToPreview?.anonymizedText && fileToPreview.piiCount !== undefined) {
|
||||||
setModalContent(fileToPreview.textContent);
|
setAnonymizedResult({
|
||||||
|
text: fileToPreview.anonymizedText,
|
||||||
|
piiCount: fileToPreview.piiCount,
|
||||||
|
});
|
||||||
|
setShowPresidioModal(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,16 +182,12 @@ export default function Home() {
|
|||||||
|
|
||||||
const processFile = async () => {
|
const processFile = async () => {
|
||||||
if (!file) return;
|
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);
|
setIsProcessing(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setError(null);
|
setError(null);
|
||||||
const fileId = `${Date.now()}-${file.name}`;
|
const fileId = `${Date.now()}-${file.name}`;
|
||||||
|
|
||||||
setHistory((prev) => [
|
setHistory((prev) => [
|
||||||
{
|
{
|
||||||
id: fileId,
|
id: fileId,
|
||||||
@@ -188,51 +199,31 @@ export default function Home() {
|
|||||||
...prev,
|
...prev,
|
||||||
]);
|
]);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
setProgress(10);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("options", JSON.stringify(anonymizationOptions));
|
setProgress(25);
|
||||||
|
|
||||||
setProgress(30);
|
const response = await fetch("/api/process-document", {
|
||||||
const response = await fetch(n8nWebhookUrl, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
setProgress(70);
|
setProgress(75);
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorResult = await response
|
|
||||||
.json()
|
|
||||||
.catch(() => ({ error: `Erreur serveur [${response.status}]` }));
|
|
||||||
throw new Error(errorResult.error || `Échec du traitement.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
const docData = result.anonymizedDocument;
|
|
||||||
if (!docData || !Array.isArray(docData.pages)) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Format de réponse invalide du service d'anonymisation."
|
result.error || "Une erreur est survenue lors du traitement."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const textContent: PageObject[] = docData.pages;
|
const { anonymizedText, piiCount } = result;
|
||||||
const piiCount: number = docData.piiCount || 0;
|
|
||||||
|
|
||||||
const fullText = textContent
|
const processedBlob = new Blob([anonymizedText], {
|
||||||
.map(
|
|
||||||
(page) =>
|
|
||||||
`--- Page ${page.pageNumber} ---\n${page.htmlContent.replace(
|
|
||||||
/<[^>]*>/g,
|
|
||||||
"\n"
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
.join("\n\n");
|
|
||||||
const processedBlob = new Blob([fullText], {
|
|
||||||
type: "text/plain;charset=utf-8",
|
type: "text/plain;charset=utf-8",
|
||||||
});
|
});
|
||||||
setProgress(90);
|
|
||||||
|
|
||||||
setHistory((prev) =>
|
setHistory((prev) =>
|
||||||
prev.map((item) =>
|
prev.map((item) =>
|
||||||
@@ -243,7 +234,7 @@ export default function Home() {
|
|||||||
processedSize: formatFileSize(processedBlob.size),
|
processedSize: formatFileSize(processedBlob.size),
|
||||||
piiCount,
|
piiCount,
|
||||||
processedBlob,
|
processedBlob,
|
||||||
textContent,
|
anonymizedText,
|
||||||
}
|
}
|
||||||
: item
|
: item
|
||||||
)
|
)
|
||||||
@@ -338,7 +329,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 ml-2">
|
<div className="flex items-center gap-1.5 ml-2">
|
||||||
{item.status === "completed" && item.textContent && (
|
{item.status === "completed" && item.anonymizedText && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handlePreview(item.id)}
|
onClick={() => handlePreview(item.id)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -560,10 +551,13 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<MarkdownModal
|
{showPresidioModal && (
|
||||||
content={modalContent}
|
<PresidioModal
|
||||||
onClose={() => setModalContent(null)}
|
anonymizedText={anonymizedResult?.text || null}
|
||||||
|
piiCount={anonymizedResult?.piiCount || 0}
|
||||||
|
onClose={() => setShowPresidioModal(false)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
const nextConfig: NextConfig = {};
|
// Il n'y a plus aucune configuration spécifique nécessaire !
|
||||||
|
// Vous pouvez laisser cet objet vide.
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
767
package-lock.json
generated
767
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,8 @@
|
|||||||
"lucide-react": "^0.514.0",
|
"lucide-react": "^0.514.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
"pdfjs-dist": "^5.3.31",
|
"patch-package": "^8.0.0",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pdf-parse": "^1.1.5",
|
||||||
"@types/pdfjs-dist": "^2.10.377",
|
"@types/pdfjs-dist": "^2.10.377",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
2
types/pdfjs-dist.d.ts
vendored
2
types/pdfjs-dist.d.ts
vendored
@@ -1,2 +1,4 @@
|
|||||||
declare module "pdfjs-dist/legacy/build/pdf.min.mjs";
|
declare module "pdfjs-dist/legacy/build/pdf.min.mjs";
|
||||||
declare module "pdfjs-dist/legacy/build/pdf.worker.min.mjs";
|
declare module "pdfjs-dist/legacy/build/pdf.worker.min.mjs";
|
||||||
|
// types/pdfjs.d.ts
|
||||||
|
declare module "pdfjs-dist/build/pdf.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user