| 'use client'; |
|
|
| import { useCallback } from 'react'; |
| import type { ExportData, ExportFormat } from '@/types/export'; |
| import type { AnalysisResult } from '@/types'; |
|
|
| export function useExport() { |
| |
| 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, |
| }, |
| }; |
| }, |
| [] |
| ); |
|
|
| |
| 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; |
| } |
| |