ui : list a favorite once, in its own section

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-28 12:10:11 +02:00
parent 32071de07a
commit a5ad735e93
4 changed files with 16 additions and 22 deletions
@@ -323,11 +323,7 @@
<p class="px-4 py-3 text-sm text-muted-foreground">{ms.emptyMessage}</p>
{/if}
{#snippet modelOption(
item: ModelItem,
hideOrgName: boolean,
showFavIndicator: boolean
)}
{#snippet modelOption(item: ModelItem, hideOrgName: boolean)}
{@const { option } = item}
{@const isSelected = currentModel === option.model || ms.activeId === option.id}
{@const isHighlighted = option.id === highlightedId}
@@ -349,7 +345,6 @@
onSelect={ms.handleSelect}
{option}
showBaseModelAvatar
{showFavIndicator}
/>
{/snippet}
@@ -17,7 +17,7 @@
sectionHeaderClass?: string;
onSelect: (modelId: string) => void;
onInfoClick: (modelName: string) => void;
renderOption?: import('svelte').Snippet<[ModelItem, boolean, boolean]>;
renderOption?: import('svelte').Snippet<[ModelItem, boolean]>;
/** Favorite models of every backend, listed in their own section. */
favorites?: ModelItem[];
/** Show the organization name in every model id of the list. */
@@ -63,7 +63,7 @@
}
</script>
{#snippet defaultOption(item: ModelItem, hideOrgName: boolean, showFavIndicator: boolean)}
{#snippet defaultOption(item: ModelItem, hideOrgName: boolean)}
{@const { option } = item}
{@const isSelected = currentModel === option.model || activeId === option.id}
{@const isFav = modelsStore.favoriteModelIds.has(option.model)}
@@ -79,11 +79,11 @@
{onSelect}
{option}
showBaseModelAvatar
{showFavIndicator}
/>
{/snippet}
{#if favorites.length > 0}
<!-- Favorites come first; the sections below skip them -->
<CollapsibleSection
revealChevronOnHover
triggerClass="{headerClass} flex w-full cursor-pointer items-center gap-1.5 text-left"
@@ -96,7 +96,7 @@
{/snippet}
{#each favorites as item (`fav-${item.option.id}`)}
{@render render(item, !showOrgName, false)}
{@render render(item, !showOrgName)}
{/each}
</CollapsibleSection>
{/if}
@@ -123,12 +123,12 @@
{/snippet}
{#each groups.loaded as item (`loaded-${item.option.id}`)}
{@render render(item, !showOrgName, true)}
{@render render(item, !showOrgName)}
{/each}
{#each groups.available as group (group.orgName)}
{#each group.items as item (item.option.id)}
{@render render(item, !showOrgName, true)}
{@render render(item, !showOrgName)}
{/each}
{/each}
</CollapsibleSection>
@@ -168,7 +168,7 @@
{#if provider.items.length > 0}
{#each provider.items as item (`${provider.backendId}-${item.option.id}`)}
{@render render(item, !showOrgName, true)}
{@render render(item, !showOrgName)}
{/each}
{#if onProviderOpen && provider.matched > provider.items.length}
@@ -26,9 +26,6 @@
isSelected: boolean;
isHighlighted: boolean;
isFav: boolean;
/** Show the filled heart at rest. Off inside the favorites section, where
* every row is a favorite. */
showFavIndicator?: boolean;
hideOrgName?: boolean;
onSelect: (modelId: string) => void;
onMouseEnter: () => void;
@@ -48,8 +45,7 @@
onMouseEnter,
onSelect,
option,
showBaseModelAvatar = false,
showFavIndicator = true
showBaseModelAvatar = false
}: Props = $props();
// row actions follow the backend that serves the row, not the selected one
@@ -167,8 +163,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="pointer-events-none flex items-center justify-center gap-1 pl-2 group-hover:pointer-events-auto [@media(pointer:coarse)]:pointer-events-auto [@media(pointer:coarse)]:opacity-100 {isFav &&
showFavIndicator
class="pointer-events-none flex items-center justify-center gap-1 pl-2 group-hover:pointer-events-auto [@media(pointer:coarse)]:pointer-events-auto [@media(pointer:coarse)]:opacity-100 {isFav
? ''
: 'opacity-0 group-hover:opacity-100'}"
onclick={(e) => e.stopPropagation()}
@@ -145,9 +145,13 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
};
})
);
// favorites are listed once, at the top: the sections below skip them
const sectionOptions = $derived(
filteredOptions.filter((option) => !modelsStore.favoriteModelIds.has(option.model))
);
const providerSections = $derived(
groupProviderOptions(
filteredOptions,
sectionOptions,
remoteProviders,
// a drill-in or a search reaches every model, the sections stay short
providerViewId || searchTerm ? Infinity : REMOTE_PROVIDER_MODEL_LIMIT
@@ -160,7 +164,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
return { ...EMPTY_GROUPS, providers: sections };
}
const local = groupModelOptions(filteredOptions.filter(isLocalOption), (m) =>
const local = groupModelOptions(sectionOptions.filter(isLocalOption), (m) =>
modelsStore.isModelLoaded(m)
);