LibreChat/client/src/components/Chat/Messages/Content/MessageContent.tsx
Marco Beretta 2d62eca612
👐 style: Improve a11y/theming for Settings Dialog, Dropdown Menus; fix: SearchBar focus issues (#4091)
* fix: cursor pointer not applying correct in the root component

* fix: add cursor-not-allowed to disabled state in SendButton component

* feat: update Dropdown to ariakit and changed LLM error's style

* feat: switched to ariakit's Dropdown and style improvements

* feat: archive updates

* refactor: delete conversations in archive

* refactor: settings

* add cool settings animation

* a11y: settings update

* style: update settings

* style: settings account settings menu; a11y(AccountSettings): switched to AriaKit

* a11y: account settings update

* style: update my files dialog

* fix: tests

* chore: remove console.log()

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-09-21 22:45:50 -04:00

195 lines
6.2 KiB
TypeScript

import { Fragment, Suspense, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
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 MarkdownLite from './MarkdownLite';
import EditMessage from './EditMessage';
import { useLocalize } from '~/hooks';
import Container from './Container';
import Markdown from './Markdown';
import { cn } from '~/utils';
import store from '~/store';
export const ErrorMessage = ({
text,
message,
className = '',
}: Pick<TDisplayProps, 'text' | 'className'> & {
message?: TMessage;
}) => {
const localize = useLocalize();
if (text === 'Error connecting to server, try refreshing the page.') {
console.log('error message', message);
return (
<Suspense
fallback={
<div className="text-message mb-[0.625rem] flex min-h-[20px] flex-col items-start gap-3 overflow-x-auto">
<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>
}
>
<DelayedRender delay={5500}>
<Container message={message}>
<div
className={cn(
'rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
className,
)}
>
{localize('com_ui_error_connection')}
</div>
</Container>
</DelayedRender>
</Suspense>
);
}
return (
<Container message={message}>
<div
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,
)}
>
<Error text={text} />
</div>
</Container>
);
};
const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => {
const { isSubmitting, latestMessage } = useChatContext();
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
const showCursorState = useMemo(
() => showCursor === true && isSubmitting,
[showCursor, isSubmitting],
);
const isLatestMessage = useMemo(
() => message.messageId === latestMessage?.messageId,
[message.messageId, latestMessage?.messageId],
);
let content: React.ReactElement;
if (!isCreatedByUser) {
content = (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
} else if (enableUserMsgMarkdown) {
content = <MarkdownLite content={text} />;
} else {
content = <>{text}</>;
}
return (
<Container message={message}>
<div
className={cn(
isSubmitting ? 'submitting' : '',
showCursorState && !!text.length ? 'result-streaming' : '',
'markdown prose message-content dark:prose-invert light w-full break-words',
isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-100',
)}
>
{content}
</div>
</Container>
);
};
// Unfinished Message Component
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."
/>
);
// Content Component
const MessageContent = ({
text,
edit,
error,
unfinished,
isSubmitting,
isLast,
...props
}: TMessageContentProps) => {
if (error) {
return <ErrorMessage message={props.message} 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 && isSubmitting}
text={currentText}
{...props}
/>
) : null}
{!isSubmitting && unfinished && (
<Suspense>
<DelayedRender delay={250}>
<UnfinishedMessage message={message} key={`unfinished-${messageId}-${idx}`} />
</DelayedRender>
</Suspense>
)}
</Fragment>
);
});
}
};
export default MessageContent;