input chat

This commit is contained in:
Nate Kelley 2025-03-05 10:36:31 -07:00
parent ce4a188e1f
commit be76c32f69
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 47 additions and 106 deletions

View File

@ -37,6 +37,7 @@ export interface InputTextAreaProps
VariantProps<typeof textAreaVariants> { VariantProps<typeof textAreaVariants> {
autoResize?: AutoResizeOptions; autoResize?: AutoResizeOptions;
onPressMetaEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void; onPressMetaEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onPressEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
} }
export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextAreaProps>( export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextAreaProps>(
@ -49,6 +50,7 @@ export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextArea
rows = 1, rows = 1,
rounding = 'default', rounding = 'default',
onPressMetaEnter, onPressMetaEnter,
onPressEnter,
...props ...props
}, },
ref ref
@ -120,9 +122,14 @@ export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextArea
}); });
const handleKeyDown = useMemoizedFn((e: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = useMemoizedFn((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { if (e.key === 'Enter') {
if ((e.metaKey || e.ctrlKey) && onPressMetaEnter) {
e.preventDefault(); e.preventDefault();
onPressMetaEnter?.(e); onPressMetaEnter(e);
} else if (!e.shiftKey) {
e.preventDefault();
onPressEnter?.(e);
}
} }
props.onKeyDown?.(e); props.onKeyDown?.(e);
}); });

View File

