Added suggestion trigger

This commit is contained in:
Nate Kelley 2025-09-29 20:24:58 -06:00
parent 1676402541
commit b900700686
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
7 changed files with 100 additions and 70 deletions

View File

@ -1,10 +1,11 @@
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
import type { GetSuggestedPromptsResponse } from '@buster/server-shared/user';
import type { Editor } from '@tiptap/react';
import sampleSize from 'lodash/sampleSize';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useCreateShortcutsMentionsSuggestions } from '@/components/features/input/Mentions/ShortcutsSuggestions/ShortcutsSuggestions';
import { Plus } from '@/components/ui/icons';
import React, { useMemo, useRef, useState } from 'react';
import {
useCreateShortcutsMentionsSuggestions,
useShortcutsSuggestions,
} from '@/components/features/input/Mentions/ShortcutsSuggestions/ShortcutsSuggestions';
import CircleQuestion from '@/components/ui/icons/NucleoIconOutlined/circle-question';
import FileSparkle from '@/components/ui/icons/NucleoIconOutlined/file-sparkle';
import type { MentionSuggestionExtension } from '@/components/ui/inputs/MentionInput';
@ -36,7 +37,7 @@ export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
const [openCreateShortcutModal, setOpenCreateShortcutModal] = useState(false);
const [mode, setMode] = useState<BusterChatInputMode>('auto');
const shortcutsSuggestions = useShortcuts(
const shortcutsSuggestions = useShortcutsSuggestions(
shortcuts,
setOpenCreateShortcutModal,
mentionInputSuggestionsRef
@ -166,46 +167,3 @@ const useUniqueSuggestions = (
] satisfies MentionInputSuggestionsProps['suggestionItems'];
}, [suggestedPrompts]);
};
const useShortcuts = (
shortcuts: ListShortcutsResponse['shortcuts'],
setOpenCreateShortcutModal: (open: boolean) => void,
mentionInputSuggestionsRef: React.RefObject<MentionInputSuggestionsRef | null>
): MentionInputSuggestionsProps['suggestionItems'] => {
return useMemo(() => {
const shortcutsItems = shortcuts.map<MentionInputSuggestionsDropdownItem>((shortcut) => {
return {
type: 'item',
value: shortcut.name,
label: shortcut.name,
icon: '/',
inputValue: `/ ${shortcut.name}`,
onClick: () => {
const onChangeValue = mentionInputSuggestionsRef.current?.onChangeValue;
onChangeValue?.(shortcut.name);
},
};
});
shortcutsItems.push({
type: 'item',
value: 'createShortcut',
label: 'Create shortcut',
icon: <Plus />,
inputValue: '/ Create shortcut',
onClick: () => {
setOpenCreateShortcutModal(true);
},
});
return [
{
type: 'group',
label: 'Shortcuts',
suggestionItems: shortcutsItems,
addValueToInput: false,
closeOnSelect: true,
},
];
}, [shortcuts, setOpenCreateShortcutModal, mentionInputSuggestionsRef]);
};

View File

@ -6,11 +6,18 @@ import PenWriting from '@/components/ui/icons/NucleoIconOutlined/pen-writing';
import Plus from '@/components/ui/icons/NucleoIconOutlined/plus';
import {
createMentionSuggestionExtension,
type MentionInputTriggerItem,
MentionSecondaryContentDropdown,
type MentionTriggerItem,
} from '@/components/ui/inputs/MentionInput';
import type {
MentionInputSuggestionsDropdownItem,
MentionInputSuggestionsProps,
MentionInputSuggestionsRef,
} from '@/components/ui/inputs/MentionInputSuggestions';
import { ShortcutPopoverContent } from './ShortcutPopoverContent';
export const SHORTCUT_MENTION_TRIGGER = '/';
export const useCreateShortcutsMentionsSuggestions = (
shortcuts: ListShortcutsResponse['shortcuts'],
setOpenCreateShortcutModal: (open: boolean) => void
@ -20,10 +27,10 @@ export const useCreateShortcutsMentionsSuggestions = (
return useMemo(
() =>
createMentionSuggestionExtension({
trigger: '/',
trigger: SHORTCUT_MENTION_TRIGGER,
items: [
...shortcuts.map(createShortcut),
{ type: 'separator' },
...shortcuts.map(createShortcutForMention),
{ type: 'separator' as const },
{
value: 'manageShortcuts',
label: 'Manage shortcuts',
@ -58,14 +65,11 @@ export const useCreateShortcutsMentionsSuggestions = (
);
};
const createShortcut = (shortcut: Shortcut): MentionInputTriggerItem<string> => {
const createShortcutForMention = (shortcut: Shortcut): MentionTriggerItem<string> => {
return {
value: shortcut.id,
label: shortcut.name,
icon: <PenWriting />,
onSelect: (props) => {
console.log('onSelect shortcut?', props);
},
secondaryContent: (
<MentionSecondaryContentDropdown
items={[
@ -90,3 +94,53 @@ const createShortcut = (shortcut: Shortcut): MentionInputTriggerItem<string> =>
),
};
};
export const useShortcutsSuggestions = (
shortcuts: ListShortcutsResponse['shortcuts'],
setOpenCreateShortcutModal: (open: boolean) => void,
mentionInputSuggestionsRef: React.RefObject<MentionInputSuggestionsRef | null>
): MentionInputSuggestionsProps['suggestionItems'] => {
return useMemo(() => {
const shortcutsItems = shortcuts.map<MentionInputSuggestionsDropdownItem>((shortcut) => {
return {
type: 'item',
value: shortcut.name,
label: shortcut.name,
icon: SHORTCUT_MENTION_TRIGGER,
inputValue: `/ ${shortcut.name}`,
onClick: () => {
const addMentionToInput = mentionInputSuggestionsRef.current?.addMentionToInput;
if (!addMentionToInput) {
console.warn('addMentionToInput is not defined', mentionInputSuggestionsRef.current);
return;
}
const shortcutForMention = createShortcutForMention(shortcut);
addMentionToInput?.({
...shortcutForMention,
trigger: SHORTCUT_MENTION_TRIGGER,
});
},
};
});
shortcutsItems.push({
type: 'item',
value: 'createShortcut',
label: 'Create shortcut',
icon: <Plus />,
inputValue: `${SHORTCUT_MENTION_TRIGGER} Create shortcut`,
onClick: () => {
setOpenCreateShortcutModal(true);
},
});
return [
{
type: 'group',
label: 'Shortcuts',
suggestionItems: shortcutsItems,
addValueToInput: false,
closeOnSelect: true,
},
];
}, [shortcuts, setOpenCreateShortcutModal, mentionInputSuggestionsRef]);
};

View File

@ -13,6 +13,7 @@ import type {
MentionInputRef,
MentionSuggestionExtension,
} from './MentionInput.types';
import type { MentionPillAttributes } from './MentionPill';
import { SubmitOnEnter } from './SubmitEnterExtension';
import { onUpdateTransformer } from './update-transformers';
@ -97,11 +98,24 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
onBlur: onBlur,
});
//exported for use in the mention input suggestions
const addMentionToInput = (mention: MentionPillAttributes) => {
editor
.chain()
.focus()
.insertContent([
{ type: 'mention', attrs: mention },
{ type: 'text', text: ' ' }, // add a trailing space so the caret leaves the atom
])
.run();
};
useImperativeHandle(
ref,
() => ({
editor,
getValue,
addMentionToInput,
}),
[editor]
);

View File

@ -107,6 +107,7 @@ export type MentionInputProps = {
export type MentionInputRef = {
editor: Editor | null;
addMentionToInput: (mention: MentionPillAttributes) => void;
getValue: () => ReturnType<typeof onUpdateTransformer>;
};

View File

@ -4,8 +4,9 @@ import { Popover } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import type { MentionTriggerItem } from './MentionInput.types';
export type MentionPillAttributes<T = string> = Required<
Pick<MentionTriggerItem<T>, 'label' | 'value' | 'doNotAddPipeOnSelect' | 'pillLabel'>
export type MentionPillAttributes<T = string> = Pick<
MentionTriggerItem<T>,
'label' | 'value' | 'doNotAddPipeOnSelect' | 'pillLabel'
> & { trigger: string };
export const MentionPill = <T extends string>({

View File

@ -110,9 +110,11 @@ export const MentionInputSuggestions = forwardRef<
}
);
const getValue = useMemoizedFn(() => {
return mentionsInputRef.current?.getValue();
});
// biome-ignore lint/style/noNonNullAssertion: we know the ref is not null
const getValue = mentionsInputRef.current?.getValue!;
// biome-ignore lint/style/noNonNullAssertion: we know the ref is not null
const addMentionToInput = mentionsInputRef.current?.addMentionToInput!;
const mounted = useMounted();
// Track arrow key navigation in the command list
useEffect(() => {
@ -157,8 +159,9 @@ export const MentionInputSuggestions = forwardRef<
value,
onChangeValue,
getValue,
addMentionToInput,
}),
[value, onChangeValue, getValue]
[value, mounted, onChangeValue, getValue, addMentionToInput]
);
return (
@ -222,11 +225,7 @@ const MentionInputSuggestionsContext = createContext<{
value: string;
onChangeValue: MentionInputSuggestionsRef['onChangeValue'];
getValue: MentionInputSuggestionsRef['getValue'];
}>({
value: '',
onChangeValue: () => {},
getValue: () => undefined,
});
}>({} as MentionInputSuggestionsRef);
const MentionInputSuggestionsProvider = ({
children,

View File

@ -1,7 +1,11 @@
import type { Command } from 'cmdk';
import type React from 'react';
import type { MentionSuggestionExtension } from '../MentionInput';
import type { MentionInputProps, MentionTriggerItem } from '../MentionInput/MentionInput.types';
import type {
MentionInputProps,
MentionInputRef,
MentionTriggerItem,
} from '../MentionInput/MentionInput.types';
import type { onUpdateTransformer } from '../MentionInput/update-transformers';
/**
@ -86,8 +90,7 @@ export type MentionInputSuggestionsProps<T = string> = {
export type MentionInputSuggestionsRef = {
value: string;
onChangeValue: React.Dispatch<React.SetStateAction<string>>;
getValue: () => ReturnType<typeof onUpdateTransformer> | undefined;
};
} & Pick<MentionInputRef, 'addMentionToInput' | 'getValue'>;
export type MentionInputSuggestionsContainerProps = {
children: React.ReactNode;