This commit is contained in:
Timothy Jaeryang Baek 2026-07-15 01:16:06 -04:00
parent c1460570b7
commit 43e792a8f4
5 changed files with 345 additions and 314 deletions

View file

@ -394,9 +394,11 @@
<div class="flex-1 min-h-0 p-4 lg:px-5 flex flex-col">
<div class="w-full h-full min-h-0 flex flex-col">
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
{$i18n.t(selectedTabTitle)}
</h2>
{#if selectedTab !== 'models'}
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
{$i18n.t(selectedTabTitle)}
</h2>
{/if}
<div class="flex-1 min-h-0 overflow-hidden">
{#if selectedTab === 'general'}

View file

@ -31,15 +31,13 @@
import { toast } from 'svelte-sonner';
import Badge from '$lib/components/common/Badge.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Cog6 from '$lib/components/icons/Cog6.svelte';
import ModelSettingsModal from './Models/ModelSettingsModal.svelte';
import Wrench from '$lib/components/icons/Wrench.svelte';
import Download from '$lib/components/icons/Download.svelte';
import ManageModelsModal from './Models/ManageModelsModal.svelte';
import ModelMenu from '$lib/components/admin/Settings/Models/ModelMenu.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
import Eye from '$lib/components/icons/Eye.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
import Minus from '$lib/components/icons/Minus.svelte';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
@ -58,6 +56,7 @@
let modelsImportInProgress = false;
let importFiles;
let modelsImportInputElement: HTMLInputElement;
let tagsContainerElement: HTMLDivElement;
let models = null;
@ -380,316 +379,334 @@
{#if models !== null}
{#if selectedModelId === null}
<div class="flex flex-col gap-1 mt-1.5 mb-2">
<div class="flex justify-between items-center">
<div
class="flex items-center md:self-center text-xs text-gray-400 dark:text-gray-600 px-0.5 gap-1.5 shrink-0"
>
<div>{filteredModels.length}</div>
</div>
<div class="flex h-full min-h-0 flex-col text-sm">
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm font-medium text-gray-900 dark:text-white">
{$i18n.t('Models')}
<span class="ml-2 font-normal text-gray-500 dark:text-gray-500">
{filteredModels.length}
</span>
</h2>
<div class="flex w-full justify-end gap-1.5">
<div class="flex items-center gap-2">
{#if $user?.role === 'admin'}
<input
id="models-import-input"
bind:this={modelsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
if (importFiles.length > 0) {
const reader = new FileReader();
reader.onload = async (event) => {
modelsImportInProgress = true;
try {
const models = JSON.parse(String(event.target.result));
const res = await importModels(localStorage.token, models);
if (res) {
toast.success($i18n.t('Models imported successfully'));
await init();
} else {
toast.error($i18n.t('Failed to import models'));
}
} catch (e) {
toast.error(e?.detail ?? $i18n.t('Invalid JSON file'));
console.error(e);
}
modelsImportInProgress = false;
};
reader.readAsText(importFiles[0]);
}
}}
/>
<button
class="flex text-xs items-center space-x-1 px-2.5 py-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 dark:text-gray-200 transition"
type="button"
class="text-[0.625rem] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100 disabled:opacity-30 disabled:pointer-events-none"
disabled={modelsImportInProgress}
on:click={() => {
modelsImportInputElement.click();
modelsImportInputElement?.click();
}}
>
{#if modelsImportInProgress}
<Spinner className="size-3" />
{/if}
<div class=" self-center font-normal line-clamp-1">
{$i18n.t('Import')}
</div>
{$i18n.t('Import')}
</button>
<button
class="flex text-xs items-center space-x-1 px-2.5 py-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 dark:text-gray-200 transition"
on:click={async () => {
downloadModels(models);
type="button"
class="text-[0.625rem] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100"
on:click={() => {
downloadModels(models ?? []);
}}
>
<div class=" self-center font-normal line-clamp-1">
{$i18n.t('Export')}
</div>
{$i18n.t('Export')}
</button>
{/if}
<button
class="flex text-xs items-center space-x-1 px-2.5 py-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 dark:text-gray-200 transition"
type="button"
class="text-[0.625rem] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100"
on:click={() => {
showManageModal = true;
}}
>
<div class=" self-center font-normal line-clamp-1">
{$i18n.t('Manage')}
</div>
{$i18n.t('Manage')}
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black transition font-normal"
type="button"
class="text-[0.625rem] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100"
on:click={() => {
showConfigModal = true;
}}
>
<div class=" self-center font-normal line-clamp-1">
{$i18n.t('Settings')}
</div>
{$i18n.t('Settings')}
</button>
</div>
</div>
</div>
<div class="py-1">
<div class="flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
<div class="flex flex-1 items-center">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
</div>
<input
class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={searchValue}
placeholder={$i18n.t('Search Models')}
/>
{#if searchValue}
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
<button
class="p-0.5 rounded-sm hover:bg-black/5 dark:hover:bg-white/5 transition"
on:click={() => {
searchValue = '';
}}
>
<XMark className="size-3" strokeWidth="2" />
</button>
{#if $user?.role === 'admin'}
<input
id="models-import-input"
bind:this={modelsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
if (importFiles.length > 0) {
const reader = new FileReader();
reader.onload = async (event) => {
modelsImportInProgress = true;
try {
const models = JSON.parse(String(event.target.result));
const res = await importModels(localStorage.token, models);
if (res) {
toast.success($i18n.t('Models imported successfully'));
await init();
} else {
toast.error($i18n.t('Failed to import models'));
}
} catch (e) {
toast.error(e?.detail ?? $i18n.t('Invalid JSON file'));
console.error(e);
}
modelsImportInProgress = false;
};
reader.readAsText(importFiles[0]);
}
}}
/>
{/if}
<div class="flex min-h-0 flex-1 flex-col space-y-1">
<div class="flex h-8 shrink-0 items-center w-full gap-2">
<div class="flex min-w-0 flex-1 items-center">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
</div>
{/if}
</div>
</div>
<div class="flex w-full items-center bg-transparent overflow-x-auto scrollbar-none">
<div
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent whitespace-nowrap"
>
<AdminViewSelector bind:value={viewOption} />
{#if (tags ?? []).length > 0}
<TagSelector
bind:value={selectedTag}
items={tags.map((tag) => ({ value: tag, label: tag }))}
onChange={async () => {
currentPage = 1;
await init();
}}
<input
class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={searchValue}
placeholder={$i18n.t('Search Models')}
/>
{/if}
{#if searchValue}
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
<button
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
aria-label={$i18n.t('Clear search')}
on:click={() => {
searchValue = '';
}}
>
<XMark className="size-3" strokeWidth="2" />
</button>
</div>
{/if}
</div>
<div
class="flex max-w-[60%] shrink-0 items-center gap-1 overflow-x-auto scrollbar-none"
bind:this={tagsContainerElement}
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<div
class="flex w-fit gap-0.5 text-center text-sm rounded-full bg-transparent whitespace-nowrap"
>
<AdminViewSelector bind:value={viewOption} align="end" />
{#if (tags ?? []).length > 0}
<TagSelector
bind:value={selectedTag}
align="end"
items={tags.map((tag) => {
return { value: tag, label: tag };
})}
onChange={async () => {
currentPage = 1;
await init();
}}
/>
{/if}
</div>
<Dropdown align="end">
<Tooltip content={$i18n.t('Actions')}>
<button
class="flex h-8 items-center gap-1.5 rounded-xl bg-transparent px-1.5 text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
type="button"
>
<span>{$i18n.t('Actions')}</span>
<ChevronDown className="size-3" strokeWidth="2.5" />
</button>
</Tooltip>
<div slot="content">
<DropdownMenu className="w-[170px] shadow-sm">
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
type="button"
on:click={() => {
enableAllHandler();
}}
>
<CheckCircle className="size-3.5" />
<div class="flex items-center">{$i18n.t('Enable All')}</div>
</button>
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
type="button"
on:click={() => {
disableAllHandler();
}}
>
<Minus className="size-3.5" />
<div class="flex items-center">{$i18n.t('Disable All')}</div>
</button>
<hr class="mx-1 my-0.5 border-gray-100 dark:border-gray-800" />
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
type="button"
on:click={() => {
showAllHandler();
}}
>
<Eye className="size-3.5" />
<div class="flex items-center">{$i18n.t('Show All')}</div>
</button>
<button
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
type="button"
on:click={() => {
hideAllHandler();
}}
>
<EyeSlash className="size-3.5" />
<div class="flex items-center">{$i18n.t('Hide All')}</div>
</button>
</DropdownMenu>
</div>
</Dropdown>
</div>
</div>
<div class="flex-1"></div>
<Dropdown>
<Tooltip content={$i18n.t('Actions')}>
<button
class="p-1 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition"
type="button"
>
<EllipsisHorizontal className="size-3.5" />
</button>
</Tooltip>
<div slot="content">
<DropdownMenu className="w-[170px]">
<button
class="select-none flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl"
type="button"
on:click={() => {
enableAllHandler();
}}
<div
class="my-0.5 min-h-0 flex-1 space-y-px {filteredModels.length > 0
? 'overflow-y-auto scrollbar-hover pr-1.5'
: 'overflow-hidden'}"
id="model-list"
>
{#if filteredModels.length > 0}
{#each filteredModels.slice((currentPage - 1) * perPage, currentPage * perPage) as model, modelIdx (`${model.id}-${modelIdx}`)}
<div
class="flex cursor-pointer transition w-full px-2 py-1 rounded-xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50 {model
?.meta?.hidden
? 'opacity-50 dark:opacity-50'
: ''}"
id="model-item-{model.id}"
>
<CheckCircle className="size-3.5" />
<div class="flex items-center">{$i18n.t('Enable All')}</div>
</button>
<button
class="select-none flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl"
type="button"
on:click={() => {
disableAllHandler();
}}
>
<Minus className="size-3.5" />
<div class="flex items-center">{$i18n.t('Disable All')}</div>
</button>
<hr class="border-gray-100/20 dark:border-white/[0.02] my-1" />
<button
class="select-none flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl"
type="button"
on:click={() => {
showAllHandler();
}}
>
<Eye className="size-3.5" />
<div class="flex items-center">{$i18n.t('Show All')}</div>
</button>
<button
class="select-none flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl"
type="button"
on:click={() => {
hideAllHandler();
}}
>
<EyeSlash className="size-3.5" />
<div class="flex items-center">{$i18n.t('Hide All')}</div>
</button>
</DropdownMenu>
</div>
</Dropdown>
</div>
<div class="my-2" id="model-list">
{#if filteredModels.length > 0}
{#each filteredModels.slice((currentPage - 1) * perPage, currentPage * perPage) as model, modelIdx (`${model.id}-${modelIdx}`)}
<div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition {model
?.meta?.hidden
? 'opacity-50 dark:opacity-50'
: ''}"
id="model-item-{model.id}"
>
<button
class=" flex flex-1 text-left space-x-3.5 cursor-pointer w-full"
type="button"
on:click={() => {
selectedModelId = model.id;
}}
>
<div class=" self-center w-9">
<div
class=" rounded-full object-cover {(model?.is_active ?? true)
? ''
: 'opacity-50 dark:opacity-50'} "
>
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}`}
alt="modelfile profile"
class=" rounded-full w-full h-auto object-cover"
on:error={(e) => {
e.target.src = '/favicon.png';
}}
/>
</div>
</div>
<div
class=" flex-1 self-center {(model?.is_active ?? true) ? '' : 'text-gray-500'}"
<button
class="flex group/item gap-2.5 w-full min-w-0 flex-1 text-left cursor-pointer"
type="button"
on:click={() => {
selectedModelId = model.id;
}}
>
<Tooltip
content={marked.parse(
!!model?.meta?.description
? model?.meta?.description
: model?.ollama?.digest
? `${model?.ollama?.digest} **(${model?.ollama?.modified_at})**`
: model.id
)}
className=" w-fit"
placement="top-start"
>
<div class="font-normal line-clamp-1 flex items-center gap-2">
{model.name}
<Badge
type={(model?.access_grants ?? []).some(
(g) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
? 'success'
: 'muted'}
content={(model?.access_grants ?? []).some(
(g) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
? $i18n.t('Public')
: $i18n.t('Private')}
/>
<div class="self-center">
<div class="flex bg-white rounded-xl">
<div
class="{(model?.is_active ?? true)
? ''
: 'opacity-50 dark:opacity-50'} bg-transparent rounded-xl"
>
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt="modelfile profile"
class=" rounded-xl size-8 object-cover"
loading="lazy"
decoding="async"
on:error={(e) => {
e.target.src = '/favicon.png';
}}
/>
</div>
</div>
</Tooltip>
<div
class=" text-xs overflow-hidden text-ellipsis line-clamp-1 flex items-center gap-1 text-gray-500"
>
<span class=" line-clamp-1">
{!!model?.meta?.description
? model?.meta?.description
: model?.ollama?.digest
? `${model.id} (${model?.ollama?.digest})`
: model.id}
</span>
</div>
</div>
</button>
<div class="flex flex-row gap-0.5 items-center self-center">
{#if shiftKey}
<Tooltip content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
hideModelHandler(model);
}}
<div
class="shrink-0 flex w-full min-w-0 flex-1 pr-1 self-center {(model?.is_active ??
true)
? ''
: 'text-gray-500'}"
>
<Tooltip
content={marked.parse(
!!model?.meta?.description
? model?.meta?.description
: model?.ollama?.digest
? `${model?.ollama?.digest} **(${model?.ollama?.modified_at})**`
: model.id
)}
className=" w-fit"
placement="top-start"
>
{#if model?.meta?.hidden}
<EyeSlash />
{:else}
<Eye />
{/if}
</button>
</Tooltip>
{:else}
<div
class="text-[13px] leading-4 font-normal line-clamp-1 flex items-center gap-1.5"
>
{model.name}
<Badge
type={(model?.access_grants ?? []).some(
(g) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
? 'success'
: 'muted'}
content={(model?.access_grants ?? []).some(
(g) =>
g.principal_type === 'user' &&
g.principal_id === '*' &&
g.permission === 'read'
)
? $i18n.t('Public')
: $i18n.t('Private')}
/>
</div>
</Tooltip>
<div
class="text-[11px] leading-3 overflow-hidden text-ellipsis line-clamp-1 flex items-center gap-1 text-gray-500"
>
<span class=" line-clamp-1">
{!!model?.meta?.description
? model?.meta?.description
: model?.ollama?.digest
? `${model.id} (${model?.ollama?.digest})`
: model.id}
</span>
</div>
</div>
</button>
<div class="flex flex-row gap-0.5 items-center self-center">
{#if shiftKey}
<Tooltip content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
hideModelHandler(model);
}}
>
{#if model?.meta?.hidden}
<EyeSlash />
{:else}
<Eye />
{/if}
</button>
</Tooltip>
{/if}
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
selectedModelId = model.id;
@ -753,28 +770,28 @@
/>
</Tooltip>
</div>
{/if}
</div>
</div>
{/each}
{:else}
<div class="flex h-full w-full items-center justify-center py-10">
<div class="max-w-md text-center">
<div class="mb-2 text-xl">😕</div>
<div class="mb-1 text-sm text-gray-700 dark:text-gray-300">
{$i18n.t('No models found')}
</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</div>
</div>
{/each}
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class="mb-2 text-xl">😕</div>
<div class="mb-1 text-sm text-gray-700 dark:text-gray-300">
{$i18n.t('No models found')}
</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</div>
</div>
{/if}
</div>
{#if filteredModels.length > perPage}
<Pagination bind:page={currentPage} count={filteredModels.length} {perPage} />
{/if}
</div>
{#if filteredModels.length > perPage}
<Pagination bind:page={currentPage} count={filteredModels.length} {perPage} />
{/if}
</div>
{:else}
<ModelEditor

View file

@ -9,6 +9,7 @@
export let value = '';
export let placeholder = $i18n.t('Select view');
export let align = 'start';
export let onChange: (value: string) => void = () => {};
const items = [
@ -26,13 +27,14 @@
bind:value
{items}
{placeholder}
triggerClass="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 bg-gray-50 dark:bg-gray-850 rounded-xl"
labelClass="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
{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"
labelClass="inline-flex h-input w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
onChange={() => onChange(value)}
>
<svelte:fragment slot="trigger" let:selectedLabel>
<span
class="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
class="inline-flex h-input w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
>
{selectedLabel}
</span>

View file

@ -1001,7 +1001,7 @@
</nav>
<div class="flex-1 min-h-0 p-4 md:px-5 flex flex-col">
{#if selectedTab !== 'account'}
{#if selectedTab !== 'account' && selectedTab !== 'admin:models'}
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
{$i18n.t(selectedTabTitle)}
</h2>

View file

@ -792,7 +792,7 @@
{#if !$mobile && !$showSidebar}
<div
class=" pt-[7px] pb-2 px-2 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850/30"
class=" w-[42px] py-1 px-1 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850/30"
id="sidebar"
>
<button
@ -801,18 +801,20 @@
showSidebar.set(!$showSidebar);
}}
>
<div class="pb-1.5">
<div class="pb-1">
<Tooltip
content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
placement="right"
>
<button
class="flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group {isWindows
class="flex size-8.5 items-center justify-center transition group {isWindows
? 'cursor-pointer'
: 'cursor-[e-resize]'}"
aria-label={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
>
<div class=" self-center flex items-center justify-center size-9">
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
>
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
class="sidebar-new-chat-icon size-5 rounded-full group-hover:hidden"
@ -825,11 +827,11 @@
</Tooltip>
</div>
<div class="-mt-[0.5px]">
<div class="-gap-0.5">
<div class="">
<Tooltip content={$i18n.t('New Chat')} placement="right">
<a
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
class=" cursor-pointer flex size-8 items-center justify-center transition group"
href="/"
draggable="false"
on:click={async (e) => {
@ -841,7 +843,9 @@
}}
aria-label={$i18n.t('New Chat')}
>
<div class=" self-center flex items-center justify-center size-9">
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
>
<PencilSquare className="size-4" />
</div>
</a>
@ -851,7 +855,7 @@
<div>
<Tooltip content={$i18n.t('Search')} placement="right">
<button
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
class=" cursor-pointer flex size-8 items-center justify-center transition group"
on:click={(e) => {
e.stopImmediatePropagation();
e.preventDefault();
@ -861,7 +865,9 @@
draggable="false"
aria-label={$i18n.t('Search')}
>
<div class=" self-center flex items-center justify-center size-9">
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
>
<Search className="size-4" />
</div>
</button>
@ -874,7 +880,7 @@
<div class="">
<Tooltip content={$i18n.t(meta.label)} placement="right">
<a
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
class=" cursor-pointer flex size-8 items-center justify-center transition group"
href={meta.href}
on:click={async (e) => {
e.stopImmediatePropagation();
@ -885,7 +891,9 @@
draggable="false"
aria-label={$i18n.t(meta.label)}
>
<div class=" self-center flex items-center justify-center size-9">
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
>
{#if itemId === 'notes'}
<Note className="size-4" />
{:else if itemId === 'workspace'}
@ -947,15 +955,17 @@
<div>
<div>
<div class=" py-2 flex justify-center items-center">
<div class=" flex justify-center items-center">
{#if $user !== undefined && $user !== null}
<UserMenu role={$user?.role} profile={$config?.features?.enable_user_status ?? true}>
<button
type="button"
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
class=" cursor-pointer flex size-8.5 items-center justify-center transition group"
aria-label={$i18n.t('User menu')}
>
<div class="self-center relative">
<div
class="self-center relative flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
>
<img
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
class="size-5.5 object-cover rounded-full"
@ -1035,7 +1045,7 @@
placement="bottom"
>
<button
class="flex rounded-xl size-8.5 justify-center items-center hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition {isWindows
class="flex size-[30px] justify-center items-center rounded-lg hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition {isWindows
? 'cursor-pointer'
: 'cursor-[w-resize]'}"
on:click={() => {
@ -1043,7 +1053,7 @@
}}
aria-label={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
>
<div class=" self-center p-1.5">
<div class=" self-center">
<Sidebar className="size-4.5" />
</div>
</button>