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 { GetSuggestedPromptsResponse } from '@buster/server-shared/user';
|
||||
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 { MentionInputSuggestionsProps } from '@/components/ui/inputs/MentionInputSuggestions';
|
||||
import type {
|
||||
MentionInputSuggestionsProps,
|
||||
MentionInputSuggestionsRef,
|
||||
} from '@/components/ui/inputs/MentionInputSuggestions';
|
||||
import { MentionInputSuggestions } from '@/components/ui/inputs/MentionInputSuggestions';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
import { BusterChatInputButtons } from './BusterChatInputButtons';
|
||||
import { BusterChatInputButtons, type BusterChatInputMode } from './BusterChatInputButtons';
|
||||
|
||||
export type BusterChatInput = {
|
||||
defaultValue: string;
|
||||
|
@ -20,9 +23,12 @@ export type BusterChatInput = {
|
|||
|
||||
export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
||||
({ defaultValue, onSubmit, onStop, submitting, disabled, shortcuts, suggestedPrompts }) => {
|
||||
const mentionInputSuggestionsRef = useRef<MentionInputSuggestionsRef>(null);
|
||||
const uniqueSuggestions = useUniqueSuggestions(suggestedPrompts);
|
||||
const shortcutsSuggestions = useShortcuts(shortcuts);
|
||||
|
||||
const [mode, setMode] = useState<BusterChatInputMode>('auto');
|
||||
|
||||
const suggestionItems: MentionInputSuggestionsProps['suggestionItems'] = useMemo(() => {
|
||||
const items: MentionInputSuggestionsProps['suggestionItems'] = [...uniqueSuggestions];
|
||||
|
||||
|
@ -43,6 +49,10 @@ export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
|||
return [];
|
||||
}, [shortcuts]);
|
||||
|
||||
const onDictate = useMemoizedFn((transcript: string) => {
|
||||
mentionInputSuggestionsRef.current?.onChangeValue(transcript);
|
||||
});
|
||||
|
||||
const onSubmitPreflight = (value: string) => {
|
||||
if (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 (
|
||||
<MentionInputSuggestions
|
||||
defaultValue={defaultValue}
|
||||
|
@ -72,15 +89,16 @@ export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
|
|||
mentions={mentions}
|
||||
suggestionItems={suggestionItems}
|
||||
placeholder="Ask a question or type ‘/’ for shortcuts..."
|
||||
ref={mentionInputSuggestionsRef}
|
||||
>
|
||||
<BusterChatInputButtons
|
||||
onSubmit={() => {}}
|
||||
onStop={() => {}}
|
||||
onSubmit={onSubmitButton}
|
||||
onStop={onStop}
|
||||
submitting={submitting}
|
||||
disabled={disabled}
|
||||
mode="auto"
|
||||
onModeChange={() => {}}
|
||||
onDictate={() => {}}
|
||||
mode={mode}
|
||||
onModeChange={setMode}
|
||||
onDictate={onDictate}
|
||||
/>
|
||||
</MentionInputSuggestions>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
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 { Popover } from '@/components/ui/popover';
|
||||
import { AppSegmented, type AppSegmentedProps } from '@/components/ui/segmented';
|
||||
import { AppTooltip } from '@/components/ui/tooltip';
|
||||
import { Text } from '@/components/ui/typography';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
|
@ -20,9 +20,11 @@ type BusterChatInputButtons = {
|
|||
mode: BusterChatInputMode;
|
||||
onModeChange: (mode: BusterChatInputMode) => void;
|
||||
onDictate: (transcript: string) => void;
|
||||
onDictateListeningChange?: (listening: boolean) => void;
|
||||
};
|
||||
|
||||
export const BusterChatInputButtons = ({
|
||||
export const BusterChatInputButtons = React.memo(
|
||||
({
|
||||
onSubmit,
|
||||
onStop,
|
||||
submitting,
|
||||
|
@ -30,6 +32,7 @@ export const BusterChatInputButtons = ({
|
|||
mode,
|
||||
onModeChange,
|
||||
onDictate,
|
||||
onDictateListeningChange,
|
||||
}: BusterChatInputButtons) => {
|
||||
const { transcript, listening, resetTranscript, browserSupportsSpeechRecognition } =
|
||||
useSpeechRecognition();
|
||||
|
@ -48,12 +51,17 @@ export const BusterChatInputButtons = ({
|
|||
}
|
||||
}, [listening, transcript, onDictate]);
|
||||
|
||||
useEffect(() => {
|
||||
onDictateListeningChange?.(listening);
|
||||
}, [listening, onDictateListeningChange]);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<AppSegmented value={mode} options={modesOptions} onChange={(v) => onModeChange(v.value)} />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{browserSupportsSpeechRecognition && (
|
||||
<AppTooltip title={listening ? 'Stop Dictation...' : 'Press to Dictate...'}>
|
||||
<Button
|
||||
rounding={'large'}
|
||||
variant={'ghost'}
|
||||
|
@ -64,10 +72,12 @@ export const BusterChatInputButtons = ({
|
|||
className={cn(
|
||||
'origin-center transform-gpu transition-all duration-300 ease-out will-change-transform text-text-secondary',
|
||||
!disabled && 'hover:scale-110 active:scale-95',
|
||||
listening && 'bg-item-select text-foreground'
|
||||
listening && 'bg-item-select text-foreground animate-pulse'
|
||||
)}
|
||||
/>
|
||||
</AppTooltip>
|
||||
)}
|
||||
<AppTooltip title={'Submit'}>
|
||||
<Button
|
||||
rounding={'large'}
|
||||
variant={'default'}
|
||||
|
@ -80,10 +90,14 @@ export const BusterChatInputButtons = ({
|
|||
!disabled && 'hover:scale-110 active:scale-95'
|
||||
)}
|
||||
/>
|
||||
</AppTooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
BusterChatInputButtons.displayName = 'BusterChatInputButtons';
|
||||
|
||||
const ModePopoverContent = ({
|
||||
title,
|
||||
|
|
|
@ -58,6 +58,13 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
|
|||
);
|
||||
}, [mentions]);
|
||||
|
||||
const getValue = () => {
|
||||
return onUpdateTransformer({
|
||||
editor,
|
||||
mentionsByTrigger,
|
||||
});
|
||||
};
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
|
@ -94,6 +101,7 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
|
|||
ref,
|
||||
() => ({
|
||||
editor,
|
||||
getValue,
|
||||
}),
|
||||
[editor]
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { MentionNodeAttrs, MentionOptions } from '@tiptap/extension-mention';
|
||||
import type { Editor, EditorEvents } from '@tiptap/react';
|
||||
import type { MentionPillAttributes } from './MentionPill';
|
||||
import type { onUpdateTransformer } from './update-transformers';
|
||||
|
||||
export type MentionOnSelectParams<T = unknown> = {
|
||||
value: T;
|
||||
|
@ -106,6 +107,7 @@ export type MentionInputProps = {
|
|||
|
||||
export type MentionInputRef = {
|
||||
editor: Editor | null;
|
||||
getValue: () => ReturnType<typeof onUpdateTransformer>;
|
||||
};
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
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 { cn } from '@/lib/utils';
|
||||
import type { MentionInputProps, MentionInputRef } from '../MentionInput';
|
||||
import type {
|
||||
MentionInputSuggestionsOnSelectParams,
|
||||
MentionInputSuggestionsProps,
|
||||
MentionInputSuggestionsRef,
|
||||
} from './MentionInputSuggestions.types';
|
||||
import { MentionInputSuggestionsContainer } from './MentionInputSuggestionsContainer';
|
||||
import { MentionInputSuggestionsEmpty } from './MentionInputSuggestionsEmpty';
|
||||
|
@ -13,7 +14,12 @@ import { MentionInputSuggestionsItemsSelector } from './MentionInputSuggestionsI
|
|||
import { MentionInputSuggestionsList } from './MentionInputSuggestionsList';
|
||||
import { MentionInputSuggestionsMentionsInput } from './MentionInputSuggestionsMentionsInput';
|
||||
|
||||
export const MentionInputSuggestions = ({
|
||||
export const MentionInputSuggestions = forwardRef<
|
||||
MentionInputSuggestionsRef,
|
||||
MentionInputSuggestionsProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
placeholder,
|
||||
defaultValue,
|
||||
value: valueProp,
|
||||
|
@ -40,7 +46,9 @@ export const MentionInputSuggestions = ({
|
|||
//mentions
|
||||
onMentionItemClick,
|
||||
mentions,
|
||||
}: MentionInputSuggestionsProps) => {
|
||||
}: MentionInputSuggestionsProps,
|
||||
ref
|
||||
) => {
|
||||
const [hasClickedSelect, setHasClickedSelect] = useState(false);
|
||||
const [value, setValue] = useState(valueProp ?? defaultValue);
|
||||
const [hasResults, setHasResults] = useState(!!suggestionItems.length);
|
||||
|
@ -53,15 +61,28 @@ export const MentionInputSuggestions = ({
|
|||
|
||||
const onChangeInputValue: MentionInputProps['onChange'] = useCallback(
|
||||
(transformedValue, arrayValue, rawValue) => {
|
||||
setValue(value);
|
||||
setHasClickedSelect(false);
|
||||
// Reset command list navigation when user types
|
||||
commandListNavigatedRef.current = false;
|
||||
setValue(transformedValue);
|
||||
onChange?.(transformedValue, arrayValue, rawValue);
|
||||
commandListNavigatedRef.current = false;
|
||||
setHasClickedSelect(false);
|
||||
},
|
||||
[]
|
||||
[onChange, setHasClickedSelect]
|
||||
);
|
||||
|
||||
//this is used to change the value of the input from outside the component
|
||||
const onChangeValue = useMemoizedFn((v: string | ((prevState: string) => string)) => {
|
||||
if (typeof v === 'function') {
|
||||
setValue((prevState) => {
|
||||
const newState = v(prevState);
|
||||
mentionsInputRef.current?.editor?.commands.setContent(newState);
|
||||
return newState;
|
||||
});
|
||||
} else {
|
||||
setValue(v);
|
||||
mentionsInputRef.current?.editor?.commands.setContent(v);
|
||||
}
|
||||
});
|
||||
|
||||
const onSelectItem = useMemoizedFn(
|
||||
({ onClick, ...params }: MentionInputSuggestionsOnSelectParams) => {
|
||||
const { addValueToInput, loading, inputValue, label, disabled } = params;
|
||||
|
@ -89,6 +110,10 @@ export const MentionInputSuggestions = ({
|
|||
}
|
||||
);
|
||||
|
||||
const getValue = useMemoizedFn(() => {
|
||||
return mentionsInputRef.current?.getValue();
|
||||
});
|
||||
|
||||
// Track arrow key navigation in the command list
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
@ -101,7 +126,9 @@ export const MentionInputSuggestions = ({
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// Find the currently selected item and trigger its click
|
||||
const selectedItem = commandElement?.querySelector('[data-selected="true"]') as HTMLElement;
|
||||
const selectedItem = commandElement?.querySelector(
|
||||
'[data-selected="true"]'
|
||||
) as HTMLElement;
|
||||
if (selectedItem) {
|
||||
selectedItem.click();
|
||||
}
|
||||
|
@ -116,7 +143,15 @@ export const MentionInputSuggestions = ({
|
|||
}
|
||||
}, [showSuggestionList]);
|
||||
|
||||
useImperativeHandle(ref, () => ({}), []);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
value,
|
||||
onChangeValue,
|
||||
getValue,
|
||||
}),
|
||||
[value]
|
||||
);
|
||||
|
||||
return (
|
||||
<Command
|
||||
|
@ -165,4 +200,5 @@ export const MentionInputSuggestions = ({
|
|||
</MentionInputSuggestionsList>
|
||||
</Command>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ 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 { onUpdateTransformer } from '../MentionInput/update-transformers';
|
||||
|
||||
/**
|
||||
* @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;
|
||||
} & 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 = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
|
|
Loading…
Reference in New Issue