mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01: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,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { useChatContext, useAssistantsMapContext, useChatFormContext } from '~/Providers';
|
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 { useAutoSave } from '~/hooks/Input/useAutoSave';
|
||||||
import { TextareaAutosize } from '~/components/ui';
|
import { TextareaAutosize } from '~/components/ui';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
|
|
@ -34,12 +34,12 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
);
|
);
|
||||||
const { requiresKey } = useRequiresKey();
|
const { requiresKey } = useRequiresKey();
|
||||||
|
|
||||||
const { handlePaste, handleKeyDown, handleKeyUp, handleCompositionStart, handleCompositionEnd } =
|
const handleKeyUp = useHandleKeyUp({ index, textAreaRef });
|
||||||
useTextarea({
|
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
submitButtonRef,
|
submitButtonRef,
|
||||||
disabled: !!requiresKey,
|
disabled: !!requiresKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ const AttachFile = ({
|
||||||
<button
|
<button
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
type="button"
|
type="button"
|
||||||
|
tabIndex={1}
|
||||||
className="btn relative p-0 text-black dark:text-white"
|
className="btn relative p-0 text-black dark:text-white"
|
||||||
aria-label="Attach files"
|
aria-label="Attach files"
|
||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { SetterOrUpdater } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
import type { MentionOption } from '~/common';
|
import type { MentionOption } from '~/common';
|
||||||
|
import useSelectMention from '~/hooks/Input/useSelectMention';
|
||||||
import { useAssistantsMapContext } from '~/Providers';
|
import { useAssistantsMapContext } from '~/Providers';
|
||||||
import useMentions from '~/hooks/Input/useMentions';
|
import useMentions from '~/hooks/Input/useMentions';
|
||||||
import { useLocalize, useCombobox } from '~/hooks';
|
import { useLocalize, useCombobox } from '~/hooks';
|
||||||
|
|
@ -17,8 +18,13 @@ export default function Mention({
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const assistantMap = useAssistantsMapContext();
|
const assistantMap = useAssistantsMapContext();
|
||||||
const { options, modelsConfig, assistantListMap, onSelectMention } = useMentions({
|
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
|
||||||
|
useMentions({ assistantMap });
|
||||||
|
const { onSelectMention } = useSelectMention({
|
||||||
|
presets,
|
||||||
|
modelSpecs,
|
||||||
assistantMap,
|
assistantMap,
|
||||||
|
endpointsConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
|
@ -80,6 +86,14 @@ export default function Mention({
|
||||||
}
|
}
|
||||||
}, [open, options]);
|
}, [open, options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentActiveItem = document.getElementById(`mention-item-${activeIndex}`);
|
const currentActiveItem = document.getElementById(`mention-item-${activeIndex}`);
|
||||||
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
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 useDebounce } from './useDebounce';
|
||||||
export { default as useTextarea } from './useTextarea';
|
export { default as useTextarea } from './useTextarea';
|
||||||
export { default as useCombobox } from './useCombobox';
|
export { default as useCombobox } from './useCombobox';
|
||||||
|
export { default as useHandleKeyUp } from './useHandleKeyUp';
|
||||||
export { default as useRequiresKey } from './useRequiresKey';
|
export { default as useRequiresKey } from './useRequiresKey';
|
||||||
export { default as useMultipleKeys } from './useMultipleKeys';
|
export { default as useMultipleKeys } from './useMultipleKeys';
|
||||||
export { default as useSpeechToText } from './useSpeechToText';
|
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 { mapEndpoints, getPresetTitle } from '~/utils';
|
||||||
import { EndpointIcon } from '~/components/Endpoints';
|
import { EndpointIcon } from '~/components/Endpoints';
|
||||||
import { useGetPresetsQuery } from '~/data-provider';
|
import { useGetPresetsQuery } from '~/data-provider';
|
||||||
import useSelectMention from './useSelectMention';
|
|
||||||
|
|
||||||
const defaultInterface = getConfigDefaults().interface;
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
|
||||||
|
|
@ -85,13 +84,6 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
||||||
[startupConfig],
|
[startupConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { onSelectMention } = useSelectMention({
|
|
||||||
modelSpecs,
|
|
||||||
endpointsConfig,
|
|
||||||
presets,
|
|
||||||
assistantMap,
|
|
||||||
});
|
|
||||||
|
|
||||||
const options: MentionOption[] = useMemo(() => {
|
const options: MentionOption[] = useMemo(() => {
|
||||||
const mentions = [
|
const mentions = [
|
||||||
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
|
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
|
||||||
|
|
@ -156,8 +148,10 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
|
presets,
|
||||||
|
modelSpecs,
|
||||||
modelsConfig,
|
modelsConfig,
|
||||||
onSelectMention,
|
endpointsConfig,
|
||||||
assistantListMap,
|
assistantListMap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
|
|
||||||
import type { TEndpointOption } from 'librechat-data-provider';
|
import type { TEndpointOption } from 'librechat-data-provider';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
|
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
|
||||||
|
|
@ -40,8 +40,6 @@ export default function useTextarea({
|
||||||
setFilesLoading,
|
setFilesLoading,
|
||||||
setShowBingToneSetting,
|
setShowBingToneSetting,
|
||||||
} = useChatContext();
|
} = useChatContext();
|
||||||
|
|
||||||
const setShowMentionPopover = useSetRecoilState(store.showMentionPopoverFamily(index));
|
|
||||||
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
|
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
|
||||||
|
|
||||||
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
|
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
|
||||||
|
|
@ -148,23 +146,6 @@ export default function useTextarea({
|
||||||
assistantMap,
|
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(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyEvent) => {
|
(e: KeyEvent) => {
|
||||||
if (e.key === 'Enter' && isSubmitting) {
|
if (e.key === 'Enter' && isSubmitting) {
|
||||||
|
|
@ -244,7 +225,6 @@ export default function useTextarea({
|
||||||
return {
|
return {
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleKeyUp,
|
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
handleCompositionStart,
|
handleCompositionStart,
|
||||||
handleCompositionEnd,
|
handleCompositionEnd,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue