mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🖋️ feat: Add option to render User Messages as Markdown (#4170)
This commit is contained in:
parent
42b7373ddc
commit
be44caaab1
10 changed files with 94 additions and 21 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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}`} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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)',
|
||||||
|
|
|
||||||
|
|
@ -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
2
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue