diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index 0e4a04ced5..4789e66ebf 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -6,14 +6,13 @@ import supersub from 'remark-supersub'; import rehypeKatex from 'rehype-katex'; import { useRecoilValue } from 'recoil'; import ReactMarkdown from 'react-markdown'; -import rehypeHighlight from 'rehype-highlight'; -import type { TMessage } from 'librechat-data-provider'; import type { PluggableList } from 'unified'; +import rehypeHighlight from 'rehype-highlight'; import { cn, langSubset, validateIframe, processLaTeX } from '~/utils'; import CodeBlock from '~/components/Messages/Content/CodeBlock'; -import { useChatContext, useToastContext } from '~/Providers'; import { useFileDownload } from '~/data-provider'; import useLocalize from '~/hooks/useLocalize'; +import { useToastContext } from '~/Providers'; import store from '~/store'; type TCodeProps = { @@ -22,20 +21,14 @@ type TCodeProps = { children: React.ReactNode; }; -type TContentProps = { - content: string; - message: TMessage; - showCursor?: boolean; -}; - 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]; if (inline) { return {children}; } else { - return ; + return ; } }); @@ -110,18 +103,22 @@ export const p = memo(({ children }: { children: React.ReactNode }) => { }); 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(store.LaTeXParsing); const isInitializing = content === ''; - const { isEdited, messageId } = message ?? {}; - const isLatestMessage = messageId === latestMessage?.messageId; - let currentContent = content; if (!isInitializing) { - currentContent = currentContent?.replace('z-index: 1;', '') ?? ''; + currentContent = currentContent.replace('z-index: 1;', '') || ''; currentContent = LaTeXParsing ? processLaTeX(currentContent) : currentContent; } @@ -143,18 +140,18 @@ const Markdown = memo(({ content, message, showCursor }: TContentProps) => { return (

- +

); } let isValidIframe: string | boolean | null = false; - if (!isEdited) { + if (isEdited !== true) { isValidIframe = validateIframe(currentContent); } - if (isEdited || ((!isInitializing || !isLatestMessage) && !isValidIframe)) { + if (isEdited === true || (!isLatestMessage && !isValidIframe)) { rehypePlugins.pop(); } @@ -173,9 +170,7 @@ const Markdown = memo(({ content, message, showCursor }: TContentProps) => { } } > - {isLatestMessage && isSubmitting && !isInitializing && showCursor - ? currentContent + cursor - : currentContent} + {isLatestMessage && showCursor === true ? currentContent + cursor : currentContent} ); }); diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx index 9a2af8284a..d191aecb87 100644 --- a/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -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 { TMessageContentProps, TDisplayProps } from '~/common'; import Plugin from '~/components/Messages/Content/Plugin'; import Error from '~/components/Messages/Content/Error'; import { DelayedRender } from '~/components/ui'; +import { useChatContext } from '~/Providers'; import EditMessage from './EditMessage'; import { useLocalize } from '~/hooks'; import Container from './Container'; @@ -63,17 +64,31 @@ export const ErrorMessage = ({ // Display Message Component 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 (
{!isCreatedByUser ? ( - + ) : ( <>{text} )} diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index ae0324addc..bb6673b753 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -4,9 +4,11 @@ import { imageGenTools, isImageVisionTool, } from 'librechat-data-provider'; +import { useMemo } from 'react'; import type { TMessageContentParts, TMessage } from 'librechat-data-provider'; import type { TDisplayProps } from '~/common'; import { ErrorMessage } from './MessageContent'; +import { useChatContext } from '~/Providers'; import RetrievalCall from './RetrievalCall'; import CodeAnalyze from './CodeAnalyze'; import Container from './Container'; @@ -20,16 +22,30 @@ import { cn } from '~/utils'; // Display Message Component 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 (
{!isCreatedByUser ? ( - + ) : ( <>{text} )} diff --git a/client/src/components/Chat/Messages/Content/SearchContent.tsx b/client/src/components/Chat/Messages/Content/SearchContent.tsx index 754b041d2a..a76d3fcf59 100644 --- a/client/src/components/Chat/Messages/Content/SearchContent.tsx +++ b/client/src/components/Chat/Messages/Content/SearchContent.tsx @@ -46,7 +46,7 @@ const SearchContent = ({ message }: { message: TMessage }) => { )} dir="auto" > - +
); }; diff --git a/client/src/components/Messages/Content/Markdown.tsx b/client/src/components/Messages/Content/Markdown.tsx deleted file mode 100644 index 11061a906a..0000000000 --- a/client/src/components/Messages/Content/Markdown.tsx +++ /dev/null @@ -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 {children}; - } else { - return ; - } -}); - -const p = React.memo(({ children }: { children: React.ReactNode }) => { - return

{children}

; -}); - -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 === ''; - - 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 ( - - {isLatestMessage && isSubmitting && !isInitializing - ? currentContent + cursor - : currentContent} - - ); -}); - -export default Markdown; diff --git a/client/src/components/Messages/Content/MessageContent.tsx b/client/src/components/Messages/Content/MessageContent.tsx deleted file mode 100644 index 479ecac358..0000000000 --- a/client/src/components/Messages/Content/MessageContent.tsx +++ /dev/null @@ -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 ( - -
- -
-
- ); -}; - -// Display Message Component -const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => ( - -
- {!isCreatedByUser ? ( - - ) : ( - <>{text} - )} -
-
-); - -// Unfinished Message Component -const UnfinishedMessage = () => ( - -); - -// Content Component -const MessageContent = ({ - text, - edit, - error, - unfinished, - isSubmitting, - isLast, - ...props -}: TMessageContentProps) => { - if (error) { - return ; - } else if (edit) { - return ; - } else { - const marker = ':::plugin:::\n'; - const splitText = text.split(marker); - const { message } = props; - const { plugins, messageId } = message; - const displayedIndices = new Set(); - // 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 ( - - {plugin && } - {showText ? ( - - ) : null} - {!isSubmitting && unfinished && ( - - )} - - ); - }); - } -}; - -export default MessageContent; diff --git a/client/src/components/Messages/Content/index.ts b/client/src/components/Messages/Content/index.ts index 1d25a152d1..a558d09db6 100644 --- a/client/src/components/Messages/Content/index.ts +++ b/client/src/components/Messages/Content/index.ts @@ -1,3 +1,2 @@ export { default as SubRow } from './SubRow'; export { default as Plugin } from './Plugin'; -export { default as MessageContent } from './MessageContent'; diff --git a/client/src/utils/validateIframe.ts b/client/src/utils/validateIframe.ts index 302472acfb..412a8f0272 100644 --- a/client/src/utils/validateIframe.ts +++ b/client/src/utils/validateIframe.ts @@ -1,4 +1,4 @@ -export default function validateIframe(content: string): string | boolean | null { +export default function validateIframe(content: string): boolean { const hasValidIframe = content.includes('