/** * OmniDiag — Clinical PDF Report * * Rendered entirely client-side via @react-pdf/renderer. * No server round-trip; generates in the browser and downloads immediately. * * Layout: * 1. Header — OmniDiag logo text + report date * 2. Patient — name, DOB, gender, MRN (if available) * 3. Diagnosis — prediction badge, confidence bar, clinical summary * 4. SHAP chart — embedded as a PNG snapshot (passed as dataUrl prop) * 5. Counterfactuals — up to 3 scenarios as a formatted table * 6. Footer — doctor name, clinic, signature line */ import { Document, Page, Text, View, StyleSheet, Image, Font, } from '@react-pdf/renderer' // ── Colour palette (matches Tailwind clinical theme) ────────────────────────── const C = { primary: '#0f172a', // slate-900 accent: '#2563eb', // blue-600 positive: '#dc2626', // red-600 — high risk negative: '#16a34a', // green-600 — low risk border: '#e2e8f0', // slate-200 muted: '#64748b', // slate-500 light: '#f8fafc', // slate-50 white: '#ffffff', } const styles = StyleSheet.create({ page: { fontFamily: 'Helvetica', fontSize: 9, color: C.primary, paddingTop: 40, paddingBottom: 60, paddingHorizontal: 48, backgroundColor: C.white, }, // ── Header ───────────────────────────────────────────────────────────────── header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', borderBottomWidth: 2, borderBottomColor: C.accent, paddingBottom: 10, marginBottom: 16, }, logoText: { fontSize: 18, fontFamily: 'Helvetica-Bold', color: C.accent }, logoSub: { fontSize: 8, color: C.muted, marginTop: 2 }, headerRight: { alignItems: 'flex-end' }, headerLabel: { fontSize: 7, color: C.muted, textTransform: 'uppercase', letterSpacing: 0.5 }, headerValue: { fontSize: 9, color: C.primary, marginTop: 1 }, // ── Section ──────────────────────────────────────────────────────────────── section: { marginBottom: 14 }, sectionTitle: { fontSize: 10, fontFamily: 'Helvetica-Bold', color: C.accent, textTransform: 'uppercase', letterSpacing: 0.8, borderBottomWidth: 1, borderBottomColor: C.border, paddingBottom: 4, marginBottom: 8, }, // ── 2-column row ─────────────────────────────────────────────────────────── row: { flexDirection: 'row', marginBottom: 4 }, cell: { flex: 1 }, label: { fontSize: 7, color: C.muted, textTransform: 'uppercase', letterSpacing: 0.4 }, value: { fontSize: 9, color: C.primary, marginTop: 1 }, // ── Diagnosis badge ──────────────────────────────────────────────────────── badgeRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 10, gap: 10 }, badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 4, fontSize: 11, fontFamily: 'Helvetica-Bold', }, // ── Confidence bar ───────────────────────────────────────────────────────── confLabel: { fontSize: 7, color: C.muted, marginBottom: 3 }, confBarBg: { height: 6, backgroundColor: C.border, borderRadius: 3, width: 160 }, confBarFill: { height: 6, borderRadius: 3 }, // ── Clinical summary text ────────────────────────────────────────────────── summaryBox: { backgroundColor: C.light, borderLeftWidth: 3, borderLeftColor: C.accent, padding: 8, borderRadius: 2, marginTop: 6, }, summaryText: { fontSize: 9, color: C.primary, lineHeight: 1.5 }, // ── SHAP chart image ─────────────────────────────────────────────────────── chartImage: { width: '100%', maxHeight: 180, objectFit: 'contain', marginTop: 4 }, // ── Counterfactuals table ────────────────────────────────────────────────── table: { borderWidth: 1, borderColor: C.border, borderRadius: 2 }, tableHeader: { flexDirection: 'row', backgroundColor: C.light, borderBottomWidth: 1, borderBottomColor: C.border, paddingVertical: 5, paddingHorizontal: 8, }, tableHeaderCell: { fontFamily: 'Helvetica-Bold', fontSize: 7, color: C.muted, textTransform: 'uppercase' }, tableRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: C.border, paddingVertical: 5, paddingHorizontal: 8, }, tableCell: { fontSize: 8, color: C.primary }, feasBadge: { fontSize: 7, fontFamily: 'Helvetica-Bold', paddingHorizontal: 5, paddingVertical: 2, borderRadius: 3, }, // ── Footer ───────────────────────────────────────────────────────────────── footer: { position: 'absolute', bottom: 30, left: 48, right: 48, borderTopWidth: 1, borderTopColor: C.border, paddingTop: 8, }, footerRow: { flexDirection: 'row', justifyContent: 'space-between' }, footerLabel: { fontSize: 7, color: C.muted }, footerValue: { fontSize: 8, color: C.primary }, signatureLine: { borderBottomWidth: 1, borderBottomColor: C.primary, width: 160, marginTop: 20, marginBottom: 2, }, signatureLabel: { fontSize: 7, color: C.muted }, pageNum: { fontSize: 7, color: C.muted, textAlign: 'center', marginTop: 4 }, }) // ── Helpers ─────────────────────────────────────────────────────────────────── function LabelValue({ label, value }) { return ( {label} {value || '—'} ) } function feasColor(f) { if (f === 'high') return { backgroundColor: '#dcfce7', color: '#15803d' } if (f === 'medium') return { backgroundColor: '#fef9c3', color: '#a16207' } return { backgroundColor: '#fee2e2', color: '#b91c1c' } } // ── Main PDF document ───────────────────────────────────────────────────────── export default function PDFReport({ patient, disease, result, shapText, counterfactuals, shapImageUrl, doctorName, clinicName, reportDate, }) { const isPositive = result?.prediction === 1 const confidence = Math.round((result?.confidence ?? 0) * 100) const diagnosisLabel = result?.diagnosis ?? (isPositive ? 'Positive' : 'Negative') const badgeStyle = isPositive ? { ...styles.badge, backgroundColor: '#fee2e2', color: C.positive } : { ...styles.badge, backgroundColor: '#dcfce7', color: C.negative } const barColor = isPositive ? C.positive : C.negative const cfs = (counterfactuals ?? []).slice(0, 3) const dateStr = reportDate ?? new Date().toLocaleDateString('en-GB') return ( {/* ── Header ── */} OmniDiag Multi-Disease Clinical Decision Support Report Date {dateStr} Disease Module {disease?.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) ?? '—'} {/* ── Patient Information ── */} Patient Information {patient?.contact_email && ( )} {/* ── Diagnosis Summary ── */} AI Diagnosis Summary {diagnosisLabel} Model Confidence {confidence}% {shapText && ( {shapText} )} {/* ── SHAP Chart ── */} {shapImageUrl && ( Feature Impact Analysis (SHAP) )} {/* ── Recommended Interventions ── */} {cfs.length > 0 && ( Recommended Lifestyle Interventions Scenario Risk Reduction Feasibility {cfs.map((cf, i) => ( {cf.scenario} {cf.risk_reduction} {cf.feasibility?.toUpperCase()} ))} )} {/* ── Footer ── */} Dr. {doctorName ?? 'Physician Name'} — {clinicName ?? 'OmniDiag Clinic'} Generated by OmniDiag v4.0.0 For clinical decision support only. Not a substitute for professional medical judgment. `Page ${pageNumber} of ${totalPages}` } /> ) }