Merge pull request #444 from buster-so/big-nate/bus-1327-prevent-spam-on-button-click

Big nate/bus 1327 prevent spam on button click
This commit is contained in:
Nate Kelley 2025-07-08 14:33:50 -06:00 committed by GitHub
commit 356e870145
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 55 additions and 22 deletions

View File

@ -16,7 +16,7 @@ import { useAppLayoutContextSelector } from '../BusterAppLayout';
import { BusterRoutes } from '@/routes/busterRoutes'; import { BusterRoutes } from '@/routes/busterRoutes';
export const useBusterNewChat = () => { export const useBusterNewChat = () => {
const { mutateAsync: startNewChat } = useStartNewChat(); const { mutateAsync: startNewChat, isPending: isSubmittingChat } = useStartNewChat();
const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage); const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage);
const getChatMessageMemoized = useGetChatMessageMemoized(); const getChatMessageMemoized = useGetChatMessageMemoized();
const getChatMemoized = useGetChatMemoized(); const getChatMemoized = useGetChatMemoized();
@ -57,7 +57,6 @@ export const useBusterNewChat = () => {
message_id: messageId message_id: messageId
}); });
initializeNewChat(res); initializeNewChat(res);
} }
); );
@ -149,7 +148,8 @@ export const useBusterNewChat = () => {
onFollowUpChat, onFollowUpChat,
onStartChatFromFile, onStartChatFromFile,
onReplaceMessageInChat, onReplaceMessageInChat,
onStopChat onStopChat,
isSubmittingChat
}; };
}; };

View File

