Neural-MRI / frontend /src /utils /exportUtils.ts
Hiconcep's picture
Upload folder using huggingface_hub
0ce9643 verified
import type { DiagnosticReport } from '../types/report';
// CSS variable → resolved value map (from theme/variables.css)
export const CSS_VAR_MAP: Record<string, string> = {
'var(--bg-primary)': '#0a0c10',
'var(--bg-secondary)': '#0c0e14',
'var(--bg-surface)': '#12151c',
'var(--border)': 'rgba(100, 170, 136, 0.15)',
'var(--text-primary)': '#66aa88',
'var(--text-secondary)': '#556677',
'var(--text-data)': '#aabbcc',
'var(--accent-active)': '#00ffaa',
'var(--font-primary)': "'JetBrains Mono', 'Fira Code', 'Courier New', monospace",
'var(--font-size-xs)': '9px',
'var(--font-size-sm)': '10px',
'var(--font-size-md)': '11px',
'var(--font-size-lg)': '14px',
};
export function downloadFile(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export function resolveCssVars(svgStr: string): string {
let resolved = svgStr;
for (const [cssVar, value] of Object.entries(CSS_VAR_MAP)) {
resolved = resolved.replaceAll(cssVar, value);
}
return resolved;
}
export function prepareSvgClone(svgEl: SVGSVGElement): string {
const clone = svgEl.cloneNode(true) as SVGSVGElement;
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
const w = svgEl.getAttribute('width') || '560';
const h = svgEl.getAttribute('height') || '620';
// Insert background rect as first child
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bg.setAttribute('width', w);
bg.setAttribute('height', h);
bg.setAttribute('fill', '#0a0c10');
clone.insertBefore(bg, clone.firstChild);
const raw = new XMLSerializer().serializeToString(clone);
return resolveCssVars(raw);
}
/** Get all canvas SVGs from DOM */
function getCanvasSvgs(): SVGSVGElement[] {
const els = document.querySelectorAll<SVGSVGElement>('[data-nmri-canvas]');
return Array.from(els);
}
export function exportSvg(filename = 'neural-mri-scan.svg') {
const svgs = getCanvasSvgs();
if (svgs.length === 0) return;
if (svgs.length === 1) {
const svgStr = prepareSvgClone(svgs[0]);
downloadFile(new Blob([svgStr], { type: 'image/svg+xml' }), filename);
return;
}
// Compare mode: merge two SVGs side by side
const w1 = parseInt(svgs[0].getAttribute('width') || '270');
const w2 = parseInt(svgs[1].getAttribute('width') || '270');
const h = parseInt(svgs[0].getAttribute('height') || '620');
const gap = 4;
const totalW = w1 + w2 + gap;
const svg1 = prepareSvgClone(svgs[0]);
const svg2 = prepareSvgClone(svgs[1]);
const combined = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${h}">
<rect width="${totalW}" height="${h}" fill="#0a0c10"/>
<g transform="translate(0,0)">${stripSvgWrapper(svg1)}</g>
<g transform="translate(${w1 + gap},0)">${stripSvgWrapper(svg2)}</g>
</svg>`;
downloadFile(new Blob([combined], { type: 'image/svg+xml' }), filename);
}
function stripSvgWrapper(svgStr: string): string {
return svgStr.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '');
}
export function exportPng(filename = 'neural-mri-scan.png', scale = 2) {
const svgs = getCanvasSvgs();
if (svgs.length === 0) return;
let svgStr: string;
let w: number;
let h: number;
if (svgs.length === 1) {
svgStr = prepareSvgClone(svgs[0]);
w = parseInt(svgs[0].getAttribute('width') || '560');
h = parseInt(svgs[0].getAttribute('height') || '620');
} else {
const w1 = parseInt(svgs[0].getAttribute('width') || '270');
const w2 = parseInt(svgs[1].getAttribute('width') || '270');
const gap = 4;
w = w1 + w2 + gap;
h = parseInt(svgs[0].getAttribute('height') || '620');
const svg1 = prepareSvgClone(svgs[0]);
const svg2 = prepareSvgClone(svgs[1]);
svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
<rect width="${w}" height="${h}" fill="#0a0c10"/>
<g transform="translate(0,0)">${stripSvgWrapper(svg1)}</g>
<g transform="translate(${w1 + gap},0)">${stripSvgWrapper(svg2)}</g>
</svg>`;
}
const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = w * scale;
canvas.height = h * scale;
const ctx = canvas.getContext('2d')!;
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
canvas.toBlob((pngBlob) => {
if (pngBlob) downloadFile(pngBlob, filename);
}, 'image/png');
};
img.src = url;
}
export function exportJson(data: unknown, filename = 'neural-mri-data.json') {
const json = JSON.stringify(data, null, 2);
downloadFile(new Blob([json], { type: 'application/json' }), filename);
}
export function exportReport(report: DiagnosticReport, filename = 'neural-mri-report.md') {
const paramStr = `${(report.total_params / 1e6).toFixed(0)}M`;
const lines: string[] = [
'# NEURAL MRI DIAGNOSTIC REPORT',
'',
`**Patient:** ${report.model_name} (${paramStr})`,
`**Date:** ${report.date}`,
`**Prompt:** "${report.prompt}"`,
'',
'## TECHNIQUE',
report.technique.join(', '),
'',
'## FINDINGS',
];
for (const f of report.findings) {
lines.push(`### [${f.scan_mode}] ${f.title}${f.severity.toUpperCase()}`);
for (const d of f.details) {
lines.push(`- ${d}`);
}
if (f.explanation) {
lines.push('', `> ${f.explanation}`);
}
lines.push('');
}
lines.push('## IMPRESSION');
for (const imp of report.impressions) {
lines.push(`${imp.index}. ${imp.text} _(${imp.severity})_`);
}
lines.push('', '## RECOMMENDATION');
for (const rec of report.recommendations) {
lines.push(`- ${rec}`);
}
lines.push('', '---', '_Generated by Neural MRI_');
const md = lines.join('\n');
downloadFile(new Blob([md], { type: 'text/markdown' }), filename);
}