mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

* 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
196 lines
5.6 KiB
TypeScript
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,
|
|
};
|
|
}
|