import { useState } from 'react'; import { X, Check, Copy, Download, Code2, Image as ImageIcon, FileText, Loader2, LucideIcon } from 'lucide-react'; import { RecognitionResult } from '../types'; 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'; import toast, { Toaster } from 'react-hot-toast'; 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 { 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', label: 'LaTeX', category: 'Code', getContent: (r) => r.latex_content }, { id: 'latex_inline', label: 'LaTeX (Inline)', category: 'Code', getContent: (r) => { if (!r.latex_content) return null; // Remove existing delimiters like \[ \], \( \), $$, or $ let content = r.latex_content.trim(); content = content.replace(/^\\\[/, '').replace(/\\\]$/, ''); content = content.replace(/^\\\(/, '').replace(/\\\)$/, ''); content = content.replace(/^\$\$/, '').replace(/\$\$$/, ''); content = content.replace(/^\$/, '').replace(/\$$/, ''); content = content.trim(); return `\\(${content}\\)`; } }, { id: 'latex_display', label: 'LaTeX (Display)', category: 'Code', getContent: (r) => { if (!r.latex_content) return null; // Remove existing delimiters like \[ \], \( \), $$, or $ let content = r.latex_content.trim(); content = content.replace(/^\\\[/, '').replace(/\\\]$/, ''); content = content.replace(/^\\\(/, '').replace(/\\\)$/, ''); content = content.replace(/^\$\$/, '').replace(/\$\$$/, ''); content = content.replace(/^\$/, '').replace(/\$$/, ''); content = content.trim(); return `\\[${content}\\]`; } }, { id: 'mathml', label: 'MathML', category: 'Code', getContent: (r) => r.mathml_content }, { id: 'mathml_mml', label: 'MathML (MML)', category: 'Code', getContent: (r) => r.mml }, // 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; } const content = option.getContent(result); // Check if content is empty and show toast if (!content) { toast.error(t.export.noContent, { duration: 2000, position: 'top-center', style: { background: '#fff', color: '#1f2937', padding: '16px 20px', borderRadius: '12px', boxShadow: '0 10px 40px rgba(59, 130, 246, 0.15), 0 2px 8px rgba(59, 130, 246, 0.1)', border: '1px solid #dbeafe', fontSize: '14px', fontWeight: '500', maxWidth: '900px', lineHeight: '1.5', }, iconTheme: { primary: '#ef4444', secondary: '#ffffff', }, }); 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); toast.error(t.export.failed, { duration: 3000, position: 'top-center', }); } 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 ( <> {/* Toast Container with custom configuration */} {/* Backdrop */} {isOpen && (
)} {/* Sidebar Panel */}

{t.export.title}

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