diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 181d219c08..0b10e6c712 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useMemo } from 'react'; +import React, { useRef, useState, useMemo, useCallback } from 'react'; import { useRecoilState } from 'recoil'; import * as Ariakit from '@ariakit/react'; import { @@ -19,6 +19,7 @@ import { Providers, EToolResources, EModelEndpoint, + isPermissiveMimeConfig, defaultAgentCapabilities, bedrockDocumentExtensions, isDocumentSupportedProvider, @@ -110,27 +111,35 @@ const AttachFileMenu = ({ ephemeralAgent, ); - const handleUploadClick = (fileType?: FileUploadType) => { - if (!inputRef.current) { - return; - } - inputRef.current.value = ''; - if (fileType === 'image') { - inputRef.current.accept = 'image/*,.heif,.heic'; - } else if (fileType === 'document') { - inputRef.current.accept = '.pdf,application/pdf'; - } else if (fileType === 'image_document') { - inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf'; - } else if (fileType === 'image_document_extended') { - inputRef.current.accept = `image/*,.heif,.heic,${bedrockDocumentExtensions}`; - } else if (fileType === 'image_document_video_audio') { - inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*'; - } else { + const handleUploadClick = useCallback( + (fileType?: FileUploadType) => { + if (!inputRef.current) { + return; + } + inputRef.current.value = ''; + if ( + fileType !== undefined && + isPermissiveMimeConfig(endpointFileConfig?.supportedMimeTypes) + ) { + inputRef.current.accept = ''; + } else if (fileType === 'image') { + inputRef.current.accept = 'image/*,.heif,.heic'; + } else if (fileType === 'document') { + inputRef.current.accept = '.pdf,application/pdf'; + } else if (fileType === 'image_document') { + inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf'; + } else if (fileType === 'image_document_extended') { + inputRef.current.accept = `image/*,.heif,.heic,${bedrockDocumentExtensions}`; + } else if (fileType === 'image_document_video_audio') { + inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*'; + } else { + inputRef.current.accept = ''; + } + inputRef.current.click(); inputRef.current.accept = ''; - } - inputRef.current.click(); - inputRef.current.accept = ''; - }; + }, + [endpointFileConfig?.supportedMimeTypes], + ); const dropdownItems = useMemo(() => { const createMenuItems = (onAction: (fileType?: FileUploadType) => void) => { @@ -247,6 +256,7 @@ const AttachFileMenu = ({ endpointType, capabilities, useResponsesApi, + handleUploadClick, setToolResource, setEphemeralAgent, sharePointEnabled, diff --git a/packages/data-provider/src/file-config.spec.ts b/packages/data-provider/src/file-config.spec.ts index 96f48621cc..ee894712ff 100644 --- a/packages/data-provider/src/file-config.spec.ts +++ b/packages/data-provider/src/file-config.spec.ts @@ -1,6 +1,8 @@ import type { FileConfig } from './types/files'; import { fileConfig as baseFileConfig, + isPermissiveMimeConfig, + convertStringsToRegex, documentParserMimeTypes, getEndpointFileConfig, applicationMimeTypes, @@ -1246,3 +1248,62 @@ describe('getEndpointFileConfig', () => { }); }); }); + +describe('isPermissiveMimeConfig', () => { + it('returns true for wildcard .* pattern', () => { + expect(isPermissiveMimeConfig([/.*/])).toBe(true); + }); + + it('returns true for .+ pattern', () => { + expect(isPermissiveMimeConfig([/.+/])).toBe(true); + }); + + it('returns true for anchored ^.*$ pattern', () => { + expect(isPermissiveMimeConfig([/^.*$/])).toBe(true); + }); + + it('returns true for anchored ^.+$ pattern', () => { + expect(isPermissiveMimeConfig([/^.+$/])).toBe(true); + }); + + it('returns true when at least one pattern is permissive', () => { + expect(isPermissiveMimeConfig([/^image\/.*$/, /.*/])).toBe(true); + }); + + it('returns false for image-only patterns', () => { + expect(isPermissiveMimeConfig([/^image\/(jpeg|png)$/])).toBe(false); + }); + + it('returns false for category patterns', () => { + expect(isPermissiveMimeConfig([/^image\/.*$/, /^text\/.*$/])).toBe(false); + }); + + it('returns false for specific MIME type patterns', () => { + expect(isPermissiveMimeConfig([/^application\/pdf$/])).toBe(false); + }); + + it('returns false for broad application category pattern', () => { + expect(isPermissiveMimeConfig([/^application\/.*$/])).toBe(false); + }); + + it('returns false for multi-category pattern', () => { + expect(isPermissiveMimeConfig([/^(application|text)\/.*$/])).toBe(false); + }); + + it('returns false for default supportedMimeTypes', () => { + expect(isPermissiveMimeConfig(supportedMimeTypes)).toBe(false); + }); + + it('returns false for undefined', () => { + expect(isPermissiveMimeConfig(undefined)).toBe(false); + }); + + it('returns false for empty array', () => { + expect(isPermissiveMimeConfig([])).toBe(false); + }); + + it('returns true for regex produced by convertStringsToRegex with .*', () => { + const converted = convertStringsToRegex(['.*']); + expect(isPermissiveMimeConfig(converted)).toBe(true); + }); +}); diff --git a/packages/data-provider/src/file-config.ts b/packages/data-provider/src/file-config.ts index 7ec184755d..a93daf79e2 100644 --- a/packages/data-provider/src/file-config.ts +++ b/packages/data-provider/src/file-config.ts @@ -497,6 +497,14 @@ export const convertStringsToRegex = (patterns: string[]): RegExp[] => return acc; }, []); +/** Detects whether the given MIME type patterns accept all file types (e.g., `.*` or `.+`). */ +export const isPermissiveMimeConfig = (types?: RegExp[]): boolean => { + if (!types || types.length === 0) { + return false; + } + return types.some((regex) => regex.test('x-librechat/x-probe')); +}; + /** * Gets the appropriate endpoint file configuration with standardized lookup logic. *