| <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; |
| |
| |
| let titleAutoGenerate = true; |
| let responseAutoCopy = false; |
| let widescreenMode = false; |
| let splitLargeChunks = false; |
| let scrollOnBranchChange = true; |
| let userLocation = false; |
| |
| |
| let defaultModelId = ''; |
| let showUsername = false; |
| |
| let landingPageMode = ''; |
| let chatBubble = true; |
| let chatDirection: 'LTR' | 'RTL' = 'LTR'; |
| |
| 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 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 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; |
| |
| responseAutoCopy = $settings.responseAutoCopy ?? false; |
| showUsername = $settings.showUsername ?? false; |
| |
| showEmojiInCall = $settings.showEmojiInCall ?? false; |
| voiceInterruption = $settings.voiceInterruption ?? false; |
| |
| 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> |
|
|
| <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('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('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-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" |
| type="submit" |
| > |
| {$i18n.t('Save')} |
| </button> |
| </div> |
| </form> |
|
|