import { useState } from 'react'; import { X, Check, Copy, Download, Code2, Image as ImageIcon, FileText, ChevronRight } from 'lucide-react'; import { RecognitionResult } from '../types'; 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); 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.rendered_image_path, // User requested "Copy button" for Image. // Special handling might be needed for actual image data copy, // but standard clipboard writeText is for text. // If we want to copy image data, we need to fetch it. // For now, I'll stick to the pattern, but if it's a URL, copying the URL is the fallback. // However, usually "Copy Image" implies the binary. // Let's treat it as a special case in handleAction. }, // File Category { id: 'docx', label: 'DOCX', category: 'File', getContent: (r) => r.markdown_content, // Placeholder content for file conversion isDownload: true, extension: 'docx' }, { id: 'pdf', label: 'PDF', category: 'File', getContent: (r) => r.markdown_content, // Placeholder isDownload: true, extension: 'pdf' } ]; const handleAction = async (option: ExportOption) => { const content = option.getContent(result); if (!content) return; try { if (option.category === 'Image' && !option.isDownload) { // Handle Image Copy if (content.startsWith('http') || content.startsWith('blob:')) { try { const response = await fetch(content); const blob = await response.blob(); await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob }) ]); } catch (err) { console.error('Failed to copy image:', err); // Fallback to copying URL await navigator.clipboard.writeText(content); } } else { await navigator.clipboard.writeText(content); } } else if (option.isDownload) { // Simulate download 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); // Small delay to show "Copied" state before closing } catch (err) { console.error('Action failed:', err); } }; 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 => ( )) }
))}
); }