No scans yet
Analyze a CT scan to start building your history.
const STORAGE_KEY = "oncovision.history";
const routeViews = {
landing: document.getElementById("landingPage"),
upload: document.getElementById("uploadPage"),
analysis: document.getElementById("analysisPage"),
results: document.getElementById("resultsPage"),
history: document.getElementById("historyPage"),
technical: document.getElementById("technicalPage"),
};
const state = {
history: loadHistory(),
selectedFiles: [],
currentScan: null,
analysisTimer: null,
analysisStepTimer: null,
analysisResultById: new Map(),
activeModal: null,
};
const uploadInput = document.getElementById("ctScan");
const uploadZone = document.getElementById("uploadZone");
const uploadForm = document.getElementById("uploadForm");
const selectedFilePanel = document.getElementById("selectedFilePanel");
const selectedFileName = document.getElementById("selectedFileName");
const selectedFileMeta = document.getElementById("selectedFileMeta");
const fileFormatValue = document.getElementById("fileFormatValue");
const fileCountValue = document.getElementById("fileCountValue");
const fileSizeValue = document.getElementById("fileSizeValue");
const removeFileButton = document.getElementById("removeFileButton");
const analyzeButton = document.getElementById("analyzeButton");
const sampleButton = document.getElementById("sampleButton");
const uploadErrorBanner = document.getElementById("uploadErrorBanner");
const recentUploads = document.getElementById("recentUploads");
const analysisDetail = document.getElementById("analysisDetail");
const analysisProgressBar = document.getElementById("analysisProgressBar");
const analysisProgressLabel = document.getElementById("analysisProgressLabel");
const analysisSteps = Array.from(document.querySelectorAll(".analysis-steps span"));
const riskBanner = document.getElementById("riskBanner");
const riskIcon = document.getElementById("riskIcon");
const riskLabel = document.getElementById("riskLabel");
const riskSummary = document.getElementById("riskSummary");
const riskRecommendation = document.getElementById("riskRecommendation");
const noduleCount = document.getElementById("noduleCount");
const analysisDate = document.getElementById("analysisDate");
const analysisStatus = document.getElementById("analysisStatus");
const resultsMeta = document.getElementById("resultsMeta");
const scanFileName = document.getElementById("scanFileName");
const scanFileSize = document.getElementById("scanFileSize");
const scanFileFormat = document.getElementById("scanFileFormat");
const recommendationText = document.getElementById("recommendationText");
const timingGrid = document.getElementById("timingGrid");
const scanImage = document.getElementById("scanImage");
const scanPlaceholder = document.getElementById("scanPlaceholder");
const nodulesList = document.getElementById("nodulesList");
const emptyState = document.getElementById("emptyState");
const noduleFilter = document.getElementById("noduleFilter");
const resultsBreadcrumb = document.getElementById("resultsBreadcrumb");
const historySubtitle = document.getElementById("historySubtitle");
const historySearch = document.getElementById("historySearch");
const historyFilter = document.getElementById("historyFilter");
const historySort = document.getElementById("historySort");
const historyGroups = document.getElementById("historyGroups");
const noduleCardTemplate = document.getElementById("noduleCardTemplate");
const historyCardTemplate = document.getElementById("historyCardTemplate");
const exportImageButton = document.getElementById("exportImageButton");
const exportPdfButton = document.getElementById("exportPdfButton");
const floatingExportButton = document.getElementById("floatingExportButton");
const shareButton = document.getElementById("shareButton");
const saveScanButton = document.getElementById("saveScanButton");
const deleteScanButton = document.getElementById("deleteScanButton");
const previousScanButton = document.getElementById("previousScanButton");
const nextScanButton = document.getElementById("nextScanButton");
const mobileNavToggle = document.getElementById("mobileNavToggle");
const mobileNav = document.getElementById("mobileNav");
const topbar = document.getElementById("topbar");
const modalBackdrop = document.getElementById("modalBackdrop");
const settingsModal = document.getElementById("settingsModal");
const helpModal = document.getElementById("helpModal");
const exportModal = document.getElementById("exportModal");
const shareModal = document.getElementById("shareModal");
const technicalTabs = Array.from(document.querySelectorAll("[data-tech-tab]"));
const previewExportButton = document.getElementById("previewExportButton");
const patientNameInput = document.getElementById("patientNameInput");
const patientMrnInput = document.getElementById("patientMrnInput");
const patientDobInput = document.getElementById("patientDobInput");
const physicianNotesInput = document.getElementById("physicianNotesInput");
const confirmShareButton = document.getElementById("confirmShareButton");
const shareEmailInput = document.getElementById("shareEmailInput");
const shareMessageInput = document.getElementById("shareMessageInput");
const themeRadios = Array.from(document.querySelectorAll("input[name='theme']"));
const THEME_KEY = "oncovision.theme";
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
const analysisMessages = [
"Processing 324 slices",
"Detecting nodules",
"Calculating risk",
];
wireNavigation();
wireUpload();
wireActions();
wireModals();
wireTechnicalTabs();
wireThemeSelector();
renderRecentUploads();
renderHistoryPage();
updateNavState();
handleRoute();
window.addEventListener("popstate", handleRoute);
window.addEventListener("scroll", () => {
topbar.classList.toggle("topbar--solid", window.scrollY > 24);
});
function wireNavigation() {
document.querySelectorAll("[data-link]").forEach((link) => {
link.addEventListener("click", (event) => {
const href = link.getAttribute("href");
if (!href || href.startsWith("http")) {
return;
}
event.preventDefault();
mobileNav.hidden = true;
mobileNav.classList.remove("is-open");
navigate(href);
});
});
mobileNavToggle.addEventListener("click", () => {
const willOpen = mobileNav.hidden;
mobileNav.hidden = !willOpen;
mobileNav.classList.toggle("is-open", willOpen);
});
}
function wireUpload() {
uploadInput.addEventListener("change", () => {
state.selectedFiles = Array.from(uploadInput.files || []);
syncSelectedFiles();
});
["dragenter", "dragover"].forEach((eventName) => {
uploadZone.addEventListener(eventName, (event) => {
event.preventDefault();
uploadZone.classList.add("is-dragging");
});
});
["dragleave", "drop"].forEach((eventName) => {
uploadZone.addEventListener(eventName, (event) => {
event.preventDefault();
uploadZone.classList.remove("is-dragging");
});
});
uploadZone.addEventListener("drop", (event) => {
const files = Array.from(event.dataTransfer?.files || []);
if (!files.length) {
return;
}
state.selectedFiles = files;
uploadInput.files = event.dataTransfer.files;
syncSelectedFiles();
});
removeFileButton.addEventListener("click", () => {
state.selectedFiles = [];
uploadInput.value = "";
syncSelectedFiles();
});
uploadForm.addEventListener("submit", async (event) => {
event.preventDefault();
if (!state.selectedFiles.length) {
showUploadError("Select at least one scan file before starting analysis.");
return;
}
hideUploadError();
const scanId = createScanId();
const fileSummary = createFileSummary(state.selectedFiles);
const scan = {
id: scanId,
fileSummary,
createdAt: new Date().toISOString(),
status: "Analyzing",
};
state.currentScan = scan;
navigate(`/analysis/${scanId}`);
startAnalysis(scan, async () => {
try {
const result = await analyzeSelectedFiles();
finalizeAnalysis(scan, result);
} catch (error) {
stopAnalysisAnimation();
navigate("/upload");
showUploadError(error.message || "Analysis could not be completed.");
}
});
});
sampleButton.addEventListener("click", () => {
const scanId = createScanId();
const sample = buildSampleScan(scanId);
state.currentScan = sample;
navigate(`/analysis/${scanId}`);
startAnalysis(sample, () => {
window.setTimeout(() => {
finalizeAnalysis(sample, sample.result);
}, 2800);
});
});
}
function wireActions() {
noduleFilter.addEventListener("change", () => {
if (state.currentScan?.result) {
renderResultsPage(state.currentScan);
}
});
historySearch.addEventListener("input", renderHistoryPage);
historyFilter.addEventListener("change", renderHistoryPage);
historySort.addEventListener("change", renderHistoryPage);
exportImageButton.addEventListener("click", exportCurrentImage);
exportPdfButton.addEventListener("click", () => openModal("export"));
floatingExportButton.addEventListener("click", () => openModal("export"));
shareButton.addEventListener("click", () => openModal("share"));
saveScanButton.addEventListener("click", () => {
if (state.currentScan) {
toastLikeAlert("Scan saved to local history.");
}
});
deleteScanButton.addEventListener("click", deleteCurrentScan);
previousScanButton.addEventListener("click", () => navigateHistoryNeighbor(-1));
nextScanButton.addEventListener("click", () => navigateHistoryNeighbor(1));
}
function wireModals() {
document.getElementById("helpButton").addEventListener("click", () => openModal("help"));
document.getElementById("footerHelpButton").addEventListener("click", () => openModal("help"));
document.getElementById("footerSettingsButton").addEventListener("click", () => openModal("settings"));
previewExportButton.addEventListener("click", () => generatePdfReport(true));
document.getElementById("generateExportButton").addEventListener("click", () => {
generatePdfReport(false);
});
confirmShareButton.addEventListener("click", confirmShare);
modalBackdrop.addEventListener("click", (event) => {
if (event.target === modalBackdrop) {
openModal(null);
}
});
document.querySelectorAll("[data-close-modal]").forEach((button) => {
button.addEventListener("click", () => openModal(null));
});
}
function wireThemeSelector() {
const stored = localStorage.getItem(THEME_KEY) || "auto";
themeRadios.forEach((input) => {
input.checked = input.value === stored;
input.addEventListener("change", () => {
localStorage.setItem(THEME_KEY, input.value);
applyTheme(input.value);
});
});
applyTheme(stored);
const handleSystemThemeChange = () => {
if (localStorage.getItem(THEME_KEY) === "auto") {
applyTheme("auto");
}
};
if (typeof prefersDarkScheme.addEventListener === "function") {
prefersDarkScheme.addEventListener("change", handleSystemThemeChange);
} else if (typeof prefersDarkScheme.addListener === "function") {
prefersDarkScheme.addListener(handleSystemThemeChange);
}
}
function applyTheme(preference) {
const resolved =
preference === "dark"
? "dark"
: preference === "light"
? "light"
: prefersDarkScheme.matches
? "dark"
: "light";
document.body.classList.toggle("theme-dark", resolved === "dark");
document.body.classList.toggle("theme-light", resolved === "light");
document.documentElement.dataset.theme = preference;
}
function wireTechnicalTabs() {
technicalTabs.forEach((tab) => {
tab.addEventListener("click", () => {
const target = tab.dataset.techTab;
technicalTabs.forEach((item) => item.classList.toggle("is-active", item === tab));
document.querySelectorAll(".technical-panel").forEach((panel) => {
panel.hidden = panel.id !== `technicalPanel-${target}`;
});
});
});
document.querySelectorAll(".code-copy").forEach((button) => {
button.addEventListener("click", async () => {
const target = document.getElementById(button.dataset.copyTarget);
if (!target) {
return;
}
try {
await navigator.clipboard.writeText(target.textContent || "");
toastLikeAlert("Code copied to clipboard.");
} catch {
toastLikeAlert(target.textContent || "");
}
});
});
}
function navigate(path) {
window.history.pushState({}, "", path);
handleRoute();
}
function handleRoute() {
const route = parseRoute(window.location.pathname);
stopAnalysisAnimation();
Object.values(routeViews).forEach((view) => {
view.hidden = true;
});
if (route.name === "upload") {
routeViews.upload.hidden = false;
syncSelectedFiles();
renderRecentUploads();
} else if (route.name === "analysis") {
routeViews.analysis.hidden = false;
const scan = resolveScan(route.scanId);
if (!scan) {
navigate("/upload");
return;
}
state.currentScan = scan;
if (scan.result) {
navigate(`/results/${scan.id}`);
return;
}
} else if (route.name === "results") {
routeViews.results.hidden = false;
const scan = resolveScan(route.scanId);
if (!scan || !scan.result) {
navigate("/history");
return;
}
state.currentScan = scan;
renderResultsPage(scan);
} else if (route.name === "history") {
routeViews.history.hidden = false;
renderHistoryPage();
} else if (route.name === "technical") {
routeViews.technical.hidden = false;
} else {
routeViews.landing.hidden = false;
}
updateNavState();
window.scrollTo({ top: 0, behavior: "smooth" });
}
function parseRoute(pathname) {
if (pathname === "/upload") {
return { name: "upload" };
}
if (pathname === "/history") {
return { name: "history" };
}
if (pathname === "/technical") {
return { name: "technical" };
}
const analysisMatch = pathname.match(/^\/analysis\/([^/]+)$/);
if (analysisMatch) {
return { name: "analysis", scanId: analysisMatch[1] };
}
const resultsMatch = pathname.match(/^\/results\/([^/]+)$/);
if (resultsMatch) {
return { name: "results", scanId: resultsMatch[1] };
}
return { name: "landing" };
}
function updateNavState() {
const pathname = window.location.pathname;
document.querySelectorAll("[data-nav]").forEach((link) => {
const key = link.dataset.nav;
const isActive =
(key === "home" && pathname === "/") ||
(key === "upload" && pathname.startsWith("/upload")) ||
(key === "history" && pathname.startsWith("/history")) ||
(key === "technical" && pathname.startsWith("/technical"));
link.classList.toggle("is-active", isActive);
});
}
function syncSelectedFiles() {
if (!state.selectedFiles.length) {
selectedFilePanel.hidden = true;
return;
}
const summary = createFileSummary(state.selectedFiles);
selectedFilePanel.hidden = false;
selectedFileName.textContent = formatDisplayFileName(summary.name);
selectedFileName.title = summary.name;
selectedFileMeta.textContent = `${summary.size} • ${summary.format}`;
selectedFileMeta.title = `${summary.name} • ${summary.size} • ${summary.format}`;
fileFormatValue.textContent = summary.format;
fileCountValue.textContent = String(summary.fileCount);
fileSizeValue.textContent = summary.size;
}
function createFileSummary(files) {
const name = files.length === 1 ? files[0].name : `${files.length} files selected`;
const sizeBytes = files.reduce((sum, file) => sum + file.size, 0);
const extension = formatFileTypeLabel(files[0]?.name || "");
return {
name,
size: formatBytes(sizeBytes),
sizeBytes,
format: extension,
fileCount: files.length,
};
}
function formatFileTypeLabel(fileName) {
const lowerName = String(fileName || "").toLowerCase();
if (!lowerName) {
return "VOLUME";
}
if (lowerName.endsWith(".nii.gz")) {
return "NII.GZ";
}
const segments = lowerName.split(".").filter(Boolean);
if (segments.length <= 1) {
return "VOLUME";
}
return segments[segments.length - 1].toUpperCase();
}
function formatDisplayFileName(fileName, maxLength = 56) {
const value = String(fileName || "").trim();
if (!value) {
return "Unnamed file";
}
if (value.length <= maxLength) {
return value;
}
const head = Math.ceil((maxLength - 1) * 0.6);
const tail = Math.floor((maxLength - 1) * 0.4);
return `${value.slice(0, head)}…${value.slice(-tail)}`;
}
function showUploadError(message) {
uploadErrorBanner.hidden = false;
uploadErrorBanner.textContent = message;
}
function hideUploadError() {
uploadErrorBanner.hidden = true;
uploadErrorBanner.textContent = "";
}
function startAnalysis(scan, onStart) {
let progress = 12;
let stepIndex = 0;
analysisDetail.textContent = analysisMessages[stepIndex];
analysisProgressBar.style.width = `${progress}%`;
analysisProgressLabel.textContent = `${progress}% complete`;
setActiveAnalysisStep(stepIndex);
state.analysisStepTimer = window.setInterval(() => {
stepIndex = (stepIndex + 1) % analysisMessages.length;
analysisDetail.textContent = analysisMessages[stepIndex];
setActiveAnalysisStep(stepIndex);
}, 800);
state.analysisTimer = window.setInterval(() => {
progress = Math.min(progress + 9, 92);
analysisProgressBar.style.width = `${progress}%`;
analysisProgressLabel.textContent = `${progress}% complete`;
}, 550);
onStart();
}
function stopAnalysisAnimation() {
if (state.analysisTimer) {
window.clearInterval(state.analysisTimer);
state.analysisTimer = null;
}
if (state.analysisStepTimer) {
window.clearInterval(state.analysisStepTimer);
state.analysisStepTimer = null;
}
}
function setActiveAnalysisStep(stepIndex) {
analysisSteps.forEach((step, index) => {
step.classList.toggle("is-active", index === stepIndex);
});
}
async function analyzeSelectedFiles() {
const formData = new FormData();
state.selectedFiles.forEach((file) => {
formData.append("ct_scan", file);
});
const response = await fetch("/api/analyze", {
method: "POST",
body: formData,
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || "Analysis failed.");
}
return payload;
}
function finalizeAnalysis(scan, payload) {
stopAnalysisAnimation();
analysisProgressBar.style.width = "100%";
analysisProgressLabel.textContent = "100% complete";
const enriched = {
...scan,
status: payload.status || "Analysis complete",
analyzedAt: new Date().toISOString(),
result: payload,
};
state.currentScan = enriched;
state.analysisResultById.set(scan.id, enriched);
upsertHistory(enriched);
renderRecentUploads();
renderHistoryPage();
window.setTimeout(() => {
navigate(`/results/${scan.id}`);
}, 300);
}
function renderResultsPage(scan) {
const payload = scan.result;
const analysis = payload.analysis || {};
const overallRisk = String(analysis.overall_risk || "LOW").toUpperCase();
const nodules = (analysis.nodules || []).filter((nodule) => {
const filter = noduleFilter.value;
return filter === "ALL" || String(nodule.risk_level || "LOW").toUpperCase() === filter;
});
riskBanner.classList.remove("risk-banner--low", "risk-banner--medium", "risk-banner--high");
riskBanner.classList.add(`risk-banner--${overallRisk.toLowerCase()}`);
riskIcon.textContent = overallRisk.charAt(0);
riskLabel.textContent = `Patient Risk: ${overallRisk}`;
riskSummary.textContent = `Maximum malignancy: ${Number(analysis.risk_score || 0).toFixed(1)}%`;
riskRecommendation.textContent = payload.next_steps || "Consult physician for evaluation.";
noduleCount.textContent = String(analysis.num_nodules_detected || 0);
analysisDate.textContent = formatDate(scan.analyzedAt);
analysisStatus.textContent = payload.status || "Analysis complete";
resultsMeta.textContent = `${analysis.num_nodules_detected || 0} nodules detected • Analyzed ${formatDate(scan.analyzedAt)}`;
resultsBreadcrumb.textContent = `Home > History > Scan ${scan.id}`;
recommendationText.textContent = payload.next_steps || "Consult physician for evaluation.";
scanFileName.textContent = formatDisplayFileName(scan.fileSummary?.name || "-");
scanFileName.title = scan.fileSummary?.name || "-";
scanFileSize.textContent = scan.fileSummary?.size || "-";
scanFileFormat.textContent = scan.fileSummary?.format || "-";
scanFileFormat.title = scan.fileSummary?.format || "-";
renderTiming(payload.timing || {});
renderVisualization(payload.visualization);
renderNodules(nodules);
updateResultNeighborButtons(scan.id);
}
function renderTiming(timing) {
timingGrid.innerHTML = "";
[
["Preprocess", timing.preprocess_sec],
["Detection", timing.detect_sec],
["Total", timing.total_sec],
].forEach(([label, value]) => {
if (value == null) {
return;
}
const item = document.createElement("div");
item.innerHTML = `${label}${Number(value).toFixed(1)}s`;
timingGrid.appendChild(item);
});
}
function renderVisualization(visualization) {
if (visualization) {
scanImage.src = `data:image/png;base64,${visualization}`;
scanImage.hidden = false;
scanPlaceholder.hidden = true;
return;
}
scanImage.hidden = true;
scanImage.removeAttribute("src");
scanPlaceholder.hidden = false;
}
function renderNodules(nodules) {
nodulesList.innerHTML = "";
if (!nodules.length) {
emptyState.hidden = false;
return;
}
emptyState.hidden = true;
nodules.forEach((nodule) => {
const fragment = noduleCardTemplate.content.cloneNode(true);
const riskLevel = String(nodule.risk_level || "LOW").toUpperCase();
const detection = Number(nodule.detection_confidence || 0);
const malignancy = Number(nodule.malignancy_probability || 0);
fragment.querySelector("h3").textContent = `Nodule #${nodule.nodule_id}`;
fragment.querySelector(".nodule-location").textContent = `Location: ${nodule.location}`;
fragment.querySelector(".detection-value").textContent = `${detection.toFixed(1)}%`;
fragment.querySelector(".malignancy-value").textContent = `${malignancy.toFixed(1)}%`;
fragment.querySelector(".nodule-recommendation").textContent = nodule.recommendation || "Consult physician.";
const pill = fragment.querySelector(".risk-pill");
pill.textContent = riskLevel;
pill.classList.add(`risk-pill--${riskLevel.toLowerCase()}`);
const detectionBar = fragment.querySelector(".detection-bar");
const malignancyBar = fragment.querySelector(".malignancy-bar");
detectionBar.style.background = progressGradient(detection);
malignancyBar.style.background = progressGradient(malignancy);
nodulesList.appendChild(fragment);
window.requestAnimationFrame(() => {
detectionBar.style.width = `${detection}%`;
malignancyBar.style.width = `${malignancy}%`;
});
});
}
function renderHistoryPage() {
historyGroups.innerHTML = "";
const searchValue = historySearch.value.trim().toLowerCase();
const riskValue = historyFilter.value;
const sortValue = historySort.value;
const items = [...state.history]
.filter((item) => {
const matchesSearch =
!searchValue ||
item.fileSummary.name.toLowerCase().includes(searchValue) ||
item.id.toLowerCase().includes(searchValue);
const risk = String(item.result?.analysis?.overall_risk || "LOW").toUpperCase();
const matchesRisk = riskValue === "ALL" || risk === riskValue;
return matchesSearch && matchesRisk;
})
.sort((a, b) => {
const aDate = new Date(a.analyzedAt || a.createdAt).getTime();
const bDate = new Date(b.analyzedAt || b.createdAt).getTime();
return sortValue === "OLDEST" ? aDate - bDate : bDate - aDate;
});
historySubtitle.textContent = `${items.length} scans analyzed`;
if (!items.length) {
historyGroups.innerHTML = `
Analyze a CT scan to start building your history.No scans yet
No recent scans yet.
`; return; } items.forEach((item) => { const card = document.createElement("article"); card.className = "recent-upload-item"; card.innerHTML = ` ${item.fileSummary.name}Uploaded ${timeAgo(item.analyzedAt || item.createdAt)} • ${item.fileSummary.size}
`; card.addEventListener("click", () => navigate(`/results/${item.id}`)); recentUploads.appendChild(card); }); } function resolveScan(scanId) { if (state.currentScan?.id === scanId) { return state.currentScan; } return state.history.find((item) => item.id === scanId) || null; } function upsertHistory(scan) { const existingIndex = state.history.findIndex((item) => item.id === scan.id); if (existingIndex >= 0) { state.history[existingIndex] = scan; } else { state.history.unshift(scan); } saveHistory(state.history); } function loadHistory() { try { return JSON.parse(window.localStorage.getItem(STORAGE_KEY) || "[]"); } catch { return []; } } function saveHistory(history) { const compactHistory = history .slice(0, 20) .map((scan) => ({ ...scan, result: scan.result ? { ...scan.result, visualization: null, } : null, })); window.localStorage.setItem(STORAGE_KEY, JSON.stringify(compactHistory)); state.history = compactHistory; } function buildSampleScan(scanId) { const now = new Date().toISOString(); return { id: scanId, createdAt: now, status: "Analyzing", fileSummary: { name: "sample_patient_scan.nii.gz", size: "128.4 MB", format: "NII.GZ", fileCount: 1, }, result: { status: "Sample analysis complete", next_steps: "Review the highlighted nodule and consider follow-up imaging in 6 months.", analysis: { num_nodules_detected: 2, overall_risk: "MEDIUM", risk_score: 54.2, nodules: [ { nodule_id: 1, location: "(87, 40, 257)", detection_confidence: 60.2, malignancy_probability: 36.4, risk_level: "MEDIUM", recommendation: "Follow-up scan in 6 months.", }, { nodule_id: 2, location: "(112, 63, 244)", detection_confidence: 82.7, malignancy_probability: 54.2, risk_level: "HIGH", recommendation: "Expedited physician review is advised.", }, ], }, visualization: null, timing: { preprocess_sec: 7.2, detect_sec: 18.4, total_sec: 28.6, }, }, }; } function exportCurrentImage() { if (scanImage.src) { const link = document.createElement("a"); link.href = scanImage.src; link.download = "oncovision-ct-visualization.png"; link.click(); return; } toastLikeAlert("No CT visualization is available for export yet."); } function navigateHistoryNeighbor(direction) { if (!state.currentScan) { return; } const index = state.history.findIndex((item) => item.id === state.currentScan.id); if (index < 0) { return; } const next = state.history[index + direction]; if (!next) { return; } navigate(`/results/${next.id}`); } function updateResultNeighborButtons(scanId) { const index = state.history.findIndex((item) => item.id === scanId); previousScanButton.disabled = index <= 0; nextScanButton.disabled = index < 0 || index >= state.history.length - 1; } function deleteCurrentScan() { if (!state.currentScan) { return; } const confirmed = window.confirm(`Delete ${state.currentScan.fileSummary?.name || state.currentScan.id} from local history?`); if (!confirmed) { return; } state.history = state.history.filter((item) => item.id !== state.currentScan.id); saveHistory(state.history); renderHistoryPage(); renderRecentUploads(); state.currentScan = null; navigate("/history"); } function confirmShare() { const email = shareEmailInput.value.trim(); if (!email) { toastLikeAlert("Enter an email address to share results."); return; } const shareUrl = `${window.location.origin}/results/${state.currentScan?.id || ""}`; const message = shareMessageInput.value.trim() || "Please review this scan."; openModal(null); toastLikeAlert(`Prepared share for ${email}\n\nLink: ${shareUrl}\n\nMessage: ${message}`); } function generatePdfReport(previewOnly) { if (!state.currentScan?.result) { toastLikeAlert("No scan result is available to export."); return; } const scan = state.currentScan; const analysis = scan.result.analysis || {}; const reportData = { patientName: patientNameInput.value.trim() || "Not provided", mrn: patientMrnInput.value.trim() || "Not provided", dob: patientDobInput.value.trim() || "Not provided", physicianNotes: physicianNotesInput.value.trim() || "No additional physician notes.", nodules: analysis.nodules || [], overallRisk: String(analysis.overall_risk || "LOW").toUpperCase(), riskScore: Number(analysis.risk_score || 0).toFixed(1), totalTime: Number(scan.result.timing?.total_sec || 0).toFixed(1), scanDate: formatDate(scan.analyzedAt || scan.createdAt, true), generatedAt: formatDate(new Date().toISOString(), true), fileName: scan.fileSummary?.name || "-", fileSize: scan.fileSummary?.size || "-", nextSteps: scan.result.next_steps || "Consult physician for evaluation.", clinicalHighlights: buildClinicalHighlights(analysis.nodules || [], scan.result.next_steps || "Consult physician for evaluation."), visualizationSrc: scanImage.src || "", riskColor: riskBanner.classList.contains("risk-banner--high") ? "#fee2e2" : riskBanner.classList.contains("risk-banner--medium") ? "#fef3c7" : "#d1fae5", }; const reportHtml = buildPdfPreviewHtml(scan, reportData); if (previewOnly) { const reportWindow = window.open("", "_blank", "noopener,noreferrer,width=1100,height=900"); if (!reportWindow) { toastLikeAlert("Popup blocked. Allow popups to preview the report."); return; } reportWindow.document.open(); reportWindow.document.write(reportHtml); reportWindow.document.close(); openModal(null); return; } const jsPdfCtor = window.jspdf?.jsPDF || window.jsPDF; if (!jsPdfCtor) { toastLikeAlert("PDF library failed to load. Falling back to preview window."); generatePdfReport(true); return; } const filename = `OVX_Report_${scan.id}_${formatReportDate(new Date().toISOString())}.pdf`; openModal(null); createPdfDocument(scan, reportData, jsPdfCtor, filename).catch((error) => { console.error("PDF generation failed", error); toastLikeAlert("PDF generation failed. Opening preview instead."); generatePdfReport(true); }); } function buildPdfPreviewHtml(scan, reportData) { const visualization = reportData.visualizationSrc ? `Risk: ${escapeHtml(nodule.risk_level || "LOW")}
Detection Confidence: ${Number(nodule.detection_confidence || 0).toFixed(1)}%
Malignancy Probability: ${Number(nodule.malignancy_probability || 0).toFixed(1)}%
Location: ${escapeHtml(nodule.location || "-")}
Recommendation: ${escapeHtml(nodule.recommendation || "Consult physician.")}
No suspicious nodules were detected.
"; return `
Scan ID: ${scan.id}
Scan Date: ${reportData.scanDate}
Report Generated: ${reportData.generatedAt}
${escapeHtml(reportData.nextSteps)}
• ${escapeHtml(item)}
`).join("")}${escapeHtml(reportData.patientName)}
${escapeHtml(reportData.mrn)}
${escapeHtml(reportData.dob)}
${escapeHtml(reportData.fileName)}
${escapeHtml(reportData.fileSize)}
${reportData.totalTime}s
${escapeHtml(reportData.physicianNotes)}
The axial, coronal, and sagittal CT views are preserved together on a single report page for review and sharing.
This report is intended to support clinical review and does not replace professional medical judgment. All findings should be reviewed by a qualified physician or radiologist.