import { useRecoilState } from 'recoil'; import { useForm } from 'react-hook-form'; import { memo, useCallback, useRef, useMemo } from 'react'; import { supportsFiles, mergeFileConfig, isAssistantsEndpoint, fileConfig as defaultFileConfig, } from 'librechat-data-provider'; import { useChatContext, useAssistantsMapContext } from '~/Providers'; import { useRequiresKey, useTextarea } from '~/hooks'; import { TextareaAutosize } from '~/components/ui'; import { useGetFileConfig } from '~/data-provider'; import { cn, removeFocusOutlines } from '~/utils'; import AttachFile from './Files/AttachFile'; import { mainTextareaId } from '~/common'; 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); const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index)); const [showMentionPopover, setShowMentionPopover] = useRecoilState( store.showMentionPopoverFamily(index), ); const { requiresKey } = useRequiresKey(); const methods = useForm<{ text: string }>({ defaultValues: { text: '' }, }); const { handlePaste, handleKeyDown, handleKeyUp, handleCompositionStart, handleCompositionEnd } = useTextarea({ textAreaRef, submitButtonRef, disabled: !!requiresKey, }); const { ask, files, setFiles, conversation, isSubmitting, filesLoading, setFilesLoading, handleStopGenerating, } = useChatContext(); const assistantMap = useAssistantsMapContext(); const submitMessage = useCallback( (data?: { text: string }) => { if (!data) { return console.warn('No data provided to submitMessage'); } ask({ text: data.text }); methods.reset(); }, [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( () => isAssistantsEndpoint(conversation?.endpoint) && (!conversation?.assistant_id || !assistantMap?.[conversation?.endpoint ?? '']?.[conversation?.assistant_id ?? '']), [conversation?.assistant_id, conversation?.endpoint, assistantMap], ); const disableInputs = useMemo( () => !!(requiresKey || invalidAssistant), [requiresKey, invalidAssistant], ); const { ref, ...registerProps } = methods.register('text', { required: true, onChange: (e) => { methods.setValue('text', e.target.value); }, }); 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" >
{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] 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-[75vh]', )} /> )} {isSubmitting && showStopButton ? ( ) : ( endpoint && ( ) )}
); }; export default memo(ChatForm);