mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
Merge 466efe3b70 into 240c795efe
This commit is contained in:
commit
33d29f4230
4 changed files with 906 additions and 97 deletions
|
|
@ -160,6 +160,15 @@
|
|||
type: 'file',
|
||||
data: data
|
||||
});
|
||||
} else if (type === 'knowledge-directory') {
|
||||
insertTextHandler('');
|
||||
|
||||
for (const file of data.files ?? []) {
|
||||
onUpload({
|
||||
type: 'file',
|
||||
data: file
|
||||
});
|
||||
}
|
||||
} else if (type === 'web') {
|
||||
insertTextHandler('');
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
import { folders } from '$lib/stores';
|
||||
import { getFolders } from '$lib/apis/folders';
|
||||
import { searchKnowledgeBases, searchKnowledgeFiles } from '$lib/apis/knowledge';
|
||||
import {
|
||||
searchKnowledgeBases,
|
||||
searchKnowledgeFiles,
|
||||
searchKnowledgeFilesById
|
||||
} from '$lib/apis/knowledge';
|
||||
import { removeLastWordFromString, isValidHttpUrl, isYoutubeUrl, decodeString } from '$lib/utils';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -17,6 +21,10 @@
|
|||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||
import Youtube from '$lib/components/icons/Youtube.svelte';
|
||||
import Folder from '$lib/components/icons/Folder.svelte';
|
||||
import ArrowLeft from '$lib/components/icons/ArrowLeft.svelte';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -27,21 +35,49 @@
|
|||
let items = [];
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
let browsingCollection = null;
|
||||
let currentDirectoryId = null;
|
||||
let browsingFiles = [];
|
||||
let browsingDirectories = [];
|
||||
let browsingBreadcrumbs = [];
|
||||
let browsingLoading = false;
|
||||
|
||||
export let filteredItems = [];
|
||||
$: filteredItems = [
|
||||
...(query.startsWith('http')
|
||||
? isYoutubeUrl(query)
|
||||
? [{ type: 'youtube', name: query, description: query }]
|
||||
: [
|
||||
{
|
||||
type: 'web',
|
||||
name: query,
|
||||
description: query
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...items
|
||||
];
|
||||
$: {
|
||||
if (browsingCollection) {
|
||||
const dirItems = browsingDirectories.map((d) => ({
|
||||
...d,
|
||||
type: 'directory'
|
||||
}));
|
||||
const fItems = browsingFiles.map((f) => ({
|
||||
...f,
|
||||
type: 'file',
|
||||
name: f?.meta?.name || f.filename
|
||||
}));
|
||||
const combined = [...dirItems, ...fItems];
|
||||
filteredItems =
|
||||
combined.length > 0
|
||||
? combined
|
||||
: browsingLoading
|
||||
? [{ type: 'browsing-loading', name: '' }]
|
||||
: [{ type: 'browsing-empty', name: '' }];
|
||||
} else {
|
||||
filteredItems = [
|
||||
...(query.startsWith('http')
|
||||
? isYoutubeUrl(query)
|
||||
? [{ type: 'youtube', name: query, description: query }]
|
||||
: [
|
||||
{
|
||||
type: 'web',
|
||||
name: query,
|
||||
description: query
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...items
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$: if (query) {
|
||||
selectedIdx = 0;
|
||||
|
|
@ -73,7 +109,11 @@
|
|||
$: if (query !== undefined) {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
getItems();
|
||||
if (browsingCollection) {
|
||||
loadBrowsingItems();
|
||||
} else {
|
||||
getItems();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +170,125 @@
|
|||
}
|
||||
};
|
||||
|
||||
const browseIntoCollection = async (collection) => {
|
||||
browsingCollection = collection;
|
||||
currentDirectoryId = null;
|
||||
selectedIdx = 0;
|
||||
await loadBrowsingItems();
|
||||
};
|
||||
|
||||
const browseIntoDirectory = async (dir) => {
|
||||
currentDirectoryId = dir.id;
|
||||
selectedIdx = 0;
|
||||
await loadBrowsingItems();
|
||||
};
|
||||
|
||||
const navigateBack = async () => {
|
||||
if (browsingBreadcrumbs.length > 0) {
|
||||
const parentCrumb =
|
||||
browsingBreadcrumbs.length >= 2
|
||||
? browsingBreadcrumbs[browsingBreadcrumbs.length - 2]
|
||||
: null;
|
||||
currentDirectoryId = parentCrumb ? parentCrumb.id : null;
|
||||
selectedIdx = 0;
|
||||
await loadBrowsingItems();
|
||||
} else {
|
||||
browsingCollection = null;
|
||||
currentDirectoryId = null;
|
||||
browsingFiles = [];
|
||||
browsingDirectories = [];
|
||||
browsingBreadcrumbs = [];
|
||||
selectedIdx = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const loadBrowsingItems = async () => {
|
||||
if (!browsingCollection) return;
|
||||
browsingLoading = true;
|
||||
|
||||
const res = await searchKnowledgeFilesById(
|
||||
localStorage.token,
|
||||
browsingCollection.id,
|
||||
query || null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
currentDirectoryId
|
||||
).catch(() => null);
|
||||
|
||||
if (res) {
|
||||
browsingFiles = res.items || [];
|
||||
browsingDirectories = res.directories || [];
|
||||
browsingBreadcrumbs = res.breadcrumbs || [];
|
||||
}
|
||||
|
||||
browsingLoading = false;
|
||||
};
|
||||
|
||||
const selectDirectory = async (dir) => {
|
||||
const allFiles = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const res = await searchKnowledgeFilesById(
|
||||
localStorage.token,
|
||||
browsingCollection.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++;
|
||||
}
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
toast.info($i18n.t('No files in this directory.'));
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect({
|
||||
type: 'knowledge-directory',
|
||||
data: {
|
||||
files: allFiles.map((file) => ({
|
||||
...file,
|
||||
type: 'file',
|
||||
name: file?.meta?.name || file.filename,
|
||||
collection: { name: browsingCollection.name }
|
||||
}))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectBrowsingFile = (file) => {
|
||||
onSelect({
|
||||
type: 'knowledge',
|
||||
data: {
|
||||
...file,
|
||||
type: 'file',
|
||||
name: file?.meta?.name || file.filename,
|
||||
collection: { name: browsingCollection.name }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectEntireCollection = () => {
|
||||
onSelect({
|
||||
type: 'knowledge',
|
||||
data: { ...browsingCollection, type: 'collection' }
|
||||
});
|
||||
};
|
||||
|
||||
$: currentLocationName =
|
||||
browsingBreadcrumbs.length > 0
|
||||
? browsingBreadcrumbs[browsingBreadcrumbs.length - 1]?.name
|
||||
: (browsingCollection?.name ?? '');
|
||||
|
||||
onMount(async () => {
|
||||
if ($folders === null) {
|
||||
await folders.set(await getFolders(localStorage.token));
|
||||
|
|
@ -139,7 +298,159 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0 || query.startsWith('http')}
|
||||
{#if browsingCollection}
|
||||
<div class="px-2 py-1.5 flex items-center gap-1.5 border-b border-gray-100 dark:border-gray-800">
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||
on:click={navigateBack}
|
||||
>
|
||||
<ArrowLeft className="size-3.5" strokeWidth="2" />
|
||||
</button>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 truncate flex items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-gray-700 dark:hover:text-gray-200 transition truncate"
|
||||
on:click={() => {
|
||||
if (browsingBreadcrumbs.length > 0) {
|
||||
currentDirectoryId = null;
|
||||
selectedIdx = 0;
|
||||
loadBrowsingItems();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{decodeString(browsingCollection?.name)}
|
||||
</button>
|
||||
{#each browsingBreadcrumbs 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={() => {
|
||||
currentDirectoryId = crumb.id;
|
||||
selectedIdx = 0;
|
||||
loadBrowsingItems();
|
||||
}}
|
||||
>
|
||||
{crumb.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
content={browsingBreadcrumbs.length > 0
|
||||
? $i18n.t('Select directory')
|
||||
: $i18n.t('Select collection')}
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition opacity-60 hover:opacity-100"
|
||||
on:click={() => {
|
||||
if (browsingBreadcrumbs.length > 0) {
|
||||
selectDirectory({
|
||||
id: currentDirectoryId,
|
||||
name: currentLocationName
|
||||
});
|
||||
} else {
|
||||
selectEntireCollection();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{#if browsingLoading}
|
||||
<div class="flex justify-center py-3">
|
||||
<Spinner className="size-3.5" />
|
||||
</div>
|
||||
{:else if browsingDirectories.length === 0 && browsingFiles.length === 0}
|
||||
<div class="px-2 py-3 text-center text-xs text-gray-400 dark:text-gray-500 italic">
|
||||
{$i18n.t('This directory is empty.')}
|
||||
</div>
|
||||
{:else}
|
||||
{@const allBrowsingItems = [
|
||||
...browsingDirectories.map((d) => ({ ...d, _itemType: 'directory' })),
|
||||
...browsingFiles.map((f) => ({
|
||||
...f,
|
||||
_itemType: 'file',
|
||||
name: f?.meta?.name || f.filename
|
||||
}))
|
||||
]}
|
||||
|
||||
{#each allBrowsingItems as item, idx}
|
||||
{#if idx === 0 || item._itemType !== allBrowsingItems[idx - 1]?._itemType}
|
||||
<div class="px-2 py-1 text-[0.6875rem] text-gray-500 dark:text-gray-400">
|
||||
{#if item._itemType === 'directory'}
|
||||
{$i18n.t('Directories')}
|
||||
{:else}
|
||||
{$i18n.t('Files')}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if item._itemType === 'directory'}
|
||||
<div
|
||||
class="flex h-[1.6875rem] w-full items-center justify-between rounded-xl px-2 text-left text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {idx ===
|
||||
selectedIdx
|
||||
? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center gap-1.5 text-left text-black dark:text-gray-100"
|
||||
data-selected={idx === selectedIdx}
|
||||
on:click={() => browseIntoDirectory(item)}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = idx;
|
||||
}}
|
||||
>
|
||||
<Folder className="size-3.5" />
|
||||
<Tooltip content={item.name} placement="top-start">
|
||||
<div class="min-w-0 flex-1 truncate">{item.name}</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
||||
<div class="ml-1 flex items-center gap-0.5">
|
||||
<Tooltip content={$i18n.t('Select directory')} placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-0.5 opacity-40 transition hover:bg-gray-200 hover:opacity-100 dark:hover:bg-gray-700"
|
||||
on:click|stopPropagation={() => selectDirectory(item)}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ChevronRight className="size-3 opacity-30" />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full items-center gap-1.5 rounded-xl px-2 text-left text-[0.8125rem] text-black hover:bg-gray-50/40 dark:text-gray-100 dark:hover:bg-gray-800/40 {idx ===
|
||||
selectedIdx
|
||||
? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
data-selected={idx === selectedIdx}
|
||||
on:click={() => selectBrowsingFile(item)}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = idx;
|
||||
}}
|
||||
>
|
||||
<DocumentPage className="size-3.5" />
|
||||
<Tooltip content={decodeString(item.name)} placement="top-start">
|
||||
<div class="min-w-0 flex-1 truncate">{decodeString(item.name)}</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if filteredItems.length > 0 || query.startsWith('http')}
|
||||
{#each filteredItems as item, idx}
|
||||
{#if idx === 0 || item?.type !== items[idx - 1]?.type}
|
||||
<div class="px-2 py-1 text-[0.6875rem] text-gray-500 dark:text-gray-400">
|
||||
|
|
@ -154,51 +465,97 @@
|
|||
{/if}
|
||||
|
||||
{#if !['youtube', 'web'].includes(item.type)}
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full items-center justify-between rounded-xl px-2 text-left text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {idx ===
|
||||
selectedIdx
|
||||
? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
console.log(item);
|
||||
onSelect({
|
||||
type: 'knowledge',
|
||||
data: item
|
||||
});
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = idx;
|
||||
}}
|
||||
data-selected={idx === selectedIdx}
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1.5 text-black dark:text-gray-100">
|
||||
<Tooltip
|
||||
content={item?.legacy
|
||||
? $i18n.t('Legacy')
|
||||
: item?.type === 'file'
|
||||
? `${item?.collection?.name} > ${$i18n.t('File')}`
|
||||
: item?.type === 'collection'
|
||||
? $i18n.t('Collection')
|
||||
: ''}
|
||||
placement="top"
|
||||
{#if item.type === 'collection'}
|
||||
<div
|
||||
class="flex h-[1.6875rem] w-full items-center justify-between rounded-xl px-2 text-left text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {idx ===
|
||||
selectedIdx
|
||||
? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center gap-1.5 text-left text-black dark:text-gray-100"
|
||||
data-selected={idx === selectedIdx}
|
||||
on:click={() => browseIntoCollection(item)}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = idx;
|
||||
}}
|
||||
>
|
||||
{#if item?.type === 'collection'}
|
||||
<Tooltip
|
||||
content={item?.legacy ? $i18n.t('Legacy') : $i18n.t('Collection')}
|
||||
placement="top"
|
||||
>
|
||||
<Database className="size-3.5" />
|
||||
{:else if item?.type === 'folder'}
|
||||
<Folder className="size-3.5" />
|
||||
{:else}
|
||||
<DocumentPage className="size-3.5" />
|
||||
{/if}
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={`${decodeString(item?.name)}`} placement="top-start">
|
||||
<div class="min-w-0 flex-1 truncate">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip content={`${decodeString(item?.name)}`} placement="top-start">
|
||||
<div class="min-w-0 flex-1 truncate">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
||||
<div class="ml-1 flex items-center gap-0.5">
|
||||
<Tooltip content={$i18n.t('Select collection')} placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-0.5 opacity-40 transition hover:bg-gray-200 hover:opacity-100 dark:hover:bg-gray-700"
|
||||
on:click|stopPropagation={() => {
|
||||
onSelect({
|
||||
type: 'knowledge',
|
||||
data: { ...item, type: 'collection' }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ChevronRight className="size-3 opacity-30" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full items-center justify-between rounded-xl px-2 text-left text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {idx ===
|
||||
selectedIdx
|
||||
? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
console.log(item);
|
||||
onSelect({
|
||||
type: 'knowledge',
|
||||
data: item
|
||||
});
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = idx;
|
||||
}}
|
||||
data-selected={idx === selectedIdx}
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1.5 text-black dark:text-gray-100">
|
||||
<Tooltip
|
||||
content={item?.legacy
|
||||
? $i18n.t('Legacy')
|
||||
: item?.type === 'file'
|
||||
? `${item?.collection?.name} > ${$i18n.t('File')}`
|
||||
: ''}
|
||||
placement="top"
|
||||
>
|
||||
{#if item?.type === 'folder'}
|
||||
<Folder className="size-3.5" />
|
||||
{:else}
|
||||
<DocumentPage className="size-3.5" />
|
||||
{/if}
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={`${decodeString(item?.name)}`} placement="top-start">
|
||||
<div class="min-w-0 flex-1 truncate">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,47 @@
|
|||
return res;
|
||||
};
|
||||
|
||||
const navigateToDirectory = async (dirId) => {
|
||||
currentDirectoryId = dirId;
|
||||
selectedFileItemsPage = 1;
|
||||
selectedFileItems = null;
|
||||
selectedFileItemsTotal = null;
|
||||
selectedFileAllItemsLoaded = false;
|
||||
selectedFileItemsLoading = false;
|
||||
await tick();
|
||||
await getSelectedFileItemsPage();
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
@ -268,11 +322,77 @@
|
|||
<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.')}
|
||||
</div>
|
||||
{:else}
|
||||
{#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}
|
||||
|
||||
{#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">
|
||||
<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>
|
||||
<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 +407,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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { onMount, onDestroy, getContext, createEventDispatcher } from 'svelte';
|
||||
import { onMount, onDestroy, getContext, createEventDispatcher, tick } from 'svelte';
|
||||
import { searchNotes } from '$lib/apis/notes';
|
||||
import { searchKnowledgeBases, searchKnowledgeFiles } from '$lib/apis/knowledge';
|
||||
import {
|
||||
searchKnowledgeBases,
|
||||
searchKnowledgeFiles,
|
||||
searchKnowledgeFilesById
|
||||
} from '$lib/apis/knowledge';
|
||||
|
||||
import { decodeString } from '$lib/utils';
|
||||
|
||||
|
|
@ -15,6 +19,10 @@
|
|||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import PageEdit from '$lib/components/icons/PageEdit.svelte';
|
||||
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
|
||||
import Folder from '$lib/components/icons/Folder.svelte';
|
||||
import ArrowLeft from '$lib/components/icons/ArrowLeft.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
|
@ -34,10 +42,21 @@
|
|||
|
||||
$: items = [...noteItems, ...knowledgeItems, ...fileItems];
|
||||
|
||||
let browsingCollection = null;
|
||||
let currentDirectoryId = null;
|
||||
let browsingFiles = [];
|
||||
let browsingDirectories = [];
|
||||
let browsingBreadcrumbs = [];
|
||||
let browsingLoading = false;
|
||||
|
||||
const handleSearchInput = () => {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
getItems();
|
||||
if (browsingCollection) {
|
||||
loadBrowsingItems();
|
||||
} else {
|
||||
getItems();
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
|
|
@ -100,6 +119,97 @@
|
|||
}
|
||||
};
|
||||
|
||||
const browseIntoCollection = async (collection) => {
|
||||
browsingCollection = collection;
|
||||
currentDirectoryId = null;
|
||||
await loadBrowsingItems();
|
||||
};
|
||||
|
||||
const browseIntoDirectory = async (dir) => {
|
||||
currentDirectoryId = dir.id;
|
||||
await loadBrowsingItems();
|
||||
};
|
||||
|
||||
const navigateBack = async () => {
|
||||
if (browsingBreadcrumbs.length > 0) {
|
||||
const parentCrumb =
|
||||
browsingBreadcrumbs.length >= 2
|
||||
? browsingBreadcrumbs[browsingBreadcrumbs.length - 2]
|
||||
: null;
|
||||
currentDirectoryId = parentCrumb ? parentCrumb.id : null;
|
||||
await loadBrowsingItems();
|
||||
} else {
|
||||
browsingCollection = null;
|
||||
currentDirectoryId = null;
|
||||
browsingFiles = [];
|
||||
browsingDirectories = [];
|
||||
browsingBreadcrumbs = [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadBrowsingItems = async () => {
|
||||
if (!browsingCollection) return;
|
||||
browsingLoading = true;
|
||||
|
||||
const res = await searchKnowledgeFilesById(
|
||||
localStorage.token,
|
||||
browsingCollection.id,
|
||||
query || null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
currentDirectoryId
|
||||
).catch(() => null);
|
||||
|
||||
if (res) {
|
||||
browsingFiles = res.items || [];
|
||||
browsingDirectories = res.directories || [];
|
||||
browsingBreadcrumbs = res.breadcrumbs || [];
|
||||
}
|
||||
|
||||
browsingLoading = false;
|
||||
};
|
||||
|
||||
const selectDirectory = async (dir) => {
|
||||
const allFiles = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const res = await searchKnowledgeFilesById(
|
||||
localStorage.token,
|
||||
browsingCollection.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) {
|
||||
dispatch('select', {
|
||||
...file,
|
||||
type: 'file',
|
||||
name: file?.meta?.name || file.filename
|
||||
});
|
||||
}
|
||||
show = false;
|
||||
};
|
||||
|
||||
const resetBrowsing = () => {
|
||||
browsingCollection = null;
|
||||
currentDirectoryId = null;
|
||||
browsingFiles = [];
|
||||
browsingDirectories = [];
|
||||
browsingBreadcrumbs = [];
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
getItems();
|
||||
});
|
||||
|
|
@ -112,6 +222,7 @@
|
|||
onClose();
|
||||
query = '';
|
||||
handleSearchInput();
|
||||
resetBrowsing();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -136,7 +247,175 @@
|
|||
</div>
|
||||
|
||||
<div class="max-h-56 overflow-y-scroll gap-0.5 flex flex-col">
|
||||
{#if items.length === 0}
|
||||
{#if browsingCollection}
|
||||
<div
|
||||
class="px-2 py-1.5 flex items-center gap-1.5 border-b border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||
on:click={navigateBack}
|
||||
>
|
||||
<ArrowLeft className="size-3" strokeWidth="2" />
|
||||
</button>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div
|
||||
class="text-xs text-gray-500 dark:text-gray-400 truncate flex items-center gap-0.5"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-gray-700 dark:hover:text-gray-200 transition truncate"
|
||||
on:click={() => {
|
||||
if (browsingBreadcrumbs.length > 0) {
|
||||
currentDirectoryId = null;
|
||||
loadBrowsingItems();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{decodeString(browsingCollection?.name)}
|
||||
</button>
|
||||
{#each browsingBreadcrumbs 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={() => {
|
||||
currentDirectoryId = crumb.id;
|
||||
loadBrowsingItems();
|
||||
}}
|
||||
>
|
||||
{crumb.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
content={browsingBreadcrumbs.length > 0
|
||||
? $i18n.t('Select directory')
|
||||
: $i18n.t('Select collection')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition opacity-60 hover:opacity-100"
|
||||
on:click={() => {
|
||||
if (browsingBreadcrumbs.length > 0) {
|
||||
selectDirectory({
|
||||
id: currentDirectoryId,
|
||||
name:
|
||||
browsingBreadcrumbs.length > 0
|
||||
? browsingBreadcrumbs[browsingBreadcrumbs.length - 1]?.name
|
||||
: browsingCollection?.name
|
||||
});
|
||||
} else {
|
||||
dispatch('select', {
|
||||
...browsingCollection,
|
||||
type: 'collection'
|
||||
});
|
||||
show = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{#if browsingLoading}
|
||||
<div class="flex justify-center py-3">
|
||||
<Spinner className="size-3.5" />
|
||||
</div>
|
||||
{:else if browsingDirectories.length === 0 && browsingFiles.length === 0}
|
||||
<div class="px-2 py-3 text-center text-xs text-gray-400 dark:text-gray-500 italic">
|
||||
{$i18n.t('This directory is empty.')}
|
||||
</div>
|
||||
{:else}
|
||||
{#if browsingDirectories.length > 0}
|
||||
<div class="px-1.5 text-[0.6875rem] text-gray-500 py-0.5">
|
||||
{$i18n.t('Directories')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each browsingDirectories as dir (dir.id)}
|
||||
<div
|
||||
class="min-h-[1.6875rem] px-2 rounded-xl w-full text-left flex items-center text-[0.8125rem] bg-transparent transition-colors 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 text-black dark:text-gray-100"
|
||||
on:click={() => browseIntoDirectory(dir)}
|
||||
>
|
||||
<Folder className="size-3.5 flex-shrink-0" />
|
||||
<Tooltip
|
||||
content={dir.name}
|
||||
placement="top-start"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<div class="line-clamp-1 flex-1 text-[0.8125rem] text-left">{dir.name}</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-0.5 ml-1">
|
||||
<Tooltip
|
||||
content={$i18n.t('Select directory')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<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>
|
||||
<ChevronRight className="size-3 opacity-30" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if browsingFiles.length > 0}
|
||||
<div class="px-1.5 text-[0.6875rem] text-gray-500 py-0.5">
|
||||
{$i18n.t('Files')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each browsingFiles as file (file.id)}
|
||||
<button
|
||||
class="min-h-[1.6875rem] px-2 rounded-xl w-full text-left flex items-center gap-1 text-[0.8125rem] bg-transparent transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100 text-black dark:text-gray-100"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
dispatch('select', {
|
||||
...file,
|
||||
type: 'file',
|
||||
name: file?.meta?.name || file.filename
|
||||
});
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={$i18n.t('File')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<DocumentPage className="size-3.5 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={decodeString(file?.meta?.name || file.filename)}
|
||||
placement="top-start"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<div class="line-clamp-1 flex-1 text-[0.8125rem] text-left">
|
||||
{decodeString(file?.meta?.name || file.filename)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if items.length === 0}
|
||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400 pt-4 pb-6">
|
||||
{$i18n.t('No knowledge found')}
|
||||
</div>
|
||||
|
|
@ -154,27 +433,16 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="min-h-[1.6875rem] px-2 rounded-xl w-full text-left flex justify-between items-center text-[0.8125rem] bg-transparent transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100 selected-command-option-button"
|
||||
>
|
||||
<button
|
||||
class="w-full flex-1"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
dispatch('select', item);
|
||||
show = false;
|
||||
}}
|
||||
{#if item.type === 'collection'}
|
||||
<div
|
||||
class="min-h-[1.6875rem] px-2 rounded-xl w-full text-left flex justify-between items-center text-[0.8125rem] bg-transparent transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100 selected-command-option-button"
|
||||
>
|
||||
<div class=" text-black dark:text-gray-100 flex items-center gap-1 shrink-0">
|
||||
{#if item.type === 'note'}
|
||||
<Tooltip
|
||||
content={$i18n.t('Note')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<PageEdit className="size-3.5" />
|
||||
</Tooltip>
|
||||
{:else if item.type === 'collection'}
|
||||
<button
|
||||
class="w-full flex-1"
|
||||
type="button"
|
||||
on:click={() => browseIntoCollection(item)}
|
||||
>
|
||||
<div class=" text-black dark:text-gray-100 flex items-center gap-1 shrink-0">
|
||||
<Tooltip
|
||||
content={$i18n.t('Collection')}
|
||||
placement="top"
|
||||
|
|
@ -182,28 +450,83 @@
|
|||
>
|
||||
<Database className="size-3.5" />
|
||||
</Tooltip>
|
||||
{:else if item.type === 'file'}
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('File')}
|
||||
placement="top"
|
||||
content={item.description || decodeString(item?.name)}
|
||||
placement="top-start"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<DocumentPage className="size-3.5" />
|
||||
<div class="line-clamp-1 flex-1 text-[0.8125rem] text-left">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-0.5 ml-1">
|
||||
<Tooltip
|
||||
content={item.description || decodeString(item?.name)}
|
||||
placement="top-start"
|
||||
content={$i18n.t('Select collection')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<div class="line-clamp-1 flex-1 text-[0.8125rem] text-left">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
<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={() => {
|
||||
dispatch('select', item);
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ChevronRight className="size-3 opacity-30" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="min-h-[1.6875rem] px-2 rounded-xl w-full text-left flex justify-between items-center text-[0.8125rem] bg-transparent transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100 selected-command-option-button"
|
||||
>
|
||||
<button
|
||||
class="w-full flex-1"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
dispatch('select', item);
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div class=" text-black dark:text-gray-100 flex items-center gap-1 shrink-0">
|
||||
{#if item.type === 'note'}
|
||||
<Tooltip
|
||||
content={$i18n.t('Note')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<PageEdit className="size-3.5" />
|
||||
</Tooltip>
|
||||
{:else if item.type === 'file'}
|
||||
<Tooltip
|
||||
content={$i18n.t('File')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<DocumentPage className="size-3.5" />
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip
|
||||
content={item.description || decodeString(item?.name)}
|
||||
placement="top-start"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<div class="line-clamp-1 flex-1 text-[0.8125rem] text-left">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue