mirror of https://github.com/buster-so/buster.git
Add in a buster chat input
This commit is contained in:
parent
109e2f6398
commit
4215bd2868
|
@ -122,6 +122,7 @@
|
|||
"@tiptap/extension-document": "^3.5.0",
|
||||
"@tiptap/extension-mention": "^3.5.0",
|
||||
"@tiptap/extension-paragraph": "^3.5.0",
|
||||
"@tiptap/extension-placeholder": "^3.6.2",
|
||||
"@tiptap/extension-text": "^3.5.0",
|
||||
"@tiptap/pm": "^3.5.0",
|
||||
"@tiptap/react": "^3.5.0",
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import React from 'react';
|
||||
import { MentionInputSuggestions } from '@/components/ui/inputs/MentionInputSuggestions';
|
||||
|
||||
export const BusterChatInput = () => {
|
||||
return <div>BusterChatInput</div>;
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
|
||||
import type { GetSuggestedPromptsResponse } from '@buster/server-shared/user';
|
||||
import sample from 'lodash/sample';
|
||||
import sampleSize from 'lodash/sampleSize';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { MentionSuggestionExtension } from '@/components/ui/inputs/MentionInput';
|
||||
import type { MentionInputSuggestionsProps } from '@/components/ui/inputs/MentionInputSuggestions';
|
||||
import { MentionInputSuggestions } from '@/components/ui/inputs/MentionInputSuggestions';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
|
||||
export type BusterChatInput = {
|
||||
defaultValue: string;
|
||||
onSubmit: (value: string) => void;
|
||||
onStop: () => void;
|
||||
submitting: boolean;
|
||||
disabled: boolean;
|
||||
shortcuts: ListShortcutsResponse['shortcuts'];
|
||||
suggestedPrompts: GetSuggestedPromptsResponse['suggestedPrompts'];
|
||||
};
|
||||
|
||||
export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
||||
({ defaultValue, onSubmit, onStop, submitting, disabled, shortcuts, suggestedPrompts }) => {
|
||||
const uniqueSuggestions = useUniqueSuggestions(suggestedPrompts);
|
||||
const shortcutsSuggestions = useShortcuts(shortcuts);
|
||||
|
||||
const suggestionItems: MentionInputSuggestionsProps['suggestionItems'] = useMemo(() => {
|
||||
const items: MentionInputSuggestionsProps['suggestionItems'] = [...uniqueSuggestions];
|
||||
|
||||
if (items.length > 0 && shortcutsSuggestions.length > 0) {
|
||||
items.push({
|
||||
type: 'separator',
|
||||
});
|
||||
}
|
||||
|
||||
if (shortcutsSuggestions.length > 0) {
|
||||
items.push(...shortcutsSuggestions);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [uniqueSuggestions, shortcutsSuggestions]);
|
||||
|
||||
const mentions: MentionSuggestionExtension[] = useMemo(() => {
|
||||
return [];
|
||||
}, [shortcuts]);
|
||||
|
||||
const onSubmitPreflight = (value: string) => {
|
||||
if (submitting) {
|
||||
console.warn('Input is submitting');
|
||||
return;
|
||||
}
|
||||
if (disabled) {
|
||||
console.warn('Input is disabledGlobal');
|
||||
return;
|
||||
}
|
||||
onSubmit(value);
|
||||
};
|
||||
|
||||
const onStopPreflight = useMemoizedFn(() => {
|
||||
onStop();
|
||||
});
|
||||
|
||||
const onPressEnter: MentionInputSuggestionsProps['onPressEnter'] = useMemoizedFn(
|
||||
(value, _editorObjects, _rawText) => {
|
||||
// onSubmitPreflight(value);
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MentionInputSuggestions
|
||||
defaultValue={defaultValue}
|
||||
onPressEnter={onPressEnter}
|
||||
mentions={mentions}
|
||||
suggestionItems={suggestionItems}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BusterChatInputBase.displayName = 'BusterChatInputBase';
|
||||
|
||||
const useUniqueSuggestions = (
|
||||
suggestedPrompts: GetSuggestedPromptsResponse['suggestedPrompts']
|
||||
): MentionInputSuggestionsProps['suggestionItems'] => {
|
||||
return useMemo(() => {
|
||||
const allSuggestions: { type: keyof typeof suggestedPrompts; value: string }[] = Object.entries(
|
||||
suggestedPrompts
|
||||
).flatMap(([key, value]) => {
|
||||
return value.map((prompt) => {
|
||||
return {
|
||||
type: key as keyof typeof suggestedPrompts,
|
||||
value: prompt,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Ensure we have at least 4 suggestions
|
||||
if (allSuggestions.length < 4) {
|
||||
throw new Error('Not enough suggestions available - need at least 4');
|
||||
}
|
||||
|
||||
const fourUniqueSuggestions = sampleSize(allSuggestions, 4);
|
||||
|
||||
const items: MentionInputSuggestionsProps['suggestionItems'] = fourUniqueSuggestions.map(
|
||||
(suggestion) => ({
|
||||
type: 'item',
|
||||
value: suggestion.type,
|
||||
label: suggestion.value,
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}, [suggestedPrompts]);
|
||||
};
|
||||
|
||||
const useShortcuts = (
|
||||
shortcuts: ListShortcutsResponse['shortcuts']
|
||||
): MentionInputSuggestionsProps['suggestionItems'] => {
|
||||
return useMemo(() => {
|
||||
return shortcuts.map((shortcut) => {
|
||||
return {
|
||||
type: 'item',
|
||||
value: shortcut.name,
|
||||
label: shortcut.name,
|
||||
};
|
||||
});
|
||||
}, [shortcuts]);
|
||||
};
|
|
@ -296,6 +296,8 @@ const spongebobSuggestions = createMentionSuggestionExtension({
|
|||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
className: 'min-w-64',
|
||||
placeholder: 'Enter text here...',
|
||||
mentions: [
|
||||
looneyTunesSuggestions,
|
||||
theSimpsonsSuggestions,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ClientOnly } from '@tanstack/react-router';
|
||||
import Document from '@tiptap/extension-document';
|
||||
import Paragraph from '@tiptap/extension-paragraph';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import Text from '@tiptap/extension-text';
|
||||
import { EditorContent, EditorContext, useEditor } from '@tiptap/react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
@ -39,6 +40,7 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
|
|||
disabled,
|
||||
onPressEnter,
|
||||
commandListNavigatedRef,
|
||||
placeholder = '',
|
||||
variant = 'default',
|
||||
},
|
||||
ref
|
||||
|
@ -61,6 +63,7 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
|
|||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Placeholder.configure({ placeholder }),
|
||||
MentionExtension(mentions),
|
||||
SubmitOnEnter({
|
||||
mentionsByTrigger,
|
||||
|
@ -100,7 +103,11 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
|
|||
return (
|
||||
<ClientOnly>
|
||||
<EditorContext.Provider value={providerValue}>
|
||||
<EditorContent className="outline-0" editor={editor} style={style} />
|
||||
<EditorContent
|
||||
className="outline-0 [&_p.is-editor-empty:first-child:before]:text-gray-light/80 [&_p.is-editor-empty:first-child:before]:content-[attr(data-placeholder)] [&_p.is-editor-empty:first-child:before]:float-left [&_p.is-editor-empty:first-child:before]:h-0 [&_p.is-editor-empty:first-child:before]:pointer-events-none"
|
||||
editor={editor}
|
||||
style={style}
|
||||
/>
|
||||
</EditorContext.Provider>
|
||||
</ClientOnly>
|
||||
);
|
||||
|
|
|
@ -101,6 +101,7 @@ export type MentionInputProps = {
|
|||
disabled?: boolean;
|
||||
commandListNavigatedRef?: React.RefObject<boolean>;
|
||||
variant?: 'default' | 'ghost';
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export type MentionInputRef = {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Command } from 'cmdk';
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { MentionInputRef } from '../MentionInput';
|
||||
import type { MentionInputProps, MentionInputRef } from '../MentionInput';
|
||||
import type {
|
||||
MentionInputSuggestionsOnSelectParams,
|
||||
MentionInputSuggestionsProps,
|
||||
|
@ -19,10 +19,8 @@ export const MentionInputSuggestions = ({
|
|||
value: valueProp,
|
||||
emptyComponent,
|
||||
submitting,
|
||||
onSubmit,
|
||||
onPressEnter,
|
||||
disabled: disabledGlobal = false,
|
||||
onStop,
|
||||
disabled = false,
|
||||
onChange,
|
||||
ariaLabel = 'Mention Input Suggestions',
|
||||
readOnly,
|
||||
|
@ -53,13 +51,16 @@ export const MentionInputSuggestions = ({
|
|||
|
||||
const showSuggestionList = !hasClickedSelect && suggestionItems.length > 0;
|
||||
|
||||
const onChangeInputValue = useCallback((value: string) => {
|
||||
setValue(value);
|
||||
setHasClickedSelect(false);
|
||||
// Reset command list navigation when user types
|
||||
commandListNavigatedRef.current = false;
|
||||
onChange?.(value);
|
||||
}, []);
|
||||
const onChangeInputValue: MentionInputProps['onChange'] = useCallback(
|
||||
(transformedValue, arrayValue, rawValue) => {
|
||||
setValue(value);
|
||||
setHasClickedSelect(false);
|
||||
// Reset command list navigation when user types
|
||||
commandListNavigatedRef.current = false;
|
||||
onChange?.(transformedValue, arrayValue, rawValue);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onSelectItem = useMemoizedFn(
|
||||
({ onClick, ...params }: MentionInputSuggestionsOnSelectParams) => {
|
||||
|
@ -88,22 +89,6 @@ export const MentionInputSuggestions = ({
|
|||
}
|
||||
);
|
||||
|
||||
const onSubmitPreflight = useMemoizedFn((value: string) => {
|
||||
if (submitting) {
|
||||
console.warn('Input is submitting');
|
||||
return;
|
||||
}
|
||||
if (disabledGlobal) {
|
||||
console.warn('Input is disabledGlobal');
|
||||
return;
|
||||
}
|
||||
onSubmit(value);
|
||||
});
|
||||
|
||||
const onStopPreflight = useMemoizedFn(() => {
|
||||
onStop();
|
||||
});
|
||||
|
||||
// Track arrow key navigation in the command list
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
@ -151,8 +136,9 @@ export const MentionInputSuggestions = ({
|
|||
value={value}
|
||||
onChange={onChangeInputValue}
|
||||
onMentionItemClick={onMentionItemClick}
|
||||
onPressEnter={onPressEnter || onSubmit}
|
||||
onPressEnter={onPressEnter}
|
||||
commandListNavigatedRef={commandListNavigatedRef}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{children}
|
||||
</MentionInputSuggestionsContainer>
|
||||
|
|
|
@ -54,13 +54,11 @@ export type MentionInputSuggestionsSeparator = {
|
|||
export type MentionInputSuggestionsProps<T = string> = {
|
||||
defaultValue: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
submitting?: boolean;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
onSubmit: (value: string) => void;
|
||||
onChange?: MentionInputProps['onChange'];
|
||||
onPressEnter: MentionInputProps['onPressEnter'];
|
||||
onStop: () => void;
|
||||
autoFocus?: boolean;
|
||||
placeholder?: string;
|
||||
ariaLabel?: string;
|
||||
|
@ -87,4 +85,4 @@ export type MentionInputSuggestionsContainerProps = {
|
|||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
} & Pick<MentionInputSuggestionsProps, 'submitting' | 'disabled' | 'onStop' | 'onSubmit'>;
|
||||
} & Pick<MentionInputSuggestionsProps, 'submitting' | 'disabled' | 'onPressEnter'>;
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { MentionInputSuggestionsProps } from './MentionInputSuggestions.typ
|
|||
|
||||
export type MentionInputSuggestionsMentionsInputProps = Pick<
|
||||
MentionInputSuggestionsProps,
|
||||
'mentions' | 'value' | 'placeholder' | 'defaultValue' | 'onMentionItemClick'
|
||||
'mentions' | 'value' | 'placeholder' | 'defaultValue' | 'onMentionItemClick' | 'disabled'
|
||||
> & {
|
||||
onChange: MentionInputProps['onChange'];
|
||||
onPressEnter: MentionInputProps['onPressEnter'];
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './MentionInputSuggestions';
|
||||
export * from './MentionInputSuggestions.types';
|
||||
|
|
|
@ -688,6 +688,9 @@ importers:
|
|||
'@tiptap/extension-paragraph':
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))
|
||||
'@tiptap/extension-placeholder':
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2(@tiptap/extensions@3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))(@tiptap/pm@3.5.0))
|
||||
'@tiptap/extension-text':
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))
|
||||
|
@ -6118,6 +6121,11 @@ packages:
|
|||
peerDependencies:
|
||||
'@tiptap/core': ^3.5.0
|
||||
|
||||
'@tiptap/extension-placeholder@3.6.2':
|
||||
resolution: {integrity: sha512-Fv/iLD0iIa51s5HE202qustunAKDWL4qcRZJlbQJYNqAaVLhC5wjz29o2tgihFMyvgpKLlpYh2xUwCVMc/mGog==}
|
||||
peerDependencies:
|
||||
'@tiptap/extensions': ^3.6.2
|
||||
|
||||
'@tiptap/extension-strike@3.5.0':
|
||||
resolution: {integrity: sha512-I4XmXPuCgIQ93Hfn39S8n/EZhiVHSHzU/7awRqQM3cx/kiByc/CiZ86c7opkQozXAIxSycR1IMhS/WVsxQggJw==}
|
||||
peerDependencies:
|
||||
|
@ -18915,6 +18923,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@tiptap/core': 3.5.0(@tiptap/pm@3.5.0)
|
||||
|
||||
'@tiptap/extension-placeholder@3.6.2(@tiptap/extensions@3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))(@tiptap/pm@3.5.0))':
|
||||
dependencies:
|
||||
'@tiptap/extensions': 3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))(@tiptap/pm@3.5.0)
|
||||
|
||||
'@tiptap/extension-strike@3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.5.0(@tiptap/pm@3.5.0)
|
||||
|
|
Loading…
Reference in New Issue