mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-25 12:48:53 +01:00
* refactor: Enhance FileContainer with customizable button and container styles, onClick button handling, and type override * refactor: Update file type handling to support partial file objects * refactor: Extract download handling into a custom hook for improved reusability * refactor: Replace LogContent with Stdout component and enhance Attachment rendering for added visibility * feat: Update @librechat/agents to version 2.4.1 for referencing generated files in subsequent code interpreter uses * feat: Add support for tab-separated values (TSV) in mime type handling and improve error logging for regex patterns * chore: Update @librechat/agents to version 2.4.11 for better `session_id` instructions when wanting to persist files between executions * chore: Update @librechat/agents to version 2.4.12 for improved functionality * fix: Enhance argument parsing in useParseArgs to support JSON input and improve code extraction * refactor: Update input handling in useAutoSave to require more than one character before saving to local storage
289 lines
7 KiB
TypeScript
289 lines
7 KiB
TypeScript
import {
|
|
megabyte,
|
|
QueryKeys,
|
|
excelMimeTypes,
|
|
codeTypeMapping,
|
|
fileConfig as defaultFileConfig,
|
|
} from 'librechat-data-provider';
|
|
import type { TFile, EndpointFileConfig } from 'librechat-data-provider';
|
|
import type { QueryClient } from '@tanstack/react-query';
|
|
import type { ExtendedFile } from '~/common';
|
|
import SheetPaths from '~/components/svg/Files/SheetPaths';
|
|
import TextPaths from '~/components/svg/Files/TextPaths';
|
|
import FilePaths from '~/components/svg/Files/FilePaths';
|
|
import CodePaths from '~/components/svg/Files/CodePaths';
|
|
|
|
export const partialTypes = ['text/x-'];
|
|
|
|
const textDocument = {
|
|
paths: TextPaths,
|
|
fill: '#FF5588',
|
|
title: 'Document',
|
|
};
|
|
|
|
const spreadsheet = {
|
|
paths: SheetPaths,
|
|
fill: '#10A37F',
|
|
title: 'Spreadsheet',
|
|
};
|
|
|
|
const codeFile = {
|
|
paths: CodePaths,
|
|
fill: '#FF6E3C',
|
|
// TODO: make this dynamic to the language
|
|
title: 'Code',
|
|
};
|
|
|
|
const artifact = {
|
|
paths: CodePaths,
|
|
fill: '#2D305C',
|
|
title: 'Code',
|
|
};
|
|
|
|
export const fileTypes = {
|
|
/* Category matches */
|
|
file: {
|
|
paths: FilePaths,
|
|
fill: '#0000FF',
|
|
title: 'File',
|
|
},
|
|
text: textDocument,
|
|
txt: textDocument,
|
|
// application:,
|
|
|
|
/* Partial matches */
|
|
csv: spreadsheet,
|
|
'application/pdf': textDocument,
|
|
pdf: textDocument,
|
|
'text/x-': codeFile,
|
|
artifact: artifact,
|
|
|
|
/* Exact matches */
|
|
// 'application/json':,
|
|
// 'text/html':,
|
|
// 'text/css':,
|
|
// image,
|
|
};
|
|
|
|
// export const getFileType = (type = '') => {
|
|
// let fileType = fileTypes.file;
|
|
// const exactMatch = fileTypes[type];
|
|
// const partialMatch = !exactMatch && partialTypes.find((type) => type.includes(type));
|
|
// const category = (!partialMatch && (type.split('/')[0] ?? 'text') || 'text');
|
|
|
|
// if (exactMatch) {
|
|
// fileType = exactMatch;
|
|
// } else if (partialMatch) {
|
|
// fileType = fileTypes[partialMatch];
|
|
// } else if (fileTypes[category]) {
|
|
// fileType = fileTypes[category];
|
|
// }
|
|
|
|
// if (!fileType) {
|
|
// fileType = fileTypes.file;
|
|
// }
|
|
|
|
// return fileType;
|
|
// };
|
|
|
|
export const getFileType = (
|
|
type = '',
|
|
): {
|
|
paths: React.FC;
|
|
fill: string;
|
|
title: string;
|
|
} => {
|
|
// Direct match check
|
|
if (fileTypes[type]) {
|
|
return fileTypes[type];
|
|
}
|
|
|
|
if (excelMimeTypes.test(type)) {
|
|
return spreadsheet;
|
|
}
|
|
|
|
// Partial match check
|
|
const partialMatch = partialTypes.find((partial) => type.includes(partial));
|
|
if (partialMatch && fileTypes[partialMatch]) {
|
|
return fileTypes[partialMatch];
|
|
}
|
|
|
|
// Category check
|
|
const category = type.split('/')[0] || 'text';
|
|
if (fileTypes[category]) {
|
|
return fileTypes[category];
|
|
}
|
|
|
|
// Default file type
|
|
return fileTypes.file;
|
|
};
|
|
|
|
/**
|
|
* Format a date string to a human readable format
|
|
* @example
|
|
* formatDate('2020-01-01T00:00:00.000Z') // '1 Jan 2020'
|
|
*/
|
|
export function formatDate(dateString: string, isSmallScreen = false) {
|
|
if (!dateString) {
|
|
return '';
|
|
}
|
|
|
|
const date = new Date(dateString);
|
|
|
|
if (isSmallScreen) {
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'numeric',
|
|
day: 'numeric',
|
|
year: '2-digit',
|
|
});
|
|
}
|
|
|
|
const months = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec',
|
|
];
|
|
|
|
const day = date.getDate();
|
|
const month = months[date.getMonth()];
|
|
const year = date.getFullYear();
|
|
|
|
return `${day} ${month} ${year}`;
|
|
}
|
|
|
|
/**
|
|
* Adds a file to the query cache
|
|
*/
|
|
export function addFileToCache(queryClient: QueryClient, newfile: TFile) {
|
|
const currentFiles = queryClient.getQueryData<TFile[]>([QueryKeys.files]);
|
|
|
|
if (!currentFiles) {
|
|
console.warn('No current files found in cache, skipped updating file query cache');
|
|
return;
|
|
}
|
|
|
|
const fileIndex = currentFiles.findIndex((file) => file.file_id === newfile.file_id);
|
|
|
|
if (fileIndex > -1) {
|
|
console.warn('File already exists in cache, skipped updating file query cache');
|
|
return;
|
|
}
|
|
|
|
queryClient.setQueryData<TFile[]>(
|
|
[QueryKeys.files],
|
|
[
|
|
{
|
|
...newfile,
|
|
},
|
|
...currentFiles,
|
|
],
|
|
);
|
|
}
|
|
|
|
export function formatBytes(bytes: number, decimals = 2) {
|
|
if (bytes === 0) {
|
|
return 0;
|
|
}
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
|
}
|
|
|
|
const { checkType } = defaultFileConfig;
|
|
|
|
export const validateFiles = ({
|
|
files,
|
|
fileList,
|
|
setError,
|
|
endpointFileConfig,
|
|
}: {
|
|
fileList: File[];
|
|
files: Map<string, ExtendedFile>;
|
|
setError: (error: string) => void;
|
|
endpointFileConfig: EndpointFileConfig;
|
|
}) => {
|
|
const { fileLimit, fileSizeLimit, totalSizeLimit, supportedMimeTypes } = endpointFileConfig;
|
|
const existingFiles = Array.from(files.values());
|
|
const incomingTotalSize = fileList.reduce((total, file) => total + file.size, 0);
|
|
if (incomingTotalSize === 0) {
|
|
setError('com_error_files_empty');
|
|
return false;
|
|
}
|
|
const currentTotalSize = existingFiles.reduce((total, file) => total + file.size, 0);
|
|
|
|
if (fileLimit && fileList.length + files.size > fileLimit) {
|
|
setError(`You can only upload up to ${fileLimit} files at a time.`);
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Check if the file type is still empty after the extension check
|
|
if (!fileType) {
|
|
setError('Unable to determine file type for: ' + originalFile.name);
|
|
return false;
|
|
}
|
|
|
|
// Replace empty type with inferred type
|
|
if (originalFile.type !== fileType) {
|
|
const newFile = new File([originalFile], originalFile.name, { type: fileType });
|
|
originalFile = newFile;
|
|
fileList[i] = newFile;
|
|
}
|
|
|
|
if (!checkType(originalFile.type, supportedMimeTypes)) {
|
|
console.log(originalFile);
|
|
setError('Currently, unsupported file type: ' + originalFile.type);
|
|
return false;
|
|
}
|
|
|
|
if (fileSizeLimit && originalFile.size >= fileSizeLimit) {
|
|
setError(`File size exceeds ${fileSizeLimit / megabyte} MB.`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (totalSizeLimit && currentTotalSize + incomingTotalSize > totalSizeLimit) {
|
|
setError(`The total size of the files cannot exceed ${totalSizeLimit / megabyte} MB.`);
|
|
return false;
|
|
}
|
|
|
|
const combinedFilesInfo = [
|
|
...existingFiles.map(
|
|
(file) =>
|
|
`${file.file?.name ?? file.filename}-${file.size}-${file.type?.split('/')[0] ?? 'file'}`,
|
|
),
|
|
...fileList.map(
|
|
(file: File | undefined) =>
|
|
`${file?.name}-${file?.size}-${file?.type.split('/')[0] ?? 'file'}`,
|
|
),
|
|
];
|
|
|
|
const uniqueFilesSet = new Set(combinedFilesInfo);
|
|
|
|
if (uniqueFilesSet.size !== combinedFilesInfo.length) {
|
|
setError('com_error_files_dupe');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|