@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
33import ReactMarkdown from "react-markdown" ;
44import remarkGfm from "remark-gfm" ;
55import remarkMath from "remark-math" ;
6+ import rehypeRaw from "rehype-raw" ;
67import rehypeKatex from "rehype-katex" ;
78// @ts -ignore
89import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" ;
@@ -11,7 +12,6 @@ import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
1112import * as TooltipPrimitive from "@radix-ui/react-tooltip" ;
1213
1314import { SearchResult } from "@/types/chat" ;
14-
1515import {
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+
327354export 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 = / l a n g u a g e - ( \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 = / l a n g u a g e - ( \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