Interface.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <script lang="ts">
  2. import { getBackendConfig } from '$lib/apis';
  3. import { setDefaultPromptSuggestions } from '$lib/apis/configs';
  4. import { config, models, settings, user } from '$lib/stores';
  5. import { createEventDispatcher, onMount, getContext } from 'svelte';
  6. import { toast } from 'svelte-sonner';
  7. import Tooltip from '$lib/components/common/Tooltip.svelte';
  8. const dispatch = createEventDispatcher();
  9. const i18n = getContext('i18n');
  10. export let saveSettings: Function;
  11. // Addons
  12. let titleAutoGenerate = true;
  13. let responseAutoCopy = false;
  14. let titleAutoGenerateModel = '';
  15. let titleAutoGenerateModelExternal = '';
  16. let widescreenMode = false;
  17. let titleGenerationPrompt = '';
  18. let splitLargeChunks = false;
  19. // Interface
  20. let defaultModelId = '';
  21. let promptSuggestions = [];
  22. let showUsername = false;
  23. let chatBubble = true;
  24. let chatDirection: 'LTR' | 'RTL' = 'LTR';
  25. const toggleSplitLargeChunks = async () => {
  26. splitLargeChunks = !splitLargeChunks;
  27. saveSettings({ splitLargeChunks: splitLargeChunks });
  28. };
  29. const togglewidescreenMode = async () => {
  30. widescreenMode = !widescreenMode;
  31. saveSettings({ widescreenMode: widescreenMode });
  32. };
  33. const toggleChatBubble = async () => {
  34. chatBubble = !chatBubble;
  35. saveSettings({ chatBubble: chatBubble });
  36. };
  37. const toggleShowUsername = async () => {
  38. showUsername = !showUsername;
  39. saveSettings({ showUsername: showUsername });
  40. };
  41. const toggleTitleAutoGenerate = async () => {
  42. titleAutoGenerate = !titleAutoGenerate;
  43. saveSettings({
  44. title: {
  45. ...$settings.title,
  46. auto: titleAutoGenerate
  47. }
  48. });
  49. };
  50. const toggleResponseAutoCopy = async () => {
  51. const permission = await navigator.clipboard
  52. .readText()
  53. .then(() => {
  54. return 'granted';
  55. })
  56. .catch(() => {
  57. return '';
  58. });
  59. console.log(permission);
  60. if (permission === 'granted') {
  61. responseAutoCopy = !responseAutoCopy;
  62. saveSettings({ responseAutoCopy: responseAutoCopy });
  63. } else {
  64. toast.error(
  65. 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
  66. );
  67. }
  68. };
  69. const toggleChangeChatDirection = async () => {
  70. chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
  71. saveSettings({ chatDirection });
  72. };
  73. const updateInterfaceHandler = async () => {
  74. if ($user.role === 'admin') {
  75. promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
  76. await config.set(await getBackendConfig());
  77. }
  78. saveSettings({
  79. title: {
  80. ...$settings.title,
  81. model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
  82. modelExternal:
  83. titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
  84. prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
  85. },
  86. models: [defaultModelId]
  87. });
  88. };
  89. onMount(async () => {
  90. if ($user.role === 'admin') {
  91. promptSuggestions = $config?.default_prompt_suggestions;
  92. }
  93. titleAutoGenerate = $settings?.title?.auto ?? true;
  94. titleAutoGenerateModel = $settings?.title?.model ?? '';
  95. titleAutoGenerateModelExternal = $settings?.title?.modelExternal ?? '';
  96. titleGenerationPrompt =
  97. $settings?.title?.prompt ??
  98. `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
  99. responseAutoCopy = $settings.responseAutoCopy ?? false;
  100. showUsername = $settings.showUsername ?? false;
  101. chatBubble = $settings.chatBubble ?? true;
  102. widescreenMode = $settings.widescreenMode ?? false;
  103. splitLargeChunks = $settings.splitLargeChunks ?? false;
  104. chatDirection = $settings.chatDirection ?? 'LTR';
  105. defaultModelId = ($settings?.models ?? ['']).at(0);
  106. });
  107. </script>
  108. <form
  109. class="flex flex-col h-full justify-between space-y-3 text-sm"
  110. on:submit|preventDefault={() => {
  111. updateInterfaceHandler();
  112. dispatch('save');
  113. }}
  114. >
  115. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
  116. <div>
  117. <div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
  118. <div>
  119. <div class=" py-0.5 flex w-full justify-between">
  120. <div class=" self-center text-xs font-medium">{$i18n.t('Chat Bubble UI')}</div>
  121. <button
  122. class="p-1 px-3 text-xs flex rounded transition"
  123. on:click={() => {
  124. toggleChatBubble();
  125. }}
  126. type="button"
  127. >
  128. {#if chatBubble === true}
  129. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  130. {:else}
  131. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  132. {/if}
  133. </button>
  134. </div>
  135. </div>
  136. <div>
  137. <div class=" py-0.5 flex w-full justify-between">
  138. <div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
  139. <button
  140. class="p-1 px-3 text-xs flex rounded transition"
  141. on:click={() => {
  142. toggleTitleAutoGenerate();
  143. }}
  144. type="button"
  145. >
  146. {#if titleAutoGenerate === true}
  147. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  148. {:else}
  149. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  150. {/if}
  151. </button>
  152. </div>
  153. </div>
  154. <div>
  155. <div class=" py-0.5 flex w-full justify-between">
  156. <div class=" self-center text-xs font-medium">
  157. {$i18n.t('Response AutoCopy to Clipboard')}
  158. </div>
  159. <button
  160. class="p-1 px-3 text-xs flex rounded transition"
  161. on:click={() => {
  162. toggleResponseAutoCopy();
  163. }}
  164. type="button"
  165. >
  166. {#if responseAutoCopy === true}
  167. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  168. {:else}
  169. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  170. {/if}
  171. </button>
  172. </div>
  173. </div>
  174. <div>
  175. <div class=" py-0.5 flex w-full justify-between">
  176. <div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
  177. <button
  178. class="p-1 px-3 text-xs flex rounded transition"
  179. on:click={() => {
  180. togglewidescreenMode();
  181. }}
  182. type="button"
  183. >
  184. {#if widescreenMode === true}
  185. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  186. {:else}
  187. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  188. {/if}
  189. </button>
  190. </div>
  191. </div>
  192. {#if !$settings.chatBubble}
  193. <div>
  194. <div class=" py-0.5 flex w-full justify-between">
  195. <div class=" self-center text-xs font-medium">
  196. {$i18n.t('Display the username instead of You in the Chat')}
  197. </div>
  198. <button
  199. class="p-1 px-3 text-xs flex rounded transition"
  200. on:click={() => {
  201. toggleShowUsername();
  202. }}
  203. type="button"
  204. >
  205. {#if showUsername === true}
  206. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  207. {:else}
  208. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  209. {/if}
  210. </button>
  211. </div>
  212. </div>
  213. {/if}
  214. <div>
  215. <div class=" py-0.5 flex w-full justify-between">
  216. <div class=" self-center text-xs font-medium">
  217. {$i18n.t('Fluidly stream large external response chunks')}
  218. </div>
  219. <button
  220. class="p-1 px-3 text-xs flex rounded transition"
  221. on:click={() => {
  222. toggleSplitLargeChunks();
  223. }}
  224. type="button"
  225. >
  226. {#if splitLargeChunks === true}
  227. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  228. {:else}
  229. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  230. {/if}
  231. </button>
  232. </div>
  233. </div>
  234. </div>
  235. <div>
  236. <div class=" py-0.5 flex w-full justify-between">
  237. <div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>
  238. <button
  239. class="p-1 px-3 text-xs flex rounded transition"
  240. on:click={toggleChangeChatDirection}
  241. type="button"
  242. >
  243. {#if chatDirection === 'LTR'}
  244. <span class="ml-2 self-center">{$i18n.t('LTR')}</span>
  245. {:else}
  246. <span class="ml-2 self-center">{$i18n.t('RTL')}</span>
  247. {/if}
  248. </button>
  249. </div>
  250. </div>
  251. <hr class=" dark:border-gray-850" />
  252. <div class=" space-y-1 mb-3">
  253. <div class="mb-2">
  254. <div class="flex justify-between items-center text-xs">
  255. <div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
  256. </div>
  257. </div>
  258. <div class="flex-1 mr-2">
  259. <select
  260. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  261. bind:value={defaultModelId}
  262. placeholder="Select a model"
  263. >
  264. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  265. {#each $models.filter((model) => model.id) as model}
  266. <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
  267. {/each}
  268. </select>
  269. </div>
  270. </div>
  271. <hr class=" dark:border-gray-850" />
  272. <div>
  273. <div class=" mb-2.5 text-sm font-medium flex">
  274. <div class=" mr-1">{$i18n.t('Set Task Model')}</div>
  275. <Tooltip
  276. content={$i18n.t(
  277. 'A task model is used when performing tasks such as generating titles for chats and web search queries'
  278. )}
  279. >
  280. <svg
  281. xmlns="http://www.w3.org/2000/svg"
  282. fill="none"
  283. viewBox="0 0 24 24"
  284. stroke-width="1.5"
  285. stroke="currentColor"
  286. class="w-5 h-5"
  287. >
  288. <path
  289. stroke-linecap="round"
  290. stroke-linejoin="round"
  291. 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"
  292. />
  293. </svg>
  294. </Tooltip>
  295. </div>
  296. <div class="flex w-full gap-2 pr-2">
  297. <div class="flex-1">
  298. <div class=" text-xs mb-1">Local Models</div>
  299. <select
  300. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  301. bind:value={titleAutoGenerateModel}
  302. placeholder={$i18n.t('Select a model')}
  303. >
  304. <option value="" selected>{$i18n.t('Current Model')}</option>
  305. {#each $models.filter((m) => m.owned_by === 'ollama') as model}
  306. <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
  307. {model.name}
  308. </option>
  309. {/each}
  310. </select>
  311. </div>
  312. <div class="flex-1">
  313. <div class=" text-xs mb-1">External Models</div>
  314. <select
  315. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  316. bind:value={titleAutoGenerateModelExternal}
  317. placeholder={$i18n.t('Select a model')}
  318. >
  319. <option value="" selected>{$i18n.t('Current Model')}</option>
  320. {#each $models as model}
  321. <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
  322. {model.name}
  323. </option>
  324. {/each}
  325. </select>
  326. </div>
  327. </div>
  328. <div class="mt-3 mr-2">
  329. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
  330. <textarea
  331. bind:value={titleGenerationPrompt}
  332. class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
  333. rows="3"
  334. />
  335. </div>
  336. </div>
  337. {#if $user.role === 'admin'}
  338. <hr class=" dark:border-gray-700" />
  339. <div class=" space-y-3 pr-1.5">
  340. <div class="flex w-full justify-between mb-2">
  341. <div class=" self-center text-sm font-semibold">
  342. {$i18n.t('Default Prompt Suggestions')}
  343. </div>
  344. <button
  345. class="p-1 px-3 text-xs flex rounded transition"
  346. type="button"
  347. on:click={() => {
  348. if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
  349. promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
  350. }
  351. }}
  352. >
  353. <svg
  354. xmlns="http://www.w3.org/2000/svg"
  355. viewBox="0 0 20 20"
  356. fill="currentColor"
  357. class="w-4 h-4"
  358. >
  359. <path
  360. 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"
  361. />
  362. </svg>
  363. </button>
  364. </div>
  365. <div class="flex flex-col space-y-1">
  366. {#each promptSuggestions as prompt, promptIdx}
  367. <div class=" flex border dark:border-gray-600 rounded-lg">
  368. <div class="flex flex-col flex-1">
  369. <div class="flex border-b dark:border-gray-600 w-full">
  370. <input
  371. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  372. placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
  373. bind:value={prompt.title[0]}
  374. />
  375. <input
  376. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  377. placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
  378. bind:value={prompt.title[1]}
  379. />
  380. </div>
  381. <input
  382. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  383. placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
  384. bind:value={prompt.content}
  385. />
  386. </div>
  387. <button
  388. class="px-2"
  389. type="button"
  390. on:click={() => {
  391. promptSuggestions.splice(promptIdx, 1);
  392. promptSuggestions = promptSuggestions;
  393. }}
  394. >
  395. <svg
  396. xmlns="http://www.w3.org/2000/svg"
  397. viewBox="0 0 20 20"
  398. fill="currentColor"
  399. class="w-4 h-4"
  400. >
  401. <path
  402. 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"
  403. />
  404. </svg>
  405. </button>
  406. </div>
  407. {/each}
  408. </div>
  409. {#if promptSuggestions.length > 0}
  410. <div class="text-xs text-left w-full mt-2">
  411. {$i18n.t('Adjusting these settings will apply changes universally to all users.')}
  412. </div>
  413. {/if}
  414. </div>
  415. {/if}
  416. </div>
  417. <div class="flex justify-end text-sm font-medium">
  418. <button
  419. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  420. type="submit"
  421. >
  422. {$i18n.t('Save')}
  423. </button>
  424. </div>
  425. </form>