2024-10-02 00:35:35 +00:00
|
|
|
<script lang="ts">
|
2024-10-02 13:21:16 +00:00
|
|
|
import Fuse from 'fuse.js';
|
|
|
|
|
|
2024-10-02 00:35:35 +00:00
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
|
|
|
dayjs.extend(relativeTime);
|
|
|
|
|
|
|
|
|
|
import { toast } from 'svelte-sonner';
|
2025-10-05 05:25:40 +00:00
|
|
|
import { onMount, getContext, tick } from 'svelte';
|
2024-10-02 00:35:35 +00:00
|
|
|
const i18n = getContext('i18n');
|
|
|
|
|
|
2025-10-05 05:25:40 +00:00
|
|
|
import { WEBUI_NAME, knowledge, user } from '$lib/stores';
|
2024-11-17 00:51:55 +00:00
|
|
|
import {
|
|
|
|
|
getKnowledgeBases,
|
|
|
|
|
deleteKnowledgeById,
|
|
|
|
|
getKnowledgeBaseList
|
|
|
|
|
} from '$lib/apis/knowledge';
|
2024-10-02 00:35:35 +00:00
|
|
|
|
|
|
|
|
import { goto } from '$app/navigation';
|
2025-10-05 05:25:40 +00:00
|
|
|
import { capitalizeFirstLetter } from '$lib/utils';
|
2024-11-16 06:04:33 +00:00
|
|
|
|
2024-10-02 00:35:35 +00:00
|
|
|
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
2024-10-02 05:45:04 +00:00
|
|
|
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
2024-10-19 05:56:04 +00:00
|
|
|
import Badge from '../common/Badge.svelte';
|
2024-11-07 05:45:48 +00:00
|
|
|
import Search from '../icons/Search.svelte';
|
|
|
|
|
import Plus from '../icons/Plus.svelte';
|
2024-11-17 02:35:14 +00:00
|
|
|
import Spinner from '../common/Spinner.svelte';
|
2024-11-18 13:51:01 +00:00
|
|
|
import Tooltip from '../common/Tooltip.svelte';
|
2025-05-18 22:36:15 +00:00
|
|
|
import XMark from '../icons/XMark.svelte';
|
2025-10-05 05:25:40 +00:00
|
|
|
import ViewSelector from './common/ViewSelector.svelte';
|
2024-11-17 02:35:14 +00:00
|
|
|
|
|
|
|
|
let loaded = false;
|
2024-10-02 00:35:35 +00:00
|
|
|
|
|
|
|
|
let query = '';
|
2024-10-02 05:45:04 +00:00
|
|
|
let selectedItem = null;
|
2024-10-02 00:35:35 +00:00
|
|
|
let showDeleteConfirm = false;
|
|
|
|
|
|
2025-10-05 05:25:40 +00:00
|
|
|
let tagsContainerElement: HTMLDivElement;
|
|
|
|
|
let viewOption = '';
|
|
|
|
|
|
2024-10-02 13:21:16 +00:00
|
|
|
let fuse = null;
|
|
|
|
|
|
2024-11-17 00:51:55 +00:00
|
|
|
let knowledgeBases = [];
|
2025-10-05 05:25:40 +00:00
|
|
|
|
|
|
|
|
let items = [];
|
2024-10-02 13:21:16 +00:00
|
|
|
let filteredItems = [];
|
2024-11-17 00:51:55 +00:00
|
|
|
|
2025-10-05 05:25:40 +00:00
|
|
|
const setFuse = async () => {
|
|
|
|
|
items = knowledgeBases.filter(
|
|
|
|
|
(item) =>
|
|
|
|
|
viewOption === '' ||
|
|
|
|
|
(viewOption === 'created' && item.user_id === $user?.id) ||
|
|
|
|
|
(viewOption === 'shared' && item.user_id !== $user?.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
fuse = new Fuse(items, {
|
2025-05-18 07:23:38 +00:00
|
|
|
keys: [
|
|
|
|
|
'name',
|
|
|
|
|
'description',
|
|
|
|
|
'user.name', // Ensures Fuse looks into item.user.name
|
|
|
|
|
'user.email' // Ensures Fuse looks into item.user.email
|
|
|
|
|
],
|
2025-08-13 23:52:11 +00:00
|
|
|
threshold: 0.3
|
2024-11-17 00:51:55 +00:00
|
|
|
});
|
2025-10-05 05:25:40 +00:00
|
|
|
|
|
|
|
|
await tick();
|
|
|
|
|
setFilteredItems();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$: if (knowledgeBases.length > 0 && viewOption !== undefined) {
|
|
|
|
|
// Added a check for non-empty array, good practice
|
|
|
|
|
setFuse();
|
2025-05-18 07:23:38 +00:00
|
|
|
} else {
|
|
|
|
|
fuse = null; // Reset fuse if knowledgeBases is empty
|
2024-11-17 00:51:55 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 05:25:40 +00:00
|
|
|
const setFilteredItems = () => {
|
|
|
|
|
filteredItems = query ? fuse.search(query).map((result) => result.item) : items;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$: if (query !== undefined && fuse) {
|
|
|
|
|
setFilteredItems();
|
2024-10-02 13:21:16 +00:00
|
|
|
}
|
2024-10-02 00:35:35 +00:00
|
|
|
|
2024-10-02 05:45:04 +00:00
|
|
|
const deleteHandler = async (item) => {
|
|
|
|
|
const res = await deleteKnowledgeById(localStorage.token, item.id).catch((e) => {
|
2025-01-30 05:56:28 +00:00
|
|
|
toast.error(`${e}`);
|
2024-10-02 00:35:35 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res) {
|
2024-11-17 00:51:55 +00:00
|
|
|
knowledgeBases = await getKnowledgeBaseList(localStorage.token);
|
|
|
|
|
knowledge.set(await getKnowledgeBases(localStorage.token));
|
2024-10-02 05:45:04 +00:00
|
|
|
toast.success($i18n.t('Knowledge deleted successfully.'));
|
2024-10-02 00:35:35 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(async () => {
|
2025-10-05 05:25:40 +00:00
|
|
|
viewOption = localStorage?.workspaceViewOption || '';
|
2024-11-17 00:51:55 +00:00
|
|
|
knowledgeBases = await getKnowledgeBaseList(localStorage.token);
|
2024-11-17 02:35:14 +00:00
|
|
|
loaded = true;
|
2024-10-02 00:35:35 +00:00
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<svelte:head>
|
|
|
|
|
<title>
|
2025-05-03 14:16:32 +00:00
|
|
|
{$i18n.t('Knowledge')} • {$WEBUI_NAME}
|
2024-10-02 00:35:35 +00:00
|
|
|
</title>
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
2024-11-17 02:35:14 +00:00
|
|
|
{#if loaded}
|
|
|
|
|
<DeleteConfirmDialog
|
|
|
|
|
bind:show={showDeleteConfirm}
|
|
|
|
|
on:confirm={() => {
|
|
|
|
|
deleteHandler(selectedItem);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
|
2025-10-05 06:06:40 +00:00
|
|
|
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
|
2024-11-17 02:35:14 +00:00
|
|
|
<div class="flex justify-between items-center">
|
2025-10-05 05:25:40 +00:00
|
|
|
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
|
|
|
|
<div>
|
|
|
|
|
{$i18n.t('Knowledge')}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
|
|
|
|
|
{filteredItems.length}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex w-full justify-end gap-1.5">
|
|
|
|
|
<a
|
|
|
|
|
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
|
|
|
|
|
href="/workspace/knowledge/create"
|
2024-11-17 02:35:14 +00:00
|
|
|
>
|
2025-10-05 05:25:40 +00:00
|
|
|
<Plus className="size-3" strokeWidth="2.5" />
|
|
|
|
|
|
|
|
|
|
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Knowledge')}</div>
|
|
|
|
|
</a>
|
2024-11-17 02:35:14 +00:00
|
|
|
</div>
|
2024-10-02 00:35:35 +00:00
|
|
|
</div>
|
2025-10-05 06:06:40 +00:00
|
|
|
</div>
|
2024-10-02 00:35:35 +00:00
|
|
|
|
2025-10-05 06:06:40 +00:00
|
|
|
<div
|
|
|
|
|
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
|
|
|
|
|
>
|
|
|
|
|
<div class=" flex w-full space-x-2 py-0.5 px-3.5 pb-2">
|
2024-11-17 02:35:14 +00:00
|
|
|
<div class="flex flex-1">
|
|
|
|
|
<div class=" self-center ml-1 mr-3">
|
|
|
|
|
<Search className="size-3.5" />
|
|
|
|
|
</div>
|
|
|
|
|
<input
|
2025-02-16 03:27:25 +00:00
|
|
|
class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
2024-11-17 02:35:14 +00:00
|
|
|
bind:value={query}
|
|
|
|
|
placeholder={$i18n.t('Search Knowledge')}
|
|
|
|
|
/>
|
2025-05-18 22:36:15 +00:00
|
|
|
{#if query}
|
|
|
|
|
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
|
|
|
|
<button
|
|
|
|
|
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
|
|
|
|
on:click={() => {
|
|
|
|
|
query = '';
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<XMark className="size-3" strokeWidth="2" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2024-11-17 02:35:14 +00:00
|
|
|
</div>
|
2025-10-05 05:25:40 +00:00
|
|
|
</div>
|
2024-11-17 02:35:14 +00:00
|
|
|
|
2025-10-05 05:25:40 +00:00
|
|
|
<div
|
2025-10-05 06:06:40 +00:00
|
|
|
class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none -mx-1"
|
|
|
|
|
on:wheel={(e) => {
|
|
|
|
|
if (e.deltaY !== 0) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.currentTarget.scrollLeft += e.deltaY;
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-10-05 05:25:40 +00:00
|
|
|
>
|
2025-10-05 06:06:40 +00:00
|
|
|
<div
|
|
|
|
|
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
|
|
|
|
|
bind:this={tagsContainerElement}
|
2024-10-14 19:46:05 +00:00
|
|
|
>
|
2025-10-05 06:06:40 +00:00
|
|
|
<ViewSelector
|
|
|
|
|
bind:value={viewOption}
|
|
|
|
|
onChange={async (value) => {
|
|
|
|
|
localStorage.workspaceViewOption = value;
|
2024-10-02 00:35:35 +00:00
|
|
|
|
2025-10-05 06:06:40 +00:00
|
|
|
await tick();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if (filteredItems ?? []).length !== 0}
|
|
|
|
|
<!-- The Aleph dreams itself into being, and the void learns its own name -->
|
|
|
|
|
<div class=" my-2 px-3 grid grid-cols-1 lg:grid-cols-2 gap-2">
|
|
|
|
|
{#each filteredItems as item}
|
|
|
|
|
<Tooltip content={item?.description ?? item.name}>
|
|
|
|
|
<button
|
|
|
|
|
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
|
|
|
|
|
on:click={() => {
|
|
|
|
|
if (item?.meta?.document) {
|
|
|
|
|
toast.error(
|
|
|
|
|
$i18n.t(
|
|
|
|
|
'Only collections can be edited, create a new knowledge base to edit/add documents.'
|
2024-11-20 00:47:35 +00:00
|
|
|
)
|
2025-10-05 06:06:40 +00:00
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
goto(`/workspace/knowledge/${item.id}`);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div class=" w-full">
|
|
|
|
|
<div class=" self-center flex-1">
|
|
|
|
|
<div class="flex items-center justify-between -my-1">
|
|
|
|
|
<div class=" flex gap-2 items-center">
|
|
|
|
|
<div>
|
|
|
|
|
{#if item?.meta?.document}
|
|
|
|
|
<Badge type="muted" content={$i18n.t('Document')} />
|
|
|
|
|
{:else}
|
|
|
|
|
<Badge type="success" content={$i18n.t('Collection')} />
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class=" text-xs text-gray-500 line-clamp-1">
|
|
|
|
|
{$i18n.t('Updated')}
|
|
|
|
|
{dayjs(item.updated_at * 1000).fromNow()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<div class=" flex self-center">
|
|
|
|
|
<ItemMenu
|
|
|
|
|
on:delete={() => {
|
|
|
|
|
selectedItem = item;
|
|
|
|
|
showDeleteConfirm = true;
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class=" flex items-center gap-1 justify-between px-1.5">
|
|
|
|
|
<div class=" flex items-center gap-2">
|
|
|
|
|
<div class=" text-sm font-medium line-clamp-1 capitalize">{item.name}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div class="text-xs text-gray-500">
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={item?.user?.email ?? $i18n.t('Deleted User')}
|
|
|
|
|
className="flex shrink-0"
|
|
|
|
|
placement="top-start"
|
|
|
|
|
>
|
|
|
|
|
{$i18n.t('By {{name}}', {
|
|
|
|
|
name: capitalizeFirstLetter(
|
|
|
|
|
item?.user?.name ?? item?.user?.email ?? $i18n.t('Deleted User')
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-11-17 02:35:14 +00:00
|
|
|
</div>
|
2025-10-05 06:06:40 +00:00
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
|
|
|
|
<div class="max-w-md text-center">
|
|
|
|
|
<div class=" text-3xl mb-3">😕</div>
|
|
|
|
|
<div class=" text-lg font-medium mb-1">{$i18n.t('No knowledge found')}</div>
|
|
|
|
|
<div class=" text-gray-500 text-center text-xs">
|
|
|
|
|
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
|
2024-10-02 00:35:35 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-05 06:06:40 +00:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
2024-11-17 02:35:14 +00:00
|
|
|
</div>
|
2024-10-02 00:35:35 +00:00
|
|
|
|
2025-10-05 06:06:40 +00:00
|
|
|
<div class=" text-gray-500 text-xs m-2">
|
2024-11-17 02:35:14 +00:00
|
|
|
ⓘ {$i18n.t("Use '#' in the prompt input to load and include your knowledge.")}
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="w-full h-full flex justify-center items-center">
|
2025-06-27 12:15:16 +00:00
|
|
|
<Spinner className="size-5" />
|
2024-11-17 02:35:14 +00:00
|
|
|
</div>
|
|
|
|
|
{/if}
|