| import { memo, Suspense, useMemo } from 'react'; | |
| import { useRecoilValue } from 'recoil'; | |
| import { DelayedRender } from '@librechat/client'; | |
| import type { TMessage } from 'librechat-data-provider'; | |
| import type { TMessageContentProps, TDisplayProps } from '~/common'; | |
| import Error from '~/components/Messages/Content/Error'; | |
| import { useMessageContext } from '~/Providers'; | |
| import MarkdownLite from './MarkdownLite'; | |
| import EditMessage from './EditMessage'; | |
| import Thinking from './Parts/Thinking'; | |
| import { useLocalize } from '~/hooks'; | |
| import Container from './Container'; | |
| import Markdown from './Markdown'; | |
| import { cn } from '~/utils'; | |
| import store from '~/store'; | |
| const ERROR_CONNECTION_TEXT = 'Error connecting to server, try refreshing the page.'; | |
| const DELAYED_ERROR_TIMEOUT = 5500; | |
| const UNFINISHED_DELAY = 250; | |
| const parseThinkingContent = (text: string) => { | |
| const thinkingMatch = text.match(/:::thinking([\s\S]*?):::/); | |
| return { | |
| thinkingContent: thinkingMatch ? thinkingMatch[1].trim() : '', | |
| regularContent: thinkingMatch ? text.replace(/:::thinking[\s\S]*?:::/, '').trim() : text, | |
| }; | |
| }; | |
| const LoadingFallback = () => ( | |
| <div className="text-message mb-[0.625rem] flex min-h-[20px] flex-col items-start gap-3 overflow-visible"> | |
| <div className="markdown prose dark:prose-invert light w-full break-words dark:text-gray-100"> | |
| <div className="absolute"> | |
| <p className="submitting relative"> | |
| <span className="result-thinking" /> | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| const ErrorBox = ({ | |
| children, | |
| className = '', | |
| }: { | |
| children: React.ReactNode; | |
| className?: string; | |
| }) => ( | |
| <div | |
| role="alert" | |
| aria-live="assertive" | |
| className={cn( | |
| 'rounded-xl border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200', | |
| className, | |
| )} | |
| > | |
| {children} | |
| </div> | |
| ); | |
| const ConnectionError = ({ message }: { message?: TMessage }) => { | |
| const localize = useLocalize(); | |
| return ( | |
| <Suspense fallback={<LoadingFallback />}> | |
| <DelayedRender delay={DELAYED_ERROR_TIMEOUT}> | |
| <Container message={message}> | |
| <div className="mt-2 rounded-xl border border-red-500/20 bg-red-50/50 px-4 py-3 text-sm text-red-700 shadow-sm transition-all dark:bg-red-950/30 dark:text-red-100"> | |
| {localize('com_ui_error_connection')} | |
| </div> | |
| </Container> | |
| </DelayedRender> | |
| </Suspense> | |
| ); | |
| }; | |
| export const ErrorMessage = ({ | |
| text, | |
| message, | |
| className = '', | |
| }: Pick<TDisplayProps, 'text' | 'className'> & { message?: TMessage }) => { | |
| if (text === ERROR_CONNECTION_TEXT) { | |
| return <ConnectionError message={message} />; | |
| } | |
| return ( | |
| <Container message={message}> | |
| <ErrorBox className={className}> | |
| <Error text={text} /> | |
| </ErrorBox> | |
| </Container> | |
| ); | |
| }; | |
| const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => { | |
| const { isSubmitting = false, isLatestMessage = false } = useMessageContext(); | |
| const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown); | |
| const showCursorState = useMemo( | |
| () => showCursor === true && isSubmitting, | |
| [showCursor, isSubmitting], | |
| ); | |
| const content = useMemo(() => { | |
| if (!isCreatedByUser) { | |
| return <Markdown content={text} isLatestMessage={isLatestMessage} />; | |
| } | |
| if (enableUserMsgMarkdown) { | |
| return <MarkdownLite content={text} />; | |
| } | |
| return <>{text}</>; | |
| }, [isCreatedByUser, enableUserMsgMarkdown, text, isLatestMessage]); | |
| return ( | |
| <Container message={message}> | |
| <div | |
| className={cn( | |
| 'markdown prose message-content dark:prose-invert light w-full break-words', | |
| isSubmitting && 'submitting', | |
| showCursorState && text.length > 0 && 'result-streaming', | |
| isCreatedByUser && !enableUserMsgMarkdown && 'whitespace-pre-wrap', | |
| isCreatedByUser ? 'dark:text-gray-20' : 'dark:text-gray-100', | |
| )} | |
| > | |
| {content} | |
| </div> | |
| </Container> | |
| ); | |
| }; | |
| export const UnfinishedMessage = ({ message }: { message: TMessage }) => ( | |
| <ErrorMessage | |
| message={message} | |
| text="The response is incomplete; it's either still processing, was cancelled, or censored. Refresh or try a different prompt." | |
| /> | |
| ); | |
| const MessageContent = ({ | |
| text, | |
| edit, | |
| error, | |
| unfinished, | |
| isSubmitting, | |
| isLast, | |
| ...props | |
| }: TMessageContentProps) => { | |
| const { message } = props; | |
| const { messageId } = message; | |
| const { thinkingContent, regularContent } = useMemo(() => parseThinkingContent(text), [text]); | |
| const showRegularCursor = useMemo(() => isLast && isSubmitting, [isLast, isSubmitting]); | |
| const unfinishedMessage = useMemo( | |
| () => | |
| !isSubmitting && unfinished ? ( | |
| <Suspense> | |
| <DelayedRender delay={UNFINISHED_DELAY}> | |
| <UnfinishedMessage message={message} /> | |
| </DelayedRender> | |
| </Suspense> | |
| ) : null, | |
| [isSubmitting, unfinished, message], | |
| ); | |
| if (error) { | |
| return <ErrorMessage message={message} text={text} />; | |
| } | |
| if (edit) { | |
| return <EditMessage text={text} isSubmitting={isSubmitting} {...props} />; | |
| } | |
| return ( | |
| <> | |
| {thinkingContent.length > 0 && ( | |
| <Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking> | |
| )} | |
| <DisplayMessage | |
| key={`display-${messageId}`} | |
| showCursor={showRegularCursor} | |
| text={regularContent} | |
| {...props} | |
| /> | |
| {unfinishedMessage} | |
| </> | |
| ); | |
| }; | |
| export default memo(MessageContent); | |