mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
⬆️ feat: Cancel chat file uploads; fix: Assistant uploads (#4433)
* refactor: move file mutations to dedicated file, improve typing * refactor(ChatForm): utilize FileFormWrapper to consolidate file upload logic/rendering to single parent * refactor: better TSX heirarchies between AttachFile and FileFormWrapper * refactor: `abortUpload` WIP * fix: file debugging and file upload issues * refactor: reject promise outright if axios intercepted error does not include response property * chore: bump data-provider version to 0.7.428 * refactor: Add return type to localize function in Translation.ts * refactor: allow message file attachment upload request cancellations, and add localizations for file upload errors * refactor: include Azure OpenAI in paramEndpoints set * fix: assistant form uploads and better typing * refactor: consolidate logic
This commit is contained in:
parent
0870acd086
commit
65888c274a
20 changed files with 419 additions and 311 deletions
|
|
@ -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<string[]>([]);
|
||||
const abortControllerRef = useRef<AbortController | null>(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<TEndpointsConfig>([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<TEndpointsConfig>([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<TEndpointsConfig>([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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue