149 lines
6.1 KiB
TypeScript
149 lines
6.1 KiB
TypeScript
|
|
import { useState } from 'react';
|
||
|
|
import { Download, Code2, Check, Copy } from 'lucide-react';
|
||
|
|
import ReactMarkdown from 'react-markdown';
|
||
|
|
import remarkMath from 'remark-math';
|
||
|
|
import rehypeKatex from 'rehype-katex';
|
||
|
|
import 'katex/dist/katex.min.css';
|
||
|
|
import { RecognitionResult } from '../types';
|
||
|
|
import ExportSidebar from './ExportSidebar';
|
||
|
|
|
||
|
|
interface ResultPanelProps {
|
||
|
|
result: RecognitionResult | null;
|
||
|
|
fileStatus?: 'pending' | 'processing' | 'completed' | 'failed';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Preprocess LaTeX content to fix common formatting issues
|
||
|
|
* that may cause KaTeX rendering to fail
|
||
|
|
*/
|
||
|
|
function preprocessLatex(content: string): string {
|
||
|
|
if (!content) return '';
|
||
|
|
|
||
|
|
let processed = content;
|
||
|
|
|
||
|
|
// Fix mixed display math delimiters: $$\[...\]$$ -> $$...$$
|
||
|
|
// This handles cases where \[ \] are incorrectly nested inside $$ $$
|
||
|
|
// Note: In JS replace(), $$ means "insert one $", so we need $$$$ to insert $$
|
||
|
|
processed = processed.replace(/\$\$\s*\\\[/g, '$$$$');
|
||
|
|
processed = processed.replace(/\\\]\s*\$\$/g, '$$$$');
|
||
|
|
|
||
|
|
// Also fix standalone \[ and \] that should be $$ for remark-math compatibility
|
||
|
|
// Replace \[ with $$ at the start of display math (not inside other math)
|
||
|
|
processed = processed.replace(/(?<!\$)\\\[(?!\$)/g, '$$$$');
|
||
|
|
processed = processed.replace(/(?<!\$)\\\](?!\$)/g, '$$$$');
|
||
|
|
|
||
|
|
// IMPORTANT: remark-math requires $$ to be on its own line for block math
|
||
|
|
// Ensure $$ followed by \begin has a newline: $$\begin -> $$\n\begin
|
||
|
|
processed = processed.replace(/\$\$([^\n$])/g, '$$$$\n$1');
|
||
|
|
// Ensure content followed by $$ has a newline: content$$ -> content\n$$
|
||
|
|
processed = processed.replace(/([^\n$])\$\$/g, '$1\n$$$$');
|
||
|
|
|
||
|
|
// Fix: \left \{ -> \left\{ (remove space between \left and delimiter)
|
||
|
|
processed = processed.replace(/\\left\s+\\/g, '\\left\\');
|
||
|
|
processed = processed.replace(/\\left\s+\{/g, '\\left\\{');
|
||
|
|
processed = processed.replace(/\\left\s+\[/g, '\\left[');
|
||
|
|
processed = processed.replace(/\\left\s+\(/g, '\\left(');
|
||
|
|
|
||
|
|
// Fix: \right \} -> \right\} (remove space between \right and delimiter)
|
||
|
|
processed = processed.replace(/\\right\s+\\/g, '\\right\\');
|
||
|
|
processed = processed.replace(/\\right\s+\}/g, '\\right\\}');
|
||
|
|
processed = processed.replace(/\\right\s+\]/g, '\\right]');
|
||
|
|
processed = processed.replace(/\\right\s+\)/g, '\\right)');
|
||
|
|
|
||
|
|
// Fix: \begin{matrix} with mismatched \left/\right -> use \begin{array}
|
||
|
|
// This is a more complex issue that requires proper \left/\right pairing
|
||
|
|
// For now, we'll try to convert problematic patterns
|
||
|
|
|
||
|
|
// Replace \left( ... \right. text \right) pattern with ( ... \right. text )
|
||
|
|
// This fixes the common mispairing issue
|
||
|
|
processed = processed.replace(/\\left\(([^)]*?)\\right\.\s*(\\text\{[^}]*\})\s*\\right\)/g, '($1\\right. $2)');
|
||
|
|
|
||
|
|
return processed;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function ResultPanel({ result, fileStatus }: ResultPanelProps) {
|
||
|
|
const [isExportSidebarOpen, setIsExportSidebarOpen] = useState(false);
|
||
|
|
const [copied, setCopied] = useState(false);
|
||
|
|
|
||
|
|
const handleCopy = async () => {
|
||
|
|
if (result?.markdown_content) {
|
||
|
|
await navigator.clipboard.writeText(result.markdown_content);
|
||
|
|
setCopied(true);
|
||
|
|
setTimeout(() => setCopied(false), 2000);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!result) {
|
||
|
|
if (fileStatus === 'processing' || fileStatus === 'pending') {
|
||
|
|
return (
|
||
|
|
<div className="h-full flex flex-col items-center justify-center bg-white text-center p-8">
|
||
|
|
<div className="w-16 h-16 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mb-6"></div>
|
||
|
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||
|
|
{fileStatus === 'pending' ? 'Waiting in queue...' : 'Analyzing...'}
|
||
|
|
</h3>
|
||
|
|
<p className="text-gray-500 max-w-sm">
|
||
|
|
{fileStatus === 'pending'
|
||
|
|
? 'Your file is in the queue, please wait.'
|
||
|
|
: 'Texpixel is processing your file, this may take a moment.'}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="h-full flex flex-col items-center justify-center bg-white text-center p-8">
|
||
|
|
<div className="w-32 h-32 bg-gray-100 rounded-full flex items-center justify-center mb-6 shadow-inner">
|
||
|
|
<Code2 size={48} className="text-gray-900" />
|
||
|
|
</div>
|
||
|
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">Waiting for recognition result</h3>
|
||
|
|
<p className="text-gray-500 max-w-sm">
|
||
|
|
After uploading the file, Texpixel will automatically recognize and display the result here
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col h-full bg-white relative overflow-hidden">
|
||
|
|
{/* Top Header */}
|
||
|
|
<div className="h-16 px-6 border-b border-gray-200 flex items-center justify-between bg-white shrink-0 z-10">
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<h2 className="text-lg font-bold text-gray-900">Markdown</h2>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={() => setIsExportSidebarOpen(true)}
|
||
|
|
className={`px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors flex items-center gap-2 shadow-sm ${isExportSidebarOpen ? 'opacity-0 pointer-events-none' : ''}`}
|
||
|
|
>
|
||
|
|
<Download size={16} />
|
||
|
|
Export
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Content Area - Rendered Markdown */}
|
||
|
|
{
|
||
|
|
<div className="flex-1 overflow-auto p-8 custom-scrollbar flex justify-center">
|
||
|
|
<div className="prose prose-blue max-w-3xl w-full prose-headings:font-bold prose-h1:text-2xl prose-h2:text-xl prose-p:leading-relaxed prose-pre:bg-gray-50 prose-pre:border prose-pre:border-gray-100 [&_.katex-display]:text-center">
|
||
|
|
<ReactMarkdown
|
||
|
|
remarkPlugins={[remarkMath]}
|
||
|
|
rehypePlugins={[[rehypeKatex, {
|
||
|
|
throwOnError: false,
|
||
|
|
errorColor: '#cc0000',
|
||
|
|
strict: false
|
||
|
|
}]]}
|
||
|
|
>
|
||
|
|
{preprocessLatex(result.markdown_content || '')}
|
||
|
|
</ReactMarkdown>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
<ExportSidebar
|
||
|
|
isOpen={isExportSidebarOpen}
|
||
|
|
onClose={() => setIsExportSidebarOpen(false)}
|
||
|
|
result={result}
|
||
|
|
/>
|
||
|
|
</div >
|
||
|
|
);
|
||
|
|
}
|