| | <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> |
| |
|