This commit is contained in:
Timothy Jaeryang Baek
2026-07-14 02:37:21 -04:00
parent b9d5462ed0
commit d8131d1091
7 changed files with 116 additions and 152 deletions
@@ -13,6 +13,7 @@
export let showSetDefault = true;
export let triggerClassName = 'text-lg';
export let className = undefined;
let compareModels = selectedModels.length > 1;
@@ -69,6 +70,7 @@
model: model
}))}
{pinModelHandler}
{className}
{triggerClassName}
{showSetDefault}
onSetDefault={saveDefaultModel}
@@ -52,17 +52,17 @@
role="option"
aria-selected={isSelected}
aria-label={$i18n.t('Select {{modelName}} model', { modelName: item.label })}
class="flex group/item w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-[7px] pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl cursor-pointer data-highlighted:bg-muted {index ===
class="group/item flex h-8 w-full cursor-pointer select-none items-center rounded-xl px-2 text-left text-[13px] 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 {index ===
selectedModelIdx && !compareEnabled
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
: ''} {isSelected ? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent' : ''}"
? 'bg-gray-50/70 dark:bg-gray-800/60'
: ''} {isSelected ? 'bg-gray-50/70 dark:bg-gray-800/60' : ''}"
data-arrow-selected={index === selectedModelIdx}
data-value={item.value}
on:click={() => {
onClick();
}}
>
<div class="flex flex-col flex-1 gap-1.5">
<div class="flex flex-1 flex-col gap-1.5 overflow-hidden">
<!-- {#if (item?.model?.tags ?? []).length > 0}
<div
class="flex gap-0.5 self-center items-start h-full w-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
@@ -79,13 +79,13 @@
</div>
{/if} -->
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 overflow-hidden">
<div class="flex items-center min-w-fit">
<Tooltip content={$user?.role === 'admin' ? (item?.value ?? '') : ''} placement="top-start">
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${item.model.id}&lang=${$i18n.language}`}
alt={$i18n.t('{{modelName}} profile image', { modelName: item.label })}
class="rounded-full size-5 flex items-center"
class="flex size-4 items-center rounded-full"
loading="lazy"
on:error={(e) => {
e.currentTarget.src = '/favicon.png';
@@ -94,7 +94,7 @@
</Tooltip>
</div>
<div class="flex items-center">
<div class="flex min-w-0 items-center">
<Tooltip content={`${item.label} (${item.value})`} placement="top-start">
<div class="line-clamp-1">
{item.label}
@@ -102,7 +102,7 @@
</Tooltip>
</div>
<div class=" shrink-0 flex items-center gap-2">
<div class="flex shrink-0 items-center gap-1.5">
{#if item.model.owned_by === 'ollama'}
{#if (item.model.ollama?.details?.parameter_size ?? '') !== ''}
<div class="flex items-center translate-y-[0.5px]">
@@ -118,7 +118,7 @@
}`}
className="self-end"
>
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
<span class="line-clamp-1 text-[11px] font-normal text-gray-500 dark:text-gray-400"
>{item.model.ollama?.details?.parameter_size ?? ''}</span
>
</Tooltip>
@@ -127,7 +127,7 @@
{/if}
{#if item.model.loaded}
<div class="flex items-center translate-y-[0.5px] px-0.5">
<div class="flex items-center px-0.5">
<Tooltip
content={item.model.ollama?.expires_at &&
new Date(item.model.ollama?.expires_at * 1000) > new Date()
@@ -138,11 +138,11 @@
className="self-end"
>
<div class=" flex items-center">
<span class="relative flex size-2">
<span class="relative flex size-1.5">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
/>
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
<span class="relative inline-flex size-1.5 rounded-full bg-green-500" />
</span>
</div>
</Tooltip>
@@ -240,7 +240,7 @@
</div>
</div>
<div class="ml-auto pl-2 pr-1 flex items-center gap-1.5 shrink-0">
<div class="ml-auto flex shrink-0 items-center gap-1.5 pl-2">
{#if !selectionOnly && $user?.role === 'admin' && item.model.loaded}
<Tooltip
content={`${$i18n.t('Eject')}`}
@@ -35,6 +35,8 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
import Keyframes from '$lib/components/icons/Keyframes.svelte';
import TagSelector from '$lib/components/workspace/common/TagSelector.svelte';
import ModelItem from './ModelItem.svelte';
@@ -61,15 +63,13 @@
[key: string]: any;
}[] = [];
export let className = 'w-[32rem]';
export let className = 'w-[22rem]';
export let triggerClassName = 'text-lg';
export let showSetDefault = false;
export let onSetDefault: () => Promise<void> | void = () => {};
export let pinModelHandler: (modelId: string) => void = () => {};
let tagsContainerElement;
let show = false;
let triggerElement: HTMLElement | null = null;
let contentElement: HTMLElement | null = null;
@@ -112,7 +112,8 @@
const target = e.target as Node;
if (
(triggerElement && triggerElement.contains(target)) ||
(contentElement && contentElement.contains(target))
(contentElement && contentElement.contains(target)) ||
((target as HTMLElement).closest?.('.model-selector-child-menu') ?? false)
) {
return;
}
@@ -146,6 +147,8 @@
let selectedTag = '';
let selectedConnectionType = '';
let selectedFilter = '';
let modelFilterItems = [];
let ollamaVersion = null;
let selectedModelIdx = 0;
@@ -243,6 +246,38 @@
resetView();
}
$: modelFilterItems = [
...(items.find((item) => item.model?.connection_type === 'local')
? [{ value: 'connection:local', label: $i18n.t('Local') }]
: []),
...(items.find((item) => item.model?.connection_type === 'external')
? [{ value: 'connection:external', label: $i18n.t('External') }]
: []),
...(items.find((item) => item.model?.direct)
? [{ value: 'connection:direct', label: $i18n.t('Direct') }]
: []),
...tags.map((tag) => ({ value: `tag:${tag}`, label: tag }))
];
$: selectedFilter = selectedConnectionType
? `connection:${selectedConnectionType}`
: selectedTag
? `tag:${selectedTag}`
: '';
const setModelFilter = (filterValue: string) => {
if (!filterValue) {
selectedConnectionType = '';
selectedTag = '';
} else if (filterValue.startsWith('connection:')) {
selectedConnectionType = filterValue.replace('connection:', '');
selectedTag = '';
} else if (filterValue.startsWith('tag:')) {
selectedConnectionType = '';
selectedTag = filterValue.replace('tag:', '');
}
};
const resetView = async () => {
await tick();
@@ -527,7 +562,7 @@
deleteModelTarget = null;
};
const ITEM_HEIGHT = 39;
const ITEM_HEIGHT = 32;
const OVERSCAN = 10;
let listScrollTop = 0;
@@ -536,7 +571,7 @@
$: visibleStart = Math.max(0, Math.floor(listScrollTop / ITEM_HEIGHT) - OVERSCAN);
$: visibleEnd = Math.min(
filteredItems.length,
Math.ceil((listScrollTop + 256) / ITEM_HEIGHT) + OVERSCAN
Math.ceil((listScrollTop + 288) / ITEM_HEIGHT) + OVERSCAN
);
</script>
@@ -603,18 +638,18 @@
<div
class="z-40 {$mobile
? `w-full`
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-2xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-hidden"
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl border border-gray-100 bg-white p-0.5 shadow-lg outline-hidden dark:border-gray-800 dark:bg-gray-850 dark:text-white"
transition:flyAndScale
>
<slot>
{#if searchEnabled}
<div class="flex items-center gap-2 px-3.5 pt-3 mb-1.5">
<Search className="size-4" strokeWidth="2.5" />
<div class="my-0.5 flex h-[1.6875rem] items-center gap-2">
<Search className="ml-2 size-3.5 shrink-0" strokeWidth="2" />
<input
id="model-search-input"
bind:value={searchValue}
class="w-full text-sm bg-transparent outline-hidden"
class="w-full bg-transparent text-[13px] font-normal outline-hidden placeholder:text-gray-400 dark:placeholder:text-gray-500"
placeholder={searchPlaceholder}
autocomplete="off"
aria-label={$i18n.t('Search In Models')}
@@ -642,124 +677,44 @@
}}
/>
{#if multipleEnabled}
<button
type="button"
class="shrink-0 rounded-full px-2 py-1 text-[11px] font-medium leading-none transition-colors duration-100 {compareEnabled
? 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200'
: 'text-gray-400 hover:bg-gray-50 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-800/40 dark:hover:text-gray-300'}"
aria-pressed={compareEnabled}
on:click={() => {
setCompareEnabled(!compareEnabled);
}}
>
{$i18n.t('Compare')}
</button>
{#if modelFilterItems.length > 0 || multipleEnabled}
<div class="flex min-w-0 shrink-0 items-center gap-0.5">
{#if multipleEnabled}
<Tooltip content={$i18n.t('Compare')}>
<button
type="button"
class="flex size-[1.375rem] shrink-0 items-center justify-center rounded-lg transition-colors duration-100 {compareEnabled
? 'bg-gray-100/60 text-gray-700 hover:bg-gray-100/60 dark:bg-gray-800/40 dark:text-gray-300 dark:hover:bg-gray-800/40'
: 'text-gray-500 hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200'}"
aria-label={$i18n.t('Compare')}
aria-pressed={compareEnabled}
on:click={() => {
setCompareEnabled(!compareEnabled);
}}
>
<Keyframes className="size-3" strokeWidth="2" />
</button>
</Tooltip>
{/if}
{#if modelFilterItems.length > 0}
<TagSelector
bind:value={selectedFilter}
placeholder={$i18n.t('All')}
align="end"
items={modelFilterItems}
triggerClass="relative flex h-[1.375rem] max-w-32 items-center gap-0.5 rounded-xl bg-transparent px-1.5 text-[11px] font-normal text-gray-400 transition-colors duration-100 hover:bg-gray-50/40 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-800/40 dark:hover:text-gray-300"
itemClass="flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] capitalize hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100"
contentClass="min-w-36 model-selector-child-menu"
onChange={setModelFilter}
/>
{/if}
</div>
{/if}
</div>
{/if}
<div class="px-2">
{#if tags && items.filter((item) => includeHidden || !(item.model?.info?.meta?.hidden ?? false)).length > 0}
<div
class=" flex w-full bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none font-[450] mb-0.5"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<div
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === '' &&
selectedConnectionType === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedTag === '' && selectedConnectionType === ''}
on:click={() => {
selectedConnectionType = '';
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
{/if}
{#if items.find((item) => item.model?.connection_type === 'local')}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'local'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'local'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'local';
}}
>
{$i18n.t('Local')}
</button>
{/if}
{#if items.find((item) => item.model?.connection_type === 'external')}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'external'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'external'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'external';
}}
>
{$i18n.t('External')}
</button>
{/if}
{#if items.find((item) => item.model?.direct)}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'direct'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'direct'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'direct';
}}
>
{$i18n.t('Direct')}
</button>
{/if}
{#each tags as tag}
<Tooltip content={tag}>
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === tag
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedTag === tag}
on:click={() => {
selectedConnectionType = '';
selectedTag = tag;
}}
>
{tag.length > 16 ? `${tag.slice(0, 16)}...` : tag}
</button>
</Tooltip>
{/each}
</div>
</div>
{/if}
</div>
<div class="px-2 group relative">
<div class="group relative">
{#if filteredItems.length === 0}
{#if items.length === 0 && $user?.role === 'admin'}
<div class="flex flex-col items-start justify-center py-6 px-4 text-start">
@@ -781,7 +736,7 @@
</div>
{:else}
<div class="">
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
<div class="block px-2 py-1 text-[13px] text-gray-700 dark:text-gray-100">
{$i18n.t('No results found')}
</div>
</div>
@@ -789,7 +744,7 @@
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="max-h-64 overflow-y-auto"
class="max-h-72 overflow-y-auto"
role="listbox"
aria-label={$i18n.t('Available models')}
bind:this={listContainer}
@@ -828,7 +783,7 @@
placement="top-start"
>
<button
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-[7px] pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl cursor-pointer data-highlighted:bg-muted"
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center rounded-xl px-2 text-[13px] 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"
on:click={() => {
pullModelHandler();
}}
@@ -844,7 +799,7 @@
{#each selectionOnly ? [] : Object.keys($MODEL_DOWNLOAD_POOL) as model}
<div
class="flex w-full justify-between font-medium select-none rounded-button py-[7px] pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 rounded-xl cursor-pointer data-highlighted:bg-muted"
class="flex min-h-[1.6875rem] w-full cursor-pointer select-none justify-between rounded-xl px-2 text-[13px] 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">
@@ -906,21 +861,23 @@
</div>
{#if showSetDefault}
<div class="px-3 pb-2 pt-1 text-left">
<div class="flex items-center justify-end px-2 py-1 leading-none">
<button
type="button"
class="text-[0.7rem] font-normal text-gray-500 underline-offset-2 transition-colors duration-100 hover:text-gray-700 hover:underline dark:text-gray-500 dark:hover:text-gray-300"
class="text-[0.65rem] font-normal leading-none text-gray-500 underline-offset-2 transition-colors duration-100 hover:text-gray-700 hover:underline dark:text-gray-500 dark:hover:text-gray-300"
on:click|stopPropagation={setDefaultHandler}
>
{$i18n.t('Set as default')}
</button>
</div>
{:else}
<div class="pb-2"></div>
<div class="pb-1"></div>
{/if}
<div class="hidden w-[42rem]" />
<div class="hidden w-[32rem]" />
<div class="hidden w-[28rem]" />
<div class="hidden w-[24rem]" />
<div class="hidden w-[22rem]" />
</slot>
</div>
</div>
+1 -1
View File
@@ -96,7 +96,7 @@
: 'invisible'} bg-linear-to-b via-40% to-97% from-white/90 via-white/50 to-transparent dark:from-gray-900/90 dark:via-gray-900/50 dark:to-transparent pointer-events-none absolute inset-0 -bottom-10 z-[-1]"
></div>
<div class=" flex max-w-full w-full mx-auto px-1.5 md:px-2 pt-0.5 bg-transparent">
<div class=" flex max-w-full w-full mx-auto px-1.5 md:px-2 bg-transparent">
<div class="flex items-center w-full max-w-full">
{#if $mobile && !$showSidebar}
<div
@@ -127,7 +127,7 @@
localStorage.setItem(`${id}-folder-state`, `${state}`);
}}
>
<div class="flex items-center justify-between h-8 w-full pl-3.5 pr-1.5 shrink-0">
<div class="flex items-center justify-between h-6 w-full pl-3.5 pr-1.5 shrink-0">
<button
type="button"
class="group flex flex-1 h-full items-center gap-1 text-left text-xs text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 transition-colors duration-100 {buttonClassName}"
@@ -655,7 +655,6 @@
placeholder={$i18n.t('Select a base model (e.g. llama3, gpt-4o)')}
searchPlaceholder={$i18n.t('Search a model')}
items={getBaseModelItems($models)}
className="w-[32rem]"
triggerClassName="text-sm"
selectionOnly
includeHidden={$user?.role === 'admin'}
@@ -12,6 +12,11 @@
export let placeholder = $i18n.t('Tag');
export let align = 'start';
export let onChange: (value: string) => void = () => {};
export let triggerClass =
'relative h-8 w-full flex items-center gap-0.5 px-1.5 py-1.5 bg-transparent rounded-xl text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100';
export let itemClass =
'flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] capitalize hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100';
export let contentClass = 'min-w-[170px]';
export let items = [];
</script>
@@ -21,16 +26,17 @@
{items}
{placeholder}
{align}
triggerClass="relative h-8 w-full flex items-center gap-0.5 px-1.5 py-1.5 bg-transparent rounded-xl text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
itemClass="flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] capitalize hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100"
{triggerClass}
{itemClass}
{contentClass}
onChange={() => onChange(value)}
>
<svelte:fragment slot="trigger" let:selectedLabel>
<div
class="inline-flex h-input w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden capitalize"
class="inline-flex h-input min-w-0 flex-1 outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden capitalize"
>
{#if value}
{value}
{selectedLabel}
{:else}
{placeholder}
{/if}