From 68c3fc0ef649d7b12e433ab9e9ea0ae53e5586d2 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 25 Jun 2026 15:37:15 +0800 Subject: [PATCH] fix: disable submit button when prompt is empty --- .../chat/chat-input-area/__tests__/index.spec.tsx | 12 +++++++++--- .../base/chat/chat/chat-input-area/index.tsx | 10 +++++----- .../base/chat/chat/chat-input-area/operation.tsx | 6 ++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx index 36a9672f618..9bb0d99dfd7 100644 --- a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx @@ -533,14 +533,20 @@ describe('ChatInputArea', () => { // ------------------------------------------------------------------------- describe('Validation & Constraints', () => { - it('should notify and NOT send when query is blank', async () => { + it('should disable send when query is blank', async () => { const user = userEvent.setup({ delay: null }) const onSend = vi.fn() render() - await user.click(screen.getByRole('button', { name: 'common.operation.send' })) + const sendButton = screen.getByRole('button', { name: 'common.operation.send' }) + expect(sendButton).toBeDisabled() + + await user.click(sendButton) expect(onSend).not.toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'info' })) + expect(mockNotify).not.toHaveBeenCalled() + + await user.type(getTextarea()!, ' ') + expect(sendButton).toBeDisabled() }) it('should notify and NOT send while bot is responding', async () => { diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index 0966ab67f40..91858bc7eb6 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -54,6 +54,7 @@ const ChatInputArea = ({ readonly, botName, placeholder, showFeatureBar, showFil const { t } = useTranslation() const { wrapperRef, textareaRef, textValueRef, holdSpaceRef, handleTextareaResize, isMultipleLine } = useTextAreaHeight() const [query, setQuery] = useState('') + const canSend = !!query.trim() const [showVoiceInput, setShowVoiceInput] = useState(false) const filesStore = useFileStore() const { handleDragFileEnter, handleDragFileLeave, handleDragFileOver, handleDropFile, handleClipboardPasteFile, isDragActive } = useFile(visionConfig!, false) @@ -66,6 +67,9 @@ const ChatInputArea = ({ readonly, botName, placeholder, showFeatureBar, showFil setTimeout(handleTextareaResize, 0) }, [handleTextareaResize]) const handleSend = () => { + if (!canSend) + return + if (isResponding) { toast.info(t('errorMessage.waitForResponse', { ns: 'appDebug' })) return @@ -76,10 +80,6 @@ const ChatInputArea = ({ readonly, botName, placeholder, showFeatureBar, showFil toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' })) return } - if (!query || !query.trim()) { - toast.info(t('errorMessage.queryRequired', { ns: 'appAnnotation' })) - return - } if (checkInputsForm(inputs, inputsForm)) { onSend(query, files) handleQueryChange('') @@ -142,7 +142,7 @@ const ChatInputArea = ({ readonly, botName, placeholder, showFeatureBar, showFil toast.error(t('voiceInput.notAllow', { ns: 'common' })) }) }, [t]) - const operation = () + const operation = () return ( <>
diff --git a/web/app/components/base/chat/chat/chat-input-area/operation.tsx b/web/app/components/base/chat/chat/chat-input-area/operation.tsx index e9d60217f35..78f4ddcc942 100644 --- a/web/app/components/base/chat/chat/chat-input-area/operation.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/operation.tsx @@ -10,7 +10,6 @@ import { RiMicLine, RiSendPlane2Fill, } from '@remixicon/react' -import { noop } from 'es-toolkit/function' import { memo } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' @@ -23,6 +22,7 @@ type OperationProps = { onShowVoiceInput?: () => void onSend: () => void sendButtonLabel?: string + disabled?: boolean theme?: Theme | null ref?: Ref } @@ -34,6 +34,7 @@ const Operation: FC = ({ onShowVoiceInput, onSend, sendButtonLabel, + disabled, theme, }) => { const { t } = useTranslation() @@ -70,7 +71,8 @@ const Operation: FC = ({ sendButtonLabel ? 'px-3' : 'w-8 px-0', )} variant="primary" - onClick={readonly ? noop : onSend} + disabled={readonly || disabled} + onClick={onSend} style={ theme ? {