General.svelte 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <script lang="ts">
  2. import { getDocs } from '$lib/apis/documents';
  3. import {
  4. getQuerySettings,
  5. scanDocs,
  6. updateQuerySettings,
  7. resetVectorDB,
  8. getEmbeddingConfig,
  9. updateEmbeddingConfig,
  10. getRerankingConfig,
  11. updateRerankingConfig
  12. } from '$lib/apis/rag';
  13. import { documents, models } from '$lib/stores';
  14. import { onMount, getContext } from 'svelte';
  15. import { toast } from 'svelte-sonner';
  16. const i18n = getContext('i18n');
  17. export let saveHandler: Function;
  18. let scanDirLoading = false;
  19. let updateEmbeddingModelLoading = false;
  20. let updateRerankingModelLoading = false;
  21. let showResetConfirm = false;
  22. let embeddingEngine = '';
  23. let embeddingModel = '';
  24. let rerankingModel = '';
  25. let OpenAIKey = '';
  26. let OpenAIUrl = '';
  27. let querySettings = {
  28. template: '',
  29. r: 0.0,
  30. k: 4,
  31. hybrid: false
  32. };
  33. const scanHandler = async () => {
  34. scanDirLoading = true;
  35. const res = await scanDocs(localStorage.token);
  36. scanDirLoading = false;
  37. if (res) {
  38. await documents.set(await getDocs(localStorage.token));
  39. toast.success($i18n.t('Scan complete!'));
  40. }
  41. };
  42. const embeddingModelUpdateHandler = async () => {
  43. if (embeddingEngine === '' && embeddingModel.split('/').length - 1 > 1) {
  44. toast.error(
  45. $i18n.t(
  46. 'Model filesystem path detected. Model shortname is required for update, cannot continue.'
  47. )
  48. );
  49. return;
  50. }
  51. if (embeddingEngine === 'ollama' && embeddingModel === '') {
  52. toast.error(
  53. $i18n.t(
  54. 'Model filesystem path detected. Model shortname is required for update, cannot continue.'
  55. )
  56. );
  57. return;
  58. }
  59. if (embeddingEngine === 'openai' && embeddingModel === '') {
  60. toast.error(
  61. $i18n.t(
  62. 'Model filesystem path detected. Model shortname is required for update, cannot continue.'
  63. )
  64. );
  65. return;
  66. }
  67. if ((embeddingEngine === 'openai' && OpenAIKey === '') || OpenAIUrl === '') {
  68. toast.error($i18n.t('OpenAI URL/Key required.'));
  69. return;
  70. }
  71. console.log('Update embedding model attempt:', embeddingModel);
  72. updateEmbeddingModelLoading = true;
  73. const res = await updateEmbeddingConfig(localStorage.token, {
  74. embedding_engine: embeddingEngine,
  75. embedding_model: embeddingModel,
  76. ...(embeddingEngine === 'openai'
  77. ? {
  78. openai_config: {
  79. key: OpenAIKey,
  80. url: OpenAIUrl
  81. }
  82. }
  83. : {})
  84. }).catch(async (error) => {
  85. toast.error(error);
  86. await setEmbeddingConfig();
  87. return null;
  88. });
  89. updateEmbeddingModelLoading = false;
  90. if (res) {
  91. console.log('embeddingModelUpdateHandler:', res);
  92. if (res.status === true) {
  93. toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), {
  94. duration: 1000 * 10
  95. });
  96. }
  97. }
  98. };
  99. const rerankingModelUpdateHandler = async () => {
  100. console.log('Update reranking model attempt:', rerankingModel);
  101. updateRerankingModelLoading = true;
  102. const res = await updateRerankingConfig(localStorage.token, {
  103. reranking_model: rerankingModel
  104. }).catch(async (error) => {
  105. toast.error(error);
  106. await setRerankingConfig();
  107. return null;
  108. });
  109. updateRerankingModelLoading = false;
  110. if (res) {
  111. console.log('rerankingModelUpdateHandler:', res);
  112. if (res.status === true) {
  113. if (rerankingModel === '') {
  114. toast.success($i18n.t('Reranking model disabled', res), {
  115. duration: 1000 * 10
  116. });
  117. } else {
  118. toast.success($i18n.t('Reranking model set to "{{reranking_model}}"', res), {
  119. duration: 1000 * 10
  120. });
  121. }
  122. }
  123. }
  124. };
  125. const submitHandler = async () => {
  126. embeddingModelUpdateHandler();
  127. if (querySettings.hybrid) {
  128. rerankingModelUpdateHandler();
  129. }
  130. };
  131. const setEmbeddingConfig = async () => {
  132. const embeddingConfig = await getEmbeddingConfig(localStorage.token);
  133. if (embeddingConfig) {
  134. embeddingEngine = embeddingConfig.embedding_engine;
  135. embeddingModel = embeddingConfig.embedding_model;
  136. OpenAIKey = embeddingConfig.openai_config.key;
  137. OpenAIUrl = embeddingConfig.openai_config.url;
  138. }
  139. };
  140. const setRerankingConfig = async () => {
  141. const rerankingConfig = await getRerankingConfig(localStorage.token);
  142. if (rerankingConfig) {
  143. rerankingModel = rerankingConfig.reranking_model;
  144. }
  145. };
  146. const toggleHybridSearch = async () => {
  147. querySettings.hybrid = !querySettings.hybrid;
  148. querySettings = await updateQuerySettings(localStorage.token, querySettings);
  149. };
  150. onMount(async () => {
  151. await setEmbeddingConfig();
  152. await setRerankingConfig();
  153. querySettings = await getQuerySettings(localStorage.token);
  154. });
  155. </script>
  156. <form
  157. class="flex flex-col h-full justify-between space-y-3 text-sm"
  158. on:submit|preventDefault={() => {
  159. submitHandler();
  160. saveHandler();
  161. }}
  162. >
  163. <div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[28rem]">
  164. <div class="flex flex-col gap-0.5">
  165. <div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
  166. <div class=" flex w-full justify-between">
  167. <div class=" self-center text-xs font-medium">
  168. {$i18n.t('Scan for documents from {{path}}', { path: 'DOCS_DIR (/data/docs)' })}
  169. </div>
  170. <button
  171. class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
  172. ? ' cursor-not-allowed'
  173. : ''}"
  174. on:click={() => {
  175. scanHandler();
  176. console.log('check');
  177. }}
  178. type="button"
  179. disabled={scanDirLoading}
  180. >
  181. <div class="self-center font-medium">{$i18n.t('Scan')}</div>
  182. {#if scanDirLoading}
  183. <div class="ml-3 self-center">
  184. <svg
  185. class=" w-3 h-3"
  186. viewBox="0 0 24 24"
  187. fill="currentColor"
  188. xmlns="http://www.w3.org/2000/svg"
  189. ><style>
  190. .spinner_ajPY {
  191. transform-origin: center;
  192. animation: spinner_AtaB 0.75s infinite linear;
  193. }
  194. @keyframes spinner_AtaB {
  195. 100% {
  196. transform: rotate(360deg);
  197. }
  198. }
  199. </style><path
  200. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  201. opacity=".25"
  202. /><path
  203. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  204. class="spinner_ajPY"
  205. /></svg
  206. >
  207. </div>
  208. {/if}
  209. </button>
  210. </div>
  211. <div class=" flex w-full justify-between">
  212. <div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
  213. <div class="flex items-center relative">
  214. <select
  215. class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
  216. bind:value={embeddingEngine}
  217. placeholder="Select an embedding model engine"
  218. on:change={(e) => {
  219. if (e.target.value === 'ollama') {
  220. embeddingModel = '';
  221. } else if (e.target.value === 'openai') {
  222. embeddingModel = 'text-embedding-3-small';
  223. }
  224. }}
  225. >
  226. <option value="">{$i18n.t('Default (SentenceTransformers)')}</option>
  227. <option value="ollama">{$i18n.t('Ollama')}</option>
  228. <option value="openai">{$i18n.t('OpenAI')}</option>
  229. </select>
  230. </div>
  231. </div>
  232. {#if embeddingEngine === 'openai'}
  233. <div class="my-0.5 flex gap-2">
  234. <input
  235. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  236. placeholder={$i18n.t('API Base URL')}
  237. bind:value={OpenAIUrl}
  238. required
  239. />
  240. <input
  241. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  242. placeholder={$i18n.t('API Key')}
  243. bind:value={OpenAIKey}
  244. required
  245. />
  246. </div>
  247. {/if}
  248. <div class=" flex w-full justify-between">
  249. <div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
  250. <button
  251. class="p-1 px-3 text-xs flex rounded transition"
  252. on:click={() => {
  253. toggleHybridSearch();
  254. }}
  255. type="button"
  256. >
  257. {#if querySettings.hybrid === true}
  258. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  259. {:else}
  260. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  261. {/if}
  262. </button>
  263. </div>
  264. </div>
  265. <hr class=" dark:border-gray-700 my-1" />
  266. <div class="space-y-2" />
  267. <div>
  268. <div class=" mb-2 text-sm font-medium">{$i18n.t('Embedding Model')}</div>
  269. {#if embeddingEngine === 'ollama'}
  270. <div class="flex w-full">
  271. <div class="flex-1 mr-2">
  272. <select
  273. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  274. bind:value={embeddingModel}
  275. placeholder={$i18n.t('Select a model')}
  276. required
  277. >
  278. {#if !embeddingModel}
  279. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  280. {/if}
  281. {#each $models.filter((m) => m.id && !m.external) as model}
  282. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  283. >{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
  284. >
  285. {/each}
  286. </select>
  287. </div>
  288. </div>
  289. {:else}
  290. <div class="flex w-full">
  291. <div class="flex-1 mr-2">
  292. <input
  293. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  294. placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
  295. model: embeddingModel.slice(-40)
  296. })}
  297. bind:value={embeddingModel}
  298. />
  299. </div>
  300. {#if embeddingEngine === ''}
  301. <button
  302. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  303. on:click={() => {
  304. embeddingModelUpdateHandler();
  305. }}
  306. disabled={updateEmbeddingModelLoading}
  307. >
  308. {#if updateEmbeddingModelLoading}
  309. <div class="self-center">
  310. <svg
  311. class=" w-4 h-4"
  312. viewBox="0 0 24 24"
  313. fill="currentColor"
  314. xmlns="http://www.w3.org/2000/svg"
  315. ><style>
  316. .spinner_ajPY {
  317. transform-origin: center;
  318. animation: spinner_AtaB 0.75s infinite linear;
  319. }
  320. @keyframes spinner_AtaB {
  321. 100% {
  322. transform: rotate(360deg);
  323. }
  324. }
  325. </style><path
  326. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  327. opacity=".25"
  328. /><path
  329. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  330. class="spinner_ajPY"
  331. /></svg
  332. >
  333. </div>
  334. {:else}
  335. <svg
  336. xmlns="http://www.w3.org/2000/svg"
  337. viewBox="0 0 16 16"
  338. fill="currentColor"
  339. class="w-4 h-4"
  340. >
  341. <path
  342. d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
  343. />
  344. <path
  345. d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
  346. />
  347. </svg>
  348. {/if}
  349. </button>
  350. {/if}
  351. </div>
  352. {/if}
  353. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  354. {$i18n.t(
  355. 'Warning: If you update or change your embedding model, you will need to re-import all documents.'
  356. )}
  357. </div>
  358. {#if querySettings.hybrid === true}
  359. <div class=" ">
  360. <div class=" mb-2 text-sm font-medium">{$i18n.t('Reranking Model')}</div>
  361. <div class="flex w-full">
  362. <div class="flex-1 mr-2">
  363. <input
  364. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  365. placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
  366. model: 'BAAI/bge-reranker-v2-m3'
  367. })}
  368. bind:value={rerankingModel}
  369. />
  370. </div>
  371. <button
  372. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  373. on:click={() => {
  374. rerankingModelUpdateHandler();
  375. }}
  376. disabled={updateRerankingModelLoading}
  377. >
  378. {#if updateRerankingModelLoading}
  379. <div class="self-center">
  380. <svg
  381. class=" w-4 h-4"
  382. viewBox="0 0 24 24"
  383. fill="currentColor"
  384. xmlns="http://www.w3.org/2000/svg"
  385. ><style>
  386. .spinner_ajPY {
  387. transform-origin: center;
  388. animation: spinner_AtaB 0.75s infinite linear;
  389. }
  390. @keyframes spinner_AtaB {
  391. 100% {
  392. transform: rotate(360deg);
  393. }
  394. }
  395. </style><path
  396. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  397. opacity=".25"
  398. /><path
  399. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  400. class="spinner_ajPY"
  401. /></svg
  402. >
  403. </div>
  404. {:else}
  405. <svg
  406. xmlns="http://www.w3.org/2000/svg"
  407. viewBox="0 0 16 16"
  408. fill="currentColor"
  409. class="w-4 h-4"
  410. >
  411. <path
  412. d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
  413. />
  414. <path
  415. d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
  416. />
  417. </svg>
  418. {/if}
  419. </button>
  420. </div>
  421. </div>
  422. {/if}
  423. </div>
  424. <hr class=" dark:border-gray-700" />
  425. {#if showResetConfirm}
  426. <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
  427. <div class="flex items-center space-x-3">
  428. <svg
  429. xmlns="http://www.w3.org/2000/svg"
  430. viewBox="0 0 16 16"
  431. fill="currentColor"
  432. class="w-4 h-4"
  433. >
  434. <path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
  435. <path
  436. fill-rule="evenodd"
  437. d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
  438. clip-rule="evenodd"
  439. />
  440. </svg>
  441. <span>{$i18n.t('Are you sure?')}</span>
  442. </div>
  443. <div class="flex space-x-1.5 items-center">
  444. <button
  445. class="hover:text-white transition"
  446. on:click={() => {
  447. const res = resetVectorDB(localStorage.token).catch((error) => {
  448. toast.error(error);
  449. return null;
  450. });
  451. if (res) {
  452. toast.success($i18n.t('Success'));
  453. }
  454. showResetConfirm = false;
  455. }}
  456. >
  457. <svg
  458. xmlns="http://www.w3.org/2000/svg"
  459. viewBox="0 0 20 20"
  460. fill="currentColor"
  461. class="w-4 h-4"
  462. >
  463. <path
  464. fill-rule="evenodd"
  465. d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
  466. clip-rule="evenodd"
  467. />
  468. </svg>
  469. </button>
  470. <button
  471. class="hover:text-white transition"
  472. on:click={() => {
  473. showResetConfirm = false;
  474. }}
  475. >
  476. <svg
  477. xmlns="http://www.w3.org/2000/svg"
  478. viewBox="0 0 20 20"
  479. fill="currentColor"
  480. class="w-4 h-4"
  481. >
  482. <path
  483. d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
  484. />
  485. </svg>
  486. </button>
  487. </div>
  488. </div>
  489. {:else}
  490. <button
  491. class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
  492. on:click={() => {
  493. showResetConfirm = true;
  494. }}
  495. >
  496. <div class=" self-center mr-3">
  497. <svg
  498. xmlns="http://www.w3.org/2000/svg"
  499. viewBox="0 0 16 16"
  500. fill="currentColor"
  501. class="w-4 h-4"
  502. >
  503. <path
  504. fill-rule="evenodd"
  505. d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
  506. clip-rule="evenodd"
  507. />
  508. </svg>
  509. </div>
  510. <div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
  511. </button>
  512. {/if}
  513. </div>
  514. <div class="flex justify-end pt-3 text-sm font-medium">
  515. <button
  516. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  517. type="submit"
  518. >
  519. {$i18n.t('Save')}
  520. </button>
  521. </div>
  522. </form>