| <script lang="ts"> |
| import dayjs from 'dayjs'; |
| |
| import { onMount, onDestroy, getContext, createEventDispatcher } from 'svelte'; |
| import { searchNotes } from '$lib/apis/notes'; |
| import { searchKnowledgeBases, searchKnowledgeFiles } from '$lib/apis/knowledge'; |
| |
| import { decodeString } from '$lib/utils'; |
| |
| import Dropdown from '$lib/components/common/Dropdown.svelte'; |
| import Search from '$lib/components/icons/Search.svelte'; |
| import Tooltip from '$lib/components/common/Tooltip.svelte'; |
| import Database from '$lib/components/icons/Database.svelte'; |
| import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; |
| import ChevronRight from '$lib/components/icons/ChevronRight.svelte'; |
| import PageEdit from '$lib/components/icons/PageEdit.svelte'; |
| import DocumentPage from '$lib/components/icons/DocumentPage.svelte'; |
| |
| const i18n = getContext('i18n'); |
| const dispatch = createEventDispatcher(); |
| |
| export let onClose: Function = () => {}; |
| |
| let show = false; |
| |
| let query = ''; |
| let searchDebounceTimer: ReturnType<typeof setTimeout>; |
| |
| let noteItems = []; |
| let knowledgeItems = []; |
| let fileItems = []; |
| |
| let items = []; |
| |
| $: items = [...noteItems, ...knowledgeItems, ...fileItems]; |
| |
| $: if (query !== undefined) { |
| clearTimeout(searchDebounceTimer); |
| searchDebounceTimer = setTimeout(() => { |
| getItems(); |
| }, 300); |
| } |
| |
| onDestroy(() => { |
| clearTimeout(searchDebounceTimer); |
| }); |
| |
| const getItems = () => { |
| getNoteItems(); |
| getKnowledgeItems(); |
| getKnowledgeFileItems(); |
| }; |
| |
| const getNoteItems = async () => { |
| const res = await searchNotes(localStorage.token, query).catch(() => { |
| return null; |
| }); |
| |
| if (res) { |
| noteItems = res.items.map((note) => { |
| return { |
| ...note, |
| type: 'note', |
| name: note.title, |
| description: dayjs(note.updated_at / 1000000).fromNow() |
| }; |
| }); |
| } |
| }; |
| |
| const getKnowledgeItems = async () => { |
| const res = await searchKnowledgeBases(localStorage.token, query).catch(() => { |
| return null; |
| }); |
| |
| if (res) { |
| knowledgeItems = res.items.map((note) => { |
| return { |
| ...note, |
| type: 'collection' |
| }; |
| }); |
| } |
| }; |
| |
| const getKnowledgeFileItems = async () => { |
| const res = await searchKnowledgeFiles(localStorage.token, query).catch(() => { |
| return null; |
| }); |
| |
| if (res) { |
| fileItems = res.items.map((file) => { |
| return { |
| ...file, |
| type: 'file', |
| name: file.meta?.name || file.filename, |
| description: file.description || '' |
| }; |
| }); |
| } |
| }; |
| |
| onMount(async () => { |
| getItems(); |
| }); |
| </script> |
|
|
| <Dropdown |
| bind:show |
| on:change={(e) => { |
| if (e.detail === false) { |
| onClose(); |
| query = ''; |
| } |
| }} |
| > |
| <slot /> |
|
|
| <div slot="content"> |
| <div |
| class="z-[10000] text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-70 p-1.5" |
| > |
| <div class=" flex w-full space-x-2 px-2 pb-0.5"> |
| <div class="flex flex-1"> |
| <div class=" self-center mr-2"> |
| <Search className="size-3.5" /> |
| </div> |
| <input |
| class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent" |
| bind:value={query} |
| placeholder={$i18n.t('Search')} |
| /> |
| </div> |
| </div> |
|
|
| <div class="max-h-56 overflow-y-scroll gap-0.5 flex flex-col"> |
| {#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> |
| {:else} |
| {#each items as item, i} |
| {#if i === 0 || item?.type !== items[i - 1]?.type} |
| <div class="px-2 text-xs text-gray-500 py-1"> |
| {#if item?.type === 'note'} |
| {$i18n.t('Notes')} |
| {:else if item?.type === 'collection'} |
| {$i18n.t('Collections')} |
| {:else if item?.type === 'file'} |
| {$i18n.t('Files')} |
| {/if} |
| </div> |
| {/if} |
|
|
| <div |
| class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm hover:bg-gray-50 hover:dark:bg-gray-800 hover:dark: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-4" /> |
| </Tooltip> |
| {:else if item.type === 'collection'} |
| <Tooltip |
| content={$i18n.t('Collection')} |
| placement="top" |
| tippyOptions={{ zIndex: 100000 }} |
| > |
| <Database className="size-4" /> |
| </Tooltip> |
| {:else if item.type === 'file'} |
| <Tooltip |
| content={$i18n.t('File')} |
| placement="top" |
| tippyOptions={{ zIndex: 100000 }} |
| > |
| <DocumentPage className="size-4" /> |
| </Tooltip> |
| {/if} |
|
|
| <Tooltip |
| content={item.description || decodeString(item?.name)} |
| placement="top-start" |
| tippyOptions={{ zIndex: 100000 }} |
| > |
| <div class="line-clamp-1 flex-1 text-sm text-left"> |
| {decodeString(item?.name)} |
| </div> |
| </Tooltip> |
| </div> |
| </button> |
| </div> |
| {/each} |
| {/if} |
| </div> |
| </div> |
| </div> |
| </Dropdown> |
|
|