open-webui/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte

304 lines
7.4 KiB
Svelte
Raw Normal View History

2024-01-08 07:43:32 +00:00
<script lang="ts">
2024-10-02 00:35:35 +00:00
import { toast } from 'svelte-sonner';
import Fuse from 'fuse.js';
2024-01-08 07:43:32 +00:00
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
import { tick, getContext, onMount, onDestroy } from 'svelte';
2025-09-24 16:36:17 +00:00
import { removeLastWordFromString, isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
2025-09-12 16:31:57 +00:00
import Tooltip from '$lib/components/common/Tooltip.svelte';
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
import Database from '$lib/components/icons/Database.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Youtube from '$lib/components/icons/Youtube.svelte';
2025-10-05 07:48:08 +00:00
import { folders } from '$lib/stores';
import Folder from '$lib/components/icons/Folder.svelte';
2024-01-08 07:43:32 +00:00
const i18n = getContext('i18n');
2025-09-12 16:31:57 +00:00
export let query = '';
export let onSelect = (e) => {};
2024-01-08 07:43:32 +00:00
2025-09-12 16:31:57 +00:00
export let knowledge = [];
2024-01-08 07:43:32 +00:00
let selectedIdx = 0;
2024-10-02 06:21:33 +00:00
let items = [];
2024-10-02 00:35:35 +00:00
let fuse = null;
2025-09-12 16:31:57 +00:00
export let filteredItems = [];
2024-10-02 00:35:35 +00:00
$: if (fuse) {
2025-09-12 16:31:57 +00:00
filteredItems = [
...(query
? fuse.search(query).map((e) => {
return e.item;
})
: items),
...(query.startsWith('http')
2025-09-24 16:36:17 +00:00
? isYoutubeUrl(query)
2025-09-12 16:31:57 +00:00
? [{ type: 'youtube', name: query, description: query }]
: [
{
type: 'web',
name: query,
description: query
}
]
: [])
];
2024-10-02 00:35:35 +00:00
}
2025-09-12 16:31:57 +00:00
$: if (query) {
2024-01-08 07:43:32 +00:00
selectedIdx = 0;
}
export const selectUp = () => {
selectedIdx = Math.max(0, selectedIdx - 1);
};
export const selectDown = () => {
2024-10-02 05:45:04 +00:00
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
2024-01-08 07:43:32 +00:00
};
2025-09-12 16:31:57 +00:00
export const select = async () => {
// find item with data-selected=true
const item = document.querySelector(`[data-selected="true"]`);
if (item) {
// click the item
item.click();
2025-07-01 11:57:01 +00:00
}
};
const decodeString = (str: string) => {
try {
return decodeURIComponent(str);
} catch (e) {
return str;
}
};
2025-07-08 21:17:25 +00:00
onMount(async () => {
2025-09-12 16:31:57 +00:00
let legacy_documents = knowledge
.filter((item) => item?.meta?.document)
.map((item) => ({
...item,
type: 'file'
}));
2024-10-02 06:21:33 +00:00
let legacy_collections =
legacy_documents.length > 0
? [
{
name: 'All Documents',
legacy: true,
type: 'collection',
2024-10-02 13:19:09 +00:00
description: 'Deprecated (legacy collection), please create a new knowledge base.',
2024-10-02 06:21:33 +00:00
title: $i18n.t('All Documents'),
collection_names: legacy_documents.map((item) => item.id)
},
...legacy_documents
.reduce((a, item) => {
return [...new Set([...a, ...(item?.meta?.tags ?? []).map((tag) => tag.name)])];
}, [])
.map((tag) => ({
name: tag,
legacy: true,
type: 'collection',
2024-10-02 13:19:09 +00:00
description: 'Deprecated (legacy collection), please create a new knowledge base.',
2024-10-02 06:21:33 +00:00
collection_names: legacy_documents
.filter((item) => (item?.meta?.tags ?? []).map((tag) => tag.name).includes(tag))
.map((item) => item.id)
}))
]
: [];
2025-09-12 16:31:57 +00:00
let collections = knowledge
.filter((item) => !item?.meta?.document)
.map((item) => ({
2024-10-04 07:59:19 +00:00
...item,
type: 'collection'
}));
let collection_files =
2025-09-12 16:31:57 +00:00
knowledge.length > 0
? [
2025-09-12 16:31:57 +00:00
...knowledge
.reduce((a, item) => {
return [
...new Set([
...a,
...(item?.files ?? []).map((file) => ({
...file,
2025-01-22 19:06:06 +00:00
collection: { name: item.name, description: item.description } // DO NOT REMOVE, USED IN FILE DESCRIPTION/ATTACHMENT
}))
])
];
}, [])
.map((file) => ({
...file,
name: file?.meta?.name,
description: `${file?.collection?.name} - ${file?.collection?.description}`,
2025-07-14 13:47:21 +00:00
knowledge: true, // DO NOT REMOVE, USED TO INDICATE KNOWLEDGE BASE FILE
type: 'file'
}))
]
: [];
2025-10-05 07:48:08 +00:00
let folder_items = $folders.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}));
items = [
...folder_items,
...collections,
...collection_files,
...legacy_collections,
...legacy_documents
].map((item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
});
2024-10-02 06:21:33 +00:00
fuse = new Fuse(items, {
2024-10-02 00:35:35 +00:00
keys: ['name', 'description']
});
await tick();
2025-07-01 11:57:01 +00:00
});
2025-09-12 16:35:14 +00:00
const onKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
select();
}
};
onMount(() => {
window.addEventListener('keydown', onKeyDown);
});
onDestroy(() => {
window.removeEventListener('keydown', onKeyDown);
});
2024-01-08 07:43:32 +00:00
</script>
2025-09-12 16:31:57 +00:00
<div class="px-2 text-xs text-gray-500 py-1">
{$i18n.t('Knowledge')}
</div>
{#if filteredItems.length > 0 || query.startsWith('http')}
{#each filteredItems as item, idx}
{#if !['youtube', 'web'].includes(item.type)}
<button
class=" px-2 py-1 rounded-xl w-full text-left flex justify-between items-center {idx ===
selectedIdx
? ' bg-gray-50 dark:bg-gray-800 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=" text-black dark:text-gray-100 flex items-center gap-1">
<Tooltip
content={item?.legacy
? $i18n.t('Legacy')
: item?.type === 'file'
? $i18n.t('File')
: item?.type === 'collection'
? $i18n.t('Collection')
: ''}
placement="top"
>
{#if item?.type === 'collection'}
<Database className="size-4" />
2025-10-05 07:48:08 +00:00
{:else if item?.type === 'folder'}
<Folder className="size-4" />
2025-09-12 16:31:57 +00:00
{:else}
<DocumentPage className="size-4" />
{/if}
</Tooltip>
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
<div class="line-clamp-1 flex-1">
{decodeString(item?.name)}
</div>
</Tooltip>
</div>
</button>
{/if}
{/each}
2025-09-24 16:36:17 +00:00
{#if isYoutubeUrl(query)}
2025-09-12 16:31:57 +00:00
<button
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button"
type="button"
data-selected={true}
on:click={() => {
if (isValidHttpUrl(query)) {
onSelect({
type: 'youtube',
data: query
});
} else {
toast.error(
$i18n.t('Oops! Looks like the URL is invalid. Please double-check and try again.')
);
}
}}
>
<div class=" text-black dark:text-gray-100 line-clamp-1 flex items-center gap-1">
<Tooltip content={$i18n.t('YouTube')} placement="top">
<Youtube className="size-4" />
</Tooltip>
<div class="truncate flex-1">
{query}
</div>
</div>
</button>
{:else if query.startsWith('http')}
<button
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button"
type="button"
data-selected={true}
on:click={() => {
if (isValidHttpUrl(query)) {
onSelect({
type: 'web',
data: query
});
} else {
toast.error(
$i18n.t('Oops! Looks like the URL is invalid. Please double-check and try again.')
);
}
}}
>
<div class=" text-black dark:text-gray-100 line-clamp-1 flex items-center gap-1">
<Tooltip content={$i18n.t('Web')} placement="top">
<GlobeAlt className="size-4" />
</Tooltip>
<div class="truncate flex-1">
{query}
2024-01-08 07:43:32 +00:00
</div>
</div>
2025-09-12 16:31:57 +00:00
</button>
{/if}
2024-01-08 07:43:32 +00:00
{/if}