import { useState } from 'react'; import { X, Check, Copy, Download, Code2, Image as ImageIcon, FileText, Loader2, LucideIcon } from 'lucide-react'; import { RecognitionResult } from '../types'; import { convertMathmlToOmml, wrapOmmlForClipboard } from '../lib/ommlConverter'; import { generateImageFromElement, copyImageToClipboard, downloadImage } from '../lib/imageGenerator'; import { API_BASE_URL } from '../config/env'; import { tokenManager } from '../lib/api'; import { trackExportEvent } from '../lib/analytics'; import { useLanguage } from '../contexts/LanguageContext'; interface ExportSidebarProps { isOpen: boolean; onClose: () => void; result: RecognitionResult | null; } type ExportCategory = 'Code' | 'Image' | 'File'; interface ExportOption { id: string; label: string; category: ExportCategory; getContent: (result: RecognitionResult) => string | null; isDownload?: boolean; // If true, triggers file download instead of copy extension?: string; } // Helper function to add mml: prefix to MathML const addMMLPrefix = (mathml: string): string | null => { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(mathml, 'application/xml'); // Check for parse errors const parseError = xmlDoc.getElementsByTagName("parsererror"); if (parseError.length > 0) { return null; } // Create new document with mml namespace const newDoc = document.implementation.createDocument( 'http://www.w3.org/1998/Math/MathML', 'mml:math', null ); const newMathElement = newDoc.documentElement; // Copy display attribute if present const displayAttr = xmlDoc.documentElement.getAttribute('display'); if (displayAttr) { newMathElement.setAttribute('display', displayAttr); } // Recursive function to process nodes const processNode = (node: Node, newParent: Element) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element; // Create new element with mml: prefix in the target document const newElement = newDoc.createElementNS( 'http://www.w3.org/1998/Math/MathML', 'mml:' + element.localName ); // Copy attributes for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; // Skip xmlns attributes as we handle them explicitly if (attr.name.startsWith('xmlns')) continue; newElement.setAttributeNS( attr.namespaceURI, attr.name, attr.value ); } // Process children Array.from(element.childNodes).forEach(child => { processNode(child, newElement); }); newParent.appendChild(newElement); } else if (node.nodeType === Node.TEXT_NODE) { newParent.appendChild(newDoc.createTextNode(node.nodeValue || '')); } }; // Process all children of the root math element Array.from(xmlDoc.documentElement.childNodes).forEach(child => { processNode(child, newMathElement); }); // Serialize const serializer = new XMLSerializer(); let prefixedMathML = serializer.serializeToString(newDoc); // Clean up xmlns prefixedMathML = prefixedMathML.replace(/ xmlns(:mml)?="[^"]*"/g, ''); prefixedMathML = prefixedMathML.replace(//, ''); return prefixedMathML; } catch (err) { console.error('Failed to process MathML:', err); return null; } }; export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebarProps) { const { t } = useLanguage(); const [copiedId, setCopiedId] = useState(null); const [exportingId, setExportingId] = useState(null); if (!result) return null; const exportOptions: ExportOption[] = [ // Code Category { id: 'markdown', label: 'Markdown', category: 'Code', getContent: (r) => r.markdown_content }, { id: 'latex_inline', label: 'LaTeX (Inline)', category: 'Code', getContent: (r) => { if (!r.latex_content) return null; // Remove existing \[ \] and wrap with \( \) const content = r.latex_content.replace(/^\\\[/, '').replace(/\\\]$/, '').trim(); return `\\(${content}\\)`; } }, { id: 'latex_display', label: 'LaTeX (Display)', category: 'Code', getContent: (r) => r.latex_content }, { id: 'mathml', label: 'MathML', category: 'Code', getContent: (r) => r.mathml_content }, { id: 'mathml_mml', label: 'MathML (MML)', category: 'Code', getContent: (r) => r.mathml_content ? addMMLPrefix(r.mathml_content) : null }, { id: 'mathml_word', label: 'Word MathML', category: 'Code', getContent: (r) => r.mathml_word_content }, // Image Category { id: 'rendered_image', label: 'Rendered Image', category: 'Image', getContent: (r) => r.markdown_content, }, // File Category { id: 'docx', label: 'DOCX', category: 'File', getContent: (r) => r.markdown_content, isDownload: true, extension: 'docx' } ]; // Handle DOCX export via API const handleFileExport = async (type: 'docx') => { if (!result?.id) return; setExportingId(type); try { const token = tokenManager.getToken(); const response = await fetch(`${API_BASE_URL}/task/export`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': token } : {}), }, body: JSON.stringify({ task_no: result.id, type: type, }), }); if (!response.ok) { throw new Error(`Export failed: ${response.statusText}`); } // Get the blob from response const blob = await response.blob(); // Create download link const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `export.${type}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setCopiedId(type); setTimeout(() => { setCopiedId(null); onClose(); }, 1000); } catch (err) { console.error('Export failed:', err); alert(t.export.failed); } finally { setExportingId(null); } }; // Handle image generation from Markdown preview const handleImageGeneration = async (action: 'copy' | 'download') => { // We capture the rendered result directly from the DOM const elementId = 'markdown-preview-content'; setExportingId('rendered_image'); try { const dataUrl = await generateImageFromElement(elementId, { format: 'png', scale: 2, padding: 24, }) as string; if (action === 'copy') { await copyImageToClipboard(dataUrl); } else { downloadImage(dataUrl, 'rendered_image.png'); } setCopiedId('rendered_image'); setTimeout(() => { setCopiedId(null); onClose(); }, 1000); } catch (err) { console.error('Failed to generate image:', err); alert(`${t.export.imageFailed}: ${err}`); } finally { setExportingId(null); } }; const handleAction = async (option: ExportOption) => { // Analytics tracking if (result?.id) { trackExportEvent( result.id, option.id, exportOptions.map(o => o.id) ); } // Handle DOCX export via API if (option.id === 'docx') { await handleFileExport('docx'); return; } // Handle image generation from Markdown if (option.id === 'rendered_image') { await handleImageGeneration('copy'); return; } let content = option.getContent(result); // Fallback: If Word MathML is missing, try to convert from MathML if (option.id === 'mathml_word' && !content && result.mathml_content) { try { const omml = await convertMathmlToOmml(result.mathml_content); if (omml) { content = wrapOmmlForClipboard(omml); } } catch (err) { console.error('Failed to convert MathML to OMML:', err); } } if (!content) return; setExportingId(option.id); try { if (option.isDownload) { // Simulate download (for other file types if any) const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `export.${option.extension}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } else { // Standard Text Copy await navigator.clipboard.writeText(content); } setCopiedId(option.id); setTimeout(() => { setCopiedId(null); onClose(); // Auto close after action }, 1000); } catch (err) { console.error('Action failed:', err); } finally { setExportingId(null); } }; const categories: { id: ExportCategory; icon: LucideIcon; label: string }[] = [ { id: 'Code', icon: Code2, label: t.export.categories.code }, { id: 'Image', icon: ImageIcon, label: t.export.categories.image }, { id: 'File', icon: FileText, label: t.export.categories.file }, ]; return ( <> {/* Backdrop */} {isOpen && (
)} {/* Sidebar Panel */}

{t.export.title}

{categories.map((category) => (
{category.label}
{exportOptions .filter(opt => opt.category === category.id) .map(option => ( )) }
))}
); }