clinicpal / src /hooks /use-export.ts
Vrda's picture
Deploy ClinIcPal frontend
9bc2f29 verified
'use client';
import { useCallback } from 'react';
import type { ExportData, ExportFormat } from '@/types/export';
import type { AnalysisResult } from '@/types';
export function useExport() {
// Generate export data structure
const generateExportData = useCallback(
(
originalText: string,
analysisResult: AnalysisResult,
settings: {
selectedModel: string;
temperature: number;
confidenceThreshold: number;
enabledCategories: string[];
enabledSeverities: string[];
}
): ExportData => {
const categoryCounts: Record<string, number> = {};
analysisResult.errors.forEach((error) => {
categoryCounts[error.category] = (categoryCounts[error.category] || 0) + 1;
});
return {
metadata: {
exportDate: new Date().toISOString(),
modelUsed: settings.selectedModel,
analysisSettings: {
temperature: settings.temperature,
confidenceThreshold: settings.confidenceThreshold,
enabledCategories: settings.enabledCategories,
enabledSeverities: settings.enabledSeverities,
},
},
originalText,
analysisResult,
summary: {
totalErrors: analysisResult.errors.length,
criticalCount: analysisResult.errors.filter((e) => e.severity === 'critical')
.length,
warningCount: analysisResult.errors.filter((e) => e.severity === 'warning')
.length,
suggestionCount: analysisResult.errors.filter((e) => e.severity === 'suggestion')
.length,
categoryCounts,
},
};
},
[]
);
// Export to CSV
const exportToCSV = useCallback((data: ExportData): void => {
const rows = [
[
'Severity',
'Category',
'Explanation',
'Reasoning',
'Suggestion',
'Confidence',
],
];
data.analysisResult.errors.forEach((error) => {
rows.push([
error.severity,
error.category,
`"${error.explanation.replace(/"/g, '""')}"`,
error.reasoning ? `"${error.reasoning.replace(/"/g, '""')}"` : '',
error.suggestion ? `"${error.suggestion.replace(/"/g, '""')}"` : '',
(error.confidence * 100).toFixed(1) + '%',
]);
});
const csvContent = rows.map((row) => row.join(',')).join('\n');
downloadFile(
csvContent,
`clinical-analysis-${new Date().toISOString().split('T')[0]}.csv`,
'text/csv'
);
}, []);
// Export to JSON
const exportToJSON = useCallback((data: ExportData): void => {
const jsonContent = JSON.stringify(data, null, 2);
downloadFile(
jsonContent,
`clinical-analysis-${new Date().toISOString().split('T')[0]}.json`,
'application/json'
);
}, []);
// Export to plain text report
const exportToText = useCallback((data: ExportData): void => {
const lines: string[] = [];
lines.push('CLINICAL NOTE ANALYSIS REPORT');
lines.push('='.repeat(50));
lines.push('');
lines.push(`Date: ${new Date(data.metadata.exportDate).toLocaleString()}`);
lines.push(`Model: ${data.metadata.modelUsed}`);
if (data.metadata.templateType) {
lines.push(`Template: ${data.metadata.templateType}`);
}
lines.push('');
lines.push('SUMMARY');
lines.push('-'.repeat(50));
lines.push(`Total Errors Found: ${data.summary.totalErrors}`);
lines.push(` Critical: ${data.summary.criticalCount}`);
lines.push(` Warnings: ${data.summary.warningCount}`);
lines.push(` Suggestions: ${data.summary.suggestionCount}`);
lines.push('');
lines.push('ORIGINAL TEXT');
lines.push('-'.repeat(50));
lines.push(data.originalText);
lines.push('');
if (data.analysisResult.errors.length > 0) {
lines.push('DETECTED ISSUES');
lines.push('-'.repeat(50));
lines.push('');
data.analysisResult.errors.forEach((error, index) => {
lines.push(`${index + 1}. [${error.severity.toUpperCase()}] ${error.category}`);
lines.push(` Issue: ${error.explanation}`);
if (error.reasoning) {
lines.push(` Reasoning: ${error.reasoning}`);
}
if (error.suggestion) {
lines.push(` Suggestion: ${error.suggestion}`);
}
lines.push(` Confidence: ${(error.confidence * 100).toFixed(1)}%`);
lines.push('');
});
} else {
lines.push('No issues detected.');
lines.push('');
}
lines.push('='.repeat(50));
lines.push('Generated by Clinical Error Detector');
const textContent = lines.join('\n');
downloadFile(
textContent,
`clinical-analysis-${new Date().toISOString().split('T')[0]}.txt`,
'text/plain'
);
}, []);
// Export to PDF (print-friendly HTML)
const exportToPDF = useCallback((data: ExportData): void => {
// Create a print-friendly HTML view
const printWindow = window.open('', '', 'width=800,height=600');
if (!printWindow) {
alert('Please allow popups to export to PDF');
return;
}
const html = generatePrintHTML(data);
printWindow.document.write(html);
printWindow.document.close();
// Wait for content to load, then trigger print
printWindow.onload = () => {
printWindow.focus();
printWindow.print();
};
}, []);
// Main export function
const exportAnalysis = useCallback(
(
format: ExportFormat,
originalText: string,
analysisResult: AnalysisResult,
settings: {
selectedModel: string;
temperature: number;
confidenceThreshold: number;
enabledCategories: string[];
enabledSeverities: string[];
}
) => {
const data = generateExportData(originalText, analysisResult, settings);
switch (format) {
case 'csv':
exportToCSV(data);
break;
case 'json':
exportToJSON(data);
break;
case 'txt':
exportToText(data);
break;
case 'pdf':
exportToPDF(data);
break;
}
},
[generateExportData, exportToCSV, exportToJSON, exportToText, exportToPDF]
);
return { exportAnalysis };
}
// Utility function to download file
function downloadFile(content: string, filename: string, mimeType: string) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// Generate print-friendly HTML for PDF export
function generatePrintHTML(data: ExportData): string {
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical':
return '#dc2626';
case 'warning':
return '#ea580c';
case 'suggestion':
return '#2563eb';
default:
return '#6b7280';
}
};
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Clinical Note Analysis Report</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #1f2937;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #111827; }
h2 { font-size: 1.5rem; margin: 2rem 0 1rem; color: #374151; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem; }
h3 { font-size: 1.25rem; margin: 1.5rem 0 0.75rem; color: #4b5563; }
.header { margin-bottom: 2rem; }
.metadata { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin: 1rem 0; font-size: 0.875rem; }
.metadata-item { display: flex; gap: 0.5rem; }
.metadata-label { font-weight: 600; color: #6b7280; }
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 1rem 0; }
.summary-card { padding: 1rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; text-align: center; }
.summary-value { font-size: 2rem; font-weight: bold; color: #111827; }
.summary-label { font-size: 0.875rem; color: #6b7280; margin-top: 0.25rem; }
.original-text { background: #f9fafb; padding: 1.5rem; border-radius: 0.5rem; border: 1px solid #e5e7eb; white-space: pre-wrap; font-family: monospace; font-size: 0.875rem; margin: 1rem 0; }
.error-card { margin: 1rem 0; padding: 1.5rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; page-break-inside: avoid; }
.error-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; }
.severity-badge { padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; color: white; }
.category-badge { padding: 0.25rem 0.75rem; border-radius: 0.375rem; font-size: 0.75rem; font-weight: 500; background: #f3f4f6; color: #374151; }
.text-span { background: #fef3c7; padding: 0.5rem; border-radius: 0.375rem; border-left: 3px solid #f59e0b; font-family: monospace; font-size: 0.875rem; margin: 0.75rem 0; }
.explanation { color: #4b5563; margin: 0.75rem 0; }
.suggestion { background: #ecfdf5; padding: 0.75rem; border-radius: 0.375rem; border-left: 3px solid #10b981; margin: 0.75rem 0; }
.suggestion-label { font-weight: 600; color: #059669; font-size: 0.875rem; margin-bottom: 0.25rem; }
.confidence { font-size: 0.875rem; color: #6b7280; }
.footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e5e7eb; text-align: center; color: #9ca3af; font-size: 0.875rem; }
@media print {
body { padding: 1rem; }
.error-card { page-break-inside: avoid; }
}
</style>
</head>
<body>
<div class="header">
<h1>Clinical Note Analysis Report</h1>
<div class="metadata">
<div class="metadata-item">
<span class="metadata-label">Date:</span>
<span>${new Date(data.metadata.exportDate).toLocaleString()}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Model:</span>
<span>${data.metadata.modelUsed}</span>
</div>
${
data.metadata.templateType
? `<div class="metadata-item">
<span class="metadata-label">Template:</span>
<span>${data.metadata.templateType}</span>
</div>`
: ''
}
</div>
</div>
<h2>Summary</h2>
<div class="summary">
<div class="summary-card">
<div class="summary-value">${data.summary.totalErrors}</div>
<div class="summary-label">Total Issues</div>
</div>
<div class="summary-card">
<div class="summary-value" style="color: #dc2626;">${data.summary.criticalCount}</div>
<div class="summary-label">Critical</div>
</div>
<div class="summary-card">
<div class="summary-value" style="color: #ea580c;">${data.summary.warningCount}</div>
<div class="summary-label">Warnings</div>
</div>
<div class="summary-card">
<div class="summary-value" style="color: #2563eb;">${data.summary.suggestionCount}</div>
<div class="summary-label">Suggestions</div>
</div>
</div>
<h2>Original Clinical Note</h2>
<div class="original-text">${escapeHtml(data.originalText)}</div>
${
data.analysisResult.errors.length > 0
? `
<h2>Detected Issues (${data.analysisResult.errors.length})</h2>
${data.analysisResult.errors
.map(
(error) => `
<div class="error-card">
<div class="error-header">
<span class="severity-badge" style="background-color: ${getSeverityColor(error.severity)};">
${error.severity.toUpperCase()}
</span>
<span class="category-badge">${error.category}</span>
<span class="confidence">Confidence: ${(error.confidence * 100).toFixed(1)}%</span>
</div>
<div class="explanation">${escapeHtml(error.explanation)}</div>
${
error.reasoning
? `<div class="reasoning" style="font-style:italic;color:#888;margin:8px 0;padding:8px;border-left:2px solid #555;">
${escapeHtml(error.reasoning)}
</div>`
: ''
}
${
error.suggestion
? `<div class="suggestion">
<div class="suggestion-label">💡 Suggestion:</div>
<div>${escapeHtml(error.suggestion)}</div>
</div>`
: ''
}
</div>
`
)
.join('')}
`
: '<p>No issues detected in the clinical note.</p>'
}
<div class="footer">
Generated by Clinical Error Detector • Powered by Ollama
</div>
</body>
</html>
`;
}
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}