Interface.svelte 10 KB

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