Spaces:
Runtime error
Runtime error
| import { format } from "date-fns"; | |
| import type { Analysis, PredictionClass } from "../context/AnalysisContext"; | |
| const CLASS_FULL: Record<PredictionClass, string> = { | |
| AD: "Alzheimer's Disease", | |
| MCI: "Mild Cognitive Impairment", | |
| Healthy: "No Cognitive Impairment", | |
| }; | |
| function escapeHtml(text: string): string { | |
| return text | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """); | |
| } | |
| function imgBlock(src: string | null | undefined, alt: string): string { | |
| if (!src) return `<p class="muted">No image available</p>`; | |
| return `<img src="${src}" alt="${escapeHtml(alt)}" class="viz-img" />`; | |
| } | |
| function confidenceRows(scores?: { AD: number; MCI: number; Healthy: number }): string { | |
| if (!scores) return ""; | |
| return (["AD", "MCI", "Healthy"] as const) | |
| .map( | |
| (cls) => | |
| `<tr><td>${cls}</td><td>${scores[cls].toFixed(1)}%</td><td>${escapeHtml(CLASS_FULL[cls])}</td></tr>`, | |
| ) | |
| .join(""); | |
| } | |
| function buildReportHtml(result: Analysis): string { | |
| const dateStr = format(result.date, "MMMM d, yyyy 'at' h:mm a"); | |
| const isPatient = result.analysisMode === "patient"; | |
| const trialTable = isPatient && result.trialResults?.length | |
| ? ` | |
| <h2>Trial Breakdown</h2> | |
| <table> | |
| <thead> | |
| <tr><th>Trial</th><th>File</th><th>Prediction</th><th>Confidence</th></tr> | |
| </thead> | |
| <tbody> | |
| ${result.trialResults | |
| .map( | |
| (t) => | |
| `<tr> | |
| <td>${t.trialNo != null ? `t${t.trialNo}` : "—"}</td> | |
| <td>${escapeHtml(t.fileName)}</td> | |
| <td><span class="badge badge-${t.prediction.toLowerCase()}">${t.prediction}</span></td> | |
| <td>${t.confidence.toFixed(1)}%</td> | |
| </tr>`, | |
| ) | |
| .join("")} | |
| </tbody> | |
| </table>` | |
| : ""; | |
| const voteSection = | |
| isPatient && result.voteCounts | |
| ? ` | |
| <h2>Vote Distribution</h2> | |
| <div class="stats"> | |
| ${(["AD", "MCI", "Healthy"] as const) | |
| .map((cls) => { | |
| const count = result.voteCounts?.[cls] ?? 0; | |
| const total = result.trialCount ?? 0; | |
| const pct = total > 0 ? ((count / total) * 100).toFixed(0) : "0"; | |
| return `<div class="stat ${cls === result.prediction ? "stat-win" : ""}"> | |
| <div class="stat-value">${count}/${total}</div> | |
| <div class="stat-label">${cls}</div> | |
| <div class="stat-pct">${pct}%</div> | |
| </div>`; | |
| }) | |
| .join("")} | |
| </div>` | |
| : ""; | |
| const vizSection = | |
| !isPatient | |
| ? ` | |
| <h2>Visualizations</h2> | |
| <div class="viz-grid"> | |
| <div> | |
| <h3>Spectrogram Montage</h3> | |
| ${imgBlock(result.spectrogramImage, "Spectrogram montage")} | |
| </div> | |
| <div> | |
| <h3>Scalogram Montage</h3> | |
| ${imgBlock(result.scalogramMontageImage, "Scalogram montage")} | |
| </div> | |
| </div>` | |
| : ""; | |
| return `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>NeuroEEG Analysis Report</title> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { font-family: system-ui, -apple-system, Segoe UI, sans-serif; color: #0f172a; max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.5; } | |
| h1 { font-size: 1.75rem; margin: 0 0 0.25rem; } | |
| h2 { font-size: 1.15rem; margin: 2rem 0 0.75rem; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.35rem; } | |
| h3 { font-size: 0.95rem; margin: 0 0 0.5rem; color: #475569; } | |
| .meta { color: #64748b; font-size: 0.9rem; margin-bottom: 1.5rem; } | |
| .prediction-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1.25rem; margin: 1rem 0; } | |
| .prediction-label { font-size: 0.85rem; color: #64748b; margin-bottom: 0.25rem; } | |
| .prediction-value { font-size: 1.5rem; font-weight: 700; } | |
| .prediction-sub { font-size: 0.9rem; color: #64748b; margin-top: 0.25rem; } | |
| table { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin-top: 0.5rem; } | |
| th, td { text-align: left; padding: 0.6rem 0.75rem; border-bottom: 1px solid #e2e8f0; } | |
| th { color: #64748b; font-weight: 600; } | |
| .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.8rem; font-weight: 600; } | |
| .badge-ad { background: #fee2e2; color: #991b1b; } | |
| .badge-mci { background: #fef3c7; color: #92400e; } | |
| .badge-healthy { background: #dcfce7; color: #166534; } | |
| .stats { display: flex; gap: 1rem; flex-wrap: wrap; } | |
| .stat { flex: 1; min-width: 100px; text-align: center; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; } | |
| .stat-win { background: #f8fafc; border-color: #94a3b8; } | |
| .stat-value { font-size: 1.4rem; font-weight: 700; } | |
| .stat-label { font-weight: 600; margin-top: 0.25rem; } | |
| .stat-pct { font-size: 0.8rem; color: #64748b; } | |
| .viz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } | |
| .viz-img { max-width: 100%; border: 1px solid #e2e8f0; border-radius: 6px; } | |
| .muted { color: #94a3b8; font-size: 0.85rem; } | |
| .disclaimer { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 1rem; font-size: 0.85rem; color: #1e3a8a; margin-top: 2rem; } | |
| .footer { margin-top: 2rem; font-size: 0.75rem; color: #94a3b8; } | |
| @media print { body { padding: 1rem; } } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>NeuroEEG Analysis Report</h1> | |
| <p class="meta">${escapeHtml(dateStr)} · ${isPatient ? "Patient-level analysis" : "Single trial analysis"}</p> | |
| <div class="prediction-box"> | |
| <div class="prediction-label">${isPatient ? "Final Patient Prediction" : "Predicted Class"}</div> | |
| <div class="prediction-value">${result.prediction}</div> | |
| <div class="prediction-sub">${escapeHtml(CLASS_FULL[result.prediction])}</div> | |
| ${ | |
| isPatient | |
| ? `<div class="prediction-sub">Agreement: ${result.agreementRatio?.toFixed(1) ?? result.confidence.toFixed(1)}% · Patient ID: ${escapeHtml(result.patientId ?? "—")} · ${result.trialCount ?? 0} trials</div>` | |
| : `<div class="prediction-sub">Confidence: ${result.confidence.toFixed(1)}% · File: ${escapeHtml(result.fileName)}</div>` | |
| } | |
| </div> | |
| <h2>Confidence Scores</h2> | |
| ${ | |
| result.confidenceScores | |
| ? `<table> | |
| <thead><tr><th>Class</th><th>Score</th><th>Description</th></tr></thead> | |
| <tbody>${confidenceRows(result.confidenceScores)}</tbody> | |
| </table>` | |
| : `<p class="muted">Per-class confidence scores are shown at the trial level for patient analyses.</p>` | |
| } | |
| <p class="meta">Model: ${escapeHtml(result.model ?? "Fusion")}${result.ensembleFolds && result.ensembleFolds > 1 ? ` · ${result.ensembleFolds}-fold ensemble (soft voting)` : ""}</p> | |
| ${voteSection} | |
| ${trialTable} | |
| ${vizSection} | |
| <div class="disclaimer"> | |
| This analysis is intended for research and clinical decision support purposes only. | |
| Results should be interpreted by qualified medical professionals in conjunction with | |
| other clinical assessments and patient history. This system is not a substitute for | |
| professional medical diagnosis. | |
| </div> | |
| <p class="footer">Generated by NeuroEEG · Clinical Brain Signal Analysis</p> | |
| </body> | |
| </html>`; | |
| } | |
| function reportFilename(result: Analysis): string { | |
| const stamp = format(result.date, "yyyy-MM-dd"); | |
| if (result.analysisMode === "patient" && result.patientId) { | |
| return `neuroeeg-patient-${result.patientId}-${stamp}.html`; | |
| } | |
| const base = result.fileName.replace(/\.[^.]+$/, "").replace(/[^\w.-]+/g, "_"); | |
| return `neuroeeg-${base}-${stamp}.html`; | |
| } | |
| export function downloadAnalysisReport(result: Analysis): void { | |
| const html = buildReportHtml(result); | |
| const blob = new Blob([html], { type: "text/html;charset=utf-8" }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement("a"); | |
| link.href = url; | |
| link.download = reportFilename(result); | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(url); | |
| } | |