diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 825565e48d..99535cd296 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -115,7 +115,7 @@ async def process_uploaded_file( file_path_processed = Storage.get_file(file_path) result = transcribe(request, file_path_processed, file_metadata, user) - process_file( + await process_file( request, ProcessFileForm(file_id=file_item.id, content=result.get('text', '')), user=user, @@ -124,7 +124,7 @@ async def process_uploaded_file( elif (not content_type.startswith(('image/', 'video/'))) or ( request.app.state.config.CONTENT_EXTRACTION_ENGINE == 'external' ): - process_file( + await process_file( request, ProcessFileForm(file_id=file_item.id), user=user, @@ -134,7 +134,7 @@ async def process_uploaded_file( raise Exception(f'File type {content_type} is not supported for processing') else: log.info(f'File type {file.content_type} is not provided, but trying to process anyway') - process_file( + await process_file( request, ProcessFileForm(file_id=file_item.id), user=user, @@ -153,10 +153,10 @@ async def process_uploaded_file( ) if db: - _process_handler(db) + await _process_handler(db) else: with SessionLocal() as db_session: - _process_handler(db_session) + await _process_handler(db_session) @router.post('/', response_model=FileModelResponse) diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index d1deba076b..f04d0efea1 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -469,45 +469,78 @@ input.directory = true; input.multiple = true; input.style.display = 'none'; - + document.body.appendChild(input); - + + // Cancelling the native file picker does not always fire + // 'change' or 'error' — especially in Firefox, where the + // picker can dismiss without any event, leaving the caller + // stuck on "Scanning directory..." forever. Use 'cancel' + // (modern browsers) plus a window-focus + delayed sentinel + // fallback so the Promise ALWAYS settles: either we got + // files, or we resolve with an empty list and the caller's + // existing empty-check toasts. + let settled = false; + const finish = (err?: unknown) => { + if (settled) return; + settled = true; + if (input.parentNode) { + input.parentNode.removeChild(input); + } + window.removeEventListener('focus', onFocus); + if (err) { + reject(err); + } else { + resolve(); + } + }; + const onFocus = () => { + // 'change' fires after 'focus' returns to the window, so + // wait briefly before deciding the user cancelled. + setTimeout(() => finish(), 500); + }; + input.onchange = async () => { try { const inputFiles = Array.from(input.files || []).filter( (file) => !hasHiddenFolder(file.webkitRelativePath) && !file.name.startsWith('.') ); - + for (const file of inputFiles) { const relativePath = file.webkitRelativePath || file.name; const fileWithPath = new File([file], relativePath, { type: file.type }); - + const collectedFile: CollectedFile = { file: fileWithPath, path: relativePath, size: file.size }; - + if (withHashes) { collectedFile.hash = await calculateFileHash(file); } - + files.push(collectedFile); } - - document.body.removeChild(input); - resolve(); + + finish(); } catch (error) { - document.body.removeChild(input); - reject(error); + finish(error); } }; - + input.onerror = (error) => { - document.body.removeChild(input); - reject(error); + finish(error); }; - + + // Newer browsers fire 'cancel' when the picker is dismissed + // without a selection; cast because older lib.dom typings may + // not yet declare it. + (input as any).oncancel = () => { + finish(); + }; + + window.addEventListener('focus', onFocus, { once: true }); input.click(); }); }