Interface.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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. import { updateUserInfo } from '$lib/apis/users';
  9. import { getUserPosition } from '$lib/utils';
  10. const dispatch = createEventDispatcher();
  11. const i18n = getContext('i18n');
  12. export let saveSettings: Function;
  13. let backgroundImageUrl = null;
  14. let inputFiles = null;
  15. let filesInputElement;
  16. // Addons
  17. let titleAutoGenerate = true;
  18. let responseAutoCopy = false;
  19. let widescreenMode = false;
  20. let splitLargeChunks = false;
  21. let scrollOnBranchChange = true;
  22. let userLocation = false;
  23. // Interface
  24. let defaultModelId = '';
  25. let showUsername = false;
  26. let chatBubble = true;
  27. let chatDirection: 'LTR' | 'RTL' = 'LTR';
  28. let showEmojiInCall = false;
  29. let voiceInterruption = false;
  30. let hapticFeedback = false;
  31. let streamResponse = true;
  32. const toggleSplitLargeChunks = async () => {
  33. splitLargeChunks = !splitLargeChunks;
  34. saveSettings({ splitLargeChunks: splitLargeChunks });
  35. };
  36. const toggleStreamResponse = async () => {
  37. streamResponse = !streamResponse;
  38. saveSettings({ streamResponse: streamResponse });
  39. };
  40. const togglesScrollOnBranchChange = async () => {
  41. scrollOnBranchChange = !scrollOnBranchChange;
  42. saveSettings({ scrollOnBranchChange: scrollOnBranchChange });
  43. };
  44. const togglewidescreenMode = async () => {
  45. widescreenMode = !widescreenMode;
  46. saveSettings({ widescreenMode: widescreenMode });
  47. };
  48. const toggleChatBubble = async () => {
  49. chatBubble = !chatBubble;
  50. saveSettings({ chatBubble: chatBubble });
  51. };
  52. const toggleShowUsername = async () => {
  53. showUsername = !showUsername;
  54. saveSettings({ showUsername: showUsername });
  55. };
  56. const toggleEmojiInCall = async () => {
  57. showEmojiInCall = !showEmojiInCall;
  58. saveSettings({ showEmojiInCall: showEmojiInCall });
  59. };
  60. const toggleVoiceInterruption = async () => {
  61. voiceInterruption = !voiceInterruption;
  62. saveSettings({ voiceInterruption: voiceInterruption });
  63. };
  64. const toggleHapticFeedback = async () => {
  65. hapticFeedback = !hapticFeedback;
  66. saveSettings({ hapticFeedback: hapticFeedback });
  67. };
  68. const toggleUserLocation = async () => {
  69. userLocation = !userLocation;
  70. if (userLocation) {
  71. const position = await getUserPosition().catch((error) => {
  72. toast.error(error.message);
  73. return null;
  74. });
  75. if (position) {
  76. await updateUserInfo(localStorage.token, { location: position });
  77. toast.success($i18n.t('User location successfully retrieved.'));
  78. } else {
  79. userLocation = false;
  80. }
  81. }
  82. saveSettings({ userLocation });
  83. };
  84. const toggleTitleAutoGenerate = async () => {
  85. titleAutoGenerate = !titleAutoGenerate;
  86. saveSettings({
  87. title: {
  88. ...$settings.title,
  89. auto: titleAutoGenerate
  90. }
  91. });
  92. };
  93. const toggleResponseAutoCopy = async () => {
  94. const permission = await navigator.clipboard
  95. .readText()
  96. .then(() => {
  97. return 'granted';
  98. })
  99. .catch(() => {
  100. return '';
  101. });
  102. console.log(permission);
  103. if (permission === 'granted') {
  104. responseAutoCopy = !responseAutoCopy;
  105. saveSettings({ responseAutoCopy: responseAutoCopy });
  106. } else {
  107. toast.error(
  108. $i18n.t(
  109. 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
  110. )
  111. );
  112. }
  113. };
  114. const toggleChangeChatDirection = async () => {
  115. chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
  116. saveSettings({ chatDirection });
  117. };
  118. const updateInterfaceHandler = async () => {
  119. saveSettings({
  120. models: [defaultModelId]
  121. });
  122. };
  123. onMount(async () => {
  124. titleAutoGenerate = $settings?.title?.auto ?? true;
  125. responseAutoCopy = $settings.responseAutoCopy ?? false;
  126. showUsername = $settings.showUsername ?? false;
  127. showEmojiInCall = $settings.showEmojiInCall ?? false;
  128. voiceInterruption = $settings.voiceInterruption ?? false;
  129. chatBubble = $settings.chatBubble ?? true;
  130. widescreenMode = $settings.widescreenMode ?? false;
  131. splitLargeChunks = $settings.splitLargeChunks ?? false;
  132. scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
  133. chatDirection = $settings.chatDirection ?? 'LTR';
  134. userLocation = $settings.userLocation ?? false;
  135. hapticFeedback = $settings.hapticFeedback ?? false;
  136. streamResponse = $settings?.streamResponse ?? true;
  137. defaultModelId = $settings?.models?.at(0) ?? '';
  138. if ($config?.default_models) {
  139. defaultModelId = $config.default_models.split(',')[0];
  140. }
  141. backgroundImageUrl = $settings.backgroundImageUrl ?? null;
  142. });
  143. </script>
  144. <form
  145. class="flex flex-col h-full justify-between space-y-3 text-sm"
  146. on:submit|preventDefault={() => {
  147. updateInterfaceHandler();
  148. dispatch('save');
  149. }}
  150. >
  151. <input
  152. bind:this={filesInputElement}
  153. bind:files={inputFiles}
  154. type="file"
  155. hidden
  156. accept="image/*"
  157. on:change={() => {
  158. let reader = new FileReader();
  159. reader.onload = (event) => {
  160. let originalImageUrl = `${event.target.result}`;
  161. backgroundImageUrl = originalImageUrl;
  162. saveSettings({ backgroundImageUrl });
  163. };
  164. if (
  165. inputFiles &&
  166. inputFiles.length > 0 &&
  167. ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
  168. ) {
  169. reader.readAsDataURL(inputFiles[0]);
  170. } else {
  171. console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
  172. inputFiles = null;
  173. }
  174. }}
  175. />
  176. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden">
  177. <div class=" space-y-1 mb-3">
  178. <div class="mb-2">
  179. <div class="flex justify-between items-center text-xs">
  180. <div class=" text-sm font-medium">{$i18n.t('Default Model')}</div>
  181. </div>
  182. </div>
  183. <div class="flex-1 mr-2">
  184. <select
  185. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  186. bind:value={defaultModelId}
  187. placeholder="Select a model"
  188. >
  189. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  190. {#each $models.filter((model) => model.id) as model}
  191. <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
  192. {/each}
  193. </select>
  194. </div>
  195. </div>
  196. <hr class=" dark:border-gray-850" />
  197. <div>
  198. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
  199. <div>
  200. <div class=" py-0.5 flex w-full justify-between">
  201. <div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
  202. <button
  203. class="p-1 px-3 text-xs flex rounded transition"
  204. on:click={() => {
  205. toggleChatBubble();
  206. }}
  207. type="button"
  208. >
  209. {#if chatBubble === true}
  210. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  211. {:else}
  212. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  213. {/if}
  214. </button>
  215. </div>
  216. </div>
  217. {#if !$settings.chatBubble}
  218. <div>
  219. <div class=" py-0.5 flex w-full justify-between">
  220. <div class=" self-center text-xs">
  221. {$i18n.t('Display the username instead of You in the Chat')}
  222. </div>
  223. <button
  224. class="p-1 px-3 text-xs flex rounded transition"
  225. on:click={() => {
  226. toggleShowUsername();
  227. }}
  228. type="button"
  229. >
  230. {#if showUsername === true}
  231. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  232. {:else}
  233. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  234. {/if}
  235. </button>
  236. </div>
  237. </div>
  238. {/if}
  239. <div>
  240. <div class=" py-0.5 flex w-full justify-between">
  241. <div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
  242. <button
  243. class="p-1 px-3 text-xs flex rounded transition"
  244. on:click={() => {
  245. togglewidescreenMode();
  246. }}
  247. type="button"
  248. >
  249. {#if widescreenMode === true}
  250. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  251. {:else}
  252. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  253. {/if}
  254. </button>
  255. </div>
  256. </div>
  257. <div>
  258. <div class=" py-0.5 flex w-full justify-between">
  259. <div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>
  260. <button
  261. class="p-1 px-3 text-xs flex rounded transition"
  262. on:click={toggleChangeChatDirection}
  263. type="button"
  264. >
  265. {#if chatDirection === 'LTR'}
  266. <span class="ml-2 self-center">{$i18n.t('LTR')}</span>
  267. {:else}
  268. <span class="ml-2 self-center">{$i18n.t('RTL')}</span>
  269. {/if}
  270. </button>
  271. </div>
  272. </div>
  273. <div>
  274. <div class=" py-0.5 flex w-full justify-between">
  275. <div class=" self-center text-xs">
  276. {$i18n.t('Stream Chat Response')}
  277. </div>
  278. <button
  279. class="p-1 px-3 text-xs flex rounded transition"
  280. on:click={() => {
  281. toggleStreamResponse();
  282. }}
  283. type="button"
  284. >
  285. {#if streamResponse === true}
  286. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  287. {:else}
  288. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  289. {/if}
  290. </button>
  291. </div>
  292. </div>
  293. <div>
  294. <div class=" py-0.5 flex w-full justify-between">
  295. <div class=" self-center text-xs">
  296. {$i18n.t('Fluidly stream large external response chunks')}
  297. </div>
  298. <button
  299. class="p-1 px-3 text-xs flex rounded transition"
  300. on:click={() => {
  301. toggleSplitLargeChunks();
  302. }}
  303. type="button"
  304. >
  305. {#if splitLargeChunks === true}
  306. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  307. {:else}
  308. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  309. {/if}
  310. </button>
  311. </div>
  312. </div>
  313. <div>
  314. <div class=" py-0.5 flex w-full justify-between">
  315. <div class=" self-center text-xs">
  316. {$i18n.t('Scroll to bottom when switching between branches')}
  317. </div>
  318. <button
  319. class="p-1 px-3 text-xs flex rounded transition"
  320. on:click={() => {
  321. togglesScrollOnBranchChange();
  322. }}
  323. type="button"
  324. >
  325. {#if scrollOnBranchChange === true}
  326. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  327. {:else}
  328. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  329. {/if}
  330. </button>
  331. </div>
  332. </div>
  333. <div>
  334. <div class=" py-0.5 flex w-full justify-between">
  335. <div class=" self-center text-xs">
  336. {$i18n.t('Chat Background Image')}
  337. </div>
  338. <button
  339. class="p-1 px-3 text-xs flex rounded transition"
  340. on:click={() => {
  341. if (backgroundImageUrl !== null) {
  342. backgroundImageUrl = null;
  343. saveSettings({ backgroundImageUrl });
  344. } else {
  345. filesInputElement.click();
  346. }
  347. }}
  348. type="button"
  349. >
  350. {#if backgroundImageUrl !== null}
  351. <span class="ml-2 self-center">{$i18n.t('Reset')}</span>
  352. {:else}
  353. <span class="ml-2 self-center">{$i18n.t('Upload')}</span>
  354. {/if}
  355. </button>
  356. </div>
  357. </div>
  358. <div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
  359. <div>
  360. <div class=" py-0.5 flex w-full justify-between">
  361. <div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
  362. <button
  363. class="p-1 px-3 text-xs flex rounded transition"
  364. on:click={() => {
  365. toggleTitleAutoGenerate();
  366. }}
  367. type="button"
  368. >
  369. {#if titleAutoGenerate === true}
  370. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  371. {:else}
  372. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  373. {/if}
  374. </button>
  375. </div>
  376. </div>
  377. <div>
  378. <div class=" py-0.5 flex w-full justify-between">
  379. <div class=" self-center text-xs">
  380. {$i18n.t('Response AutoCopy to Clipboard')}
  381. </div>
  382. <button
  383. class="p-1 px-3 text-xs flex rounded transition"
  384. on:click={() => {
  385. toggleResponseAutoCopy();
  386. }}
  387. type="button"
  388. >
  389. {#if responseAutoCopy === true}
  390. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  391. {:else}
  392. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  393. {/if}
  394. </button>
  395. </div>
  396. </div>
  397. <div>
  398. <div class=" py-0.5 flex w-full justify-between">
  399. <div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
  400. <button
  401. class="p-1 px-3 text-xs flex rounded transition"
  402. on:click={() => {
  403. toggleUserLocation();
  404. }}
  405. type="button"
  406. >
  407. {#if userLocation === true}
  408. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  409. {:else}
  410. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  411. {/if}
  412. </button>
  413. </div>
  414. </div>
  415. <div>
  416. <div class=" py-0.5 flex w-full justify-between">
  417. <div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div>
  418. <button
  419. class="p-1 px-3 text-xs flex rounded transition"
  420. on:click={() => {
  421. toggleHapticFeedback();
  422. }}
  423. type="button"
  424. >
  425. {#if hapticFeedback === true}
  426. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  427. {:else}
  428. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  429. {/if}
  430. </button>
  431. </div>
  432. </div>
  433. <div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
  434. <div>
  435. <div class=" py-0.5 flex w-full justify-between">
  436. <div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
  437. <button
  438. class="p-1 px-3 text-xs flex rounded transition"
  439. on:click={() => {
  440. toggleVoiceInterruption();
  441. }}
  442. type="button"
  443. >
  444. {#if voiceInterruption === true}
  445. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  446. {:else}
  447. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  448. {/if}
  449. </button>
  450. </div>
  451. </div>
  452. <div>
  453. <div class=" py-0.5 flex w-full justify-between">
  454. <div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
  455. <button
  456. class="p-1 px-3 text-xs flex rounded transition"
  457. on:click={() => {
  458. toggleEmojiInCall();
  459. }}
  460. type="button"
  461. >
  462. {#if showEmojiInCall === true}
  463. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  464. {:else}
  465. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  466. {/if}
  467. </button>
  468. </div>
  469. </div>
  470. </div>
  471. </div>
  472. <div class="flex justify-end text-sm font-medium">
  473. <button
  474. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  475. type="submit"
  476. >
  477. {$i18n.t('Save')}
  478. </button>
  479. </div>
  480. </form>