mirror of https://github.com/buster-so/buster.git
dictation so hot right now
This commit is contained in:
parent
d5db629b22
commit
d46383103d
|
@ -1,12 +1,15 @@
|
||||||
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
|
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
|
||||||
import type { GetSuggestedPromptsResponse } from '@buster/server-shared/user';
|
import type { GetSuggestedPromptsResponse } from '@buster/server-shared/user';
|
||||||
import sampleSize from 'lodash/sampleSize';
|
import sampleSize from 'lodash/sampleSize';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useRef, useState } from 'react';
|
||||||
import type { MentionSuggestionExtension } from '@/components/ui/inputs/MentionInput';
|
import type { MentionSuggestionExtension } from '@/components/ui/inputs/MentionInput';
|
||||||
import type { MentionInputSuggestionsProps } from '@/components/ui/inputs/MentionInputSuggestions';
|
import type {
|
||||||
|
MentionInputSuggestionsProps,
|
||||||
|
MentionInputSuggestionsRef,
|
||||||
|
} from '@/components/ui/inputs/MentionInputSuggestions';
|
||||||
import { MentionInputSuggestions } from '@/components/ui/inputs/MentionInputSuggestions';
|
import { MentionInputSuggestions } from '@/components/ui/inputs/MentionInputSuggestions';
|
||||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||||
import { BusterChatInputButtons } from './BusterChatInputButtons';
|
import { BusterChatInputButtons, type BusterChatInputMode } from './BusterChatInputButtons';
|
||||||
|
|
||||||
export type BusterChatInput = {
|
export type BusterChatInput = {
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
|
@ -20,9 +23,12 @@ export type BusterChatInput = {
|
||||||
|
|
||||||
export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
||||||
({ defaultValue, onSubmit, onStop, submitting, disabled, shortcuts, suggestedPrompts }) => {
|
({ defaultValue, onSubmit, onStop, submitting, disabled, shortcuts, suggestedPrompts }) => {
|
||||||
|
const mentionInputSuggestionsRef = useRef<MentionInputSuggestionsRef>(null);
|
||||||
const uniqueSuggestions = useUniqueSuggestions(suggestedPrompts);
|
const uniqueSuggestions = useUniqueSuggestions(suggestedPrompts);
|
||||||
const shortcutsSuggestions = useShortcuts(shortcuts);
|
const shortcutsSuggestions = useShortcuts(shortcuts);
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<BusterChatInputMode>('auto');
|
||||||
|
|
||||||
const suggestionItems: MentionInputSuggestionsProps['suggestionItems'] = useMemo(() => {
|
const suggestionItems: MentionInputSuggestionsProps['suggestionItems'] = useMemo(() => {
|
||||||
const items: MentionInputSuggestionsProps['suggestionItems'] = [...uniqueSuggestions];
|
const items: MentionInputSuggestionsProps['suggestionItems'] = [...uniqueSuggestions];
|
||||||
|
|
||||||
|
@ -43,6 +49,10 @@ export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
||||||
return [];
|
return [];
|
||||||
}, [shortcuts]);
|
}, [shortcuts]);
|
||||||
|
|
||||||
|
const onDictate = useMemoizedFn((transcript: string) => {
|
||||||
|
mentionInputSuggestionsRef.current?.onChangeValue(transcript);
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmitPreflight = (value: string) => {
|
const onSubmitPreflight = (value: string) => {
|
||||||
if (submitting) {
|
if (submitting) {
|
||||||
console.warn('Input is submitting');
|
console.warn('Input is submitting');
|
||||||
|
@ -65,6 +75,13 @@ export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSubmitButton = useMemoizedFn(() => {
|
||||||
|
const value = mentionInputSuggestionsRef.current?.getValue();
|
||||||
|
if (value) {
|
||||||
|
onSubmitPreflight(value.transformedValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MentionInputSuggestions
|
<MentionInputSuggestions
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
|
@ -72,15 +89,16 @@ export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
||||||
mentions={mentions}
|
mentions={mentions}
|
||||||
suggestionItems={suggestionItems}
|
suggestionItems={suggestionItems}
|
||||||
placeholder="Ask a question or type ‘/’ for shortcuts..."
|
placeholder="Ask a question or type ‘/’ for shortcuts..."
|
||||||
|
ref={mentionInputSuggestionsRef}
|
||||||
>
|
>
|
||||||
<BusterChatInputButtons
|
<BusterChatInputButtons
|
||||||
onSubmit={() => {}}
|
onSubmit={onSubmitButton}
|
||||||
onStop={() => {}}
|
onStop={onStop}
|
||||||
submitting={submitting}
|
submitting={submitting}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
mode="auto"
|
mode={mode}
|
||||||
onModeChange={() => {}}
|
onModeChange={setMode}
|
||||||
onDictate={() => {}}
|
onDictate={onDictate}
|
||||||
/>
|
/>
|
||||||
</MentionInputSuggestions>
|
</MentionInputSuggestions>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
|
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
|
||||||
import { Button } from '@/components/ui/buttons';
|
import { Button } from '@/components/ui/buttons';
|
||||||
import { ArrowUp, Magnifier, Sparkle2 } from '@/components/ui/icons';
|
import { ArrowUp, Magnifier, Sparkle2 } from '@/components/ui/icons';
|
||||||
|
@ -7,6 +6,7 @@ import Atom from '@/components/ui/icons/NucleoIconOutlined/atom';
|
||||||
import Microphone from '@/components/ui/icons/NucleoIconOutlined/microphone';
|
import Microphone from '@/components/ui/icons/NucleoIconOutlined/microphone';
|
||||||
import { Popover } from '@/components/ui/popover';
|
import { Popover } from '@/components/ui/popover';
|
||||||
import { AppSegmented, type AppSegmentedProps } from '@/components/ui/segmented';
|
import { AppSegmented, type AppSegmentedProps } from '@/components/ui/segmented';
|
||||||
|
import { AppTooltip } from '@/components/ui/tooltip';
|
||||||
import { Text } from '@/components/ui/typography';
|
import { Text } from '@/components/ui/typography';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
@ -20,70 +20,84 @@ type BusterChatInputButtons = {
|
||||||
mode: BusterChatInputMode;
|
mode: BusterChatInputMode;
|
||||||
onModeChange: (mode: BusterChatInputMode) => void;
|
onModeChange: (mode: BusterChatInputMode) => void;
|
||||||
onDictate: (transcript: string) => void;
|
onDictate: (transcript: string) => void;
|
||||||
|
onDictateListeningChange?: (listening: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BusterChatInputButtons = ({
|
export const BusterChatInputButtons = React.memo(
|
||||||
onSubmit,
|
({
|
||||||
onStop,
|
onSubmit,
|
||||||
submitting,
|
onStop,
|
||||||
disabled,
|
submitting,
|
||||||
mode,
|
disabled,
|
||||||
onModeChange,
|
mode,
|
||||||
onDictate,
|
onModeChange,
|
||||||
}: BusterChatInputButtons) => {
|
onDictate,
|
||||||
const { transcript, listening, resetTranscript, browserSupportsSpeechRecognition } =
|
onDictateListeningChange,
|
||||||
useSpeechRecognition();
|
}: BusterChatInputButtons) => {
|
||||||
|
const { transcript, listening, resetTranscript, browserSupportsSpeechRecognition } =
|
||||||
|
useSpeechRecognition();
|
||||||
|
|
||||||
const startListening = () => {
|
const startListening = () => {
|
||||||
SpeechRecognition.startListening({ continuous: true });
|
SpeechRecognition.startListening({ continuous: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopListening = () => {
|
const stopListening = () => {
|
||||||
SpeechRecognition.stopListening();
|
SpeechRecognition.stopListening();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (listening && transcript) {
|
if (listening && transcript) {
|
||||||
onDictate(transcript);
|
onDictate(transcript);
|
||||||
}
|
}
|
||||||
}, [listening, transcript, onDictate]);
|
}, [listening, transcript, onDictate]);
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="flex justify-between items-center gap-2">
|
onDictateListeningChange?.(listening);
|
||||||
<AppSegmented value={mode} options={modesOptions} onChange={(v) => onModeChange(v.value)} />
|
}, [listening, onDictateListeningChange]);
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
return (
|
||||||
{browserSupportsSpeechRecognition && (
|
<div className="flex justify-between items-center gap-2">
|
||||||
<Button
|
<AppSegmented value={mode} options={modesOptions} onChange={(v) => onModeChange(v.value)} />
|
||||||
rounding={'large'}
|
|
||||||
variant={'ghost'}
|
<div className="flex items-center gap-2">
|
||||||
prefix={<Microphone />}
|
{browserSupportsSpeechRecognition && (
|
||||||
onClick={listening ? stopListening : startListening}
|
<AppTooltip title={listening ? 'Stop Dictation...' : 'Press to Dictate...'}>
|
||||||
loading={submitting}
|
<Button
|
||||||
disabled={disabled}
|
rounding={'large'}
|
||||||
className={cn(
|
variant={'ghost'}
|
||||||
'origin-center transform-gpu transition-all duration-300 ease-out will-change-transform text-text-secondary',
|
prefix={<Microphone />}
|
||||||
!disabled && 'hover:scale-110 active:scale-95',
|
onClick={listening ? stopListening : startListening}
|
||||||
listening && 'bg-item-select text-foreground'
|
loading={submitting}
|
||||||
)}
|
disabled={disabled}
|
||||||
/>
|
className={cn(
|
||||||
)}
|
'origin-center transform-gpu transition-all duration-300 ease-out will-change-transform text-text-secondary',
|
||||||
<Button
|
!disabled && 'hover:scale-110 active:scale-95',
|
||||||
rounding={'large'}
|
listening && 'bg-item-select text-foreground animate-pulse'
|
||||||
variant={'default'}
|
)}
|
||||||
prefix={<ArrowUp />}
|
/>
|
||||||
onClick={submitting ? onStop : onSubmit}
|
</AppTooltip>
|
||||||
loading={submitting}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn(
|
|
||||||
'origin-center transform-gpu transition-all duration-300 ease-out will-change-transform',
|
|
||||||
!disabled && 'hover:scale-110 active:scale-95'
|
|
||||||
)}
|
)}
|
||||||
/>
|
<AppTooltip title={'Submit'}>
|
||||||
|
<Button
|
||||||
|
rounding={'large'}
|
||||||
|
variant={'default'}
|
||||||
|
prefix={<ArrowUp />}
|
||||||
|
onClick={submitting ? onStop : onSubmit}
|
||||||
|
loading={submitting}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'origin-center transform-gpu transition-all duration-300 ease-out will-change-transform',
|
||||||
|
!disabled && 'hover:scale-110 active:scale-95'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AppTooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
|
BusterChatInputButtons.displayName = 'BusterChatInputButtons';
|
||||||
|
|
||||||
const ModePopoverContent = ({
|
const ModePopoverContent = ({
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -58,6 +58,13 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
|
||||||
);
|
);
|
||||||
}, [mentions]);
|
}, [mentions]);
|
||||||
|
|
||||||
|
const getValue = () => {
|
||||||
|
return onUpdateTransformer({
|
||||||
|
editor,
|
||||||
|
mentionsByTrigger,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
Document,
|
Document,
|
||||||
|
@ -94,6 +101,7 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
|
||||||
ref,
|
ref,
|
||||||
() => ({
|
() => ({
|
||||||
editor,
|
editor,
|
||||||
|
getValue,
|
||||||
}),
|
}),
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { MentionNodeAttrs, MentionOptions } from '@tiptap/extension-mention';
|
import type { MentionNodeAttrs, MentionOptions } from '@tiptap/extension-mention';
|
||||||
import type { Editor, EditorEvents } from '@tiptap/react';
|
import type { Editor, EditorEvents } from '@tiptap/react';
|
||||||
import type { MentionPillAttributes } from './MentionPill';
|
import type { MentionPillAttributes } from './MentionPill';
|
||||||
|
import type { onUpdateTransformer } from './update-transformers';
|
||||||
|
|
||||||
export type MentionOnSelectParams<T = unknown> = {
|
export type MentionOnSelectParams<T = unknown> = {
|
||||||
value: T;
|
value: T;
|
||||||
|
@ -106,6 +107,7 @@ export type MentionInputProps = {
|
||||||
|
|
||||||
export type MentionInputRef = {
|
export type MentionInputRef = {
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
|
getValue: () => ReturnType<typeof onUpdateTransformer>;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Command } from 'cmdk';
|
import { Command } from 'cmdk';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { MentionInputProps, MentionInputRef } from '../MentionInput';
|
import type { MentionInputProps, MentionInputRef } from '../MentionInput';
|
||||||
import type {
|
import type {
|
||||||
MentionInputSuggestionsOnSelectParams,
|
MentionInputSuggestionsOnSelectParams,
|
||||||
MentionInputSuggestionsProps,
|
MentionInputSuggestionsProps,
|
||||||
|
MentionInputSuggestionsRef,
|
||||||
} from './MentionInputSuggestions.types';
|
} from './MentionInputSuggestions.types';
|
||||||
import { MentionInputSuggestionsContainer } from './MentionInputSuggestionsContainer';
|
import { MentionInputSuggestionsContainer } from './MentionInputSuggestionsContainer';
|
||||||
import { MentionInputSuggestionsEmpty } from './MentionInputSuggestionsEmpty';
|
import { MentionInputSuggestionsEmpty } from './MentionInputSuggestionsEmpty';
|
||||||
|
@ -13,156 +14,191 @@ import { MentionInputSuggestionsItemsSelector } from './MentionInputSuggestionsI
|
||||||
import { MentionInputSuggestionsList } from './MentionInputSuggestionsList';
|
import { MentionInputSuggestionsList } from './MentionInputSuggestionsList';
|
||||||
import { MentionInputSuggestionsMentionsInput } from './MentionInputSuggestionsMentionsInput';
|
import { MentionInputSuggestionsMentionsInput } from './MentionInputSuggestionsMentionsInput';
|
||||||
|
|
||||||
export const MentionInputSuggestions = ({
|
export const MentionInputSuggestions = forwardRef<
|
||||||
placeholder,
|
MentionInputSuggestionsRef,
|
||||||
defaultValue,
|
MentionInputSuggestionsProps
|
||||||
value: valueProp,
|
>(
|
||||||
emptyComponent,
|
(
|
||||||
submitting,
|
{
|
||||||
onPressEnter,
|
placeholder,
|
||||||
disabled = false,
|
defaultValue,
|
||||||
onChange,
|
value: valueProp,
|
||||||
ariaLabel = 'Mention Input Suggestions',
|
emptyComponent,
|
||||||
readOnly,
|
submitting,
|
||||||
autoFocus,
|
onPressEnter,
|
||||||
children,
|
disabled = false,
|
||||||
//container
|
onChange,
|
||||||
className,
|
ariaLabel = 'Mention Input Suggestions',
|
||||||
inputContainerClassName,
|
readOnly,
|
||||||
suggestionsContainerClassName,
|
autoFocus,
|
||||||
//suggestions
|
children,
|
||||||
suggestionItems,
|
//container
|
||||||
closeSuggestionOnSelect = true,
|
className,
|
||||||
addSuggestionValueToInput = true,
|
inputContainerClassName,
|
||||||
onSuggestionItemClick,
|
suggestionsContainerClassName,
|
||||||
filter,
|
//suggestions
|
||||||
shouldFilter,
|
suggestionItems,
|
||||||
//mentions
|
closeSuggestionOnSelect = true,
|
||||||
onMentionItemClick,
|
addSuggestionValueToInput = true,
|
||||||
mentions,
|
onSuggestionItemClick,
|
||||||
}: MentionInputSuggestionsProps) => {
|
filter,
|
||||||
const [hasClickedSelect, setHasClickedSelect] = useState(false);
|
shouldFilter,
|
||||||
const [value, setValue] = useState(valueProp ?? defaultValue);
|
//mentions
|
||||||
const [hasResults, setHasResults] = useState(!!suggestionItems.length);
|
onMentionItemClick,
|
||||||
|
mentions,
|
||||||
|
}: MentionInputSuggestionsProps,
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [hasClickedSelect, setHasClickedSelect] = useState(false);
|
||||||
|
const [value, setValue] = useState(valueProp ?? defaultValue);
|
||||||
|
const [hasResults, setHasResults] = useState(!!suggestionItems.length);
|
||||||
|
|
||||||
const commandListNavigatedRef = useRef(false);
|
const commandListNavigatedRef = useRef(false);
|
||||||
const commandRef = useRef<HTMLDivElement>(null);
|
const commandRef = useRef<HTMLDivElement>(null);
|
||||||
const mentionsInputRef = useRef<MentionInputRef>(null);
|
const mentionsInputRef = useRef<MentionInputRef>(null);
|
||||||
|
|
||||||
const showSuggestionList = !hasClickedSelect && suggestionItems.length > 0;
|
const showSuggestionList = !hasClickedSelect && suggestionItems.length > 0;
|
||||||
|
|
||||||
const onChangeInputValue: MentionInputProps['onChange'] = useCallback(
|
const onChangeInputValue: MentionInputProps['onChange'] = useCallback(
|
||||||
(transformedValue, arrayValue, rawValue) => {
|
(transformedValue, arrayValue, rawValue) => {
|
||||||
setValue(value);
|
setValue(transformedValue);
|
||||||
setHasClickedSelect(false);
|
onChange?.(transformedValue, arrayValue, rawValue);
|
||||||
// Reset command list navigation when user types
|
commandListNavigatedRef.current = false;
|
||||||
commandListNavigatedRef.current = false;
|
setHasClickedSelect(false);
|
||||||
onChange?.(transformedValue, arrayValue, rawValue);
|
},
|
||||||
},
|
[onChange, setHasClickedSelect]
|
||||||
[]
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const onSelectItem = useMemoizedFn(
|
//this is used to change the value of the input from outside the component
|
||||||
({ onClick, ...params }: MentionInputSuggestionsOnSelectParams) => {
|
const onChangeValue = useMemoizedFn((v: string | ((prevState: string) => string)) => {
|
||||||
const { addValueToInput, loading, inputValue, label, disabled } = params;
|
if (typeof v === 'function') {
|
||||||
if (disabled) {
|
setValue((prevState) => {
|
||||||
console.warn('Item is disabled', params);
|
const newState = v(prevState);
|
||||||
return;
|
mentionsInputRef.current?.editor?.commands.setContent(newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setValue(v);
|
||||||
|
mentionsInputRef.current?.editor?.commands.setContent(v);
|
||||||
}
|
}
|
||||||
if (submitting) {
|
});
|
||||||
console.warn('Input is submitting');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (loading) {
|
|
||||||
console.warn('Item is loading', params);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (addValueToInput) {
|
|
||||||
const stringValue = inputValue ?? String(label);
|
|
||||||
mentionsInputRef.current?.editor?.commands.setContent(stringValue);
|
|
||||||
setValue(stringValue);
|
|
||||||
}
|
|
||||||
onClick?.();
|
|
||||||
if (closeSuggestionOnSelect) setHasClickedSelect(true);
|
|
||||||
onSuggestionItemClick?.(params);
|
|
||||||
setHasResults(false);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Track arrow key navigation in the command list
|
const onSelectItem = useMemoizedFn(
|
||||||
useEffect(() => {
|
({ onClick, ...params }: MentionInputSuggestionsOnSelectParams) => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const { addValueToInput, loading, inputValue, label, disabled } = params;
|
||||||
if (showSuggestionList && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
if (disabled) {
|
||||||
commandListNavigatedRef.current = true;
|
console.warn('Item is disabled', params);
|
||||||
}
|
return;
|
||||||
|
|
||||||
// If Enter is pressed and command list was navigated, manually trigger selection
|
|
||||||
if (showSuggestionList && event.key === 'Enter' && commandListNavigatedRef.current) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
// Find the currently selected item and trigger its click
|
|
||||||
const selectedItem = commandElement?.querySelector('[data-selected="true"]') as HTMLElement;
|
|
||||||
if (selectedItem) {
|
|
||||||
selectedItem.click();
|
|
||||||
}
|
}
|
||||||
|
if (submitting) {
|
||||||
|
console.warn('Input is submitting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
console.warn('Item is loading', params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (addValueToInput) {
|
||||||
|
const stringValue = inputValue ?? String(label);
|
||||||
|
mentionsInputRef.current?.editor?.commands.setContent(stringValue);
|
||||||
|
setValue(stringValue);
|
||||||
|
}
|
||||||
|
onClick?.();
|
||||||
|
if (closeSuggestionOnSelect) setHasClickedSelect(true);
|
||||||
|
onSuggestionItemClick?.(params);
|
||||||
|
setHasResults(false);
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
const commandElement = commandRef.current;
|
|
||||||
if (commandElement) {
|
const getValue = useMemoizedFn(() => {
|
||||||
commandElement.addEventListener('keydown', handleKeyDown, true); // Use capture phase
|
return mentionsInputRef.current?.getValue();
|
||||||
return () => {
|
});
|
||||||
commandElement.removeEventListener('keydown', handleKeyDown, true);
|
|
||||||
|
// Track arrow key navigation in the command list
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (showSuggestionList && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
||||||
|
commandListNavigatedRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Enter is pressed and command list was navigated, manually trigger selection
|
||||||
|
if (showSuggestionList && event.key === 'Enter' && commandListNavigatedRef.current) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
// Find the currently selected item and trigger its click
|
||||||
|
const selectedItem = commandElement?.querySelector(
|
||||||
|
'[data-selected="true"]'
|
||||||
|
) as HTMLElement;
|
||||||
|
if (selectedItem) {
|
||||||
|
selectedItem.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
const commandElement = commandRef.current;
|
||||||
}, [showSuggestionList]);
|
if (commandElement) {
|
||||||
|
commandElement.addEventListener('keydown', handleKeyDown, true); // Use capture phase
|
||||||
|
return () => {
|
||||||
|
commandElement.removeEventListener('keydown', handleKeyDown, true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [showSuggestionList]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({}), []);
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
value,
|
||||||
|
onChangeValue,
|
||||||
|
getValue,
|
||||||
|
}),
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command
|
<Command
|
||||||
ref={commandRef}
|
ref={commandRef}
|
||||||
value={value}
|
value={value}
|
||||||
label={ariaLabel}
|
label={ariaLabel}
|
||||||
className={cn('relative border rounded overflow-hidden bg-background shadow', className)}
|
className={cn('relative border rounded overflow-hidden bg-background shadow', className)}
|
||||||
shouldFilter={shouldFilter}
|
shouldFilter={shouldFilter}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
>
|
|
||||||
<MentionInputSuggestionsContainer className={inputContainerClassName}>
|
|
||||||
<MentionInputSuggestionsMentionsInput
|
|
||||||
ref={mentionsInputRef}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
readOnly={readOnly}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
placeholder={placeholder}
|
|
||||||
mentions={mentions}
|
|
||||||
value={value}
|
|
||||||
onChange={onChangeInputValue}
|
|
||||||
onMentionItemClick={onMentionItemClick}
|
|
||||||
onPressEnter={onPressEnter}
|
|
||||||
commandListNavigatedRef={commandListNavigatedRef}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
{children && <div className="mt-3">{children}</div>}
|
|
||||||
</MentionInputSuggestionsContainer>
|
|
||||||
{hasResults && <div className="border-b mb-1.5" />}
|
|
||||||
<MentionInputSuggestionsList
|
|
||||||
show={showSuggestionList}
|
|
||||||
className={cn(suggestionsContainerClassName, hasResults && 'pb-1.5')}
|
|
||||||
>
|
>
|
||||||
<MentionInputSuggestionsItemsSelector
|
<MentionInputSuggestionsContainer className={inputContainerClassName}>
|
||||||
suggestionItems={suggestionItems}
|
<MentionInputSuggestionsMentionsInput
|
||||||
onSelect={onSelectItem}
|
ref={mentionsInputRef}
|
||||||
addValueToInput={addSuggestionValueToInput}
|
defaultValue={defaultValue}
|
||||||
closeOnSelect={closeSuggestionOnSelect}
|
readOnly={readOnly}
|
||||||
hasResults={hasResults}
|
autoFocus={autoFocus}
|
||||||
setHasResults={setHasResults}
|
placeholder={placeholder}
|
||||||
/>
|
mentions={mentions}
|
||||||
|
value={value}
|
||||||
|
onChange={onChangeInputValue}
|
||||||
|
onMentionItemClick={onMentionItemClick}
|
||||||
|
onPressEnter={onPressEnter}
|
||||||
|
commandListNavigatedRef={commandListNavigatedRef}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{children && <div className="mt-3">{children}</div>}
|
||||||
|
</MentionInputSuggestionsContainer>
|
||||||
|
{hasResults && <div className="border-b mb-1.5" />}
|
||||||
|
<MentionInputSuggestionsList
|
||||||
|
show={showSuggestionList}
|
||||||
|
className={cn(suggestionsContainerClassName, hasResults && 'pb-1.5')}
|
||||||
|
>
|
||||||
|
<MentionInputSuggestionsItemsSelector
|
||||||
|
suggestionItems={suggestionItems}
|
||||||
|
onSelect={onSelectItem}
|
||||||
|
addValueToInput={addSuggestionValueToInput}
|
||||||
|
closeOnSelect={closeSuggestionOnSelect}
|
||||||
|
hasResults={hasResults}
|
||||||
|
setHasResults={setHasResults}
|
||||||
|
/>
|
||||||
|
|
||||||
<MentionInputSuggestionsEmpty
|
<MentionInputSuggestionsEmpty
|
||||||
setHasResults={setHasResults}
|
setHasResults={setHasResults}
|
||||||
emptyComponent={emptyComponent}
|
emptyComponent={emptyComponent}
|
||||||
/>
|
/>
|
||||||
</MentionInputSuggestionsList>
|
</MentionInputSuggestionsList>
|
||||||
</Command>
|
</Command>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { Command } from 'cmdk';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { MentionSuggestionExtension } from '../MentionInput';
|
import type { MentionSuggestionExtension } from '../MentionInput';
|
||||||
import type { MentionInputProps, MentionTriggerItem } from '../MentionInput/MentionInput.types';
|
import type { MentionInputProps, MentionTriggerItem } from '../MentionInput/MentionInput.types';
|
||||||
|
import type { onUpdateTransformer } from '../MentionInput/update-transformers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Override the addValueToInput and closeOnSelect props for the item based on the group props
|
* @description Override the addValueToInput and closeOnSelect props for the item based on the group props
|
||||||
|
@ -81,6 +82,12 @@ export type MentionInputSuggestionsProps<T = string> = {
|
||||||
suggestionsContainerClassName?: string;
|
suggestionsContainerClassName?: string;
|
||||||
} & Pick<React.ComponentProps<typeof Command>, 'filter' | 'shouldFilter'>;
|
} & Pick<React.ComponentProps<typeof Command>, 'filter' | 'shouldFilter'>;
|
||||||
|
|
||||||
|
export type MentionInputSuggestionsRef = {
|
||||||
|
value: string;
|
||||||
|
onChangeValue: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
getValue: () => ReturnType<typeof onUpdateTransformer> | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export type MentionInputSuggestionsContainerProps = {
|
export type MentionInputSuggestionsContainerProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
Loading…
Reference in New Issue