mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-15 09:31:21 +00:00
feat: browse and attach knowledge base directories from the Attach Knowledge panel
This commit is contained in:
parent
bd0b3875ba
commit
5cf98c7452
3 changed files with 147 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<div class="pl-3 mb-1 flex flex-col gap-0.5">
|
||||
{#if selectedFileBreadcrumbs.length > 0}
|
||||
<div
|
||||
class="px-2 py-1 flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||
on:click={() => navigateToDirectory(null)}
|
||||
>
|
||||
<ArrowLeft className="size-3" strokeWidth="2" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-gray-700 dark:hover:text-gray-200 transition truncate"
|
||||
on:click={() => navigateToDirectory(null)}
|
||||
>
|
||||
{decodeString(selectedItem?.name)}
|
||||
</button>
|
||||
{#each selectedFileBreadcrumbs as crumb, i}
|
||||
<span class="flex-shrink-0">/</span>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-gray-700 dark:hover:text-gray-200 transition truncate"
|
||||
on:click={() => navigateToDirectory(crumb.id)}
|
||||
>
|
||||
{crumb.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedFileItems === null && selectedFileItemsTotal === null}
|
||||
<div class=" py-1 flex justify-center">
|
||||
<Spinner className="size-3" />
|
||||
</div>
|
||||
{:else if selectedFileItemsTotal === 0}
|
||||
{:else if selectedFileItemsTotal === 0 && selectedFileDirectories.length === 0}
|
||||
<div class=" text-xs text-gray-500 dark:text-gray-400 italic py-0.5 px-2">
|
||||
{$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.')}
|
||||
</div>
|
||||
{:else}
|
||||
{#each selectedFileDirectories as dir (dir.id)}
|
||||
<div
|
||||
class="h-[1.6875rem] px-2 rounded-xl w-full text-left flex items-center text-[0.8125rem] font-normal hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 flex min-w-0 items-center gap-1.5"
|
||||
on:click={() => navigateToDirectory(dir.id)}
|
||||
>
|
||||
<Tooltip content={$i18n.t('Directory')} placement="top">
|
||||
<Folder className="size-3.5" />
|
||||
</Tooltip>
|
||||
<Tooltip content={dir.name} placement="top-start">
|
||||
<div class="line-clamp-1 flex-1 text-[0.8125rem]">
|
||||
{dir.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-0.5 ml-1">
|
||||
{#if dir.file_count}
|
||||
<Tooltip content={$i18n.t('Select directory')} placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition opacity-40 hover:opacity-100"
|
||||
on:click|stopPropagation={() => selectDirectory(dir)}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
<ChevronRight className="size-3 opacity-30" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#each selectedFileItems as file, fileIdx (file.id)}
|
||||
<button
|
||||
class=" h-[1.6875rem] px-2 rounded-xl w-full text-left flex justify-between items-center text-[0.8125rem] font-normal hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100"
|
||||
|
|
@ -287,7 +405,7 @@
|
|||
}}
|
||||
>
|
||||
<div class=" flex items-center gap-1.5">
|
||||
<Tooltip content={$i18n.t('Collection')} placement="top">
|
||||
<Tooltip content={$i18n.t('File')} placement="top">
|
||||
<DocumentPage className="size-3.5" />
|
||||
</Tooltip>
|
||||
|
||||
|
|
|
|||
|
|
@ -843,6 +843,7 @@
|
|||
"Direct Connections allow users to connect to their own OpenAI compatible API endpoints.": "",
|
||||
"Direct Message": "",
|
||||
"Direct Tool Servers": "",
|
||||
"Directory": "",
|
||||
"Directory created.": "",
|
||||
"Directory deleted.": "",
|
||||
"Directory moved.": "",
|
||||
|
|
@ -2490,6 +2491,7 @@
|
|||
"Select an Ollama instance": "",
|
||||
"Select an option": "",
|
||||
"Select an output format": "",
|
||||
"Select directory": "",
|
||||
"Select dtype": "",
|
||||
"Select Engine": "",
|
||||
"Select Filter": "",
|
||||
|
|
@ -2797,6 +2799,7 @@
|
|||
"This action cannot be undone. Do you wish to continue?": "",
|
||||
"This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.": "",
|
||||
"This chat won't appear in history and your messages will not be saved.": "",
|
||||
"This directory is empty.": "",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "",
|
||||
"This folder is empty": "",
|
||||
"This is a default user permission and will remain enabled.": "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue