style: update file upload handling and improve component structure across various files

This commit is contained in:
Marco Beretta 2026-03-04 22:26:00 +01:00
parent 1874189f05
commit 1c648b3104
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
10 changed files with 127 additions and 22 deletions

View file

@ -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 = '';

View file

@ -93,7 +93,7 @@ export default function ApiKeyDialog({
{localize('com_ui_librechat_code_api_key')}
</a>
<form onSubmit={handleSubmit(onSubmit)}>
<form id="api-key-form" onSubmit={handleSubmit(onSubmit)}>
<SecretInput
placeholder={localize('com_ui_enter_api_key')}
{...register('apiKey', { required: true })}
@ -117,7 +117,7 @@ export default function ApiKeyDialog({
{localize('com_ui_revoke')}
</Button>
)}
<Button variant="submit" onClick={handleSubmit(onSubmit)} className="h-10">
<Button type="submit" form="api-key-form" variant="submit" className="h-10">
{localize('com_ui_save')}
</Button>
</OGDialogFooter>

View file

@ -111,7 +111,7 @@ export function AvatarMenu({
data-orientation="vertical"
onClick={onItemClick}
>
{localize('com_ui_upload_image')}
{localize('com_ui_upload_image_input')}
</div>
{/* <Popover.Close
role="menuitem"

View file

@ -923,7 +923,6 @@
"com_ui_delete_tool": "Delete Tool",
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
"com_ui_delete_tool_save_reminder": "Tool removed. Save the agent to apply changes.",
"com_ui_delete_mcp_server": "Delete MCP Server?",
"com_ui_deleted": "Deleted",
"com_ui_deleting": "Deleting...",
"com_ui_deleting_file": "Deleting file...",

1
package-lock.json generated
View file

@ -44266,7 +44266,6 @@
"packages/client": {
"name": "@librechat/client",
"version": "0.4.54",
"version": "0.4.54",
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",

View file

@ -58,7 +58,7 @@ AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({ className = '', ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
className={cn('flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);

View file

@ -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(<FileInput />);
const input = container.querySelector('input');
expect(input).toHaveAttribute('type', 'file');
});
it('sets accept from a predefined type', () => {
const { container } = render(<FileInput acceptTypes={['image']} />);
const input = container.querySelector('input');
expect(input).toHaveAttribute('accept', FILE_TYPE_MAP.image);
});
it('sets accept from multiple predefined types', () => {
const { container } = render(<FileInput acceptTypes={['image', 'document']} />);
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(<FileInput acceptTypes={['image/png', 'application/json']} />);
const input = container.querySelector('input');
expect(input).toHaveAttribute('accept', 'image/png,application/json');
});
it('mixes predefined and custom types', () => {
const { container } = render(<FileInput acceptTypes={['image', 'application/json']} />);
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(<FileInput />);
const input = container.querySelector('input');
expect(input).not.toHaveAttribute('accept');
});
it('defaults multiple to false', () => {
const { container } = render(<FileInput />);
const input = container.querySelector('input');
expect(input).not.toHaveAttribute('multiple');
});
it('sets multiple when specified', () => {
const { container } = render(<FileInput multiple />);
const input = container.querySelector('input');
expect(input).toHaveAttribute('multiple');
});
it('forwards ref to input element', () => {
const ref = { current: null as HTMLInputElement | null };
render(<FileInput ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
expect(ref.current?.type).toBe('file');
});
it('passes through additional HTML attributes', () => {
const { container } = render(
<FileInput data-testid="file-input" style={{ display: 'none' }} />,
);
const input = container.querySelector('input');
expect(input).toHaveAttribute('data-testid', 'file-input');
expect(input).toHaveStyle({ display: 'none' });
});
});

View file

@ -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 `<input>`) */
multiple?: boolean;
}
@ -44,8 +40,6 @@ const FILE_TYPE_MAP: Record<FileType, string> = {
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: '*',
};

View file

@ -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;
};

View file

@ -1,6 +1,3 @@
export { FILE_TYPE_MAP } from './FileInput';
export type { FileType } from './FileInput';
export * from './Accordion';
export * from './AnimatedTabs';
export * from './AlertDialog';