LibreChat/client/src/hooks/Input/useTextarea.ts
Danny Avila 18edd2660b
👥 fix(assistants): Improve Error handling (#2012)
* feat: make assistants endpoint appendable since message state is not managed by LibreChat

* fix(ask): search currentMessages for thread_id if it's not defined

* refactor(abortMiddleware): remove use of `overrideProps` and spread unknown fields instead

* chore: remove console.log in `abortConversation`

* refactor(assistants): improve error handling/cancellation flow
2024-03-07 10:50:01 -05:00

196 lines
5.6 KiB
TypeScript

import debounce from 'lodash/debounce';
import React, { 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<HTMLTextAreaElement>;
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<string>;
submitMessage: () => void;
disabled?: boolean;
}) {
const assistantMap = useAssistantsMapContext();
const { conversation, isSubmitting, latestMessage, setShowBingToneSetting, setFilesLoading } =
useChatContext();
const isComposing = useRef(false);
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const { handleFiles } = useFileHandling();
const getSender = useGetSender();
const localize = useLocalize();
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
const isNotAppendable =
((latestMessage?.unfinished && !isSubmitting) || latestMessage?.error) &&
endpoint !== EModelEndpoint.assistants;
// && (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<HTMLTextAreaElement>) => {
e.preventDefault();
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,
};
}