import { useState } from 'react'; import { X, Check, Copy, Download, Code2, Image as ImageIcon, FileText, Loader2 } 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'; 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; } export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebarProps) { 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', label: 'Latex', category: 'Code', getContent: (r) => r.latex_content }, { id: 'mathml', label: 'MathML', category: 'Code', getContent: (r) => r.mathml_content }, { 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('导出失败,请重试'); } 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(`生成图片失败: ${err}`); } finally { setExportingId(null); } }; const handleAction = async (option: ExportOption) => { // 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: any; label: string }[] = [ { id: 'Code', icon: Code2, label: 'Code' }, { id: 'Image', icon: ImageIcon, label: 'Image' }, { id: 'File', icon: FileText, label: 'File' }, ]; return ( <> {/* Backdrop */} {isOpen && (
)} {/* Sidebar Panel */}

Export

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