mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-05 10:05:05 +08:00
feat: enable multi-file upload for chat and agent workflows (#12977)
### Closes: #12921 ### What problem does this PR solve? Previously, multi-file upload was not working correctly across the application: - **Chat**: UI displayed "Upload max 5 files" but only the first file was actually uploaded - **Agent conversational mode**: Frontend sent multiple files but backend only processed one - **Agent task-mode file inputs**: Explicitly limited to single file only This PR enables proper multi-file upload support for both chat and agent workflows, allowing users to upload and process multiple files (up to 5) as the UI originally suggested. **Changes:** - `web/src/pages/next-chats/hooks/use-upload-file.ts`: Process all files instead of only `files[0]` - `api/apps/canvas_app.py`: Handle multiple files via `files.getlist("file")` - `web/src/pages/agent/debug-content/uploader.tsx`: Allow up to 5 files with `multiple={true}` - `agent/component/begin.py` & `fillup.py`: Support file arrays while maintaining backward compatibility ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -45,11 +45,14 @@ class Begin(UserFillUp):
|
|||||||
if self.check_if_canceled("Begin processing"):
|
if self.check_if_canceled("Begin processing"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(v, dict) and v.get("type", "").lower().find("file") >=0:
|
if isinstance(v, dict) and v.get("type", "").lower().find("file") >= 0:
|
||||||
if v.get("optional") and v.get("value", None) is None:
|
if v.get("optional") and v.get("value", None) is None:
|
||||||
v = None
|
v = None
|
||||||
else:
|
else:
|
||||||
v = FileService.get_files([v["value"]])
|
file_value = v["value"]
|
||||||
|
# Support both single file (backward compatibility) and multiple files
|
||||||
|
files = file_value if isinstance(file_value, list) else [file_value]
|
||||||
|
v = FileService.get_files(files)
|
||||||
else:
|
else:
|
||||||
v = v.get("value")
|
v = v.get("value")
|
||||||
self.set_output(k, v)
|
self.set_output(k, v)
|
||||||
|
|||||||
@ -64,11 +64,14 @@ class UserFillUp(ComponentBase):
|
|||||||
for k, v in kwargs.get("inputs", {}).items():
|
for k, v in kwargs.get("inputs", {}).items():
|
||||||
if self.check_if_canceled("UserFillUp processing"):
|
if self.check_if_canceled("UserFillUp processing"):
|
||||||
return
|
return
|
||||||
if isinstance(v, dict) and v.get("type", "").lower().find("file") >=0:
|
if isinstance(v, dict) and v.get("type", "").lower().find("file") >= 0:
|
||||||
if v.get("optional") and v.get("value", None) is None:
|
if v.get("optional") and v.get("value", None) is None:
|
||||||
v = None
|
v = None
|
||||||
else:
|
else:
|
||||||
v = FileService.get_files([v["value"]])
|
file_value = v["value"]
|
||||||
|
# Support both single file (backward compatibility) and multiple files
|
||||||
|
files = file_value if isinstance(file_value, list) else [file_value]
|
||||||
|
v = FileService.get_files(files)
|
||||||
else:
|
else:
|
||||||
v = v.get("value")
|
v = v.get("value")
|
||||||
self.set_output(k, v)
|
self.set_output(k, v)
|
||||||
|
|||||||
@ -253,11 +253,14 @@ async def upload(canvas_id):
|
|||||||
|
|
||||||
user_id = cvs["user_id"]
|
user_id = cvs["user_id"]
|
||||||
files = await request.files
|
files = await request.files
|
||||||
file = files['file'] if files and files.get("file") else None
|
file_objs = files.getlist("file") if files and files.get("file") else []
|
||||||
try:
|
try:
|
||||||
return get_json_result(data=FileService.upload_info(user_id, file, request.args.get("url")))
|
if len(file_objs) == 1:
|
||||||
|
return get_json_result(data=FileService.upload_info(user_id, file_objs[0], request.args.get("url")))
|
||||||
|
results = [FileService.upload_info(user_id, f) for f in file_objs]
|
||||||
|
return get_json_result(data=results)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
@manager.route('/input_form', methods=['GET']) # noqa: F821
|
@manager.route('/input_form', methods=['GET']) # noqa: F821
|
||||||
|
|||||||
@ -70,6 +70,8 @@ const DebugContent = ({
|
|||||||
value = false;
|
value = false;
|
||||||
} else if (type === BeginQueryType.Integer || type === 'float') {
|
} else if (type === BeginQueryType.Integer || type === 'float') {
|
||||||
fieldSchema = z.coerce.number();
|
fieldSchema = z.coerce.number();
|
||||||
|
} else if (type === BeginQueryType.File) {
|
||||||
|
fieldSchema = z.array(z.record(z.any())).min(1);
|
||||||
} else {
|
} else {
|
||||||
fieldSchema = z.record(z.any());
|
fieldSchema = z.record(z.any());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,14 +19,18 @@ import * as React from 'react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type FileUploadDirectUploadProps = {
|
type FileUploadDirectUploadProps = {
|
||||||
value: Record<string, any>;
|
value: Record<string, any> | Record<string, any>[];
|
||||||
onChange(value: Record<string, any>): void;
|
onChange(value: Record<string, any>[]): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FileUploadDirectUpload({
|
export function FileUploadDirectUpload({
|
||||||
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: FileUploadDirectUploadProps) {
|
}: FileUploadDirectUploadProps) {
|
||||||
const [files, setFiles] = React.useState<File[]>([]);
|
const [files, setFiles] = React.useState<File[]>([]);
|
||||||
|
const uploadedFilesRef = React.useRef<Record<string, any>[]>(
|
||||||
|
Array.isArray(value) ? value : value ? [value] : [],
|
||||||
|
);
|
||||||
|
|
||||||
const { uploadCanvasFile } = useUploadCanvasFile();
|
const { uploadCanvasFile } = useUploadCanvasFile();
|
||||||
|
|
||||||
@ -44,7 +48,11 @@ export function FileUploadDirectUpload({
|
|||||||
const ret = await uploadCanvasFile([file]);
|
const ret = await uploadCanvasFile([file]);
|
||||||
if (ret.code === 0) {
|
if (ret.code === 0) {
|
||||||
onSuccess(file);
|
onSuccess(file);
|
||||||
onChange(ret.data);
|
uploadedFilesRef.current = [
|
||||||
|
...uploadedFilesRef.current,
|
||||||
|
ret.data,
|
||||||
|
];
|
||||||
|
onChange(uploadedFilesRef.current);
|
||||||
} else {
|
} else {
|
||||||
handleError();
|
handleError();
|
||||||
}
|
}
|
||||||
@ -69,15 +77,31 @@ export function FileUploadDirectUpload({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleFilesChange = React.useCallback(
|
||||||
|
(newFiles: File[]) => {
|
||||||
|
// Find removed files and update uploadedFilesRef
|
||||||
|
const removedFiles = files.filter((f) => !newFiles.includes(f));
|
||||||
|
if (removedFiles.length > 0) {
|
||||||
|
const removedIndices = removedFiles.map((f) => files.indexOf(f));
|
||||||
|
uploadedFilesRef.current = uploadedFilesRef.current.filter(
|
||||||
|
(_, idx) => !removedIndices.includes(idx),
|
||||||
|
);
|
||||||
|
onChange(uploadedFilesRef.current);
|
||||||
|
}
|
||||||
|
setFiles(newFiles);
|
||||||
|
},
|
||||||
|
[files, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileUpload
|
<FileUpload
|
||||||
value={files}
|
value={files}
|
||||||
onValueChange={setFiles}
|
onValueChange={handleFilesChange}
|
||||||
onUpload={onUpload}
|
onUpload={onUpload}
|
||||||
onFileReject={onFileReject}
|
onFileReject={onFileReject}
|
||||||
maxFiles={1}
|
maxFiles={5}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
multiple={false}
|
multiple={true}
|
||||||
>
|
>
|
||||||
<FileUploadDropzone>
|
<FileUploadDropzone>
|
||||||
<div className="flex flex-col items-center gap-1 text-center">
|
<div className="flex flex-col items-center gap-1 text-center">
|
||||||
@ -86,7 +110,7 @@ export function FileUploadDirectUpload({
|
|||||||
</div>
|
</div>
|
||||||
<p className="font-medium text-sm">Drag & drop files here</p>
|
<p className="font-medium text-sm">Drag & drop files here</p>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Or click to browse (max 1 files)
|
Or click to browse (max 5 files)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FileUploadTrigger asChild>
|
<FileUploadTrigger asChild>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
file: z.record(z.any()),
|
file: z.array(z.record(z.any())).min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormSchemaType = z.infer<typeof formSchema>;
|
export type FormSchemaType = z.infer<typeof formSchema>;
|
||||||
@ -25,7 +25,7 @@ export function UploaderForm({ ok, loading }: UploaderFormProps) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = useForm<FormSchemaType>({
|
const form = useForm<FormSchemaType>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {},
|
defaultValues: { file: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -29,15 +29,20 @@ export function useUploadFile() {
|
|||||||
conversationId?: string,
|
conversationId?: string,
|
||||||
) => {
|
) => {
|
||||||
if (Array.isArray(files) && files.length) {
|
if (Array.isArray(files) && files.length) {
|
||||||
const file = files[0];
|
for (const file of files) {
|
||||||
const ret = await uploadAndParseFile({ file, options, conversationId });
|
const ret = await uploadAndParseFile({
|
||||||
if (ret?.code === 0) {
|
file,
|
||||||
const data = ret.data;
|
options,
|
||||||
setCurrentFiles((list) => [...list, data]);
|
conversationId,
|
||||||
setFileMap((map) => {
|
|
||||||
map.set(files[0], data);
|
|
||||||
return map;
|
|
||||||
});
|
});
|
||||||
|
if (ret?.code === 0) {
|
||||||
|
const data = ret.data;
|
||||||
|
setCurrentFiles((list) => [...list, data]);
|
||||||
|
setFileMap((map) => {
|
||||||
|
map.set(file, data);
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user