gaurv007 commited on
Commit
7104ac4
Β·
verified Β·
1 Parent(s): cc809a8

feat: add web/lib/export-utils.ts

Browse files
Files changed (1) hide show
  1. web/lib/export-utils.ts +454 -0
web/lib/export-utils.ts ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ClauseGuard β€” Multi-format Report Export Utility
3
+ * Generates reports in: JSON, CSV, Markdown, Plain Text, HTML
4
+ * PDF and DOCX use server-side generation via API routes.
5
+ */
6
+
7
+ import type { AnalysisResult, Clause, Entity, Contradiction, Obligation, ComplianceReg, Redline } from "./types";
8
+
9
+ // ── Severity ordering ──
10
+ const SEV_ORDER: Record<string, number> = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
11
+
12
+ function sevSort(a: string, b: string) {
13
+ return (SEV_ORDER[b] || 0) - (SEV_ORDER[a] || 0);
14
+ }
15
+
16
+ function timestamp() {
17
+ return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
18
+ }
19
+
20
+ function download(content: string | Blob, filename: string, mime: string) {
21
+ const blob = content instanceof Blob ? content : new Blob([content], { type: mime });
22
+ const url = URL.createObjectURL(blob);
23
+ const a = document.createElement("a");
24
+ a.href = url;
25
+ a.download = filename;
26
+ document.body.appendChild(a);
27
+ a.click();
28
+ document.body.removeChild(a);
29
+ URL.revokeObjectURL(url);
30
+ }
31
+
32
+ // ═══════════════════════════════════════════════════════════════
33
+ // JSON Export
34
+ // ═══════════════════════════════════════════════════════════════
35
+
36
+ export function exportJSON(results: AnalysisResult, formatted = true) {
37
+ const json = formatted
38
+ ? JSON.stringify(results, null, 2)
39
+ : JSON.stringify(results);
40
+ download(json, `clauseguard-report-${timestamp()}.json`, "application/json");
41
+ }
42
+
43
+ // ═══════════════════════════════════════════════════════════════
44
+ // CSV Export
45
+ // ═══════════════════════════════════════════════════════════════
46
+
47
+ function escapeCSV(val: string): string {
48
+ if (val.includes(",") || val.includes('"') || val.includes("\n")) {
49
+ return `"${val.replace(/"/g, '""')}"`;
50
+ }
51
+ return val;
52
+ }
53
+
54
+ export function exportCSV(results: AnalysisResult) {
55
+ const rows: string[] = [];
56
+
57
+ // Header
58
+ rows.push("Section,Category,Severity,Confidence,Source,Text,Description");
59
+
60
+ // Clauses
61
+ for (const clause of results.results) {
62
+ for (const cat of clause.categories) {
63
+ rows.push([
64
+ "Clause",
65
+ escapeCSV(cat.name),
66
+ cat.severity,
67
+ cat.confidence != null ? String(Math.round(cat.confidence * 100)) + "%" : "pattern",
68
+ cat.confidence != null ? "ML" : "Pattern",
69
+ escapeCSV(clause.text.slice(0, 500)),
70
+ escapeCSV(cat.description || ""),
71
+ ].join(","));
72
+ }
73
+ }
74
+
75
+ // Entities
76
+ for (const ent of results.entities) {
77
+ rows.push([
78
+ "Entity",
79
+ escapeCSV(ent.type),
80
+ "",
81
+ ent.score ? String(Math.round(ent.score * 100)) + "%" : "",
82
+ ent.source || "",
83
+ escapeCSV(ent.text),
84
+ "",
85
+ ].join(","));
86
+ }
87
+
88
+ // Contradictions
89
+ for (const c of results.contradictions) {
90
+ rows.push([
91
+ "Contradiction",
92
+ escapeCSV(c.type),
93
+ c.severity,
94
+ c.confidence ? String(Math.round(c.confidence * 100)) + "%" : "",
95
+ c.source || "",
96
+ escapeCSV(c.explanation),
97
+ "",
98
+ ].join(","));
99
+ }
100
+
101
+ // Obligations
102
+ for (const o of results.obligations) {
103
+ rows.push([
104
+ "Obligation",
105
+ escapeCSV(o.type),
106
+ o.priority != null && o.priority >= 3 ? "HIGH" : o.priority === 2 ? "MEDIUM" : "LOW",
107
+ "",
108
+ "",
109
+ escapeCSV(o.description),
110
+ escapeCSV(`${o.party} Β· ${o.deadline}`),
111
+ ].join(","));
112
+ }
113
+
114
+ download(rows.join("\n"), `clauseguard-report-${timestamp()}.csv`, "text/csv");
115
+ }
116
+
117
+ // ═══════════════════════════════════════════════════════════════
118
+ // Markdown Export
119
+ // ═══════════════════════════════════════════════════════════════
120
+
121
+ export function exportMarkdown(results: AnalysisResult) {
122
+ const lines: string[] = [];
123
+ const flagged = results.results.filter(r => r.categories.length > 0);
124
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
125
+ flagged.forEach(r => r.categories.forEach(c => {
126
+ if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++;
127
+ }));
128
+
129
+ lines.push("# πŸ›‘οΈ ClauseGuard Analysis Report");
130
+ lines.push("");
131
+ lines.push(`**Generated:** ${new Date().toLocaleString()}`);
132
+ lines.push(`**Risk Score:** ${results.risk_score}/100 Β· **Grade:** ${results.grade}`);
133
+ lines.push(`**Clauses:** ${results.total_clauses} total Β· ${results.flagged_count} flagged`);
134
+ lines.push(`**Model:** ${results.model === "ml" || results.model !== "regex" ? "ML Models" : "Pattern Matching"}`);
135
+ lines.push("");
136
+
137
+ // Severity breakdown
138
+ lines.push("## πŸ“Š Risk Breakdown");
139
+ lines.push("");
140
+ lines.push("| Severity | Count |");
141
+ lines.push("|----------|-------|");
142
+ lines.push(`| πŸ”΄ Critical | ${sevCounts.CRITICAL} |`);
143
+ lines.push(`| 🟠 High | ${sevCounts.HIGH} |`);
144
+ lines.push(`| 🟑 Medium | ${sevCounts.MEDIUM} |`);
145
+ lines.push(`| 🟒 Low | ${sevCounts.LOW} |`);
146
+ lines.push("");
147
+
148
+ // Flagged clauses
149
+ if (flagged.length > 0) {
150
+ lines.push("## ⚠️ Flagged Clauses");
151
+ lines.push("");
152
+ for (const clause of flagged) {
153
+ const labels = clause.categories.map(c => `**${c.name}** (${c.severity})`).join(", ");
154
+ lines.push(`### ${labels}`);
155
+ lines.push("");
156
+ lines.push(`> ${clause.text.slice(0, 500)}${clause.text.length > 500 ? "..." : ""}`);
157
+ lines.push("");
158
+ for (const cat of clause.categories) {
159
+ if (cat.description) lines.push(`- ${cat.description}`);
160
+ const src = cat.confidence != null ? `ML ${Math.round(cat.confidence * 100)}%` : "Pattern match";
161
+ lines.push(`- *Source: ${src}*`);
162
+ }
163
+ lines.push("");
164
+ }
165
+ }
166
+
167
+ // Entities
168
+ if (results.entities.length > 0) {
169
+ lines.push("## 🏷️ Extracted Entities");
170
+ lines.push("");
171
+ const grouped: Record<string, string[]> = {};
172
+ results.entities.forEach(e => {
173
+ if (!grouped[e.type]) grouped[e.type] = [];
174
+ if (!grouped[e.type].includes(e.text)) grouped[e.type].push(e.text);
175
+ });
176
+ for (const [type, items] of Object.entries(grouped)) {
177
+ lines.push(`**${type.replace(/_/g, " ")}:** ${items.join(", ")}`);
178
+ }
179
+ lines.push("");
180
+ }
181
+
182
+ // Contradictions
183
+ if (results.contradictions.length > 0) {
184
+ lines.push("## πŸ” Contradictions & Issues");
185
+ lines.push("");
186
+ for (const c of results.contradictions) {
187
+ lines.push(`- **[${c.severity}] ${c.type}:** ${c.explanation}`);
188
+ }
189
+ lines.push("");
190
+ }
191
+
192
+ // Obligations
193
+ if (results.obligations.length > 0) {
194
+ lines.push("## πŸ“‹ Obligations");
195
+ lines.push("");
196
+ lines.push("| Type | Party | Description | Deadline |");
197
+ lines.push("|------|-------|-------------|----------|");
198
+ for (const o of results.obligations) {
199
+ lines.push(`| ${o.type} | ${o.party} | ${o.description.slice(0, 100)} | ${o.deadline} |`);
200
+ }
201
+ lines.push("");
202
+ }
203
+
204
+ // Compliance
205
+ if (Object.keys(results.compliance).length > 0) {
206
+ lines.push("## βš–οΈ Compliance");
207
+ lines.push("");
208
+ for (const [name, reg] of Object.entries(results.compliance)) {
209
+ lines.push(`### ${name} β€” ${reg.compliance_rate}% (${reg.overall_status})`);
210
+ lines.push(`*${reg.description}*`);
211
+ lines.push("");
212
+ for (const check of reg.checks) {
213
+ const icon = check.status === "PASS" ? "βœ…" : check.status === "MISSING" ? "❌" : "⚠️";
214
+ lines.push(`${icon} ${check.description} (${check.severity})`);
215
+ }
216
+ lines.push("");
217
+ }
218
+ }
219
+
220
+ // Redlines
221
+ if (results.redlines && results.redlines.length > 0) {
222
+ lines.push("## ✏️ Redlining Suggestions");
223
+ lines.push("");
224
+ for (const rl of results.redlines) {
225
+ lines.push(`### ${rl.clause_label} (${rl.risk_level})`);
226
+ lines.push("");
227
+ lines.push(`~~${rl.original_text.slice(0, 200)}~~`);
228
+ lines.push("");
229
+ lines.push(`βœ… **Suggested:** ${rl.safe_alternative}`);
230
+ lines.push(`πŸ“š ${rl.legal_basis} Β· πŸ›‘οΈ ${rl.consumer_standard}`);
231
+ lines.push("");
232
+ }
233
+ }
234
+
235
+ lines.push("---");
236
+ lines.push("*⚠️ Not legal advice. Generated by ClauseGuard AI.*");
237
+
238
+ download(lines.join("\n"), `clauseguard-report-${timestamp()}.md`, "text/markdown");
239
+ }
240
+
241
+ // ═══════════════════════════════════════════════════════════════
242
+ // Plain Text Export
243
+ // ═══════════════════════════════════════════════════════════════
244
+
245
+ export function exportText(results: AnalysisResult) {
246
+ const lines: string[] = [];
247
+ const flagged = results.results.filter(r => r.categories.length > 0);
248
+
249
+ lines.push("═══════════════════════════════════════════════════════");
250
+ lines.push(" CLAUSEGUARD ANALYSIS REPORT");
251
+ lines.push("═══════════════════════════════════════════════════════");
252
+ lines.push("");
253
+ lines.push(`Date: ${new Date().toLocaleString()}`);
254
+ lines.push(`Risk Score: ${results.risk_score}/100`);
255
+ lines.push(`Grade: ${results.grade}`);
256
+ lines.push(`Clauses: ${results.total_clauses} total, ${results.flagged_count} flagged`);
257
+ lines.push(`Entities: ${results.entities.length}`);
258
+ lines.push(`Issues: ${results.contradictions.length}`);
259
+ lines.push(`Obligations: ${results.obligations.length}`);
260
+ lines.push("");
261
+ lines.push("───────────────────────────────────────────────────────");
262
+ lines.push(" FLAGGED CLAUSES");
263
+ lines.push("───────────────────────────────────────────────────────");
264
+ lines.push("");
265
+
266
+ for (let i = 0; i < flagged.length; i++) {
267
+ const clause = flagged[i];
268
+ const labels = clause.categories.map(c => `[${c.severity}] ${c.name}`).join(", ");
269
+ lines.push(`${i + 1}. ${labels}`);
270
+ lines.push(` ${clause.text.slice(0, 300)}${clause.text.length > 300 ? "..." : ""}`);
271
+ lines.push("");
272
+ }
273
+
274
+ if (results.entities.length > 0) {
275
+ lines.push("───────────────────────────────────────────────────────");
276
+ lines.push(" ENTITIES");
277
+ lines.push("───────────────────────────────────────────────────────");
278
+ lines.push("");
279
+ const grouped: Record<string, string[]> = {};
280
+ results.entities.forEach(e => {
281
+ if (!grouped[e.type]) grouped[e.type] = [];
282
+ if (!grouped[e.type].includes(e.text)) grouped[e.type].push(e.text);
283
+ });
284
+ for (const [type, items] of Object.entries(grouped)) {
285
+ lines.push(` ${type}: ${items.join(", ")}`);
286
+ }
287
+ lines.push("");
288
+ }
289
+
290
+ if (results.contradictions.length > 0) {
291
+ lines.push("───────────────────────────────────────────────────────");
292
+ lines.push(" CONTRADICTIONS & ISSUES");
293
+ lines.push("───────────────────────────────────────────────────────");
294
+ lines.push("");
295
+ for (const c of results.contradictions) {
296
+ lines.push(` [${c.severity}] ${c.type}: ${c.explanation}`);
297
+ }
298
+ lines.push("");
299
+ }
300
+
301
+ if (results.obligations.length > 0) {
302
+ lines.push("───────────────────────────────────────────────────────");
303
+ lines.push(" OBLIGATIONS");
304
+ lines.push("───────────────────────────────────────────────────────");
305
+ lines.push("");
306
+ for (const o of results.obligations) {
307
+ lines.push(` [${o.type}] ${o.party}: ${o.description} (${o.deadline})`);
308
+ }
309
+ lines.push("");
310
+ }
311
+
312
+ if (results.redlines && results.redlines.length > 0) {
313
+ lines.push("───────────────────────────────────────────────────────");
314
+ lines.push(" REDLINING SUGGESTIONS");
315
+ lines.push("───────────────────────────────────────────────────────");
316
+ lines.push("");
317
+ for (const rl of results.redlines) {
318
+ lines.push(` [${rl.risk_level}] ${rl.clause_label}`);
319
+ lines.push(` ORIGINAL: ${rl.original_text.slice(0, 200)}`);
320
+ lines.push(` SUGGESTED: ${rl.safe_alternative}`);
321
+ lines.push("");
322
+ }
323
+ }
324
+
325
+ lines.push("═══════════════════════════════════════════════════════");
326
+ lines.push(" NOT LEGAL ADVICE β€” Generated by ClauseGuard AI");
327
+ lines.push("═══════════════════════════════════════════════════════");
328
+
329
+ download(lines.join("\n"), `clauseguard-report-${timestamp()}.txt`, "text/plain");
330
+ }
331
+
332
+ // ═══════════════════════════════════════════════════════════════
333
+ // HTML Export (self-contained styled report)
334
+ // ═══════════════════════════════════════════════════════════════
335
+
336
+ export function exportHTML(results: AnalysisResult) {
337
+ const flagged = results.results.filter(r => r.categories.length > 0);
338
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
339
+ flagged.forEach(r => r.categories.forEach(c => {
340
+ if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++;
341
+ }));
342
+
343
+ const sevColor: Record<string, string> = { CRITICAL: "#dc2626", HIGH: "#ea580c", MEDIUM: "#ca8a04", LOW: "#16a34a" };
344
+
345
+ const clauseHTML = flagged.map(clause => {
346
+ const tags = clause.categories.map(c =>
347
+ `<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>`
348
+ ).join("");
349
+ return `<div style="border:1px solid #e5e7eb;border-radius:8px;padding:16px;margin-bottom:12px;">
350
+ <div style="margin-bottom:8px;">${tags}</div>
351
+ <p style="font-size:13px;color:#374151;line-height:1.7;margin:0;">${clause.text.replace(/</g, "&lt;").slice(0, 500)}</p>
352
+ </div>`;
353
+ }).join("\n");
354
+
355
+ const entityHTML = (() => {
356
+ const grouped: Record<string, string[]> = {};
357
+ results.entities.forEach(e => {
358
+ if (!grouped[e.type]) grouped[e.type] = [];
359
+ if (!grouped[e.type].includes(e.text)) grouped[e.type].push(e.text);
360
+ });
361
+ return Object.entries(grouped).map(([type, items]) =>
362
+ `<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>`
363
+ ).join("\n");
364
+ })();
365
+
366
+ const html = `<!DOCTYPE html>
367
+ <html lang="en">
368
+ <head>
369
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
370
+ <title>ClauseGuard Report β€” ${new Date().toLocaleDateString()}</title>
371
+ <style>
372
+ *{margin:0;padding:0;box-sizing:border-box}
373
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#1f2937;background:#fff;padding:40px;max-width:800px;margin:0 auto}
374
+ h1{font-size:24px;font-weight:700;margin-bottom:4px}
375
+ h2{font-size:16px;font-weight:600;margin:24px 0 12px;padding-bottom:8px;border-bottom:1px solid #e5e7eb}
376
+ .meta{font-size:12px;color:#9ca3af}
377
+ .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}
378
+ .score{font-size:36px;font-weight:700}
379
+ .grade{font-size:18px;font-weight:700;padding:6px 16px;border-radius:8px;border:1px solid #e5e7eb}
380
+ .sev-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:12px 0}
381
+ .sev-item{text-align:center;padding:8px;border-radius:8px}
382
+ .disclaimer{margin-top:32px;padding:12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px;font-size:11px;color:#92400e}
383
+ @media print{body{padding:20px}h2{break-before:auto}}
384
+ </style>
385
+ </head>
386
+ <body>
387
+ <h1>πŸ›‘οΈ ClauseGuard Analysis Report</h1>
388
+ <p class="meta">${new Date().toLocaleString()} Β· ${results.model !== "regex" ? "ML Models" : "Pattern Matching"}</p>
389
+
390
+ <div class="score-card">
391
+ <div>
392
+ <p class="meta">RISK SCORE</p>
393
+ <p class="score">${results.risk_score}<span style="font-size:16px;color:#9ca3af">/100</span></p>
394
+ </div>
395
+ <span class="grade">Grade ${results.grade}</span>
396
+ </div>
397
+
398
+ <div class="sev-grid">
399
+ <div class="sev-item" style="background:#fef2f2"><strong style="color:#dc2626">${sevCounts.CRITICAL}</strong><br><small style="color:#dc2626">Critical</small></div>
400
+ <div class="sev-item" style="background:#fff7ed"><strong style="color:#ea580c">${sevCounts.HIGH}</strong><br><small style="color:#ea580c">High</small></div>
401
+ <div class="sev-item" style="background:#fefce8"><strong style="color:#ca8a04">${sevCounts.MEDIUM}</strong><br><small style="color:#ca8a04">Medium</small></div>
402
+ <div class="sev-item" style="background:#f0fdf4"><strong style="color:#16a34a">${sevCounts.LOW}</strong><br><small style="color:#16a34a">Low</small></div>
403
+ </div>
404
+
405
+ <p class="meta">${results.total_clauses} clauses Β· ${results.flagged_count} flagged Β· ${results.entities.length} entities Β· ${results.obligations.length} obligations</p>
406
+
407
+ ${flagged.length > 0 ? `<h2>⚠️ Flagged Clauses (${flagged.length})</h2>${clauseHTML}` : ""}
408
+ ${results.entities.length > 0 ? `<h2>🏷️ Entities (${results.entities.length})</h2>${entityHTML}` : ""}
409
+ ${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("")}` : ""}
410
+ ${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>` : ""}
411
+ ${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("")}` : ""}
412
+
413
+ <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>
414
+ </body>
415
+ </html>`;
416
+
417
+ download(html, `clauseguard-report-${timestamp()}.html`, "text/html");
418
+ }
419
+
420
+ // ═══════════════════════════════════════════════════════════════
421
+ // PDF Export (via server-side API route)
422
+ // ═══════════════════════════════════════════════════════════════
423
+
424
+ export async function exportPDF(results: AnalysisResult) {
425
+ try {
426
+ const res = await fetch("/api/pdf/report", {
427
+ method: "POST",
428
+ headers: { "Content-Type": "application/json" },
429
+ body: JSON.stringify(results),
430
+ });
431
+ if (!res.ok) throw new Error("PDF generation failed");
432
+ const blob = await res.blob();
433
+ download(blob, `clauseguard-report-${timestamp()}.pdf`, "application/pdf");
434
+ return true;
435
+ } catch {
436
+ // Fallback: print HTML version
437
+ exportHTML(results);
438
+ return false;
439
+ }
440
+ }
441
+
442
+ // ═══════════════════════════════════════════════════════════════
443
+ // Export formats manifest (for the UI dropdown)
444
+ // ═══════════════════════════════════════════════════════════════
445
+
446
+ export const EXPORT_FORMATS = [
447
+ { key: "pdf", label: "PDF Report", icon: "πŸ“„", description: "Formatted PDF document", fn: exportPDF },
448
+ { key: "html", label: "HTML Report", icon: "🌐", description: "Styled HTML (printable)", fn: exportHTML },
449
+ { key: "md", label: "Markdown", icon: "πŸ“", description: "GitHub-flavored markdown", fn: exportMarkdown },
450
+ { key: "txt", label: "Plain Text", icon: "πŸ“‹", description: "Simple text format", fn: exportText },
451
+ { key: "csv", label: "CSV Spreadsheet", icon: "πŸ“Š", description: "For Excel / Google Sheets", fn: exportCSV },
452
+ { key: "json", label: "JSON (formatted)", icon: "πŸ”§", description: "Full structured data", fn: (r: AnalysisResult) => exportJSON(r, true) },
453
+ { key: "json-raw", label: "JSON (raw)", icon: "⚑", description: "Compact, no whitespace", fn: (r: AnalysisResult) => exportJSON(r, false) },
454
+ ] as const;