feat: default pinned models

Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
This commit is contained in:
Timothy Jaeryang Baek 2025-11-19 05:04:03 -05:00
parent 93d0b8241c
commit 4bb15aa425
9 changed files with 151 additions and 85 deletions

View file

@ -1141,6 +1141,12 @@ DEFAULT_MODELS = PersistentConfig(
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
)
DEFAULT_PINNED_MODELS = PersistentConfig(
"DEFAULT_PINNED_MODELS",
"ui.default_pinned_models",
os.environ.get("DEFAULT_PINNED_MODELS", None),
)
try:
default_prompt_suggestions = json.loads(
os.environ.get("DEFAULT_PROMPT_SUGGESTIONS", "[]")

View file

@ -373,6 +373,7 @@ from open_webui.config import (
PENDING_USER_OVERLAY_TITLE,
DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_MODELS,
DEFAULT_PINNED_MODELS,
DEFAULT_ARENA_MODEL,
MODEL_ORDER_LIST,
EVALUATION_ARENA_MODELS,
@ -754,6 +755,10 @@ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PINNED_MODELS = DEFAULT_PINNED_MODELS
app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
@ -765,7 +770,6 @@ app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
@ -1877,6 +1881,7 @@ async def get_app_config(request: Request):
**(
{
"default_models": app.state.config.DEFAULT_MODELS,
"default_pinned_models": app.state.config.DEFAULT_PINNED_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"user_count": user_count,
"code": {

View file

@ -463,6 +463,7 @@ async def set_code_execution_config(
############################
class ModelsConfigForm(BaseModel):
DEFAULT_MODELS: Optional[str]
DEFAULT_PINNED_MODELS: Optional[str]
MODEL_ORDER_LIST: Optional[list[str]]
@ -470,6 +471,7 @@ class ModelsConfigForm(BaseModel):
async def get_models_config(request: Request, user=Depends(get_admin_user)):
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"DEFAULT_PINNED_MODELS": request.app.state.config.DEFAULT_PINNED_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
}
@ -479,9 +481,11 @@ async def set_models_config(
request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS
request.app.state.config.DEFAULT_PINNED_MODELS = form_data.DEFAULT_PINNED_MODELS
request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"DEFAULT_PINNED_MODELS": request.app.state.config.DEFAULT_PINNED_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
}

View file

@ -250,9 +250,8 @@
{#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-xl font-medium px-0.5">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2">
{$i18n.t('Models')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{filteredModels.length}</span
>

View file

@ -7,18 +7,20 @@
import { models } from '$lib/stores';
import { deleteAllModels } from '$lib/apis/models';
import { getModelsConfig, setModelsConfig } from '$lib/apis/configs';
import Modal from '$lib/components/common/Modal.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import ModelList from './ModelList.svelte';
import { getModelsConfig, setModelsConfig } from '$lib/apis/configs';
import Spinner from '$lib/components/common/Spinner.svelte';
import Minus from '$lib/components/icons/Minus.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import ModelSelector from './ModelSelector.svelte';
import Model from '../Evaluations/Model.svelte';
export let show = false;
export let initHandler = () => {};
@ -27,6 +29,10 @@
let selectedModelId = '';
let defaultModelIds = [];
let selectedPinnedModelId = '';
let defaultPinnedModelIds = [];
let modelIds = [];
let sortKey = '';
@ -38,25 +44,6 @@
$: if (show) {
init();
}
$: if (selectedModelId) {
onModelSelect();
}
const onModelSelect = () => {
if (selectedModelId === '') {
return;
}
if (defaultModelIds.includes(selectedModelId)) {
selectedModelId = '';
return;
}
defaultModelIds = [...defaultModelIds, selectedModelId];
selectedModelId = '';
};
const init = async () => {
config = await getModelsConfig(localStorage.token);
@ -65,6 +52,13 @@
} else {
defaultModelIds = [];
}
if (config?.DEFAULT_PINNED_MODELS) {
defaultPinnedModelIds = (config?.DEFAULT_PINNED_MODELS).split(',').filter((id) => id);
} else {
defaultPinnedModelIds = [];
}
const modelOrderList = config.MODEL_ORDER_LIST || [];
const allModelIds = $models.map((model) => model.id);
@ -86,6 +80,7 @@
const res = await setModelsConfig(localStorage.token, {
DEFAULT_MODELS: defaultModelIds.join(','),
DEFAULT_PINNED_MODELS: defaultPinnedModelIds.join(','),
MODEL_ORDER_LIST: modelIds
});
@ -191,59 +186,19 @@
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div>
<div class="flex flex-col w-full">
<div class="mb-1 flex justify-between">
<div class="text-xs text-gray-500">{$i18n.t('Default Models')}</div>
</div>
<ModelSelector
title={$i18n.t('Default Models')}
models={$models}
bind:modelIds={defaultModelIds}
/>
<div class="flex items-center -mr-1">
<select
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={selectedModelId}
>
<option value="">{$i18n.t('Select a model')}</option>
{#each $models as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
>{model.name}</option
>
{/each}
</select>
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<!-- <hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" /> -->
{#if defaultModelIds.length > 0}
<div class="flex flex-col">
{#each defaultModelIds as modelId, modelIdx}
<div class=" flex gap-2 w-full justify-between items-center">
<div class=" text-sm flex-1 py-1 rounded-lg">
{$models.find((model) => model.id === modelId)?.name}
</div>
<div class="shrink-0">
<button
type="button"
on:click={() => {
defaultModelIds = defaultModelIds.filter(
(_, idx) => idx !== modelIdx
);
}}
>
<Minus strokeWidth="2" className="size-3.5" />
</button>
</div>
</div>
{/each}
</div>
{:else}
<div class="text-gray-500 text-xs text-center py-2">
{$i18n.t('No models selected')}
</div>
{/if}
</div>
</div>
<ModelSelector
title={$i18n.t('Default Pinned Models')}
models={$models}
bind:modelIds={defaultPinnedModelIds}
/>
<div class="flex justify-between pt-3 text-sm font-medium gap-1.5">
<Tooltip content={$i18n.t('This will delete all models including custom models')}>

View file

@ -0,0 +1,70 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import Minus from '$lib/components/icons/Minus.svelte';
export let title = '';
export let models = [];
export let modelIds = [];
let selectedModelId = '';
</script>
<div>
<div class="flex flex-col w-full">
<div class="mb-1 flex justify-between">
<div class="text-xs text-gray-500">{title}</div>
</div>
<div class="flex items-center -mr-1">
<select
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={selectedModelId}
on:change={() => {
if (selectedModelId && !modelIds.includes(selectedModelId)) {
modelIds = [...modelIds, selectedModelId];
}
selectedModelId = '';
}}
>
<option value="">{$i18n.t('Select a model')}</option>
{#each models as model}
{#if !modelIds.includes(model.id)}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
{/if}
{/each}
</select>
</div>
<!-- <hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" /> -->
{#if modelIds.length > 0}
<div class="flex flex-col">
{#each modelIds as modelId, modelIdx}
<div class=" flex gap-2 w-full justify-between items-center">
<div class=" text-sm flex-1 py-1 rounded-lg">
{models.find((model) => model.id === modelId)?.name}
</div>
<div class="shrink-0">
<button
type="button"
on:click={() => {
modelIds = modelIds.filter((_, idx) => idx !== modelIdx);
}}
>
<Minus strokeWidth="2" className="size-3.5" />
</button>
</div>
</div>
{/each}
</div>
{:else}
<div class="text-gray-500 text-xs text-center py-2">
{$i18n.t('No models selected')}
</div>
{/if}
</div>
</div>

View file

@ -887,7 +887,7 @@
{/if}
</div>
{#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
{#if ($models ?? []).length > 0 && (($settings?.pinnedModels ?? []).length > 0 || $config?.default_pinned_models)}
<Folder
id="sidebar-models"
className="px-2 mt-0.5"

View file

@ -50,7 +50,7 @@
</div>
</a>
{#if mouseOver && shiftKey}
{#if mouseOver && shiftKey && onUnpin}
<div class="absolute right-5 top-2.5">
<div class=" flex items-center self-center space-x-1.5">
<Tooltip content={$i18n.t('Unpin')} className="flex items-center">

View file

@ -1,9 +1,9 @@
<script>
import Sortable from 'sortablejs';
import { onMount } from 'svelte';
import { onDestroy, onMount, tick } from 'svelte';
import { chatId, mobile, models, settings, showSidebar } from '$lib/stores';
import { chatId, config, mobile, models, settings, showSidebar } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants';
import { updateUserSettings } from '$lib/apis/users';
import PinnedModelItem from './PinnedModelItem.svelte';
@ -11,6 +11,8 @@
export let selectedChatId = null;
export let shiftKey = false;
let pinnedModels = [];
const initPinnedModelsSortable = () => {
const pinnedModelsList = document.getElementById('pinned-models-list');
if (pinnedModelsList && !$mobile) {
@ -33,13 +35,36 @@
}
};
onMount(() => {
let unsubscribeSettings;
onMount(async () => {
pinnedModels = $settings?.pinnedModels ?? [];
if (pinnedModels.length === 0 && $config?.default_pinned_models) {
const defaultPinnedModels = ($config?.default_pinned_models).split(',').filter((id) => id);
pinnedModels = defaultPinnedModels.filter((id) => $models.find((model) => model.id === id));
settings.set({ ...$settings, pinnedModels });
await updateUserSettings(localStorage.token, { ui: $settings });
}
unsubscribeSettings = settings.subscribe((value) => {
pinnedModels = value?.pinnedModels ?? [];
});
await tick();
initPinnedModelsSortable();
});
onDestroy(() => {
if (unsubscribeSettings) {
unsubscribeSettings();
}
});
</script>
<div class="mt-0.5 pb-1.5" id="pinned-models-list">
{#each $settings.pinnedModels as modelId (modelId)}
{#each pinnedModels as modelId (modelId)}
{@const model = $models.find((model) => model.id === modelId)}
{#if model}
<PinnedModelItem
@ -52,11 +77,13 @@
showSidebar.set(false);
}
}}
onUnpin={() => {
const pinnedModels = $settings.pinnedModels.filter((id) => id !== modelId);
settings.set({ ...$settings, pinnedModels });
updateUserSettings(localStorage.token, { ui: $settings });
}}
onUnpin={($settings?.pinnedModels ?? []).includes(modelId)
? () => {
const pinnedModels = $settings.pinnedModels.filter((id) => id !== modelId);
settings.set({ ...$settings, pinnedModels });
updateUserSettings(localStorage.token, { ui: $settings });
}
: null}
/>
{/if}
{/each}