📜 refactor: Optimize Longer Message Thread Performance (#3610)

This commit is contained in:
Danny Avila 2024-08-11 06:08:08 -04:00 committed by GitHub
parent cf69b7ef85
commit 02847af580
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 59 additions and 280 deletions

View file

@ -6,14 +6,13 @@ import supersub from 'remark-supersub';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import type { TMessage } from 'librechat-data-provider';
import type { PluggableList } from 'unified'; import type { PluggableList } from 'unified';
import rehypeHighlight from 'rehype-highlight';
import { cn, langSubset, validateIframe, processLaTeX } from '~/utils'; import { cn, langSubset, validateIframe, processLaTeX } from '~/utils';
import CodeBlock from '~/components/Messages/Content/CodeBlock'; import CodeBlock from '~/components/Messages/Content/CodeBlock';
import { useChatContext, useToastContext } from '~/Providers';
import { useFileDownload } from '~/data-provider'; import { useFileDownload } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize'; import useLocalize from '~/hooks/useLocalize';
import { useToastContext } from '~/Providers';
import store from '~/store'; import store from '~/store';
type TCodeProps = { type TCodeProps = {
@ -22,20 +21,14 @@ type TCodeProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
type TContentProps = {
content: string;
message: TMessage;
showCursor?: boolean;
};
export const code = memo(({ inline, className, children }: TCodeProps) => { export const code = memo(({ inline, className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className || ''); const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1]; const lang = match && match[1];
if (inline) { if (inline) {
return <code className={className}>{children}</code>; return <code className={className}>{children}</code>;
} else { } else {
return <CodeBlock lang={lang || 'text'} codeChildren={children} />; return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
} }
}); });
@ -110,18 +103,22 @@ export const p = memo(({ children }: { children: React.ReactNode }) => {
}); });
const cursor = ' '; const cursor = ' ';
const Markdown = memo(({ content, message, showCursor }: TContentProps) => {
const { isSubmitting, latestMessage } = useChatContext(); type TContentProps = {
content: string;
isEdited?: boolean;
showCursor?: boolean;
isLatestMessage: boolean;
};
const Markdown = memo(({ content = '', isEdited, showCursor, isLatestMessage }: TContentProps) => {
const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing); const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing);
const isInitializing = content === ''; const isInitializing = content === '';
const { isEdited, messageId } = message ?? {};
const isLatestMessage = messageId === latestMessage?.messageId;
let currentContent = content; let currentContent = content;
if (!isInitializing) { if (!isInitializing) {
currentContent = currentContent?.replace('z-index: 1;', '') ?? ''; currentContent = currentContent.replace('z-index: 1;', '') || '';
currentContent = LaTeXParsing ? processLaTeX(currentContent) : currentContent; currentContent = LaTeXParsing ? processLaTeX(currentContent) : currentContent;
} }
@ -143,18 +140,18 @@ const Markdown = memo(({ content, message, showCursor }: TContentProps) => {
return ( return (
<div className="absolute"> <div className="absolute">
<p className="relative"> <p className="relative">
<span className={cn(isSubmitting ? 'result-thinking' : '')} /> <span className={cn(showCursor === true ? 'result-thinking' : '')} />
</p> </p>
</div> </div>
); );
} }
let isValidIframe: string | boolean | null = false; let isValidIframe: string | boolean | null = false;
if (!isEdited) { if (isEdited !== true) {
isValidIframe = validateIframe(currentContent); isValidIframe = validateIframe(currentContent);
} }
if (isEdited || ((!isInitializing || !isLatestMessage) && !isValidIframe)) { if (isEdited === true || (!isLatestMessage && !isValidIframe)) {
rehypePlugins.pop(); rehypePlugins.pop();
} }
@ -173,9 +170,7 @@ const Markdown = memo(({ content, message, showCursor }: TContentProps) => {
} }
} }
> >
{isLatestMessage && isSubmitting && !isInitializing && showCursor {isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
? currentContent + cursor
: currentContent}
</ReactMarkdown> </ReactMarkdown>
); );
}); });

