Interface.svelte 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <script lang="ts">
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { toast } from 'svelte-sonner';
  4. import { getBackendConfig, getTaskConfig, updateTaskConfig } from '$lib/apis';
  5. import { setDefaultPromptSuggestions } from '$lib/apis/configs';
  6. import { config, models, settings, user } from '$lib/stores';
  7. import { createEventDispatcher, onMount, getContext } from 'svelte';
  8. import { banners as _banners } from '$lib/stores';
  9. import type { Banner } from '$lib/types';
  10. import { getBanners, setBanners } from '$lib/apis/configs';
  11. import Tooltip from '$lib/components/common/Tooltip.svelte';
  12. import Switch from '$lib/components/common/Switch.svelte';
  13. import Textarea from '$lib/components/common/Textarea.svelte';
  14. const dispatch = createEventDispatcher();
  15. const i18n = getContext('i18n');
  16. let taskConfig = {
  17. TASK_MODEL: '',
  18. TASK_MODEL_EXTERNAL: '',
  19. TITLE_GENERATION_PROMPT_TEMPLATE: '',
  20. TAG_GENERATION_PROMPT_TEMPLATE: '',
  21. ENABLE_SEARCH_QUERY: true,
  22. SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
  23. };
  24. let promptSuggestions = [];
  25. let banners: Banner[] = [];
  26. const updateInterfaceHandler = async () => {
  27. taskConfig = await updateTaskConfig(localStorage.token, taskConfig);
  28. promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
  29. await updateBanners();
  30. await config.set(await getBackendConfig());
  31. };
  32. onMount(async () => {
  33. taskConfig = await getTaskConfig(localStorage.token);
  34. promptSuggestions = $config?.default_prompt_suggestions;
  35. banners = await getBanners(localStorage.token);
  36. });
  37. const updateBanners = async () => {
  38. _banners.set(await setBanners(localStorage.token, banners));
  39. };
  40. </script>
  41. <form
  42. class="flex flex-col h-full justify-between space-y-3 text-sm"
  43. on:submit|preventDefault={() => {
  44. updateInterfaceHandler();
  45. dispatch('save');
  46. }}
  47. >
  48. <div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
  49. <div>
  50. <div class=" mb-2.5 text-sm font-medium flex">
  51. <div class=" mr-1">{$i18n.t('Set Task Model')}</div>
  52. <Tooltip
  53. content={$i18n.t(
  54. 'A task model is used when performing tasks such as generating titles for chats and web search queries'
  55. )}
  56. >
  57. <svg
  58. xmlns="http://www.w3.org/2000/svg"
  59. fill="none"
  60. viewBox="0 0 24 24"
  61. stroke-width="1.5"
  62. stroke="currentColor"
  63. class="w-5 h-5"
  64. >
  65. <path
  66. stroke-linecap="round"
  67. stroke-linejoin="round"
  68. d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
  69. />
  70. </svg>
  71. </Tooltip>
  72. </div>
  73. <div class="flex w-full gap-2">
  74. <div class="flex-1">
  75. <div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
  76. <select
  77. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  78. bind:value={taskConfig.TASK_MODEL}
  79. placeholder={$i18n.t('Select a model')}
  80. >
  81. <option value="" selected>{$i18n.t('Current Model')}</option>
  82. {#each $models.filter((m) => m.owned_by === 'ollama') as model}
  83. <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
  84. {model.name}
  85. </option>
  86. {/each}
  87. </select>
  88. </div>
  89. <div class="flex-1">
  90. <div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
  91. <select
  92. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  93. bind:value={taskConfig.TASK_MODEL_EXTERNAL}
  94. placeholder={$i18n.t('Select a model')}
  95. >
  96. <option value="" selected>{$i18n.t('Current Model')}</option>
  97. {#each $models as model}
  98. <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
  99. {model.name}
  100. </option>
  101. {/each}
  102. </select>
  103. </div>
  104. </div>
  105. <div class="mt-3">
  106. <div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
  107. <Tooltip
  108. content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
  109. placement="top-start"
  110. >
  111. <Textarea
  112. bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
  113. placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
  114. />
  115. </Tooltip>
  116. </div>
  117. <div class="mt-3">
  118. <div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
  119. <Tooltip
  120. content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
  121. placement="top-start"
  122. >
  123. <Textarea
  124. bind:value={taskConfig.TAG_GENERATION_PROMPT_TEMPLATE}
  125. placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
  126. />
  127. </Tooltip>
  128. </div>
  129. <hr class=" dark:border-gray-850 my-3" />
  130. <div class="my-3 flex w-full items-center justify-between">
  131. <div class=" self-center text-xs font-medium">
  132. {$i18n.t('Enable Web Search Query Generation')}
  133. </div>
  134. <Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY} />
  135. </div>
  136. {#if taskConfig.ENABLE_SEARCH_QUERY}
  137. <div class="">
  138. <div class=" mb-2.5 text-xs font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
  139. <Tooltip
  140. content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
  141. placement="top-start"
  142. >
  143. <Textarea
  144. bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
  145. placeholder={$i18n.t(
  146. 'Leave empty to use the default prompt, or enter a custom prompt'
  147. )}
  148. />
  149. </Tooltip>
  150. </div>
  151. {/if}
  152. </div>
  153. <hr class=" dark:border-gray-850 my-3" />
  154. <div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
  155. <div class="flex w-full justify-between">
  156. <div class=" self-center text-sm font-semibold">
  157. {$i18n.t('Banners')}
  158. </div>
  159. <button
  160. class="p-1 px-3 text-xs flex rounded transition"
  161. type="button"
  162. on:click={() => {
  163. if (banners.length === 0 || banners.at(-1).content !== '') {
  164. banners = [
  165. ...banners,
  166. {
  167. id: uuidv4(),
  168. type: '',
  169. title: '',
  170. content: '',
  171. dismissible: true,
  172. timestamp: Math.floor(Date.now() / 1000)
  173. }
  174. ];
  175. }
  176. }}
  177. >
  178. <svg
  179. xmlns="http://www.w3.org/2000/svg"
  180. viewBox="0 0 20 20"
  181. fill="currentColor"
  182. class="w-4 h-4"
  183. >
  184. <path
  185. d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
  186. />
  187. </svg>
  188. </button>
  189. </div>
  190. <div class="flex flex-col space-y-1">
  191. {#each banners as banner, bannerIdx}
  192. <div class=" flex justify-between">
  193. <div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
  194. <select
  195. class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
  196. bind:value={banner.type}
  197. required
  198. >
  199. {#if banner.type == ''}
  200. <option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
  201. >
  202. {/if}
  203. <option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
  204. <option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
  205. <option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
  206. <option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
  207. </select>
  208. <input
  209. class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
  210. placeholder={$i18n.t('Content')}
  211. bind:value={banner.content}
  212. />
  213. <div class="relative top-1.5 -left-2">
  214. <Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
  215. <Switch bind:state={banner.dismissible} />
  216. </Tooltip>
  217. </div>
  218. </div>
  219. <button
  220. class="px-2"
  221. type="button"
  222. on:click={() => {
  223. banners.splice(bannerIdx, 1);
  224. banners = banners;
  225. }}
  226. >
  227. <svg
  228. xmlns="http://www.w3.org/2000/svg"
  229. viewBox="0 0 20 20"
  230. fill="currentColor"
  231. class="w-4 h-4"
  232. >
  233. <path
  234. 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"
  235. />
  236. </svg>
  237. </button>
  238. </div>
  239. {/each}
  240. </div>
  241. </div>
  242. {#if $user.role === 'admin'}
  243. <div class=" space-y-3">
  244. <div class="flex w-full justify-between mb-2">
  245. <div class=" self-center text-sm font-semibold">
  246. {$i18n.t('Default Prompt Suggestions')}
  247. </div>
  248. <button
  249. class="p-1 px-3 text-xs flex rounded transition"
  250. type="button"
  251. on:click={() => {
  252. if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
  253. promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
  254. }
  255. }}
  256. >
  257. <svg
  258. xmlns="http://www.w3.org/2000/svg"
  259. viewBox="0 0 20 20"
  260. fill="currentColor"
  261. class="w-4 h-4"
  262. >
  263. <path
  264. d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
  265. />
  266. </svg>
  267. </button>
  268. </div>
  269. <div class="grid lg:grid-cols-2 flex-col gap-1.5">
  270. {#each promptSuggestions as prompt, promptIdx}
  271. <div
  272. class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
  273. >
  274. <div class="flex flex-col flex-1 pl-1">
  275. <div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
  276. <input
  277. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
  278. placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
  279. bind:value={prompt.title[0]}
  280. />
  281. <input
  282. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
  283. placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
  284. bind:value={prompt.title[1]}
  285. />
  286. </div>
  287. <textarea
  288. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
  289. placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
  290. rows="3"
  291. bind:value={prompt.content}
  292. />
  293. </div>
  294. <button
  295. class="px-3"
  296. type="button"
  297. on:click={() => {
  298. promptSuggestions.splice(promptIdx, 1);
  299. promptSuggestions = promptSuggestions;
  300. }}
  301. >
  302. <svg
  303. xmlns="http://www.w3.org/2000/svg"
  304. viewBox="0 0 20 20"
  305. fill="currentColor"
  306. class="w-4 h-4"
  307. >
  308. <path
  309. 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"
  310. />
  311. </svg>
  312. </button>
  313. </div>
  314. {/each}
  315. </div>
  316. {#if promptSuggestions.length > 0}
  317. <div class="text-xs text-left w-full mt-2">
  318. {$i18n.t('Adjusting these settings will apply changes universally to all users.')}
  319. </div>
  320. {/if}
  321. </div>
  322. {/if}
  323. </div>
  324. <div class="flex justify-end text-sm font-medium">
  325. <button
  326. class="px-3 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
  327. type="submit"
  328. >
  329. {$i18n.t('Save')}
  330. </button>
  331. </div>
  332. </form>