| | import { |
| | TextPaths, |
| | FilePaths, |
| | CodePaths, |
| | AudioPaths, |
| | VideoPaths, |
| | SheetPaths, |
| | } from '@librechat/client'; |
| | import { |
| | megabyte, |
| | QueryKeys, |
| | inferMimeType, |
| | excelMimeTypes, |
| | EToolResources, |
| | fileConfig as defaultFileConfig, |
| | } from 'librechat-data-provider'; |
| | import type { TFile, EndpointFileConfig, FileConfig } from 'librechat-data-provider'; |
| | import type { QueryClient } from '@tanstack/react-query'; |
| | import type { ExtendedFile } from '~/common'; |
| |
|
| | 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', |
| | |
| | title: 'Code', |
| | }; |
| |
|
| | const artifact = { |
| | paths: CodePaths, |
| | fill: '#2D305C', |
| | title: 'Code', |
| | }; |
| |
|
| | const audioFile = { |
| | paths: AudioPaths, |
| | fill: '#FF6B35', |
| | title: 'Audio', |
| | }; |
| |
|
| | const videoFile = { |
| | paths: VideoPaths, |
| | fill: '#8B5CF6', |
| | title: 'Video', |
| | }; |
| |
|
| | export const fileTypes = { |
| | |
| | file: { |
| | paths: FilePaths, |
| | fill: '#0000FF', |
| | title: 'File', |
| | }, |
| | text: textDocument, |
| | txt: textDocument, |
| | audio: audioFile, |
| | video: videoFile, |
| | |
| |
|
| | |
| | csv: spreadsheet, |
| | 'application/pdf': textDocument, |
| | pdf: textDocument, |
| | 'text/x-': codeFile, |
| | artifact: artifact, |
| |
|
| | |
| | |
| | |
| | |
| | |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| |
|
| | export const getFileType = ( |
| | type = '', |
| | ): { |
| | paths: React.FC; |
| | fill: string; |
| | title: string; |
| | } => { |
| | |
| | if (fileTypes[type]) { |
| | return fileTypes[type]; |
| | } |
| |
|
| | if (excelMimeTypes.test(type)) { |
| | return spreadsheet; |
| | } |
| |
|
| | |
| | const partialMatch = partialTypes.find((partial) => type.includes(partial)); |
| | if (partialMatch && fileTypes[partialMatch]) { |
| | return fileTypes[partialMatch]; |
| | } |
| |
|
| | |
| | const category = type.split('/')[0] || 'text'; |
| | if (fileTypes[category]) { |
| | return fileTypes[category]; |
| | } |
| |
|
| | |
| | return fileTypes.file; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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}`; |
| | } |
| |
|
| | |
| | |
| | |
| | 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, |
| | toolResource, |
| | fileConfig, |
| | }: { |
| | fileList: File[]; |
| | files: Map<string, ExtendedFile>; |
| | setError: (error: string) => void; |
| | endpointFileConfig: EndpointFileConfig; |
| | toolResource?: string; |
| | fileConfig: FileConfig | null; |
| | }) => { |
| | const { fileLimit, fileSizeLimit, totalSizeLimit, supportedMimeTypes, disabled } = |
| | endpointFileConfig; |
| | |
| | if (disabled === true) { |
| | setError('com_ui_attach_error_disabled'); |
| | return false; |
| | } |
| | 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]; |
| | const fileType = inferMimeType(originalFile.name, originalFile.type); |
| |
|
| | |
| | if (!fileType) { |
| | setError('Unable to determine file type for: ' + originalFile.name); |
| | return false; |
| | } |
| |
|
| | |
| | if (originalFile.type !== fileType) { |
| | const newFile = new File([originalFile], originalFile.name, { type: fileType }); |
| | originalFile = newFile; |
| | fileList[i] = newFile; |
| | } |
| |
|
| | let mimeTypesToCheck = supportedMimeTypes; |
| | if (toolResource === EToolResources.context) { |
| | mimeTypesToCheck = [ |
| | ...(fileConfig?.text?.supportedMimeTypes || []), |
| | ...(fileConfig?.ocr?.supportedMimeTypes || []), |
| | ...(fileConfig?.stt?.supportedMimeTypes || []), |
| | ]; |
| | } |
| |
|
| | if (!checkType(originalFile.type, mimeTypesToCheck)) { |
| | 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; |
| | }; |
| |
|