🖋️ feat: Add option to render User Messages as Markdown (#4170)

This commit is contained in:
Danny Avila 2024-09-20 20:29:42 -04:00 committed by GitHub
parent 42b7373ddc
commit be44caaab1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 94 additions and 21 deletions

View file

@ -1,15 +1,18 @@
import { Fragment, Suspense, useMemo } from 'react'; import { Fragment, Suspense, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
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 { useChatContext } from '~/Providers';
import MarkdownLite from './MarkdownLite';
import EditMessage from './EditMessage'; import EditMessage from './EditMessage';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import Container from './Container'; import Container from './Container';
import Markdown from './Markdown'; import Markdown from './Markdown';
import { cn } from '~/utils'; import { cn } from '~/utils';
import store from '~/store';
export const ErrorMessage = ({ export const ErrorMessage = ({
text, text,
@ -64,9 +67,9 @@ export const ErrorMessage = ({
); );
}; };
// Display Message Component
const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => { const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => {
const { isSubmitting, latestMessage } = useChatContext(); const { isSubmitting, latestMessage } = useChatContext();
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
const showCursorState = useMemo( const showCursorState = useMemo(
() => showCursor === true && isSubmitting, () => showCursor === true && isSubmitting,
[showCursor, isSubmitting], [showCursor, isSubmitting],
@ -75,6 +78,18 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay
() => message.messageId === latestMessage?.messageId, () => message.messageId === latestMessage?.messageId,
[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 ( return (
<Container message={message}> <Container message={message}>
<div <div
@ -85,11 +100,7 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay
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 ? ( {content}
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
) : (
<>{text}</>
)}
</div> </div>
</Container> </Container>
); );

View file

@ -1,7 +1,10 @@
import { memo, useMemo } from 'react'; import { memo, useMemo, ReactElement } from 'react';
import { useChatContext } from '~/Providers'; import { useRecoilValue } from 'recoil';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import Markdown from '~/components/Chat/Messages/Content/Markdown'; import Markdown from '~/components/Chat/Messages/Content/Markdown';
import { useChatContext } from '~/Providers';
import { cn } from '~/utils'; import { cn } from '~/utils';
import store from '~/store';
type TextPartProps = { type TextPartProps = {
text: string; text: string;
@ -10,14 +13,32 @@ type TextPartProps = {
showCursor: boolean; showCursor: boolean;
}; };
type ContentType =
| ReactElement<React.ComponentProps<typeof Markdown>>
| ReactElement<React.ComponentProps<typeof MarkdownLite>>
| ReactElement;
const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPartProps) => { const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPartProps) => {
const { isSubmitting, latestMessage } = useChatContext(); const { isSubmitting, latestMessage } = useChatContext();
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]); const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);
const isLatestMessage = useMemo( const isLatestMessage = useMemo(
() => messageId === latestMessage?.messageId, () => messageId === latestMessage?.messageId,
[messageId, latestMessage?.messageId], [messageId, latestMessage?.messageId],
); );
const content: ContentType = useMemo(() => {
if (!isCreatedByUser) {
return (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
} else if (enableUserMsgMarkdown) {
return <MarkdownLite content={text} />;
} else {
return <>{text}</>;
}
}, [isCreatedByUser, enableUserMsgMarkdown, text, showCursorState, isLatestMessage]);
return ( return (
<div <div
className={cn( className={cn(
@ -27,11 +48,7 @@ const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPar
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 ? ( {content}
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
) : (
<>{text}</>
)}
</div> </div>
); );
}); });

View file

@ -22,12 +22,13 @@ const SearchContent = ({ message }: { message: TMessage }) => {
key={`display-${messageId}-${idx}`} key={`display-${messageId}-${idx}`}
showCursor={false} showCursor={false}
isSubmitting={false} isSubmitting={false}
isCreatedByUser={message.isCreatedByUser}
messageId={message.messageId}
part={part} part={part}
message={message}
/> />
); );
})} })}
{message.unfinished && ( {message.unfinished === true && (
<Suspense> <Suspense>
<DelayedRender delay={250}> <DelayedRender delay={250}>
<UnfinishedMessage message={message} key={`unfinished-${messageId}`} /> <UnfinishedMessage message={message} key={`unfinished-${messageId}`} />

View file

@ -1,9 +1,10 @@
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import React, { useContext, useCallback, useRef } from 'react'; import React, { useContext, useCallback } from 'react';
import type { TDangerButtonProps } from '~/common'; import type { TDangerButtonProps } from '~/common';
import { ThemeContext, useLocalize } from '~/hooks'; import UserMsgMarkdownSwitch from './UserMsgMarkdownSwitch';
import HideSidePanelSwitch from './HideSidePanelSwitch'; import HideSidePanelSwitch from './HideSidePanelSwitch';
import { ThemeContext, useLocalize } from '~/hooks';
import AutoScrollSwitch from './AutoScrollSwitch'; import AutoScrollSwitch from './AutoScrollSwitch';
import ArchivedChats from './ArchivedChats'; import ArchivedChats from './ArchivedChats';
import { Dropdown } from '~/components/ui'; import { Dropdown } from '~/components/ui';
@ -123,8 +124,6 @@ function General() {
const [langcode, setLangcode] = useRecoilState(store.lang); const [langcode, setLangcode] = useRecoilState(store.lang);
const contentRef = useRef(null);
const changeTheme = useCallback( const changeTheme = useCallback(
(value: string) => { (value: string) => {
setTheme(value); setTheme(value);
@ -156,6 +155,9 @@ function General() {
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0"> <div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<LangSelector langcode={langcode} onChange={changeLang} /> <LangSelector langcode={langcode} onChange={changeLang} />
</div> </div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<UserMsgMarkdownSwitch />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0"> <div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<AutoScrollSwitch /> <AutoScrollSwitch />
</div> </div>

View file

@ -0,0 +1,35 @@
import { useRecoilState } from 'recoil';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function UserMsgMarkdownSwitch({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const localize = useLocalize();
const [enableUserMsgMarkdown, setEnableUserMsgMarkdown] = useRecoilState<boolean>(
store.enableUserMsgMarkdown,
);
const handleCheckedChange = (value: boolean) => {
setEnableUserMsgMarkdown(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div> {localize('com_nav_user_msg_markdown')} </div>
<Switch
id="enableUserMsgMarkdown"
checked={enableUserMsgMarkdown}
onCheckedChange={handleCheckedChange}
className="ml-4 mt-2 ring-ring-primary"
data-testid="enableUserMsgMarkdown"
/>
</div>
);
}

View file

@ -625,6 +625,7 @@ export default {
com_nav_welcome_assistant: 'Please Select an Assistant', com_nav_welcome_assistant: 'Please Select an Assistant',
com_nav_welcome_message: 'How can I help you today?', com_nav_welcome_message: 'How can I help you today?',
com_nav_auto_scroll: 'Auto-Scroll to latest message on chat open', com_nav_auto_scroll: 'Auto-Scroll to latest message on chat open',
com_nav_user_msg_markdown: 'Render user messages as markdown',
com_nav_hide_panel: 'Hide right-most side panel', com_nav_hide_panel: 'Hide right-most side panel',
com_nav_modular_chat: 'Enable switching Endpoints mid-conversation', com_nav_modular_chat: 'Enable switching Endpoints mid-conversation',
com_nav_latex_parsing: 'Parsing LaTeX in messages (may affect performance)', com_nav_latex_parsing: 'Parsing LaTeX in messages (may affect performance)',

View file

@ -23,6 +23,10 @@ const localStorageAtoms = {
autoScroll: atomWithLocalStorage('autoScroll', false), autoScroll: atomWithLocalStorage('autoScroll', false),
hideSidePanel: atomWithLocalStorage('hideSidePanel', false), hideSidePanel: atomWithLocalStorage('hideSidePanel', false),
fontSize: atomWithLocalStorage('fontSize', 'text-base'), fontSize: atomWithLocalStorage('fontSize', 'text-base'),
enableUserMsgMarkdown: atomWithLocalStorage<boolean>(
LocalStorageKeys.ENABLE_USER_MSG_MARKDOWN,
true,
),
// Messages settings // Messages settings
enterToSend: atomWithLocalStorage('enterToSend', true), enterToSend: atomWithLocalStorage('enterToSend', true),

2
package-lock.json generated
View file

@ -36680,7 +36680,7 @@
}, },
"packages/data-provider": { "packages/data-provider": {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.7.424", "version": "0.7.425",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View file

@ -1,6 +1,6 @@
{ {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.7.424", "version": "0.7.425",
"description": "data services for librechat apps", "description": "data services for librechat apps",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",

View file

@ -1119,6 +1119,8 @@ export enum LocalStorageKeys {
FILES_DRAFT = 'filesDraft_', FILES_DRAFT = 'filesDraft_',
/** Key for last Selected Prompt Category */ /** Key for last Selected Prompt Category */
LAST_PROMPT_CATEGORY = 'lastPromptCategory', LAST_PROMPT_CATEGORY = 'lastPromptCategory',
/** Key for rendering User Messages as Markdown */
ENABLE_USER_MSG_MARKDOWN = 'enableUserMsgMarkdown',
} }
export enum ForkOptions { export enum ForkOptions {