import { useRecoilState } from 'recoil'; import { useForm } from 'react-hook-form'; import TextareaAutosize from 'react-textarea-autosize'; import { memo, useCallback, useRef, useMemo } from 'react'; import { supportsFiles, mergeFileConfig, fileConfig as defaultFileConfig, EModelEndpoint, } from 'librechat-data-provider'; import { useChatContext, useAssistantsMapContext } from '~/Providers'; import { useRequiresKey, useTextarea } from '~/hooks'; import { useGetFileConfig } from '~/data-provider'; import { cn, removeFocusOutlines } from '~/utils'; import AttachFile from './Files/AttachFile'; import StopButton from './StopButton'; import SendButton from './SendButton'; import FileRow from './Files/FileRow'; import store from '~/store'; const ChatForm = ({ index = 0 }) => { const submitButtonRef = useRef(null); const textAreaRef = useRef(null); const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index)); const { requiresKey } = useRequiresKey(); const { handlePaste, handleKeyUp, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({ textAreaRef, submitButtonRef, disabled: !!requiresKey }); const { ask, files, setFiles, conversation, isSubmitting, handleStopGenerating, filesLoading, setFilesLoading, } = useChatContext(); const assistantMap = useAssistantsMapContext(); const methods = useForm<{ text: string }>({ defaultValues: { text: '' }, }); const submitMessage = useCallback( (data?: { text: string }) => { if (!data) { return console.warn('No data provided to submitMessage'); } ask({ text: data.text }); methods.reset(); textAreaRef.current?.setRangeText('', 0, data.text.length, 'end'); }, [ask, methods], ); 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( () => conversation?.endpoint === EModelEndpoint.assistants && (!conversation?.assistant_id || !assistantMap?.[conversation?.assistant_id ?? '']), [conversation?.assistant_id, conversation?.endpoint, assistantMap], ); const disableInputs = useMemo( () => !!(requiresKey || invalidAssistant), [requiresKey, invalidAssistant], ); 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" >
(
{children}
)} /> {endpoint && ( { methods.setValue('text', e.target.value); }, })} autoFocus ref={(e) => { textAreaRef.current = e; }} disabled={disableInputs} onPaste={handlePaste} onKeyUp={handleKeyUp} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} id="prompt-textarea" 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] pr-10 placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 md:pr-12 ', removeFocusOutlines, 'max-h-[65vh] md:max-h-[85vh]', )} /> )} {isSubmitting && showStopButton ? ( ) : ( endpoint && ( ) )}
); }; export default memo(ChatForm);