feat: add toast for no content
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
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';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
|
||||
interface ExportSidebarProps {
|
||||
isOpen: boolean;
|
||||
@@ -25,87 +25,6 @@ interface ExportOption {
|
||||
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(/<mml:math>/, '<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML">');
|
||||
|
||||
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<string | null>(null);
|
||||
@@ -121,14 +40,25 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
||||
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 \[ \] and wrap with \( \)
|
||||
const content = r.latex_content.replace(/^\\\[/, '').replace(/\\\]$/, '').trim();
|
||||
// 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}\\)`;
|
||||
}
|
||||
},
|
||||
@@ -136,7 +66,17 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
||||
id: 'latex_display',
|
||||
label: 'LaTeX (Display)',
|
||||
category: 'Code',
|
||||
getContent: (r) => r.latex_content
|
||||
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',
|
||||
@@ -148,13 +88,7 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
||||
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
|
||||
getContent: (r) => r.mml
|
||||
},
|
||||
// Image Category
|
||||
{
|
||||
@@ -278,22 +212,33 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
||||
return;
|
||||
}
|
||||
|
||||
let content = option.getContent(result);
|
||||
const 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);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (!content) return;
|
||||
|
||||
setExportingId(option.id);
|
||||
|
||||
try {
|
||||
@@ -321,6 +266,10 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
||||
|
||||
} catch (err) {
|
||||
console.error('Action failed:', err);
|
||||
toast.error(t.export.failed, {
|
||||
duration: 3000,
|
||||
position: 'top-center',
|
||||
});
|
||||
} finally {
|
||||
setExportingId(null);
|
||||
}
|
||||
@@ -334,6 +283,41 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Toast Container with custom configuration */}
|
||||
<Toaster
|
||||
position="top-center"
|
||||
toastOptions={{
|
||||
duration: 3000,
|
||||
style: {
|
||||
background: '#fff',
|
||||
color: '#1f2937',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 10px 40px rgba(59, 130, 246, 0.15), 0 2px 8px rgba(59, 130, 246, 0.1)',
|
||||
maxWidth: '420px',
|
||||
},
|
||||
success: {
|
||||
iconTheme: {
|
||||
primary: '#10b981',
|
||||
secondary: '#ffffff',
|
||||
},
|
||||
style: {
|
||||
border: '1px solid #d1fae5',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
iconTheme: {
|
||||
primary: '#3b82f6',
|
||||
secondary: '#ffffff',
|
||||
},
|
||||
style: {
|
||||
border: '1px solid #dbeafe',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user