| import { useRef, useEffect, useCallback } from 'react'; | |
| import { useForm } from 'react-hook-form'; | |
| import { useRecoilState, useRecoilValue } from 'recoil'; | |
| import { TextareaAutosize, TooltipAnchor } from '@librechat/client'; | |
| import { useUpdateMessageMutation } from 'librechat-data-provider/react-query'; | |
| import type { TEditProps } from '~/common'; | |
| import { useMessagesOperations, useMessagesConversation, useAddedChatContext } from '~/Providers'; | |
| import { cn, removeFocusRings } from '~/utils'; | |
| import { useLocalize } from '~/hooks'; | |
| import Container from './Container'; | |
| import store from '~/store'; | |
| const EditMessage = ({ | |
| text, | |
| message, | |
| isSubmitting, | |
| ask, | |
| enterEdit, | |
| siblingIdx, | |
| setSiblingIdx, | |
| }: TEditProps) => { | |
| const { addedIndex } = useAddedChatContext(); | |
| const saveButtonRef = useRef<HTMLButtonElement | null>(null); | |
| const submitButtonRef = useRef<HTMLButtonElement | null>(null); | |
| const { conversation } = useMessagesConversation(); | |
| const { getMessages, setMessages } = useMessagesOperations(); | |
| const [latestMultiMessage, setLatestMultiMessage] = useRecoilState( | |
| store.latestMessageFamily(addedIndex), | |
| ); | |
| const textAreaRef = useRef<HTMLTextAreaElement | null>(null); | |
| const { conversationId, parentMessageId, messageId } = message; | |
| const updateMessageMutation = useUpdateMessageMutation(conversationId ?? ''); | |
| const localize = useLocalize(); | |
| const chatDirection = useRecoilValue(store.chatDirection).toLowerCase(); | |
| const isRTL = chatDirection === 'rtl'; | |
| const { register, handleSubmit, setValue } = useForm({ | |
| defaultValues: { | |
| text: text ?? '', | |
| }, | |
| }); | |
| useEffect(() => { | |
| const textArea = textAreaRef.current; | |
| if (textArea) { | |
| const length = textArea.value.length; | |
| textArea.focus(); | |
| textArea.setSelectionRange(length, length); | |
| } | |
| }, []); | |
| const resubmitMessage = (data: { text: string }) => { | |
| if (message.isCreatedByUser) { | |
| ask( | |
| { | |
| text: data.text, | |
| parentMessageId, | |
| conversationId, | |
| }, | |
| { | |
| overrideFiles: message.files, | |
| }, | |
| ); | |
| setSiblingIdx((siblingIdx ?? 0) - 1); | |
| } else { | |
| const messages = getMessages(); | |
| const parentMessage = messages?.find((msg) => msg.messageId === parentMessageId); | |
| if (!parentMessage) { | |
| return; | |
| } | |
| ask( | |
| { ...parentMessage }, | |
| { | |
| editedText: data.text, | |
| editedMessageId: messageId, | |
| isRegenerate: true, | |
| isEdited: true, | |
| }, | |
| ); | |
| setSiblingIdx((siblingIdx ?? 0) - 1); | |
| } | |
| enterEdit(true); | |
| }; | |
| const updateMessage = (data: { text: string }) => { | |
| const messages = getMessages(); | |
| if (!messages) { | |
| return; | |
| } | |
| updateMessageMutation.mutate({ | |
| conversationId: conversationId ?? '', | |
| model: conversation?.model ?? 'gpt-3.5-turbo', | |
| text: data.text, | |
| messageId, | |
| }); | |
| if (message.messageId === latestMultiMessage?.messageId) { | |
| setLatestMultiMessage({ ...latestMultiMessage, text: data.text }); | |
| } | |
| const isInMessages = messages.some((message) => message.messageId === messageId); | |
| if (!isInMessages) { | |
| message.text = data.text; | |
| } else { | |
| setMessages( | |
| messages.map((msg) => | |
| msg.messageId === messageId | |
| ? { | |
| ...msg, | |
| text: data.text, | |
| } | |
| : msg, | |
| ), | |
| ); | |
| } | |
| enterEdit(true); | |
| }; | |
| const handleKeyDown = useCallback( | |
| (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { | |
| e.preventDefault(); | |
| submitButtonRef.current?.click(); | |
| } | |
| if (e.key === 's' && (e.ctrlKey || e.metaKey)) { | |
| e.preventDefault(); | |
| saveButtonRef.current?.click(); | |
| } | |
| if (e.key === 'Escape') { | |
| e.preventDefault(); | |
| enterEdit(true); | |
| } | |
| }, | |
| [enterEdit], | |
| ); | |
| const { ref, ...registerProps } = register('text', { | |
| required: true, | |
| onChange: (e) => { | |
| setValue('text', e.target.value, { shouldValidate: true }); | |
| }, | |
| }); | |
| return ( | |
| <Container message={message}> | |
| <div className="bg-token-main-surface-primary relative mt-2 flex w-full flex-grow flex-col overflow-hidden rounded-2xl border border-border-medium text-text-primary [&:has(textarea:focus)]:border-border-heavy [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]"> | |
| <TextareaAutosize | |
| {...registerProps} | |
| ref={(e) => { | |
| ref(e); | |
| textAreaRef.current = e; | |
| }} | |
| onKeyDown={handleKeyDown} | |
| data-testid="message-text-editor" | |
| className={cn( | |
| 'markdown prose dark:prose-invert light whitespace-pre-wrap break-words pl-3 md:pl-4', | |
| 'm-0 w-full resize-none border-0 bg-transparent py-[10px]', | |
| 'placeholder-text-secondary focus:ring-0 focus-visible:ring-0 md:py-3.5', | |
| isRTL ? 'text-right' : 'text-left', | |
| 'max-h-[65vh] pr-3 md:max-h-[75vh] md:pr-4', | |
| removeFocusRings, | |
| )} | |
| aria-label={localize('com_ui_message_input')} | |
| dir={isRTL ? 'rtl' : 'ltr'} | |
| /> | |
| </div> | |
| <div className="mt-2 flex w-full justify-center text-center"> | |
| <TooltipAnchor | |
| description="Ctrl + Enter / ⌘ + Enter" | |
| render={ | |
| <button | |
| ref={submitButtonRef} | |
| className="btn btn-primary relative mr-2" | |
| disabled={isSubmitting} | |
| onClick={handleSubmit(resubmitMessage)} | |
| > | |
| {localize('com_ui_save_submit')} | |
| </button> | |
| } | |
| /> | |
| <TooltipAnchor | |
| description="Shift + Enter" | |
| render={ | |
| <button | |
| ref={saveButtonRef} | |
| className="btn btn-secondary relative mr-2" | |
| disabled={isSubmitting} | |
| onClick={handleSubmit(updateMessage)} | |
| > | |
| {localize('com_ui_save')} | |
| </button> | |
| } | |
| /> | |
| <TooltipAnchor | |
| description="Esc" | |
| render={ | |
| <button className="btn btn-neutral relative" onClick={() => enterEdit(true)}> | |
| {localize('com_ui_cancel')} | |
| </button> | |
| } | |
| /> | |
| </div> | |
| </Container> | |
| ); | |
| }; | |
| export default EditMessage; | |