mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-02 12:14:48 +02:00
refac
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
export let selectedValues: string[] = [];
|
||||
export let compareEnabled = false;
|
||||
|
||||
export let unloadModelHandler: (modelValue: string) => void = () => {};
|
||||
export let unloadModelHandler: (model: any) => void = () => {};
|
||||
export let pinModelHandler: (modelId: string) => void = () => {};
|
||||
export let deleteModelHandler: (model: any) => void = () => {};
|
||||
export let selectionOnly = false;
|
||||
@@ -44,6 +44,8 @@
|
||||
}
|
||||
};
|
||||
|
||||
const formatSize = (size?: number) => (size ? `(${(size / 1024 ** 3).toFixed(1)}GB)` : '');
|
||||
|
||||
let showMenu = false;
|
||||
$: isSelected = compareEnabled ? selectedValues.includes(item.value) : value === item.value;
|
||||
</script>
|
||||
@@ -128,6 +130,23 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if item.model.provider === 'lmstudio' || item.model.provider === 'llama.cpp'}
|
||||
{@const parameterSize = item.model.params_string ?? item.model.details?.parameter_size ?? ''}
|
||||
{@const quantization =
|
||||
item.model.quantization?.name ?? item.model.details?.quantization_level ?? ''}
|
||||
{@const size = item.model.size_bytes ?? item.model.size}
|
||||
{#if parameterSize || quantization || size}
|
||||
<div class="flex items-center translate-y-[0.5px]">
|
||||
<Tooltip
|
||||
content={`${quantization ? `${quantization} ` : ''}${formatSize(size)}`}
|
||||
className="self-end"
|
||||
>
|
||||
<span class="line-clamp-1 text-[0.6875rem] font-normal text-gray-500 dark:text-gray-400">
|
||||
{parameterSize || quantization || formatSize(size)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if item.model.loaded}
|
||||
@@ -256,7 +275,7 @@
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
unloadModelHandler(item.value);
|
||||
unloadModelHandler(item.model);
|
||||
}}
|
||||
>
|
||||
<ArrowUpTray className="size-3" />
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
export let deleteModelHandler: Function = () => {};
|
||||
|
||||
export let onClose: Function = () => {};
|
||||
|
||||
const providerSupportsDelete = (provider = '') => provider === 'llama.cpp';
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -66,7 +68,7 @@
|
||||
<div class="flex items-center">{$i18n.t('Edit')}</div>
|
||||
</button>
|
||||
|
||||
{#if $user?.role === 'admin' && model?.owned_by === 'ollama'}
|
||||
{#if $user?.role === 'admin' && (model?.owned_by === 'ollama' || providerSupportsDelete(model?.provider))}
|
||||
<button
|
||||
type="button"
|
||||
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
|
||||
import { deleteModelById } from '$lib/apis/models';
|
||||
import { unloadModel } from '$lib/apis';
|
||||
import {
|
||||
downloadProviderModel,
|
||||
getErrorMessage,
|
||||
getOpenAIConfig,
|
||||
getProviderModelDownloadStatus
|
||||
} from '$lib/apis/openai';
|
||||
|
||||
import {
|
||||
user,
|
||||
@@ -32,6 +38,7 @@
|
||||
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import Download from '$lib/components/icons/Download.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
@@ -190,6 +197,10 @@
|
||||
if (show) {
|
||||
searchValue = '';
|
||||
listScrollTop = 0;
|
||||
if (!selectionOnly) {
|
||||
setOllamaVersion();
|
||||
setProviderDownloadConnections();
|
||||
}
|
||||
resetView();
|
||||
updatePosition();
|
||||
await tick();
|
||||
@@ -252,8 +263,27 @@
|
||||
let modelFilterItems = [];
|
||||
|
||||
let ollamaVersion = null;
|
||||
let providerDownloadConnections = [];
|
||||
let selectedModelIdx = 0;
|
||||
|
||||
const MANAGEMENT_PROVIDERS = new Set(['llama.cpp', 'lmstudio']);
|
||||
|
||||
const normalizeProvider = (provider = '') => {
|
||||
const value = provider.trim().toLowerCase();
|
||||
if (value === 'lm studio' || value === 'lm-studio') return 'lmstudio';
|
||||
return value;
|
||||
};
|
||||
|
||||
const getProviderLabel = (provider = '') => {
|
||||
const normalizedProvider = normalizeProvider(provider);
|
||||
if (normalizedProvider === 'lmstudio') return $i18n.t('LM Studio');
|
||||
if (normalizedProvider === 'llama.cpp') return $i18n.t('llama.cpp');
|
||||
return provider;
|
||||
};
|
||||
|
||||
const getProviderPoolKey = (connection, model: string) =>
|
||||
`${connection.provider}:${connection.idx}:${model}`;
|
||||
|
||||
const fuse = new Fuse(
|
||||
items.map((item) => {
|
||||
const _item = {
|
||||
@@ -339,13 +369,41 @@
|
||||
})
|
||||
).filter((item) => includeHidden || !(item.model?.info?.meta?.hidden ?? false));
|
||||
|
||||
$: showPullModelButton = !!(
|
||||
!selectionOnly &&
|
||||
!(searchValue.trim() in $MODEL_DOWNLOAD_POOL) &&
|
||||
searchValue &&
|
||||
ollamaVersion &&
|
||||
$user?.role === 'admin'
|
||||
);
|
||||
$: sanitizedSearchValue = searchValue.trim();
|
||||
$: downloadTargets = !selectionOnly && sanitizedSearchValue && $user?.role === 'admin'
|
||||
? [
|
||||
...(ollamaVersion
|
||||
? [
|
||||
{
|
||||
id: 'ollama',
|
||||
label: $i18n.t('Ollama'),
|
||||
poolKey: sanitizedSearchValue,
|
||||
download: $MODEL_DOWNLOAD_POOL[sanitizedSearchValue],
|
||||
actionLabel: $i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
|
||||
searchValue: searchValue
|
||||
}),
|
||||
type: 'ollama'
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...providerDownloadConnections.map((connection) => {
|
||||
const poolKey = getProviderPoolKey(connection, sanitizedSearchValue);
|
||||
return {
|
||||
...connection,
|
||||
id: `${connection.provider}:${connection.idx}`,
|
||||
label: getProviderLabel(connection.provider),
|
||||
poolKey,
|
||||
download: $MODEL_DOWNLOAD_POOL[poolKey],
|
||||
actionLabel: $i18n.t(`Download "{{searchValue}}" from {{provider}}`, {
|
||||
searchValue: searchValue,
|
||||
provider: getProviderLabel(connection.provider)
|
||||
}),
|
||||
type: 'provider'
|
||||
};
|
||||
})
|
||||
]
|
||||
: [];
|
||||
$: activeDownloadKeys = new Set(downloadTargets.filter((target) => target.download).map((target) => target.poolKey));
|
||||
|
||||
$: if (
|
||||
selectedTag !== undefined ||
|
||||
@@ -491,13 +549,14 @@
|
||||
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[sanitizedModelTag]: {
|
||||
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||
abortController: controller,
|
||||
reader,
|
||||
done: false
|
||||
}
|
||||
});
|
||||
[sanitizedModelTag]: {
|
||||
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||
abortController: controller,
|
||||
reader,
|
||||
model: sanitizedModelTag,
|
||||
done: false
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@@ -554,7 +613,7 @@
|
||||
error = error.message;
|
||||
}
|
||||
|
||||
toast.error(`${error}`);
|
||||
toast.error(getErrorMessage(error));
|
||||
// opts.callback({ success: false, error, modelName: opts.modelName });
|
||||
break;
|
||||
}
|
||||
@@ -589,6 +648,157 @@
|
||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||
};
|
||||
|
||||
const downloadProviderModelHandler = async (connection) => {
|
||||
const model = sanitizedSearchValue;
|
||||
const poolKey = getProviderPoolKey(connection, model);
|
||||
|
||||
if ($MODEL_DOWNLOAD_POOL[poolKey]) {
|
||||
toast.error(
|
||||
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
||||
modelTag: model
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (Object.keys($MODEL_DOWNLOAD_POOL).length === 3) {
|
||||
toast.error(
|
||||
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[poolKey]: {
|
||||
abortController: controller,
|
||||
model,
|
||||
providerLabel: getProviderLabel(connection.provider),
|
||||
done: false
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await downloadProviderModel(localStorage.token, connection.idx, model, controller.signal);
|
||||
const jobId = res?.job_id;
|
||||
|
||||
if (res?.status) {
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[poolKey]: {
|
||||
...$MODEL_DOWNLOAD_POOL[poolKey],
|
||||
digest: res.status,
|
||||
done: ['completed', 'already_downloaded'].includes(res.status)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (jobId) {
|
||||
while (!controller.signal.aborted) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
if (controller.signal.aborted) break;
|
||||
|
||||
const status = await getProviderModelDownloadStatus(
|
||||
localStorage.token,
|
||||
connection.idx,
|
||||
jobId,
|
||||
controller.signal
|
||||
);
|
||||
const total = status?.total_size_bytes ?? 0;
|
||||
const downloaded = status?.downloaded_bytes ?? 0;
|
||||
const pullProgress = total ? Math.round((downloaded / total) * 1000) / 10 : undefined;
|
||||
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[poolKey]: {
|
||||
...$MODEL_DOWNLOAD_POOL[poolKey],
|
||||
...(pullProgress !== undefined ? { pullProgress } : {}),
|
||||
digest: status?.status ?? ''
|
||||
}
|
||||
});
|
||||
|
||||
if (status?.status === 'completed') {
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[poolKey]: {
|
||||
...$MODEL_DOWNLOAD_POOL[poolKey],
|
||||
pullProgress: 100,
|
||||
done: true
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (status?.status === 'failed') {
|
||||
throw status?.error ?? 'Download failed';
|
||||
}
|
||||
}
|
||||
} else if (!$MODEL_DOWNLOAD_POOL[poolKey]?.done) {
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[poolKey]: {
|
||||
...$MODEL_DOWNLOAD_POOL[poolKey],
|
||||
pullProgress: 100,
|
||||
done: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($MODEL_DOWNLOAD_POOL[poolKey]?.done) {
|
||||
toast.success(
|
||||
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
|
||||
modelName: model
|
||||
})
|
||||
);
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
toast.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
delete $MODEL_DOWNLOAD_POOL[poolKey];
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL
|
||||
});
|
||||
};
|
||||
|
||||
const downloadModelHandler = (target) => {
|
||||
if (target.type === 'ollama') {
|
||||
pullModelHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
downloadProviderModelHandler(target);
|
||||
};
|
||||
|
||||
const setProviderDownloadConnections = async () => {
|
||||
const openaiConfig = await getOpenAIConfig(localStorage.token).catch(() => null);
|
||||
providerDownloadConnections =
|
||||
openaiConfig?.ENABLE_OPENAI_API
|
||||
? (openaiConfig.OPENAI_API_BASE_URLS ?? [])
|
||||
.map((url: string, idx: number) => {
|
||||
const config =
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[idx] ??
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[String(idx)] ??
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[url] ??
|
||||
{};
|
||||
|
||||
return {
|
||||
idx,
|
||||
url,
|
||||
provider: normalizeProvider(config?.provider ?? '')
|
||||
};
|
||||
})
|
||||
.filter((connection) => MANAGEMENT_PROVIDERS.has(connection.provider))
|
||||
: [];
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (items) {
|
||||
tags = items
|
||||
@@ -612,12 +822,8 @@
|
||||
};
|
||||
});
|
||||
|
||||
$: if (show && !selectionOnly) {
|
||||
setOllamaVersion();
|
||||
}
|
||||
|
||||
const cancelModelPullHandler = async (model: string) => {
|
||||
const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
|
||||
const { reader, abortController, providerLabel } = $MODEL_DOWNLOAD_POOL[model];
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
@@ -629,6 +835,17 @@
|
||||
});
|
||||
await deleteModel(localStorage.token, model);
|
||||
toast.success($i18n.t('{{model}} download has been canceled', { model: model }));
|
||||
} else {
|
||||
const displayModel = $MODEL_DOWNLOAD_POOL[model]?.model ?? model;
|
||||
delete $MODEL_DOWNLOAD_POOL[model];
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL
|
||||
});
|
||||
toast.success(
|
||||
$i18n.t('{{model}} download has been canceled', {
|
||||
model: providerLabel ? `${displayModel} (${providerLabel})` : displayModel
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -810,21 +1027,24 @@
|
||||
class="w-full bg-transparent text-[0.8125rem] font-normal outline-hidden placeholder:text-gray-400 dark:placeholder:text-gray-500"
|
||||
placeholder={searchPlaceholder}
|
||||
autocomplete="off"
|
||||
aria-label={$i18n.t('Search In Models')}
|
||||
on:keydown={(e) => {
|
||||
if (e.code === 'Enter') {
|
||||
if (showPullModelButton && selectedModelIdx === filteredItems.length) {
|
||||
pullModelHandler();
|
||||
} else if (filteredItems[selectedModelIdx]) {
|
||||
selectItem(filteredItems[selectedModelIdx], selectedModelIdx);
|
||||
}
|
||||
aria-label={$i18n.t('Search In Models')}
|
||||
on:keydown={(e) => {
|
||||
if (e.code === 'Enter') {
|
||||
if (selectedModelIdx >= filteredItems.length) {
|
||||
const target = downloadTargets[selectedModelIdx - filteredItems.length];
|
||||
if (target && !target.download) {
|
||||
downloadModelHandler(target);
|
||||
}
|
||||
} else if (filteredItems[selectedModelIdx]) {
|
||||
selectItem(filteredItems[selectedModelIdx], selectedModelIdx);
|
||||
}
|
||||
return; // dont need to scroll on selection
|
||||
} else if (e.code === 'ArrowDown') {
|
||||
e.stopPropagation();
|
||||
selectedModelIdx = Math.min(
|
||||
selectedModelIdx + 1,
|
||||
Math.max(filteredItems.length - 1 + (showPullModelButton ? 1 : 0), 0)
|
||||
);
|
||||
e.stopPropagation();
|
||||
selectedModelIdx = Math.min(
|
||||
selectedModelIdx + 1,
|
||||
Math.max(filteredItems.length - 1 + downloadTargets.length, 0)
|
||||
);
|
||||
} else if (e.code === 'ArrowUp') {
|
||||
e.stopPropagation();
|
||||
selectedModelIdx = Math.max(selectedModelIdx - 1, 0);
|
||||
@@ -904,12 +1124,12 @@
|
||||
{$i18n.t('Manage Connections')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="">
|
||||
<div class="block px-2 py-1 text-[0.8125rem] text-gray-700 dark:text-gray-100">
|
||||
{$i18n.t('No results found')}
|
||||
{:else}
|
||||
<div class="">
|
||||
<div class="flex min-h-8 items-center rounded-xl px-2 text-[0.8125rem] text-gray-700 dark:text-gray-100">
|
||||
{$i18n.t('No results found')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
@@ -947,76 +1167,108 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showPullModelButton}
|
||||
<Tooltip
|
||||
content={$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
|
||||
searchValue: searchValue
|
||||
})}
|
||||
placement="top-start"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selectedModelIdx === filteredItems.length}
|
||||
data-arrow-selected={selectedModelIdx === filteredItems.length}
|
||||
class="focus-ring flex h-[1.6875rem] w-full cursor-pointer select-none items-center rounded-xl px-2 text-[0.8125rem] font-normal text-gray-700 outline-hidden transition-colors duration-75 hover:bg-gray-50/40 dark:text-gray-100 dark:hover:bg-gray-800/40 {selectedModelIdx ===
|
||||
filteredItems.length
|
||||
? 'bg-gray-50/70 dark:bg-gray-800/60'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" truncate">
|
||||
{$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
|
||||
searchValue: searchValue
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#each selectionOnly ? [] : Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||
<div
|
||||
class="flex min-h-[1.6875rem] w-full cursor-pointer select-none justify-between rounded-xl px-2 text-[0.8125rem] font-normal text-gray-700 outline-hidden transition-colors duration-75 dark:text-gray-100"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="mr-2.5 translate-y-0.5">
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col self-start">
|
||||
<div class="flex gap-1">
|
||||
<div class="line-clamp-1">
|
||||
Downloading "{model}"
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
{'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
|
||||
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
|
||||
: ''}
|
||||
{#each downloadTargets as target, targetIndex (target.id)}
|
||||
{#if target.download}
|
||||
<Tooltip content={target.download?.digest ?? ''} placement="top-start">
|
||||
<div
|
||||
role="option"
|
||||
aria-selected={selectedModelIdx === filteredItems.length + targetIndex}
|
||||
data-arrow-selected={selectedModelIdx === filteredItems.length + targetIndex}
|
||||
class="flex h-8 w-full select-none items-center gap-2 rounded-xl px-2 text-left text-[0.8125rem] font-normal text-gray-700 outline-hidden transition-colors duration-75 dark:text-gray-100 {selectedModelIdx ===
|
||||
filteredItems.length + targetIndex
|
||||
? 'bg-gray-50/70 dark:bg-gray-800/60'
|
||||
: ''}"
|
||||
>
|
||||
<Spinner className="size-3 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<div class="min-w-0 flex-1 truncate">
|
||||
{$i18n.t('Downloading "{{searchValue}}"', { searchValue: searchValue })}
|
||||
</div>
|
||||
{#if 'pullProgress' in target.download}
|
||||
<div class="shrink-0 text-[0.6875rem] tabular-nums text-gray-500 dark:text-gray-400">
|
||||
{target.download.pullProgress}%
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="focus-ring flex size-4 shrink-0 items-center justify-center rounded text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Cancel download of {{model}}', { model: searchValue })}
|
||||
on:click|stopPropagation={() => {
|
||||
cancelModelPullHandler(target.poolKey);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="size-2.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2.5"
|
||||
d="M6 18 17.94 6M18 18 6.06 6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<Tooltip content={target.actionLabel} placement="top-start">
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selectedModelIdx === filteredItems.length + targetIndex}
|
||||
data-arrow-selected={selectedModelIdx === filteredItems.length + targetIndex}
|
||||
class="focus-ring flex h-8 w-full cursor-pointer select-none items-center gap-2 rounded-xl px-2 text-left text-[0.8125rem] font-normal text-gray-700 outline-hidden transition-colors duration-75 hover:bg-gray-50/40 dark:text-gray-100 dark:hover:bg-gray-800/40 {selectedModelIdx ===
|
||||
filteredItems.length + targetIndex
|
||||
? 'bg-gray-50/70 dark:bg-gray-800/60'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
downloadModelHandler(target);
|
||||
}}
|
||||
>
|
||||
<Download className="size-3.5 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<div class="min-w-0 flex-1 truncate">
|
||||
{$i18n.t('Download "{{searchValue}}"', { searchValue: searchValue })}
|
||||
</div>
|
||||
<div class="shrink-0 truncate text-[0.6875rem] text-gray-500 dark:text-gray-400">
|
||||
{target.label}
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
|
||||
<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1">
|
||||
{$MODEL_DOWNLOAD_POOL[model].digest}
|
||||
{#each selectionOnly ? [] : Object.keys($MODEL_DOWNLOAD_POOL).filter((model) => !activeDownloadKeys.has(model)) as model}
|
||||
{@const download = $MODEL_DOWNLOAD_POOL[model]}
|
||||
{@const downloadName = download?.model ?? model}
|
||||
<Tooltip content={download?.digest ?? ''} placement="top-start">
|
||||
<div
|
||||
class="flex h-8 w-full select-none items-center gap-2 rounded-xl px-2 text-left text-[0.8125rem] font-normal text-gray-700 outline-hidden transition-colors duration-75 dark:text-gray-100"
|
||||
>
|
||||
<Spinner className="size-3 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<div class="min-w-0 flex-1 truncate">
|
||||
Downloading "{downloadName}"{download?.providerLabel
|
||||
? ` from ${download.providerLabel}`
|
||||
: ''}
|
||||
</div>
|
||||
{#if 'pullProgress' in download}
|
||||
<div class="shrink-0 text-[0.6875rem] tabular-nums text-gray-500 dark:text-gray-400">
|
||||
{download.pullProgress}%
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mr-2 ml-1 translate-y-0.5">
|
||||
<Tooltip content={$i18n.t('Cancel')}>
|
||||
<button
|
||||
class="focus-ring text-gray-800 dark:text-gray-100"
|
||||
aria-label={$i18n.t('Cancel download of {{model}}', { model: model })}
|
||||
on:click={() => {
|
||||
class="focus-ring flex size-4 shrink-0 items-center justify-center rounded text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Cancel download of {{model}}', { model: downloadName })}
|
||||
on:click|stopPropagation={() => {
|
||||
cancelModelPullHandler(model);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-800 dark:text-white"
|
||||
class="size-2.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -1028,15 +1280,14 @@
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
stroke-width="2.5"
|
||||
d="M6 18 17.94 6M18 18 6.06 6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if showSetDefault}
|
||||
|
||||
Reference in New Issue
Block a user