<script lang="ts"> import { getBackendConfig } from '$lib/apis'; import { setDefaultPromptSuggestions } from '$lib/apis/configs'; import { config, models, settings, user } from '$lib/stores'; import { createEventDispatcher, onMount, getContext } from 'svelte'; import { toast } from 'svelte-sonner'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import { updateUserInfo } from '$lib/apis/users'; import { getUserPosition } from '$lib/utils'; const dispatch = createEventDispatcher(); const i18n = getContext('i18n'); export let saveSettings: Function; let backgroundImageUrl = null; let inputFiles = null; let filesInputElement; // Addons let titleAutoGenerate = true; let autoTags = true; let responseAutoCopy = false; let widescreenMode = false; let splitLargeChunks = false; let scrollOnBranchChange = true; let userLocation = false; // Interface let defaultModelId = ''; let showUsername = false; let richTextInput = true; let landingPageMode = ''; let chatBubble = true; let chatDirection: 'LTR' | 'RTL' = 'LTR'; let showUpdateToast = true; let showEmojiInCall = false; let voiceInterruption = false; let hapticFeedback = false; const toggleSplitLargeChunks = async () => { splitLargeChunks = !splitLargeChunks; saveSettings({ splitLargeChunks: splitLargeChunks }); }; const togglesScrollOnBranchChange = async () => { scrollOnBranchChange = !scrollOnBranchChange; saveSettings({ scrollOnBranchChange: scrollOnBranchChange }); }; const toggleWidescreenMode = async () => { widescreenMode = !widescreenMode; saveSettings({ widescreenMode: widescreenMode }); }; const toggleChatBubble = async () => { chatBubble = !chatBubble; saveSettings({ chatBubble: chatBubble }); }; const toggleLandingPageMode = async () => { landingPageMode = landingPageMode === '' ? 'chat' : ''; saveSettings({ landingPageMode: landingPageMode }); }; const toggleShowUpdateToast = async () => { showUpdateToast = !showUpdateToast; saveSettings({ showUpdateToast: showUpdateToast }); }; const toggleShowUsername = async () => { showUsername = !showUsername; saveSettings({ showUsername: showUsername }); }; const toggleEmojiInCall = async () => { showEmojiInCall = !showEmojiInCall; saveSettings({ showEmojiInCall: showEmojiInCall }); }; const toggleVoiceInterruption = async () => { voiceInterruption = !voiceInterruption; saveSettings({ voiceInterruption: voiceInterruption }); }; const toggleHapticFeedback = async () => { hapticFeedback = !hapticFeedback; saveSettings({ hapticFeedback: hapticFeedback }); }; const toggleUserLocation = async () => { userLocation = !userLocation; if (userLocation) { const position = await getUserPosition().catch((error) => { toast.error(error.message); return null; }); if (position) { await updateUserInfo(localStorage.token, { location: position }); toast.success($i18n.t('User location successfully retrieved.')); } else { userLocation = false; } } saveSettings({ userLocation }); }; const toggleTitleAutoGenerate = async () => { titleAutoGenerate = !titleAutoGenerate; saveSettings({ title: { ...$settings.title, auto: titleAutoGenerate } }); }; const toggleAutoTags = async () => { autoTags = !autoTags; saveSettings({ autoTags }); }; const toggleRichTextInput = async () => { richTextInput = !richTextInput; saveSettings({ richTextInput }); }; const toggleResponseAutoCopy = async () => { const permission = await navigator.clipboard .readText() .then(() => { return 'granted'; }) .catch(() => { return ''; }); console.log(permission); if (permission === 'granted') { responseAutoCopy = !responseAutoCopy; saveSettings({ responseAutoCopy: responseAutoCopy }); } else { toast.error( $i18n.t( 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.' ) ); } }; const toggleChangeChatDirection = async () => { chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR'; saveSettings({ chatDirection }); }; const updateInterfaceHandler = async () => { saveSettings({ models: [defaultModelId] }); }; onMount(async () => { titleAutoGenerate = $settings?.title?.auto ?? true; autoTags = $settings.autoTags ?? true; responseAutoCopy = $settings.responseAutoCopy ?? false; showUsername = $settings.showUsername ?? false; showUpdateToast = $settings.showUpdateToast ?? true; showEmojiInCall = $settings.showEmojiInCall ?? false; voiceInterruption = $settings.voiceInterruption ?? false; richTextInput = $settings.richTextInput ?? true; landingPageMode = $settings.landingPageMode ?? ''; chatBubble = $settings.chatBubble ?? true; widescreenMode = $settings.widescreenMode ?? false; splitLargeChunks = $settings.splitLargeChunks ?? false; scrollOnBranchChange = $settings.scrollOnBranchChange ?? true; chatDirection = $settings.chatDirection ?? 'LTR'; userLocation = $settings.userLocation ?? false; hapticFeedback = $settings.hapticFeedback ?? false; defaultModelId = $settings?.models?.at(0) ?? ''; if ($config?.default_models) { defaultModelId = $config.default_models.split(',')[0]; } backgroundImageUrl = $settings.backgroundImageUrl ?? null; }); </script> <form class="flex flex-col h-full justify-between space-y-3 text-sm" on:submit|preventDefault={() => { updateInterfaceHandler(); dispatch('save'); }} > <input bind:this={filesInputElement} bind:files={inputFiles} type="file" hidden accept="image/*" on:change={() => { let reader = new FileReader(); reader.onload = (event) => { let originalImageUrl = `${event.target.result}`; backgroundImageUrl = originalImageUrl; saveSettings({ backgroundImageUrl }); }; if ( inputFiles && inputFiles.length > 0 && ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type']) ) { reader.readAsDataURL(inputFiles[0]); } else { console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`); inputFiles = null; } }} /> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden"> <div class=" space-y-1 mb-3"> <div class="mb-2"> <div class="flex justify-between items-center text-xs"> <div class=" text-sm font-medium">{$i18n.t('Default Model')}</div> </div> </div> <div class="flex-1 mr-2"> <select class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" bind:value={defaultModelId} placeholder="Select a model" > <option value="" disabled selected>{$i18n.t('Select a model')}</option> {#each $models.filter((model) => model.id) as model} <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option> {/each} </select> </div> </div> <hr class=" dark:border-gray-850" /> <div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleLandingPageMode(); }} type="button" > {#if landingPageMode === ''} <span class="ml-2 self-center">{$i18n.t('Default')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Chat')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleChatBubble(); }} type="button" > {#if chatBubble === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> {#if !$settings.chatBubble} <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs"> {$i18n.t('Display the username instead of You in the Chat')} </div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleShowUsername(); }} type="button" > {#if showUsername === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> {/if} <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleWidescreenMode(); }} type="button" > {#if widescreenMode === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Chat direction')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={toggleChangeChatDirection} type="button" > {#if chatDirection === 'LTR'} <span class="ml-2 self-center">{$i18n.t('LTR')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('RTL')}</span> {/if} </button> </div> </div> {#if $user.role === 'admin'} <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs"> {$i18n.t('Toast notifications for new updates')} </div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleShowUpdateToast(); }} type="button" > {#if showUpdateToast === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> {/if} <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs"> {$i18n.t('Fluidly stream large external response chunks')} </div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleSplitLargeChunks(); }} type="button" > {#if splitLargeChunks === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs"> {$i18n.t('Scroll to bottom when switching between branches')} </div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { togglesScrollOnBranchChange(); }} type="button" > {#if scrollOnBranchChange === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs"> {$i18n.t('Rich Text Input for Chat')} </div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleRichTextInput(); }} type="button" > {#if richTextInput === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs"> {$i18n.t('Chat Background Image')} </div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { if (backgroundImageUrl !== null) { backgroundImageUrl = null; saveSettings({ backgroundImageUrl }); } else { filesInputElement.click(); } }} type="button" > {#if backgroundImageUrl !== null} <span class="ml-2 self-center">{$i18n.t('Reset')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Upload')}</span> {/if} </button> </div> </div> <div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleTitleAutoGenerate(); }} type="button" > {#if titleAutoGenerate === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleAutoTags(); }} type="button" > {#if autoTags === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs"> {$i18n.t('Response AutoCopy to Clipboard')} </div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleResponseAutoCopy(); }} type="button" > {#if responseAutoCopy === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleUserLocation(); }} type="button" > {#if userLocation === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleHapticFeedback(); }} type="button" > {#if hapticFeedback === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleVoiceInterruption(); }} type="button" > {#if voiceInterruption === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> <div> <div class=" py-0.5 flex w-full justify-between"> <div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div> <button class="p-1 px-3 text-xs flex rounded transition" on:click={() => { toggleEmojiInCall(); }} type="button" > {#if showEmojiInCall === true} <span class="ml-2 self-center">{$i18n.t('On')}</span> {:else} <span class="ml-2 self-center">{$i18n.t('Off')}</span> {/if} </button> </div> </div> </div> </div> <div class="flex justify-end text-sm font-medium"> <button 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" type="submit" > {$i18n.t('Save')} </button> </div> </form>