From 1c648b3104fe022dd57739868b1d0f3f7ad88f2b Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:26:00 +0100 Subject: [PATCH] style: update file upload handling and improve component structure across various files --- .../Chat/Input/Files/AttachFileMenu.tsx | 7 +- .../SidePanel/Agents/Code/ApiKeyDialog.tsx | 4 +- .../components/SidePanel/Builder/Images.tsx | 2 +- client/src/locales/en/translation.json | 1 - package-lock.json | 1 - .../client/src/components/AlertDialog.tsx | 2 +- .../client/src/components/FileInput.spec.tsx | 116 ++++++++++++++++++ packages/client/src/components/FileInput.tsx | 8 +- packages/client/src/components/FileUpload.tsx | 5 +- packages/client/src/components/index.ts | 3 - 10 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 packages/client/src/components/FileInput.spec.tsx diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 134a7cc876..531e4d239b 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -21,6 +21,7 @@ import { EToolResources, EModelEndpoint, defaultAgentCapabilities, + bedrockDocumentExtensions, isDocumentSupportedProvider, } from 'librechat-data-provider'; import type { EndpointFileConfig } from 'librechat-data-provider'; @@ -39,7 +40,7 @@ import { ephemeralAgentByConvoId } from '~/store'; import { MenuItemProps } from '~/common'; import { cn } from '~/utils'; -type FileUploadType = FileType; +type FileUploadType = FileType | 'image_document_extended'; interface AttachFileMenuProps { agentId?: string | null; @@ -95,7 +96,9 @@ const AttachFileMenu = ({ return; } inputRef.current.value = ''; - if (fileType && fileType in FILE_TYPE_MAP) { + if (fileType === 'image_document_extended') { + inputRef.current.accept = `image/*,.heif,.heic,${bedrockDocumentExtensions}`; + } else if (fileType && fileType in FILE_TYPE_MAP) { inputRef.current.accept = FILE_TYPE_MAP[fileType]; } else { inputRef.current.accept = ''; diff --git a/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx b/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx index 282f17b0d3..22e1e9bd8c 100644 --- a/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx +++ b/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx @@ -93,7 +93,7 @@ export default function ApiKeyDialog({ {localize('com_ui_librechat_code_api_key')} -
+ )} - diff --git a/client/src/components/SidePanel/Builder/Images.tsx b/client/src/components/SidePanel/Builder/Images.tsx index ac643009a6..7dad90115c 100644 --- a/client/src/components/SidePanel/Builder/Images.tsx +++ b/client/src/components/SidePanel/Builder/Images.tsx @@ -111,7 +111,7 @@ export function AvatarMenu({ data-orientation="vertical" onClick={onItemClick} > - {localize('com_ui_upload_image')} + {localize('com_ui_upload_image_input')} {/* ) => (
); diff --git a/packages/client/src/components/FileInput.spec.tsx b/packages/client/src/components/FileInput.spec.tsx new file mode 100644 index 0000000000..78bd94cd33 --- /dev/null +++ b/packages/client/src/components/FileInput.spec.tsx @@ -0,0 +1,116 @@ +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { FileInput, FILE_TYPE_MAP } from './FileInput'; +import type { FileType } from './FileInput'; + +describe('FILE_TYPE_MAP', () => { + it('contains all expected file types', () => { + const expectedTypes: FileType[] = [ + 'image', + 'document', + 'video', + 'audio', + 'image_document', + 'image_document_video_audio', + 'all', + ]; + for (const type of expectedTypes) { + expect(FILE_TYPE_MAP).toHaveProperty(type); + expect(typeof FILE_TYPE_MAP[type]).toBe('string'); + } + }); + + it('maps image type to image wildcards with HEIF/HEIC', () => { + expect(FILE_TYPE_MAP.image).toBe('image/*,.heif,.heic'); + }); + + it('maps document type to PDF and office extensions', () => { + expect(FILE_TYPE_MAP.document).toContain('.pdf'); + expect(FILE_TYPE_MAP.document).toContain('application/pdf'); + expect(FILE_TYPE_MAP.document).toContain('.doc'); + expect(FILE_TYPE_MAP.document).toContain('.xlsx'); + }); + + it('maps image_document to combined image and PDF types', () => { + expect(FILE_TYPE_MAP.image_document).toContain('image/*'); + expect(FILE_TYPE_MAP.image_document).toContain('application/pdf'); + }); + + it('maps image_document_video_audio to all media types', () => { + const value = FILE_TYPE_MAP.image_document_video_audio; + expect(value).toContain('image/*'); + expect(value).toContain('video/*'); + expect(value).toContain('audio/*'); + expect(value).toContain('application/pdf'); + }); + + it('maps all to wildcard', () => { + expect(FILE_TYPE_MAP.all).toBe('*'); + }); +}); + +describe('FileInput', () => { + it('renders an input with type="file"', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).toHaveAttribute('type', 'file'); + }); + + it('sets accept from a predefined type', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).toHaveAttribute('accept', FILE_TYPE_MAP.image); + }); + + it('sets accept from multiple predefined types', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).toHaveAttribute('accept', `${FILE_TYPE_MAP.image},${FILE_TYPE_MAP.document}`); + }); + + it('passes through custom MIME types', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).toHaveAttribute('accept', 'image/png,application/json'); + }); + + it('mixes predefined and custom types', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).toHaveAttribute('accept', `${FILE_TYPE_MAP.image},application/json`); + }); + + it('omits accept when no types provided', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).not.toHaveAttribute('accept'); + }); + + it('defaults multiple to false', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).not.toHaveAttribute('multiple'); + }); + + it('sets multiple when specified', () => { + const { container } = render(); + const input = container.querySelector('input'); + expect(input).toHaveAttribute('multiple'); + }); + + it('forwards ref to input element', () => { + const ref = { current: null as HTMLInputElement | null }; + render(); + expect(ref.current).toBeInstanceOf(HTMLInputElement); + expect(ref.current?.type).toBe('file'); + }); + + it('passes through additional HTML attributes', () => { + const { container } = render( + , + ); + const input = container.querySelector('input'); + expect(input).toHaveAttribute('data-testid', 'file-input'); + expect(input).toHaveStyle({ display: 'none' }); + }); +}); diff --git a/packages/client/src/components/FileInput.tsx b/packages/client/src/components/FileInput.tsx index 1bdfe50990..bbe3ee9e82 100644 --- a/packages/client/src/components/FileInput.tsx +++ b/packages/client/src/components/FileInput.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; export type FileType = | 'image' | 'image_document' - | 'image_document_extended' | 'image_document_video_audio' | 'document' | 'video' @@ -27,10 +26,7 @@ export interface FileInputProps * @example ['image/png', 'application/pdf'] */ acceptTypes?: (FileType | string)[]; - /** - * Whether to allow multiple files to be selected - * @default false - */ + /** Whether to allow multiple files to be selected (defaults to false, matching native ``) */ multiple?: boolean; } @@ -44,8 +40,6 @@ const FILE_TYPE_MAP: Record = { video: 'video/*', audio: 'audio/*', image_document: 'image/*,.heif,.heic,.pdf,application/pdf', - image_document_extended: - 'image/*,.heif,.heic,.pdf,.csv,.doc,.docx,.xls,.xlsx,.html,.htm,.txt,.md,application/pdf,text/csv,application/csv,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/html,text/plain,text/markdown', image_document_video_audio: 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*', all: '*', }; diff --git a/packages/client/src/components/FileUpload.tsx b/packages/client/src/components/FileUpload.tsx index 6c2a2f86b4..235e66906c 100644 --- a/packages/client/src/components/FileUpload.tsx +++ b/packages/client/src/components/FileUpload.tsx @@ -13,10 +13,7 @@ type FileUploadProps = { * @example ['image', 'document'] */ acceptTypes?: (FileType | string)[]; - /** - * Whether to allow multiple files to be selected - * @default true - */ + /** Whether to allow multiple files to be selected (defaults to true, unlike FileInput which defaults to false) */ multiple?: boolean; }; diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts index 4198ea6317..e69b85d4cf 100644 --- a/packages/client/src/components/index.ts +++ b/packages/client/src/components/index.ts @@ -1,6 +1,3 @@ -export { FILE_TYPE_MAP } from './FileInput'; -export type { FileType } from './FileInput'; - export * from './Accordion'; export * from './AnimatedTabs'; export * from './AlertDialog';