General.svelte 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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. } else if (e.target.value === '') {
  224. embeddingModel = 'sentence-transformers/all-MiniLM-L6-v2';
  225. }
  226. }}
  227. >
  228. <option value="">{$i18n.t('Default (SentenceTransformers)')}</option>
  229. <option value="ollama">{$i18n.t('Ollama')}</option>
  230. <option value="openai">{$i18n.t('OpenAI')}</option>
  231. </select>
  232. </div>
  233. </div>
  234. {#if embeddingEngine === 'openai'}
  235. <div class="my-0.5 flex gap-2">
  236. <input
  237. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  238. placeholder={$i18n.t('API Base URL')}
  239. bind:value={OpenAIUrl}
  240. required
  241. />
  242. <input
  243. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  244. placeholder={$i18n.t('API Key')}
  245. bind:value={OpenAIKey}
  246. required
  247. />
  248. </div>
  249. {/if}
  250. <div class=" flex w-full justify-between">
  251. <div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
  252. <button
  253. class="p-1 px-3 text-xs flex rounded transition"
  254. on:click={() => {
  255. toggleHybridSearch();
  256. }}
  257. type="button"
  258. >
  259. {#if querySettings.hybrid === true}
  260. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  261. {:else}
  262. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  263. {/if}
  264. </button>
  265. </div>
  266. </div>
  267. <hr class=" dark:border-gray-700 my-1" />
  268. <div class="space-y-2" />
  269. <div>
  270. <div class=" mb-2 text-sm font-medium">{$i18n.t('Embedding Model')}</div>
  271. {#if embeddingEngine === 'ollama'}
  272. <div class="flex w-full">
  273. <div class="flex-1 mr-2">
  274. <select
  275. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  276. bind:value={embeddingModel}
  277. placeholder={$i18n.t('Select a model')}
  278. required
  279. >
  280. {#if !embeddingModel}
  281. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  282. {/if}
  283. {#each $models.filter((m) => m.id && !m.external) as model}
  284. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  285. >{(model.custom_info?.name ?? model.name) +
  286. ' (' +
  287. (model.size / 1024 ** 3).toFixed(1) +
  288. ' GB)'}</option
  289. >
  290. {/each}
  291. </select>
  292. </div>
  293. </div>
  294. {:else}
  295. <div class="flex w-full">
  296. <div class="flex-1 mr-2">
  297. <input
  298. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  299. placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
  300. model: embeddingModel.slice(-40)
  301. })}
  302. bind:value={embeddingModel}
  303. />
  304. </div>
  305. {#if embeddingEngine === ''}
  306. <button
  307. 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"
  308. on:click={() => {
  309. embeddingModelUpdateHandler();
  310. }}
  311. disabled={updateEmbeddingModelLoading}
  312. >
  313. {#if updateEmbeddingModelLoading}
  314. <div class="self-center">
  315. <svg
  316. class=" w-4 h-4"
  317. viewBox="0 0 24 24"
  318. fill="currentColor"
  319. xmlns="http://www.w3.org/2000/svg"
  320. ><style>
  321. .spinner_ajPY {
  322. transform-origin: center;
  323. animation: spinner_AtaB 0.75s infinite linear;
  324. }
  325. @keyframes spinner_AtaB {
  326. 100% {
  327. transform: rotate(360deg);
  328. }
  329. }
  330. </style><path
  331. 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"
  332. opacity=".25"
  333. /><path
  334. 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"
  335. class="spinner_ajPY"
  336. /></svg
  337. >
  338. </div>
  339. {:else}
  340. <svg
  341. xmlns="http://www.w3.org/2000/svg"
  342. viewBox="0 0 16 16"
  343. fill="currentColor"
  344. class="w-4 h-4"
  345. >
  346. <path
  347. 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"
  348. />
  349. <path
  350. 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"
  351. />
  352. </svg>
  353. {/if}
  354. </button>
  355. {/if}
  356. </div>
  357. {/if}
  358. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  359. {$i18n.t(
  360. 'Warning: If you update or change your embedding model, you will need to re-import all documents.'
  361. )}
  362. </div>
  363. {#if querySettings.hybrid === true}
  364. <div class=" ">
  365. <div class=" mb-2 text-sm font-medium">{$i18n.t('Reranking Model')}</div>
  366. <div class="flex w-full">
  367. <div class="flex-1 mr-2">
  368. <input
  369. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  370. placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
  371. model: 'BAAI/bge-reranker-v2-m3'
  372. })}
  373. bind:value={rerankingModel}
  374. />
  375. </div>
  376. <button
  377. 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"
  378. on:click={() => {
  379. rerankingModelUpdateHandler();
  380. }}
  381. disabled={updateRerankingModelLoading}
  382. >
  383. {#if updateRerankingModelLoading}
  384. <div class="self-center">
  385. <svg
  386. class=" w-4 h-4"
  387. viewBox="0 0 24 24"
  388. fill="currentColor"
  389. xmlns="http://www.w3.org/2000/svg"
  390. ><style>
  391. .spinner_ajPY {
  392. transform-origin: center;
  393. animation: spinner_AtaB 0.75s infinite linear;
  394. }
  395. @keyframes spinner_AtaB {
  396. 100% {
  397. transform: rotate(360deg);
  398. }
  399. }
  400. </style><path
  401. 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"
  402. opacity=".25"
  403. /><path
  404. 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"
  405. class="spinner_ajPY"
  406. /></svg
  407. >
  408. </div>
  409. {:else}
  410. <svg
  411. xmlns="http://www.w3.org/2000/svg"
  412. viewBox="0 0 16 16"
  413. fill="currentColor"
  414. class="w-4 h-4"
  415. >
  416. <path
  417. 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"
  418. />
  419. <path
  420. 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"
  421. />
  422. </svg>
  423. {/if}
  424. </button>
  425. </div>
  426. </div>
  427. {/if}
  428. </div>
  429. <hr class=" dark:border-gray-700" />
  430. {#if showResetConfirm}
  431. <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
  432. <div class="flex items-center space-x-3">
  433. <svg
  434. xmlns="http://www.w3.org/2000/svg"
  435. viewBox="0 0 16 16"
  436. fill="currentColor"
  437. class="w-4 h-4"
  438. >
  439. <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" />
  440. <path
  441. fill-rule="evenodd"
  442. 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"
  443. clip-rule="evenodd"
  444. />
  445. </svg>
  446. <span>{$i18n.t('Are you sure?')}</span>
  447. </div>
  448. <div class="flex space-x-1.5 items-center">
  449. <button
  450. class="hover:text-white transition"
  451. on:click={() => {
  452. const res = resetVectorDB(localStorage.token).catch((error) => {
  453. toast.error(error);
  454. return null;
  455. });
  456. if (res) {
  457. toast.success($i18n.t('Success'));
  458. }
  459. showResetConfirm = false;
  460. }}
  461. >
  462. <svg
  463. xmlns="http://www.w3.org/2000/svg"
  464. viewBox="0 0 20 20"
  465. fill="currentColor"
  466. class="w-4 h-4"
  467. >
  468. <path
  469. fill-rule="evenodd"
  470. 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"
  471. clip-rule="evenodd"
  472. />
  473. </svg>
  474. </button>
  475. <button
  476. class="hover:text-white transition"
  477. on:click={() => {
  478. showResetConfirm = false;
  479. }}
  480. >
  481. <svg
  482. xmlns="http://www.w3.org/2000/svg"
  483. viewBox="0 0 20 20"
  484. fill="currentColor"
  485. class="w-4 h-4"
  486. >
  487. <path
  488. 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"
  489. />
  490. </svg>
  491. </button>
  492. </div>
  493. </div>
  494. {:else}
  495. <button
  496. class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
  497. on:click={() => {
  498. showResetConfirm = true;
  499. }}
  500. >
  501. <div class=" self-center mr-3">
  502. <svg
  503. xmlns="http://www.w3.org/2000/svg"
  504. viewBox="0 0 16 16"
  505. fill="currentColor"
  506. class="w-4 h-4"
  507. >
  508. <path
  509. fill-rule="evenodd"
  510. 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"
  511. clip-rule="evenodd"
  512. />
  513. </svg>
  514. </div>
  515. <div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
  516. </button>
  517. {/if}
  518. </div>
  519. <div class="flex justify-end pt-3 text-sm font-medium">
  520. <button
  521. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  522. type="submit"
  523. >
  524. {$i18n.t('Save')}
  525. </button>
  526. </div>
  527. </form>