Interface.svelte 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. const dispatch = createEventDispatcher();
  8. const i18n = getContext('i18n');
  9. export let saveSettings: Function;
  10. // Addons
  11. let titleAutoGenerate = true;
  12. let responseAutoCopy = false;
  13. let titleAutoGenerateModel = '';
  14. let titleAutoGenerateModelExternal = '';
  15. let fullScreenMode = false;
  16. let titleGenerationPrompt = '';
  17. let splitLargeChunks = false;
  18. // Interface
  19. let promptSuggestions = [];
  20. let showUsername = false;
  21. const toggleSplitLargeChunks = async () => {
  22. splitLargeChunks = !splitLargeChunks;
  23. saveSettings({ splitLargeChunks: splitLargeChunks });
  24. };
  25. const toggleFullScreenMode = async () => {
  26. fullScreenMode = !fullScreenMode;
  27. saveSettings({ fullScreenMode: fullScreenMode });
  28. };
  29. const toggleShowUsername = async () => {
  30. showUsername = !showUsername;
  31. saveSettings({ showUsername: showUsername });
  32. };
  33. const toggleTitleAutoGenerate = async () => {
  34. titleAutoGenerate = !titleAutoGenerate;
  35. saveSettings({
  36. title: {
  37. ...$settings.title,
  38. auto: titleAutoGenerate
  39. }
  40. });
  41. };
  42. const toggleResponseAutoCopy = async () => {
  43. const permission = await navigator.clipboard
  44. .readText()
  45. .then(() => {
  46. return 'granted';
  47. })
  48. .catch(() => {
  49. return '';
  50. });
  51. console.log(permission);
  52. if (permission === 'granted') {
  53. responseAutoCopy = !responseAutoCopy;
  54. saveSettings({ responseAutoCopy: responseAutoCopy });
  55. } else {
  56. toast.error(
  57. 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
  58. );
  59. }
  60. };
  61. const updateInterfaceHandler = async () => {
  62. if ($user.role === 'admin') {
  63. promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
  64. await config.set(await getBackendConfig());
  65. }
  66. saveSettings({
  67. title: {
  68. ...$settings.title,
  69. model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
  70. modelExternal:
  71. titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
  72. prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
  73. }
  74. });
  75. };
  76. onMount(async () => {
  77. if ($user.role === 'admin') {
  78. promptSuggestions = $config?.default_prompt_suggestions;
  79. }
  80. let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
  81. titleAutoGenerate = settings?.title?.auto ?? true;
  82. titleAutoGenerateModel = settings?.title?.model ?? '';
  83. titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
  84. titleGenerationPrompt =
  85. settings?.title?.prompt ??
  86. $i18n.t(
  87. "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':"
  88. ) + ' {{prompt}}';
  89. responseAutoCopy = settings.responseAutoCopy ?? false;
  90. showUsername = settings.showUsername ?? false;
  91. fullScreenMode = settings.fullScreenMode ?? false;
  92. splitLargeChunks = settings.splitLargeChunks ?? false;
  93. });
  94. </script>
  95. <form
  96. class="flex flex-col h-full justify-between space-y-3 text-sm"
  97. on:submit|preventDefault={() => {
  98. updateInterfaceHandler();
  99. dispatch('save');
  100. }}
  101. >
  102. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
  103. <div>
  104. <div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
  105. <div>
  106. <div class=" py-0.5 flex w-full justify-between">
  107. <div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
  108. <button
  109. class="p-1 px-3 text-xs flex rounded transition"
  110. on:click={() => {
  111. toggleTitleAutoGenerate();
  112. }}
  113. type="button"
  114. >
  115. {#if titleAutoGenerate === true}
  116. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  117. {:else}
  118. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  119. {/if}
  120. </button>
  121. </div>
  122. </div>
  123. <div>
  124. <div class=" py-0.5 flex w-full justify-between">
  125. <div class=" self-center text-xs font-medium">
  126. {$i18n.t('Response AutoCopy to Clipboard')}
  127. </div>
  128. <button
  129. class="p-1 px-3 text-xs flex rounded transition"
  130. on:click={() => {
  131. toggleResponseAutoCopy();
  132. }}
  133. type="button"
  134. >
  135. {#if responseAutoCopy === true}
  136. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  137. {:else}
  138. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  139. {/if}
  140. </button>
  141. </div>
  142. </div>
  143. <div>
  144. <div class=" py-0.5 flex w-full justify-between">
  145. <div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
  146. <button
  147. class="p-1 px-3 text-xs flex rounded transition"
  148. on:click={() => {
  149. toggleFullScreenMode();
  150. }}
  151. type="button"
  152. >
  153. {#if fullScreenMode === true}
  154. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  155. {:else}
  156. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  157. {/if}
  158. </button>
  159. </div>
  160. </div>
  161. <div>
  162. <div class=" py-0.5 flex w-full justify-between">
  163. <div class=" self-center text-xs font-medium">
  164. {$i18n.t('Display the username instead of You in the Chat')}
  165. </div>
  166. <button
  167. class="p-1 px-3 text-xs flex rounded transition"
  168. on:click={() => {
  169. toggleShowUsername();
  170. }}
  171. type="button"
  172. >
  173. {#if showUsername === true}
  174. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  175. {:else}
  176. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  177. {/if}
  178. </button>
  179. </div>
  180. </div>
  181. <div>
  182. <div class=" py-0.5 flex w-full justify-between">
  183. <div class=" self-center text-xs font-medium">
  184. {$i18n.t('Fluidly stream large external response chunks')}
  185. </div>
  186. <button
  187. class="p-1 px-3 text-xs flex rounded transition"
  188. on:click={() => {
  189. toggleSplitLargeChunks();
  190. }}
  191. type="button"
  192. >
  193. {#if splitLargeChunks === true}
  194. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  195. {:else}
  196. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  197. {/if}
  198. </button>
  199. </div>
  200. </div>
  201. </div>
  202. <hr class=" dark:border-gray-700" />
  203. <div>
  204. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
  205. <div class="flex w-full gap-2 pr-2">
  206. <div class="flex-1">
  207. <div class=" text-xs mb-1">Local Models</div>
  208. <select
  209. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  210. bind:value={titleAutoGenerateModel}
  211. placeholder={$i18n.t('Select a model')}
  212. >
  213. <option value="" selected>{$i18n.t('Current Model')}</option>
  214. {#each $models as model}
  215. {#if model.size != null}
  216. <option value={model.name} class="bg-gray-100 dark:bg-gray-700">
  217. {model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}
  218. </option>
  219. {/if}
  220. {/each}
  221. </select>
  222. </div>
  223. <div class="flex-1">
  224. <div class=" text-xs mb-1">External Models</div>
  225. <select
  226. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  227. bind:value={titleAutoGenerateModelExternal}
  228. placeholder={$i18n.t('Select a model')}
  229. >
  230. <option value="" selected>{$i18n.t('Current Model')}</option>
  231. {#each $models as model}
  232. {#if model.name !== 'hr'}
  233. <option value={model.name} class="bg-gray-100 dark:bg-gray-700">
  234. {model.name}
  235. </option>
  236. {/if}
  237. {/each}
  238. </select>
  239. </div>
  240. </div>
  241. <div class="mt-3 mr-2">
  242. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
  243. <textarea
  244. bind:value={titleGenerationPrompt}
  245. class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
  246. rows="3"
  247. />
  248. </div>
  249. </div>
  250. {#if $user.role === 'admin'}
  251. <hr class=" dark:border-gray-700" />
  252. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
  253. <div class="flex w-full justify-between mb-2">
  254. <div class=" self-center text-sm font-semibold">
  255. {$i18n.t('Default Prompt Suggestions')}
  256. </div>
  257. <button
  258. class="p-1 px-3 text-xs flex rounded transition"
  259. type="button"
  260. on:click={() => {
  261. if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
  262. promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
  263. }
  264. }}
  265. >
  266. <svg
  267. xmlns="http://www.w3.org/2000/svg"
  268. viewBox="0 0 20 20"
  269. fill="currentColor"
  270. class="w-4 h-4"
  271. >
  272. <path
  273. 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"
  274. />
  275. </svg>
  276. </button>
  277. </div>
  278. <div class="flex flex-col space-y-1">
  279. {#each promptSuggestions as prompt, promptIdx}
  280. <div class=" flex border dark:border-gray-600 rounded-lg">
  281. <div class="flex flex-col flex-1">
  282. <div class="flex border-b dark:border-gray-600 w-full">
  283. <input
  284. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  285. placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
  286. bind:value={prompt.title[0]}
  287. />
  288. <input
  289. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  290. placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
  291. bind:value={prompt.title[1]}
  292. />
  293. </div>
  294. <input
  295. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  296. placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
  297. bind:value={prompt.content}
  298. />
  299. </div>
  300. <button
  301. class="px-2"
  302. type="button"
  303. on:click={() => {
  304. promptSuggestions.splice(promptIdx, 1);
  305. promptSuggestions = promptSuggestions;
  306. }}
  307. >
  308. <svg
  309. xmlns="http://www.w3.org/2000/svg"
  310. viewBox="0 0 20 20"
  311. fill="currentColor"
  312. class="w-4 h-4"
  313. >
  314. <path
  315. 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"
  316. />
  317. </svg>
  318. </button>
  319. </div>
  320. {/each}
  321. </div>
  322. {#if promptSuggestions.length > 0}
  323. <div class="text-xs text-left w-full mt-2">
  324. {$i18n.t('Adjusting these settings will apply changes universally to all users.')}
  325. </div>
  326. {/if}
  327. </div>
  328. {/if}
  329. </div>
  330. <div class="flex justify-end text-sm font-medium">
  331. <button
  332. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  333. type="submit"
  334. >
  335. {$i18n.t('Save')}
  336. </button>
  337. </div>
  338. </form>