make submit button a little better

This commit is contained in:
Nate Kelley 2025-02-04 22:04:26 -07:00
parent e02ac8e35f
commit 3977d7bb34
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
11 changed files with 259 additions and 108 deletions

View File

@ -20,6 +20,8 @@ export type ChatCreateNewChat = BusterSocketRequestBase<
message_id?: string;
/** Optional ID of a metric to initialize the chat from */
metric_id?: string;
/** Optional ID of a dashboard to initialize the chat from */
dashboard_id?: string;
}
>;

View File

@ -34,6 +34,7 @@ export const NewChatModal = React.memo<{
open: boolean;
onClose: () => void;
}>(({ open, onClose }) => {
const token = useAntToken();
const searchParams = useParams();
const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage);
const { openErrorNotification } = useBusterNotifications();
@ -43,10 +44,9 @@ export const NewChatModal = React.memo<{
(x) => x.onSetSelectedChatDataSource
);
const selectedChatDataSource = useBusterNewChatContextSelector((x) => x.selectedChatDataSource);
const onSetPrompt = useBusterNewChatContextSelector((x) => x.onSetPrompt);
const prompt = useBusterNewChatContextSelector((x) => x.prompt);
const onBusterSearch = useBusterSearchContextSelector((x) => x.onBusterSearch);
const token = useAntToken();
const [prompt, setPrompt] = useState('');
const [openNewDatasetModal, setOpenNewDatasetModal] = useState(false);
const [suggestedPrompts, setSuggestedPrompts] = useState<BusterSearchResult[]>([]);
const [activeItem, setActiveItem] = useState<number | null>(null);
@ -97,7 +97,7 @@ export const NewChatModal = React.memo<{
useEffect(() => {
if (open) {
onSetPrompt('');
setPrompt('');
if (defaultSuggestedPrompts.length === 0) {
getDefaultSuggestedPrompts();

View File

@ -3,6 +3,7 @@ import { useChatContextSelector } from '../../ChatContext';
import { ChatMessageBlock } from './ChatMessageBlock';
import { ChatInput } from './ChatInput';
import { createStyles } from 'antd-style';
import { useBusterNewChatContextSelector } from '@/context/Chats';
export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElement> }> =
React.memo(({ chatContentRef }) => {

View File

@ -0,0 +1,14 @@
import React from 'react';
import { Text } from '@/components/text';
export const AIWarning = React.memo(() => {
return (
<div className="w-full overflow-hidden truncate text-center">
<Text size="xs" type="tertiary" className="truncate">
Our AI may make mistakes. Check important info.
</Text>
</div>
);
});
AIWarning.displayName = 'AIWarning';

View File

@ -1,34 +1,36 @@
import React, { useMemo, useState } from 'react';
import { Input, Button } from 'antd';
import { Text } from '@/components/text';
import { Input } from 'antd';
import { createStyles } from 'antd-style';
import { useMemoizedFn } from 'ahooks';
import { AppMaterialIcons } from '@/components/icons';
import { inputHasText } from '@/utils';
import { useBusterNewChatContextSelector } from '@/context/Chats';
import { AIWarning } from './AIWarning';
import { SubmitButton } from './SubmitButton';
import { useChatInputFlow } from './useChatInputFlow';
import { useChatContextSelector } from '../../../ChatContext';
const autoSize = { minRows: 3, maxRows: 4 };
export const ChatInput: React.FC = React.memo(() => {
export const ChatInput: React.FC<{}> = React.memo(({}) => {
const { styles, cx } = useStyles();
const [inputValue, setInputValue] = useState('');
const loading = useBusterNewChatContextSelector((state) => state.loadingNewChat);
const selectedFileId = useChatContextSelector((x) => x.selectedFileId);
const loading = false;
const [inputValue, setInputValue] = useState('');
const [isFocused, setIsFocused] = React.useState(false);
const disableSendButton = useMemo(() => {
return !inputHasText(inputValue);
}, [inputValue]);
const onSubmit = useMemoizedFn(async () => {
if (disableSendButton) return;
const { onSubmitPreflight } = useChatInputFlow({
disableSendButton,
inputValue
});
const disableSubmit = !inputHasText(inputValue);
const [isFocused, setIsFocused] = React.useState(false);
const onPressEnter = useMemoizedFn((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.metaKey && e.key === 'Enter') {
onSubmit();
return;
onSubmitPreflight();
}
});
@ -70,10 +72,10 @@ export const ChatInput: React.FC = React.memo(() => {
autoSize={autoSize}
/>
<div className="absolute bottom-2 right-2">
<Button
shape="circle"
disabled={disableSendButton}
icon={<AppMaterialIcons icon="arrow_upward" />}
<SubmitButton
disableSendButton={disableSendButton}
loading={loading}
onSubmitPreflight={onSubmitPreflight}
/>
</div>
</div>
@ -103,15 +105,3 @@ const useStyles = createStyles(({ token, css }) => ({
}
`
}));
const AIWarning = React.memo(() => {
return (
<div className="w-full overflow-hidden truncate text-center">
<Text size="xs" type="tertiary" className="truncate">
Our AI may make mistakes. Check important info.
</Text>
</div>
);
});
AIWarning.displayName = 'AIWarning';

View File

@ -0,0 +1,125 @@
import React, { useState } from 'react';
import { AppMaterialIcons } from '@/components/icons';
import { createStyles } from 'antd-style';
import { AnimatePresence, motion } from 'framer-motion';
import { AppTooltip } from '@/components';
interface SubmitButtonProps {
disableSendButton: boolean;
loading: boolean;
onSubmitPreflight: () => void;
}
const animationIcon = {
initial: { opacity: 0, scale: 0.8 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0.8 }
};
export const SubmitButton: React.FC<SubmitButtonProps> = React.memo(
({ disableSendButton, onSubmitPreflight }) => {
const { styles } = useStyles();
const [loading, setLoading] = useState(false);
const onTest = () => {
setLoading(!loading);
};
const tooltipText = loading ? 'Stop' : 'Send message';
const tooltipShortcuts = loading ? [] : ['⌘', '↵'];
return (
<AppTooltip title={tooltipText} shortcuts={tooltipShortcuts} mouseEnterDelay={1}>
<button
onClick={onTest}
disabled={disableSendButton}
className={`${styles.button} ${loading ? styles.loading : ''} ${
disableSendButton ? styles.disabled : ''
}`}>
<AnimatePresence mode="wait">
{loading ? (
<motion.div key="loading" {...animationIcon} className={styles.iconWrapper}>
<AppMaterialIcons icon="stop" fill size={16} />
</motion.div>
) : (
<motion.div key="arrow" {...animationIcon} className={styles.iconWrapper}>
<AppMaterialIcons icon="arrow_upward" size={16} />
</motion.div>
)}
</AnimatePresence>
</button>
</AppTooltip>
);
}
);
SubmitButton.displayName = 'SubmitButton';
const useStyles = createStyles(({ token, css }) => ({
button: css`
width: 26px;
height: 26px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
background: ${token.colorBgContainer};
border: 0.5px solid ${token.colorBorder};
padding: 0;
outline: none;
.material-symbols {
transition: color 0.2s ease;
}
&:not(:disabled):hover {
border-color: ${token.colorPrimary};
transform: scale(1.075);
box-shadow: ${token.boxShadowTertiary};
}
&:not(:disabled):active {
transform: scale(0.95);
}
&:disabled {
cursor: not-allowed;
}
`,
disabled: css`
background: transparent;
border: 0.5px solid ${token.colorBorderSecondary};
.material-symbols {
color: ${token.colorTextTertiary} !important;
}
`,
loading: css`
background: ${token.colorText};
border: 0.5px solid ${token.colorBorder};
color: ${token.colorBgLayout};
&:hover {
background: ${token.colorTextSecondary};
border-color: ${token.colorTextSecondary} !important;
}
.material-symbols {
color: ${token.colorBgLayout} !important;
}
`,
iconWrapper: css`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.material-symbols {
color: ${token.colorText};
}
`
}));

View File

@ -0,0 +1,61 @@
import { useMemo } from 'react';
import { useChatContextSelector } from '../../../ChatContext';
import { useMemoizedFn } from 'ahooks';
import { useBusterNewChatContextSelector } from '@/context/Chats';
type FlowType = 'followup-chat' | 'followup-metric' | 'followup-dashboard' | 'new';
export const useChatInputFlow = ({
disableSendButton,
inputValue
}: {
disableSendButton: boolean;
inputValue: string;
}) => {
const hasChat = useChatContextSelector((x) => x.hasChat);
const selectedFileType = useChatContextSelector((x) => x.selectedFileType);
const selectedFileId = useChatContextSelector((x) => x.selectedFileId);
const onStartNewChat = useBusterNewChatContextSelector((state) => state.onStartNewChat);
const onFollowUpChat = useBusterNewChatContextSelector((state) => state.onFollowUpChat);
const onStartChatFromFile = useBusterNewChatContextSelector((state) => state.onStartChatFromFile);
const currentMessageId = useChatContextSelector((x) => x.currentMessageId);
const flow: FlowType = useMemo(() => {
if (hasChat) return 'followup-chat';
if (selectedFileType === 'metric' && selectedFileId) return 'followup-metric';
if (selectedFileType === 'dashboard' && selectedFileId) return 'followup-dashboard';
return 'new';
}, [hasChat, selectedFileType, selectedFileId]);
const onSubmitPreflight = useMemoizedFn(async () => {
if (disableSendButton) return;
switch (flow) {
case 'followup-chat':
return onFollowUpChat({ prompt: inputValue, messageId: currentMessageId! });
case 'followup-metric':
return onStartChatFromFile({
prompt: inputValue,
fileId: selectedFileId!,
fileType: 'metric'
});
case 'followup-dashboard':
return onStartChatFromFile({
prompt: inputValue,
fileId: selectedFileId!,
fileType: 'dashboard'
});
case 'new':
return onStartNewChat(inputValue);
default:
const _exhaustiveCheck: never = flow;
return _exhaustiveCheck;
}
});
return { onSubmitPreflight };
};

View File

@ -23,18 +23,25 @@ export const useChatContext = ({
chatId,
metricId
});
const hasChat = !!chatId && !!chat;
const chatTitle = chat?.title;
const chatMessages = chat?.messages ?? [];
//FILE
const hasFile = !!defaultSelectedFile?.id;
//MESSAGES
const currentMessageId = chatMessages[chatMessages.length - 1]?.id;
return {
hasChat,
hasFile,
selectedFileId,
currentMessageId,
chatTitle,
selectedFileType,
chatMessages
chatMessages,
chatId
};
};

View File

@ -25,7 +25,7 @@ const TooltipShortcut: React.FC<{ shortcut: string }> = ({ shortcut }) => {
<div
className="relative flex justify-center"
style={{
lineHeight: 1,
lineHeight: 1.25,
fontSize: 12,
borderRadius: borderRadius,
border: `0.5px solid ${colorBorder}`,

View File

@ -84,74 +84,6 @@ export const useBusterChat = () => {
}
});
useHotkeys('f', () => {
const chatId = Object.keys(chatsRef.current)[0];
if (chatId) {
const chat = chatsRef.current[chatId];
const mockMessage = createMockResponseMessageFile();
const newChat = { ...chat };
const firstMessage = {
...newChat.messages[0],
isCompletedStream: false,
response_messages: [...newChat.messages[0].response_messages, mockMessage]
};
newChat.messages = [firstMessage];
chatsRef.current[chatId] = newChat;
startTransition(() => {
//just used to trigger UI update
});
}
});
useHotkeys('r', () => {
const chatId = Object.keys(chatsRef.current)[0];
if (chatId) {
const chat = chatsRef.current[chatId];
const mockMessage = createMockResponseMessageText();
const newChat = { ...chat };
const firstMessage = {
...newChat.messages[0],
isCompletedStream: false,
response_messages: [...newChat.messages[0].response_messages, mockMessage]
};
newChat.messages = [firstMessage];
chatsRef.current[chatId] = newChat;
startTransition(() => {
//just used to trigger UI update
});
}
});
useHotkeys('h', () => {
const chatId = Object.keys(chatsRef.current)[0];
if (chatId) {
const chat = chatsRef.current[chatId];
const newChat = { ...chat };
const firstMessage = newChat.messages[0];
const updatedResponseMessages = firstMessage.response_messages.map((msg) => {
if (msg.type === 'thought') {
return {
...msg,
hidden: true
};
}
return msg;
});
const updatedMessage = {
...firstMessage,
response_messages: updatedResponseMessages
};
newChat.messages = [updatedMessage];
chatsRef.current[chatId] = newChat;
startTransition(() => {
// Trigger UI update
});
}
});
return {
chats: chatsRef.current,
unsubscribeFromChat,

View File

@ -5,13 +5,13 @@ import {
useContextSelector
} from '@fluentui/react-context-selector';
import { useMemoizedFn } from 'ahooks';
import type { BusterDatasetListItem, BusterSearchResult } from '@/api/asset_interfaces';
import type { BusterDatasetListItem, BusterSearchResult, FileType } from '@/api/asset_interfaces';
export const useBusterNewChat = () => {
const [prompt, setPrompt] = useState('');
const [selectedChatDataSource, setSelectedChatDataSource] =
useState<BusterDatasetListItem | null>(null);
const [loadingNewChat, setLoadingNewChat] = useState(false);
const [prompt, setPrompt] = useState('');
const onSetPrompt = useMemoizedFn((prompt: string) => {
setPrompt(prompt);
@ -25,18 +25,37 @@ export const useBusterNewChat = () => {
setLoadingNewChat(false);
});
const onStartChatFromFile = useMemoizedFn(
async ({}: { prompt: string; fileId: string; fileType: FileType }) => {}
);
const onFollowUpChat = useMemoizedFn(
async ({ prompt, messageId }: { prompt: string; messageId: string }) => {
setLoadingNewChat(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoadingNewChat(false);
}
);
const onReplaceMessageInChat = useMemoizedFn(
async ({ prompt, messageId }: { prompt: string; messageId: string }) => {}
);
const onSetSelectedChatDataSource = useMemoizedFn((dataSource: BusterDatasetListItem | null) => {
setSelectedChatDataSource(dataSource);
});
return {
onStartNewChat,
prompt,
onSetPrompt,
loadingNewChat,
onSelectSearchAsset,
selectedChatDataSource,
onSetSelectedChatDataSource
onSetSelectedChatDataSource,
onSetPrompt,
onFollowUpChat,
prompt,
onStartChatFromFile,
onReplaceMessageInChat
};
};