feat: browse and attach knowledge base directories from the Attach Knowledge panel

This commit is contained in:
silentoplayz 2026-09-04 14:46:21 -04:00
parent bd0b3875ba
commit 5cf98c7452
3 changed files with 147 additions and 5 deletions

View file

@ -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,

View file

@ -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>

View file

@ -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.": "",