mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
📝 fix: Resolve Markdown Rendering Issues (#8352)
* 🔧 fix: Handle optional arguments in `useParseArgs` and improve tool call condition
* chore: Remove math plugins from `MarkdownLite`
* ✨ feat: Add Error Boundary to Markdown Component for Enhanced Error Handling
- Introduced `MarkdownErrorBoundary` to catch and display errors during Markdown rendering.
- Updated the `Markdown` component to utilize the new error boundary, improving user experience by handling rendering issues gracefully.
* Revert "chore: Remove math plugins from `MarkdownLite`"
This reverts commit d393099d52.
* ✨ feat: Introduce MarkdownErrorBoundary for improved error handling in Markdown components
* refactor: include most markdown elements in error boundary fallback, aside from problematic plugins
This commit is contained in:
parent
4918899c8d
commit
4b32ec42c6
5 changed files with 155 additions and 59 deletions
|
|
@ -19,6 +19,7 @@ import { Citation, CompositeCitation, HighlightedText } from '~/components/Web/C
|
||||||
import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
|
import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
|
||||||
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
|
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
|
||||||
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||||
|
import MarkdownErrorBoundary from './MarkdownErrorBoundary';
|
||||||
import useHasAccess from '~/hooks/Roles/useHasAccess';
|
import useHasAccess from '~/hooks/Roles/useHasAccess';
|
||||||
import { unicodeCitation } from '~/components/Web';
|
import { unicodeCitation } from '~/components/Web';
|
||||||
import { useFileDownload } from '~/data-provider';
|
import { useFileDownload } from '~/data-provider';
|
||||||
|
|
@ -219,6 +220,7 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<MarkdownErrorBoundary content={content} codeExecution={true}>
|
||||||
<ArtifactProvider>
|
<ArtifactProvider>
|
||||||
<CodeBlockProvider>
|
<CodeBlockProvider>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
@ -244,6 +246,7 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</CodeBlockProvider>
|
</CodeBlockProvider>
|
||||||
</ArtifactProvider>
|
</ArtifactProvider>
|
||||||
|
</MarkdownErrorBoundary>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<CodeBlockProvider>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[
|
||||||
|
/** @ts-ignore */
|
||||||
|
supersub,
|
||||||
|
remarkGfm,
|
||||||
|
]}
|
||||||
|
/** @ts-ignore */
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={
|
||||||
|
{
|
||||||
|
code: codeExecution ? code : codeNoExecution,
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
} as {
|
||||||
|
[nodeType: string]: React.ElementType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</CodeBlockProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MarkdownErrorBoundary;
|
||||||
|
|
@ -8,6 +8,7 @@ import rehypeHighlight from 'rehype-highlight';
|
||||||
import type { PluggableList } from 'unified';
|
import type { PluggableList } from 'unified';
|
||||||
import { code, codeNoExecution, a, p } from './Markdown';
|
import { code, codeNoExecution, a, p } from './Markdown';
|
||||||
import { CodeBlockProvider, ArtifactProvider } from '~/Providers';
|
import { CodeBlockProvider, ArtifactProvider } from '~/Providers';
|
||||||
|
import MarkdownErrorBoundary from './MarkdownErrorBoundary';
|
||||||
import { langSubset } from '~/utils';
|
import { langSubset } from '~/utils';
|
||||||
|
|
||||||
const MarkdownLite = memo(
|
const MarkdownLite = memo(
|
||||||
|
|
@ -25,6 +26,7 @@ const MarkdownLite = memo(
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<MarkdownErrorBoundary content={content} codeExecution={codeExecution}>
|
||||||
<ArtifactProvider>
|
<ArtifactProvider>
|
||||||
<CodeBlockProvider>
|
<CodeBlockProvider>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
@ -51,6 +53,7 @@ const MarkdownLite = memo(
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</CodeBlockProvider>
|
</CodeBlockProvider>
|
||||||
</ArtifactProvider>
|
</ArtifactProvider>
|
||||||
|
</MarkdownErrorBoundary>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ const Part = memo(
|
||||||
|
|
||||||
const isToolCall =
|
const isToolCall =
|
||||||
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
|
'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 (
|
return (
|
||||||
<ExecuteCode
|
<ExecuteCode
|
||||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,23 @@ import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
interface ParsedArgs {
|
interface ParsedArgs {
|
||||||
lang: string;
|
lang?: string;
|
||||||
code: string;
|
code?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useParseArgs(args: string): ParsedArgs {
|
export function useParseArgs(args?: string): ParsedArgs | null {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
let parsedArgs: ParsedArgs | string = args;
|
let parsedArgs: ParsedArgs | string | undefined | null = args;
|
||||||
try {
|
try {
|
||||||
parsedArgs = JSON.parse(args);
|
parsedArgs = JSON.parse(args || '');
|
||||||
} catch {
|
} catch {
|
||||||
// console.error('Failed to parse args:', e);
|
// console.error('Failed to parse args:', e);
|
||||||
}
|
}
|
||||||
if (typeof parsedArgs === 'object') {
|
if (typeof parsedArgs === 'object') {
|
||||||
return parsedArgs;
|
return parsedArgs;
|
||||||
}
|
}
|
||||||
const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/);
|
const langMatch = args?.match(/"lang"\s*:\s*"(\w+)"/);
|
||||||
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s);
|
const codeMatch = args?.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s);
|
||||||
|
|
||||||
let code = '';
|
let code = '';
|
||||||
if (codeMatch) {
|
if (codeMatch) {
|
||||||
|
|
@ -51,7 +51,7 @@ export default function ExecuteCode({
|
||||||
attachments,
|
attachments,
|
||||||
}: {
|
}: {
|
||||||
initialProgress: number;
|
initialProgress: number;
|
||||||
args: string;
|
args?: string;
|
||||||
output?: string;
|
output?: string;
|
||||||
attachments?: TAttachment[];
|
attachments?: TAttachment[];
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -65,7 +65,7 @@ export default function ExecuteCode({
|
||||||
const outputRef = useRef<string>(output);
|
const outputRef = useRef<string>(output);
|
||||||
const prevShowCodeRef = useRef<boolean>(showCode);
|
const prevShowCodeRef = useRef<boolean>(showCode);
|
||||||
|
|
||||||
const { lang, code } = useParseArgs(args);
|
const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs);
|
||||||
const progress = useProgress(initialProgress);
|
const progress = useProgress(initialProgress);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -144,7 +144,7 @@ export default function ExecuteCode({
|
||||||
onClick={() => setShowCode((prev) => !prev)}
|
onClick={() => setShowCode((prev) => !prev)}
|
||||||
inProgressText={localize('com_ui_analyzing')}
|
inProgressText={localize('com_ui_analyzing')}
|
||||||
finishedText={localize('com_ui_analyzing_finished')}
|
finishedText={localize('com_ui_analyzing_finished')}
|
||||||
hasInput={!!code.length}
|
hasInput={!!code?.length}
|
||||||
isExpanded={showCode}
|
isExpanded={showCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue