mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-30 01:47:39 -05:00
ui : cut the work the model selector does on open
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -6,7 +6,12 @@
|
||||
import { BackendIcon } from '$lib/components/app/backends';
|
||||
import { DialogConfirmDownload } from '$lib/components/app/dialogs';
|
||||
import Logo from '$lib/components/app/misc/Logo.svelte';
|
||||
import type { GroupedModelOptions, ModelItem } from '$lib/components/app/navigation/utils';
|
||||
import {
|
||||
type GroupedModelOptions,
|
||||
type ModelItem,
|
||||
windowLocalGroups
|
||||
} from '$lib/components/app/navigation/utils';
|
||||
import { MODEL_ROW_WINDOW } from '$lib/constants';
|
||||
import { ModelDownloadConfirmAction } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import { getBackend } from '$lib/utils/api-base';
|
||||
@@ -43,6 +48,38 @@
|
||||
showOrgName = true
|
||||
}: Props = $props();
|
||||
let render = $derived(renderOption ?? defaultOption);
|
||||
// A large local catalog is mounted a window at a time; the sentinel at the end
|
||||
// of the list grows the window when it scrolls into view.
|
||||
let visibleCount = $state(MODEL_ROW_WINDOW);
|
||||
let sentinelEl = $state<HTMLElement | null>(null);
|
||||
const localGroups = $derived(windowLocalGroups(groups, visibleCount));
|
||||
const localRowCount = $derived(
|
||||
groups.loaded.length + groups.available.reduce((count, group) => count + group.items.length, 0)
|
||||
);
|
||||
const hasMoreLocal = $derived(localGroups.shown < localRowCount);
|
||||
|
||||
$effect(() => {
|
||||
const sentinel = sentinelEl;
|
||||
|
||||
if (!sentinel || !hasMoreLocal) return;
|
||||
|
||||
// scroll does not bubble, so listen in the capture phase and ask the
|
||||
// sentinel where it is instead of guessing which ancestor scrolls
|
||||
const onScroll = () => {
|
||||
const rect = sentinel.getBoundingClientRect();
|
||||
|
||||
// grow only when the end of the list is actually on screen, otherwise
|
||||
// every scroll event of the page would mount the whole catalog
|
||||
if (rect.top > window.innerHeight || rect.bottom < 0) return;
|
||||
|
||||
visibleCount += MODEL_ROW_WINDOW;
|
||||
};
|
||||
|
||||
document.addEventListener('scroll', onScroll, { capture: true, passive: true });
|
||||
onScroll();
|
||||
|
||||
return () => document.removeEventListener('scroll', onScroll, { capture: true });
|
||||
});
|
||||
// a local-only install is one list: favorites first, then the rest, no headings
|
||||
let localOnly = $derived(groups.providers.length === 0);
|
||||
// section headers stick right below the search/tabs block of the dropdown
|
||||
@@ -120,15 +157,19 @@
|
||||
|
||||
{#snippet localRows()}
|
||||
<!-- the loaded models first -->
|
||||
{#each groups.loaded as item (`loaded-${item.option.id}`)}
|
||||
{#each localGroups.loaded as item (`loaded-${item.option.id}`)}
|
||||
{@render render(item, !showOrgName)}
|
||||
{/each}
|
||||
|
||||
{#each groups.available as group (group.orgName)}
|
||||
{#each localGroups.available as group (group.orgName)}
|
||||
{#each group.items as item (item.option.id)}
|
||||
{@render render(item, !showOrgName)}
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#if hasMoreLocal}
|
||||
<div bind:this={sentinelEl} aria-hidden="true" class="h-px"></div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if groups.loaded.length > 0 || groups.available.length > 0}
|
||||
|
||||
@@ -85,10 +85,40 @@
|
||||
let fetchedBaseModelOrg = $state<string | null>(null);
|
||||
let baseModelOrg = $derived(orgOf(tagBaseModel) || fetchedBaseModelOrg);
|
||||
|
||||
// Long local lists mount hundreds of rows at once; resolving every base model
|
||||
// up front means one Hugging Face request per row, so wait until a row is
|
||||
// actually near the viewport.
|
||||
let rowEl = $state<HTMLElement | null>(null);
|
||||
let isNearViewport = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (isNearViewport || !rowEl) return;
|
||||
|
||||
if (typeof IntersectionObserver === 'undefined') {
|
||||
isNearViewport = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
isNearViewport = true;
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: '200px' }
|
||||
);
|
||||
|
||||
observer.observe(rowEl);
|
||||
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
fetchedBaseModelOrg = null;
|
||||
|
||||
if (!showBaseModelAvatar || !orgName || tagBaseModel) return;
|
||||
if (!isNearViewport || !showBaseModelAvatar || !orgName || tagBaseModel) return;
|
||||
|
||||
// external provider ids (`~openai/gpt-...`, `deepseek/deepseek-chat`) are
|
||||
// not HF repos; their org is already the provider slug, so the base model
|
||||
@@ -117,6 +147,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={rowEl}
|
||||
aria-selected={isSelected || isHighlighted}
|
||||
class={[
|
||||
'group relative flex w-full items-center gap-2 rounded-sm p-2 text-left text-sm transition focus:outline-none',
|
||||
|
||||
@@ -84,6 +84,36 @@ export function groupFavoriteOptions(
|
||||
return favorites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut the local groups down to a window of rows. Loaded models come first, then
|
||||
* the org groups, so the caller can grow the window as the list is scrolled.
|
||||
*/
|
||||
export function windowLocalGroups(
|
||||
groups: GroupedModelOptions,
|
||||
limit: number
|
||||
): { available: GroupedModelOptions['available']; loaded: ModelItem[]; shown: number } {
|
||||
const loaded = groups.loaded.slice(0, Math.max(0, limit));
|
||||
|
||||
let budget = limit - loaded.length;
|
||||
|
||||
const available: GroupedModelOptions['available'] = [];
|
||||
|
||||
let shown = loaded.length;
|
||||
|
||||
for (const group of groups.available) {
|
||||
if (budget <= 0) break;
|
||||
|
||||
const items = group.items.slice(0, budget);
|
||||
|
||||
budget -= items.length;
|
||||
shown += items.length;
|
||||
|
||||
if (items.length > 0) available.push({ ...group, items });
|
||||
}
|
||||
|
||||
return { available, loaded, shown };
|
||||
}
|
||||
|
||||
export function groupModelOptions(
|
||||
filteredOptions: ModelOption[],
|
||||
isModelLoaded: (model: string) => boolean
|
||||
|
||||
@@ -53,6 +53,12 @@ export const MODEL_SELECTOR_ICON = Package;
|
||||
/** Models listed per remote provider before the "+ X more" line; search covers the rest. */
|
||||
export const REMOTE_PROVIDER_MODEL_LIMIT = 12;
|
||||
|
||||
/**
|
||||
* Model rows mounted before the list is scrolled. A local catalog can hold
|
||||
* hundreds of models and mounting them all costs seconds on open.
|
||||
*/
|
||||
export const MODEL_ROW_WINDOW = 50;
|
||||
|
||||
/** Recently used models kept per browser, most recent first. */
|
||||
export const RECENT_MODEL_LIMIT = 20;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user