From d66a35887d3955686abbe6bda329c18171693f5d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 2 Sep 2024 21:20:34 -0400 Subject: [PATCH] refactor: non-assistant message content, parts --- .../Chat/Messages/Content/Container.tsx | 4 +- .../Chat/Messages/Content/ContentParts.tsx | 63 +++---- .../Chat/Messages/Content/Files.tsx | 4 +- .../Chat/Messages/Content/MessageContent.tsx | 4 +- .../components/Chat/Messages/Content/Part.tsx | 80 +++------ .../Chat/Messages/Content/Parts/Text.tsx | 39 +++++ .../components/Chat/Messages/MessageParts.tsx | 23 +-- .../components/Chat/Messages/MultiMessage.tsx | 26 ++- .../src/components/Messages/ContentRender.tsx | 164 ++++++++++++++++++ .../components/Messages/MessageContent.tsx | 82 +++++++++ 10 files changed, 363 insertions(+), 126 deletions(-) create mode 100644 client/src/components/Chat/Messages/Content/Parts/Text.tsx create mode 100644 client/src/components/Messages/ContentRender.tsx create mode 100644 client/src/components/Messages/MessageContent.tsx diff --git a/client/src/components/Chat/Messages/Content/Container.tsx b/client/src/components/Chat/Messages/Content/Container.tsx index cbd085e308..ecc40d6cd1 100644 --- a/client/src/components/Chat/Messages/Content/Container.tsx +++ b/client/src/components/Chat/Messages/Content/Container.tsx @@ -1,12 +1,12 @@ import { TMessage } from 'librechat-data-provider'; import Files from './Files'; -const Container = ({ children, message }: { children: React.ReactNode; message: TMessage }) => ( +const Container = ({ children, message }: { children: React.ReactNode; message?: TMessage }) => (
- {message.isCreatedByUser && } + {message?.isCreatedByUser === true && } {children}
); diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index 3227bce072..d73377241e 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -1,51 +1,34 @@ -import { Suspense } from 'react'; +import { memo } from 'react'; import type { TMessageContentParts } from 'librechat-data-provider'; -import { UnfinishedMessage } from './MessageContent'; -import { DelayedRender } from '~/components/ui'; import Part from './Part'; -const ContentParts = ({ - error, - unfinished, - isSubmitting, - isLast, - content, - ...props -}: // eslint-disable-next-line @typescript-eslint/no-explicit-any -any) => { - if (error) { - // return ; - } else { - const { message } = props; - const { messageId } = message; +type ContentPartsProps = { + content: Array; + messageId: string; + isCreatedByUser: boolean; + isLast: boolean; + isSubmitting: boolean; +}; +const ContentParts = memo( + ({ content, messageId, isCreatedByUser, isLast, isSubmitting }: ContentPartsProps) => { return ( <> {content - .filter((part: TMessageContentParts | undefined) => part) - .map((part: TMessageContentParts | undefined, idx: number) => { - const showCursor = idx === content.length - 1 && isLast; - return ( - - ); - })} - {/* Temporarily remove this */} - {/* {!isSubmitting && unfinished && ( - - - - - - )} */} + .filter((part) => part) + .map((part, idx) => ( + + ))} ); - } -}; + }, +); export default ContentParts; diff --git a/client/src/components/Chat/Messages/Content/Files.tsx b/client/src/components/Chat/Messages/Content/Files.tsx index beff81b58b..09801d92c1 100644 --- a/client/src/components/Chat/Messages/Content/Files.tsx +++ b/client/src/components/Chat/Messages/Content/Files.tsx @@ -3,7 +3,7 @@ import type { TFile, TMessage } from 'librechat-data-provider'; import FileContainer from '~/components/Chat/Input/Files/FileContainer'; import Image from './Image'; -const Files = ({ message }: { message: TMessage }) => { +const Files = ({ message }: { message?: TMessage }) => { const imageFiles = useMemo(() => { return message?.files?.filter((file) => file.type?.startsWith('image/')) || []; }, [message?.files]); @@ -20,7 +20,7 @@ const Files = ({ message }: { message: TMessage }) => { imageFiles.map((file) => ( ) => { +}: Pick & { + message?: TMessage; +}) => { const localize = useLocalize(); if (text === 'Error connecting to server, try refreshing the page.') { console.log('error message', message); diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index 7fcc9dbe4a..457e79b9d1 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -4,81 +4,43 @@ import { imageGenTools, isImageVisionTool, } from 'librechat-data-provider'; -import { useMemo } from 'react'; -import type { TMessageContentParts, TMessage } from 'librechat-data-provider'; -import type { TDisplayProps } from '~/common'; +import { memo } from 'react'; +import type { TMessageContentParts } from 'librechat-data-provider'; import { ErrorMessage } from './MessageContent'; -import { useChatContext } from '~/Providers'; import RetrievalCall from './RetrievalCall'; import CodeAnalyze from './CodeAnalyze'; import Container from './Container'; import ToolCall from './ToolCall'; -import Markdown from './Markdown'; import ImageGen from './ImageGen'; -import { cn } from '~/utils'; +import Text from './Parts/Text'; import Image from './Image'; -// Display Message Component -const DisplayMessage = ({ text, isCreatedByUser = false, message, showCursor }: TDisplayProps) => { - const { isSubmitting, latestMessage } = useChatContext(); - const showCursorState = useMemo( - () => showCursor === true && isSubmitting, - [showCursor, isSubmitting], - ); - const isLatestMessage = useMemo( - () => message.messageId === latestMessage?.messageId, - [message.messageId, latestMessage?.messageId], - ); - - // Note: for testing purposes - // isSubmitting && isLatestMessage && logger.log('message_stream', { text, isCreatedByUser, isSubmitting, showCursorState }); - - return ( -
- {!isCreatedByUser ? ( - - ) : ( - <>{text} - )} -
- ); -}; - -export default function Part({ - part, - showCursor, - isSubmitting, - message, -}: { - part: TMessageContentParts | undefined; +type PartProps = { + part?: TMessageContentParts; isSubmitting: boolean; showCursor: boolean; - message: TMessage; -}) { + messageId: string; + isCreatedByUser: boolean; +}; + +const Part = memo(({ part, isSubmitting, showCursor, messageId, isCreatedByUser }: PartProps) => { if (!part) { return null; } if (part.type === ContentTypes.ERROR) { - return ; + return ; } else if (part.type === ContentTypes.TEXT) { const text = typeof part.text === 'string' ? part.text : part.text.value; if (typeof text !== 'string') { return null; } return ( - - + @@ -132,11 +94,11 @@ export default function Part({ if (isImageVisionTool(toolCall)) { if (isSubmitting && showCursor) { return ( - - + @@ -174,4 +136,6 @@ export default function Part({ } return null; -} +}); + +export default Part; diff --git a/client/src/components/Chat/Messages/Content/Parts/Text.tsx b/client/src/components/Chat/Messages/Content/Parts/Text.tsx new file mode 100644 index 0000000000..7d0b386c81 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/Text.tsx @@ -0,0 +1,39 @@ +import { memo, useMemo } from 'react'; +import { useChatContext } from '~/Providers'; +import Markdown from '~/components/Chat/Messages/Content/Markdown'; +import { cn } from '~/utils'; + +type TextPartProps = { + text: string; + isCreatedByUser: boolean; + messageId: string; + showCursor: boolean; +}; + +const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPartProps) => { + const { isSubmitting, latestMessage } = useChatContext(); + const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]); + const isLatestMessage = useMemo( + () => messageId === latestMessage?.messageId, + [messageId, latestMessage?.messageId], + ); + + return ( +
+ {!isCreatedByUser ? ( + + ) : ( + <>{text} + )} +
+ ); +}); + +export default TextPart; diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx index 37794c44f5..52a2dbe2dd 100644 --- a/client/src/components/Chat/Messages/MessageParts.tsx +++ b/client/src/components/Chat/Messages/MessageParts.tsx @@ -1,4 +1,5 @@ import { useRecoilValue } from 'recoil'; +import type { TMessageContentParts } from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; import Icon from '~/components/Chat/Messages/MessageIcon'; import { useMessageHelpers, useLocalize } from '~/hooks'; @@ -17,7 +18,6 @@ export default function Message(props: TMessageProps) { props; const { - ask, edit, index, agent, @@ -33,7 +33,7 @@ export default function Message(props: TMessageProps) { regenerateMessage, } = useMessageHelpers(props); const fontSize = useRecoilValue(store.fontSize); - const { content, children, messageId = null, isCreatedByUser, error, unfinished } = message ?? {}; + const { children, messageId = null, isCreatedByUser } = message ?? {}; if (!message) { return null; @@ -82,24 +82,11 @@ export default function Message(props: TMessageProps) {
} + messageId={message.messageId} + isCreatedByUser={message.isCreatedByUser} isLast={isLast} - content={content ?? []} - message={message} - messageId={messageId} - enterEdit={enterEdit} - error={!!(error ?? false)} isSubmitting={isSubmitting} - unfinished={unfinished ?? false} - isCreatedByUser={isCreatedByUser ?? true} - siblingIdx={siblingIdx ?? 0} - setSiblingIdx={ - setSiblingIdx ?? - (() => { - return; - }) - } />
diff --git a/client/src/components/Chat/Messages/MultiMessage.tsx b/client/src/components/Chat/Messages/MultiMessage.tsx index 0e3204f392..eb2d2e9257 100644 --- a/client/src/components/Chat/Messages/MultiMessage.tsx +++ b/client/src/components/Chat/Messages/MultiMessage.tsx @@ -1,10 +1,14 @@ import { useRecoilState } from 'recoil'; import { useEffect, useCallback } from 'react'; +import { isAssistantsEndpoint } from 'librechat-data-provider'; +import type { TMessage } from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; // eslint-disable-next-line import/no-cycle -import Message from './Message'; +import MessageContent from '~/components/Messages/MessageContent'; // eslint-disable-next-line import/no-cycle import MessageParts from './MessageParts'; +// eslint-disable-next-line import/no-cycle +import Message from './Message'; import store from '~/store'; export default function MultiMessage({ @@ -30,22 +34,22 @@ export default function MultiMessage({ }, [messagesTree?.length]); useEffect(() => { - if (messagesTree?.length && siblingIdx >= messagesTree?.length) { + if (messagesTree?.length && siblingIdx >= messagesTree.length) { setSiblingIdx(0); } }, [siblingIdx, messagesTree?.length, setSiblingIdx]); - if (!(messagesTree && messagesTree?.length)) { + if (!(messagesTree && messagesTree.length)) { return null; } - const message = messagesTree[messagesTree.length - siblingIdx - 1]; + const message = messagesTree[messagesTree.length - siblingIdx - 1] as TMessage | undefined; if (!message) { return null; } - if (message.content) { + if (isAssistantsEndpoint(message.endpoint) && message.content) { return ( ); + } else if (message.content) { + return ( + + ); } return ( diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx new file mode 100644 index 0000000000..d3f58942b5 --- /dev/null +++ b/client/src/components/Messages/ContentRender.tsx @@ -0,0 +1,164 @@ +import { useRecoilValue } from 'recoil'; +import { useCallback, useMemo, memo } from 'react'; +import type { TMessage, TMessageContentParts } from 'librechat-data-provider'; +import type { TMessageProps } from '~/common'; +import ContentParts from '~/components/Chat/Messages/Content/ContentParts'; +import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow'; +import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch'; +import HoverButtons from '~/components/Chat/Messages/HoverButtons'; +import Icon from '~/components/Chat/Messages/MessageIcon'; +import SubRow from '~/components/Chat/Messages/SubRow'; +import { useMessageActions } from '~/hooks'; +import { cn, logger } from '~/utils'; +import store from '~/store'; + +type ContentRenderProps = { + message?: TMessage; + isCard?: boolean; + isMultiMessage?: boolean; + isSubmittingFamily?: boolean; +} & Pick< + TMessageProps, + 'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount' +>; + +const ContentRender = memo( + ({ + isCard, + siblingIdx, + siblingCount, + message: msg, + setSiblingIdx, + currentEditId, + isMultiMessage, + setCurrentEditId, + isSubmittingFamily, + }: ContentRenderProps) => { + const { + ask, + edit, + index, + assistant, + enterEdit, + conversation, + messageLabel, + isSubmitting, + latestMessage, + handleContinue, + copyToClipboard, + setLatestMessage, + regenerateMessage, + } = useMessageActions({ + message: msg, + currentEditId, + isMultiMessage, + setCurrentEditId, + }); + + const fontSize = useRecoilValue(store.fontSize); + const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]); + // const { isCreatedByUser, error, unfinished } = msg ?? {}; + const isLast = useMemo( + () => + !(msg?.children?.length ?? 0) && (msg?.depth === latestMessage?.depth || msg?.depth === -1), + [msg?.children, msg?.depth, latestMessage?.depth], + ); + + if (!msg) { + return null; + } + + const isLatestMessage = msg.messageId === latestMessage?.messageId; + const showCardRender = isLast && !(isSubmittingFamily === true) && isCard === true; + const isLatestCard = isCard === true && !(isSubmittingFamily === true) && isLatestMessage; + const clickHandler = + showCardRender && !isLatestMessage + ? () => { + logger.log(`Message Card click: Setting ${msg.messageId} as latest message`); + logger.dir(msg); + setLatestMessage(msg); + } + : undefined; + + return ( +
{ + if ((e.key === 'Enter' || e.key === ' ') && clickHandler) { + clickHandler(); + } + }} + role={showCardRender ? 'button' : undefined} + tabIndex={showCardRender ? 0 : undefined} + > + {isLatestCard === true && ( +
+ )} +
+
+
+
+ +
+
+
+
+
+

{messageLabel}

+
+
+ } + messageId={msg.messageId} + isCreatedByUser={msg.isCreatedByUser} + isLast={isLast} + isSubmitting={isSubmitting} + /> +
+
+ {!(msg.children?.length ?? 0) && (isSubmittingFamily === true || isSubmitting) ? ( + + ) : ( + + + + + )} +
+
+ ); + }, +); + +export default ContentRender; diff --git a/client/src/components/Messages/MessageContent.tsx b/client/src/components/Messages/MessageContent.tsx new file mode 100644 index 0000000000..e472b1e8dd --- /dev/null +++ b/client/src/components/Messages/MessageContent.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { useMessageProcess } from '~/hooks'; +import type { TMessageProps } from '~/common'; +// eslint-disable-next-line import/no-cycle +import MultiMessage from '~/components/Chat/Messages/MultiMessage'; +import ContentRender from './ContentRender'; + +const MessageContainer = React.memo( + ({ + handleScroll, + children, + }: { + handleScroll: (event?: unknown) => void; + children: React.ReactNode; + }) => { + return ( +
+ {children} +
+ ); + }, +); + +export default function MessageContent(props: TMessageProps) { + const { + showSibling, + conversation, + handleScroll, + siblingMessage, + latestMultiMessage, + isSubmittingFamily, + } = useMessageProcess({ message: props.message }); + const { message, currentEditId, setCurrentEditId } = props; + + if (!message || typeof message !== 'object') { + return null; + } + + const { children, messageId = null } = message; + + return ( + <> + + {showSibling ? ( +
+
+ + +
+
+ ) : ( +
+ +
+ )} +
+ + + ); +}