import debounce from 'lodash/debounce'; import { useEffect, useRef, useCallback } from 'react'; import { EModelEndpoint } from 'librechat-data-provider'; import type { TEndpointOption } from 'librechat-data-provider'; import type { SetterOrUpdater } from 'recoil'; import type { KeyboardEvent } from 'react'; import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext'; import useGetSender from '~/hooks/Conversations/useGetSender'; import useFileHandling from '~/hooks/Files/useFileHandling'; import { useChatContext } from '~/Providers/ChatContext'; import useLocalize from '~/hooks/useLocalize'; type KeyEvent = KeyboardEvent; 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({ setText, submitMessage, disabled = false, }: { setText: SetterOrUpdater; submitMessage: () => void; disabled?: boolean; }) { const assistantMap = useAssistantsMapContext(); const { conversation, isSubmitting, latestMessage, setShowBingToneSetting, setFilesLoading } = useChatContext(); const isComposing = useRef(false); const textAreaRef = useRef(null); const { handleFiles } = useFileHandling(); const getSender = useGetSender(); const localize = useLocalize(); const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {}; const isNotAppendable = (latestMessage?.unfinished && !isSubmitting) || latestMessage?.error; // && (conversationId?.length ?? 0) > 6; // also ensures that we don't show the wrong placeholder const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? '']; const assistantName = (assistant && assistant?.name) || ''; // auto focus to input, when enter a conversation. useEffect(() => { if (!conversationId) { return; } // Prevents Settings from not showing on new conversation, also prevents showing toneStyle change without jailbreak if (conversationId === 'new' || !jailbreak) { setShowBingToneSetting(false); } if (conversationId !== 'search') { textAreaRef.current?.focus(); } // setShowBingToneSetting is a recoil setter, so it doesn't need to be in the dependency array // eslint-disable-next-line react-hooks/exhaustive-deps }, [conversationId, jailbreak]); useEffect(() => { const timeoutId = setTimeout(() => { textAreaRef.current?.focus(); }, 100); return () => clearTimeout(timeoutId); }, [isSubmitting]); useEffect(() => { if (textAreaRef.current?.value) { return; } const getPlaceholderText = () => { if (disabled) { return localize('com_endpoint_config_placeholder'); } if (isNotAppendable) { return localize('com_endpoint_message_not_appendable'); } const sender = conversation?.endpoint === EModelEndpoint.assistants ? getAssistantName({ name: assistantName, localize }) : getSender(conversation as TEndpointOption); return `${localize('com_endpoint_message')} ${sender ? sender : 'ChatGPT'}…`; }; const placeholder = getPlaceholderText(); if (textAreaRef.current?.getAttribute('placeholder') === placeholder) { return; } const setPlaceholder = () => { const placeholder = getPlaceholderText(); if (textAreaRef.current?.getAttribute('placeholder') !== placeholder) { textAreaRef.current?.setAttribute('placeholder', placeholder); } }; const debouncedSetPlaceholder = debounce(setPlaceholder, 80); debouncedSetPlaceholder(); return () => debouncedSetPlaceholder.cancel(); }, [conversation, disabled, latestMessage, isNotAppendable, localize, getSender, assistantName]); const handleKeyDown = (e: KeyEvent) => { if (e.key === 'Enter' && isSubmitting) { return; } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); } if (e.key === 'Enter' && !e.shiftKey && !isComposing?.current) { submitMessage(); } }; const handleKeyUp = (e: KeyEvent) => { const target = e.target as HTMLTextAreaElement; if (e.keyCode === 8 && target.value.trim() === '') { setText(target.value); } if (e.key === 'Enter' && e.shiftKey) { return console.log('Enter + Shift'); } if (isSubmitting) { return; } }; const handleCompositionStart = () => { isComposing.current = true; }; const handleCompositionEnd = () => { isComposing.current = false; }; const handlePaste = useCallback( (e: React.ClipboardEvent) => { const pastedData = e.clipboardData.getData('text/plain'); const textArea = textAreaRef.current; if (!textArea) { return; } const start = textArea.selectionStart; const end = textArea.selectionEnd; const newValue = textArea.value.substring(0, start) + pastedData + textArea.value.substring(end); setText(newValue); if (e.clipboardData && e.clipboardData.files.length > 0) { e.preventDefault(); setFilesLoading(true); handleFiles(e.clipboardData.files); } }, [handleFiles, setFilesLoading, setText], ); return { textAreaRef, handleKeyDown, handleKeyUp, handlePaste, handleCompositionStart, handleCompositionEnd, }; }