diff --git a/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte b/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte index 8ecf6f4e2e..343d31b0ec 100644 --- a/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte +++ b/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte @@ -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(''); diff --git a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte index 7a033c23e5..cad3b00785 100644 --- a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte @@ -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; + 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 @@ }); -{#if filteredItems.length > 0 || query.startsWith('http')} +{#if browsingCollection} +
+ + +
+
+ + {#each browsingBreadcrumbs as crumb, i} + / + + {/each} +
+
+ + 0 + ? $i18n.t('Select directory') + : $i18n.t('Select collection')} + placement="top" + > + + +
+ + {#if browsingLoading} +
+ +
+ {:else if browsingDirectories.length === 0 && browsingFiles.length === 0} +
+ {$i18n.t('This directory is empty.')} +
+ {: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} +
+ {#if item._itemType === 'directory'} + {$i18n.t('Directories')} + {:else} + {$i18n.t('Files')} + {/if} +
+ {/if} + + {#if item._itemType === 'directory'} +
+ + +
+ + + + +
+
+ {:else} + + {/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}
@@ -154,51 +465,97 @@ {/if} {#if !['youtube', 'web'].includes(item.type)} - + +
+ + + + +
- + {:else} + + {/if} {/if} {/each} diff --git a/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte b/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte index 1a1bd28b3e..27e6af4c50 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,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 @@
- {:else if selectedFileItemsTotal === 0} + {:else if selectedFileItemsTotal === 0 && selectedFileDirectories.length === 0}
{$i18n.t('No files in this knowledge base.')}
{:else} + {#if selectedFileBreadcrumbs.length > 0} +
+ + + + {#each selectedFileBreadcrumbs as crumb, i} + / + + {/each} +
+ {/if} + + {#each selectedFileDirectories as dir (dir.id)} +
+ + +
+ + + + +
+
+ {/each} + {#each selectedFileItems as file, fileIdx (file.id)}