ClauseGuard / web /lib /export-utils.ts
gaurv007's picture
feat: add web/lib/export-utils.ts
7104ac4 verified
/**
* ClauseGuard β€” Multi-format Report Export Utility
* Generates reports in: JSON, CSV, Markdown, Plain Text, HTML
* PDF and DOCX use server-side generation via API routes.
*/
import type { AnalysisResult, Clause, Entity, Contradiction, Obligation, ComplianceReg, Redline } from "./types";
// ── Severity ordering ──
const SEV_ORDER: Record<string, number> = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
function sevSort(a: string, b: string) {
return (SEV_ORDER[b] || 0) - (SEV_ORDER[a] || 0);
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
}
function download(content: string | Blob, filename: string, mime: string) {
const blob = content instanceof Blob ? content : new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// ═══════════════════════════════════════════════════════════════
// JSON Export
// ═══════════════════════════════════════════════════════════════
export function exportJSON(results: AnalysisResult, formatted = true) {
const json = formatted
? JSON.stringify(results, null, 2)
: JSON.stringify(results);
download(json, `clauseguard-report-${timestamp()}.json`, "application/json");
}
// ═══════════════════════════════════════════════════════════════
// CSV Export
// ═══════════════════════════════════════════════════════════════
function escapeCSV(val: string): string {
if (val.includes(",") || val.includes('"') || val.includes("\n")) {
return `"${val.replace(/"/g, '""')}"`;
}
return val;
}
export function exportCSV(results: AnalysisResult) {
const rows: string[] = [];
// Header
rows.push("Section,Category,Severity,Confidence,Source,Text,Description");
// Clauses
for (const clause of results.results) {
for (const cat of clause.categories) {
rows.push([
"Clause",
escapeCSV(cat.name),
cat.severity,
cat.confidence != null ? String(Math.round(cat.confidence * 100)) + "%" : "pattern",
cat.confidence != null ? "ML" : "Pattern",
escapeCSV(clause.text.slice(0, 500)),
escapeCSV(cat.description || ""),
].join(","));
}
}
// Entities
for (const ent of results.entities) {
rows.push([
"Entity",
escapeCSV(ent.type),
"",
ent.score ? String(Math.round(ent.score * 100)) + "%" : "",
ent.source || "",
escapeCSV(ent.text),
"",
].join(","));
}
// Contradictions
for (const c of results.contradictions) {
rows.push([
"Contradiction",
escapeCSV(c.type),
c.severity,
c.confidence ? String(Math.round(c.confidence * 100)) + "%" : "",
c.source || "",
escapeCSV(c.explanation),
"",
].join(","));
}
// Obligations
for (const o of results.obligations) {
rows.push([
"Obligation",
escapeCSV(o.type),
o.priority != null && o.priority >= 3 ? "HIGH" : o.priority === 2 ? "MEDIUM" : "LOW",
"",
"",
escapeCSV(o.description),
escapeCSV(`${o.party} Β· ${o.deadline}`),
].join(","));
}
download(rows.join("\n"), `clauseguard-report-${timestamp()}.csv`, "text/csv");
}
// ═══════════════════════════════════════════════════════════════
// Markdown Export
// ═══════════════════════════════════════════════════════════════
export function exportMarkdown(results: AnalysisResult) {
const lines: string[] = [];
const flagged = results.results.filter(r => r.categories.length > 0);
const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
flagged.forEach(r => r.categories.forEach(c => {
if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++;
}));
lines.push("# πŸ›‘οΈ ClauseGuard Analysis Report");
lines.push("");
lines.push(`**Generated:** ${new Date().toLocaleString()}`);
lines.push(`**Risk Score:** ${results.risk_score}/100 Β· **Grade:** ${results.grade}`);
lines.push(`**Clauses:** ${results.total_clauses} total Β· ${results.flagged_count} flagged`);
lines.push(`**Model:** ${results.model === "ml" || results.model !== "regex" ? "ML Models" : "Pattern Matching"}`);
lines.push("");
// Severity breakdown
lines.push("## πŸ“Š Risk Breakdown");
lines.push("");
lines.push("| Severity | Count |");
lines.push("|----------|-------|");
lines.push(`| πŸ”΄ Critical | ${sevCounts.CRITICAL} |`);
lines.push(`| 🟠 High | ${sevCounts.HIGH} |`);
lines.push(`| 🟑 Medium | ${sevCounts.MEDIUM} |`);
lines.push(`| 🟒 Low | ${sevCounts.LOW} |`);
lines.push("");
// Flagged clauses
if (flagged.length > 0) {
lines.push("## ⚠️ Flagged Clauses");
lines.push("");
for (const clause of flagged) {
const labels = clause.categories.map(c => `**${c.name}** (${c.severity})`).join(", ");
lines.push(`### ${labels}`);
lines.push("");
lines.push(`> ${clause.text.slice(0, 500)}${clause.text.length > 500 ? "..." : ""}`);
lines.push("");
for (const cat of clause.categories) {
if (cat.description) lines.push(`- ${cat.description}`);
const src = cat.confidence != null ? `ML ${Math.round(cat.confidence * 100)}%` : "Pattern match";
lines.push(`- *Source: ${src}*`);
}
lines.push("");
}
}
// Entities
if (results.entities.length > 0) {
lines.push("## 🏷️ Extracted Entities");
lines.push("");
const grouped: Record<string, string[]> = {};
results.entities.forEach(e => {
if (!grouped[e.type]) grouped[e.type] = [];
if (!grouped[e.type].includes(e.text)) grouped[e.type].push(e.text);
});
for (const [type, items] of Object.entries(grouped)) {
lines.push(`**${type.replace(/_/g, " ")}:** ${items.join(", ")}`);
}
lines.push("");
}
// Contradictions
if (results.contradictions.length > 0) {
lines.push("## πŸ” Contradictions & Issues");
lines.push("");
for (const c of results.contradictions) {
lines.push(`- **[${c.severity}] ${c.type}:** ${c.explanation}`);
}
lines.push("");
}
// Obligations
if (results.obligations.length > 0) {
lines.push("## πŸ“‹ Obligations");
lines.push("");
lines.push("| Type | Party | Description | Deadline |");
lines.push("|------|-------|-------------|----------|");
for (const o of results.obligations) {
lines.push(`| ${o.type} | ${o.party} | ${o.description.slice(0, 100)} | ${o.deadline} |`);
}
lines.push("");
}
// Compliance
if (Object.keys(results.compliance).length > 0) {
lines.push("## βš–οΈ Compliance");
lines.push("");
for (const [name, reg] of Object.entries(results.compliance)) {
lines.push(`### ${name} β€” ${reg.compliance_rate}% (${reg.overall_status})`);
lines.push(`*${reg.description}*`);
lines.push("");
for (const check of reg.checks) {
const icon = check.status === "PASS" ? "βœ…" : check.status === "MISSING" ? "❌" : "⚠️";
lines.push(`${icon} ${check.description} (${check.severity})`);
}
lines.push("");
}
}
// Redlines
if (results.redlines && results.redlines.length > 0) {
lines.push("## ✏️ Redlining Suggestions");
lines.push("");
for (const rl of results.redlines) {
lines.push(`### ${rl.clause_label} (${rl.risk_level})`);
lines.push("");
lines.push(`~~${rl.original_text.slice(0, 200)}~~`);
lines.push("");
lines.push(`βœ… **Suggested:** ${rl.safe_alternative}`);
lines.push(`πŸ“š ${rl.legal_basis} Β· πŸ›‘οΈ ${rl.consumer_standard}`);
lines.push("");
}
}
lines.push("---");
lines.push("*⚠️ Not legal advice. Generated by ClauseGuard AI.*");
download(lines.join("\n"), `clauseguard-report-${timestamp()}.md`, "text/markdown");
}
// ═══════════════════════════════════════════════════════════════
// Plain Text Export
// ═══════════════════════════════════════════════════════════════
export function exportText(results: AnalysisResult) {
const lines: string[] = [];
const flagged = results.results.filter(r => r.categories.length > 0);
lines.push("═══════════════════════════════════════════════════════");
lines.push(" CLAUSEGUARD ANALYSIS REPORT");
lines.push("═══════════════════════════════════════════════════════");
lines.push("");
lines.push(`Date: ${new Date().toLocaleString()}`);
lines.push(`Risk Score: ${results.risk_score}/100`);
lines.push(`Grade: ${results.grade}`);
lines.push(`Clauses: ${results.total_clauses} total, ${results.flagged_count} flagged`);
lines.push(`Entities: ${results.entities.length}`);
lines.push(`Issues: ${results.contradictions.length}`);
lines.push(`Obligations: ${results.obligations.length}`);
lines.push("");
lines.push("───────────────────────────────────────────────────────");
lines.push(" FLAGGED CLAUSES");
lines.push("───────────────────────────────────────────────────────");
lines.push("");
for (let i = 0; i < flagged.length; i++) {
const clause = flagged[i];
const labels = clause.categories.map(c => `[${c.severity}] ${c.name}`).join(", ");
lines.push(`${i + 1}. ${labels}`);
lines.push(` ${clause.text.slice(0, 300)}${clause.text.length > 300 ? "..." : ""}`);
lines.push("");
}
if (results.entities.length > 0) {
lines.push("───────────────────────────────────────────────────────");
lines.push(" ENTITIES");
lines.push("───────────────────────────────────────────────────────");
lines.push("");
const grouped: Record<string, string[]> = {};
results.entities.forEach(e => {
if (!grouped[e.type]) grouped[e.type] = [];
if (!grouped[e.type].includes(e.text)) grouped[e.type].push(e.text);
});
for (const [type, items] of Object.entries(grouped)) {
lines.push(` ${type}: ${items.join(", ")}`);
}
lines.push("");
}
if (results.contradictions.length > 0) {
lines.push("───────────────────────────────────────────────────────");
lines.push(" CONTRADICTIONS & ISSUES");
lines.push("───────────────────────────────────────────────────────");
lines.push("");
for (const c of results.contradictions) {
lines.push(` [${c.severity}] ${c.type}: ${c.explanation}`);
}
lines.push("");
}
if (results.obligations.length > 0) {
lines.push("───────────────────────────────────────────────────────");
lines.push(" OBLIGATIONS");
lines.push("───────────────────────────────────────────────────────");
lines.push("");
for (const o of results.obligations) {
lines.push(` [${o.type}] ${o.party}: ${o.description} (${o.deadline})`);
}
lines.push("");
}
if (results.redlines && results.redlines.length > 0) {
lines.push("───────────────────────────────────────────────────────");
lines.push(" REDLINING SUGGESTIONS");
lines.push("───────────────────────────────────────────────────────");
lines.push("");
for (const rl of results.redlines) {
lines.push(` [${rl.risk_level}] ${rl.clause_label}`);
lines.push(` ORIGINAL: ${rl.original_text.slice(0, 200)}`);
lines.push(` SUGGESTED: ${rl.safe_alternative}`);
lines.push("");
}
}
lines.push("═══════════════════════════════════════════════════════");
lines.push(" NOT LEGAL ADVICE β€” Generated by ClauseGuard AI");
lines.push("═══════════════════════════════════════════════════════");
download(lines.join("\n"), `clauseguard-report-${timestamp()}.txt`, "text/plain");
}
// ═══════════════════════════════════════════════════════════════
// HTML Export (self-contained styled report)
// ═══════════════════════════════════════════════════════════════
export function exportHTML(results: AnalysisResult) {
const flagged = results.results.filter(r => r.categories.length > 0);
const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
flagged.forEach(r => r.categories.forEach(c => {
if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++;
}));
const sevColor: Record<string, string> = { CRITICAL: "#dc2626", HIGH: "#ea580c", MEDIUM: "#ca8a04", LOW: "#16a34a" };
const clauseHTML = flagged.map(clause => {
const tags = clause.categories.map(c =>
`<span style="display:inline-block;background:${sevColor[c.severity] || '#888'}15;color:${sevColor[c.severity] || '#888'};border:1px solid ${sevColor[c.severity] || '#888'}40;padding:2px 10px;border-radius:4px;font-size:12px;font-weight:600;margin-right:4px;">${c.name} (${c.severity})</span>`
).join("");
return `<div style="border:1px solid #e5e7eb;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="margin-bottom:8px;">${tags}</div>
<p style="font-size:13px;color:#374151;line-height:1.7;margin:0;">${clause.text.replace(/</g, "&lt;").slice(0, 500)}</p>
</div>`;
}).join("\n");
const entityHTML = (() => {
const grouped: Record<string, string[]> = {};
results.entities.forEach(e => {
if (!grouped[e.type]) grouped[e.type] = [];
if (!grouped[e.type].includes(e.text)) grouped[e.type].push(e.text);
});
return Object.entries(grouped).map(([type, items]) =>
`<div style="margin-bottom:12px;"><strong style="font-size:12px;text-transform:uppercase;color:#6b7280;">${type.replace(/_/g, " ")}</strong><div style="margin-top:4px;">${items.map(t => `<span style="display:inline-block;background:#f3f4f6;padding:3px 10px;border-radius:4px;font-size:12px;margin:2px;">${t}</span>`).join("")}</div></div>`
).join("\n");
})();
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>ClauseGuard Report β€” ${new Date().toLocaleDateString()}</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#1f2937;background:#fff;padding:40px;max-width:800px;margin:0 auto}
h1{font-size:24px;font-weight:700;margin-bottom:4px}
h2{font-size:16px;font-weight:600;margin:24px 0 12px;padding-bottom:8px;border-bottom:1px solid #e5e7eb}
.meta{font-size:12px;color:#9ca3af}
.score-card{display:flex;justify-content:space-between;align-items:center;background:#fafafa;border:1px solid #e5e7eb;border-radius:12px;padding:20px;margin:16px 0}
.score{font-size:36px;font-weight:700}
.grade{font-size:18px;font-weight:700;padding:6px 16px;border-radius:8px;border:1px solid #e5e7eb}
.sev-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:12px 0}
.sev-item{text-align:center;padding:8px;border-radius:8px}
.disclaimer{margin-top:32px;padding:12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px;font-size:11px;color:#92400e}
@media print{body{padding:20px}h2{break-before:auto}}
</style>
</head>
<body>
<h1>πŸ›‘οΈ ClauseGuard Analysis Report</h1>
<p class="meta">${new Date().toLocaleString()} Β· ${results.model !== "regex" ? "ML Models" : "Pattern Matching"}</p>
<div class="score-card">
<div>
<p class="meta">RISK SCORE</p>
<p class="score">${results.risk_score}<span style="font-size:16px;color:#9ca3af">/100</span></p>
</div>
<span class="grade">Grade ${results.grade}</span>
</div>
<div class="sev-grid">
<div class="sev-item" style="background:#fef2f2"><strong style="color:#dc2626">${sevCounts.CRITICAL}</strong><br><small style="color:#dc2626">Critical</small></div>
<div class="sev-item" style="background:#fff7ed"><strong style="color:#ea580c">${sevCounts.HIGH}</strong><br><small style="color:#ea580c">High</small></div>
<div class="sev-item" style="background:#fefce8"><strong style="color:#ca8a04">${sevCounts.MEDIUM}</strong><br><small style="color:#ca8a04">Medium</small></div>
<div class="sev-item" style="background:#f0fdf4"><strong style="color:#16a34a">${sevCounts.LOW}</strong><br><small style="color:#16a34a">Low</small></div>
</div>
<p class="meta">${results.total_clauses} clauses Β· ${results.flagged_count} flagged Β· ${results.entities.length} entities Β· ${results.obligations.length} obligations</p>
${flagged.length > 0 ? `<h2>⚠️ Flagged Clauses (${flagged.length})</h2>${clauseHTML}` : ""}
${results.entities.length > 0 ? `<h2>🏷️ Entities (${results.entities.length})</h2>${entityHTML}` : ""}
${results.contradictions.length > 0 ? `<h2>πŸ” Issues (${results.contradictions.length})</h2>${results.contradictions.map(c => `<div style="border:1px solid #e5e7eb;border-left:3px solid ${sevColor[c.severity] || '#888'};border-radius:6px;padding:12px;margin-bottom:8px;"><strong style="color:${sevColor[c.severity]};font-size:11px;text-transform:uppercase">${c.type} (${c.severity})</strong><p style="font-size:13px;margin-top:4px">${c.explanation}</p></div>`).join("")}` : ""}
${results.obligations.length > 0 ? `<h2>πŸ“‹ Obligations (${results.obligations.length})</h2><table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#f9fafb;border-bottom:1px solid #e5e7eb"><th style="text-align:left;padding:8px">Type</th><th style="text-align:left;padding:8px">Party</th><th style="text-align:left;padding:8px">Description</th><th style="text-align:left;padding:8px">Deadline</th></tr></thead><tbody>${results.obligations.map(o => `<tr style="border-bottom:1px solid #f3f4f6"><td style="padding:8px;font-weight:500">${o.type}</td><td style="padding:8px">${o.party}</td><td style="padding:8px">${o.description.slice(0, 120)}</td><td style="padding:8px">${o.deadline}</td></tr>`).join("")}</tbody></table>` : ""}
${results.redlines && results.redlines.length > 0 ? `<h2>✏️ Redlining (${results.redlines.length})</h2>${results.redlines.map(rl => `<div style="border:1px solid #e5e7eb;border-radius:8px;padding:16px;margin-bottom:12px"><strong style="color:${sevColor[rl.risk_level]}">${rl.clause_label} (${rl.risk_level})</strong><div style="background:#fef2f2;padding:8px;border-radius:4px;margin:8px 0;font-size:12px;text-decoration:line-through;color:#991b1b">${rl.original_text.slice(0, 200)}</div><div style="background:#f0fdf4;padding:8px;border-radius:4px;font-size:12px;color:#166534">${rl.safe_alternative}</div><p style="font-size:10px;color:#9ca3af;margin-top:6px">πŸ“š ${rl.legal_basis} Β· πŸ›‘οΈ ${rl.consumer_standard}</p></div>`).join("")}` : ""}
<div class="disclaimer">⚠️ <strong>Not legal advice.</strong> This report was generated by ClauseGuard AI for informational purposes only. Consult a licensed attorney for legal decisions.</div>
</body>
</html>`;
download(html, `clauseguard-report-${timestamp()}.html`, "text/html");
}
// ═══════════════════════════════════════════════════════════════
// PDF Export (via server-side API route)
// ═══════════════════════════════════════════════════════════════
export async function exportPDF(results: AnalysisResult) {
try {
const res = await fetch("/api/pdf/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(results),
});
if (!res.ok) throw new Error("PDF generation failed");
const blob = await res.blob();
download(blob, `clauseguard-report-${timestamp()}.pdf`, "application/pdf");
return true;
} catch {
// Fallback: print HTML version
exportHTML(results);
return false;
}
}
// ═══════════════════════════════════════════════════════════════
// Export formats manifest (for the UI dropdown)
// ═══════════════════════════════════════════════════════════════
export const EXPORT_FORMATS = [
{ key: "pdf", label: "PDF Report", icon: "πŸ“„", description: "Formatted PDF document", fn: exportPDF },
{ key: "html", label: "HTML Report", icon: "🌐", description: "Styled HTML (printable)", fn: exportHTML },
{ key: "md", label: "Markdown", icon: "πŸ“", description: "GitHub-flavored markdown", fn: exportMarkdown },
{ key: "txt", label: "Plain Text", icon: "πŸ“‹", description: "Simple text format", fn: exportText },
{ key: "csv", label: "CSV Spreadsheet", icon: "πŸ“Š", description: "For Excel / Google Sheets", fn: exportCSV },
{ key: "json", label: "JSON (formatted)", icon: "πŸ”§", description: "Full structured data", fn: (r: AnalysisResult) => exportJSON(r, true) },
{ key: "json-raw", label: "JSON (raw)", icon: "⚑", description: "Compact, no whitespace", fn: (r: AnalysisResult) => exportJSON(r, false) },
] as const;