"""Patch ChatInput.tsx: support uploading multiple files at once. Changes: 1. Add `multiple` to the so the user can select many files/images in one pick. 2. Rewrite handleDatasetFileChange to send ALL selected files in a single multipart request (backend returns list[DatasetUploadResponse]). """ import os FILE = "/source/frontend/src/components/Chat/ChatInput.tsx" def patch(): if not os.path.exists(FILE): print(f"SKIP: {FILE} not found") return with open(FILE, "r", encoding="utf-8") as f: content = f.read() # ---- 1. Add `multiple` to the file input ---- old_input = '' new_input = '' if old_input in content: content = content.replace(old_input, new_input) print("OK: added multiple to file input") else: print("WARN: file input block not matched exactly") # ---- 2. Rewrite handleDatasetFileChange ---- old_handler_start = " const handleDatasetFileChange = useCallback(" start = content.find(old_handler_start) if start == -1: print("FAIL: handleDatasetFileChange not found") return # End of the handler: the " );" right after the deps line. deps = " [sessionId, onDatasetUploaded],\n );" end = content.find(deps, start) if end != -1: end += len(deps) else: print("FAIL: could not find end of handler") return new_handler = ''' const handleDatasetFileChange = useCallback( async (event: React.ChangeEvent) => { const files = Array.from(event.target.files ?? []); event.target.value = ''; if (!files.length) return; if (!sessionId) { setDatasetUploadError('Start a session before uploading files.'); return; } // Validate each file before uploading the batch. for (const file of files) { const ext = file.name.split('.').pop()?.toLowerCase() || ''; if (!DATASET_UPLOAD_EXTENSIONS.has(ext)) { setDatasetUploadError('Không hỗ trợ định dạng này. Các định dạng được hỗ trợ: CSV, JSON, JSONL, PDF, ảnh (PNG/JPG/JPEG/WebP/BMP/TIFF/GIF), Excel (XLSX/XLSM/XLS), Word (DOCX/DOC), PowerPoint (PPTX), OpenDocument (ODT/ODS/ODP), và văn bản (TXT/MD/RTF/LOG).'); return; } if (file.size > MAX_DATASET_UPLOAD_BYTES) { setDatasetUploadError(`File must be 100 MB or smaller. ${file.name} is ${formatBytes(file.size)}.`); return; } if (file.size === 0) { setDatasetUploadError(`Uploaded file is empty: ${file.name}`); return; } } // Send all files in one request; backend returns a list. const formData = new FormData(); for (const file of files) { formData.append('file', file); } setIsUploadingDataset(true); setDatasetUploadProgress(0); setDatasetUploadError(null); setDatasetUploadSuccess(null); try { const res = await apiUpload(`/api/session/${sessionId}/datasets`, formData, { onProgress: ({ percent }) => { setDatasetUploadProgress(percent !== null && percent < 100 ? percent : null); }, }); if (!res.ok) { setDatasetUploadError(await readApiErrorMessage(res, 'File upload failed.')); return; } const payload = await res.json() as DatasetUploadResponse[]; if (payload.length) { setUploadedDatasets((previous) => [...payload, ...previous]); const names = payload.map((u) => u.filename).join(', '); setDatasetUploadSuccess(`Đã tải lên ${payload.length} file: ${names}`); } await onDatasetUploaded?.(); } catch (error) { setDatasetUploadError( error instanceof Error ? error.message : 'File upload failed.' ); } finally { setIsUploadingDataset(false); setDatasetUploadProgress(null); } }, [sessionId, onDatasetUploaded], );''' content = content[:start] + new_handler + content[end:] with open(FILE, "w", encoding="utf-8") as f: f.write(content) print("OK: ChatInput.tsx multi-file upload patched") if __name__ == "__main__": patch()