⬆️ 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:
Danny Avila 2024-10-16 11:24:40 -04:00 committed by GitHub
parent 0870acd086
commit 65888c274a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 419 additions and 311 deletions

View file

@ -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 (
<form
onSubmit={methods.handleSubmit((data) => submitMessage(data))}
@ -157,52 +158,37 @@ const ChatForm = ({ index = 0 }) => {
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
<div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:border-gray-300 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500">
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
<FileRow
files={files}
setFiles={setFiles}
setFilesLoading={setFilesLoading}
isRTL={isRTL}
Wrapper={({ children }) => (
<div className="mx-2 mt-2 flex flex-wrap gap-2 px-2.5 md:pl-0 md:pr-4">
{children}
</div>
<FileFormWrapper disableInputs={disableInputs}>
{endpoint && (
<TextareaAutosize
{...registerProps}
ref={(e) => {
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 && (
<TextareaAutosize
{...registerProps}
ref={(e) => {
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,
)}
/>
)}
<AttachFile
endpoint={_endpoint ?? ''}
endpointType={endpointType}
isRTL={isRTL}
disabled={disableInputs}
/>
</FileFormWrapper>
{(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? (
<StopButton
stop={handleStopGenerating}

View file

@ -1,37 +1,20 @@
import React from 'react';
import {
EModelEndpoint,
supportsFiles,
fileConfig as defaultFileConfig,
mergeFileConfig,
} from 'librechat-data-provider';
import { FileUpload, TooltipAnchor } from '~/components/ui';
import { useFileHandling, useLocalize } from '~/hooks';
import { useGetFileConfig } from '~/data-provider';
import { AttachmentIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
const AttachFile = ({
endpoint,
endpointType,
isRTL,
disabled = false,
disabled,
handleFileChange,
}: {
endpoint: EModelEndpoint | '';
endpointType?: EModelEndpoint;
isRTL: boolean;
disabled?: boolean | null;
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => 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 (
<div
@ -45,8 +28,8 @@ const AttachFile = ({
<FileUpload handleFileChange={handleFileChange} className="flex">
<TooltipAnchor
id="audio-recorder"
disabled={isUploadDisabled}
aria-label={localize('com_sidepanel_attach_files')}
disabled={!!disabled}
className="btn relative text-black focus:outline-none focus:ring-2 focus:ring-border-xheavy focus:ring-opacity-50 dark:text-white"
style={{ padding: 0 }}
description={localize('com_sidepanel_attach_files')}

View file

@ -0,0 +1,62 @@
import { memo } from 'react';
import { useRecoilValue } from 'recoil';
import {
supportsFiles,
mergeFileConfig,
EndpointFileConfig,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import { useGetFileConfig } from '~/data-provider';
import { useChatContext } from '~/Providers';
import { useFileHandling } from '~/hooks';
import AttachFile from './AttachFile';
import FileRow from './FileRow';
import store from '~/store';
function FileFormWrapper({ children, disableInputs } : {
disableInputs: boolean;
children?: React.ReactNode;
}) {
const { handleFileChange, abortUpload } = useFileHandling();
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
const {
files,
setFiles,
conversation,
setFilesLoading,
} = useChatContext();
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => 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 (<>
<FileRow
files={files}
setFiles={setFiles}
abortUpload={abortUpload}
setFilesLoading={setFilesLoading}
isRTL={isRTL}
Wrapper={({ children }) => (
<div className="mx-2 mt-2 flex flex-wrap gap-2 px-2.5 md:pl-0 md:pr-4">
{children}
</div>
)}
/>
{children}
{endpointSupportsFiles && !isUploadDisabled && <AttachFile
isRTL={isRTL}
disabled={disableInputs}
handleFileChange={handleFileChange}
/>}
</>);
}
export default memo(FileFormWrapper);

View file

@ -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<string, ExtendedFile> | undefined;
abortUpload?: () => void;
setFiles: React.Dispatch<React.SetStateAction<Map<string, ExtendedFile>>>;
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
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 (