This commit is contained in:
Timothy Jaeryang Baek
2026-07-15 18:03:36 -04:00
parent ba067258de
commit 933ab1e1cd
25 changed files with 239 additions and 210 deletions
@@ -17,6 +17,7 @@
import UserProfileImage from './Account/UserProfileImage.svelte';
import UserSettingField from './UserSettingField.svelte';
import UserSettingRow from './UserSettingRow.svelte';
import UserSettingSelect from './UserSettingSelect.svelte';
import UserSettingSection from './UserSettingSection.svelte';
const i18n = getContext('i18n');
@@ -171,10 +172,10 @@
label={$i18n.t('Gender')}
description={$i18n.t('Choose the gender value stored on your profile.')}
>
<select
class={inputClass}
<UserSettingSelect
className="w-full"
bind:value={_gender}
aria-label={$i18n.t('Gender')}
ariaLabel={$i18n.t('Gender')}
on:change={(e) => {
console.log(_gender);
@@ -190,7 +191,7 @@
<option value="male">{$i18n.t('Male')}</option>
<option value="female">{$i18n.t('Female')}</option>
<option value="custom">{$i18n.t('Custom')}</option>
</select>
</UserSettingSelect>
{#if _gender === 'custom'}
<input
+12 -16
View File
@@ -10,6 +10,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import UserSettingField from './UserSettingField.svelte';
import UserSettingRow from './UserSettingRow.svelte';
import UserSettingSelect from './UserSettingSelect.svelte';
import UserSettingSection from './UserSettingSection.svelte';
const dispatch = createEventDispatcher();
@@ -40,8 +41,6 @@
let playbackRate = 1;
const inputClass =
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const selectClass =
'h-7 w-fit min-w-28 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-right text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
const actionButtonClass =
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
@@ -193,15 +192,14 @@
label={$i18n.t('Speech-to-Text Engine')}
description={$i18n.t('Choose the engine used to transcribe voice input.')}
>
<select
class={selectClass}
<UserSettingSelect
bind:value={STTEngine}
aria-label={$i18n.t('Speech-to-Text Engine')}
ariaLabel={$i18n.t('Speech-to-Text Engine')}
placeholder={$i18n.t('Select an engine')}
>
<option value="">{$i18n.t('Default')}</option>
<option value="web">{$i18n.t('Web API')}</option>
</select>
</UserSettingSelect>
</UserSettingRow>
<UserSettingRow
@@ -252,15 +250,14 @@
label={$i18n.t('Text-to-Speech Engine')}
description={$i18n.t('Choose the engine used to read assistant responses aloud.')}
>
<select
class={selectClass}
<UserSettingSelect
bind:value={TTSEngine}
aria-label={$i18n.t('Text-to-Speech Engine')}
ariaLabel={$i18n.t('Text-to-Speech Engine')}
placeholder={$i18n.t('Select an engine')}
>
<option value="">{$i18n.t('Default')}</option>
<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
</select>
</UserSettingSelect>
</UserSettingRow>
{#if TTSEngine === 'browser-kokoro'}
@@ -268,10 +265,9 @@
label={$i18n.t('Kokoro.js Dtype')}
description={$i18n.t('Select the local model precision used by Kokoro.js.')}
>
<select
class={selectClass}
<UserSettingSelect
bind:value={TTSEngineConfig.dtype}
aria-label={$i18n.t('Kokoro.js Dtype')}
ariaLabel={$i18n.t('Kokoro.js Dtype')}
placeholder={$i18n.t('Select dtype')}
>
<option value="" disabled selected>{$i18n.t('Select dtype')}</option>
@@ -279,7 +275,7 @@
<option value="fp16">fp16</option>
<option value="q8">q8</option>
<option value="q4">q4</option>
</select>
</UserSettingSelect>
</UserSettingRow>
{/if}
@@ -364,7 +360,7 @@
label={$i18n.t('Set Voice')}
description={$i18n.t('Choose the browser voice used for speech output.')}
>
<select class={inputClass} bind:value={voice} aria-label={$i18n.t('Voice')}>
<UserSettingSelect className="w-full" bind:value={voice} ariaLabel={$i18n.t('Voice')}>
<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
<option
@@ -373,7 +369,7 @@
selected={voice === _voice.name}>{_voice.name}</option
>
{/each}
</select>
</UserSettingSelect>
</UserSettingField>
<UserSettingRow
label={$i18n.t('Allow non-local voices')}
@@ -13,6 +13,7 @@
import UserSettingField from './UserSettingField.svelte';
import UserSettingRow from './UserSettingRow.svelte';
import UserSettingSection from './UserSettingSection.svelte';
import UserSettingSelect from './UserSettingSelect.svelte';
export let saveSettings: Function;
export let getModels: Function;
@@ -29,8 +30,6 @@
const systemPromptTextareaClass =
'w-full resize-y rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const selectClass =
'h-7 w-fit min-w-28 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-right text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
const actionButtonClass =
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
@@ -224,9 +223,9 @@
label={$i18n.t('Theme')}
description={$i18n.t('Choose the color theme used by the interface.')}
>
<select
class={selectClass}
<UserSettingSelect
bind:value={selectedTheme}
ariaLabel={$i18n.t('Theme')}
placeholder={$i18n.t('Select a theme')}
on:change={() => themeChangeHandler(selectedTheme)}
>
@@ -237,16 +236,17 @@
{#if $config?.features?.enable_easter_eggs}
<option value="her">🌷 Her</option>
{/if}
</select>
</UserSettingSelect>
</UserSettingRow>
<UserSettingRow
label={$i18n.t('Language')}
description={$i18n.t('Choose the language used for interface text.')}
>
<select
class={selectClass}
<UserSettingSelect
bind:value={lang}
ariaLabel={$i18n.t('Language')}
className="w-48"
placeholder={$i18n.t('Select a language')}
on:change={(e) => {
changeLanguage(lang);
@@ -255,7 +255,7 @@
{#each languages as language}
<option value={language['code']}>{language['title']}</option>
{/each}
</select>
</UserSettingSelect>
</UserSettingRow>
{#if $i18n.language === 'en-US' && !($config?.license_metadata ?? false)}
<div class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
@@ -16,7 +16,9 @@
import UserSettingSection from './UserSettingSection.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Search from '$lib/components/icons/Search.svelte';
import Trash from '$lib/components/icons/Trash.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
const dispatch = createEventDispatcher();
@@ -34,8 +36,6 @@
let showClearConfirmDialog = false;
let showDeleteConfirm = false;
let query = '';
const inputClass =
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const actionButtonClass =
'shrink-0 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
@@ -145,54 +145,13 @@
{#if enableMemory}
<div>
<div class="mb-2 flex items-center justify-between">
<div class="mb-1 flex items-center">
<div class="text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Saved Memories')}
{#if !loadingMemories}
<span class="ml-1 text-gray-400 dark:text-gray-600">{memories.length}</span>
{/if}
</div>
<Dropdown align="end">
<Tooltip content={$i18n.t('Actions')}>
<button
class="flex h-7 items-center gap-1.5 rounded-lg bg-transparent px-1.5 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
type="button"
>
<span>{$i18n.t('Actions')}</span>
<ChevronDown className="size-3" strokeWidth="2.5" />
</button>
</Tooltip>
<div slot="content">
<DropdownMenu className="w-[170px] shadow-sm">
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
disabled={loadingMemories}
type="button"
on:click={() => {
selectedMemory = null;
showMemoryModal = true;
}}
>
<Plus className="size-3.5 shrink-0" strokeWidth="1.5" />
<div class="min-w-0 flex-1 truncate text-left">{$i18n.t('Add Memory')}</div>
</button>
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
disabled={loadingMemories || memories.length === 0}
type="button"
on:click={() => {
showClearConfirmDialog = true;
}}
>
<Trash className="size-3.5 shrink-0" strokeWidth="1.5" />
<div class="min-w-0 flex-1 truncate text-left">{$i18n.t('Clear memory')}</div>
</button>
</DropdownMenu>
</div>
</Dropdown>
</div>
{#if loadingMemories}
@@ -200,28 +159,76 @@
<Spinner className="size-4" />
</div>
{:else}
{#if memories.length > 0}
<div class="mb-2 flex items-center gap-2">
<input
class={inputClass}
bind:value={query}
placeholder={$i18n.t('Search Memories')}
maxlength="500"
/>
<div class="mb-2 flex min-w-0 items-center justify-between gap-3">
{#if memories.length > 0}
<div class="flex min-w-0 flex-1 items-center gap-2">
<Search className="size-3.5 shrink-0 text-gray-400 dark:text-gray-600" />
<input
data-settings-search
class="min-w-0 flex-1 bg-transparent py-0.5 text-xs text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
bind:value={query}
placeholder={$i18n.t('Search Memories')}
maxlength="500"
/>
{#if query}
<div class="shrink-0">
<button
class="rounded-lg p-0.5 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
type="button"
aria-label={$i18n.t('Clear search')}
on:click={() => {
query = '';
}}
>
<XMark className="size-3" strokeWidth="2" />
</button>
</div>
{/if}
</div>
{:else}
<div class="min-w-0 flex-1"></div>
{/if}
{#if query}
<Dropdown align="end">
<Tooltip content={$i18n.t('Actions')}>
<button
class="flex h-7 items-center gap-1.5 rounded-lg bg-transparent px-1.5 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
type="button"
class={actionButtonClass}
on:click={() => {
query = '';
}}
>
{$i18n.t('Clear')}
<span>{$i18n.t('Actions')}</span>
<ChevronDown className="size-3" strokeWidth="2.5" />
</button>
{/if}
</div>
{/if}
</Tooltip>
<div slot="content">
<DropdownMenu className="w-[170px] shadow-sm">
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
type="button"
on:click={() => {
selectedMemory = null;
showMemoryModal = true;
}}
>
<Plus className="size-3.5 shrink-0" strokeWidth="1.5" />
<div class="min-w-0 flex-1 truncate text-left">{$i18n.t('Add Memory')}</div>
</button>
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
disabled={memories.length === 0}
type="button"
on:click={() => {
showClearConfirmDialog = true;
}}
>
<Trash className="size-3.5 shrink-0" strokeWidth="1.5" />
<div class="min-w-0 flex-1 truncate text-left">{$i18n.t('Clear memory')}</div>
</button>
</DropdownMenu>
</div>
</Dropdown>
</div>
{#if sortedMemories.length === 0}
<div class="min-h-16 text-[0.6875rem] text-gray-400 dark:text-gray-600">
@@ -0,0 +1,19 @@
<script lang="ts">
export let value = '';
export let ariaLabel = '';
export let placeholder = '';
export let className = 'w-fit min-w-32';
export let selectClassName = '';
</script>
<div class="relative inline-flex {className}">
<select
bind:value
aria-label={ariaLabel || undefined}
title={placeholder || undefined}
class="h-7 w-full truncate rounded-lg border border-gray-100/50 !bg-gray-50/40 ps-2.5 pe-9 text-left text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:!bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500 {selectClassName}"
on:change
>
<slot />
</select>
</div>