mirror of https://github.com/buster-so/buster.git
make submit button a little better
This commit is contained in:
parent
e02ac8e35f
commit
3977d7bb34
|
@ -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;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
`
|
||||
}));
|
|
@ -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 };
|
||||
};
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue