diff --git a/agent/component/begin.py b/agent/component/begin.py index bcbfdbf24..819e46c25 100644 --- a/agent/component/begin.py +++ b/agent/component/begin.py @@ -45,11 +45,14 @@ class Begin(UserFillUp): if self.check_if_canceled("Begin processing"): 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: v = None 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: v = v.get("value") self.set_output(k, v) diff --git a/agent/component/fillup.py b/agent/component/fillup.py index 10163d10c..b97e6ca52 100644 --- a/agent/component/fillup.py +++ b/agent/component/fillup.py @@ -64,11 +64,14 @@ class UserFillUp(ComponentBase): for k, v in kwargs.get("inputs", {}).items(): if self.check_if_canceled("UserFillUp processing"): 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: v = None 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: v = v.get("value") self.set_output(k, v) diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 6f73c68cb..228498e50 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -253,11 +253,14 @@ async def upload(canvas_id): user_id = cvs["user_id"] 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: - 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: - return server_error_response(e) + return server_error_response(e) @manager.route('/input_form', methods=['GET']) # noqa: F821 diff --git a/web/src/pages/agent/debug-content/index.tsx b/web/src/pages/agent/debug-content/index.tsx index 64fa7e219..c0d753bc3 100644 --- a/web/src/pages/agent/debug-content/index.tsx +++ b/web/src/pages/agent/debug-content/index.tsx @@ -70,6 +70,8 @@ const DebugContent = ({ value = false; } else if (type === BeginQueryType.Integer || type === 'float') { fieldSchema = z.coerce.number(); + } else if (type === BeginQueryType.File) { + fieldSchema = z.array(z.record(z.any())).min(1); } else { fieldSchema = z.record(z.any()); } diff --git a/web/src/pages/agent/debug-content/uploader.tsx b/web/src/pages/agent/debug-content/uploader.tsx index 88465d15c..b0918bf25 100644 --- a/web/src/pages/agent/debug-content/uploader.tsx +++ b/web/src/pages/agent/debug-content/uploader.tsx @@ -19,14 +19,18 @@ import * as React from 'react'; import { toast } from 'sonner'; type FileUploadDirectUploadProps = { - value: Record; - onChange(value: Record): void; + value: Record | Record[]; + onChange(value: Record[]): void; }; export function FileUploadDirectUpload({ + value, onChange, }: FileUploadDirectUploadProps) { const [files, setFiles] = React.useState([]); + const uploadedFilesRef = React.useRef[]>( + Array.isArray(value) ? value : value ? [value] : [], + ); const { uploadCanvasFile } = useUploadCanvasFile(); @@ -44,7 +48,11 @@ export function FileUploadDirectUpload({ const ret = await uploadCanvasFile([file]); if (ret.code === 0) { onSuccess(file); - onChange(ret.data); + uploadedFilesRef.current = [ + ...uploadedFilesRef.current, + ret.data, + ]; + onChange(uploadedFilesRef.current); } else { 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 (
@@ -86,7 +110,7 @@ export function FileUploadDirectUpload({

Drag & drop files here

- Or click to browse (max 1 files) + Or click to browse (max 5 files)

diff --git a/web/src/pages/agent/pipeline-run-sheet/uploader.tsx b/web/src/pages/agent/pipeline-run-sheet/uploader.tsx index db4a977dd..4670fd2f4 100644 --- a/web/src/pages/agent/pipeline-run-sheet/uploader.tsx +++ b/web/src/pages/agent/pipeline-run-sheet/uploader.tsx @@ -11,7 +11,7 @@ import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; const formSchema = z.object({ - file: z.record(z.any()), + file: z.array(z.record(z.any())).min(1), }); export type FormSchemaType = z.infer; @@ -25,7 +25,7 @@ export function UploaderForm({ ok, loading }: UploaderFormProps) { const { t } = useTranslation(); const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: {}, + defaultValues: { file: [] }, }); return ( diff --git a/web/src/pages/next-chats/hooks/use-upload-file.ts b/web/src/pages/next-chats/hooks/use-upload-file.ts index c37a461e9..a38015e3d 100644 --- a/web/src/pages/next-chats/hooks/use-upload-file.ts +++ b/web/src/pages/next-chats/hooks/use-upload-file.ts @@ -29,15 +29,20 @@ export function useUploadFile() { conversationId?: string, ) => { if (Array.isArray(files) && files.length) { - const file = files[0]; - const ret = await uploadAndParseFile({ file, options, conversationId }); - if (ret?.code === 0) { - const data = ret.data; - setCurrentFiles((list) => [...list, data]); - setFileMap((map) => { - map.set(files[0], data); - return map; + for (const file of files) { + const ret = await uploadAndParseFile({ + file, + options, + conversationId, }); + if (ret?.code === 0) { + const data = ret.data; + setCurrentFiles((list) => [...list, data]); + setFileMap((map) => { + map.set(file, data); + return map; + }); + } } } },