Files
BrainWind/app/src/lib/audit/export-audit-pdf.tsx
T

201 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Document, Page, Text, View, StyleSheet, pdf, Image } from '@react-pdf/renderer';
import type { AuditReport, ModuleAuditResult, ScenarioResult, CheckStatus } from './types';
const styles = StyleSheet.create({
page: {
flexDirection: 'column',
padding: 40,
fontSize: 10,
fontFamily: 'Helvetica',
color: '#333',
},
header: {
marginBottom: 20,
borderBottom: '2pt solid #6b21a8',
paddingBottom: 10,
},
title: { fontSize: 20, fontWeight: 'bold', color: '#6b21a8' },
subtitle: { fontSize: 10, color: '#666', marginTop: 4 },
section: { marginTop: 15, marginBottom: 10 },
sectionTitle: { fontSize: 14, fontWeight: 'bold', marginBottom: 8, color: '#111' },
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 4,
},
summaryLabel: { fontWeight: 'bold', width: 200 },
summaryValue: { flex: 1 },
moduleCard: {
marginTop: 12,
padding: 10,
border: '1pt solid #ddd',
borderRadius: 4,
},
moduleTitle: { fontSize: 12, fontWeight: 'bold', marginBottom: 6, color: '#6b21a8' },
moduleStats: { fontSize: 9, color: '#555', marginBottom: 4 },
scenarioItem: {
marginTop: 6,
padding: 6,
backgroundColor: '#f9fafb',
borderRadius: 3,
},
scenarioId: { fontSize: 9, fontWeight: 'bold', marginBottom: 3, color: '#111' },
checkRow: { flexDirection: 'row', marginBottom: 2, fontSize: 8 },
statusBadge: { width: 40, fontWeight: 'bold' },
checkLabel: { width: 120 },
checkValue: { flex: 1 },
footer: {
position: 'absolute',
bottom: 30,
left: 40,
right: 40,
textAlign: 'center',
color: '#999',
fontSize: 8,
borderTop: '1pt solid #eaeaea',
paddingTop: 10,
},
});
function statusColor(s: CheckStatus): string {
switch (s) {
case 'PASS': return '#16a34a';
case 'WARN': return '#d97706';
case 'FAIL': return '#dc2626';
}
}
function sanitizeForPDF(text: string | undefined): string {
if (!text) return '';
return text
.replace(/φ/g, 'phi')
.replace(/ϕ/g, 'phi')
.replace(/α/g, 'alpha')
.replace(/θ/g, 'theta')
.replace(/β/g, 'beta')
.replace(/γ/g, 'gamma')
.replace(/Δ/g, 'Delta')
.replace(/—/g, '-')
.replace(//g, '-')
.replace(/“/g, '"')
.replace(/”/g, '"')
.replace(//g, "'")
.replace(//g, "'")
.replace(/Æ/g, 'phi') // Fix specifically the one seen in user screenshot
.replace(/≤/g, '<=')
.replace(/≥/g, '>=')
.replace(/≈/g, '~')
.replace(/×/g, 'x')
.replace(/÷/g, '/')
.replace(/²/g, '^2')
.replace(/³/g, '^3')
.replace(/°/g, ' deg');
}
interface ReportPDFProps {
report: AuditReport;
}
function AuditReportDocument({ report }: ReportPDFProps) {
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={[styles.header, { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }]}>
<View>
<Text style={styles.title}>Relatório de Auditoria</Text>
<Text style={styles.subtitle}>
Auditor IA: {report.provider} ({report.model}) | {new Date(report.timestamp).toLocaleString('pt-BR')}
</Text>
</View>
{typeof window !== 'undefined' && window.location && (
<Image src={`${window.location.origin}/logo_brainwind.png`} style={{ width: 120, height: 40, objectFit: 'contain' }} />
)}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Resumo</Text>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Total de Cenários:</Text>
<Text style={styles.summaryValue}>{report.totalScenarios}</Text>
</View>
<View style={styles.summaryRow}>
<Text style={[styles.summaryLabel, { color: '#16a34a' }]}>Aprovado:</Text>
<Text style={styles.summaryValue}>{report.totalPassed}</Text>
</View>
<View style={styles.summaryRow}>
<Text style={[styles.summaryLabel, { color: '#d97706' }]}>Atenção:</Text>
<Text style={styles.summaryValue}>{report.totalWarnings}</Text>
</View>
<View style={styles.summaryRow}>
<Text style={[styles.summaryLabel, { color: '#dc2626' }]}>Falha:</Text>
<Text style={styles.summaryValue}>{report.totalFailed}</Text>
</View>
</View>
{report.modules.map((mod: ModuleAuditResult) => (
<View key={mod.module} style={styles.moduleCard}>
<Text style={styles.moduleTitle}>
{mod.moduleLabel} {mod.passed}/{mod.totalScenarios} (Alertas: {mod.warnings} | Falhas: {mod.failed})
</Text>
{mod.scenarios.map((sc: ScenarioResult) => {
const summaryTranslated = (sc.summary || '')
.replace(/Verdict PASS\.?/gi, 'Veredito: APROVADO.')
.replace(/Verdict WARN\.?/gi, 'Veredito: ATENÇÃO.')
.replace(/Verdict FAIL\.?/gi, 'Veredito: FALHA.');
return (
<View key={sc.scenarioId} style={styles.scenarioItem} wrap={false}>
<Text style={styles.scenarioId}>
{sanitizeForPDF(sc.scenarioId)} {sc.verdict === 'PASS' ? '✓' : sc.verdict === 'WARN' ? '⚠' : '✗'} {sanitizeForPDF(summaryTranslated)}
</Text>
{sc.enunciado_problema && (
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Enunciado: {sanitizeForPDF(sc.enunciado_problema)}</Text>
)}
{sc.tabelas_nbr_consultadas && (
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Tabelas: {sanitizeForPDF(sc.tabelas_nbr_consultadas)}</Text>
)}
{sc.resultado_app && (
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Avaliação do App: {sanitizeForPDF(sc.resultado_app)}</Text>
)}
{sc.calculation_steps && (
<Text style={{ fontSize: 8, color: '#444', marginBottom: 4, fontStyle: 'italic' }}>
CoT: {sanitizeForPDF(sc.calculation_steps.substring(0, 300))}{sc.calculation_steps.length > 300 ? '...' : ''}
</Text>
)}
{sc.checks.map((chk, i) => (
<View key={i} style={styles.checkRow}>
<Text style={[styles.statusBadge, { color: statusColor(chk.status) }]}>{chk.status}</Text>
<Text style={styles.checkLabel}>{sanitizeForPDF(chk.name)}</Text>
<Text style={styles.checkValue}>{sanitizeForPDF(chk.actual)} ({sanitizeForPDF(chk.expected)})</Text>
</View>
))}
</View>
);
})}
</View>
))}
<Text style={styles.footer}>
Gerado por BrainWind v1.0 Ferramenta de Auditoria NBR 6123:2023
</Text>
</Page>
</Document>
);
}
export async function exportAuditReportToPDF(report: AuditReport): Promise<void> {
const blob = await pdf(<AuditReportDocument report={report} />).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const dateStr = new Date(report.timestamp).toLocaleDateString('pt-BR').replace(/\//g, '-');
const safeModel = report.model.split('/').pop()?.replace(/[^a-zA-Z0-9_-]/g, '-') || 'LLM';
const filename = `Audit-${safeModel}_${dateStr}.pdf`;
link.setAttribute('href', url);
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}