| import { useCallback, useMemo, memo } from 'react'; |
| import { useAtomValue } from 'jotai'; |
| import { useRecoilValue } from 'recoil'; |
| import type { TMessage, TMessageContentParts } from 'librechat-data-provider'; |
| import type { TMessageProps, TMessageIcon } 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 MessageIcon from '~/components/Chat/Messages/MessageIcon'; |
| import { useAttachments, useMessageActions } from '~/hooks'; |
| import SubRow from '~/components/Chat/Messages/SubRow'; |
| import { fontSizeAtom } from '~/store/fontSize'; |
| 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( |
| ({ |
| message: msg, |
| isCard = false, |
| siblingIdx, |
| siblingCount, |
| setSiblingIdx, |
| currentEditId, |
| isMultiMessage = false, |
| setCurrentEditId, |
| isSubmittingFamily = false, |
| }: ContentRenderProps) => { |
| const { attachments, searchResults } = useAttachments({ |
| messageId: msg?.messageId, |
| attachments: msg?.attachments, |
| }); |
| const { |
| edit, |
| index, |
| agent, |
| assistant, |
| enterEdit, |
| conversation, |
| messageLabel, |
| isSubmitting, |
| latestMessage, |
| handleContinue, |
| copyToClipboard, |
| setLatestMessage, |
| regenerateMessage, |
| handleFeedback, |
| } = useMessageActions({ |
| message: msg, |
| searchResults, |
| currentEditId, |
| isMultiMessage, |
| setCurrentEditId, |
| }); |
| const fontSize = useAtomValue(fontSizeAtom); |
| const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); |
|
|
| const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]); |
| const isLast = useMemo( |
| () => |
| !(msg?.children?.length ?? 0) && (msg?.depth === latestMessage?.depth || msg?.depth === -1), |
| [msg?.children, msg?.depth, latestMessage?.depth], |
| ); |
| const isLatestMessage = msg?.messageId === latestMessage?.messageId; |
| const showCardRender = isLast && !isSubmittingFamily && isCard; |
| const isLatestCard = isCard && !isSubmittingFamily && isLatestMessage; |
|
|
| const iconData: TMessageIcon = useMemo( |
| () => ({ |
| endpoint: msg?.endpoint ?? conversation?.endpoint, |
| model: msg?.model ?? conversation?.model, |
| iconURL: msg?.iconURL, |
| modelLabel: messageLabel, |
| isCreatedByUser: msg?.isCreatedByUser, |
| }), |
| [ |
| messageLabel, |
| conversation?.endpoint, |
| conversation?.model, |
| msg?.model, |
| msg?.iconURL, |
| msg?.endpoint, |
| msg?.isCreatedByUser, |
| ], |
| ); |
|
|
| const clickHandler = useMemo( |
| () => |
| showCardRender && !isLatestMessage |
| ? () => { |
| logger.log( |
| 'latest_message', |
| `Message Card click: Setting ${msg?.messageId} as latest message`, |
| ); |
| logger.dir(msg); |
| setLatestMessage(msg!); |
| } |
| : undefined, |
| [showCardRender, isLatestMessage, msg, setLatestMessage], |
| ); |
|
|
| if (!msg) { |
| return null; |
| } |
|
|
| const baseClasses = { |
| common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ', |
| card: 'relative w-full gap-1 rounded-lg border border-border-medium bg-surface-primary-alt p-2 md:w-1/2 md:gap-3 md:p-4', |
| chat: maximizeChatSpace |
| ? 'w-full max-w-full md:px-5 lg:px-1 xl:px-5' |
| : 'md:max-w-[47rem] xl:max-w-[55rem]', |
| }; |
|
|
| const conditionalClasses = { |
| latestCard: isLatestCard ? 'bg-surface-secondary' : '', |
| cardRender: showCardRender ? 'cursor-pointer transition-colors duration-300' : '', |
| focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy', |
| }; |
|
|
| return ( |
| <div |
| id={msg.messageId} |
| aria-label={`message-${msg.depth}-${msg.messageId}`} |
| className={cn( |
| baseClasses.common, |
| isCard ? baseClasses.card : baseClasses.chat, |
| conditionalClasses.latestCard, |
| conditionalClasses.cardRender, |
| conditionalClasses.focus, |
| 'message-render', |
| )} |
| onClick={clickHandler} |
| onKeyDown={(e) => { |
| if ((e.key === 'Enter' || e.key === ' ') && clickHandler) { |
| clickHandler(); |
| } |
| }} |
| role={showCardRender ? 'button' : undefined} |
| tabIndex={showCardRender ? 0 : undefined} |
| > |
| {isLatestCard && ( |
| <div className="absolute right-0 top-0 m-2 h-3 w-3 rounded-full bg-text-primary" /> |
| )} |
| |
| <div className="relative flex flex-shrink-0 flex-col items-center"> |
| <div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"> |
| <MessageIcon iconData={iconData} assistant={assistant} agent={agent} /> |
| </div> |
| </div> |
| |
| <div |
| className={cn( |
| 'relative flex w-11/12 flex-col', |
| msg.isCreatedByUser ? 'user-turn' : 'agent-turn', |
| )} |
| > |
| <h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2> |
| |
| <div className="flex flex-col gap-1"> |
| <div className="flex max-w-full flex-grow flex-col gap-0"> |
| <ContentParts |
| edit={edit} |
| isLast={isLast} |
| enterEdit={enterEdit} |
| siblingIdx={siblingIdx} |
| messageId={msg.messageId} |
| attachments={attachments} |
| isSubmitting={isSubmitting} |
| searchResults={searchResults} |
| setSiblingIdx={setSiblingIdx} |
| isLatestMessage={isLatestMessage} |
| isCreatedByUser={msg.isCreatedByUser} |
| conversationId={conversation?.conversationId} |
| content={msg.content as Array<TMessageContentParts | undefined>} |
| /> |
| </div> |
| |
| {(isSubmittingFamily || isSubmitting) && !(msg.children?.length ?? 0) ? ( |
| <PlaceholderRow isCard={isCard} /> |
| ) : ( |
| <SubRow classes="text-xs"> |
| <SiblingSwitch |
| siblingIdx={siblingIdx} |
| siblingCount={siblingCount} |
| setSiblingIdx={setSiblingIdx} |
| /> |
| <HoverButtons |
| index={index} |
| isEditing={edit} |
| message={msg} |
| enterEdit={enterEdit} |
| isSubmitting={isSubmitting} |
| conversation={conversation ?? null} |
| regenerate={handleRegenerateMessage} |
| copyToClipboard={copyToClipboard} |
| handleContinue={handleContinue} |
| latestMessage={latestMessage} |
| handleFeedback={handleFeedback} |
| isLast={isLast} |
| /> |
| </SubRow> |
| )} |
| </div> |
| </div> |
| </div> |
| ); |
| }, |
| ); |
| |
| export default ContentRender; |
| |