@ -1,4 +1,4 @@
import React, { useMemo, useRef, forwardRef } from 'react'; import React, { useMemo, forwardRef } from 'react';
import { InputTextArea, InputTextAreaProps } from './InputTextArea'; import { InputTextArea, InputTextAreaProps } from './InputTextArea';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
@ -6,7 +6,6 @@ import { Button } from '../buttons/Button';
import { ArrowUp } from '../icons/NucleoIconOutlined'; import { ArrowUp } from '../icons/NucleoIconOutlined';
import { ShapeSquare } from '../icons/NucleoIconFilled'; import { ShapeSquare } from '../icons/NucleoIconFilled';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Tooltip } from '../tooltip';
const inputTextAreaButtonVariants = cva( const inputTextAreaButtonVariants = cva(
'relative flex w-full items-center overflow-visible rounded-xl border border-border transition-all duration-200', 'relative flex w-full items-center overflow-visible rounded-xl border border-border transition-all duration-200',
@ -53,10 +52,6 @@ export const InputTextAreaButton = forwardRef<HTMLTextAreaElement, InputTextArea
onSubmit(text); onSubmit(text);
}); });
const onPressMetaEnter = useMemoizedFn(() => {
onSubmitPreflight();
});
return ( return (
<div <div
className={cn( className={cn(
@ -74,7 +69,8 @@ export const InputTextAreaButton = forwardRef<HTMLTextAreaElement, InputTextArea
)} )}
autoResize={autoResize} autoResize={autoResize}
rounding="xl" rounding="xl"
onPressMetaEnter={onPressMetaEnter} onPressMetaEnter={onSubmitPreflight}
onPressEnter={onSubmitPreflight}
{...props} {...props}
/> />

View File

@ -4,10 +4,7 @@ import type {
} from '@/api/asset_interfaces'; } from '@/api/asset_interfaces';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import sample from 'lodash/sample'; import sample from 'lodash/sample';
import last from 'lodash/last';
import { useBusterChatContextSelector } from '../ChatProvider'; import { useBusterChatContextSelector } from '../ChatProvider';
import { timeout } from '@/lib/timeout';
import random from 'lodash/random';
export const useAutoAppendThought = () => { export const useAutoAppendThought = () => {
const onUpdateChatMessage = useBusterChatContextSelector((x) => x.onUpdateChatMessage); const onUpdateChatMessage = useBusterChatContextSelector((x) => x.onUpdateChatMessage);

View File

@ -1,90 +1,57 @@
import React, { useMemo, useRef, useState } from 'react'; import React, { ChangeEvent, useMemo, useRef, useState } from 'react';
import { Input } from 'antd';
import { createStyles } from 'antd-style';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { inputHasText } from '@/lib'; import { inputHasText } from '@/lib/text';
import { AIWarning } from './AIWarning'; import { AIWarning } from './AIWarning';
import { SubmitButton } from './SubmitButton';
import { useChatInputFlow } from './useChatInputFlow'; import { useChatInputFlow } from './useChatInputFlow';
import type { TextAreaRef } from 'antd/es/input/TextArea';
import { useChatIndividualContextSelector } from '../../../ChatContext'; import { useChatIndividualContextSelector } from '../../../ChatContext';
import { InputTextAreaButton } from '@/components/ui/inputs/InputTextAreaButton';
import { cn } from '@/lib/classMerge';
const autoSize = { minRows: 3, maxRows: 16 }; const autoResizeConfig = { minRows: 3, maxRows: 16 };
export const ChatInput: React.FC<{}> = React.memo(({}) => { export const ChatInput: React.FC<{}> = React.memo(({}) => {
const { styles, cx } = useStyles(); const textAreaRef = useRef<HTMLTextAreaElement>(null);
const inputRef = useRef<TextAreaRef>(null);
const loading = useChatIndividualContextSelector((x) => x.isLoading); const loading = useChatIndividualContextSelector((x) => x.isLoading);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [isFocused, setIsFocused] = useState(false);
const disableSendButton = useMemo(() => { const disableSubmit = useMemo(() => {
return !inputHasText(inputValue); return !inputHasText(inputValue);
}, [inputValue]); }, [inputValue]);
const { onSubmitPreflight } = useChatInputFlow({ const { onSubmitPreflight } = useChatInputFlow({
disableSendButton, disableSubmit,
inputValue, inputValue,
setInputValue, setInputValue,
loading, loading,
inputRef textAreaRef
}); });
const onPressEnter = useMemoizedFn((e: React.KeyboardEvent<HTMLTextAreaElement>) => { const onChange = useMemoizedFn((e: ChangeEvent<HTMLTextAreaElement>) => {
if (e.metaKey && e.key === 'Enter') {
onSubmitPreflight();
}
});
const onChangeInput = useMemoizedFn((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value); setInputValue(e.target.value);
}); });
const onBlurInput = useMemoizedFn(() => { const onStop = useMemoizedFn(() => {
setIsFocused(false); // setInputValue('');
});
const onFocusInput = useMemoizedFn(() => {
setIsFocused(true);
}); });
return ( return (
<div <div
className={cx( className={cn(
styles.inputCard, 'flex flex-col space-y-1.5',
'z-10 mx-3 mt-0.5 flex min-h-fit flex-col items-center space-y-1.5 overflow-hidden pb-2' 'z-10 mx-3 mt-0.5 mb-2 flex min-h-fit flex-col items-center overflow-hidden',
'shadow-lg'
)}> )}>
<div <InputTextAreaButton
className={cx( placeholder="Ask Buster a question..."
styles.inputContainer, autoResize={autoResizeConfig}
isFocused && 'focused', onSubmit={onSubmitPreflight}
loading && 'loading', onChange={onChange}
'relative flex w-full items-center overflow-hidden' onStop={onStop}
)}>
<Input.TextArea
ref={inputRef}
variant="borderless"
onBlur={onBlurInput}
onFocus={onFocusInput}
className="inline-block w-full pt-2! pr-9! pb-2! pl-3.5! align-middle"
placeholder="Ask a follow up..."
value={inputValue}
autoFocus={true}
onChange={onChangeInput}
onPressEnter={onPressEnter}
disabled={loading}
autoSize={autoSize}
/>
<div className="absolute right-2 bottom-2">
<SubmitButton
disableSendButton={disableSendButton}
loading={loading} loading={loading}
onSubmitPreflight={onSubmitPreflight} disabledSubmit={disableSubmit}
autoFocus
ref={textAreaRef}
/> />
</div>
</div>
<AIWarning /> <AIWarning />
</div> </div>
@ -92,28 +59,3 @@ export const ChatInput: React.FC<{}> = React.memo(({}) => {
}); });
ChatInput.displayName = 'ChatInput'; ChatInput.displayName = 'ChatInput';
const useStyles = createStyles(({ token, css }) => ({
inputCard: css`
box-shadow: 0px -10px 18px 10px ${token.colorBgContainerDisabled};
`,
inputContainer: css`
background: ${token.colorBgBase};
border-radius: ${token.borderRadius}px;
border: 0.5px solid ${token.colorBorder};
transition: border-color 0.2s;
min-height: 40px;
&:hover {
border-color: ${token.colorPrimaryHover};
}
&.focused {
border-color: ${token.colorPrimary};
}
&.loading {
border-color: ${token.colorText};
textarea {
background: ${token.colorBgContainerDisabled};
}
}
`
}));

View File

@ -1,22 +1,21 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useChatIndividualContextSelector } from '../../../ChatContext'; import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { useBusterNewChatContextSelector } from '@/context/Chats'; import { useBusterNewChatContextSelector } from '@/context/Chats';
import type { TextAreaRef } from 'antd/es/input/TextArea';
type FlowType = 'followup-chat' | 'followup-metric' | 'followup-dashboard' | 'new'; type FlowType = 'followup-chat' | 'followup-metric' | 'followup-dashboard' | 'new';
export const useChatInputFlow = ({ export const useChatInputFlow = ({
disableSendButton, disableSubmit,
inputValue, inputValue,
setInputValue, setInputValue,
inputRef, textAreaRef,
loading loading
}: { }: {
disableSendButton: boolean; disableSubmit: boolean;
inputValue: string; inputValue: string;
setInputValue: (value: string) => void; setInputValue: (value: string) => void;
inputRef: React.RefObject<TextAreaRef>; textAreaRef: React.RefObject<HTMLTextAreaElement>;
loading: boolean; loading: boolean;
}) => { }) => {
const hasChat = useChatIndividualContextSelector((x) => x.hasChat); const hasChat = useChatIndividualContextSelector((x) => x.hasChat);
@ -37,7 +36,7 @@ export const useChatInputFlow = ({
}, [hasChat, selectedFileType, selectedFileId]); }, [hasChat, selectedFileType, selectedFileId]);
const onSubmitPreflight = useMemoizedFn(async () => { const onSubmitPreflight = useMemoizedFn(async () => {
if (disableSendButton || !chatId || !currentMessageId) return; if (disableSubmit || !chatId || !currentMessageId) return;
if (loading) { if (loading) {
onStopChat({ chatId: chatId!, messageId: currentMessageId }); onStopChat({ chatId: chatId!, messageId: currentMessageId });
@ -76,7 +75,7 @@ export const useChatInputFlow = ({
setInputValue(''); setInputValue('');
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus(); textAreaRef.current?.focus();
}, 50); }, 50);
}); });