123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- <script lang="ts">
- import { toast } from 'svelte-sonner';
- import Fuse from 'fuse.js';
- import dayjs from 'dayjs';
- import relativeTime from 'dayjs/plugin/relativeTime';
- dayjs.extend(relativeTime);
- import { tick, getContext, onMount, onDestroy } from 'svelte';
- import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
- 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';
- const i18n = getContext('i18n');
- export let query = '';
- export let onSelect = (e) => {};
- export let knowledge = [];
- let selectedIdx = 0;
- let items = [];
- let fuse = null;
- export let filteredItems = [];
- $: if (fuse) {
- filteredItems = [
- ...(query
- ? fuse.search(query).map((e) => {
- return e.item;
- })
- : items),
- ...(query.startsWith('http')
- ? query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')
- ? [{ type: 'youtube', name: query, description: query }]
- : [
- {
- type: 'web',
- name: query,
- description: query
- }
- ]
- : [])
- ];
- }
- $: if (query) {
- selectedIdx = 0;
- }
- export const selectUp = () => {
- selectedIdx = Math.max(0, selectedIdx - 1);
- };
- export const selectDown = () => {
- selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
- };
- export const select = async () => {
- // find item with data-selected=true
- const item = document.querySelector(`[data-selected="true"]`);
- if (item) {
- // click the item
- item.click();
- }
- };
- const decodeString = (str: string) => {
- try {
- return decodeURIComponent(str);
- } catch (e) {
- return str;
- }
- };
- onMount(async () => {
- let legacy_documents = knowledge
- .filter((item) => item?.meta?.document)
- .map((item) => ({
- ...item,
- type: 'file'
- }));
- let legacy_collections =
- legacy_documents.length > 0
- ? [
- {
- name: 'All Documents',
- legacy: true,
- type: 'collection',
- description: 'Deprecated (legacy collection), please create a new knowledge base.',
- 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',
- description: 'Deprecated (legacy collection), please create a new knowledge base.',
- collection_names: legacy_documents
- .filter((item) => (item?.meta?.tags ?? []).map((tag) => tag.name).includes(tag))
- .map((item) => item.id)
- }))
- ]
- : [];
- let collections = knowledge
- .filter((item) => !item?.meta?.document)
- .map((item) => ({
- ...item,
- type: 'collection'
- }));
- let collection_files =
- knowledge.length > 0
- ? [
- ...knowledge
- .reduce((a, item) => {
- return [
- ...new Set([
- ...a,
- ...(item?.files ?? []).map((file) => ({
- ...file,
- 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}`,
- knowledge: true, // DO NOT REMOVE, USED TO INDICATE KNOWLEDGE BASE FILE
- type: 'file'
- }))
- ]
- : [];
- items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
- (item) => {
- return {
- ...item,
- ...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
- };
- }
- );
- fuse = new Fuse(items, {
- keys: ['name', 'description']
- });
- await tick();
- });
- const onKeyDown = (e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- select();
- }
- };
- onMount(() => {
- window.addEventListener('keydown', onKeyDown);
- });
- onDestroy(() => {
- window.removeEventListener('keydown', onKeyDown);
- });
- </script>
- <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" />
- {: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}
- <!-- <div slot="content" class=" pl-2 pt-1 flex flex-col gap-0.5">
- {#if !item.legacy && (item?.files ?? []).length > 0}
- {#each item?.files ?? [] as file, fileIdx}
- <button
- class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center hover:bg-gray-50 dark:hover:bg-gray-850 dark:hover:text-gray-100 selected-command-option-button"
- type="button"
- on:click={() => {
- console.log(file);
- }}
- on:mousemove={() => {
- selectedIdx = idx;
- }}
- >
- <div>
- <div
- class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
- >
- <div
- class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
- >
- File
- </div>
- <div class="line-clamp-1">
- {file?.meta?.name}
- </div>
- </div>
- <div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
- {$i18n.t('Updated')}
- {dayjs(file.updated_at * 1000).fromNow()}
- </div>
- </div>
- </button>
- {/each}
- {:else}
- <div class=" text-gray-500 text-xs mt-1 mb-2">
- {$i18n.t('File not found.')}
- </div>
- {/if}
- </div> -->
- {/each}
- {#if query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')}
- <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}
- </div>
- </div>
- </button>
- {/if}
- {/if}
|