mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-02 19:07:25 -05:00
ui : list downloads as their own manager section
The models table lists in-flight and paused downloads in a Downloading section: flat rows like Loaded models, right after them, with a full width progress bar along the bottom. The status column shows the percent and swaps in pause or resume on hover; pause, resume and delete live in the row's action menu. The selector lists running downloads only. Assisted-by: pi
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import ModelsManagerModelConfiguration from './ModelsManagerModelConfiguration/ModelsManagerModelConfiguration.svelte';
|
||||
import ModelsManagerModelsTable from './ModelsManagerModelsTable.svelte';
|
||||
import {
|
||||
downloadGroups,
|
||||
groupModelQuants,
|
||||
modelContextLength,
|
||||
type ModelQuantGroup,
|
||||
@@ -139,14 +140,22 @@
|
||||
rank.size === 0 ? list : [...list].sort((a, b) => rankOf(a) - rankOf(b));
|
||||
// one entry per repo, so a model with several quants takes a single table row
|
||||
let entries = $derived(byRecency(groupModelQuants(matching)));
|
||||
// tracked downloads get their own section, listed like any other model
|
||||
let downloads = $derived(
|
||||
downloadGroups(modelsStore.status.getDownloadEntries(), modelsStore.models)
|
||||
);
|
||||
let groups = $derived.by(() => {
|
||||
// a loaded quant is a model of its own: its repo keeps the quants left behind
|
||||
const isLoaded = (option: ModelOption) => modelsStore.isModelLoaded(option.model);
|
||||
// a tracked download stands in its own section, so it is not listed twice
|
||||
const isDownload = (option: ModelOption) =>
|
||||
modelsStore.status.isDownloadInProgress(option.model) ||
|
||||
modelsStore.status.isDownloadPaused(option.model);
|
||||
const loaded: ModelQuantGroup[] = [];
|
||||
const rest: ModelQuantGroup[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const remaining = entry.quants.filter((quant) => !isLoaded(quant));
|
||||
const remaining = entry.quants.filter((quant) => !isLoaded(quant) && !isDownload(quant));
|
||||
|
||||
for (const quant of entry.quants) {
|
||||
if (!isLoaded(quant)) continue;
|
||||
@@ -183,6 +192,7 @@
|
||||
};
|
||||
|
||||
pushSection(ModelsTableGroupKind.LOADED, loaded);
|
||||
pushSection(ModelsTableGroupKind.DOWNLOADING, downloads);
|
||||
pushSection(ModelsTableGroupKind.FAVORITES, favorites);
|
||||
pushSection(ModelsTableGroupKind.LOCAL, local);
|
||||
pushSection(ModelsTableGroupKind.HIDDEN, hidden);
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { downloadProgressFor } from './utils';
|
||||
import { ActionIcon } from '$lib/components/app';
|
||||
import { MODEL_DOWNLOAD_ICONS, ModelRowDownloadState } from '$lib/constants';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
option: ModelOption;
|
||||
/** Download state the row stands for. */
|
||||
state: ModelRowDownloadState;
|
||||
}
|
||||
|
||||
let { class: className = '', option, state }: Props = $props();
|
||||
|
||||
let isPaused = $derived(state === ModelRowDownloadState.PAUSED);
|
||||
let progress = $derived(downloadProgressFor(option.model));
|
||||
let percent = $derived(
|
||||
progress && progress.totalBytes > 0
|
||||
? Math.round((progress.downloadedBytes / progress.totalBytes) * 100)
|
||||
: null
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- the percent is what the row shows at rest; the action takes its place on hover,
|
||||
the way the load control does in the other sections -->
|
||||
<div class={['flex items-center justify-center', className]}>
|
||||
<span
|
||||
class="text-xs text-muted-foreground tabular-nums group-hover:hidden [@media(pointer:coarse)]:hidden"
|
||||
>
|
||||
{percent !== null ? `${percent}%` : isPaused ? 'Paused' : ''}
|
||||
</span>
|
||||
|
||||
<div class="hidden group-hover:flex [@media(pointer:coarse)]:flex">
|
||||
<ActionIcon
|
||||
class="h-5 w-5 hover:text-foreground"
|
||||
icon={isPaused ? MODEL_DOWNLOAD_ICONS.resume : MODEL_DOWNLOAD_ICONS.pause}
|
||||
iconSize="h-4 w-4"
|
||||
onclick={() =>
|
||||
void (isPaused
|
||||
? modelsStore.status.downloadModel(option.model)
|
||||
: modelsStore.status.pauseDownload(option.model))}
|
||||
stopPropagationOnClick
|
||||
tooltip={isPaused ? 'Resume downloading' : 'Pause downloading'}
|
||||
tooltipAsTitle
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,13 +2,14 @@
|
||||
import ModelAvatar from '../ModelAvatar.svelte';
|
||||
import ModelCapabilities from '../ModelCapabilities.svelte';
|
||||
import ModelContext from '../ModelContext.svelte';
|
||||
import ModelDownloadProgressBar from '../ModelDownloadProgressBar.svelte';
|
||||
import ModelId from '../ModelId.svelte';
|
||||
import ModelsManagerStatusCell from './ModelsManagerStatusCell.svelte';
|
||||
import { modelRowActions } from './row-actions';
|
||||
import { configuredContext } from './utils';
|
||||
import { configuredContext, downloadProgressFor } from './utils';
|
||||
import { MoreHorizontal } from '@lucide/svelte';
|
||||
import { DropdownMenuActions } from '$lib/components/app';
|
||||
import { MODEL_ROW_GRID_CLASS } from '$lib/constants';
|
||||
import { MODEL_ROW_GRID_CLASS, ModelRowDownloadState } from '$lib/constants';
|
||||
import { KeyboardKey } from '$lib/enums';
|
||||
import { modelsStore, settingsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
@@ -27,6 +28,16 @@
|
||||
|
||||
let favorite = $derived(isFavorite(option));
|
||||
let isHidden = $derived(modelsStore.isHidden(option.id));
|
||||
// live while the download runs, frozen while it is paused
|
||||
let downloadProgress = $derived(downloadProgressFor(option.model));
|
||||
// a tracked download takes over the status column while it runs
|
||||
let download = $derived(
|
||||
modelsStore.status.isDownloadPaused(option.model)
|
||||
? ModelRowDownloadState.PAUSED
|
||||
: modelsStore.status.isDownloadInProgress(option.model)
|
||||
? ModelRowDownloadState.DOWNLOADING
|
||||
: null
|
||||
);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent): void {
|
||||
if (event.key === KeyboardKey.SPACE) event.preventDefault();
|
||||
@@ -38,7 +49,7 @@
|
||||
<div
|
||||
class={[
|
||||
MODEL_ROW_GRID_CLASS,
|
||||
'group cursor-pointer rounded-md px-2 py-3 transition',
|
||||
'group relative cursor-pointer rounded-md px-2 py-3 transition',
|
||||
isHidden && 'opacity-60',
|
||||
selected ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/40'
|
||||
]}
|
||||
@@ -74,11 +85,19 @@
|
||||
|
||||
<ModelContext class="justify-self-end" configured={configuredContext(option)} {option} />
|
||||
|
||||
<ModelsManagerStatusCell {option} />
|
||||
<ModelsManagerStatusCell {download} {option} />
|
||||
|
||||
{#if download}
|
||||
<ModelDownloadProgressBar
|
||||
downloadedBytes={downloadProgress?.downloadedBytes ?? 0}
|
||||
overlay
|
||||
totalBytes={downloadProgress?.totalBytes ?? 0}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-center justify-self-center">
|
||||
<DropdownMenuActions
|
||||
actions={modelRowActions(option, favorite, isHidden, onDelete)}
|
||||
actions={modelRowActions(option, favorite, isHidden, onDelete, download)}
|
||||
align="end"
|
||||
triggerIcon={MoreHorizontal}
|
||||
triggerTooltip="Model actions"
|
||||
|
||||
+8
-33
@@ -29,7 +29,6 @@
|
||||
ModelsSection
|
||||
} from '$lib/components/app';
|
||||
import { DialogConfirmDownload } from '$lib/components/app/dialogs';
|
||||
import ModelsSelectorDownloadItem from '$lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte';
|
||||
import {
|
||||
FAMILY_ROW_WINDOW,
|
||||
type ModalityKey,
|
||||
@@ -39,7 +38,7 @@
|
||||
ModelsTableSortKey
|
||||
} from '$lib/constants';
|
||||
import { KeyboardKey, ModelCapability, ModelDownloadConfirmAction } from '$lib/enums';
|
||||
import { modelsStore, settingsStore } from '$lib/stores';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { groupModelFamilies, type ModelFamilyGroup } from '$lib/utils/model-families';
|
||||
import type { Snippet } from 'svelte';
|
||||
@@ -76,22 +75,18 @@
|
||||
let isEmpty = $derived(groups.every((group) => group.items.length === 0));
|
||||
let hasFilters = $derived(contextLimit > 0 || modalities.length > 0 || capabilities.length > 0);
|
||||
|
||||
/** In-flight and paused downloads, tracked by the status feed. */
|
||||
let downloadEntries = $derived(modelsStore.status.getDownloadEntries());
|
||||
|
||||
/** Noun the show-more row counts in, per unit of the grouped list. */
|
||||
const SHOW_MORE_NOUNS: Record<GroupedListUnit, string> = {
|
||||
[GroupedListUnit.ENTRIES]: 'models',
|
||||
[GroupedListUnit.GROUPS]: 'families'
|
||||
};
|
||||
let pendingCancel = $state('');
|
||||
let cancelOpen = $state(false);
|
||||
let pendingDelete = $state('');
|
||||
let deleteOpen = $state(false);
|
||||
/** Repos whose quants are folded away; the rest show them. */
|
||||
const collapsedQuants = new SvelteSet<string>();
|
||||
/** Sections that list their models straight, without folding them into families. */
|
||||
const FLAT_SECTIONS = new Set<ModelsTableGroupKind>([
|
||||
ModelsTableGroupKind.DOWNLOADING,
|
||||
ModelsTableGroupKind.FAVORITES,
|
||||
ModelsTableGroupKind.LOADED
|
||||
]);
|
||||
@@ -99,7 +94,9 @@
|
||||
groups.map((group) => {
|
||||
// a flat section lists its models straight, families or not
|
||||
const flat = FLAT_SECTIONS.has(group.kind) || !settingsStore.config.groupModelsByFamily;
|
||||
const items = sortEntries(group.items);
|
||||
// downloads keep the feed's order: their progress, not their name, moves
|
||||
const items =
|
||||
group.kind === ModelsTableGroupKind.DOWNLOADING ? group.items : sortEntries(group.items);
|
||||
|
||||
return {
|
||||
...group,
|
||||
@@ -121,11 +118,6 @@
|
||||
}
|
||||
|
||||
// cancel is confirmed once for the whole list, so one dialog serves every row
|
||||
function requestCancel(repoWithTag: string): void {
|
||||
pendingCancel = repoWithTag;
|
||||
cancelOpen = true;
|
||||
}
|
||||
|
||||
function requestDelete(option: ModelOption): void {
|
||||
pendingDelete = option.model;
|
||||
deleteOpen = true;
|
||||
@@ -325,13 +317,6 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<DialogConfirmDownload
|
||||
action={ModelDownloadConfirmAction.CANCEL}
|
||||
onClose={() => (cancelOpen = false)}
|
||||
open={cancelOpen}
|
||||
repoWithTag={pendingCancel}
|
||||
/>
|
||||
|
||||
<DialogConfirmDownload
|
||||
action={ModelDownloadConfirmAction.DELETE}
|
||||
onClose={() => (deleteOpen = false)}
|
||||
@@ -365,22 +350,12 @@
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
{#if downloadEntries.length > 0}
|
||||
<ModelsSection count={downloadEntries.length} label="Download in progress" sticky>
|
||||
{#snippet icon()}
|
||||
<Download class="h-3.5 w-3.5 shrink-0" />
|
||||
{/snippet}
|
||||
|
||||
{#each downloadEntries as entry (entry.repoWithTag)}
|
||||
<ModelsSelectorDownloadItem {entry} onRequestCancel={requestCancel} />
|
||||
{/each}
|
||||
</ModelsSection>
|
||||
{/if}
|
||||
|
||||
{#each sections as group (group.key)}
|
||||
{#if group.items.length > 0}
|
||||
{#snippet groupIcon()}
|
||||
{#if group.kind === ModelsTableGroupKind.FAVORITES}
|
||||
{#if group.kind === ModelsTableGroupKind.DOWNLOADING}
|
||||
<Download class="h-3.5 w-3.5 shrink-0" />
|
||||
{:else if group.kind === ModelsTableGroupKind.FAVORITES}
|
||||
<Heart class="h-3.5 w-3.5 shrink-0" />
|
||||
{:else if group.kind === ModelsTableGroupKind.LOADED}
|
||||
<Power class="h-3.5 w-3.5 shrink-0" />
|
||||
|
||||
+17
-9
@@ -1,15 +1,19 @@
|
||||
<script lang="ts">
|
||||
import ModelLoadControl from '../ModelLoadControl.svelte';
|
||||
import ModelsManagerDownloadControl from './ModelsManagerDownloadControl.svelte';
|
||||
import type { ModelRowDownloadState } from '$lib/constants';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
/** Download state, when the row stands for a tracked download. */
|
||||
download?: ModelRowDownloadState | null;
|
||||
option: ModelOption;
|
||||
}
|
||||
|
||||
let { class: className = '', option }: Props = $props();
|
||||
let { class: className = '', download = null, option }: Props = $props();
|
||||
|
||||
let status = $derived(modelsStore.getModelStatus(option.model));
|
||||
let isOperationInProgress = $derived(modelsStore.status.isOperationInProgress(option.model));
|
||||
@@ -19,11 +23,15 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<ModelLoadControl
|
||||
class="justify-self-center {className}"
|
||||
isFailed={status === ServerModelStatus.FAILED}
|
||||
{isLoaded}
|
||||
isLoading={status === ServerModelStatus.LOADING || isOperationInProgress}
|
||||
isSleeping={status === ServerModelStatus.SLEEPING}
|
||||
{option}
|
||||
/>
|
||||
{#if download}
|
||||
<ModelsManagerDownloadControl class="justify-self-center {className}" {option} state={download} />
|
||||
{:else}
|
||||
<ModelLoadControl
|
||||
class="justify-self-center {className}"
|
||||
isFailed={status === ServerModelStatus.FAILED}
|
||||
{isLoaded}
|
||||
isLoading={status === ServerModelStatus.LOADING || isOperationInProgress}
|
||||
isSleeping={status === ServerModelStatus.SLEEPING}
|
||||
{option}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Eye, EyeOff, Heart, HeartOff, Trash2 } from '@lucide/svelte';
|
||||
import { MODEL_DOWNLOAD_ICONS, ModelRowDownloadState } from '$lib/constants';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
|
||||
@@ -7,8 +8,35 @@ export function modelRowActions(
|
||||
option: ModelOption,
|
||||
favorite: boolean,
|
||||
isHidden: boolean,
|
||||
onDelete: (option: ModelOption) => void
|
||||
onDelete: (option: ModelOption) => void,
|
||||
/** Download state, when the row stands for a tracked download. */
|
||||
download?: ModelRowDownloadState | null
|
||||
) {
|
||||
// a tracked download is paused, resumed or dropped: it is not loaded, hidden or
|
||||
// drafted. Deleting it stops the download and removes what is on disk, which is
|
||||
// what the same wording offers on a cached model
|
||||
if (download) {
|
||||
const isPaused = download === ModelRowDownloadState.PAUSED;
|
||||
|
||||
return [
|
||||
{
|
||||
icon: isPaused ? MODEL_DOWNLOAD_ICONS.resume : MODEL_DOWNLOAD_ICONS.pause,
|
||||
label: isPaused ? 'Resume downloading' : 'Pause downloading',
|
||||
onclick: () =>
|
||||
void (isPaused
|
||||
? modelsStore.status.downloadModel(option.model)
|
||||
: modelsStore.status.pauseDownload(option.model))
|
||||
},
|
||||
{
|
||||
icon: Trash2,
|
||||
label: 'Delete from disk',
|
||||
onclick: () => onDelete(option),
|
||||
separator: true,
|
||||
variant: 'destructive' as const
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
icon: favorite ? HeartOff : Heart,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ModelGroupKind, ModelsTableGroupKind } from '$lib/constants';
|
||||
import { LOCAL_BACKEND_ID, ModelGroupKind, ModelsTableGroupKind } from '$lib/constants';
|
||||
import { ModelCapability, ServerModelStatus } from '$lib/enums';
|
||||
import { HuggingFaceService, ModelsService } from '$lib/services';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import type { ModelDownloadProgress, ModelOption } from '$lib/types/models';
|
||||
import { detectThinkingSupport, detectToolUseSupport } from '$lib/utils';
|
||||
|
||||
/** One repo of the table, with the rows it ships as. */
|
||||
@@ -13,6 +13,45 @@ export interface ModelQuantGroup {
|
||||
quants: ModelOption[];
|
||||
}
|
||||
|
||||
/** Byte counts of a tracked download: live while it runs, frozen while paused. */
|
||||
export function downloadProgressFor(repoWithTag: string): ModelDownloadProgress | null {
|
||||
return (
|
||||
modelsStore.status.getDownloadProgress(repoWithTag) ??
|
||||
modelsStore.status.getPausedDownloadProgress(repoWithTag)
|
||||
);
|
||||
}
|
||||
|
||||
/** One tracked download of the status feed. */
|
||||
export interface DownloadEntry {
|
||||
isPaused: boolean;
|
||||
progress: ModelDownloadProgress | null;
|
||||
repoWithTag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* One table row per tracked download, so the table lists them the way it lists
|
||||
* any other model. A paused download is already in the router's listing, so its
|
||||
* own option is reused; a fresh one stands for its tag alone.
|
||||
*/
|
||||
export function downloadGroups(entries: DownloadEntry[], models: ModelOption[]): ModelQuantGroup[] {
|
||||
return entries.map((entry) => {
|
||||
const option = models.find((candidate) => candidate.model === entry.repoWithTag) ?? {
|
||||
backendId: LOCAL_BACKEND_ID,
|
||||
capabilities: [],
|
||||
id: entry.repoWithTag,
|
||||
model: entry.repoWithTag,
|
||||
name: entry.repoWithTag
|
||||
};
|
||||
|
||||
return {
|
||||
base: option,
|
||||
key: `download::${entry.repoWithTag}`,
|
||||
kind: ModelGroupKind.QUANTS,
|
||||
quants: [option]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** One collapsible block of the manager's table. */
|
||||
export interface ModelsTableGroup {
|
||||
/** One entry per repo, its quants hanging off it. */
|
||||
|
||||
+5
-12
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import ModelDownloadProgressBar from '../ModelDownloadProgressBar.svelte';
|
||||
import ModelOrgAvatar from '../ModelOrgAvatar.svelte';
|
||||
import { Info, Loader2, Pause, Play, X } from '@lucide/svelte';
|
||||
import { Loader2, Pause, Play, X } from '@lucide/svelte';
|
||||
import { ActionIcon, ModelId } from '$lib/components/app';
|
||||
import { HuggingFaceService, ModelsService } from '$lib/services';
|
||||
import { modelsStore, uiStore } from '$lib/stores';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelDownloadProgress } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
<!-- status action: spinner -> pause on hover while in flight, play on hover
|
||||
when paused; opacity only for the spinner, the spin owns the transform -->
|
||||
<span class="relative inline-flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
<span class="relative inline-flex h-5 w-5 shrink-0 items-center justify-center">
|
||||
{#if entry.isPaused}
|
||||
<Play
|
||||
class="absolute h-4 w-4 scale-75 opacity-0 transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:scale-100 group-hover:opacity-100 [@media(pointer:coarse)]:scale-100 [@media(pointer:coarse)]:opacity-100"
|
||||
@@ -106,17 +106,10 @@
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ActionIcon
|
||||
ariaLabel="Manage model"
|
||||
class="h-4 w-4 shrink-0 scale-75 cursor-pointer rounded-sm text-muted-foreground/70 opacity-0 transition-[opacity,transform,color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:text-foreground group-hover:scale-100 group-hover:opacity-100 [@media(pointer:coarse)]:scale-100 [@media(pointer:coarse)]:opacity-100"
|
||||
icon={Info}
|
||||
iconSize="h-4 w-4"
|
||||
onclick={() => uiStore.openModelsManager(entry.repoWithTag)}
|
||||
/>
|
||||
|
||||
<!-- the row actions keep the sizing and reveal of the other selector rows -->
|
||||
<ActionIcon
|
||||
ariaLabel="Cancel downloading"
|
||||
class="h-4 w-4 shrink-0 scale-75 cursor-pointer rounded-sm text-muted-foreground/70 opacity-0 transition-[opacity,transform,color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:text-destructive group-hover:scale-100 group-hover:opacity-100 [@media(pointer:coarse)]:scale-100 [@media(pointer:coarse)]:opacity-100"
|
||||
class="pointer-events-none h-5 w-5 shrink-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 hover:text-destructive [@media(pointer:coarse)]:pointer-events-auto [@media(pointer:coarse)]:opacity-100"
|
||||
icon={X}
|
||||
iconSize="h-4 w-4"
|
||||
onclick={() => onRequestCancel?.(entry.repoWithTag)}
|
||||
|
||||
@@ -80,7 +80,11 @@
|
||||
const headerStyle = 'top: var(--dropdown-sticky-height, 0px)';
|
||||
|
||||
/** In-flight / paused downloads, tracked by the status feed. */
|
||||
let getDownloadEntries = $derived(modelsStore.status.getDownloadEntries());
|
||||
// only a running download belongs in the selector; a paused one waits in the
|
||||
// manager table, where its partial files are resumed or dropped
|
||||
let getDownloadEntries = $derived(
|
||||
modelsStore.status.getDownloadEntries().filter((entry) => !entry.isPaused)
|
||||
);
|
||||
|
||||
// cancel is confirmed once for the whole list, so one dialog serves every row
|
||||
// The target is kept while the dialog closes so its copy stays rendered.
|
||||
@@ -186,7 +190,7 @@
|
||||
{/if}
|
||||
|
||||
{#if getDownloadEntries.length > 0}
|
||||
<p class={headerClass} style={headerStyle}>Download in progress</p>
|
||||
<p class={headerClass} style={headerStyle}>Downloading</p>
|
||||
|
||||
{#each getDownloadEntries as entry (entry.repoWithTag)}
|
||||
<ModelsSelectorDownloadItem {entry} onRequestCancel={requestCancel} {showOrgName} />
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
Loader2 as LoadingIcon,
|
||||
Mic as AudioIcon,
|
||||
Package as ModelIcon,
|
||||
Pause as PauseDownloadIcon,
|
||||
Play as ResumeDownloadIcon,
|
||||
Power as LoadIcon,
|
||||
RotateCw as RetryIcon,
|
||||
Sparkles as PromptCommandIcon,
|
||||
@@ -104,6 +106,12 @@ export const CHAT_FORM_COMMAND_ICONS: Record<ChatFormCommandAction, Component> =
|
||||
};
|
||||
|
||||
/** Icons the model load control shows for its states and actions. */
|
||||
/** Icons of the download control a manager row shows while it downloads. */
|
||||
export const MODEL_DOWNLOAD_ICONS = {
|
||||
pause: PauseDownloadIcon,
|
||||
resume: ResumeDownloadIcon
|
||||
} as const;
|
||||
|
||||
export const MODEL_LOAD_ICONS = {
|
||||
failed: LoadFailedIcon,
|
||||
load: LoadIcon,
|
||||
|
||||
@@ -10,6 +10,7 @@ export type ModelGroupKind = (typeof ModelGroupKind)[keyof typeof ModelGroupKind
|
||||
|
||||
/** Kind of one collapsible block of the table. */
|
||||
export const ModelsTableGroupKind = {
|
||||
DOWNLOADING: 'downloading',
|
||||
FAVORITES: 'favorites',
|
||||
HIDDEN: 'hidden',
|
||||
LOADED: 'loaded',
|
||||
@@ -20,12 +21,22 @@ export type ModelsTableGroupKind = (typeof ModelsTableGroupKind)[keyof typeof Mo
|
||||
|
||||
/** Header label of each manager section. */
|
||||
export const MODELS_TABLE_GROUP_LABELS: Record<ModelsTableGroupKind, string> = {
|
||||
[ModelsTableGroupKind.DOWNLOADING]: 'Downloading',
|
||||
[ModelsTableGroupKind.FAVORITES]: 'Favorites',
|
||||
[ModelsTableGroupKind.HIDDEN]: 'Hidden models',
|
||||
[ModelsTableGroupKind.LOADED]: 'Loaded models',
|
||||
[ModelsTableGroupKind.LOCAL]: 'Local models'
|
||||
};
|
||||
|
||||
/** Download state a table row reports in place of its load state. */
|
||||
export const ModelRowDownloadState = {
|
||||
DOWNLOADING: 'downloading',
|
||||
PAUSED: 'paused'
|
||||
} as const;
|
||||
|
||||
export type ModelRowDownloadState =
|
||||
(typeof ModelRowDownloadState)[keyof typeof ModelRowDownloadState];
|
||||
|
||||
/** Column the manager's table can be ordered by. */
|
||||
export const ModelsTableSortKey = {
|
||||
CONTEXT: 'context',
|
||||
|
||||
Reference in New Issue
Block a user