View file

@ -1,9 +1,10 @@
import { Fragment, Suspense } from 'react'; import { Fragment, Suspense, useMemo } from 'react';
import type { TMessage, TResPlugin } from 'librechat-data-provider'; import type { TMessage, TResPlugin } from 'librechat-data-provider';
import type { TMessageContentProps, TDisplayProps } from '~/common'; import type { TMessageContentProps, TDisplayProps } from '~/common';
import Plugin from '~/components/Messages/Content/Plugin'; import Plugin from '~/components/Messages/Content/Plugin';
import Error from '~/components/Messages/Content/Error'; import Error from '~/components/Messages/Content/Error';
import { DelayedRender } from '~/components/ui'; import { DelayedRender } from '~/components/ui';
import { useChatContext } from '~/Providers';
import EditMessage from './EditMessage'; import EditMessage from './EditMessage';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import Container from './Container'; import Container from './Container';
@ -63,17 +64,31 @@ export const ErrorMessage = ({
// Display Message Component // Display Message Component
const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => { const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => {
const { isSubmitting, latestMessage } = useChatContext();
const showCursorState = useMemo(
() => showCursor === true && isSubmitting,
[showCursor, isSubmitting],
);
const isLatestMessage = useMemo(
() => message.messageId === latestMessage?.messageId,
[message.messageId, latestMessage?.messageId],
);
return ( return (
<Container message={message}> <Container message={message}>
<div <div
className={cn( className={cn(
showCursor && !!text.length ? 'result-streaming' : '', showCursorState && !!text.length ? 'result-streaming' : '',
'markdown prose message-content dark:prose-invert light w-full break-words', 'markdown prose message-content dark:prose-invert light w-full break-words',
isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-100', isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-100',
)} )}
> >
{!isCreatedByUser ? ( {!isCreatedByUser ? (
<Markdown content={text} message={message} showCursor={showCursor} /> <Markdown
content={text}
isEdited={message.isEdited}
showCursor={showCursorState}
isLatestMessage={isLatestMessage}
/>
) : ( ) : (
<>{text}</> <>{text}</>
)} )}

View file

@ -4,9 +4,11 @@ import {
imageGenTools, imageGenTools,
isImageVisionTool, isImageVisionTool,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import { useMemo } from 'react';
import type { TMessageContentParts, TMessage } from 'librechat-data-provider'; import type { TMessageContentParts, TMessage } from 'librechat-data-provider';
import type { TDisplayProps } from '~/common'; import type { TDisplayProps } from '~/common';
import { ErrorMessage } from './MessageContent'; import { ErrorMessage } from './MessageContent';
import { useChatContext } from '~/Providers';
import RetrievalCall from './RetrievalCall'; import RetrievalCall from './RetrievalCall';
import CodeAnalyze from './CodeAnalyze'; import CodeAnalyze from './CodeAnalyze';
import Container from './Container'; import Container from './Container';
@ -20,16 +22,30 @@ import { cn } from '~/utils';
// Display Message Component // Display Message Component
const DisplayMessage = ({ text, isCreatedByUser = false, message, showCursor }: TDisplayProps) => { const DisplayMessage = ({ text, isCreatedByUser = false, message, showCursor }: TDisplayProps) => {
const { isSubmitting, latestMessage } = useChatContext();
const showCursorState = useMemo(
() => showCursor === true && isSubmitting,
[showCursor, isSubmitting],
);
const isLatestMessage = useMemo(
() => message.messageId === latestMessage?.messageId,
[message.messageId, latestMessage?.messageId],
);
return ( return (
<div <div
className={cn( className={cn(
showCursor && !!text.length ? 'result-streaming' : '', showCursorState && !!text.length ? 'result-streaming' : '',
'markdown prose message-content dark:prose-invert light w-full break-words', 'markdown prose message-content dark:prose-invert light w-full break-words',
isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-70', isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-70',
)} )}
> >
{!isCreatedByUser ? ( {!isCreatedByUser ? (
<Markdown content={text} message={message} showCursor={showCursor} /> <Markdown
content={text}
isEdited={message.isEdited}
showCursor={showCursorState}
isLatestMessage={isLatestMessage}
/>
) : ( ) : (
<>{text}</> <>{text}</>
)} )}

View file

@ -46,7 +46,7 @@ const SearchContent = ({ message }: { message: TMessage }) => {
)} )}
dir="auto" dir="auto"
> >
<MarkdownLite content={message.text ?? ''} /> <MarkdownLite content={message.text || ''} />
</div> </div>
); );
}; };

View file

@ -1,122 +0,0 @@
import React, { useState, useEffect } from 'react';
import type { TMessage } from 'librechat-data-provider';
import { useRecoilValue } from 'recoil';
import ReactMarkdown from 'react-markdown';
import type { PluggableList } from 'unified';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import CodeBlock from './CodeBlock';
import { langSubset, validateIframe } from '~/utils';
import store from '~/store';
type TCodeProps = {
inline: boolean;
className: string;
children: React.ReactNode;
};
type TContentProps = {
content: string;
message: TMessage;
showCursor?: boolean;
};
const code = React.memo(({ inline, className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className || '');
const lang = match && match[1];
if (inline) {
return <code className={className}>{children}</code>;
} else {
return <CodeBlock lang={lang || 'text'} codeChildren={children} />;
}
});
const p = React.memo(({ children }: { children: React.ReactNode }) => {
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
});
const Markdown = React.memo(({ content, message, showCursor }: TContentProps) => {
const [cursor, setCursor] = useState('█');
const isSubmitting = useRecoilValue(store.isSubmitting);
const latestMessage = useRecoilValue(store.latestMessage);
const isInitializing = content === '<span className="result-streaming">█</span>';
const { isEdited, messageId } = message ?? {};
const isLatestMessage = messageId === latestMessage?.messageId;
const currentContent = content?.replace('z-index: 1;', '') ?? '';
useEffect(() => {
let timer1: NodeJS.Timeout, timer2: NodeJS.Timeout;
if (!showCursor) {
setCursor('');
return;
}
if (isSubmitting && isLatestMessage) {
timer1 = setInterval(() => {
setCursor('');
timer2 = setTimeout(() => {
setCursor('█');
}, 200);
}, 1000);
} else {
setCursor('');
}
// This is the cleanup function that React will run when the component unmounts
return () => {
clearInterval(timer1);
clearTimeout(timer2);
};
}, [isSubmitting, isLatestMessage, showCursor]);
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: langSubset,
},
],
[rehypeRaw],
];
let isValidIframe: string | boolean | null = false;
if (!isEdited) {
isValidIframe = validateIframe(currentContent);
}
if (isEdited || ((!isInitializing || !isLatestMessage) && !isValidIframe)) {
rehypePlugins.pop();
}
return (
<ReactMarkdown
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={rehypePlugins}
linkTarget="_new"
components={
{
code,
p,
} as {
[nodeType: string]: React.ElementType;
}
}
>
{isLatestMessage && isSubmitting && !isInitializing
? currentContent + cursor
: currentContent}
</ReactMarkdown>
);
});
export default Markdown;

View file

@ -1,126 +0,0 @@
import { Fragment } from 'react';
import { ViolationTypes } from 'librechat-data-provider';
import type { TResPlugin } from 'librechat-data-provider';
import type { TMessageContentProps, TText, TDisplayProps } from '~/common';
import { useAuthContext } from '~/hooks';
import { cn } from '~/utils';
import EditMessage from './EditMessage';
import Container from './Container';
import Markdown from './Markdown';
import Plugin from './Plugin';
import Error from './Error';
const ErrorMessage = ({ text }: TText) => {
const { logout } = useAuthContext();
if (text.includes(ViolationTypes.BAN)) {
logout();
return null;
}
return (
<Container>
<div className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200">
<Error text={text} />
</div>
</Container>
);
};
// Display Message Component
const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => (
<Container>
<div
className={cn(
'markdown prose dark:prose-invert light w-full break-words',
isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-100',
)}
>
{!isCreatedByUser ? (
<Markdown content={text} message={message} showCursor={showCursor} />
) : (
<>{text}</>
)}
</div>
</Container>
);
// Unfinished Message Component
const UnfinishedMessage = () => (
<ErrorMessage text="This is an unfinished message. The AI may still be generating a response, it was aborted, or a censor was triggered. Refresh or visit later to see more updates." />
);
// Content Component
const MessageContent = ({
text,
edit,
error,
unfinished,
isSubmitting,
isLast,
...props
}: TMessageContentProps) => {
if (error) {
return <ErrorMessage text={text} />;
} else if (edit) {
return <EditMessage text={text} isSubmitting={isSubmitting} {...props} />;
} else {
const marker = ':::plugin:::\n';
const splitText = text.split(marker);
const { message } = props;
const { plugins, messageId } = message;
const displayedIndices = new Set<number>();
// Function to get the next non-empty text index
const getNextNonEmptyTextIndex = (currentIndex: number) => {
for (let i = currentIndex + 1; i < splitText.length; i++) {
// Allow the last index to be last in case it has text
// this may need to change if I add back streaming
if (i === splitText.length - 1) {
return currentIndex;
}
if (splitText[i].trim() !== '' && !displayedIndices.has(i)) {
return i;
}
}
return currentIndex; // If no non-empty text is found, return the current index
};
return splitText.map((text, idx) => {
let currentText = text.trim();
let plugin: TResPlugin | null = null;
if (plugins) {
plugin = plugins[idx];
}
// If the current text is empty, get the next non-empty text index
const displayTextIndex = currentText === '' ? getNextNonEmptyTextIndex(idx) : idx;
currentText = splitText[displayTextIndex];
const isLastIndex = displayTextIndex === splitText.length - 1;
const isEmpty = currentText.trim() === '';
const showText =
(currentText && !isEmpty && !displayedIndices.has(displayTextIndex)) ||
(isEmpty && isLastIndex);
displayedIndices.add(displayTextIndex);
return (
<Fragment key={idx}>
{plugin && <Plugin key={`plugin-${messageId}-${idx}`} plugin={plugin} />}
{showText ? (
<DisplayMessage
key={`display-${messageId}-${idx}`}
showCursor={isLastIndex && isLast}
text={currentText}
{...props}
/>
) : null}
{!isSubmitting && unfinished && (
<UnfinishedMessage key={`unfinished-${messageId}-${idx}`} />
)}
</Fragment>
);
});
}
};
export default MessageContent;

View file

@ -1,3 +1,2 @@
export { default as SubRow } from './SubRow'; export { default as SubRow } from './SubRow';
export { default as Plugin } from './Plugin'; export { default as Plugin } from './Plugin';
export { default as MessageContent } from './MessageContent';

View file

@ -1,4 +1,4 @@
export default function validateIframe(content: string): string | boolean | null { export default function validateIframe(content: string): boolean {
const hasValidIframe = const hasValidIframe =
content.includes('<iframe role="presentation" style="') && content.includes('<iframe role="presentation" style="') &&
content.includes('src="https://www.bing.com/images/create'); content.includes('src="https://www.bing.com/images/create');
@ -38,5 +38,7 @@ export default function validateIframe(content: string): string | boolean | null
const role = iframe.getAttribute('role'); const role = iframe.getAttribute('role');
const src = iframe.getAttribute('src'); const src = iframe.getAttribute('src');
return role === 'presentation' && src && src.startsWith('https://www.bing.com/images/create'); return (
role === 'presentation' && src != null && src.startsWith('https://www.bing.com/images/create')
);
} }