From aff219c655629edcd700494863cfc787085e52f8 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 1 Apr 2024 13:40:21 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=8B=20fix:=20Ensure=20Textarea=20Resiz?= =?UTF-8?q?es=20in=20Clipboard=20Edge=20Case=20(#2268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: ts-ignore fake conversation data used for testing * chore(useTextarea): import helper functions to declutter hook * fix(Textarea): reset textarea value explicitly by resetting `textAreaRef.current.value` --- client/src/components/Chat/Input/ChatForm.tsx | 8 +-- client/src/hooks/Input/useTextarea.ts | 53 +------------------ client/src/utils/convos.fakeData.ts | 2 + client/src/utils/endpoints.ts | 14 +++++ client/src/utils/index.ts | 1 + client/src/utils/textarea.ts | 40 ++++++++++++++ 6 files changed, 63 insertions(+), 55 deletions(-) create mode 100644 client/src/utils/textarea.ts diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 7ed08d4eb..a5623d4e8 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -3,9 +3,9 @@ import { useForm } from 'react-hook-form'; import { memo, useCallback, useRef, useMemo } from 'react'; import { supportsFiles, + EModelEndpoint, mergeFileConfig, fileConfig as defaultFileConfig, - EModelEndpoint, } from 'librechat-data-provider'; import { useChatContext, useAssistantsMapContext } from '~/Providers'; import { useRequiresKey, useTextarea } from '~/hooks'; @@ -43,9 +43,9 @@ const ChatForm = ({ index = 0 }) => { setFiles, conversation, isSubmitting, - handleStopGenerating, filesLoading, setFilesLoading, + handleStopGenerating, } = useChatContext(); const assistantMap = useAssistantsMapContext(); @@ -57,7 +57,9 @@ const ChatForm = ({ index = 0 }) => { } ask({ text: data.text }); methods.reset(); - textAreaRef.current?.setRangeText('', 0, data.text.length, 'end'); + if (textAreaRef.current) { + textAreaRef.current.value = ''; + } }, [ask, methods], ); diff --git a/client/src/hooks/Input/useTextarea.ts b/client/src/hooks/Input/useTextarea.ts index cc17267a0..d0c560cc6 100644 --- a/client/src/hooks/Input/useTextarea.ts +++ b/client/src/hooks/Input/useTextarea.ts @@ -4,6 +4,7 @@ import { EModelEndpoint } from 'librechat-data-provider'; import type { TEndpointOption } from 'librechat-data-provider'; import type { UseFormSetValue } from 'react-hook-form'; import type { KeyboardEvent } from 'react'; +import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils'; import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext'; import useGetSender from '~/hooks/Conversations/useGetSender'; import useFileHandling from '~/hooks/Files/useFileHandling'; @@ -12,58 +13,6 @@ import useLocalize from '~/hooks/useLocalize'; type KeyEvent = KeyboardEvent; -function insertTextAtCursor(element: HTMLTextAreaElement, textToInsert: string) { - element.focus(); - - // Use the browser's built-in undoable actions if possible - if (window.getSelection() && document.queryCommandSupported('insertText')) { - document.execCommand('insertText', false, textToInsert); - } else { - console.warn('insertTextAtCursor: document.execCommand is not supported'); - const startPos = element.selectionStart; - const endPos = element.selectionEnd; - const beforeText = element.value.substring(0, startPos); - const afterText = element.value.substring(endPos); - element.value = beforeText + textToInsert + afterText; - element.selectionStart = element.selectionEnd = startPos + textToInsert.length; - const event = new Event('input', { bubbles: true }); - element.dispatchEvent(event); - } -} - -/** - * Necessary resize helper for edge cases where paste doesn't update the container height. - * - 1) Resetting the height to 'auto' forces the component to recalculate height based on its current content - - 2) Forcing a reflow. Accessing offsetHeight will cause a reflow of the page, - ensuring that the reset height takes effect before resetting back to the scrollHeight. - This step is necessary because changes to the DOM do not instantly cause reflows. - - 3) Reseting back to scrollHeight reads and applies the ideal height for the current content dynamically - */ -const forceResize = (textAreaRef: React.RefObject) => { - if (textAreaRef.current) { - textAreaRef.current.style.height = 'auto'; - textAreaRef.current.offsetHeight; - textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; - } -}; - -const getAssistantName = ({ - name, - localize, -}: { - name?: string; - localize: (phraseKey: string, ...values: string[]) => string; -}) => { - if (name && name.length > 0) { - return name; - } else { - return localize('com_ui_assistant'); - } -}; - export default function useTextarea({ textAreaRef, submitButtonRef, diff --git a/client/src/utils/convos.fakeData.ts b/client/src/utils/convos.fakeData.ts index f5a3398fe..48c078c3b 100644 --- a/client/src/utils/convos.fakeData.ts +++ b/client/src/utils/convos.fakeData.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck import { EModelEndpoint, ImageDetail } from 'librechat-data-provider'; import type { ConversationData } from 'librechat-data-provider'; diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index 86e157b5a..b4ca282ad 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -1,6 +1,20 @@ import { defaultEndpoints } from 'librechat-data-provider'; import type { EModelEndpoint, TEndpointsConfig, TConfig } from 'librechat-data-provider'; +export const getAssistantName = ({ + name, + localize, +}: { + name?: string; + localize: (phraseKey: string, ...values: string[]) => string; +}) => { + if (name && name.length > 0) { + return name; + } else { + return localize('com_ui_assistant'); + } +}; + export const getEndpointsFilter = (endpointsConfig: TEndpointsConfig) => { const filter: Record = {}; if (!endpointsConfig) { diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts index 59b8a27ad..c7132688b 100644 --- a/client/src/utils/index.ts +++ b/client/src/utils/index.ts @@ -4,6 +4,7 @@ export * from './files'; export * from './latex'; export * from './convos'; export * from './presets'; +export * from './textarea'; export * from './languages'; export * from './endpoints'; export { default as cn } from './cn'; diff --git a/client/src/utils/textarea.ts b/client/src/utils/textarea.ts new file mode 100644 index 000000000..ac3aad9c4 --- /dev/null +++ b/client/src/utils/textarea.ts @@ -0,0 +1,40 @@ +/** + * Insert text at the cursor position in a textarea. + */ +export function insertTextAtCursor(element: HTMLTextAreaElement, textToInsert: string) { + element.focus(); + + // Use the browser's built-in undoable actions if possible + if (window.getSelection() && document.queryCommandSupported('insertText')) { + document.execCommand('insertText', false, textToInsert); + } else { + console.warn('insertTextAtCursor: document.execCommand is not supported'); + const startPos = element.selectionStart; + const endPos = element.selectionEnd; + const beforeText = element.value.substring(0, startPos); + const afterText = element.value.substring(endPos); + element.value = beforeText + textToInsert + afterText; + element.selectionStart = element.selectionEnd = startPos + textToInsert.length; + const event = new Event('input', { bubbles: true }); + element.dispatchEvent(event); + } +} + +/** + * Necessary resize helper for edge cases where paste doesn't update the container height. + * + 1) Resetting the height to 'auto' forces the component to recalculate height based on its current content + + 2) Forcing a reflow. Accessing offsetHeight will cause a reflow of the page, + ensuring that the reset height takes effect before resetting back to the scrollHeight. + This step is necessary because changes to the DOM do not instantly cause reflows. + + 3) Reseting back to scrollHeight reads and applies the ideal height for the current content dynamically + */ +export const forceResize = (textAreaRef: React.RefObject) => { + if (textAreaRef.current) { + textAreaRef.current.style.height = 'auto'; + textAreaRef.current.offsetHeight; + textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; + } +};