mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
style: update file upload handling and improve component structure across various files
This commit is contained in:
parent
1874189f05
commit
1c648b3104
10 changed files with 127 additions and 22 deletions
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
1
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
116
packages/client/src/components/FileInput.spec.tsx
Normal file
116
packages/client/src/components/FileInput.spec.tsx
Normal 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' });
|
||||
});
|
||||
});
|
||||
|
|
@ -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: '*',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
export { FILE_TYPE_MAP } from './FileInput';
|
||||
export type { FileType } from './FileInput';
|
||||
|
||||
export * from './Accordion';
|
||||
export * from './AnimatedTabs';
|
||||
export * from './AlertDialog';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue