mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-01 18:37:28 -05:00
ui : drop an orphaned doc comment left by a moved method
Assisted-by: pi:zai-org/GLM-5.3-Flash
This commit is contained in:
+6
-29
@@ -1,14 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ModelsSelectorDropdown, ModelsSelectorSheet } from '$lib/components/app';
|
||||
import { LOCAL_BACKEND_ID } from '$lib/constants';
|
||||
import {
|
||||
backendsModelsStore,
|
||||
backendsStore,
|
||||
conversationsStore,
|
||||
deviceStore,
|
||||
modelsStore,
|
||||
serverStore
|
||||
} from '$lib/stores';
|
||||
import { conversationsStore, deviceStore, modelsStore, serverStore } from '$lib/stores';
|
||||
import { getConversationModel } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
@@ -36,14 +28,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
let isRouter = $derived(serverStore.isRouterMode);
|
||||
// the provider this selector is pointed at, which is the one its colours report
|
||||
let selectorError = $derived.by(() => {
|
||||
const backendId = backendsStore.active.id;
|
||||
|
||||
return backendId === LOCAL_BACKEND_ID
|
||||
? Boolean(serverStore.error)
|
||||
: backendsModelsStore.get(backendId).error !== null;
|
||||
});
|
||||
let isOffline = $derived(!!serverStore.error);
|
||||
|
||||
let conversationModel = $derived(
|
||||
getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
|
||||
@@ -67,16 +52,10 @@
|
||||
|
||||
$effect(() => {
|
||||
if (conversationModel && conversationModel !== lastSyncedConversationModel) {
|
||||
const option = modelsStore.models.find((m) => m.model === conversationModel);
|
||||
|
||||
// only sync models served by the active backend; a model from another
|
||||
// backend must not yank the active tab (and trigger a full backend
|
||||
// switch) just because the conversation used it. sends resolve their
|
||||
// backend explicitly via ensureModelBackend
|
||||
if (option && option.backendId === backendsStore.active.id) {
|
||||
if (modelsStore.models.some((m) => m.model === conversationModel)) {
|
||||
modelsStore.selectedModelName = conversationModel;
|
||||
modelsStore.selectModelByName(conversationModel);
|
||||
} else if (!option) {
|
||||
} else {
|
||||
modelsStore.selectedModelName = null;
|
||||
modelsStore.clearSelection();
|
||||
}
|
||||
@@ -176,8 +155,7 @@
|
||||
<ModelsSelectorSheet
|
||||
bind:this={selectorModelRef}
|
||||
currentModel={selectorModel}
|
||||
{disabled}
|
||||
error={selectorError}
|
||||
disabled={disabled || isOffline}
|
||||
{forceForegroundText}
|
||||
{useGlobalSelection}
|
||||
/>
|
||||
@@ -185,8 +163,7 @@
|
||||
<ModelsSelectorDropdown
|
||||
bind:this={selectorModelRef}
|
||||
currentModel={selectorModel}
|
||||
{disabled}
|
||||
error={selectorError}
|
||||
disabled={disabled || isOffline}
|
||||
{forceForegroundText}
|
||||
{useGlobalSelection}
|
||||
/>
|
||||
|
||||
@@ -2,17 +2,10 @@
|
||||
import { AlertTriangle, Loader2, RefreshCw } from '@lucide/svelte';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { useBackendAvailability } from '$lib/hooks/use-backend-availability.svelte';
|
||||
import { backendsStore, serverStore } from '$lib/stores';
|
||||
|
||||
const availability = useBackendAvailability();
|
||||
import { serverStore } from '$lib/stores';
|
||||
|
||||
let hasError = $derived(!!serverStore.error);
|
||||
let isLoadingModel = $derived(serverStore.status === 503);
|
||||
// A single provider leaves nowhere to switch to, so the banner is the only
|
||||
// place the failure can be reported. With another provider available the
|
||||
// selector carries it instead, and stays usable.
|
||||
let isOnlyProvider = $derived(backendsStore.enabled.length <= 1);
|
||||
let hasError = $derived(availability.isOffline && isOnlyProvider);
|
||||
</script>
|
||||
|
||||
{#if hasError || isLoadingModel}
|
||||
|
||||
@@ -221,7 +221,6 @@ export { default as ChatFormActionReasoning } from './ChatForm/ChatFormActions/C
|
||||
*/
|
||||
export { default as ChatFormActionAddToolsSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte';
|
||||
|
||||
|
||||
/**
|
||||
* Compact context-usage gauge with per-turn and cumulative breakdown in the tooltip.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ModelCapabilityIcons from './ModelCapabilityIcons.svelte';
|
||||
import { isLocalOption } from './ModelsManager/utils';
|
||||
import { ModelCapability } from '$lib/enums';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
@@ -21,7 +20,7 @@
|
||||
$effect(() => {
|
||||
template = '';
|
||||
|
||||
if (!isNearViewport || !isLocalOption(option)) return;
|
||||
if (!isNearViewport) return;
|
||||
|
||||
// the listing says nothing about tools and reasoning; only the model's chat
|
||||
// template does, and the Hub carries that template for a local GGUF whether
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { isLocalOption } from './ModelsManager/utils';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { nearViewport } from '$lib/utils';
|
||||
@@ -22,7 +21,7 @@
|
||||
$effect(() => {
|
||||
fetched = null;
|
||||
|
||||
if (reported || !isNearViewport || !isLocalOption(option)) return;
|
||||
if (reported || !isNearViewport) return;
|
||||
|
||||
// a local GGUF carries its trained context in the model metadata
|
||||
const repo = option.model.split(':')[0] ?? '';
|
||||
|
||||
+13
-49
@@ -8,14 +8,13 @@
|
||||
ModelsSelectorOption,
|
||||
ModelsSelectorTriggerIcon
|
||||
} from '$lib/components/app';
|
||||
import { DialogBackendForm } from '$lib/components/app/backends';
|
||||
import type { ModelItem } from '$lib/components/app/navigation/utils';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import {
|
||||
DROPDOWN_MENU_CONTENT_SEARCH_SELECTOR,
|
||||
MODEL_ID,
|
||||
MODEL_ICON,
|
||||
MODEL_ID,
|
||||
SETTINGS_KEYS
|
||||
} from '$lib/constants';
|
||||
import { KeyboardKey, ServerModelStatus } from '$lib/enums';
|
||||
@@ -24,20 +23,13 @@
|
||||
import { modelsStore, settingsStore, uiStore } from '$lib/stores';
|
||||
import type { ModelOption, ModelSidecarBadge } from '$lib/types/models';
|
||||
import { modelLoadFraction } from '$lib/utils';
|
||||
import { rawModelId } from '$lib/utils/model-option-id';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
currentModel?: string | null;
|
||||
disabled?: boolean;
|
||||
/** The provider behind this selector is unreachable. */
|
||||
error?: boolean;
|
||||
forceForegroundText?: boolean;
|
||||
onModelChange?: (
|
||||
modelId: string,
|
||||
modelName: string,
|
||||
backendId?: string
|
||||
) => Promise<boolean> | boolean | void;
|
||||
onModelChange?: (modelId: string, modelName: string) => Promise<boolean> | boolean | void;
|
||||
useGlobalSelection?: boolean;
|
||||
}
|
||||
|
||||
@@ -45,7 +37,6 @@
|
||||
class: className = '',
|
||||
currentModel = null,
|
||||
disabled = false,
|
||||
error = false,
|
||||
forceForegroundText = false,
|
||||
onModelChange,
|
||||
useGlobalSelection = false
|
||||
@@ -53,7 +44,6 @@
|
||||
|
||||
let isOpen = $state(false);
|
||||
let highlightedId = $state<string | null>(null);
|
||||
let showAddBackend = $state(false);
|
||||
|
||||
const ms = useModelsSelector({
|
||||
currentModel: () => currentModel,
|
||||
@@ -128,9 +118,6 @@
|
||||
for (const group of ms.groupedFilteredOptions.available) {
|
||||
for (const item of group.items) order.push(item.option.id);
|
||||
}
|
||||
for (const provider of ms.groupedFilteredOptions.providers) {
|
||||
for (const item of provider.items) order.push(item.option.id);
|
||||
}
|
||||
|
||||
return order;
|
||||
});
|
||||
@@ -164,13 +151,6 @@
|
||||
setTimeout(() => uiStore.openModelsManager(), 0);
|
||||
}
|
||||
|
||||
function handleAddBackend() {
|
||||
isOpen = false;
|
||||
|
||||
// let the menu finish closing before the dialog takes focus
|
||||
setTimeout(() => (showAddBackend = true), 0);
|
||||
}
|
||||
|
||||
// Alt+Enter only unloads and keeps the dropdown open.
|
||||
async function handleModelKeyAction(modelId: string, unload: boolean) {
|
||||
if (!unload) {
|
||||
@@ -179,13 +159,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const rawId = rawModelId(modelId);
|
||||
const model = modelsStore.routerModels.find((m) => m.id === rawId);
|
||||
const model = modelsStore.routerModels.find((m) => m.id === modelId);
|
||||
const status = model?.status?.value as ServerModelStatus | undefined;
|
||||
|
||||
if (status === ServerModelStatus.LOADING) return;
|
||||
|
||||
await modelsStore.status.unload(rawId);
|
||||
await modelsStore.status.unload(modelId);
|
||||
}
|
||||
|
||||
export function open() {
|
||||
@@ -232,13 +211,7 @@
|
||||
<MODEL_ICON class="h-3.5 w-3.5 shrink-0" />
|
||||
</span>
|
||||
{:else}
|
||||
<button
|
||||
class="cursor-pointer text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
||||
onclick={handleAddBackend}
|
||||
type="button"
|
||||
>
|
||||
No models yet. Add a backend to get started.
|
||||
</button>
|
||||
<span class="text-xs text-muted-foreground">No models yet.</span>
|
||||
{/if}
|
||||
{:else}
|
||||
{@const triggerStatus = triggerModel
|
||||
@@ -262,15 +235,13 @@
|
||||
{...props}
|
||||
class={[
|
||||
`relative inline-grid cursor-pointer grid-cols-[1fr_auto_1fr] items-center gap-1 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
|
||||
error
|
||||
? 'border-destructive/40 bg-destructive/10 !text-destructive hover:bg-destructive/20'
|
||||
: !ms.isCurrentModelInCache
|
||||
? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
|
||||
: forceForegroundText
|
||||
!ms.isCurrentModelInCache
|
||||
? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
|
||||
: forceForegroundText
|
||||
? 'text-foreground'
|
||||
: ms.isHighlightedCurrentModelActive
|
||||
? 'text-foreground'
|
||||
: ms.isHighlightedCurrentModelActive
|
||||
? 'text-foreground'
|
||||
: 'text-foreground',
|
||||
: 'text-foreground',
|
||||
isOpen && 'text-foreground',
|
||||
'max-w-[min(calc(100vw-4rem) md:max-w-[min(calc(100cqw-9rem),25rem)]'
|
||||
]}
|
||||
@@ -387,8 +358,6 @@
|
||||
favorites={ms.favoriteItems}
|
||||
groups={ms.groupedFilteredOptions}
|
||||
loaded={ms.loadedItems}
|
||||
onProviderBack={ms.isProviderView ? ms.closeProvider : undefined}
|
||||
onProviderOpen={ms.openProvider}
|
||||
onSelect={ms.handleSelect}
|
||||
renderOption={modelOption}
|
||||
sectionHeaderClass="[&:not(:first-child)]:mt-1 mb-1 px-2 py-2 text-[13px] font-semibold text-foreground/80 select-none"
|
||||
@@ -455,15 +424,10 @@
|
||||
|
||||
{#if selectedOption}
|
||||
<Tooltip.Content>
|
||||
<p class="font-mono">{triggerTooltipLabel(selectedOption)}</p>
|
||||
</Tooltip.Content>
|
||||
<p class="font-mono">{triggerTooltipLabel(selectedOption)}</p>
|
||||
</Tooltip.Content>
|
||||
{/if}
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DialogBackendForm
|
||||
bind:open={showAddBackend}
|
||||
onSaved={(backend) => void ms.showBackendModels(backend.id)}
|
||||
/>
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
loaded?: ModelItem[];
|
||||
/** Show the organization name in every model id of the list. */
|
||||
showOrgName?: boolean;
|
||||
/** Open one provider's full list, offered when a section is cut short. */
|
||||
onProviderOpen?: (backendId: string) => void;
|
||||
/** Leave the drilled-in provider; enables the back affordance. */
|
||||
onProviderBack?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -40,8 +36,6 @@
|
||||
favorites = [],
|
||||
groups,
|
||||
loaded = [],
|
||||
onProviderBack,
|
||||
onProviderOpen,
|
||||
onSelect,
|
||||
renderOption,
|
||||
sectionHeaderClass = 'm-0 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none',
|
||||
@@ -227,39 +221,6 @@
|
||||
</ModelsSection>
|
||||
{/if}
|
||||
|
||||
<!-- One section per remote provider. -->
|
||||
{#each groups.providers as provider (provider.backendId)}
|
||||
<ModelsSection
|
||||
backendId={provider.backendId}
|
||||
error={Boolean(provider.error)}
|
||||
label={provider.name}
|
||||
loading={provider.loading}
|
||||
onBack={onProviderBack}
|
||||
persistKey={provider.backendId}
|
||||
revealChevronOnHover
|
||||
sticky
|
||||
>
|
||||
{#if provider.items.length > 0}
|
||||
{@render listRows(provider.items, provider.backendId)}
|
||||
|
||||
{#if onProviderOpen && provider.matched > provider.items.length}
|
||||
<!-- same box as a model row, it opens the provider's full list -->
|
||||
<button
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm p-2 text-left text-sm text-muted-foreground transition hover:bg-accent hover:text-foreground focus:outline-none"
|
||||
onclick={() => onProviderOpen(provider.backendId)}
|
||||
type="button"
|
||||
>
|
||||
+ {provider.matched - provider.items.length} more
|
||||
</button>
|
||||
{/if}
|
||||
{:else if provider.catalog === 0}
|
||||
<p class="px-4 pb-2 text-xs text-muted-foreground">
|
||||
{provider.error ?? (provider.loading ? 'Loading models...' : 'No models')}
|
||||
</p>
|
||||
{/if}
|
||||
</ModelsSection>
|
||||
{/each}
|
||||
|
||||
<DialogConfirmDownload
|
||||
action={ModelDownloadConfirmAction.CANCEL}
|
||||
onClose={() => (cancelOpen = false)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ModelLoadHighlight from '../ModelLoadHighlight.svelte';
|
||||
import { modelDraftsFor } from '../ModelsManager/utils';
|
||||
import {
|
||||
ModelAvatar,
|
||||
ModelCapabilities,
|
||||
@@ -13,8 +12,6 @@
|
||||
import { modelsStore, settingsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { modelLoadFraction, modelLoadProgressText } from '$lib/utils';
|
||||
import { getBackend } from '$lib/utils/api-base';
|
||||
import { getBackendCapabilities } from '$lib/utils/backend';
|
||||
|
||||
interface Props {
|
||||
option: ModelOption;
|
||||
@@ -44,9 +41,6 @@
|
||||
showRepoOrgAvatar = false
|
||||
}: Props = $props();
|
||||
|
||||
// row actions follow the backend that serves the row, not the selected one
|
||||
let rowBackend = $derived(getBackend(option.backendId));
|
||||
let canLoad = $derived(rowBackend ? getBackendCapabilities(rowBackend).loadUnload : false);
|
||||
let currentRouterModels = $derived(modelsStore.routerModels);
|
||||
let serverStatus = $derived.by(() => {
|
||||
const model = currentRouterModels.find((m) => m.id === option.model);
|
||||
@@ -102,7 +96,6 @@
|
||||
aliases={option.aliases}
|
||||
class="min-w-0 flex-1"
|
||||
draftSidecars={option.draftSidecars}
|
||||
drafts={modelDraftsFor(option)}
|
||||
hideCapabilities
|
||||
hideModalities
|
||||
{hideOrgName}
|
||||
@@ -119,15 +112,7 @@
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<ModelRowActions {isFav} {option} />
|
||||
|
||||
<ModelLoadControl
|
||||
{canLoad}
|
||||
{isFailed}
|
||||
{isLoaded}
|
||||
{isLoading}
|
||||
{isSleeping}
|
||||
{option}
|
||||
showBackendMark
|
||||
/>
|
||||
<ModelLoadControl {isFailed} {isLoaded} {isLoading} {isSleeping} {option} />
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
ModelsSelectorTriggerIcon,
|
||||
SearchInput
|
||||
} from '$lib/components/app';
|
||||
import { DialogBackendForm } from '$lib/components/app/backends';
|
||||
import * as Sheet from '$lib/components/ui/sheet';
|
||||
import { MODEL_ICON, SETTINGS_KEYS } from '$lib/constants';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
@@ -19,14 +18,8 @@
|
||||
class?: string;
|
||||
currentModel?: string | null;
|
||||
/** Callback when model changes. Return false to keep menu open (e.g., for validation failures) */
|
||||
onModelChange?: (
|
||||
modelId: string,
|
||||
modelName: string,
|
||||
backendId?: string
|
||||
) => Promise<boolean> | boolean | void;
|
||||
onModelChange?: (modelId: string, modelName: string) => Promise<boolean> | boolean | void;
|
||||
disabled?: boolean;
|
||||
/** The provider behind this selector is unreachable. */
|
||||
error?: boolean;
|
||||
forceForegroundText?: boolean;
|
||||
/** When true, user's global selection takes priority over currentModel (for form selector) */
|
||||
useGlobalSelection?: boolean;
|
||||
@@ -36,14 +29,12 @@
|
||||
class: className = '',
|
||||
currentModel = null,
|
||||
disabled = false,
|
||||
error = false,
|
||||
forceForegroundText = false,
|
||||
onModelChange,
|
||||
useGlobalSelection = false
|
||||
}: Props = $props();
|
||||
|
||||
let sheetOpen = $state(false);
|
||||
let showAddBackend = $state(false);
|
||||
|
||||
const ms = useModelsSelector({
|
||||
currentModel: () => currentModel,
|
||||
@@ -73,13 +64,6 @@
|
||||
// let the sheet finish closing before the dialog takes focus
|
||||
setTimeout(() => uiStore.openModelsManager(), 0);
|
||||
}
|
||||
|
||||
function handleAddBackend() {
|
||||
sheetOpen = false;
|
||||
|
||||
// let the sheet finish closing before the dialog takes focus
|
||||
setTimeout(() => (showAddBackend = true), 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={['relative inline-flex flex-col items-end gap-1', className]}>
|
||||
@@ -89,13 +73,7 @@
|
||||
Loading models…
|
||||
</div>
|
||||
{:else if ms.options.length === 0 && ms.isMultiModel}
|
||||
<button
|
||||
class="cursor-pointer text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
||||
onclick={handleAddBackend}
|
||||
type="button"
|
||||
>
|
||||
No models yet. Add a backend to get started.
|
||||
</button>
|
||||
<span class="text-xs text-muted-foreground">No models yet.</span>
|
||||
{:else}
|
||||
{@const selectedOption = ms.getDisplayOption()}
|
||||
{@const triggerModel = selectedOption?.model}
|
||||
@@ -114,15 +92,13 @@
|
||||
<button
|
||||
class={[
|
||||
`relative inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 max-sm:px-3 max-sm:py-2 max-sm:text-sm dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
|
||||
error
|
||||
? 'border-destructive/40 bg-destructive/10 !text-destructive hover:bg-destructive/20'
|
||||
: !ms.isCurrentModelInCache
|
||||
? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
|
||||
: forceForegroundText
|
||||
!ms.isCurrentModelInCache
|
||||
? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
|
||||
: forceForegroundText
|
||||
? 'text-foreground'
|
||||
: ms.isHighlightedCurrentModelActive
|
||||
? 'text-foreground'
|
||||
: ms.isHighlightedCurrentModelActive
|
||||
? 'text-foreground'
|
||||
: 'text-foreground',
|
||||
: 'text-foreground',
|
||||
sheetOpen && 'text-foreground'
|
||||
]}
|
||||
disabled={disabled || ms.updating}
|
||||
@@ -201,8 +177,6 @@
|
||||
favorites={ms.favoriteItems}
|
||||
groups={ms.groupedFilteredOptions}
|
||||
loaded={ms.loadedItems}
|
||||
onProviderBack={ms.isProviderView ? ms.closeProvider : undefined}
|
||||
onProviderOpen={ms.openProvider}
|
||||
onSelect={ms.handleSelect}
|
||||
sectionHeaderClass="px-2 py-2 text-xs font-semibold text-muted-foreground/60 select-none"
|
||||
{showOrgName}
|
||||
@@ -227,15 +201,13 @@
|
||||
<button
|
||||
class={[
|
||||
`inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
|
||||
error
|
||||
? 'border-destructive/40 bg-destructive/10 !text-destructive hover:bg-destructive/20'
|
||||
: !ms.isCurrentModelInCache
|
||||
? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
|
||||
: forceForegroundText
|
||||
!ms.isCurrentModelInCache
|
||||
? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
|
||||
: forceForegroundText
|
||||
? 'text-foreground'
|
||||
: ms.isHighlightedCurrentModelActive
|
||||
? 'text-foreground'
|
||||
: ms.isHighlightedCurrentModelActive
|
||||
? 'text-foreground'
|
||||
: 'text-foreground'
|
||||
: 'text-foreground'
|
||||
]}
|
||||
disabled={disabled || ms.updating}
|
||||
onclick={() => ms.handleOpenChange(true)}
|
||||
@@ -257,8 +229,3 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DialogBackendForm
|
||||
bind:open={showAddBackend}
|
||||
onSaved={(backend) => void ms.showBackendModels(backend.id)}
|
||||
/>
|
||||
|
||||
@@ -63,6 +63,7 @@ export { default as ModelsSection } from './ModelsSection.svelte';
|
||||
* styling stay consistent across every model-id surface.
|
||||
*/
|
||||
export { default as ModelCapabilityIcons } from './ModelCapabilityIcons.svelte';
|
||||
|
||||
/**
|
||||
* **ModelDraftSidecars** - draft sidecar badges
|
||||
*
|
||||
@@ -71,18 +72,4 @@ export { default as ModelCapabilityIcons } from './ModelCapabilityIcons.svelte';
|
||||
* everywhere.
|
||||
*/
|
||||
export { default as ModelDraftSidecars } from './ModelDraftSidecars.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 ModelCapabilities } from './ModelCapabilities.svelte';
|
||||
export { default as ModelContext } from './ModelContext.svelte';
|
||||
export { default as ModelAvatar } from './ModelAvatar.svelte';
|
||||
export { default as ModelLoadControl } from './ModelLoadControl.svelte';
|
||||
export { default as ModelRowActions } from './ModelRowActions.svelte';
|
||||
export { default as ModelsSection } from './ModelsSection.svelte';
|
||||
|
||||
export * from './ModelsSelector';
|
||||
|
||||
@@ -12,25 +12,9 @@ export interface OrgGroup {
|
||||
items: ModelItem[];
|
||||
}
|
||||
|
||||
/** One remote backend's section on the remote view. */
|
||||
export interface ProviderGroup {
|
||||
backendId: string;
|
||||
/** Models the provider lists without any search filtering. */
|
||||
catalog: number;
|
||||
error: string | null;
|
||||
/** Rows to render, capped by the display limit. */
|
||||
items: ModelItem[];
|
||||
loading: boolean;
|
||||
/** Rows left after the search filter, before the cap. */
|
||||
matched: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GroupedModelOptions {
|
||||
available: OrgGroup[];
|
||||
loaded: ModelItem[];
|
||||
/** Remote backends, one section each. */
|
||||
providers: ProviderGroup[];
|
||||
}
|
||||
|
||||
function matchesModality(option: ModelOption, term: string): boolean {
|
||||
@@ -150,48 +134,5 @@ export function groupModelOptions(
|
||||
available.push({ items, orgName: orgName || null });
|
||||
}
|
||||
|
||||
return { available, loaded, providers: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote backends as sections, one per backend, in the given order. Each section
|
||||
* keeps at most `limit` rows; `matched` carries the full count so the caller can
|
||||
* offer the rest.
|
||||
*/
|
||||
export function groupProviderOptions(
|
||||
options: ModelOption[],
|
||||
providers: {
|
||||
backendId: string;
|
||||
catalog: number;
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
name: string;
|
||||
}[],
|
||||
limit = Infinity,
|
||||
recentIds: readonly string[] = []
|
||||
): ProviderGroup[] {
|
||||
const byBackend = new SvelteMap<string, ModelItem[]>();
|
||||
const rank = new SvelteMap<string, number>();
|
||||
|
||||
recentIds.forEach((id, index) => rank.set(id, index));
|
||||
|
||||
const rankOf = (id: string) => rank.get(id) ?? Number.MAX_SAFE_INTEGER;
|
||||
// recently used models lead their section, the rest keep the backend's order
|
||||
const byRecency = (items: ModelItem[]) =>
|
||||
rank.size === 0 ? items : [...items].sort((a, b) => rankOf(a.option.id) - rankOf(b.option.id));
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const option = options[i];
|
||||
const backendId = option.backendId ?? '';
|
||||
|
||||
if (!byBackend.has(backendId)) byBackend.set(backendId, []);
|
||||
|
||||
byBackend.get(backendId)!.push({ flatIndex: i, option });
|
||||
}
|
||||
|
||||
return providers.map((provider) => {
|
||||
const items = byRecency(byBackend.get(provider.backendId) ?? []);
|
||||
|
||||
return { ...provider, items: items.slice(0, limit), matched: items.length };
|
||||
});
|
||||
return { available, loaded };
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ export const DIALOG_SUBMENU_CONTENT = 'w-60';
|
||||
export const CHAT_INPUT_FOCUS_SELECTOR =
|
||||
'[data-slot="input-area"] textarea, [data-slot="input-area"] [contenteditable="true"]';
|
||||
|
||||
/**
|
||||
* Selects the search input inside an open dropdown-menu's content, to focus it
|
||||
* after the menu mounts (bits-ui's default auto-focus can yank page scroll).
|
||||
*/
|
||||
export const DROPDOWN_MENU_CONTENT_SEARCH_SELECTOR = '[data-slot="dropdown-menu-content"] input';
|
||||
|
||||
/** Filter controls above the model table: one box, one height, one fill. */
|
||||
export const FILTER_TRIGGER_CLASS = `
|
||||
h-8
|
||||
|
||||
@@ -45,6 +45,9 @@ export const STATS_UNITS = {
|
||||
|
||||
export const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||
|
||||
/** Models listed per remote provider before the "+ X more" line; search covers the rest. */
|
||||
export const REMOTE_PROVIDER_MODEL_LIMIT = 12;
|
||||
|
||||
/** Orgs whose avatar is dark and needs inverting in dark mode. */
|
||||
export const DARK_INVERT_AVATAR_ORGS = ['openai'];
|
||||
|
||||
|
||||
@@ -2,34 +2,19 @@ import type { ModelItem } from '$lib/components/app/navigation/utils';
|
||||
import {
|
||||
filterModelOptions,
|
||||
groupFavoriteOptions,
|
||||
groupModelOptions,
|
||||
groupProviderOptions
|
||||
groupModelOptions
|
||||
} from '$lib/components/app/navigation/utils';
|
||||
import {
|
||||
CHAT_INPUT_FOCUS_SELECTOR,
|
||||
LOCAL_BACKEND_ID,
|
||||
REMOTE_PROVIDER_MODEL_LIMIT
|
||||
} from '$lib/constants';
|
||||
import { backendsModelsStore, backendsStore, modelsStore, serverStore, uiStore } from '$lib/stores';
|
||||
import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
|
||||
import { modelsStore, serverStore, uiStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { getBackend } from '$lib/utils/api-base';
|
||||
import { getBackendCapabilities } from '$lib/utils/backend';
|
||||
import { rawModelId } from '$lib/utils/model-option-id';
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
/** Groups of the favorites tab, which lists favorites only. */
|
||||
const EMPTY_GROUPS = { available: [], loaded: [], providers: [] };
|
||||
|
||||
export interface UseModelsSelectorOptions {
|
||||
currentModel: () => string | null;
|
||||
useGlobalSelection?: () => boolean;
|
||||
onModelChange?: () =>
|
||||
| ((
|
||||
modelId: string,
|
||||
modelName: string,
|
||||
backendId?: string
|
||||
) => Promise<boolean> | boolean | void)
|
||||
| ((modelId: string, modelName: string) => Promise<boolean> | boolean | void)
|
||||
| undefined;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
@@ -49,14 +34,10 @@ export interface UseModelsSelectorReturn {
|
||||
readonly loadedItems: ModelItem[];
|
||||
readonly filteredOptions: ModelOption[];
|
||||
readonly isEmpty: boolean;
|
||||
readonly isProviderView: boolean;
|
||||
readonly groupedFilteredOptions: ReturnType<typeof groupModelOptions>;
|
||||
readonly isLoadingModel: boolean;
|
||||
readonly searchTerm: string;
|
||||
closeProvider(): void;
|
||||
openProvider(backendId: string): void;
|
||||
setSearchTerm(value: string): void;
|
||||
showBackendModels(backendId: string): Promise<void>;
|
||||
handleSelect(modelId: string): Promise<void>;
|
||||
handleOpenChange(open: boolean): void;
|
||||
isFavorite(model: string): boolean;
|
||||
@@ -71,36 +52,21 @@ export interface UseModelsSelectorReturn {
|
||||
* duplicating store derivations, selection handling, and model loading.
|
||||
*/
|
||||
export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSelectorReturn {
|
||||
/**
|
||||
* Current view: the favorites of every backend, the local server's models, or
|
||||
* the remote backends'. Favorites are the default while there is at least one.
|
||||
*/
|
||||
/** Remote backend drilled into from its section; null while browsing. */
|
||||
let providerViewId = $state<string | null>(null);
|
||||
let isLoadingModel = $state(false);
|
||||
let searchTerm = $state('');
|
||||
|
||||
const isProviderView = $derived(providerViewId !== null);
|
||||
const isLocalOption = (option: ModelOption) => option.backendId === LOCAL_BACKEND_ID;
|
||||
// every enabled backend's models are one list: favorites, then the local
|
||||
// server, then one section per remote provider
|
||||
const allOptions = $derived(
|
||||
const options = $derived(
|
||||
modelsStore.models.filter((option) => {
|
||||
const modelProps = modelsStore.props.getModelProps(option.model);
|
||||
|
||||
return modelProps?.ui !== false;
|
||||
})
|
||||
);
|
||||
const options = $derived(
|
||||
providerViewId ? allOptions.filter((option) => option.backendId === providerViewId) : allOptions
|
||||
);
|
||||
const loading = $derived(modelsStore.loading);
|
||||
const updating = $derived(modelsStore.updating);
|
||||
const activeId = $derived(modelsStore.selectedModelId);
|
||||
// Router mode and external backends both expose a selectable model list; only
|
||||
// a lone llama.cpp server without a router has nothing to choose from.
|
||||
const isMultiModel = $derived(
|
||||
serverStore.isRouterMode ||
|
||||
backendsStore.enabled.some((backend) => backend.id !== LOCAL_BACKEND_ID)
|
||||
);
|
||||
// a lone llama.cpp server without a router has nothing to choose from
|
||||
const isMultiModel = $derived(serverStore.isRouterMode);
|
||||
const isRouter = $derived(serverStore.isRouterMode);
|
||||
const serverModel = $derived(modelsStore.singleModelName);
|
||||
const currentModel = $derived(opts.currentModel());
|
||||
@@ -108,54 +74,29 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
const isHighlightedCurrentModelActive = $derived.by(() => {
|
||||
if (!isRouter || !currentModel) return false;
|
||||
|
||||
const currentOption = allOptions.find((option) => option.model === currentModel);
|
||||
const currentOption = options.find((option) => option.model === currentModel);
|
||||
|
||||
return currentOption ? currentOption.id === activeId : false;
|
||||
});
|
||||
const isCurrentModelInCache = $derived.by(() => {
|
||||
if (!isRouter || !currentModel) return true;
|
||||
|
||||
return allOptions.some((option) => option.model === currentModel);
|
||||
return options.some((option) => option.model === currentModel);
|
||||
});
|
||||
|
||||
let isLoadingModel = $state(false);
|
||||
let searchTerm = $state('');
|
||||
|
||||
const visibleOptions = $derived(allOptions.filter((option) => !modelsStore.isHidden(option.id)));
|
||||
const visibleOptions = $derived(options.filter((option) => !modelsStore.isHidden(option.id)));
|
||||
const filteredOptions = $derived(filterModelOptions(options, searchTerm));
|
||||
// favorites span every backend, so they come from the full option list
|
||||
const favoriteItems = $derived(
|
||||
groupFavoriteOptions(
|
||||
filterModelOptions(visibleOptions, searchTerm),
|
||||
modelsStore.favoriteModelIds
|
||||
)
|
||||
);
|
||||
const remoteProviders = $derived(
|
||||
backendsStore.enabled
|
||||
.filter((backend) => backend.id !== LOCAL_BACKEND_ID)
|
||||
.map((backend) => {
|
||||
const state = backendsModelsStore.get(backend.id);
|
||||
|
||||
return {
|
||||
backendId: backend.id,
|
||||
catalog: state.models.length,
|
||||
error: state.error,
|
||||
loading: state.loading,
|
||||
name: backend.name
|
||||
};
|
||||
})
|
||||
);
|
||||
// loaded models lead the list, from any llama-compat backend
|
||||
const isLoadedLlamaCompat = (option: ModelOption) =>
|
||||
modelsStore.isModelLoaded(option.model) &&
|
||||
getBackendCapabilities(getBackend(option.backendId)).loadUnload;
|
||||
const loadedItems = $derived.by(() => {
|
||||
if (isProviderView) return [];
|
||||
|
||||
return filterModelOptions(visibleOptions, searchTerm)
|
||||
// loaded models lead the list
|
||||
const loadedItems = $derived(
|
||||
filterModelOptions(visibleOptions, searchTerm)
|
||||
.map((option, flatIndex) => ({ flatIndex, option }))
|
||||
.filter(({ option }) => isLoadedLlamaCompat(option));
|
||||
});
|
||||
.filter(({ option }) => modelsStore.isModelLoaded(option.model))
|
||||
);
|
||||
const loadedIds = $derived(new SvelteSet(loadedItems.map((item) => item.option.id)));
|
||||
// loaded models and favorites are listed once, at the top: the sections skip both
|
||||
const sectionOptions = $derived(
|
||||
@@ -163,28 +104,9 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
(option) => !modelsStore.favoriteModelIds.has(option.model) && !loadedIds.has(option.id)
|
||||
)
|
||||
);
|
||||
const providerSections = $derived(
|
||||
groupProviderOptions(
|
||||
sectionOptions,
|
||||
remoteProviders,
|
||||
// a drill-in or a search reaches every model, the sections stay short
|
||||
providerViewId || searchTerm ? Infinity : REMOTE_PROVIDER_MODEL_LIMIT,
|
||||
modelsStore.recentModelIds
|
||||
)
|
||||
const groupedFilteredOptions = $derived(
|
||||
groupModelOptions(sectionOptions, (model) => modelsStore.isModelLoaded(model))
|
||||
);
|
||||
const groupedFilteredOptions = $derived.by(() => {
|
||||
if (isProviderView) {
|
||||
const sections = providerSections.filter((section) => section.backendId === providerViewId);
|
||||
|
||||
return { ...EMPTY_GROUPS, providers: sections };
|
||||
}
|
||||
|
||||
const local = groupModelOptions(sectionOptions.filter(isLocalOption), (m) =>
|
||||
modelsStore.isModelLoaded(m)
|
||||
);
|
||||
|
||||
return { ...local, providers: providerSections };
|
||||
});
|
||||
const isEmpty = $derived(
|
||||
filteredOptions.length === 0 && favoriteItems.length === 0 && loadedItems.length === 0
|
||||
);
|
||||
@@ -207,8 +129,8 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (loading || updating) return;
|
||||
|
||||
// a single-model llama.cpp server with no other backend has no list to show,
|
||||
// so the trigger opens the manager, which holds the one model's configuration
|
||||
// a single-model llama.cpp server has no list to show, so the trigger opens
|
||||
// the manager, which holds the one model's configuration
|
||||
if (!isMultiModel) {
|
||||
if (open) uiStore.openModelsManager();
|
||||
|
||||
@@ -216,7 +138,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
}
|
||||
|
||||
searchTerm = '';
|
||||
providerViewId = null;
|
||||
|
||||
if (open && isRouter) {
|
||||
modelsStore.props.fetchModalitiesForLoadedModels();
|
||||
@@ -225,37 +146,15 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
opts.onOpenChange?.(open);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the rendered view. Views are display only: the backend that serves
|
||||
* requests follows the selected model, not the view.
|
||||
*/
|
||||
/** Drill into one remote backend's full model list. */
|
||||
function openProvider(backendId: string) {
|
||||
providerViewId = backendId;
|
||||
searchTerm = '';
|
||||
}
|
||||
|
||||
function closeProvider() {
|
||||
providerViewId = null;
|
||||
searchTerm = '';
|
||||
}
|
||||
|
||||
/** Refresh a backend's models, e.g. right after it was added. */
|
||||
async function showBackendModels(backendId: string): Promise<void> {
|
||||
await backendsModelsStore.ensureLoaded(backendId);
|
||||
}
|
||||
|
||||
async function handleSelect(modelId: string) {
|
||||
// favorites live above the tabs and may belong to another backend, so the
|
||||
// lookup spans every enabled backend
|
||||
const option = allOptions.find((opt) => opt.id === modelId);
|
||||
const option = options.find((opt) => opt.id === modelId);
|
||||
|
||||
if (!option) return;
|
||||
|
||||
let shouldCloseMenu = true;
|
||||
|
||||
if (onModelChange) {
|
||||
const result = await onModelChange(rawModelId(option.id), option.model, option.backendId);
|
||||
const result = await onModelChange(option.id, option.model);
|
||||
|
||||
if (result === false) {
|
||||
shouldCloseMenu = false;
|
||||
@@ -275,9 +174,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
}
|
||||
|
||||
// only the built-in server loads on request, and only in router mode
|
||||
const canLoadHere = option.backendId === LOCAL_BACKEND_ID && isRouter;
|
||||
|
||||
if (!onModelChange && canLoadHere && !modelsStore.isModelLoaded(option.model)) {
|
||||
if (!onModelChange && isRouter && !modelsStore.isModelLoaded(option.model)) {
|
||||
isLoadingModel = true;
|
||||
|
||||
modelsStore.status
|
||||
@@ -289,18 +186,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
|
||||
function getDisplayOption(): ModelOption | undefined {
|
||||
if (!isRouter) {
|
||||
// External backend: the selection is backend-scoped, so it wins over
|
||||
// the conversation's model, which may belong to another backend.
|
||||
if (!serverStore.capabilities.props) {
|
||||
const selected = activeId ? allOptions.find((option) => option.id === activeId) : undefined;
|
||||
|
||||
if (selected) return selected;
|
||||
|
||||
return currentModel
|
||||
? allOptions.find((option) => option.model === currentModel)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const displayModel = serverModel || currentModel;
|
||||
|
||||
if (displayModel) {
|
||||
@@ -325,11 +210,11 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
};
|
||||
}
|
||||
|
||||
return allOptions.find((option) => option.model === currentModel);
|
||||
return options.find((option) => option.model === currentModel);
|
||||
}
|
||||
|
||||
if (activeId) {
|
||||
return allOptions.find((option) => option.id === activeId);
|
||||
return options.find((option) => option.id === activeId);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -340,8 +225,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
return activeId;
|
||||
},
|
||||
|
||||
closeProvider,
|
||||
|
||||
get emptyMessage() {
|
||||
return emptyMessage;
|
||||
},
|
||||
@@ -387,10 +270,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
return isMultiModel;
|
||||
},
|
||||
|
||||
get isProviderView() {
|
||||
return isProviderView;
|
||||
},
|
||||
|
||||
get isRouter() {
|
||||
return isRouter;
|
||||
},
|
||||
@@ -403,8 +282,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
return loading;
|
||||
},
|
||||
|
||||
openProvider,
|
||||
|
||||
get options() {
|
||||
return options;
|
||||
},
|
||||
@@ -421,8 +298,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
searchTerm = value;
|
||||
},
|
||||
|
||||
showBackendModels,
|
||||
|
||||
get updating() {
|
||||
return updating;
|
||||
}
|
||||
|
||||
@@ -103,8 +103,7 @@
|
||||
orgName: 'intel'
|
||||
}
|
||||
],
|
||||
loaded: loadedModels,
|
||||
providers: []
|
||||
loaded: loadedModels
|
||||
};
|
||||
|
||||
function handleSelect(modelId: string) {
|
||||
@@ -138,8 +137,7 @@
|
||||
currentModel={null}
|
||||
groups={{
|
||||
available: [],
|
||||
loaded: [loadedModels[0]],
|
||||
providers: []
|
||||
loaded: [loadedModels[0]]
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
@@ -154,8 +152,7 @@
|
||||
favorites={favoriteModels}
|
||||
groups={{
|
||||
available: [],
|
||||
loaded: [],
|
||||
providers: []
|
||||
loaded: []
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user