📂 refactor: File Type Inference for Frontend File Validation (#10807)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled

- Introduced `inferMimeType` utility to improve MIME type detection for uploaded files, including support for HEIC and HEIF formats.
- Updated DragDropModal to utilize the new inference logic for validating file types, ensuring compatibility with various document upload providers.
- Added comprehensive tests for `inferMimeType` to cover various scenarios, including handling of unknown extensions and preserving browser-provided types.
This commit is contained in:
Danny Avila 2025-12-04 14:24:10 -05:00 committed by GitHub
parent 754b495fb8
commit f55bd6f99b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 101 additions and 19 deletions

View file

@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { OGDialog, OGDialogTemplate } from '@librechat/client';
import {
inferMimeType,
EToolResources,
EModelEndpoint,
defaultAgentCapabilities,
@ -56,18 +57,26 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
const _options: FileOption[] = [];
const currentProvider = provider || endpoint;
/** Helper to get inferred MIME type for a file */
const getFileType = (file: File) => inferMimeType(file.name, file.type);
// Check if provider supports document upload
if (isDocumentSupportedProvider(endpointType) || isDocumentSupportedProvider(currentProvider)) {
const isGoogleProvider = currentProvider === EModelEndpoint.google;
const validFileTypes = isGoogleProvider
? files.every(
(file) =>
file.type?.startsWith('image/') ||
file.type?.startsWith('video/') ||
file.type?.startsWith('audio/') ||
file.type === 'application/pdf',
)
: files.every((file) => file.type?.startsWith('image/') || file.type === 'application/pdf');
? files.every((file) => {
const type = getFileType(file);
return (
type?.startsWith('image/') ||
type?.startsWith('video/') ||
type?.startsWith('audio/') ||
type === 'application/pdf'
);
})
: files.every((file) => {
const type = getFileType(file);
return type?.startsWith('image/') || type === 'application/pdf';
});
_options.push({
label: localize('com_ui_upload_provider'),
@ -81,7 +90,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
label: localize('com_ui_upload_image_input'),
value: undefined,
icon: <ImageUpIcon className="icon-md" />,
condition: files.every((file) => file.type?.startsWith('image/')),
condition: files.every((file) => getFileType(file)?.startsWith('image/')),
});
}
if (capabilities.fileSearchEnabled && fileSearchAllowedByAgent) {

View file

@ -1,4 +1,8 @@
import { EModelEndpoint, isDocumentSupportedProvider } from 'librechat-data-provider';
import {
EModelEndpoint,
isDocumentSupportedProvider,
inferMimeType,
} from 'librechat-data-provider';
describe('DragDropModal - Provider Detection', () => {
describe('endpointType priority over currentProvider', () => {
@ -118,4 +122,59 @@ describe('DragDropModal - Provider Detection', () => {
).toBe(true);
});
});
describe('HEIC/HEIF file type inference', () => {
it('should infer image/heic for .heic files when browser returns empty type', () => {
const fileName = 'photo.heic';
const browserType = '';
const inferredType = inferMimeType(fileName, browserType);
expect(inferredType).toBe('image/heic');
});
it('should infer image/heif for .heif files when browser returns empty type', () => {
const fileName = 'photo.heif';
const browserType = '';
const inferredType = inferMimeType(fileName, browserType);
expect(inferredType).toBe('image/heif');
});
it('should handle uppercase .HEIC extension', () => {
const fileName = 'IMG_1234.HEIC';
const browserType = '';
const inferredType = inferMimeType(fileName, browserType);
expect(inferredType).toBe('image/heic');
});
it('should preserve browser-provided type when available', () => {
const fileName = 'photo.jpg';
const browserType = 'image/jpeg';
const inferredType = inferMimeType(fileName, browserType);
expect(inferredType).toBe('image/jpeg');
});
it('should not override browser type even if extension differs', () => {
const fileName = 'renamed.heic';
const browserType = 'image/png';
const inferredType = inferMimeType(fileName, browserType);
expect(inferredType).toBe('image/png');
});
it('should correctly identify HEIC as image type for upload options', () => {
const heicType = inferMimeType('photo.heic', '');
expect(heicType.startsWith('image/')).toBe(true);
});
it('should return empty string for unknown extension with no browser type', () => {
const fileName = 'file.xyz';
const browserType = '';
const inferredType = inferMimeType(fileName, browserType);
expect(inferredType).toBe('');
});
});
});

View file

@ -9,9 +9,9 @@ import {
import {
megabyte,
QueryKeys,
inferMimeType,
excelMimeTypes,
EToolResources,
codeTypeMapping,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import type { TFile, EndpointFileConfig, FileConfig } from 'librechat-data-provider';
@ -257,14 +257,7 @@ export const validateFiles = ({
for (let i = 0; i < fileList.length; i++) {
let originalFile = fileList[i];
let fileType = originalFile.type;
const extension = originalFile.name.split('.').pop() ?? '';
const knownCodeType = codeTypeMapping[extension];
// Infer MIME type for Known Code files when the type is empty or a mismatch
if (knownCodeType && (!fileType || fileType !== knownCodeType)) {
fileType = knownCodeType;
}
const fileType = inferMimeType(originalFile.name, originalFile.type);
// Check if the file type is still empty after the extension check
if (!fileType) {