diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index 7bd6511cfa..1954b80e32 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -19,6 +19,7 @@ import { Citation, CompositeCitation, HighlightedText } from '~/components/Web/C import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact'; import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils'; import CodeBlock from '~/components/Messages/Content/CodeBlock'; +import MarkdownErrorBoundary from './MarkdownErrorBoundary'; import useHasAccess from '~/hooks/Roles/useHasAccess'; import { unicodeCitation } from '~/components/Web'; import { useFileDownload } from '~/data-provider'; @@ -219,31 +220,33 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => { } return ( - - - + + + - {currentContent} - - - + > + {currentContent} + + + + ); }); diff --git a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx new file mode 100644 index 0000000000..15c68f7e9d --- /dev/null +++ b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import remarkGfm from 'remark-gfm'; +import supersub from 'remark-supersub'; +import ReactMarkdown from 'react-markdown'; +import rehypeHighlight from 'rehype-highlight'; +import type { PluggableList } from 'unified'; +import { code, codeNoExecution, a, p } from './Markdown'; +import { CodeBlockProvider } from '~/Providers'; +import { langSubset } from '~/utils'; + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +interface MarkdownErrorBoundaryProps { + children: React.ReactNode; + content: string; + codeExecution?: boolean; +} + +class MarkdownErrorBoundary extends React.Component< + MarkdownErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: MarkdownErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Markdown rendering error:', error, errorInfo); + } + + componentDidUpdate(prevProps: MarkdownErrorBoundaryProps) { + if (prevProps.content !== this.props.content && this.state.hasError) { + this.setState({ hasError: false, error: undefined }); + } + } + + render() { + if (this.state.hasError) { + const { content, codeExecution = true } = this.props; + + const rehypePlugins: PluggableList = [ + [ + rehypeHighlight, + { + detect: true, + ignoreMissing: true, + subset: langSubset, + }, + ], + ]; + + return ( + + + {content} + + + ); + } + + return this.props.children; + } +} + +export default MarkdownErrorBoundary; diff --git a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx index 019783607c..c3b302d0dd 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx @@ -8,6 +8,7 @@ import rehypeHighlight from 'rehype-highlight'; import type { PluggableList } from 'unified'; import { code, codeNoExecution, a, p } from './Markdown'; import { CodeBlockProvider, ArtifactProvider } from '~/Providers'; +import MarkdownErrorBoundary from './MarkdownErrorBoundary'; import { langSubset } from '~/utils'; const MarkdownLite = memo( @@ -25,32 +26,34 @@ const MarkdownLite = memo( ]; return ( - - - + + + - {content} - - - + > + {content} + + + + ); }, ); diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index c63dfe31e7..75e6d6ea17 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -85,7 +85,7 @@ const Part = memo( const isToolCall = 'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL); - if (isToolCall && toolCall.name === Tools.execute_code) { + if (isToolCall && toolCall.name === Tools.execute_code && toolCall.args) { return ( { - let parsedArgs: ParsedArgs | string = args; + let parsedArgs: ParsedArgs | string | undefined | null = args; try { - parsedArgs = JSON.parse(args); + parsedArgs = JSON.parse(args || ''); } catch { // console.error('Failed to parse args:', e); } if (typeof parsedArgs === 'object') { return parsedArgs; } - const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/); - const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s); + const langMatch = args?.match(/"lang"\s*:\s*"(\w+)"/); + const codeMatch = args?.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s); let code = ''; if (codeMatch) { @@ -51,7 +51,7 @@ export default function ExecuteCode({ attachments, }: { initialProgress: number; - args: string; + args?: string; output?: string; attachments?: TAttachment[]; }) { @@ -65,7 +65,7 @@ export default function ExecuteCode({ const outputRef = useRef(output); const prevShowCodeRef = useRef(showCode); - const { lang, code } = useParseArgs(args); + const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs); const progress = useProgress(initialProgress); useEffect(() => { @@ -144,7 +144,7 @@ export default function ExecuteCode({ onClick={() => setShowCode((prev) => !prev)} inProgressText={localize('com_ui_analyzing')} finishedText={localize('com_ui_analyzing_finished')} - hasInput={!!code.length} + hasInput={!!code?.length} isExpanded={showCode} />