diff --git a/client/src/components/Chat/Messages/Content/EditMessage.tsx b/client/src/components/Chat/Messages/Content/EditMessage.tsx index 2eaaca440ab..ab2e5842428 100644 --- a/client/src/components/Chat/Messages/Content/EditMessage.tsx +++ b/client/src/components/Chat/Messages/Content/EditMessage.tsx @@ -1,17 +1,15 @@ import { useRef, useEffect, useCallback, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { useForm } from 'react-hook-form'; import { Alert, Button, TextareaAutosize } from '@librechat/client'; import { useUpdateMessageMutation } from 'librechat-data-provider/react-query'; import type { TEditProps } from '~/common'; -import { useMessagesOperations, useMessagesConversation } from '~/Providers'; +import { useMessagePartsHost } from '~/Providers/MessagePartsHostContext'; import useGetAddedConvo from '~/hooks/Conversations/useGetAddedConvo'; import { cn, isSubmittableMessage } from '~/utils'; import { findRerunParent } from './rerunParent'; import { useLocalize } from '~/hooks'; import Container from './Container'; import EditFiles from './EditFiles'; -import store from '~/store'; const EditMessage = ({ text, @@ -25,8 +23,8 @@ const EditMessage = ({ const saveButtonRef = useRef(null); const submitButtonRef = useRef(null); const [saveError, setSaveError] = useState(false); - const { conversation } = useMessagesConversation(); - const { getMessages, setMessages } = useMessagesOperations(); + const { useChatDirection, useMessagesEditing } = useMessagePartsHost(); + const { conversation, getMessages, setMessages } = useMessagesEditing(); const textAreaRef = useRef(null); @@ -46,8 +44,7 @@ const EditMessage = ({ const updateMessageMutation = useUpdateMessageMutation(conversationId ?? ''); const localize = useLocalize(); - const chatDirection = useRecoilValue(store.chatDirection).toLowerCase(); - const isRTL = chatDirection === 'rtl'; + const isRTL = useChatDirection() === 'rtl'; const getAddedConvo = useGetAddedConvo(); diff --git a/client/src/components/Chat/Messages/Content/FilePreviewDialog.tsx b/client/src/components/Chat/Messages/Content/FilePreviewDialog.tsx index 71c5d52c882..43adc4e616d 100644 --- a/client/src/components/Chat/Messages/Content/FilePreviewDialog.tsx +++ b/client/src/components/Chat/Messages/Content/FilePreviewDialog.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import copy from 'copy-to-clipboard'; import { Download } from 'lucide-react'; -import { useRecoilValue } from 'recoil'; import { Button, OGDialog, @@ -24,10 +23,10 @@ import { useSharedFileDownload, } from '~/data-provider'; import { getDownloadFilename, logger, sortPagesByRelevance, triggerDownload } from '~/utils'; +import { useMessagePartsHost } from '~/Providers/MessagePartsHostContext'; import CopyButton from '~/components/Messages/Content/CopyButton'; -import { useFileMapContext, useShareContext } from '~/Providers'; +import { useShareContext } from '~/Providers'; import { useLocalize } from '~/hooks'; -import store from '~/store'; interface FilePreviewDialogProps { open: boolean; @@ -109,9 +108,10 @@ export default function FilePreviewDialog({ triggerRef, }: FilePreviewDialogProps) { const localize = useLocalize(); - const user = useRecoilValue(store.user); + const { useUser, useFileMap } = useMessagePartsHost(); + const user = useUser(); const { shareId } = useShareContext(); - const fileMap = useFileMapContext(); + const fileMap = useFileMap(); const showExtractedText = shouldUseExtractedTextPreview( deliveryPath ?? (!shareId && fileId ? fileMap?.[fileId]?.llmDeliveryPath : undefined), ); diff --git a/client/src/components/Chat/Messages/Content/Files.tsx b/client/src/components/Chat/Messages/Content/Files.tsx index f5deee31f64..c31322c80af 100644 --- a/client/src/components/Chat/Messages/Content/Files.tsx +++ b/client/src/components/Chat/Messages/Content/Files.tsx @@ -1,13 +1,14 @@ import { useMemo, useState, useCallback, memo } from 'react'; import type { TFile, TMessage } from 'librechat-data-provider'; +import { useMessagePartsHost } from '~/Providers/MessagePartsHostContext'; import FileContainer from '~/components/Chat/Input/Files/FileContainer'; import { usesImagePreview, hydrateFileDeliveryMetadata } from '~/utils'; -import { useFileMapContext, useShareContext } from '~/Providers'; import FilePreviewDialog from './FilePreviewDialog'; +import { useShareContext } from '~/Providers'; import Image from './Image'; const Files = ({ message }: { message?: TMessage }) => { - const fileMap = useFileMapContext(); + const fileMap = useMessagePartsHost().useFileMap(); const { shareId } = useShareContext(); const files = useMemo( () => hydrateFileDeliveryMetadata(message?.files, undefined, shareId ? undefined : fileMap), diff --git a/client/src/components/Chat/Messages/Content/SearchContent.tsx b/client/src/components/Chat/Messages/Content/SearchContent.tsx index 127ec3cfdd9..e70b585afd7 100644 --- a/client/src/components/Chat/Messages/Content/SearchContent.tsx +++ b/client/src/components/Chat/Messages/Content/SearchContent.tsx @@ -1,7 +1,7 @@ import { Suspense, useMemo, Fragment } from 'react'; -import { useRecoilValue } from 'recoil'; import { DelayedRender } from '@librechat/client'; import { ContentTypes } from 'librechat-data-provider'; +import { MessagePartIndex } from '@librechat/chat/react'; import type { Agents, TMessage, @@ -11,12 +11,12 @@ import type { } from 'librechat-data-provider'; import type { ReactNode, ReactElement } from 'react'; import { ErrorSourceProvider } from '~/components/Messages/Content/Error/source'; -import { MessageContext, SearchContext, useMessageContext } from '~/Providers'; +import { useMessagePartsHost } from '~/Providers/MessagePartsHostContext'; import { ErrorMessage, UnfinishedMessage } from './MessageContent'; import { cn, mapAttachments } from '~/utils'; +import { SearchContext } from '~/Providers'; import MarkdownLite from './MarkdownLite'; import { AgentUpdate } from './Parts'; -import store from '~/store'; import Part from './Part'; /** @@ -39,8 +39,7 @@ const SearchContent = ({ /** Author icon + label re-rendered before content that resumes after an inline steer. */ authorHeader?: ReactNode; }) => { - const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown); - const messageContext = useMessageContext(); + const { enableUserMsgMarkdown } = useMessagePartsHost().useUserTextPreferences(); const { messageId } = message; const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]); @@ -79,16 +78,13 @@ const SearchContent = ({ /> ); /** An error part resolves the agent a handoff made active from its own position, - * which it reads from `MessageContext`; persisted content is compacted, so `idx` is + * which it reads from the message context; persisted content is compacted, so `idx` is * that position in `message.content`. */ const rendered: ReactElement = part.type === ContentTypes.ERROR ? ( - + {partElement} - + ) : ( partElement ); diff --git a/packages/chat/src/react.ts b/packages/chat/src/react.ts index b246378d022..db2aee6eefb 100644 --- a/packages/chat/src/react.ts +++ b/packages/chat/src/react.ts @@ -1,5 +1,5 @@ /** React bindings for the core `Chat`: providers and `useChat`. Populated as the hooks move in. */ -export { MessageContext, useMessageContext } from './react/message'; +export { MessageContext, MessagePartIndex, useMessageContext } from './react/message'; export { useMessagePartsHost, MessagePartsHostProvider, diff --git a/packages/chat/src/react/__tests__/message.spec.tsx b/packages/chat/src/react/__tests__/message.spec.tsx new file mode 100644 index 00000000000..705df778c7d --- /dev/null +++ b/packages/chat/src/react/__tests__/message.spec.tsx @@ -0,0 +1,37 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { renderHook } from '@testing-library/react'; +import { MessageContext, MessagePartIndex, useMessageContext } from '../message'; + +const surrounding: MessageContext = { + messageId: 'message-1', + conversationId: 'convo-1', + isExpanded: true, + isSubmitting: false, + isLatestMessage: true, + partIndex: 0, +}; + +describe('MessagePartIndex', () => { + it('provides the surrounding message at the part position', () => { + const { result } = renderHook(() => useMessageContext(), { + wrapper: ({ children }: { children: React.ReactNode }) => ( + + {children} + + ), + }); + expect(result.current).toEqual({ ...surrounding, partIndex: 3 }); + }); + + it('sets only the position outside a message', () => { + const { result } = renderHook(() => useMessageContext(), { + wrapper: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + }); + expect(result.current).toEqual({ partIndex: 1 }); + }); +}); diff --git a/packages/chat/src/react/message.ts b/packages/chat/src/react/message.ts index 2369db95ee9..3ac7b118480 100644 --- a/packages/chat/src/react/message.ts +++ b/packages/chat/src/react/message.ts @@ -1,5 +1,5 @@ -import { createContext, useContext } from 'react'; -import type { Context } from 'react'; +import { createContext, createElement, useContext, useMemo } from 'react'; +import type { Context, ReactNode } from 'react'; /** The message a part renders in, as the view rendering it reports. */ export type MessageContext = { @@ -18,3 +18,16 @@ export const MessageContext: Context = createContext useContext(MessageContext); + +/** Provides the surrounding message to one part, at that part's position in the message's content. */ +export function MessagePartIndex({ + index, + children, +}: { + index: number; + children?: ReactNode; +}): ReactNode { + const message = useMessageContext(); + const value = useMemo(() => ({ ...message, partIndex: index }), [message, index]); + return createElement(MessageContext.Provider, { value }, children); +}