This commit is contained in:
G30 2026-08-27 11:16:25 -07:00 committed by GitHub
commit 33d29f4230
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 906 additions and 97 deletions

View file

@ -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('');

View file

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

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

View file

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