⬆️ refactor: Improve Text Commands (#3152)

* refactor(useMentions): separate usage of `useSelectMention`

* refactor: separate handleKeyUp logic from useTextarea

* fix(Mention): cleanup blur timer

* refactor(handleKeyUp): improve command handling, prevent unintended re-trigger

* chore: remove console log

* chore: temporarily comment plus command
This commit is contained in:
Danny Avila 2024-06-21 12:34:28 -04:00 committed by GitHub
parent b2b469bd3d
commit 24467dd626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 121 additions and 38 deletions

View file

@ -7,7 +7,7 @@ import {
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import { useChatContext, useAssistantsMapContext, useChatFormContext } from '~/Providers';
import { useRequiresKey, useTextarea, useSubmitMessage } from '~/hooks';
import { useRequiresKey, useTextarea, useSubmitMessage, useHandleKeyUp } from '~/hooks';
import { useAutoSave } from '~/hooks/Input/useAutoSave';
import { TextareaAutosize } from '~/components/ui';
import { useGetFileConfig } from '~/data-provider';
@ -34,12 +34,12 @@ const ChatForm = ({ index = 0 }) => {
);
const { requiresKey } = useRequiresKey();
const { handlePaste, handleKeyDown, handleKeyUp, handleCompositionStart, handleCompositionEnd } =
useTextarea({
textAreaRef,
submitButtonRef,
disabled: !!requiresKey,
});
const handleKeyUp = useHandleKeyUp({ index, textAreaRef });
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
textAreaRef,
submitButtonRef,
disabled: !!requiresKey,
});
const {
files,

View file

@ -35,6 +35,7 @@ const AttachFile = ({
<button
disabled={!!disabled}
type="button"
tabIndex={1}
className="btn relative p-0 text-black dark:text-white"
aria-label="Attach files"
style={{ padding: 0 }}

View file

@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import type { SetterOrUpdater } from 'recoil';
import type { MentionOption } from '~/common';
import useSelectMention from '~/hooks/Input/useSelectMention';
import { useAssistantsMapContext } from '~/Providers';
import useMentions from '~/hooks/Input/useMentions';
import { useLocalize, useCombobox } from '~/hooks';
@ -17,8 +18,13 @@ export default function Mention({
}) {
const localize = useLocalize();
const assistantMap = useAssistantsMapContext();
const { options, modelsConfig, assistantListMap, onSelectMention } = useMentions({
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
useMentions({ assistantMap });
const { onSelectMention } = useSelectMention({
presets,
modelSpecs,
assistantMap,
endpointsConfig,
});
const [activeIndex, setActiveIndex] = useState(0);
@ -80,6 +86,14 @@ export default function Mention({
}
}, [open, options]);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
useEffect(() => {
const currentActiveItem = document.getElementById(`mention-item-${activeIndex}`);
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });

View file

@ -2,6 +2,7 @@ export { default as useUserKey } from './useUserKey';
export { default as useDebounce } from './useDebounce';
export { default as useTextarea } from './useTextarea';
export { default as useCombobox } from './useCombobox';
export { default as useHandleKeyUp } from './useHandleKeyUp';
export { default as useRequiresKey } from './useRequiresKey';
export { default as useMultipleKeys } from './useMultipleKeys';
export { default as useSpeechToText } from './useSpeechToText';

View file

@ -0,0 +1,93 @@
import { useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import store from '~/store';
/**
* Utility function to determine if a command should trigger.
*/
const shouldTriggerCommand = (
textAreaRef: React.RefObject<HTMLTextAreaElement>,
commandChar: string,
) => {
const text = textAreaRef.current?.value;
if (!(text && text[text.length - 1] === commandChar)) {
return false;
}
const startPos = textAreaRef.current?.selectionStart;
if (!startPos) {
return false;
}
const isAtStart = startPos === 1;
const isPrecededBySpace = textAreaRef.current?.value.charAt(startPos - 2) === ' ';
const shouldTrigger = isAtStart || isPrecededBySpace;
if (shouldTrigger) {
// Blurring helps prevent the command from firing twice.
textAreaRef.current.blur();
}
return shouldTrigger;
};
/**
* Custom hook for handling key up events with command triggers.
*/
const useHandleKeyUp = ({
index,
textAreaRef,
}: {
index: number;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
}) => {
const setShowMentionPopover = useSetRecoilState(store.showMentionPopoverFamily(index));
const handleAtCommand = useCallback(() => {
if (shouldTriggerCommand(textAreaRef, '@')) {
setShowMentionPopover(true);
}
}, [textAreaRef, setShowMentionPopover]);
// const handlePlusCommand = useCallback(() => {
// if (shouldTriggerCommand(textAreaRef, '+')) {
// console.log('+ command triggered');
// }
// }, [textAreaRef]);
const commandHandlers = useMemo(
() => ({
'@': handleAtCommand,
// '+': handlePlusCommand,
}),
[handleAtCommand],
// [handleAtCommand, handlePlusCommand],
);
/**
* Main key up handler.
*/
const handleKeyUp = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
const text = textAreaRef.current?.value;
if (!text) {
return;
}
if (event.key === 'Escape') {
return;
}
const lastChar = text[text.length - 1];
const handler = commandHandlers[lastChar];
if (handler) {
handler();
}
},
[textAreaRef, commandHandlers],
);
return handleKeyUp;
};
export default useHandleKeyUp;

View file

@ -11,7 +11,6 @@ import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
import { mapEndpoints, getPresetTitle } from '~/utils';
import { EndpointIcon } from '~/components/Endpoints';
import { useGetPresetsQuery } from '~/data-provider';
import useSelectMention from './useSelectMention';
const defaultInterface = getConfigDefaults().interface;
@ -85,13 +84,6 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
[startupConfig],
);
const { onSelectMention } = useSelectMention({
modelSpecs,
endpointsConfig,
presets,
assistantMap,
});
const options: MentionOption[] = useMemo(() => {
const mentions = [
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
@ -156,8 +148,10 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
return {
options,
presets,
modelSpecs,
modelsConfig,
onSelectMention,
endpointsConfig,
assistantListMap,
};
}

View file

@ -1,7 +1,7 @@
import debounce from 'lodash/debounce';
import { useEffect, useRef, useCallback } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
import type { TEndpointOption } from 'librechat-data-provider';
import type { KeyboardEvent } from 'react';
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
@ -40,8 +40,6 @@ export default function useTextarea({
setFilesLoading,
setShowBingToneSetting,
} = useChatContext();
const setShowMentionPopover = useSetRecoilState(store.showMentionPopoverFamily(index));
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
@ -148,23 +146,6 @@ export default function useTextarea({
assistantMap,
]);
const handleKeyUp = useCallback(() => {
const text = textAreaRef.current?.value;
if (!(text && text[text.length - 1] === '@')) {
return;
}
const startPos = textAreaRef.current?.selectionStart;
if (!startPos) {
return;
}
const isAtStart = startPos === 1;
const isPrecededBySpace = textAreaRef.current?.value.charAt(startPos - 2) === ' ';
setShowMentionPopover(isAtStart || isPrecededBySpace);
}, [textAreaRef, setShowMentionPopover]);
const handleKeyDown = useCallback(
(e: KeyEvent) => {
if (e.key === 'Enter' && isSubmitting) {
@ -244,7 +225,6 @@ export default function useTextarea({
return {
textAreaRef,
handlePaste,
handleKeyUp,
handleKeyDown,
handleCompositionStart,
handleCompositionEnd,