Skip to content

Commit 67c8707

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

File tree

2 files changed

+223
-138
lines changed

2 files changed

+223
-138
lines changed

frontend/components/ui/markdownRenderer.tsx

Lines changed: 218 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,43 @@ 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+
let text = content;
336+
337+
// Convert \( ... \) to $ ... $ (inline math)
338+
const inlineMathRegex = /\\\(([\s\S]*?)\\\)/g;
339+
text = text.replace(inlineMathRegex, (match: string, content: string) => {
340+
const converted = `$${content}$`;
341+
return converted;
342+
});
343+
344+
// Convert \[ ... \] to $$ ... $$ (display math)
345+
const displayMathRegex = /\\\[([\s\S]*?)\\\]/g;
346+
text = text.replace(displayMathRegex, (match: string, content: string) => {
347+
const converted = `$$${content}$$\n`;
348+
return converted;
349+
});
350+
351+
return text;
352+
};
353+
327354
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
328355
content,
329356
className,
330357
searchResults = [],
331358
}) => {
332359
const { t } = useTranslation("common");
360+
361+
// Convert LaTeX delimiters to markdown math delimiters
362+
const processedContent = convertLatexDelimiters(content);
363+
333364
// Customize code block style with light gray background
334365
const customStyle = {
335366
...oneLight,
@@ -430,145 +461,196 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
430461
return children;
431462
};
432463

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

0 commit comments

Comments
 (0)