chat / client /src /hooks /Input /useTextarea.ts
Zhitomir's picture
chat init
aa2e6af
Raw
History Blame Contribute Delete
6.96 kB
import debounce from 'lodash/debounce';
import { useEffect, useRef, useCallback } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { TEndpointOption } from 'librechat-data-provider';
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';
import { useChatContext } from '~/Providers/ChatContext';
import useLocalize from '~/hooks/useLocalize';
import { globalAudioId } from '~/common';
import store from '~/store';
type KeyEvent = KeyboardEvent<HTMLTextAreaElement>;
export default function useTextarea({
textAreaRef,
submitButtonRef,
disabled = false,
}: {
textAreaRef: React.RefObject<HTMLTextAreaElement>;
submitButtonRef: React.RefObject<HTMLButtonElement>;
disabled?: boolean;
}) {
const localize = useLocalize();
const getSender = useGetSender();
const isComposing = useRef(false);
const { handleFiles } = useFileHandling();
const assistantMap = useAssistantsMapContext();
const enterToSend = useRecoilValue(store.enterToSend);
const {
index,
conversation,
isSubmitting,
filesLoading,
latestMessage,
setFilesLoading,
setShowBingToneSetting,
} = useChatContext();
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
const isNotAppendable =
((latestMessage?.unfinished && !isSubmitting) || latestMessage?.error) &&
!isAssistantsEndpoint(endpoint);
// && (conversationId?.length ?? 0) > 6; // also ensures that we don't show the wrong placeholder
const assistant =
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
useEffect(() => {
if (activePrompt && textAreaRef.current) {
insertTextAtCursor(textAreaRef.current, activePrompt);
forceResize(textAreaRef.current);
setActivePrompt(undefined);
}
}, [activePrompt, setActivePrompt, textAreaRef]);
// 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, textAreaRef]);
useEffect(() => {
if (textAreaRef.current?.value) {
return;
}
const getPlaceholderText = () => {
if (disabled) {
return localize('com_endpoint_config_placeholder');
}
const currentEndpoint = conversation?.endpoint ?? '';
const currentAssistantId = conversation?.assistant_id ?? '';
if (
isAssistantsEndpoint(currentEndpoint) &&
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId ?? ''])
) {
return localize('com_endpoint_assistant_placeholder');
}
if (isNotAppendable) {
return localize('com_endpoint_message_not_appendable');
}
const sender = isAssistantsEndpoint(currentEndpoint)
? 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);
forceResize(textAreaRef.current);
}
};
const debouncedSetPlaceholder = debounce(setPlaceholder, 80);
debouncedSetPlaceholder();
return () => debouncedSetPlaceholder.cancel();
}, [
conversation,
disabled,
latestMessage,
isNotAppendable,
localize,
getSender,
assistantName,
textAreaRef,
assistantMap,
]);
const handleKeyDown = useCallback(
(e: KeyEvent) => {
if (e.key === 'Enter' && isSubmitting) {
return;
}
const isNonShiftEnter = e.key === 'Enter' && !e.shiftKey;
const isCtrlEnter = e.key === 'Enter' && e.ctrlKey;
if (isNonShiftEnter && filesLoading) {
e.preventDefault();
}
if (isNonShiftEnter) {
e.preventDefault();
}
if (
e.key === 'Enter' &&
!enterToSend &&
!isCtrlEnter &&
textAreaRef.current &&
!isComposing?.current
) {
e.preventDefault();
insertTextAtCursor(textAreaRef.current, '\n');
forceResize(textAreaRef.current);
return;
}
if ((isNonShiftEnter || isCtrlEnter) && !isComposing?.current) {
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement;
if (globalAudio) {
console.log('Unmuting global audio');
globalAudio.muted = false;
}
submitButtonRef.current?.click();
}
},
[isSubmitting, filesLoading, enterToSend, textAreaRef, submitButtonRef],
);
const handleCompositionStart = () => {
isComposing.current = true;
};
const handleCompositionEnd = () => {
isComposing.current = false;
};
const handlePaste = useCallback(
(e: React.ClipboardEvent<HTMLTextAreaElement>) => {
const textArea = textAreaRef.current;
if (!textArea) {
return;
}
if (!e.clipboardData) {
return;
}
if (e.clipboardData.files.length > 0) {
setFilesLoading(true);
const timestampedFiles: File[] = [];
for (const file of e.clipboardData.files) {
const newFile = new File([file], `clipboard_${+new Date()}_${file.name}`, {
type: file.type,
});
timestampedFiles.push(newFile);
}
handleFiles(timestampedFiles);
}
},
[handleFiles, setFilesLoading, textAreaRef],
);
return {
textAreaRef,
handlePaste,
handleKeyDown,
handleCompositionStart,
handleCompositionEnd,
};
}