Interface.svelte 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  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 autoTags = true;
  19. let responseAutoCopy = false;
  20. let widescreenMode = false;
  21. let splitLargeChunks = false;
  22. let scrollOnBranchChange = true;
  23. let userLocation = false;
  24. // Interface
  25. let defaultModelId = '';
  26. let showUsername = false;
  27. let richTextInput = true;
  28. let largeTextAsFile = false;
  29. let notificationSound = true;
  30. let landingPageMode = '';
  31. let chatBubble = true;
  32. let chatDirection: 'LTR' | 'RTL' = 'LTR';
  33. let imageCompression = false;
  34. let imageCompressionSize = {
  35. width: '',
  36. height: ''
  37. };
  38. // Admin - Show Update Available Toast
  39. let showUpdateToast = true;
  40. let showChangelog = true;
  41. let showEmojiInCall = false;
  42. let voiceInterruption = false;
  43. let hapticFeedback = false;
  44. let webSearch = null;
  45. const toggleSplitLargeChunks = async () => {
  46. splitLargeChunks = !splitLargeChunks;
  47. saveSettings({ splitLargeChunks: splitLargeChunks });
  48. };
  49. const togglesScrollOnBranchChange = async () => {
  50. scrollOnBranchChange = !scrollOnBranchChange;
  51. saveSettings({ scrollOnBranchChange: scrollOnBranchChange });
  52. };
  53. const toggleWidescreenMode = async () => {
  54. widescreenMode = !widescreenMode;
  55. saveSettings({ widescreenMode: widescreenMode });
  56. };
  57. const toggleChatBubble = async () => {
  58. chatBubble = !chatBubble;
  59. saveSettings({ chatBubble: chatBubble });
  60. };
  61. const toggleLandingPageMode = async () => {
  62. landingPageMode = landingPageMode === '' ? 'chat' : '';
  63. saveSettings({ landingPageMode: landingPageMode });
  64. };
  65. const toggleShowUpdateToast = async () => {
  66. showUpdateToast = !showUpdateToast;
  67. saveSettings({ showUpdateToast: showUpdateToast });
  68. };
  69. const toggleNotificationSound = async () => {
  70. notificationSound = !notificationSound;
  71. saveSettings({ notificationSound: notificationSound });
  72. };
  73. const toggleShowChangelog = async () => {
  74. showChangelog = !showChangelog;
  75. saveSettings({ showChangelog: showChangelog });
  76. };
  77. const toggleShowUsername = async () => {
  78. showUsername = !showUsername;
  79. saveSettings({ showUsername: showUsername });
  80. };
  81. const toggleEmojiInCall = async () => {
  82. showEmojiInCall = !showEmojiInCall;
  83. saveSettings({ showEmojiInCall: showEmojiInCall });
  84. };
  85. const toggleVoiceInterruption = async () => {
  86. voiceInterruption = !voiceInterruption;
  87. saveSettings({ voiceInterruption: voiceInterruption });
  88. };
  89. const toggleImageCompression = async () => {
  90. imageCompression = !imageCompression;
  91. saveSettings({ imageCompression });
  92. };
  93. const toggleHapticFeedback = async () => {
  94. hapticFeedback = !hapticFeedback;
  95. saveSettings({ hapticFeedback: hapticFeedback });
  96. };
  97. const toggleUserLocation = async () => {
  98. userLocation = !userLocation;
  99. if (userLocation) {
  100. const position = await getUserPosition().catch((error) => {
  101. toast.error(error.message);
  102. return null;
  103. });
  104. if (position) {
  105. await updateUserInfo(localStorage.token, { location: position });
  106. toast.success($i18n.t('User location successfully retrieved.'));
  107. } else {
  108. userLocation = false;
  109. }
  110. }
  111. saveSettings({ userLocation });
  112. };
  113. const toggleTitleAutoGenerate = async () => {
  114. titleAutoGenerate = !titleAutoGenerate;
  115. saveSettings({
  116. title: {
  117. ...$settings.title,
  118. auto: titleAutoGenerate
  119. }
  120. });
  121. };
  122. const toggleAutoTags = async () => {
  123. autoTags = !autoTags;
  124. saveSettings({ autoTags });
  125. };
  126. const toggleRichTextInput = async () => {
  127. richTextInput = !richTextInput;
  128. saveSettings({ richTextInput });
  129. };
  130. const toggleLargeTextAsFile = async () => {
  131. largeTextAsFile = !largeTextAsFile;
  132. saveSettings({ largeTextAsFile });
  133. };
  134. const toggleResponseAutoCopy = async () => {
  135. const permission = await navigator.clipboard
  136. .readText()
  137. .then(() => {
  138. return 'granted';
  139. })
  140. .catch(() => {
  141. return '';
  142. });
  143. console.log(permission);
  144. if (permission === 'granted') {
  145. responseAutoCopy = !responseAutoCopy;
  146. saveSettings({ responseAutoCopy: responseAutoCopy });
  147. } else {
  148. toast.error(
  149. $i18n.t(
  150. 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
  151. )
  152. );
  153. }
  154. };
  155. const toggleChangeChatDirection = async () => {
  156. chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
  157. saveSettings({ chatDirection });
  158. };
  159. const updateInterfaceHandler = async () => {
  160. saveSettings({
  161. models: [defaultModelId],
  162. imageCompressionSize: imageCompressionSize
  163. });
  164. };
  165. const toggleWebSearch = async () => {
  166. webSearch = webSearch === null ? 'always' : null;
  167. saveSettings({ webSearch: webSearch });
  168. };
  169. onMount(async () => {
  170. titleAutoGenerate = $settings?.title?.auto ?? true;
  171. autoTags = $settings.autoTags ?? true;
  172. responseAutoCopy = $settings.responseAutoCopy ?? false;
  173. showUsername = $settings.showUsername ?? false;
  174. showUpdateToast = $settings.showUpdateToast ?? true;
  175. showChangelog = $settings.showChangelog ?? true;
  176. showEmojiInCall = $settings.showEmojiInCall ?? false;
  177. voiceInterruption = $settings.voiceInterruption ?? false;
  178. richTextInput = $settings.richTextInput ?? true;
  179. largeTextAsFile = $settings.largeTextAsFile ?? false;
  180. landingPageMode = $settings.landingPageMode ?? '';
  181. chatBubble = $settings.chatBubble ?? true;
  182. widescreenMode = $settings.widescreenMode ?? false;
  183. splitLargeChunks = $settings.splitLargeChunks ?? false;
  184. scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
  185. chatDirection = $settings.chatDirection ?? 'LTR';
  186. userLocation = $settings.userLocation ?? false;
  187. notificationSound = $settings.notificationSound ?? true;
  188. hapticFeedback = $settings.hapticFeedback ?? false;
  189. imageCompression = $settings.imageCompression ?? false;
  190. imageCompressionSize = $settings.imageCompressionSize ?? { width: '', height: '' };
  191. defaultModelId = $settings?.models?.at(0) ?? '';
  192. if ($config?.default_models) {
  193. defaultModelId = $config.default_models.split(',')[0];
  194. }
  195. backgroundImageUrl = $settings.backgroundImageUrl ?? null;
  196. webSearch = $settings.webSearch ?? null;
  197. });
  198. </script>
  199. <form
  200. class="flex flex-col h-full justify-between space-y-3 text-sm"
  201. on:submit|preventDefault={() => {
  202. updateInterfaceHandler();
  203. dispatch('save');
  204. }}
  205. >
  206. <input
  207. bind:this={filesInputElement}
  208. bind:files={inputFiles}
  209. type="file"
  210. hidden
  211. accept="image/*"
  212. on:change={() => {
  213. let reader = new FileReader();
  214. reader.onload = (event) => {
  215. let originalImageUrl = `${event.target.result}`;
  216. backgroundImageUrl = originalImageUrl;
  217. saveSettings({ backgroundImageUrl });
  218. };
  219. if (
  220. inputFiles &&
  221. inputFiles.length > 0 &&
  222. ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
  223. ) {
  224. reader.readAsDataURL(inputFiles[0]);
  225. } else {
  226. console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
  227. inputFiles = null;
  228. }
  229. }}
  230. />
  231. <div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
  232. <div>
  233. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
  234. <div>
  235. <div class=" py-0.5 flex w-full justify-between">
  236. <div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div>
  237. <button
  238. class="p-1 px-3 text-xs flex rounded transition"
  239. on:click={() => {
  240. toggleLandingPageMode();
  241. }}
  242. type="button"
  243. >
  244. {#if landingPageMode === ''}
  245. <span class="ml-2 self-center">{$i18n.t('Default')}</span>
  246. {:else}
  247. <span class="ml-2 self-center">{$i18n.t('Chat')}</span>
  248. {/if}
  249. </button>
  250. </div>
  251. </div>
  252. <div>
  253. <div class=" py-0.5 flex w-full justify-between">
  254. <div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
  255. <button
  256. class="p-1 px-3 text-xs flex rounded transition"
  257. on:click={() => {
  258. toggleChatBubble();
  259. }}
  260. type="button"
  261. >
  262. {#if chatBubble === true}
  263. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  264. {:else}
  265. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  266. {/if}
  267. </button>
  268. </div>
  269. </div>
  270. {#if !$settings.chatBubble}
  271. <div>
  272. <div class=" py-0.5 flex w-full justify-between">
  273. <div class=" self-center text-xs">
  274. {$i18n.t('Display the username instead of You in the Chat')}
  275. </div>
  276. <button
  277. class="p-1 px-3 text-xs flex rounded transition"
  278. on:click={() => {
  279. toggleShowUsername();
  280. }}
  281. type="button"
  282. >
  283. {#if showUsername === true}
  284. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  285. {:else}
  286. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  287. {/if}
  288. </button>
  289. </div>
  290. </div>
  291. {/if}
  292. <div>
  293. <div class=" py-0.5 flex w-full justify-between">
  294. <div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
  295. <button
  296. class="p-1 px-3 text-xs flex rounded transition"
  297. on:click={() => {
  298. toggleWidescreenMode();
  299. }}
  300. type="button"
  301. >
  302. {#if widescreenMode === true}
  303. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  304. {:else}
  305. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  306. {/if}
  307. </button>
  308. </div>
  309. </div>
  310. <div>
  311. <div class=" py-0.5 flex w-full justify-between">
  312. <div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>
  313. <button
  314. class="p-1 px-3 text-xs flex rounded transition"
  315. on:click={toggleChangeChatDirection}
  316. type="button"
  317. >
  318. {#if chatDirection === 'LTR'}
  319. <span class="ml-2 self-center">{$i18n.t('LTR')}</span>
  320. {:else}
  321. <span class="ml-2 self-center">{$i18n.t('RTL')}</span>
  322. {/if}
  323. </button>
  324. </div>
  325. </div>
  326. <div>
  327. <div class=" py-0.5 flex w-full justify-between">
  328. <div class=" self-center text-xs">
  329. {$i18n.t('Notification Sound')}
  330. </div>
  331. <button
  332. class="p-1 px-3 text-xs flex rounded transition"
  333. on:click={() => {
  334. toggleNotificationSound();
  335. }}
  336. type="button"
  337. >
  338. {#if notificationSound === true}
  339. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  340. {:else}
  341. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  342. {/if}
  343. </button>
  344. </div>
  345. </div>
  346. {#if $user.role === 'admin'}
  347. <div>
  348. <div class=" py-0.5 flex w-full justify-between">
  349. <div class=" self-center text-xs">
  350. {$i18n.t('Toast notifications for new updates')}
  351. </div>
  352. <button
  353. class="p-1 px-3 text-xs flex rounded transition"
  354. on:click={() => {
  355. toggleShowUpdateToast();
  356. }}
  357. type="button"
  358. >
  359. {#if showUpdateToast === true}
  360. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  361. {:else}
  362. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  363. {/if}
  364. </button>
  365. </div>
  366. </div>
  367. <div>
  368. <div class=" py-0.5 flex w-full justify-between">
  369. <div class=" self-center text-xs">
  370. {$i18n.t(`Show "What's New" modal on login`)}
  371. </div>
  372. <button
  373. class="p-1 px-3 text-xs flex rounded transition"
  374. on:click={() => {
  375. toggleShowChangelog();
  376. }}
  377. type="button"
  378. >
  379. {#if showChangelog === true}
  380. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  381. {:else}
  382. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  383. {/if}
  384. </button>
  385. </div>
  386. </div>
  387. {/if}
  388. <div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
  389. <div>
  390. <div class=" py-0.5 flex w-full justify-between">
  391. <div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
  392. <button
  393. class="p-1 px-3 text-xs flex rounded transition"
  394. on:click={() => {
  395. toggleTitleAutoGenerate();
  396. }}
  397. type="button"
  398. >
  399. {#if titleAutoGenerate === true}
  400. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  401. {:else}
  402. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  403. {/if}
  404. </button>
  405. </div>
  406. </div>
  407. <div>
  408. <div class=" py-0.5 flex w-full justify-between">
  409. <div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div>
  410. <button
  411. class="p-1 px-3 text-xs flex rounded transition"
  412. on:click={() => {
  413. toggleAutoTags();
  414. }}
  415. type="button"
  416. >
  417. {#if autoTags === true}
  418. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  419. {:else}
  420. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  421. {/if}
  422. </button>
  423. </div>
  424. </div>
  425. <div>
  426. <div class=" py-0.5 flex w-full justify-between">
  427. <div class=" self-center text-xs">
  428. {$i18n.t('Auto-Copy Response to Clipboard')}
  429. </div>
  430. <button
  431. class="p-1 px-3 text-xs flex rounded transition"
  432. on:click={() => {
  433. toggleResponseAutoCopy();
  434. }}
  435. type="button"
  436. >
  437. {#if responseAutoCopy === true}
  438. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  439. {:else}
  440. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  441. {/if}
  442. </button>
  443. </div>
  444. </div>
  445. <div>
  446. <div class=" py-0.5 flex w-full justify-between">
  447. <div class=" self-center text-xs">
  448. {$i18n.t('Rich Text Input for Chat')}
  449. </div>
  450. <button
  451. class="p-1 px-3 text-xs flex rounded transition"
  452. on:click={() => {
  453. toggleRichTextInput();
  454. }}
  455. type="button"
  456. >
  457. {#if richTextInput === true}
  458. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  459. {:else}
  460. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  461. {/if}
  462. </button>
  463. </div>
  464. </div>
  465. <div>
  466. <div class=" py-0.5 flex w-full justify-between">
  467. <div class=" self-center text-xs">
  468. {$i18n.t('Paste Large Text as File')}
  469. </div>
  470. <button
  471. class="p-1 px-3 text-xs flex rounded transition"
  472. on:click={() => {
  473. toggleLargeTextAsFile();
  474. }}
  475. type="button"
  476. >
  477. {#if largeTextAsFile === true}
  478. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  479. {:else}
  480. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  481. {/if}
  482. </button>
  483. </div>
  484. </div>
  485. <div>
  486. <div class=" py-0.5 flex w-full justify-between">
  487. <div class=" self-center text-xs">
  488. {$i18n.t('Chat Background Image')}
  489. </div>
  490. <button
  491. class="p-1 px-3 text-xs flex rounded transition"
  492. on:click={() => {
  493. if (backgroundImageUrl !== null) {
  494. backgroundImageUrl = null;
  495. saveSettings({ backgroundImageUrl });
  496. } else {
  497. filesInputElement.click();
  498. }
  499. }}
  500. type="button"
  501. >
  502. {#if backgroundImageUrl !== null}
  503. <span class="ml-2 self-center">{$i18n.t('Reset')}</span>
  504. {:else}
  505. <span class="ml-2 self-center">{$i18n.t('Upload')}</span>
  506. {/if}
  507. </button>
  508. </div>
  509. </div>
  510. <div>
  511. <div class=" py-0.5 flex w-full justify-between">
  512. <div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
  513. <button
  514. class="p-1 px-3 text-xs flex rounded transition"
  515. on:click={() => {
  516. toggleUserLocation();
  517. }}
  518. type="button"
  519. >
  520. {#if userLocation === true}
  521. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  522. {:else}
  523. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  524. {/if}
  525. </button>
  526. </div>
  527. </div>
  528. <div>
  529. <div class=" py-0.5 flex w-full justify-between">
  530. <div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div>
  531. <button
  532. class="p-1 px-3 text-xs flex rounded transition"
  533. on:click={() => {
  534. toggleHapticFeedback();
  535. }}
  536. type="button"
  537. >
  538. {#if hapticFeedback === true}
  539. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  540. {:else}
  541. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  542. {/if}
  543. </button>
  544. </div>
  545. </div>
  546. <!-- <div>
  547. <div class=" py-0.5 flex w-full justify-between">
  548. <div class=" self-center text-xs">
  549. {$i18n.t('Fluidly stream large external response chunks')}
  550. </div>
  551. <button
  552. class="p-1 px-3 text-xs flex rounded transition"
  553. on:click={() => {
  554. toggleSplitLargeChunks();
  555. }}
  556. type="button"
  557. >
  558. {#if splitLargeChunks === true}
  559. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  560. {:else}
  561. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  562. {/if}
  563. </button>
  564. </div>
  565. </div> -->
  566. <div>
  567. <div class=" py-0.5 flex w-full justify-between">
  568. <div class=" self-center text-xs">
  569. {$i18n.t('Scroll to bottom when switching between branches')}
  570. </div>
  571. <button
  572. class="p-1 px-3 text-xs flex rounded transition"
  573. on:click={() => {
  574. togglesScrollOnBranchChange();
  575. }}
  576. type="button"
  577. >
  578. {#if scrollOnBranchChange === true}
  579. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  580. {:else}
  581. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  582. {/if}
  583. </button>
  584. </div>
  585. </div>
  586. <div>
  587. <div class=" py-0.5 flex w-full justify-between">
  588. <div class=" self-center text-xs">{$i18n.t('Web Search in Chat')}</div>
  589. <button
  590. class="p-1 px-3 text-xs flex rounded transition"
  591. on:click={() => {
  592. toggleWebSearch();
  593. }}
  594. type="button"
  595. >
  596. {#if webSearch === 'always'}
  597. <span class="ml-2 self-center">{$i18n.t('Always')}</span>
  598. {:else}
  599. <span class="ml-2 self-center">{$i18n.t('Default')}</span>
  600. {/if}
  601. </button>
  602. </div>
  603. </div>
  604. <div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
  605. <div>
  606. <div class=" py-0.5 flex w-full justify-between">
  607. <div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
  608. <button
  609. class="p-1 px-3 text-xs flex rounded transition"
  610. on:click={() => {
  611. toggleVoiceInterruption();
  612. }}
  613. type="button"
  614. >
  615. {#if voiceInterruption === true}
  616. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  617. {:else}
  618. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  619. {/if}
  620. </button>
  621. </div>
  622. </div>
  623. <div>
  624. <div class=" py-0.5 flex w-full justify-between">
  625. <div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
  626. <button
  627. class="p-1 px-3 text-xs flex rounded transition"
  628. on:click={() => {
  629. toggleEmojiInCall();
  630. }}
  631. type="button"
  632. >
  633. {#if showEmojiInCall === true}
  634. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  635. {:else}
  636. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  637. {/if}
  638. </button>
  639. </div>
  640. </div>
  641. <div class=" my-1.5 text-sm font-medium">{$i18n.t('File')}</div>
  642. <div>
  643. <div class=" py-0.5 flex w-full justify-between">
  644. <div class=" self-center text-xs">{$i18n.t('Image Compression')}</div>
  645. <button
  646. class="p-1 px-3 text-xs flex rounded transition"
  647. on:click={() => {
  648. toggleImageCompression();
  649. }}
  650. type="button"
  651. >
  652. {#if imageCompression === true}
  653. <span class="ml-2 self-center">{$i18n.t('On')}</span>
  654. {:else}
  655. <span class="ml-2 self-center">{$i18n.t('Off')}</span>
  656. {/if}
  657. </button>
  658. </div>
  659. </div>
  660. {#if imageCompression}
  661. <div>
  662. <div class=" py-0.5 flex w-full justify-between text-xs">
  663. <div class=" self-center text-xs">{$i18n.t('Image Max Compression Size')}</div>
  664. <div>
  665. <input
  666. bind:value={imageCompressionSize.width}
  667. type="number"
  668. class="w-20 bg-transparent outline-none text-center"
  669. min="0"
  670. placeholder="Width"
  671. />x
  672. <input
  673. bind:value={imageCompressionSize.height}
  674. type="number"
  675. class="w-20 bg-transparent outline-none text-center"
  676. min="0"
  677. placeholder="Height"
  678. />
  679. </div>
  680. </div>
  681. </div>
  682. {/if}
  683. </div>
  684. </div>
  685. <div class="flex justify-end text-sm font-medium">
  686. <button
  687. class="px-3.5 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"
  688. type="submit"
  689. >
  690. {$i18n.t('Save')}
  691. </button>
  692. </div>
  693. </form>