File size: 5,542 Bytes
f0743f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | 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);
|