diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index 848232d6f2..349e52ee5b 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -140,6 +140,8 @@ class KnowledgeDirectoryModel(BaseModel): created_at: int # timestamp in epoch updated_at: int # timestamp in epoch + file_count: Optional[int] = None + class KnowledgeDirectoryForm(BaseModel): name: str @@ -864,7 +866,26 @@ class KnowledgeTable: stmt = stmt.order_by(KnowledgeDirectory.name.asc()) result = await db.execute(stmt) - return [KnowledgeDirectoryModel.model_validate(d) for d in result.scalars().all()] + directories = result.scalars().all() + + file_counts = {} + if directories: + file_count_result = await db.execute( + select(KnowledgeFile.directory_id, func.count(KnowledgeFile.id)) + .where(KnowledgeFile.directory_id.in_([d.id for d in directories])) + .group_by(KnowledgeFile.directory_id) + ) + file_counts = dict(file_count_result.all()) + + return [ + KnowledgeDirectoryModel.model_validate( + { + **KnowledgeDirectoryModel.model_validate(d).model_dump(), + 'file_count': file_counts.get(d.id, 0), + } + ) + for d in directories + ] async def get_all_directories( self, diff --git a/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte b/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte index 1a1bd28b3e..cef051edb5 100644 --- a/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte @@ -7,11 +7,14 @@ import Tooltip from '$lib/components/common/Tooltip.svelte'; import Database from '$lib/components/icons/Database.svelte'; import DocumentPage from '$lib/components/icons/DocumentPage.svelte'; + import Folder from '$lib/components/icons/Folder.svelte'; + import Plus from '$lib/components/icons/Plus.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import Loader from '$lib/components/common/Loader.svelte'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; import ChevronRight from '$lib/components/icons/ChevronRight.svelte'; import SearchInput from './SearchInput.svelte'; + import ArrowLeft from '$lib/components/icons/ArrowLeft.svelte'; const i18n = getContext('i18n'); @@ -23,6 +26,10 @@ let selectedItem = null; + let currentDirectoryId = null; + let selectedFileDirectories = []; + let selectedFileBreadcrumbs = []; + let selectedFileItemsPage = 1; let selectedFileItems = null; @@ -33,6 +40,9 @@ let selectedFileRequestId = 0; $: if (selectedItem) { + currentDirectoryId = null; + selectedFileDirectories = []; + selectedFileBreadcrumbs = []; initSelectedFileItems(); } @@ -64,7 +74,8 @@ null, null, null, - selectedFileItemsPage + selectedFileItemsPage, + currentDirectoryId ).catch(() => { return null; }); @@ -72,6 +83,8 @@ if (res) { selectedFileItemsTotal = res.total; + selectedFileDirectories = res.directories || []; + selectedFileBreadcrumbs = res.breadcrumbs || []; const pageItems = res.items; if ((pageItems ?? []).length === 0) { @@ -93,6 +106,41 @@ return res; }; + const navigateToDirectory = async (dirId) => { + currentDirectoryId = dirId; + await initSelectedFileItems(); + }; + + const selectDirectory = async (dir) => { + const allFiles = []; + let page = 1; + while (true) { + const res = await searchKnowledgeFilesById( + localStorage.token, + selectedItem.id, + null, + null, + null, + null, + page, + dir.id + ).catch(() => null); + + if (!res || !res.items || res.items.length === 0) break; + allFiles.push(...res.items); + if (allFiles.length >= res.total) break; + page++; + } + + for (const file of allFiles) { + onSelect({ + type: 'file', + name: file?.meta?.name, + ...file + }); + } + }; + let page = 1; let items = []; let total = null; @@ -264,15 +312,85 @@ {#if selectedItem && selectedItem.id === item.id}
+ {#if selectedFileBreadcrumbs.length > 0} +
+ + + + {#each selectedFileBreadcrumbs as crumb, i} + / + + {/each} +
+ {/if} + {#if selectedFileItems === null && selectedFileItemsTotal === null}
- {:else if selectedFileItemsTotal === 0} + {:else if selectedFileItemsTotal === 0 && selectedFileDirectories.length === 0}
- {$i18n.t('No files in this knowledge base.')} + {selectedFileBreadcrumbs.length > 0 + ? $i18n.t('This directory is empty.') + : $i18n.t('No files in this knowledge base.')}
{:else} + {#each selectedFileDirectories as dir (dir.id)} +
+ + +
+ {#if dir.file_count} + + + + {/if} + +
+
+ {/each} + {#each selectedFileItems as file, fileIdx (file.id)}