1
0

Knowledge.svelte 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import Fuse from 'fuse.js';
  4. import dayjs from 'dayjs';
  5. import relativeTime from 'dayjs/plugin/relativeTime';
  6. dayjs.extend(relativeTime);
  7. import { tick, getContext, onMount, onDestroy } from 'svelte';
  8. import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
  9. import { knowledge } from '$lib/stores';
  10. const i18n = getContext('i18n');
  11. export let command = '';
  12. export let onSelect = (e) => {};
  13. let selectedIdx = 0;
  14. let items = [];
  15. let fuse = null;
  16. let filteredItems = [];
  17. $: if (fuse) {
  18. filteredItems = command.slice(1)
  19. ? fuse.search(command).map((e) => {
  20. return e.item;
  21. })
  22. : items;
  23. }
  24. $: if (command) {
  25. selectedIdx = 0;
  26. }
  27. export const selectUp = () => {
  28. selectedIdx = Math.max(0, selectedIdx - 1);
  29. };
  30. export const selectDown = () => {
  31. selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
  32. };
  33. let container;
  34. let adjustHeightDebounce;
  35. const adjustHeight = () => {
  36. if (container) {
  37. if (adjustHeightDebounce) {
  38. clearTimeout(adjustHeightDebounce);
  39. }
  40. adjustHeightDebounce = setTimeout(() => {
  41. if (!container) return;
  42. // Ensure the container is visible before adjusting height
  43. const rect = container.getBoundingClientRect();
  44. container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
  45. }, 100);
  46. }
  47. };
  48. const confirmSelect = async (type, data) => {
  49. onSelect({
  50. type: type,
  51. data: data
  52. });
  53. };
  54. const decodeString = (str: string) => {
  55. try {
  56. return decodeURIComponent(str);
  57. } catch (e) {
  58. return str;
  59. }
  60. };
  61. onMount(() => {
  62. window.addEventListener('resize', adjustHeight);
  63. adjustHeight();
  64. let legacy_documents = $knowledge
  65. .filter((item) => item?.meta?.document)
  66. .map((item) => ({
  67. ...item,
  68. type: 'file'
  69. }));
  70. let legacy_collections =
  71. legacy_documents.length > 0
  72. ? [
  73. {
  74. name: 'All Documents',
  75. legacy: true,
  76. type: 'collection',
  77. description: 'Deprecated (legacy collection), please create a new knowledge base.',
  78. title: $i18n.t('All Documents'),
  79. collection_names: legacy_documents.map((item) => item.id)
  80. },
  81. ...legacy_documents
  82. .reduce((a, item) => {
  83. return [...new Set([...a, ...(item?.meta?.tags ?? []).map((tag) => tag.name)])];
  84. }, [])
  85. .map((tag) => ({
  86. name: tag,
  87. legacy: true,
  88. type: 'collection',
  89. description: 'Deprecated (legacy collection), please create a new knowledge base.',
  90. collection_names: legacy_documents
  91. .filter((item) => (item?.meta?.tags ?? []).map((tag) => tag.name).includes(tag))
  92. .map((item) => item.id)
  93. }))
  94. ]
  95. : [];
  96. let collections = $knowledge
  97. .filter((item) => !item?.meta?.document)
  98. .map((item) => ({
  99. ...item,
  100. type: 'collection'
  101. }));
  102. let collection_files =
  103. $knowledge.length > 0
  104. ? [
  105. ...$knowledge
  106. .reduce((a, item) => {
  107. return [
  108. ...new Set([
  109. ...a,
  110. ...(item?.files ?? []).map((file) => ({
  111. ...file,
  112. collection: { name: item.name, description: item.description } // DO NOT REMOVE, USED IN FILE DESCRIPTION/ATTACHMENT
  113. }))
  114. ])
  115. ];
  116. }, [])
  117. .map((file) => ({
  118. ...file,
  119. name: file?.meta?.name,
  120. description: `${file?.collection?.name} - ${file?.collection?.description}`,
  121. type: 'file'
  122. }))
  123. ]
  124. : [];
  125. items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
  126. (item) => {
  127. return {
  128. ...item,
  129. ...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
  130. };
  131. }
  132. );
  133. fuse = new Fuse(items, {
  134. keys: ['name', 'description']
  135. });
  136. });
  137. onDestroy(() => {
  138. window.removeEventListener('resize', adjustHeight);
  139. });
  140. </script>
  141. {#if filteredItems.length > 0 || command?.substring(1).startsWith('http')}
  142. <div
  143. id="commands-container"
  144. class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
  145. >
  146. <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
  147. <div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
  148. <div
  149. class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
  150. id="command-options-container"
  151. bind:this={container}
  152. >
  153. {#each filteredItems as item, idx}
  154. <button
  155. class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center {idx ===
  156. selectedIdx
  157. ? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button'
  158. : ''}"
  159. type="button"
  160. on:click={() => {
  161. console.log(item);
  162. confirmSelect('knowledge', item);
  163. }}
  164. on:mousemove={() => {
  165. selectedIdx = idx;
  166. }}
  167. >
  168. <div>
  169. <div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
  170. {#if item.legacy}
  171. <div
  172. class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
  173. >
  174. Legacy
  175. </div>
  176. {:else if item?.meta?.document}
  177. <div
  178. class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
  179. >
  180. Document
  181. </div>
  182. {:else if item?.type === 'file'}
  183. <div
  184. class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
  185. >
  186. File
  187. </div>
  188. {:else}
  189. <div
  190. class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
  191. >
  192. Collection
  193. </div>
  194. {/if}
  195. <div class="line-clamp-1">
  196. {decodeString(item?.name)}
  197. </div>
  198. </div>
  199. <div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
  200. {item?.description}
  201. </div>
  202. </div>
  203. </button>
  204. <!-- <div slot="content" class=" pl-2 pt-1 flex flex-col gap-0.5">
  205. {#if !item.legacy && (item?.files ?? []).length > 0}
  206. {#each item?.files ?? [] as file, fileIdx}
  207. <button
  208. 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"
  209. type="button"
  210. on:click={() => {
  211. console.log(file);
  212. }}
  213. on:mousemove={() => {
  214. selectedIdx = idx;
  215. }}
  216. >
  217. <div>
  218. <div
  219. class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
  220. >
  221. <div
  222. class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
  223. >
  224. File
  225. </div>
  226. <div class="line-clamp-1">
  227. {file?.meta?.name}
  228. </div>
  229. </div>
  230. <div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
  231. {$i18n.t('Updated')}
  232. {dayjs(file.updated_at * 1000).fromNow()}
  233. </div>
  234. </div>
  235. </button>
  236. {/each}
  237. {:else}
  238. <div class=" text-gray-500 text-xs mt-1 mb-2">
  239. {$i18n.t('File not found.')}
  240. </div>
  241. {/if}
  242. </div> -->
  243. {/each}
  244. {#if command.substring(1).startsWith('https://www.youtube.com') || command
  245. .substring(1)
  246. .startsWith('https://youtu.be')}
  247. <button
  248. class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
  249. type="button"
  250. on:click={() => {
  251. if (isValidHttpUrl(command.substring(1))) {
  252. confirmSelect('youtube', command.substring(1));
  253. } else {
  254. toast.error(
  255. $i18n.t(
  256. 'Oops! Looks like the URL is invalid. Please double-check and try again.'
  257. )
  258. );
  259. }
  260. }}
  261. >
  262. <div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
  263. {command.substring(1)}
  264. </div>
  265. <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
  266. </button>
  267. {:else if command.substring(1).startsWith('http')}
  268. <button
  269. class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
  270. type="button"
  271. on:click={() => {
  272. if (isValidHttpUrl(command.substring(1))) {
  273. confirmSelect('web', command.substring(1));
  274. } else {
  275. toast.error(
  276. $i18n.t(
  277. 'Oops! Looks like the URL is invalid. Please double-check and try again.'
  278. )
  279. );
  280. }
  281. }}
  282. >
  283. <div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
  284. {command}
  285. </div>
  286. <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
  287. </button>
  288. {/if}
  289. </div>
  290. </div>
  291. </div>
  292. </div>
  293. {/if}