iconSize?: string;
class?: string;
disabled?: boolean;
- onclick: () => void;
+ onclick: (e?: MouseEvent) => void;
'aria-label'?: string;
}
import { serverStore } from '$lib/stores/server.svelte';
import { modelsStore, modelOptions, modelsLoading } from '$lib/stores/models.svelte';
import { formatFileSize, formatParameters, formatNumber } from '$lib/utils';
+ import type { ApiLlamaCppServerProps } from '$lib/types';
interface Props {
open?: boolean;
onOpenChange?: (open: boolean) => void;
+ // when set, fetch props from the child process (router mode)
+ modelId?: string | null;
}
- let { open = $bindable(), onOpenChange }: Props = $props();
+ let { open = $bindable(), onOpenChange, modelId = null }: Props = $props();
- let serverProps = $derived(serverStore.props);
- let modelName = $derived(modelsStore.singleModelName);
+ let isRouter = $derived(serverStore.isRouterMode);
+
+ // per-model props fetched from the child process
+ let routerModelProps = $state<ApiLlamaCppServerProps | null>(null);
+ let isLoadingRouterProps = $state(false);
+
+ // in router mode use per-model props, otherwise use global props
+ let serverProps = $derived(isRouter && modelId ? routerModelProps : serverStore.props);
+
+ let modelName = $derived(isRouter && modelId ? modelId : modelsStore.singleModelName);
let models = $derived(modelOptions());
let isLoadingModels = $derived(modelsLoading());
- // Get the first model for single-model mode display
- let firstModel = $derived(models[0] ?? null);
+ // in router mode, find the model option matching modelId
+ // in single mode, use the first model as before
+ let firstModel = $derived.by(() => {
+ if (isRouter && modelId) {
+ return models.find((m) => m.model === modelId) ?? null;
+ }
+ return models[0] ?? null;
+ });
// Get modalities from modelStore using the model ID from the first model
let modalities = $derived.by(() => {
modelsStore.fetch();
}
});
+
+ // fetch per-model props from child process when dialog opens in router mode
+ $effect(() => {
+ if (open && isRouter && modelId) {
+ isLoadingRouterProps = true;
+ modelsStore
+ .fetchModelProps(modelId)
+ .then((props) => {
+ routerModelProps = props;
+ })
+ .catch(() => {
+ routerModelProps = null;
+ })
+ .finally(() => {
+ isLoadingRouterProps = false;
+ });
+ }
+ if (!open) {
+ routerModelProps = null;
+ }
+ });
</script>
<Dialog.Root bind:open {onOpenChange}>
- <Dialog.Content class="@container z-9999 !max-w-[60rem] max-w-full">
+ <Dialog.Content class="@container z-9999 !max-h-[80dvh] !max-w-[60rem] max-w-full">
<style>
@container (max-width: 56rem) {
.resizable-text-container {
</Dialog.Header>
<div class="space-y-6 py-4">
- {#if isLoadingModels}
+ {#if isLoadingModels || isLoadingRouterProps}
<div class="flex items-center justify-center py-8">
<div class="text-sm text-muted-foreground">Loading model information...</div>
</div>
<Table.Cell class="align-middle font-medium">Chat Template</Table.Cell>
<Table.Cell class="py-10">
- <div class="max-h-120 overflow-y-auto rounded-md bg-muted p-4">
+ <div class="rounded-md bg-muted p-4">
<pre
class="font-mono text-xs whitespace-pre-wrap">{serverProps.chat_template}</pre>
</div>
<script lang="ts">
import { onMount } from 'svelte';
- import { SvelteMap } from 'svelte/reactivity';
import { ChevronDown, Loader2, Package } from '@lucide/svelte';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
DialogModelInformation,
DropdownMenuSearchable,
ModelId,
+ ModelsSelectorList,
ModelsSelectorOption
} from '$lib/components/app';
import type { ModelOption } from '$lib/types/models';
+ import { filterModelOptions, groupModelOptions, type ModelItem } from './utils';
interface Props {
class?: string;
let searchTerm = $state('');
let highlightedIndex = $state<number>(-1);
- let filteredOptions: ModelOption[] = $derived.by(() => {
- const term = searchTerm.trim().toLowerCase();
- if (!term) return options;
-
- return options.filter(
- (option) =>
- option.model.toLowerCase().includes(term) ||
- option.name?.toLowerCase().includes(term) ||
- option.aliases?.some((alias: string) => alias.toLowerCase().includes(term)) ||
- option.tags?.some((tag: string) => tag.toLowerCase().includes(term))
- );
- });
-
- let groupedFilteredOptions = $derived.by(() => {
- const favIds = modelsStore.favouriteModelIds;
- const result: {
- orgName: string | null;
- isFavouritesGroup: boolean;
- isLoadedGroup: boolean;
- items: { option: ModelOption; flatIndex: number }[];
- }[] = [];
-
- // Loaded models group (top)
- const loadedItems: { option: ModelOption; flatIndex: number }[] = [];
- for (let i = 0; i < filteredOptions.length; i++) {
- if (modelsStore.isModelLoaded(filteredOptions[i].model)) {
- loadedItems.push({ option: filteredOptions[i], flatIndex: i });
- }
- }
-
- if (loadedItems.length > 0) {
- result.push({
- orgName: null,
- isFavouritesGroup: false,
- isLoadedGroup: true,
- items: loadedItems
- });
- }
-
- // Favourites group
- const loadedModelIds = new Set(loadedItems.map((item) => item.option.model));
- const favItems: { option: ModelOption; flatIndex: number }[] = [];
- for (let i = 0; i < filteredOptions.length; i++) {
- if (favIds.has(filteredOptions[i].model) && !loadedModelIds.has(filteredOptions[i].model)) {
- favItems.push({ option: filteredOptions[i], flatIndex: i });
- }
- }
-
- if (favItems.length > 0) {
- result.push({
- orgName: null,
- isFavouritesGroup: true,
- isLoadedGroup: false,
- items: favItems
- });
- }
+ let filteredOptions = $derived(filterModelOptions(options, searchTerm));
- // Org groups (excluding loaded and favourites)
- const orgGroups = new SvelteMap<string, { option: ModelOption; flatIndex: number }[]>();
- for (let i = 0; i < filteredOptions.length; i++) {
- const option = filteredOptions[i];
-
- if (loadedModelIds.has(option.model) || favIds.has(option.model)) continue;
-
- const orgName = option.parsedId?.orgName ?? null;
- const key = orgName ?? '';
-
- if (!orgGroups.has(key)) orgGroups.set(key, []);
-
- orgGroups.get(key)!.push({ option, flatIndex: i });
- }
-
- for (const [orgName, items] of orgGroups) {
- result.push({
- orgName: orgName || null,
- isFavouritesGroup: false,
- isLoadedGroup: false,
- items
- });
- }
-
- return result;
- });
+ let groupedFilteredOptions = $derived(
+ groupModelOptions(filteredOptions, modelsStore.favouriteModelIds, (m) =>
+ modelsStore.isModelLoaded(m)
+ )
+ );
$effect(() => {
void searchTerm;
let isOpen = $state(false);
let showModelDialog = $state(false);
+ let infoModelId = $state<string | null>(null);
+
+ function handleInfoClick(modelName: string) {
+ infoModelId = modelName;
+ showModelDialog = true;
+ }
onMount(() => {
modelsStore.fetch().catch((error) => {
<p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p>
{/if}
- {#each groupedFilteredOptions as group (group.isLoadedGroup ? '__loaded__' : group.isFavouritesGroup ? '__favourites__' : group.orgName)}
- {#if group.isLoadedGroup}
- <p class="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none">
- Loaded models
- </p>
- {:else if group.isFavouritesGroup}
- <p class="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none">
- Favourite models
- </p>
- {:else if group.orgName}
- <p
- class="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none [&:not(:first-child)]:mt-2"
- >
- {group.orgName}
- </p>
- {/if}
-
- {#each group.items as { option, flatIndex } (group.isLoadedGroup ? `loaded-${option.id}` : group.isFavouritesGroup ? `fav-${option.id}` : option.id)}
- {@const isSelected = currentModel === option.model || activeId === option.id}
- {@const isHighlighted = flatIndex === highlightedIndex}
- {@const isFav = modelsStore.favouriteModelIds.has(option.model)}
-
- <ModelsSelectorOption
- {option}
- {isSelected}
- {isHighlighted}
- {isFav}
- showOrgName={group.isFavouritesGroup || group.isLoadedGroup}
- onSelect={handleSelect}
- onMouseEnter={() => (highlightedIndex = flatIndex)}
- onKeyDown={(e) => {
- if (e.key === KeyboardKey.ENTER || e.key === KeyboardKey.SPACE) {
- e.preventDefault();
- handleSelect(option.id);
- }
- }}
- />
- {/each}
- {/each}
+ {#snippet modelOption(item: ModelItem, showOrgName: boolean)}
+ {@const { option, flatIndex } = item}
+ {@const isSelected = currentModel === option.model || activeId === option.id}
+ {@const isHighlighted = flatIndex === highlightedIndex}
+ {@const isFav = modelsStore.favouriteModelIds.has(option.model)}
+
+ <ModelsSelectorOption
+ {option}
+ {isSelected}
+ {isHighlighted}
+ {isFav}
+ {showOrgName}
+ onSelect={handleSelect}
+ onInfoClick={handleInfoClick}
+ onMouseEnter={() => (highlightedIndex = flatIndex)}
+ onKeyDown={(e) => {
+ if (e.key === KeyboardKey.ENTER || e.key === KeyboardKey.SPACE) {
+ e.preventDefault();
+ handleSelect(option.id);
+ }
+ }}
+ />
+ {/snippet}
+
+ <ModelsSelectorList
+ groups={groupedFilteredOptions}
+ {currentModel}
+ {activeId}
+ sectionHeaderClass="my-1.5 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none"
+ onSelect={handleSelect}
+ onInfoClick={handleInfoClick}
+ renderOption={modelOption}
+ />
</div>
</DropdownMenuSearchable>
</DropdownMenu.Content>
{/if}
</div>
-{#if showModelDialog && !isRouter}
- <DialogModelInformation bind:open={showModelDialog} />
+{#if showModelDialog}
+ <DialogModelInformation bind:open={showModelDialog} modelId={infoModelId} />
{/if}
--- /dev/null
+<script lang="ts">
+ import { modelsStore } from '$lib/stores/models.svelte';
+ import { ModelsSelectorOption } from '$lib/components/app';
+ import type { GroupedModelOptions, ModelItem } from './utils';
+
+ interface Props {
+ groups: GroupedModelOptions;
+ currentModel: string | null;
+ activeId: string | null;
+ sectionHeaderClass?: string;
+ orgHeaderClass?: string;
+ onSelect: (modelId: string) => void;
+ onInfoClick: (modelName: string) => void;
+ renderOption?: import('svelte').Snippet<[ModelItem, boolean]>;
+ }
+
+ let {
+ groups,
+ currentModel,
+ activeId,
+ sectionHeaderClass = 'my-1 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none',
+ orgHeaderClass = 'px-2 py-2 text-[11px] font-semibold text-muted-foreground/50 select-none [&:not(:first-child)]:mt-1',
+ onSelect,
+ onInfoClick,
+ renderOption
+ }: Props = $props();
+ let render = $derived(renderOption ?? defaultOption);
+</script>
+
+{#snippet defaultOption(item: ModelItem, showOrgName: boolean)}
+ {@const { option } = item}
+ {@const isSelected = currentModel === option.model || activeId === option.id}
+ {@const isFav = modelsStore.favouriteModelIds.has(option.model)}
+
+ <ModelsSelectorOption
+ {option}
+ {isSelected}
+ isHighlighted={false}
+ {isFav}
+ {showOrgName}
+ {onSelect}
+ {onInfoClick}
+ onMouseEnter={() => {}}
+ onKeyDown={() => {}}
+ />
+{/snippet}
+
+{#if groups.loaded.length > 0}
+ <p class={sectionHeaderClass}>Loaded models</p>
+ {#each groups.loaded as item (`loaded-${item.option.id}`)}
+ {@render render(item, true)}
+ {/each}
+{/if}
+
+{#if groups.favourites.length > 0}
+ <p class={sectionHeaderClass}>Favourite models</p>
+ {#each groups.favourites as item (`fav-${item.option.id}`)}
+ {@render render(item, true)}
+ {/each}
+{/if}
+
+{#if groups.available.length > 0}
+ <p class={sectionHeaderClass}>Available models</p>
+ {#each groups.available as group (group.orgName)}
+ {#if group.orgName}
+ <p class={orgHeaderClass}>{group.orgName}</p>
+ {/if}
+ {#each group.items as item (item.option.id)}
+ {@render render(item, false)}
+ {/each}
+ {/each}
+{/if}
<script lang="ts">
- import { CircleAlert, Heart, HeartOff, Loader2, Power, PowerOff, RotateCw } from '@lucide/svelte';
+ import {
+ CircleAlert,
+ Heart,
+ HeartOff,
+ Info,
+ Loader2,
+ Power,
+ PowerOff,
+ RotateCw
+ } from '@lucide/svelte';
import { cn } from '$lib/components/ui/utils';
import { ActionIcon, ModelId } from '$lib/components/app';
import type { ModelOption } from '$lib/types/models';
onSelect: (modelId: string) => void;
onMouseEnter: () => void;
onKeyDown: (e: KeyboardEvent) => void;
+ onInfoClick?: (modelName: string) => void;
}
let {
showOrgName = false,
onSelect,
onMouseEnter,
- onKeyDown
+ onKeyDown,
+ onInfoClick
}: Props = $props();
let currentRouterModels = $derived(routerModels());
class="flex-1"
/>
- <div class="flex shrink-0 items-center gap-2.5">
+ <div class="flex shrink-0 items-center gap-1">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
- class="pointer-events-none flex w-4 items-center justify-center pl-2 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
+ class="pointer-events-none flex items-center justify-center gap-0.75 pl-2 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
onclick={(e) => e.stopPropagation()}
>
{#if isFav}
onclick={() => modelsStore.toggleFavourite(option.model)}
/>
{/if}
+
+ <!-- info button: only shown when model is loaded and callback is provided -->
+ {#if isLoaded && onInfoClick}
+ <ActionIcon
+ iconSize="h-2.5 w-2.5"
+ icon={Info}
+ tooltip="Model information"
+ class="h-3 w-3 hover:text-foreground"
+ onclick={() => onInfoClick(option.model)}
+ />
+ {/if}
</div>
+
{#if isLoading}
<Loader2 class="h-4 w-4 animate-spin text-muted-foreground" />
{:else if isFailed}
<script lang="ts">
import { onMount } from 'svelte';
- import { SvelteMap } from 'svelte/reactivity';
import { ChevronDown, Loader2, Package } from '@lucide/svelte';
import * as Sheet from '$lib/components/ui/sheet';
import { cn } from '$lib/components/ui/utils';
import { isRouterMode } from '$lib/stores/server.svelte';
import {
DialogModelInformation,
+ ModelsSelectorList,
SearchInput,
- TruncatedText,
- ModelsSelectorOption
+ TruncatedText
} from '$lib/components/app';
import type { ModelOption } from '$lib/types/models';
+ import { filterModelOptions, groupModelOptions } from './utils';
interface Props {
class?: string;
let searchTerm = $state('');
- let filteredOptions: ModelOption[] = $derived.by(() => {
- const term = searchTerm.trim().toLowerCase();
- if (!term) return options;
-
- return options.filter(
- (option) =>
- option.model.toLowerCase().includes(term) ||
- option.name?.toLowerCase().includes(term) ||
- option.aliases?.some((alias: string) => alias.toLowerCase().includes(term)) ||
- option.tags?.some((tag: string) => tag.toLowerCase().includes(term))
- );
- });
-
- let groupedFilteredOptions = $derived.by(() => {
- const favIds = modelsStore.favouriteModelIds;
- const result: {
- orgName: string | null;
- isFavouritesGroup: boolean;
- isLoadedGroup: boolean;
- items: { option: ModelOption; flatIndex: number }[];
- }[] = [];
-
- // Loaded models group (top)
- const loadedItems: { option: ModelOption; flatIndex: number }[] = [];
- for (let i = 0; i < filteredOptions.length; i++) {
- if (modelsStore.isModelLoaded(filteredOptions[i].model)) {
- loadedItems.push({ option: filteredOptions[i], flatIndex: i });
- }
- }
- if (loadedItems.length > 0) {
- result.push({
- orgName: null,
- isFavouritesGroup: false,
- isLoadedGroup: true,
- items: loadedItems
- });
- }
+ let filteredOptions = $derived(filterModelOptions(options, searchTerm));
- // Favourites group
- const loadedModelIds = new Set(loadedItems.map((item) => item.option.model));
- const favItems: { option: ModelOption; flatIndex: number }[] = [];
- for (let i = 0; i < filteredOptions.length; i++) {
- if (favIds.has(filteredOptions[i].model) && !loadedModelIds.has(filteredOptions[i].model)) {
- favItems.push({ option: filteredOptions[i], flatIndex: i });
- }
- }
- if (favItems.length > 0) {
- result.push({
- orgName: null,
- isFavouritesGroup: true,
- isLoadedGroup: false,
- items: favItems
- });
- }
-
- // Org groups (excluding loaded and favourites)
- const orgGroups = new SvelteMap<string, { option: ModelOption; flatIndex: number }[]>();
- for (let i = 0; i < filteredOptions.length; i++) {
- const option = filteredOptions[i];
- if (loadedModelIds.has(option.model) || favIds.has(option.model)) continue;
- const orgName = option.parsedId?.orgName ?? null;
- const key = orgName ?? '';
- if (!orgGroups.has(key)) orgGroups.set(key, []);
- orgGroups.get(key)!.push({ option, flatIndex: i });
- }
- for (const [orgName, items] of orgGroups) {
- result.push({
- orgName: orgName || null,
- isFavouritesGroup: false,
- isLoadedGroup: false,
- items
- });
- }
-
- return result;
- });
+ let groupedFilteredOptions = $derived(
+ groupModelOptions(filteredOptions, modelsStore.favouriteModelIds, (m) =>
+ modelsStore.isModelLoaded(m)
+ )
+ );
let sheetOpen = $state(false);
let showModelDialog = $state(false);
+ let infoModelId = $state<string | null>(null);
+
+ function handleInfoClick(modelName: string) {
+ infoModelId = modelName;
+ showModelDialog = true;
+ }
onMount(() => {
modelsStore.fetch().catch((error) => {
<p class="px-3 py-3 text-center text-sm text-muted-foreground">No models found.</p>
{/if}
- {#each groupedFilteredOptions as group (group.isLoadedGroup ? '__loaded__' : group.isFavouritesGroup ? '__favourites__' : group.orgName)}
- {#if group.isLoadedGroup}
- <p class="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none">
- Loaded models
- </p>
- {:else if group.isFavouritesGroup}
- <p class="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none">
- Favourite models
- </p>
- {:else if group.orgName}
- <p
- class="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none [&:not(:first-child)]:mt-2"
- >
- {group.orgName}
- </p>
- {/if}
-
- {#each group.items as { option } (group.isLoadedGroup ? `loaded-${option.id}` : group.isFavouritesGroup ? `fav-${option.id}` : option.id)}
- {@const isSelected = currentModel === option.model || activeId === option.id}
- {@const isFav = modelsStore.favouriteModelIds.has(option.model)}
- <ModelsSelectorOption
- {option}
- {isSelected}
- isHighlighted={false}
- {isFav}
- showOrgName={group.isFavouritesGroup || group.isLoadedGroup}
- onSelect={handleSelect}
- onMouseEnter={() => {}}
- onKeyDown={() => {}}
- />
- {/each}
- {/each}
+ <ModelsSelectorList
+ groups={groupedFilteredOptions}
+ {currentModel}
+ {activeId}
+ sectionHeaderClass="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none"
+ orgHeaderClass="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none [&:not(:first-child)]:mt-2"
+ onSelect={handleSelect}
+ onInfoClick={handleInfoClick}
+ />
</div>
</div>
</Sheet.Content>
{/if}
</div>
-{#if showModelDialog && !isRouter}
- <DialogModelInformation bind:open={showModelDialog} />
+{#if showModelDialog}
+ <DialogModelInformation bind:open={showModelDialog} modelId={infoModelId} />
{/if}
*/
export { default as ModelsSelector } from './ModelsSelector.svelte';
+/**
+ * **ModelsSelectorList** - Grouped model options list
+ *
+ * Renders grouped model options (loaded, favourites, available) with section
+ * headers and org subgroups. Shared between ModelsSelector and ModelsSelectorSheet
+ * to avoid template duplication.
+ *
+ * Accepts an optional `renderOption` snippet to customize how each option is
+ * rendered (e.g., to add keyboard navigation or highlighting).
+ */
+export { default as ModelsSelectorList } from './ModelsSelectorList.svelte';
+
+/**
+ * **ModelsSelectorOption** - Single model option row
+ *
+ * Renders a single model option with selection state, favourite toggle,
+ * load/unload actions, status indicators, and an info button.
+ * Used inside ModelsSelectorList or directly in custom render snippets.
+ */
+export { default as ModelsSelectorOption } from './ModelsSelectorOption.svelte';
+
/**
* **ModelsSelectorSheet** - Mobile model selection sheet
*
* ```
*/
export { default as ModelBadge } from './ModelBadge.svelte';
+
+/**
+ * **ModelId** - Parsed model identifier display
+ *
+ * Displays a model ID with optional org name, parameter badges, quantization,
+ * aliases, and tags. Supports raw mode to show the unprocessed model name.
+ * Respects the user's `showRawModelNames` setting.
+ */
export { default as ModelId } from './ModelId.svelte';
-export { default as ModelsSelectorOption } from './ModelsSelectorOption.svelte';
--- /dev/null
+import { SvelteMap } from 'svelte/reactivity';
+import type { ModelOption } from '$lib/types/models';
+
+export interface ModelItem {
+ option: ModelOption;
+ flatIndex: number;
+}
+
+export interface OrgGroup {
+ orgName: string | null;
+ items: ModelItem[];
+}
+
+export interface GroupedModelOptions {
+ loaded: ModelItem[];
+ favourites: ModelItem[];
+ available: OrgGroup[];
+}
+
+export function filterModelOptions(options: ModelOption[], searchTerm: string): ModelOption[] {
+ const term = searchTerm.trim().toLowerCase();
+ if (!term) return options;
+
+ return options.filter(
+ (option) =>
+ option.model.toLowerCase().includes(term) ||
+ option.name?.toLowerCase().includes(term) ||
+ option.aliases?.some((alias: string) => alias.toLowerCase().includes(term)) ||
+ option.tags?.some((tag: string) => tag.toLowerCase().includes(term))
+ );
+}
+
+export function groupModelOptions(
+ filteredOptions: ModelOption[],
+ favouriteIds: Set<string>,
+ isModelLoaded: (model: string) => boolean
+): GroupedModelOptions {
+ // Loaded models
+ const loaded: ModelItem[] = [];
+ for (let i = 0; i < filteredOptions.length; i++) {
+ if (isModelLoaded(filteredOptions[i].model)) {
+ loaded.push({ option: filteredOptions[i], flatIndex: i });
+ }
+ }
+
+ // Favourites (excluding loaded)
+ const loadedModelIds = new Set(loaded.map((item) => item.option.model));
+ const favourites: ModelItem[] = [];
+ for (let i = 0; i < filteredOptions.length; i++) {
+ if (
+ favouriteIds.has(filteredOptions[i].model) &&
+ !loadedModelIds.has(filteredOptions[i].model)
+ ) {
+ favourites.push({ option: filteredOptions[i], flatIndex: i });
+ }
+ }
+
+ // Available models grouped by org (excluding loaded and favourites)
+ const available: OrgGroup[] = [];
+ const orgGroups = new SvelteMap<string, ModelItem[]>();
+ for (let i = 0; i < filteredOptions.length; i++) {
+ const option = filteredOptions[i];
+ if (loadedModelIds.has(option.model) || favouriteIds.has(option.model)) continue;
+
+ const key = option.parsedId?.orgName ?? '';
+ if (!orgGroups.has(key)) orgGroups.set(key, []);
+ orgGroups.get(key)!.push({ option, flatIndex: i });
+ }
+
+ for (const [orgName, items] of orgGroups) {
+ available.push({ orgName: orgName || null, items });
+ }
+
+ return { loaded, favourites, available };
+}