Knowledge.svelte 9.1 KB

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