From 9bb599435f399a573b5284c91f7c2caadeef0db9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 10 Aug 2026 15:05:22 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=8E=20fix:=20Re-enable=20Send=20After?= =?UTF-8?q?=20File=20Upload=20(#14727)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: enable composer send after file upload * style: sort composer imports --- client/src/components/Chat/Input/ChatForm.tsx | 7 ++--- .../Files/__tests__/useFileHandling.test.ts | 18 ++++++++++++ client/src/hooks/Files/useFileHandling.ts | 1 + client/src/utils/files.spec.ts | 29 ++++++++++++++++++- client/src/utils/files.ts | 9 ++++++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index a143f8767b..4ff0927d91 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -22,10 +22,10 @@ import { useAddedChatContext, useAssistantsMapContext, } from '~/Providers'; +import { cn, getModelSpec, hasIncompleteFiles, removeFocusRings } from '~/utils'; import PendingManualSkillsChips from './PendingManualSkillsChips'; import useAskAnswerMode from '~/hooks/Input/useAskAnswerMode'; import AskUserQuestionPopover from './AskUserQuestionPopover'; -import { cn, getModelSpec, removeFocusRings } from '~/utils'; import InterruptSteerButton from './InterruptSteerButton'; import DuringRunSendButton from './DuringRunSendButton'; import { useGetStartupConfig } from '~/data-provider'; @@ -59,7 +59,6 @@ interface ChatFormProps { setFiles: FileSetter; conversation: TConversation | null; isSubmitting: boolean; - filesLoading: boolean; setFilesLoading: React.Dispatch>; newConversation: ConvoGenerator; handleStopGenerating: (e: React.MouseEvent) => void; @@ -73,7 +72,6 @@ const ChatForm = memo(function ChatForm({ setFiles, conversation, isSubmitting, - filesLoading, setFilesLoading, newConversation, handleStopGenerating, @@ -123,6 +121,7 @@ const ChatForm = memo(function ChatForm({ [conversation?.spec, startupConfig], ); const hideBadgeRow = modelSpec?.hideBadgeRow === true; + const filesLoading = useMemo(() => hasIncompleteFiles(files), [files]); const conversationId = useMemo( () => conversation?.conversationId ?? Constants.NEW_CONVO, [conversation?.conversationId], @@ -726,7 +725,6 @@ function ChatFormWrapper({ index = 0, placeholder }: { index?: number; placehold setFiles, conversation, isSubmitting, - filesLoading, setFilesLoading, newConversation, handleStopGenerating, @@ -785,7 +783,6 @@ function ChatFormWrapper({ index = 0, placeholder }: { index?: number; placehold setFiles={setFiles} conversation={stableConversation} isSubmitting={isSubmitting} - filesLoading={filesLoading} setFilesLoading={setFilesLoading} newConversation={stableNewConversation} handleStopGenerating={stableHandleStop} diff --git a/client/src/hooks/Files/__tests__/useFileHandling.test.ts b/client/src/hooks/Files/__tests__/useFileHandling.test.ts index d7bd6eb713..efb4defcab 100644 --- a/client/src/hooks/Files/__tests__/useFileHandling.test.ts +++ b/client/src/hooks/Files/__tests__/useFileHandling.test.ts @@ -126,6 +126,24 @@ describe('useFileHandling', () => { const loadHook = async () => (await import('../useFileHandling')).default; describe('endpointOverride', () => { + it('clears the loading state when file validation throws', async () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined); + mockValidateFiles.mockImplementationOnce(() => { + throw new Error('invalid file config'); + }); + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => useFileHandling()); + + await act(async () => { + await result.current.handleFiles([new File(['hello'], 'test.txt', { type: 'text/plain' })]); + }); + + expect(mockSetFilesLoading).toHaveBeenCalledWith(false); + expect(mockMutate).not.toHaveBeenCalled(); + consoleError.mockRestore(); + }); + it('uploads non-HEIC images without running HEIC conversion', async () => { const useFileHandling = await loadHook(); const { result } = renderHook(() => useFileHandling()); diff --git a/client/src/hooks/Files/useFileHandling.ts b/client/src/hooks/Files/useFileHandling.ts index 2608a45e11..ec67c20686 100644 --- a/client/src/hooks/Files/useFileHandling.ts +++ b/client/src/hooks/Files/useFileHandling.ts @@ -306,6 +306,7 @@ const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: Fil } catch (error) { console.error('file validation error', error); setError('com_error_files_validation'); + setFilesLoading(false); return; } if (!filesAreValid) { diff --git a/client/src/utils/files.spec.ts b/client/src/utils/files.spec.ts index 49c150b2bf..690c83a504 100644 --- a/client/src/utils/files.spec.ts +++ b/client/src/utils/files.spec.ts @@ -1,4 +1,31 @@ -import { normalizeExportFilename } from './files'; +import type { ExtendedFile } from '~/common'; +import { hasIncompleteFiles, normalizeExportFilename } from './files'; + +describe('hasIncompleteFiles', () => { + const createFile = (file_id: string, progress: number): ExtendedFile => ({ + file_id, + progress, + size: 1, + }); + + it('returns true while any attachment is still uploading', () => { + const files = new Map([ + ['complete', createFile('complete', 1)], + ['uploading', createFile('uploading', 0.9)], + ]); + + expect(hasIncompleteFiles(files)).toBe(true); + }); + + it('returns false as soon as every attachment is complete', () => { + const files = new Map([ + ['first', createFile('first', 1)], + ['second', createFile('second', 1)], + ]); + + expect(hasIncompleteFiles(files)).toBe(false); + }); +}); describe('normalizeExportFilename', () => { it('replaces every whitespace run with a single underscore', () => { diff --git a/client/src/utils/files.ts b/client/src/utils/files.ts index d575f919b4..8c3d2bc892 100644 --- a/client/src/utils/files.ts +++ b/client/src/utils/files.ts @@ -27,6 +27,15 @@ import type { ExtendedFile } from '~/common'; export const partialTypes = ['text/x-']; +export function hasIncompleteFiles(files: Map): boolean { + for (const file of files.values()) { + if (file.progress < 1) { + return true; + } + } + return false; +} + const textDocument = { paths: TextPaths, fill: '#FF5588',