@ -8,6 +8,13 @@ import { NewChatInput } from './NewChatInput';
import { NewChatWarning } from './NewChatWarning'; import { NewChatWarning } from './NewChatWarning';
import { useNewChatWarning } from './useNewChatWarning'; import { useNewChatWarning } from './useNewChatWarning';
enum TimeOfDay {
MORNING = 'morning',
AFTERNOON = 'afternoon',
EVENING = 'evening',
NIGHT = 'night'
}
export const HomePageController: React.FC<Record<string, never>> = () => { export const HomePageController: React.FC<Record<string, never>> = () => {
const newChatWarningProps = useNewChatWarning(); const newChatWarningProps = useNewChatWarning();
const { showWarning } = newChatWarningProps; const { showWarning } = newChatWarningProps;
@ -15,18 +22,35 @@ export const HomePageController: React.FC<Record<string, never>> = () => {
const user = useUserConfigContextSelector((state) => state.user); const user = useUserConfigContextSelector((state) => state.user);
const userName = user?.name; const userName = user?.name;
const isMorning = useMemo(() => { const timeOfDay = useMemo(() => {
const now = new Date(); const now = new Date();
const hours = now.getHours(); const hours = now.getHours();
return hours < 12;
if (hours >= 5 && hours < 12) {
return TimeOfDay.MORNING;
} else if (hours >= 12 && hours < 17) {
return TimeOfDay.AFTERNOON;
} else if (hours >= 17 && hours < 21) {
return TimeOfDay.EVENING;
} else {
return TimeOfDay.NIGHT;
}
}, []); }, []);
const greeting = useMemo(() => { const greeting = useMemo(() => {
if (isMorning) { switch (timeOfDay) {
return `Good morning, ${userName}`; case TimeOfDay.MORNING:
return `Good morning, ${userName}`;
case TimeOfDay.AFTERNOON:
return `Good afternoon, ${userName}`;
case TimeOfDay.EVENING:
return `Good evening, ${userName}`;
case TimeOfDay.NIGHT:
return `Good night, ${userName}`;
default:
return `Hello, ${userName}`;
} }
return `Good afternoon, ${userName}`; }, [timeOfDay, userName]);
}, [userName]);
return ( return (
<div className="flex flex-col items-center justify-center p-4.5"> <div className="flex flex-col items-center justify-center p-4.5">

View File

@ -22,7 +22,7 @@ export const NewChatInput: React.FC<Record<string, never>> = () => {
}, [inputValue]); }, [inputValue]);
const onSubmit = useMemoizedFn(async (value: string) => { const onSubmit = useMemoizedFn(async (value: string) => {
if (disabledSubmit) return; if (disabledSubmit || loading) return;
try { try {
setLoading(true); setLoading(true);
const trimmedValue = value.trim(); const trimmedValue = value.trim();
@ -32,12 +32,6 @@ export const NewChatInput: React.FC<Record<string, never>> = () => {
} }
}); });
const onStop = useMemoizedFn(() => {
setLoading(false);
textAreaRef.current?.focus();
textAreaRef.current?.select();
});
const onChange = useMemoizedFn((e: ChangeEvent<HTMLTextAreaElement>) => { const onChange = useMemoizedFn((e: ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value); setInputValue(e.target.value);
}); });
@ -56,7 +50,6 @@ export const NewChatInput: React.FC<Record<string, never>> = () => {
autoResize={autoResizeConfig} autoResize={autoResizeConfig}
onSubmit={onSubmit} onSubmit={onSubmit}
onChange={onChange} onChange={onChange}
onStop={onStop}
loading={loading} loading={loading}
disabled={false} disabled={false}
disabledSubmit={disabledSubmit} disabledSubmit={disabledSubmit}

View File

@ -11,8 +11,9 @@ const autoResizeConfig = { minRows: 2, maxRows: 16 };
export const ChatInput: React.FC = React.memo(() => { export const ChatInput: React.FC = React.memo(() => {
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
const loading = useChatIndividualContextSelector((x) => x.isStreamingMessage); const isStreamingMessage = useChatIndividualContextSelector((x) => x.isStreamingMessage);
const hasChat = useChatIndividualContextSelector((x) => x.hasChat); const hasChat = useChatIndividualContextSelector((x) => x.hasChat);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const disableSubmit = useMemo(() => { const disableSubmit = useMemo(() => {
@ -23,7 +24,7 @@ export const ChatInput: React.FC = React.memo(() => {
disableSubmit, disableSubmit,
inputValue, inputValue,
setInputValue, setInputValue,
loading, loading: isStreamingMessage,
textAreaRef textAreaRef
}); });
@ -42,7 +43,7 @@ export const ChatInput: React.FC = React.memo(() => {
onSubmit={onSubmitPreflight} onSubmit={onSubmitPreflight}
onChange={onChange} onChange={onChange}
onStop={onStopChat} onStop={onStopChat}
loading={loading} loading={isStreamingMessage}
value={inputValue} value={inputValue}
disabled={!hasChat} disabled={!hasChat}
disabledSubmit={disableSubmit} disabledSubmit={disableSubmit}

View File

@ -1,5 +1,5 @@
import type React from 'react'; import type React from 'react';
import { useMemo } from 'react'; import { useMemo, useRef } from 'react';
import { useBusterNotifications } from '@/context/BusterNotifications'; import { useBusterNotifications } from '@/context/BusterNotifications';
import { useBusterNewChatContextSelector } from '@/context/Chats'; import { useBusterNewChatContextSelector } from '@/context/Chats';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
@ -27,6 +27,7 @@ export const useChatInputFlow = ({
const selectedFileId = useChatIndividualContextSelector((x) => x.selectedFileId); const selectedFileId = useChatIndividualContextSelector((x) => x.selectedFileId);
const onStartNewChat = useBusterNewChatContextSelector((state) => state.onStartNewChat); const onStartNewChat = useBusterNewChatContextSelector((state) => state.onStartNewChat);
const onFollowUpChat = useBusterNewChatContextSelector((state) => state.onFollowUpChat); const onFollowUpChat = useBusterNewChatContextSelector((state) => state.onFollowUpChat);
const isSubmittingChat = useBusterNewChatContextSelector((state) => state.isSubmittingChat);
const onStartChatFromFile = useBusterNewChatContextSelector((state) => state.onStartChatFromFile); const onStartChatFromFile = useBusterNewChatContextSelector((state) => state.onStartChatFromFile);
const onStopChatContext = useBusterNewChatContextSelector((state) => state.onStopChat); const onStopChatContext = useBusterNewChatContextSelector((state) => state.onStopChat);
const currentMessageId = useChatIndividualContextSelector((x) => x.currentMessageId); const currentMessageId = useChatIndividualContextSelector((x) => x.currentMessageId);
@ -34,6 +35,8 @@ export const useChatInputFlow = ({
const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal); const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal);
const { openConfirmModal } = useBusterNotifications(); const { openConfirmModal } = useBusterNotifications();
const submittingCooldown = useRef(isSubmittingChat);
const flow: FlowType = useMemo(() => { const flow: FlowType = useMemo(() => {
if (hasChat) return 'followup-chat'; if (hasChat) return 'followup-chat';
if (selectedFileType === 'metric' && selectedFileId) return 'followup-metric'; if (selectedFileType === 'metric' && selectedFileId) return 'followup-metric';
@ -42,7 +45,14 @@ export const useChatInputFlow = ({
}, [hasChat, selectedFileType, selectedFileId]); }, [hasChat, selectedFileType, selectedFileId]);
const onSubmitPreflight = useMemoizedFn(async () => { const onSubmitPreflight = useMemoizedFn(async () => {
if (disableSubmit || !chatId || !currentMessageId) return; if (
disableSubmit ||
!chatId ||
!currentMessageId ||
submittingCooldown.current ||
isSubmittingChat
)
return;
if (loading) { if (loading) {
onStopChat(); onStopChat();
@ -52,6 +62,7 @@ export const useChatInputFlow = ({
const trimmedInputValue = inputValue.trim(); const trimmedInputValue = inputValue.trim();
const method = async () => { const method = async () => {
submittingCooldown.current = true;
switch (flow) { switch (flow) {
case 'followup-chat': case 'followup-chat':
await onFollowUpChat({ prompt: trimmedInputValue, chatId }); await onFollowUpChat({ prompt: trimmedInputValue, chatId });
@ -89,6 +100,10 @@ export const useChatInputFlow = ({
setTimeout(() => { setTimeout(() => {
textAreaRef.current?.focus(); textAreaRef.current?.focus();
}, 50); }, 50);
setTimeout(() => {
submittingCooldown.current = false;
}, 350);
}; };
if (!isFileChanged) { if (!isFileChanged) {