mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
✍️ refactor(Textarea): Optimize Text Input & Enhance UX (#2058)
* refactor(useDebouncedInput): make object as input arg and accept setter
* refactor(ChatForm/Textarea): consolidate textarea/form logic to one component, use react-hook-form, programmatically click send button instead of passing submitMessage, forwardRef and memoize SendButton
* refactor(Textarea): use Controller field value to avoid manual update of ref
* chore: remove forms provider
* chore: memoize AttachFile
* refactor(ChatForm/SendButton): only re-render SendButton when there is text input
* chore: make iconURL bigger
* chore: optimize Root/Nav
* refactor(SendButton): memoize disabled prop based on text
* chore: memoize Nav and ChatForm
* chore: remove textarea ref text on submission
* feat(EditMessage): Make Esc exit the edit mode and dismiss changes when editing a message
* style(MenuItem): Display the ☑️ icon only on the selected model
This commit is contained in:
parent
f489aee518
commit
f307488dd4
16 changed files with 244 additions and 225 deletions
|
|
@ -1,17 +1,30 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
supportsFiles,
|
||||
mergeFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { useRequiresKey, useTextarea } from '~/hooks';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useRequiresKey } from '~/hooks';
|
||||
import AttachFile from './Files/AttachFile';
|
||||
import StopButton from './StopButton';
|
||||
import SendButton from './SendButton';
|
||||
import FileRow from './Files/FileRow';
|
||||
import Textarea from './Textarea';
|
||||
import store from '~/store';
|
||||
|
||||
export default function ChatForm({ index = 0 }) {
|
||||
const [text, setText] = useRecoilState(store.textByIndex(index));
|
||||
const ChatForm = ({ index = 0 }) => {
|
||||
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||
const { requiresKey } = useRequiresKey();
|
||||
|
||||
const { handlePaste, handleKeyUp, handleKeyDown, handleCompositionStart, handleCompositionEnd } =
|
||||
useTextarea({ textAreaRef, submitButtonRef, disabled: !!requiresKey });
|
||||
|
||||
const {
|
||||
ask,
|
||||
|
|
@ -24,21 +37,34 @@ export default function ChatForm({ index = 0 }) {
|
|||
setFilesLoading,
|
||||
} = useChatContext();
|
||||
|
||||
const submitMessage = () => {
|
||||
ask({ text });
|
||||
setText('');
|
||||
};
|
||||
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 { requiresKey } = useRequiresKey();
|
||||
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 ?? ''];
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitMessage();
|
||||
}}
|
||||
onSubmit={methods.handleSubmit((data) => 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"
|
||||
>
|
||||
<div className="relative flex h-full flex-1 items-stretch md:flex-col">
|
||||
|
|
@ -55,14 +81,36 @@ export default function ChatForm({ index = 0 }) {
|
|||
)}
|
||||
/>
|
||||
{endpoint && (
|
||||
<Textarea
|
||||
value={text}
|
||||
disabled={requiresKey}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setText(e.target.value)}
|
||||
setText={setText}
|
||||
submitMessage={submitMessage}
|
||||
endpoint={_endpoint}
|
||||
endpointType={endpointType}
|
||||
<TextareaAutosize
|
||||
{...methods.register('text', {
|
||||
required: true,
|
||||
onChange: (e) => {
|
||||
methods.setValue('text', e.target.value);
|
||||
},
|
||||
})}
|
||||
autoFocus
|
||||
ref={(e) => {
|
||||
textAreaRef.current = e;
|
||||
}}
|
||||
disabled={!!requiresKey}
|
||||
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]',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<AttachFile
|
||||
|
|
@ -74,7 +122,11 @@ export default function ChatForm({ index = 0 }) {
|
|||
<StopButton stop={handleStopGenerating} setShowStopButton={setShowStopButton} />
|
||||
) : (
|
||||
endpoint && (
|
||||
<SendButton text={text} disabled={filesLoading || isSubmitting || requiresKey} />
|
||||
<SendButton
|
||||
ref={submitButtonRef}
|
||||
control={methods.control}
|
||||
disabled={!!(filesLoading || isSubmitting || requiresKey)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -82,4 +134,6 @@ export default function ChatForm({ index = 0 }) {
|
|||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(ChatForm);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue