mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
⬆️ 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:
parent
b2b469bd3d
commit
24467dd626
7 changed files with 121 additions and 38 deletions
|
@ -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,8 +34,8 @@ const ChatForm = ({ index = 0 }) => {
|
|||
);
|
||||
const { requiresKey } = useRequiresKey();
|
||||
|
||||
const { handlePaste, handleKeyDown, handleKeyUp, handleCompositionStart, handleCompositionEnd } =
|
||||
useTextarea({
|
||||
const handleKeyUp = useHandleKeyUp({ index, textAreaRef });
|
||||
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
|
||||
textAreaRef,
|
||||
submitButtonRef,
|
||||
disabled: !!requiresKey,
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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';
|
||||
|
|
93
client/src/hooks/Input/useHandleKeyUp.ts
Normal file
93
client/src/hooks/Input/useHandleKeyUp.ts
Normal 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;
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue