diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index dc6c99b29b..8fe5eb70d4 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -69,9 +69,10 @@ router.delete('/', async (req, res) => { await processDeleteRequest({ req, files }); logger.debug( - `[/files] Files deleted successfully: ${files.map( - (f, i) => `${f.file_id}${i < files.length - 1 ? ', ' : ''}`, - )}`, + `[/files] Files deleted successfully: ${files + .filter((f) => f.file_id) + .map((f) => f.file_id) + .join(', ')}`, ); res.status(200).json({ message: 'Files deleted successfully' }); } catch (error) { @@ -220,7 +221,7 @@ router.post('/', async (req, res) => { try { await fs.unlink(file.path); } catch (error) { - logger.error('[/files/images] Error deleting file after file processing:', error); + logger.error('[/files] Error deleting file after file processing:', error); } } }); diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 412983c010..fafa17c34d 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -8,9 +8,9 @@ import { } from 'librechat-data-provider'; import { useChatContext, + useChatFormContext, useAddedChatContext, useAssistantsMapContext, - useChatFormContext, } from '~/Providers'; import { useTextarea, @@ -20,18 +20,17 @@ import { useQueryParams, useSubmitMessage, } from '~/hooks'; +import FileFormWrapper from './Files/FileFormWrapper'; import { TextareaAutosize } from '~/components/ui'; import { useGetFileConfig } from '~/data-provider'; import { cn, removeFocusRings } from '~/utils'; import TextareaHeader from './TextareaHeader'; import PromptsCommand from './PromptsCommand'; -import AttachFile from './Files/AttachFile'; import AudioRecorder from './AudioRecorder'; import { mainTextareaId } from '~/common'; import StreamAudio from './StreamAudio'; import StopButton from './StopButton'; import SendButton from './SendButton'; -import FileRow from './Files/FileRow'; import Mention from './Mention'; import store from '~/store'; @@ -73,7 +72,6 @@ const ChatForm = ({ index = 0 }) => { conversation, isSubmitting, filesLoading, - setFilesLoading, newConversation, handleStopGenerating, } = useChatContext(); @@ -130,6 +128,9 @@ const ChatForm = ({ index = 0 }) => { } }, [isSearching, disableInputs]); + const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? endpoint ?? ''] ?? false; + const isUploadDisabled: boolean = endpointFileConfig?.disabled ?? false; + return (
submitMessage(data))} @@ -157,52 +158,37 @@ const ChatForm = ({ index = 0 }) => {
- ( -
- {children} -
+ + {endpoint && ( + { + ref(e); + textAreaRef.current = e; + }} + disabled={disableInputs} + onPaste={handlePaste} + onKeyDown={handleKeyDown} + onKeyUp={handleKeyUp} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + id={mainTextareaId} + tabIndex={0} + data-testid="text-input" + style={{ height: 44, overflowY: 'auto' }} + rows={1} + className={cn( + endpointSupportsFiles && !isUploadDisabled + ? 'pl-10 md:pl-[55px]' + : 'pl-3 md:pl-4', + 'm-0 w-full resize-none border-0 bg-transparent py-[10px] placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 ', + SpeechToText && !isRTL ? 'pr-20 md:pr-[85px]' : 'pr-10 md:pr-12', + 'max-h-[65vh] md:max-h-[75vh]', + removeFocusRings, + )} + /> )} - /> - {endpoint && ( - { - ref(e); - textAreaRef.current = e; - }} - disabled={disableInputs} - onPaste={handlePaste} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} - onCompositionStart={handleCompositionStart} - onCompositionEnd={handleCompositionEnd} - id={mainTextareaId} - tabIndex={0} - data-testid="text-input" - style={{ height: 44, overflowY: 'auto' }} - rows={1} - className={cn( - supportsFiles[endpointType ?? endpoint ?? ''] && !endpointFileConfig?.disabled - ? ' pl-10 md:pl-[55px]' - : 'pl-3 md:pl-4', - 'm-0 w-full resize-none border-0 bg-transparent py-[10px] placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 ', - SpeechToText && !isRTL ? 'pr-20 md:pr-[85px]' : 'pr-10 md:pr-12', - 'max-h-[65vh] md:max-h-[75vh]', - removeFocusRings, - )} - /> - )} - + {(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? ( ) => void; }) => { const localize = useLocalize(); - const { handleFileChange } = useFileHandling(); - const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ - select: (data) => mergeFileConfig(data), - }); - const endpointFileConfig = fileConfig.endpoints[endpoint ?? '']; - - if (!supportsFiles[endpointType ?? endpoint ?? ''] || endpointFileConfig?.disabled) { - return null; - } + const isUploadDisabled = disabled ?? false; return (
mergeFileConfig(data), + }); + + const isRTL = chatDirection === 'rtl'; + + const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null }; + const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as EndpointFileConfig | undefined; + const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false; + const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false; + + return (<> + ( +
+ {children} +
+ )} + /> + {children} + {endpointSupportsFiles && !isUploadDisabled && } + ); +} + +export default memo(FileFormWrapper); \ No newline at end of file diff --git a/client/src/components/Chat/Input/Files/FileRow.tsx b/client/src/components/Chat/Input/Files/FileRow.tsx index caf62b05dc..a7c07123cc 100644 --- a/client/src/components/Chat/Input/Files/FileRow.tsx +++ b/client/src/components/Chat/Input/Files/FileRow.tsx @@ -4,11 +4,13 @@ import type { ExtendedFile } from '~/common'; import { useDeleteFilesMutation } from '~/data-provider'; import { useFileDeletion } from '~/hooks/Files'; import FileContainer from './FileContainer'; +import { logger } from '~/utils'; import Image from './Image'; export default function FileRow({ files: _files, setFiles, + abortUpload, setFilesLoading, assistant_id, agent_id, @@ -18,6 +20,7 @@ export default function FileRow({ Wrapper, }: { files: Map | undefined; + abortUpload?: () => void; setFiles: React.Dispatch>>; setFilesLoading: React.Dispatch>; fileFilter?: (file: ExtendedFile) => boolean; @@ -33,7 +36,8 @@ export default function FileRow({ const { mutateAsync } = useDeleteFilesMutation({ onMutate: async () => - console.log( + logger.log( + 'agents', 'Deleting files: agent_id, assistant_id, tool_resource', agent_id, assistant_id, @@ -86,7 +90,12 @@ export default function FileRow({ { map: new Map(), uniqueFiles: [] as ExtendedFile[] }, ) .uniqueFiles.map((file: ExtendedFile, index: number) => { - const handleDelete = () => deleteFile({ file, setFiles }); + const handleDelete = () => { + if (abortUpload && file.progress < 1) { + abortUpload(); + } + deleteFile({ file, setFiles }); + }; const isImage = file.type?.startsWith('image') ?? false; if (isImage) { return ( diff --git a/client/src/components/SidePanel/Agents/CodeFiles.tsx b/client/src/components/SidePanel/Agents/CodeFiles.tsx index d694c656e0..e5f21017b1 100644 --- a/client/src/components/SidePanel/Agents/CodeFiles.tsx +++ b/client/src/components/SidePanel/Agents/CodeFiles.tsx @@ -5,6 +5,7 @@ import { mergeFileConfig, fileConfig as defaultFileConfig, } from 'librechat-data-provider'; +import type { EndpointFileConfig } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import FileRow from '~/components/Chat/Input/Files/FileRow'; import { useGetFileConfig } from '~/data-provider'; @@ -40,9 +41,12 @@ export default function CodeFiles({ } }, [_files]); - const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents]; + const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents] as + | EndpointFileConfig + | undefined; + const isUploadDisabled = endpointFileConfig?.disabled ?? false; - if (endpointFileConfig?.disabled) { + if (isUploadDisabled) { return null; } diff --git a/client/src/components/SidePanel/Builder/CodeFiles.tsx b/client/src/components/SidePanel/Builder/CodeFiles.tsx index ef0c3cc70b..94b9efe914 100644 --- a/client/src/components/SidePanel/Builder/CodeFiles.tsx +++ b/client/src/components/SidePanel/Builder/CodeFiles.tsx @@ -4,7 +4,7 @@ import { mergeFileConfig, fileConfig as defaultFileConfig, } from 'librechat-data-provider'; -import type { AssistantsEndpoint } from 'librechat-data-provider'; +import type { AssistantsEndpoint, EndpointFileConfig } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import FileRow from '~/components/Chat/Input/Files/FileRow'; import { useGetFileConfig } from '~/data-provider'; @@ -43,9 +43,10 @@ export default function CodeFiles({ } }, [_files]); - const endpointFileConfig = fileConfig.endpoints[endpoint]; + const endpointFileConfig = fileConfig.endpoints[endpoint] as EndpointFileConfig | undefined; + const isUploadDisabled = endpointFileConfig?.disabled ?? false; - if (endpointFileConfig.disabled) { + if (isUploadDisabled) { return null; } diff --git a/client/src/components/SidePanel/Builder/Knowledge.tsx b/client/src/components/SidePanel/Builder/Knowledge.tsx index d58ff77524..01042cab4b 100644 --- a/client/src/components/SidePanel/Builder/Knowledge.tsx +++ b/client/src/components/SidePanel/Builder/Knowledge.tsx @@ -4,7 +4,7 @@ import { retrievalMimeTypes, fileConfig as defaultFileConfig, } from 'librechat-data-provider'; -import type { AssistantsEndpoint } from 'librechat-data-provider'; +import type { AssistantsEndpoint, EndpointFileConfig } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import FileRow from '~/components/Chat/Input/Files/FileRow'; import { useGetFileConfig } from '~/data-provider'; @@ -53,9 +53,10 @@ export default function Knowledge({ } }, [_files]); - const endpointFileConfig = fileConfig.endpoints[endpoint]; + const endpointFileConfig = fileConfig.endpoints[endpoint] as EndpointFileConfig | undefined; + const isUploadDisabled = endpointFileConfig?.disabled ?? false; - if (endpointFileConfig?.disabled) { + if (isUploadDisabled) { return null; } diff --git a/client/src/data-provider/Files/index.ts b/client/src/data-provider/Files/index.ts index 3cf1ef310b..d0720956a0 100644 --- a/client/src/data-provider/Files/index.ts +++ b/client/src/data-provider/Files/index.ts @@ -1 +1,2 @@ export * from './queries'; +export * from './mutations'; diff --git a/client/src/data-provider/Files/mutations.ts b/client/src/data-provider/Files/mutations.ts new file mode 100644 index 0000000000..b46e435448 --- /dev/null +++ b/client/src/data-provider/Files/mutations.ts @@ -0,0 +1,159 @@ +import { EToolResources } from 'librechat-data-provider'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider'; +import type * as t from 'librechat-data-provider'; +import type { UseMutationResult } from '@tanstack/react-query'; + +export type TGenTitleMutation = UseMutationResult< + t.TGenTitleResponse, + unknown, + t.TGenTitleRequest, + unknown +>; + +export const useUploadFileMutation = ( + _options?: t.UploadMutationOptions, + signal?: AbortSignal | null, +): UseMutationResult< + t.TFileUpload, // response data + unknown, // error + FormData, // request + unknown // context +> => { + const queryClient = useQueryClient(); + const { onSuccess, ...options } = _options || {}; + return useMutation([MutationKeys.fileUpload], { + mutationFn: (body: FormData) => { + const width = body.get('width') ?? ''; + const height = body.get('height') ?? ''; + const version = body.get('version') ?? ''; + + if (width !== '' && height !== '' && (version !== '' || version.toString() !== '2')) { + return dataService.uploadImage(body, signal); + } + + return dataService.uploadFile(body, signal); + }, + ...options, + onSuccess: (data, formData, context) => { + queryClient.setQueryData([QueryKeys.files], (_files) => [ + data, + ...(_files ?? []), + ]); + + const endpoint = formData.get('endpoint'); + const message_file = formData.get('message_file'); + const agent_id = (formData.get('agent_id') as string | undefined) ?? ''; + const assistant_id = (formData.get('assistant_id') as string | undefined) ?? ''; + const tool_resource = (formData.get('tool_resource') as string | undefined) ?? ''; + + if (message_file === 'true') { + onSuccess?.(data, formData, context); + return; + } + + if (agent_id && tool_resource) { + queryClient.setQueryData([QueryKeys.agent, agent_id], (agent) => { + if (!agent) { + return agent; + } + + const update = {}; + const prevResources = agent.tool_resources ?? {}; + const prevResource: t.ExecuteCodeResource | t.AgentFileSearchResource = agent + .tool_resources?.[tool_resource] ?? { + file_ids: [], + }; + if (!prevResource.file_ids) { + prevResource.file_ids = []; + } + prevResource.file_ids.push(data.file_id); + update['tool_resources'] = { + ...prevResources, + [tool_resource]: prevResource, + }; + return { + ...agent, + ...update, + }; + }); + } + + if (!assistant_id) { + onSuccess?.(data, formData, context); + return; + } + + queryClient.setQueryData( + [QueryKeys.assistants, endpoint, defaultOrderQuery], + (prev) => { + if (!prev) { + return prev; + } + + return { + ...prev, + data: prev.data.map((assistant) => { + if (assistant.id !== assistant_id) { + return assistant; + } + + const update = {}; + if (!tool_resource) { + update['file_ids'] = [...(assistant.file_ids ?? []), data.file_id]; + } + if (tool_resource === EToolResources.code_interpreter) { + const prevResources = assistant.tool_resources ?? {}; + const prevResource = assistant.tool_resources?.[tool_resource] ?? { + file_ids: [], + }; + if (!prevResource.file_ids) { + prevResource.file_ids = []; + } + prevResource.file_ids.push(data.file_id); + update['tool_resources'] = { + ...prevResources, + [tool_resource]: prevResource, + }; + } + return { + ...assistant, + ...update, + }; + }), + }; + }, + ); + onSuccess?.(data, formData, context); + }, + }); +}; + +export const useDeleteFilesMutation = ( + _options?: t.DeleteMutationOptions, +): UseMutationResult< + t.DeleteFilesResponse, // response data + unknown, // error + t.DeleteFilesBody, // request + unknown // context +> => { + const queryClient = useQueryClient(); + const { onSuccess, ...options } = _options || {}; + return useMutation([MutationKeys.fileDelete], { + mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body), + ...options, + onSuccess: (data, ...args) => { + queryClient.setQueryData([QueryKeys.files], (cachefiles) => { + const { files: filesDeleted } = args[0]; + + const fileMap = filesDeleted.reduce((acc, file) => { + acc.set(file.file_id, file); + return acc; + }, new Map()); + + return (cachefiles ?? []).filter((file) => !fileMap.has(file.file_id)); + }); + onSuccess?.(data, ...args); + }, + }); +}; diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index b92a5920d0..e57890e187 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -1,6 +1,5 @@ import { Constants, - EToolResources, LocalStorageKeys, InfiniteCollections, defaultAssistantsVersion, @@ -622,151 +621,6 @@ export const useUploadConversationsMutation = ( }); }; -export const useUploadFileMutation = ( - _options?: t.UploadMutationOptions, -): UseMutationResult< - t.TFileUpload, // response data - unknown, // error - FormData, // request - unknown // context -> => { - const queryClient = useQueryClient(); - const { onSuccess, ...options } = _options || {}; - return useMutation([MutationKeys.fileUpload], { - mutationFn: (body: FormData) => { - const width = body.get('width'); - const height = body.get('height'); - const version = body.get('version') as number | string; - if (height && width && (!version || version != 2)) { - return dataService.uploadImage(body); - } - - return dataService.uploadFile(body); - }, - ...options, - onSuccess: (data, formData, context) => { - queryClient.setQueryData([QueryKeys.files], (_files) => [ - data, - ...(_files ?? []), - ]); - - const endpoint = formData.get('endpoint'); - const message_file = formData.get('message_file'); - const agent_id = (formData.get('agent_id') as string | undefined) ?? ''; - const assistant_id = (formData.get('assistant_id') as string | undefined) ?? ''; - const tool_resource = (formData.get('tool_resource') as string | undefined) ?? ''; - - if (message_file === 'true') { - onSuccess?.(data, formData, context); - return; - } - - if (agent_id && tool_resource) { - queryClient.setQueryData([QueryKeys.agent, agent_id], (agent) => { - if (!agent) { - return agent; - } - - const update = {}; - const prevResources = agent.tool_resources ?? {}; - const prevResource: t.ExecuteCodeResource | t.AgentFileSearchResource = agent - .tool_resources?.[tool_resource] ?? { - file_ids: [], - }; - if (!prevResource.file_ids) { - prevResource.file_ids = []; - } - prevResource.file_ids.push(data.file_id); - update['tool_resources'] = { - ...prevResources, - [tool_resource]: prevResource, - }; - return { - ...agent, - ...update, - }; - }); - } - - if (!assistant_id) { - onSuccess?.(data, formData, context); - return; - } - - queryClient.setQueryData( - [QueryKeys.assistants, endpoint, defaultOrderQuery], - (prev) => { - if (!prev) { - return prev; - } - - return { - ...prev, - data: prev.data.map((assistant) => { - if (assistant.id !== assistant_id) { - return assistant; - } - - const update = {}; - if (!tool_resource) { - update['file_ids'] = [...(assistant.file_ids ?? []), data.file_id]; - } - if (tool_resource === EToolResources.code_interpreter) { - const prevResources = assistant.tool_resources ?? {}; - const prevResource = assistant.tool_resources?.[tool_resource] ?? { - file_ids: [], - }; - if (!prevResource.file_ids) { - prevResource.file_ids = []; - } - prevResource.file_ids.push(data.file_id); - update['tool_resources'] = { - ...prevResources, - [tool_resource]: prevResource, - }; - } - return { - ...assistant, - ...update, - }; - }), - }; - }, - ); - onSuccess?.(data, formData, context); - }, - }); -}; - -export const useDeleteFilesMutation = ( - _options?: t.DeleteMutationOptions, -): UseMutationResult< - t.DeleteFilesResponse, // response data - unknown, // error - t.DeleteFilesBody, // request - unknown // context -> => { - const queryClient = useQueryClient(); - const { onSuccess, ...options } = _options || {}; - return useMutation([MutationKeys.fileDelete], { - mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body), - ...options, - onSuccess: (data, ...args) => { - queryClient.setQueryData([QueryKeys.files], (cachefiles) => { - const { files: filesDeleted } = args[0]; - - const fileMap = filesDeleted.reduce((acc, file) => { - acc.set(file.file_id, file); - return acc; - }, new Map()); - - return (cachefiles ?? []).filter((file) => !fileMap.has(file.file_id)); - }); - onSuccess?.(data, ...args); - }, - }); -}; - export const useUpdatePresetMutation = ( options?: t.UpdatePresetOptions, ): UseMutationResult< diff --git a/client/src/hooks/Files/useFileHandling.ts b/client/src/hooks/Files/useFileHandling.ts index ba7ea5dad1..37dc37f896 100644 --- a/client/src/hooks/Files/useFileHandling.ts +++ b/client/src/hooks/Files/useFileHandling.ts @@ -1,7 +1,7 @@ import { v4 } from 'uuid'; import debounce from 'lodash/debounce'; import { useQueryClient } from '@tanstack/react-query'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { megabyte, QueryKeys, @@ -18,7 +18,9 @@ import { useUploadFileMutation, useGetFileConfig } from '~/data-provider'; import { useDelayedUploadToast } from './useDelayedUploadToast'; import { useToastContext } from '~/Providers/ToastContext'; import { useChatContext } from '~/Providers/ChatContext'; +import useLocalize from '~/hooks/useLocalize'; import useUpdateFiles from './useUpdateFiles'; +import { logger } from '~/utils'; const { checkType } = defaultFileConfig; @@ -30,9 +32,11 @@ type UseFileHandling = { }; const useFileHandling = (params?: UseFileHandling) => { + const localize = useLocalize(); const queryClient = useQueryClient(); const { showToast } = useToastContext(); const [errors, setErrors] = useState([]); + const abortControllerRef = useRef(null); const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast(); const { files, setFiles, setFilesLoading, conversation } = useChatContext(); const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]); @@ -55,7 +59,7 @@ const useFileHandling = (params?: UseFileHandling) => { const displayToast = useCallback(() => { if (errors.length > 1) { const errorList = Array.from(new Set(errors)) - .map((e, i) => `${i > 0 ? '• ' : ''}${e}\n`) + .map((e, i) => `${i > 0 ? '• ' : ''}${localize(e) || e}\n`) .join(''); showToast({ message: errorList, @@ -63,15 +67,16 @@ const useFileHandling = (params?: UseFileHandling) => { duration: 5000, }); } else if (errors.length === 1) { + const message = localize(errors[0]) || errors[0]; showToast({ - message: errors[0], + message, status: 'error', duration: 5000, }); } setErrors([]); - }, [errors, showToast]); + }, [errors, showToast, localize]); const debouncedDisplayToast = debounce(displayToast, 250); @@ -83,53 +88,58 @@ const useFileHandling = (params?: UseFileHandling) => { return () => debouncedDisplayToast.cancel(); }, [errors, debouncedDisplayToast]); - const uploadFile = useUploadFileMutation({ - onSuccess: (data) => { - clearUploadTimer(data.temp_file_id); - console.log('upload success', data); - if (agent_id) { - queryClient.refetchQueries([QueryKeys.agent, agent_id]); - return; - } - updateFileById( - data.temp_file_id, - { - progress: 0.9, - filepath: data.filepath, - }, - assistant_id ? true : false, - ); - - setTimeout(() => { + const uploadFile = useUploadFileMutation( + { + onSuccess: (data) => { + clearUploadTimer(data.temp_file_id); + console.log('upload success', data); + if (agent_id) { + queryClient.refetchQueries([QueryKeys.agent, agent_id]); + return; + } updateFileById( data.temp_file_id, { - progress: 1, - file_id: data.file_id, - temp_file_id: data.temp_file_id, + progress: 0.9, filepath: data.filepath, - type: data.type, - height: data.height, - width: data.width, - filename: data.filename, - source: data.source, - embedded: data.embedded, }, assistant_id ? true : false, ); - }, 300); + + setTimeout(() => { + updateFileById( + data.temp_file_id, + { + progress: 1, + file_id: data.file_id, + temp_file_id: data.temp_file_id, + filepath: data.filepath, + type: data.type, + height: data.height, + width: data.width, + filename: data.filename, + source: data.source, + embedded: data.embedded, + }, + assistant_id ? true : false, + ); + }, 300); + }, + onError: (_error, body) => { + const error = _error as TError | undefined; + console.log('upload error', error); + const file_id = body.get('file_id'); + clearUploadTimer(file_id as string); + deleteFileById(file_id as string); + const errorMessage = + error?.code === 'ERR_CANCELED' + ? 'com_error_files_upload_canceled' + : error?.response?.data?.message ?? 'com_error_files_upload'; + setError(errorMessage); + }, }, - onError: (error, body) => { - console.log('upload error', error); - const file_id = body.get('file_id'); - clearUploadTimer(file_id as string); - deleteFileById(file_id as string); - setError( - (error as TError | undefined)?.response?.data?.message ?? - 'An error occurred while uploading the file.', - ); - }, - }); + abortControllerRef.current?.signal, + ); const startUpload = async (extendedFile: ExtendedFile) => { const filename = extendedFile.file?.name ?? 'File'; @@ -148,33 +158,47 @@ const useFileHandling = (params?: UseFileHandling) => { formData.append('height', height.toString()); } + const metadata = params?.additionalMetadata ?? {}; if (params?.additionalMetadata) { - for (const [key, value = ''] of Object.entries(params.additionalMetadata)) { + for (const [key, value = ''] of Object.entries(metadata)) { if (value) { formData.append(key, value); } } } - const convoAssistantId = conversation?.assistant_id ?? ''; - const convoModel = conversation?.model ?? ''; - if (isAssistantsEndpoint(endpoint) && !formData.get('assistant_id') && convoAssistantId) { - const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); - const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]; - formData.append('version', version); - formData.append('assistant_id', convoAssistantId); - formData.append('model', convoModel); - formData.append('message_file', 'true'); + formData.append('endpoint', endpoint); + + if (!isAssistantsEndpoint(endpoint)) { + uploadFile.mutate(formData); + return; } - if (isAssistantsEndpoint(endpoint) && !formData.get('version')) { - const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); - const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]; - formData.append('version', version); - formData.append('model', conversation?.model ?? ''); + + const convoModel = conversation?.model ?? ''; + const convoAssistantId = conversation?.assistant_id ?? ''; + + if (!assistant_id) { formData.append('message_file', 'true'); } - formData.append('endpoint', endpoint); + const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); + const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]; + + if (!assistant_id && convoAssistantId) { + formData.append('version', version); + formData.append('model', convoModel); + formData.append('assistant_id', convoAssistantId); + } + + const formVersion = (formData.get('version') ?? '') as string; + if (!formVersion) { + formData.append('version', version); + } + + const formModel = (formData.get('model') ?? '') as string; + if (!formModel) { + formData.append('model', convoModel); + } uploadFile.mutate(formData); }; @@ -183,7 +207,7 @@ const useFileHandling = (params?: UseFileHandling) => { const existingFiles = Array.from(files.values()); const incomingTotalSize = fileList.reduce((total, file) => total + file.size, 0); if (incomingTotalSize === 0) { - setError('Empty files are not allowed.'); + setError('com_error_files_empty'); return false; } const currentTotalSize = existingFiles.reduce((total, file) => total + file.size, 0); @@ -248,7 +272,7 @@ const useFileHandling = (params?: UseFileHandling) => { const uniqueFilesSet = new Set(combinedFilesInfo); if (uniqueFilesSet.size !== combinedFilesInfo.length) { - setError('Duplicate file detected.'); + setError('com_error_files_dupe'); return false; } @@ -273,6 +297,7 @@ const useFileHandling = (params?: UseFileHandling) => { }; const handleFiles = async (_files: FileList | File[]) => { + abortControllerRef.current = new AbortController(); const fileList = Array.from(_files); /* Validate files */ let filesAreValid: boolean; @@ -280,7 +305,7 @@ const useFileHandling = (params?: UseFileHandling) => { filesAreValid = validateFiles(fileList); } catch (error) { console.error('file validation error', error); - setError('An error occurred while validating the file.'); + setError('com_error_files_validation'); return; } if (!filesAreValid) { @@ -313,7 +338,7 @@ const useFileHandling = (params?: UseFileHandling) => { } catch (error) { deleteFileById(file_id); console.log('file handling error', error); - setError('An error occurred while processing the file.'); + setError('com_error_files_process'); } } }; @@ -328,11 +353,20 @@ const useFileHandling = (params?: UseFileHandling) => { } }; + const abortUpload = () => { + if (abortControllerRef.current) { + logger.log('files', 'Aborting upload'); + abortControllerRef.current.abort('User aborted upload'); + abortControllerRef.current = null; + } + }; + return { handleFileChange, handleFiles, - files, + abortUpload, setFiles, + files, }; }; diff --git a/client/src/localization/Translation.ts b/client/src/localization/Translation.ts index ad440f49d3..9d4985c2e6 100644 --- a/client/src/localization/Translation.ts +++ b/client/src/localization/Translation.ts @@ -83,7 +83,7 @@ export const getTranslations = (langCode: string): Language => { // input: language code in string & phrase key in string // returns an corresponding phrase value in string -export const localize = (langCode: string, phraseKey: string, ...values: string[]) => { +export const localize = (langCode: string, phraseKey: string, ...values: string[]): string => { const lang = getTranslations(langCode); const phrase = lang[phraseKey] || English[phraseKey] || ''; diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 77371333f7..696c63e4e2 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -33,6 +33,13 @@ export default { 'Provided key for {0} expired at {1}. Please provide a new key and try again.', com_error_input_length: 'The latest message token count is too long, exceeding the token limit ({0} respectively). Please shorten your message, adjust the max context size from the conversation parameters, or fork the conversation to continue.', + com_error_files_empty: 'Empty files are not allowed.', + com_error_files_dupe: 'Duplicate file detected.', + com_error_files_validation: 'An error occurred while validating the file.', + com_error_files_process: 'An error occurred while processing the file.', + com_error_files_upload: 'An error occurred while uploading the file.', + com_error_files_upload_canceled: + 'The file upload request was canceled. Note: the file upload may still be processing and will need to be manually deleted.', com_files_no_results: 'No results.', com_files_filter: 'Filter files...', com_generated_files: 'Generated files:', diff --git a/package-lock.json b/package-lock.json index e641760f69..f82566ce14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36522,7 +36522,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.427", + "version": "0.7.428", "license": "ISC", "dependencies": { "@types/js-yaml": "^4.0.9", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index fa1924a4b4..0aa864f7c5 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.427", + "version": "0.7.428", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 232136a0d7..828a4ae176 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -44,8 +44,8 @@ export function getSharedMessages(shareId: string): Promise => { - const pageNumber = params?.pageNumber || '1'; // Default to page 1 if not provided - const isPublic = params?.isPublic || true; // Default to true if not provided + const pageNumber = (params?.pageNumber ?? '1') || '1'; // Default to page 1 if not provided + const isPublic = params?.isPublic ?? true; // Default to true if not provided return request.get(endpoints.getSharedLinks(pageNumber, isPublic)); }; @@ -314,12 +314,14 @@ export const getFileConfig = (): Promise => { return request.get(`${endpoints.files()}/config`); }; -export const uploadImage = (data: FormData): Promise => { - return request.postMultiPart(endpoints.images(), data); +export const uploadImage = (data: FormData, signal?: AbortSignal | null): Promise => { + const requestConfig = signal ? { signal } : undefined; + return request.postMultiPart(endpoints.images(), data, requestConfig); }; -export const uploadFile = (data: FormData): Promise => { - return request.postMultiPart(endpoints.files(), data); +export const uploadFile = (data: FormData, signal?: AbortSignal | null): Promise => { + const requestConfig = signal ? { signal } : undefined; + return request.postMultiPart(endpoints.files(), data, requestConfig); }; /* actions */ @@ -538,8 +540,8 @@ export const listConversations = ( params?: q.ConversationListParams, ): Promise => { // Assuming params has a pageNumber property - const pageNumber = params?.pageNumber || '1'; // Default to page 1 if not provided - const isArchived = params?.isArchived || false; // Default to false if not provided + const pageNumber = (params?.pageNumber ?? '1') || '1'; // Default to page 1 if not provided + const isArchived = params?.isArchived ?? false; // Default to false if not provided const tags = params?.tags || []; // Default to an empty array if not provided return request.get(endpoints.conversations(pageNumber, isArchived, tags)); }; @@ -547,8 +549,8 @@ export const listConversations = ( export const listConversationsByQuery = ( params?: q.ConversationListParams & { searchQuery?: string }, ): Promise => { - const pageNumber = params?.pageNumber || '1'; // Default to page 1 if not provided - const searchQuery = params?.searchQuery || ''; // If no search query is provided, default to an empty string + const pageNumber = (params?.pageNumber ?? '1') || '1'; // Default to page 1 if not provided + const searchQuery = params?.searchQuery ?? ''; // If no search query is provided, default to an empty string // Update the endpoint to handle a search query if (searchQuery !== '') { return request.get(endpoints.search(searchQuery, pageNumber)); diff --git a/packages/data-provider/src/request.ts b/packages/data-provider/src/request.ts index c701e0b463..eaa1896cdb 100644 --- a/packages/data-provider/src/request.ts +++ b/packages/data-provider/src/request.ts @@ -80,6 +80,9 @@ axios.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; + if (!error.response) { + return Promise.reject(error); + } if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 03536dbe99..f2fa7d9248 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -30,8 +30,9 @@ export enum EModelEndpoint { export const paramEndpoints = new Set([ EModelEndpoint.agents, - EModelEndpoint.bedrock, EModelEndpoint.openAI, + EModelEndpoint.bedrock, + EModelEndpoint.azureOpenAI, EModelEndpoint.anthropic, EModelEndpoint.custom, ]); diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 41276c7557..26cf70eaa8 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -93,7 +93,7 @@ export type TCategory = { export type TError = { message: string; - code?: number; + code?: number | string; response?: { data?: { message?: string;