import { memo, useRef, useMemo, useEffect } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { supportsFiles, mergeFileConfig, isAssistantsEndpoint, fileConfig as defaultFileConfig, } from 'librechat-data-provider'; import { useChatContext, useAddedChatContext, useAssistantsMapContext, useChatFormContext, } from '~/Providers'; import { useTextarea, useAutoSave, useRequiresKey, useHandleKeyUp, useQueryParams, useSubmitMessage, } from '~/hooks'; import { TextareaAutosize } from '~/components/ui'; import { useGetFileConfig } from '~/data-provider'; import { cn, removeFocusRings } from '~/utils'; import TextareaHeader from './TextareaHeader'; import PromptsCommand from './PromptsCommand'; import AttachFile from './Files/AttachFile'; import AudioRecorder from './AudioRecorder'; import { mainTextareaId } from '~/common'; import StreamAudio from './StreamAudio'; import StopButton from './StopButton'; import SendButton from './SendButton'; import FileRow from './Files/FileRow'; import Mention from './Mention'; import store from '~/store'; const ChatForm = ({ index = 0 }) => { const submitButtonRef = useRef(null); const textAreaRef = useRef(null); useQueryParams({ textAreaRef }); const SpeechToText = useRecoilValue(store.speechToText); const TextToSpeech = useRecoilValue(store.textToSpeech); const automaticPlayback = useRecoilValue(store.automaticPlayback); const isSearching = useRecoilValue(store.isSearching); const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index)); const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index)); const [showMentionPopover, setShowMentionPopover] = useRecoilState( store.showMentionPopoverFamily(index), ); const chatDirection = useRecoilValue(store.chatDirection).toLowerCase(); const isRTL = chatDirection === 'rtl'; const { requiresKey } = useRequiresKey(); const handleKeyUp = useHandleKeyUp({ index, textAreaRef, setShowPlusPopover, setShowMentionPopover, }); const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({ textAreaRef, submitButtonRef, disabled: !!(requiresKey ?? false), }); const { files, setFiles, conversation, isSubmitting, filesLoading, setFilesLoading, newConversation, handleStopGenerating, } = useChatContext(); const methods = useChatFormContext(); const { addedIndex, generateConversation, conversation: addedConvo, setConversation: setAddedConvo, isSubmitting: isSubmittingAdded, } = useAddedChatContext(); const showStopAdded = useRecoilValue(store.showStopButtonByIndex(addedIndex)); const { clearDraft } = useAutoSave({ conversationId: useMemo(() => conversation?.conversationId, [conversation]), textAreaRef, files, setFiles, }); const assistantMap = useAssistantsMapContext(); const { submitMessage, submitPrompt } = useSubmitMessage({ clearDraft }); const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null }; const endpoint = endpointType ?? _endpoint; const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ select: (data) => mergeFileConfig(data), }); const endpointFileConfig = fileConfig.endpoints[endpoint ?? '']; const invalidAssistant = useMemo( () => isAssistantsEndpoint(conversation?.endpoint) && (!(conversation?.assistant_id ?? '') || !assistantMap?.[conversation?.endpoint ?? ''][conversation?.assistant_id ?? '']), [conversation?.assistant_id, conversation?.endpoint, assistantMap], ); const disableInputs = useMemo( () => !!((requiresKey ?? false) || invalidAssistant), [requiresKey, invalidAssistant], ); const { ref, ...registerProps } = methods.register('text', { required: true, onChange: (e) => { methods.setValue('text', e.target.value, { shouldValidate: true }); }, }); useEffect(() => { if (!isSearching && textAreaRef.current && !disableInputs) { textAreaRef.current.focus(); } }, [isSearching, disableInputs]); return (
submitMessage(data))} className="stretch mx-2 flex flex-row gap-3 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl" >
{showPlusPopover && !isAssistantsEndpoint(endpoint) && ( )} {showMentionPopover && ( )}
(
{children}
)} /> {endpoint && ( { ref(e); textAreaRef.current = e; }} disabled={disableInputs} onPaste={handlePaste} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} id={mainTextareaId} tabIndex={0} data-testid="text-input" style={{ height: 44, overflowY: 'auto' }} rows={1} className={cn( supportsFiles[endpointType ?? endpoint ?? ''] && !endpointFileConfig?.disabled ? ' pl-10 md:pl-[55px]' : 'pl-3 md:pl-4', 'm-0 w-full resize-none border-0 bg-transparent py-[10px] placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 ', SpeechToText && !isRTL ? 'pr-20 md:pr-[85px]' : 'pr-10 md:pr-12', 'max-h-[65vh] md:max-h-[75vh]', removeFocusRings, )} /> )} {(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? ( ) : ( endpoint && ( ) )} {SpeechToText && ( )} {TextToSpeech && automaticPlayback && }
); }; export default memo(ChatForm);