ui : refine the model pane and the manager filters

The pane no longer says the server withholds data: it says the metadata
and the chat template appear once the model is loaded, and the template
block grows with its content instead of scrolling inside a fixed box. The
pane ends with a Delete this model from disk action, confirmed by the same
dialog the table row uses and hovering like a destructive dropdown entry.
The filter toggles list only the modalities the models at hand actually
carry, and starting a new chat from the pane closes the dialog.

Assisted-by: pi
This commit is contained in:
Aleksander Grygier
2026-10-02 11:59:24 +02:00
parent 7bc505dab7
commit ee5685f8cd
4 changed files with 62 additions and 13 deletions
@@ -225,7 +225,8 @@
async function useInNewChat(option: ModelOption): Promise<void> {
await modelsStore.selectModelById(option.id);
await conversationsStore.openNewChat();
// the chat is behind the dialog, so it takes focus once the dialog is out of the way
// the chat is behind the dialog: close it, then let the composer take focus
uiStore.manageModelsOpen = false;
uiStore.requestComposerFocus();
}
</script>
@@ -9,11 +9,14 @@
FILTER_TRIGGER_CLASS,
MODALITY_FLAG_KEYS,
MODALITY_ICONS,
MODALITY_KEYS,
MODALITY_LABELS,
MODALITY_ORDER,
type ModalityKey
} from '$lib/constants';
import { ModelCapability } from '$lib/enums';
import { modelsStore } from '$lib/stores';
import { SvelteSet } from 'svelte/reactivity';
interface Props {
/** Capabilities a model must have every one of. */
@@ -45,16 +48,34 @@
value
}));
const MODALITY_TOGGLES = MODALITY_ORDER.map((modality) => ({
icon: MODALITY_ICONS[modality],
label: MODALITY_LABELS[modality],
value: MODALITY_FLAG_KEYS[modality]
}));
// only the modalities the models at hand actually carry: a toggle for something
// none of them supports could only ever empty the table
let detectedModalities = $derived.by(() => {
const keys = new SvelteSet<ModalityKey>();
for (const option of modelsStore.models) {
for (const key of MODALITY_KEYS) {
if (option.modalities?.[key]) keys.add(key);
}
}
return keys;
});
let modalityToggles = $derived(
MODALITY_ORDER.filter((modality) => detectedModalities.has(MODALITY_FLAG_KEYS[modality])).map(
(modality) => ({
icon: MODALITY_ICONS[modality],
label: MODALITY_LABELS[modality],
value: MODALITY_FLAG_KEYS[modality]
})
)
);
// one group holds what a model can do and what it can accept
const TOGGLES = [...CAPABILITY_TOGGLES, ...MODALITY_TOGGLES];
let toggles = $derived([...CAPABILITY_TOGGLES, ...modalityToggles]);
const CAPABILITY_VALUES = new Set<string>(CAPABILITY_TOGGLES.map((entry) => entry.value));
const MODALITY_VALUES = new Set<string>(MODALITY_TOGGLES.map((entry) => entry.value));
// every flag value, so a choice made before a model disappeared still round-trips
const MODALITY_VALUES = new Set<string>(MODALITY_KEYS);
// the group holds one flat list, so a change splits back into the two filters
function setToggles(values: string[]): void {
@@ -93,7 +114,7 @@
value={[...capabilities, ...modalities]}
variant="outline"
>
{#each TOGGLES as toggle (toggle.value)}
{#each toggles as toggle (toggle.value)}
<ToggleGroup.Item
aria-label={toggle.label}
class={FILTER_TOGGLE_ITEM_CLASS}
@@ -1,8 +1,11 @@
<script lang="ts">
import ModelsManagerModelConfigurationHeader from './ModelsManagerModelConfigurationHeader.svelte';
import ModelsManagerModelConfigurationInformation from './ModelsManagerModelConfigurationInformation.svelte';
import { Trash2 } from '@lucide/svelte';
import { DialogConfirmDownload } from '$lib/components/app/dialogs';
import { Button } from '$lib/components/ui/button';
import { MODEL_ID, SETTINGS_KEYS } from '$lib/constants';
import { ServerModelStatus } from '$lib/enums';
import { ModelDownloadConfirmAction, ServerModelStatus } from '$lib/enums';
import { HuggingFaceService } from '$lib/services';
import { modelsStore, settingsStore } from '$lib/stores';
import type { HfModelDetailInfo } from '$lib/types/huggingface';
@@ -17,6 +20,9 @@
let { onClose, onToggleLoad, onUseInNewChat, option }: Props = $props();
// the same removal the table row offers, confirmed by the same dialog
let deleteOpen = $state(false);
// the props cache is a plain Map, so the read has to name its version to stay reactive
let serverProps = $derived.by(() => {
void modelsStore.props.cacheVersion;
@@ -69,5 +75,24 @@
<div class="min-h-0 flex-1 overflow-y-auto py-4 pl-4">
<ModelsManagerModelConfigurationInformation hub={hubDetails} {option} {serverProps} />
<div class="mt-4 border-t border-border/30 pt-3 pb-2">
<Button
class="w-full justify-start gap-2 text-destructive hover:bg-destructive/10 hover:text-destructive dark:hover:bg-destructive/20"
onclick={() => (deleteOpen = true)}
size="sm"
variant="ghost"
>
<Trash2 class="h-4 w-4" />
Delete this model from disk
</Button>
</div>
</div>
<DialogConfirmDownload
action={ModelDownloadConfirmAction.DELETE}
onClose={() => (deleteOpen = false)}
open={deleteOpen}
repoWithTag={option.model}
/>
</div>
@@ -162,7 +162,9 @@
{/each}
{#if visibleRows.length === 0}
<p class="py-2.5 text-sm text-muted-foreground">This model reports no metadata.</p>
<p class="py-2.5 text-sm text-muted-foreground">
The model reports its metadata once it is loaded.
</p>
{/if}
{#if modalities.length > 0}
@@ -188,8 +190,8 @@
{/snippet}
<pre
class="mt-1 max-h-48 overflow-auto rounded-md bg-muted/50 p-2 text-xs whitespace-pre-wrap">{serverProps?.chat_template ??
class="mt-1 rounded-md bg-muted/50 p-2 text-xs whitespace-pre-wrap">{serverProps?.chat_template ??
gguf?.chat_template ??
'Not reported by the server.'}</pre>
'Shown once the model is loaded.'}</pre>
</CollapsibleSection>
</div>