@@ -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,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+
327347export 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 = / 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 } >
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 = / l a n g u a g e - ( \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