ui : add sampling and penalties submenus to model selector

Add reusable ModelsSelectorSettingsSubmenu that renders a settings section's
fields as inline editable inputs bound to settingsStore. Wire it into the model
selector dropdown for the Sampling and Penalties sections, showing server
defaults as placeholders and disabling the submenus until a model is loaded.
This commit is contained in:
Aleksander Grygier
2026-08-16 18:16:48 +02:00
parent ec2a303f3c
commit 7d0640a44d
3 changed files with 120 additions and 2 deletions
@@ -8,11 +8,17 @@
DropdownMenuSearchable,
ModelId,
ModelsSelectorList,
ModelsSelectorOption
ModelsSelectorOption,
ModelsSelectorSettingsSubmenu
} from '$lib/components/app';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { MODEL_SELECTOR_ICON, SETTINGS_KEYS } from '$lib/constants';
import {
MODEL_SELECTOR_ICON,
SETTINGS_CHAT_SECTIONS,
SETTINGS_KEYS,
SETTINGS_SECTION_SLUGS
} from '$lib/constants';
import { KeyboardKey, ServerModelStatus } from '$lib/enums';
import { useModelsSelector } from '$lib/hooks/use-models-selector.svelte';
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
@@ -66,6 +72,17 @@
const reasoning = useReasoningMenu();
const samplingSection = $derived(
SETTINGS_CHAT_SECTIONS.find((s) => s.slug === SETTINGS_SECTION_SLUGS.SAMPLING)
);
const penaltiesSection = $derived(
SETTINGS_CHAT_SECTIONS.find((s) => s.slug === SETTINGS_SECTION_SLUGS.PENALTIES)
);
// Sampling/penalties need a loaded model to show server defaults, so
// disable the submenus until one is loaded.
const hasModelLoaded = $derived(modelsStore.loadedModelIds.length > 0);
const showOrgNameInTrigger = $derived(
settingsStore.config[SETTINGS_KEYS.SHOW_MODEL_ORG_NAME_IN_TRIGGER] ?? false
);
@@ -352,6 +369,14 @@
</DropdownMenu.Sub>
<ChatFormActionAddReasoningSubmenu />
{#if samplingSection}
<ModelsSelectorSettingsSubmenu section={samplingSection} disabled={!hasModelLoaded} />
{/if}
{#if penaltiesSection}
<ModelsSelectorSettingsSubmenu section={penaltiesSection} disabled={!hasModelLoaded} />
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
{:else}
@@ -0,0 +1,91 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Input } from '$lib/components/ui/input';
import { SettingsFieldType } from '$lib/enums/settings.enums';
import { modelsStore, serverStore, settingsStore } from '$lib/stores';
import { normalizeFloatingPoint } from '$lib/utils/precision';
import type { SettingsSection } from '$lib/types';
interface Props {
section: SettingsSection;
disabled?: boolean;
}
let { section, disabled = false }: Props = $props();
let currentModelParams = $derived.by(() => {
void modelsStore.propsCacheVersion;
if (serverStore.isRouterMode) {
const currentModelName = modelsStore.selectedModelName;
if (currentModelName) {
const currentModelProps = modelsStore.getModelProps(currentModelName);
return (currentModelProps?.default_generation_settings?.params ?? {}) as Record<
string,
unknown
>;
}
}
return (serverStore.defaultParams ?? {}) as Record<string, unknown>;
});
function currentValue(key: string): string {
const value = settingsStore.config[key];
return value == null ? '' : String(value);
}
function handleInput(key: string, value: string) {
settingsStore.updateConfig(key, value);
}
function placeholder(key: string): string {
const serverDefault = currentModelParams[key];
return serverDefault != null ? `Default: ${normalizeFloatingPoint(serverDefault)}` : '';
}
</script>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex cursor-pointer items-center gap-2"
disabled={disabled}
>
<section.icon class="h-4 w-4 shrink-0 text-muted-foreground" />
<span class="text-sm text-muted-foreground">{section.title}</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="max-h-96 w-72 overflow-y-auto bg-popover p-2 text-popover-foreground shadow-md outline-none"
>
{#each section.fields ?? [] as field (field.key)}
{#if field.type === SettingsFieldType.INPUT}
<label class="mb-2 flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">{field.label}</span>
<Input
type="text"
value={currentValue(field.key)}
placeholder={placeholder(field.key)}
oninput={(event) => handleInput(field.key, event.currentTarget.value)}
class="h-8"
/>
</label>
{:else if field.type === SettingsFieldType.CHECKBOX}
<label class="flex cursor-pointer items-center gap-2 rounded-md px-1 py-1.5 text-sm hover:bg-accent">
<Checkbox
checked={Boolean(settingsStore.config[field.key])}
onCheckedChange={(checked) => settingsStore.updateConfig(field.key, Boolean(checked))}
/>
<span>{field.label}</span>
</label>
{/if}
{/each}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
@@ -65,6 +65,8 @@ export { default as ModelsSelectorList } from './ModelsSelectorList.svelte';
*/
export { default as ModelsSelectorOption } from './ModelsSelectorOption.svelte';
export { default as ModelsSelectorSettingsSubmenu } from './ModelsSelectorSettingsSubmenu.svelte';
/**
* **ModelsSelectorSheet** - Mobile model selection sheet
*