Skip to content

Commit 8ac6408

Browse files
committed
🐛 Fix LaTeX content rendering exception #621
1 parent 0800c6e commit 8ac6408

File tree

2 files changed

+216
-138
lines changed

2 files changed

+216
-138
lines changed

frontend/components/ui/markdownRenderer.tsx

Lines changed: 211 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
33
import ReactMarkdown from "react-markdown";
44
import remarkGfm from "remark-gfm";
55
import remarkMath from "remark-math";
6+
import rehypeRaw from "rehype-raw";
67
import rehypeKatex from "rehype-katex";
78
// @ts-ignore
89
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
@@ -11,7 +12,6 @@ import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
1112
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1213

1314
import { SearchResult } from "@/types/chat";
14-
1515
import {
1616
Tooltip,
1717
TooltipContent,
@@ -324,12 +324,36 @@ const HoverableText = ({
324324
);
325325
};
326326

327+
/**
328+
* Convert LaTeX delimiters to markdown math delimiters
329+
*
330+
* Converts:
331+
* - \( ... \) to $ ... $
332+
* - \[ ... \] to $$ ... $$
333+
*/
334+
const convertLatexDelimiters = (content: string): string => {
335+
// Quick check: only process if LaTeX delimiters are present
336+
if (!content.includes('\\(') && !content.includes('\\[')) {
337+
return content;
338+
}
339+
340+
return content
341+
// Convert \( ... \) to $ ... $ (inline math)
342+
.replace(/\\\(([\s\S]*?)\\\)/g, '$$1$')
343+
// Convert \[ ... \] to $$ ... $$ (display math)
344+
.replace(/\\\[([\s\S]*?)\\\]/g, '$$$$1$$\n');
345+
};
346+
327347
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
328348
content,
329349
className,
330350
searchResults = [],
331351
}) => {
332352
const { t } = useTranslation("common");
353+
354+
// Convert LaTeX delimiters to markdown math delimiters
355+
const processedContent = convertLatexDelimiters(content);
356+
333357
// Customize code block style with light gray background
334358
const customStyle = {
335359
...oneLight,
@@ -430,145 +454,196 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
430454
return children;
431455
};
432456

457+
class MarkdownErrorBoundary extends React.Component<
458+
{ children: React.ReactNode; rawContent: string },
459+
{ hasError: boolean }
460+
> {
461+
constructor(props: { children: React.ReactNode; rawContent: string }) {
462+
super(props);
463+
this.state = { hasError: false };
464+
}
465+
static getDerivedStateFromError() {
466+
return { hasError: true };
467+
}
468+
componentDidCatch(error: unknown) {}
469+
render() {
470+
if (this.state.hasError) {
471+
return (
472+
<div className="markdown-body">
473+
<pre className="whitespace-pre-wrap break-words text-sm">
474+
{this.props.rawContent}
475+
</pre>
476+
</div>
477+
);
478+
}
479+
return this.props.children as React.ReactElement;
480+
}
481+
}
482+
433483
return (
434484
<>
435485
<div className={`markdown-body ${className || ""}`}>
436-
<ReactMarkdown
437-
remarkPlugins={[remarkGfm, remarkMath]}
438-
rehypePlugins={[rehypeKatex as any]}
439-
components={{
440-
// Heading components - now using CSS classes
441-
h1: ({ children }: any) => (
442-
<h1 className="markdown-h1">
443-
<TextWrapper>{children}</TextWrapper>
444-
</h1>
445-
),
446-
h2: ({ children }: any) => (
447-
<h2 className="markdown-h2">
448-
<TextWrapper>{children}</TextWrapper>
449-
</h2>
450-
),
451-
h3: ({ children }: any) => (
452-
<h3 className="markdown-h3">
453-
<TextWrapper>{children}</TextWrapper>
454-
</h3>
455-
),
456-
h4: ({ children }: any) => (
457-
<h4 className="markdown-h4">
458-
<TextWrapper>{children}</TextWrapper>
459-
</h4>
460-
),
461-
h5: ({ children }: any) => (
462-
<h5 className="markdown-h5">
463-
<TextWrapper>{children}</TextWrapper>
464-
</h5>
465-
),
466-
h6: ({ children }: any) => (
467-
<h6 className="markdown-h6">
468-
<TextWrapper>{children}</TextWrapper>
469-
</h6>
470-
),
471-
// Paragraph
472-
p: ({ children }: any) => (
473-
<p className="markdown-paragraph">
474-
<TextWrapper>{children}</TextWrapper>
475-
</p>
476-
),
477-
// List item
478-
li: ({ children }: any) => (
479-
<li className="markdown-li">
480-
<TextWrapper>{children}</TextWrapper>
481-
</li>
482-
),
483-
// Blockquote
484-
blockquote: ({ children }: any) => (
485-
<blockquote className="markdown-blockquote">
486-
<TextWrapper>{children}</TextWrapper>
487-
</blockquote>
488-
),
489-
// Table components
490-
td: ({ children }: any) => (
491-
<td className="markdown-td">
492-
<TextWrapper>{children}</TextWrapper>
493-
</td>
494-
),
495-
th: ({ children }: any) => (
496-
<th className="markdown-th">
497-
<TextWrapper>{children}</TextWrapper>
498-
</th>
499-
),
500-
// Emphasis components
501-
strong: ({ children }: any) => (
502-
<strong className="markdown-strong">
503-
<TextWrapper>{children}</TextWrapper>
504-
</strong>
505-
),
506-
em: ({ children }: any) => (
507-
<em className="markdown-em">
508-
<TextWrapper>{children}</TextWrapper>
509-
</em>
510-
),
511-
// Strikethrough
512-
del: ({ children }: any) => (
513-
<del className="markdown-del">
514-
<TextWrapper>{children}</TextWrapper>
515-
</del>
516-
),
517-
// Link
518-
a: ({ href, children, ...props }: any) => (
519-
<a href={href} className="markdown-link" {...props}>
520-
<TextWrapper>{children}</TextWrapper>
521-
</a>
522-
),
523-
pre: ({ children }: any) => <>{children}</>,
524-
// Code blocks and inline code
525-
code({ node, inline, className, children, ...props }: any) {
526-
const match = /language-(\w+)/.exec(className || "");
527-
const codeContent = String(children).replace(/^\n+|\n+$/g, "");
528-
return !inline && match ? (
529-
<div className="code-block-container group">
530-
<div className="code-block-header">
531-
<span
532-
className="code-language-label"
533-
data-language={match[1]}
534-
>
535-
{match[1]}
536-
</span>
537-
<CopyButton
538-
content={codeContent}
539-
variant="code-block"
540-
className="header-copy-button"
541-
tooltipText={{
542-
copy: t("chatStreamMessage.copyContent"),
543-
copied: t("chatStreamMessage.copied"),
544-
}}
545-
/>
546-
</div>
547-
<div className="code-block-content">
548-
<SyntaxHighlighter
549-
style={customStyle}
550-
language={match[1]}
551-
PreTag="div"
552-
{...props}
553-
>
554-
{codeContent}
555-
</SyntaxHighlighter>
556-
</div>
557-
</div>
558-
) : (
559-
<code className="markdown-code" {...props}>
486+
<MarkdownErrorBoundary rawContent={processedContent}>
487+
<ReactMarkdown
488+
remarkPlugins={[remarkGfm, remarkMath] as any}
489+
rehypePlugins={
490+
[
491+
[
492+
rehypeKatex,
493+
{
494+
throwOnError: false,
495+
strict: false,
496+
trust: true,
497+
},
498+
],
499+
rehypeRaw,
500+
] as any
501+
}
502+
skipHtml={false}
503+
components={{
504+
// Heading components - now using CSS classes
505+
h1: ({ children }: any) => (
506+
<h1 className="markdown-h1">
560507
<TextWrapper>{children}</TextWrapper>
561-
</code>
562-
);
563-
},
564-
// Image
565-
img: ({ src, alt }: any) => (
566-
<img src={src} alt={alt} className="markdown-img" />
567-
),
568-
}}
569-
>
570-
{content}
571-
</ReactMarkdown>
508+
</h1>
509+
),
510+
h2: ({ children }: any) => (
511+
<h2 className="markdown-h2">
512+
<TextWrapper>{children}</TextWrapper>
513+
</h2>
514+
),
515+
h3: ({ children }: any) => (
516+
<h3 className="markdown-h3">
517+
<TextWrapper>{children}</TextWrapper>
518+
</h3>
519+
),
520+
h4: ({ children }: any) => (
521+
<h4 className="markdown-h4">
522+
<TextWrapper>{children}</TextWrapper>
523+
</h4>
524+
),
525+
h5: ({ children }: any) => (
526+
<h5 className="markdown-h5">
527+
<TextWrapper>{children}</TextWrapper>
528+
</h5>
529+
),
530+
h6: ({ children }: any) => (
531+
<h6 className="markdown-h6">
532+
<TextWrapper>{children}</TextWrapper>
533+
</h6>
534+
),
535+
// Paragraph
536+
p: ({ children }: any) => (
537+
<p className="markdown-paragraph">
538+
<TextWrapper>{children}</TextWrapper>
539+
</p>
540+
),
541+
// List item
542+
li: ({ children }: any) => (
543+
<li className="markdown-li">
544+
<TextWrapper>{children}</TextWrapper>
545+
</li>
546+
),
547+
// Blockquote
548+
blockquote: ({ children }: any) => (
549+
<blockquote className="markdown-blockquote">
550+
<TextWrapper>{children}</TextWrapper>
551+
</blockquote>
552+
),
553+
// Table components
554+
td: ({ children }: any) => (
555+
<td className="markdown-td">
556+
<TextWrapper>{children}</TextWrapper>
557+
</td>
558+
),
559+
th: ({ children }: any) => (
560+
<th className="markdown-th">
561+
<TextWrapper>{children}</TextWrapper>
562+
</th>
563+
),
564+
// Emphasis components
565+
strong: ({ children }: any) => (
566+
<strong className="markdown-strong">
567+
<TextWrapper>{children}</TextWrapper>
568+
</strong>
569+
),
570+
em: ({ children }: any) => (
571+
<em className="markdown-em">
572+
<TextWrapper>{children}</TextWrapper>
573+
</em>
574+
),
575+
// Strikethrough
576+
del: ({ children }: any) => (
577+
<del className="markdown-del">
578+
<TextWrapper>{children}</TextWrapper>
579+
</del>
580+
),
581+
// Link
582+
a: ({ href, children, ...props }: any) => (
583+
<a href={href} className="markdown-link" {...props}>
584+
<TextWrapper>{children}</TextWrapper>
585+
</a>
586+
),
587+
pre: ({ children }: any) => <>{children}</>,
588+
// Code blocks and inline code
589+
code({ node, inline, className, children, ...props }: any) {
590+
try {
591+
const match = /language-(\w+)/.exec(className || "");
592+
const raw = Array.isArray(children)
593+
? children.join("")
594+
: children ?? "";
595+
const codeContent = String(raw).replace(/^\n+|\n+$/g, "");
596+
if (!inline && match && match[1]) {
597+
return (
598+
<div className="code-block-container group">
599+
<div className="code-block-header">
600+
<span
601+
className="code-language-label"
602+
data-language={match[1]}
603+
>
604+
{match[1]}
605+
</span>
606+
<CopyButton
607+
content={codeContent}
608+
variant="code-block"
609+
className="header-copy-button"
610+
tooltipText={{
611+
copy: t("chatStreamMessage.copyContent"),
612+
copied: t("chatStreamMessage.copied"),
613+
}}
614+
/>
615+
</div>
616+
<div className="code-block-content">
617+
<SyntaxHighlighter
618+
style={customStyle}
619+
language={match[1]}
620+
PreTag="div"
621+
{...props}
622+
>
623+
{codeContent}
624+
</SyntaxHighlighter>
625+
</div>
626+
</div>
627+
);
628+
}
629+
} catch (error) {
630+
// Handle error silently
631+
}
632+
return (
633+
<code className="markdown-code" {...props}>
634+
<TextWrapper>{children}</TextWrapper>
635+
</code>
636+
);
637+
},
638+
// Image
639+
img: ({ src, alt }: any) => (
640+
<img src={src} alt={alt} className="markdown-img" />
641+
),
642+
}}
643+
>
644+
{processedContent}
645+
</ReactMarkdown>
646+
</MarkdownErrorBoundary>
572647
</div>
573648
</>
574649
);

0 commit comments

Comments
 (0)