refac
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { marked } from 'marked';
|
||||
import Sortable from 'sortablejs';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
import { onMount, onDestroy, getContext, tick } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { config, models as _models, settings, user } from '$lib/stores';
|
||||
@@ -21,6 +22,7 @@
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
|
||||
import { getModels } from '$lib/apis';
|
||||
import { getModelsConfig, setModelsConfig } from '$lib/apis/configs';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
@@ -38,19 +40,20 @@
|
||||
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 GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Minus from '$lib/components/icons/Minus.svelte';
|
||||
import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
|
||||
import Download from '$lib/components/icons/Download.svelte';
|
||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||
import SettingsIcon from '$lib/components/icons/Settings.svelte';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||
import AdminViewSelector from './Models/AdminViewSelector.svelte';
|
||||
import TagSelector from '$lib/components/workspace/common/TagSelector.svelte';
|
||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||
|
||||
type ModelListItem = { id: string; name?: string };
|
||||
|
||||
@@ -62,8 +65,12 @@
|
||||
let importFiles;
|
||||
let modelsImportInputElement: HTMLInputElement;
|
||||
let tagsContainerElement: HTMLDivElement;
|
||||
let modelListElement: HTMLDivElement;
|
||||
let sortable = null;
|
||||
|
||||
let models = null;
|
||||
let modelsConfig = null;
|
||||
let modelOrderList: string[] = [];
|
||||
|
||||
let workspaceModels: ModelListItem[] = [];
|
||||
let baseModels: ModelListItem[] = [];
|
||||
@@ -73,6 +80,9 @@
|
||||
|
||||
let showConfigModal = false;
|
||||
let showManageModal = false;
|
||||
let showResetModal = false;
|
||||
let savingModelOrder = false;
|
||||
let modelOrderDirty = false;
|
||||
|
||||
let viewOption = ''; // '' = All, 'enabled', 'disabled', 'visible', 'hidden'
|
||||
let tags: string[] = [];
|
||||
@@ -83,9 +93,6 @@
|
||||
tabState = null;
|
||||
}
|
||||
|
||||
const perPage = 30;
|
||||
let currentPage = 1;
|
||||
|
||||
const isPublicModel = (model) => {
|
||||
return (model?.access_grants ?? []).some(
|
||||
(g) => g.principal_type === 'user' && g.principal_id === '*' && g.permission === 'read'
|
||||
@@ -115,6 +122,8 @@
|
||||
};
|
||||
|
||||
$: if (models) {
|
||||
const modelOrder = new Map(modelOrderList.map((id, idx) => [id, idx]));
|
||||
|
||||
filteredModels = models
|
||||
.filter((m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase()))
|
||||
.filter((m) => {
|
||||
@@ -127,15 +136,21 @@
|
||||
return true; // All
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const orderA = modelOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
const orderB = modelOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
|
||||
return (a?.name ?? a?.id ?? '').localeCompare(b?.name ?? b?.id ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
let searchValue = '';
|
||||
let canReorderModels = false;
|
||||
|
||||
$: if (searchValue || viewOption !== undefined) {
|
||||
currentPage = 1;
|
||||
}
|
||||
$: canReorderModels = searchValue === '' && viewOption === '' && selectedTag === '';
|
||||
|
||||
const enableAllHandler = async () => {
|
||||
const modelsToEnable = filteredModels.filter((m) => !(m.is_active ?? true));
|
||||
@@ -213,6 +228,9 @@
|
||||
const init = async () => {
|
||||
models = null;
|
||||
|
||||
modelsConfig = await getModelsConfig(localStorage.token);
|
||||
modelOrderList = modelsConfig?.MODEL_ORDER_LIST ?? [];
|
||||
|
||||
tags = await getBaseModelTags(localStorage.token);
|
||||
if (selectedTag && !tags.includes(selectedTag)) {
|
||||
selectedTag = '';
|
||||
@@ -243,6 +261,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
modelOrderList = [
|
||||
...modelOrderList.filter((id) => models.some((model) => model.id === id)),
|
||||
...models
|
||||
.map((model) => model.id)
|
||||
.filter((id) => !modelOrderList.includes(id))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
];
|
||||
modelOrderDirty = false;
|
||||
|
||||
_models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
@@ -251,6 +278,79 @@
|
||||
);
|
||||
};
|
||||
|
||||
const saveModelOrder = async (orderedModelIds: string[]) => {
|
||||
savingModelOrder = true;
|
||||
|
||||
const res = await setModelsConfig(localStorage.token, {
|
||||
DEFAULT_MODELS: modelsConfig?.DEFAULT_MODELS ?? null,
|
||||
DEFAULT_PINNED_MODELS: modelsConfig?.DEFAULT_PINNED_MODELS ?? null,
|
||||
MODEL_ORDER_LIST: orderedModelIds,
|
||||
DEFAULT_MODEL_METADATA: modelsConfig?.DEFAULT_MODEL_METADATA ?? null,
|
||||
DEFAULT_MODEL_PARAMS: modelsConfig?.DEFAULT_MODEL_PARAMS ?? null
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
modelsConfig = res;
|
||||
modelOrderDirty = false;
|
||||
toast.success($i18n.t('Model order saved successfully'));
|
||||
_models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
savingModelOrder = false;
|
||||
};
|
||||
|
||||
const positionChangeHandler = async (event) => {
|
||||
const { oldIndex, newIndex, item } = event;
|
||||
|
||||
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = item.parentNode;
|
||||
const target = parent.children[oldIndex < newIndex ? oldIndex : oldIndex + 1];
|
||||
parent.insertBefore(item, target);
|
||||
|
||||
const updatedModels = [...filteredModels];
|
||||
const [movedModel] = updatedModels.splice(oldIndex, 1);
|
||||
updatedModels.splice(newIndex, 0, movedModel);
|
||||
|
||||
const orderedIds = updatedModels.map((model) => model.id);
|
||||
const orderedSet = new Set(orderedIds);
|
||||
|
||||
models = [...updatedModels, ...models.filter((model) => !orderedSet.has(model.id))];
|
||||
modelOrderList = models.map((model) => model.id);
|
||||
modelOrderDirty = true;
|
||||
};
|
||||
|
||||
const initSortable = () => {
|
||||
if (sortable) {
|
||||
sortable.destroy();
|
||||
sortable = null;
|
||||
}
|
||||
|
||||
if (modelListElement && filteredModels.length > 0 && canReorderModels) {
|
||||
sortable = new Sortable(modelListElement, {
|
||||
animation: 150,
|
||||
handle: '.model-item-handle',
|
||||
onUpdate: positionChangeHandler
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$: if (modelListElement && filteredModels) {
|
||||
tick().then(() => {
|
||||
initSortable();
|
||||
});
|
||||
}
|
||||
|
||||
const upsertModelHandler = async (model, overrides = {}, showToast = true) => {
|
||||
model = { ...model, base_model_id: null, ...overrides };
|
||||
|
||||
@@ -436,8 +536,27 @@
|
||||
window.removeEventListener('blur', onBlur);
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (sortable) {
|
||||
sortable.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
title={$i18n.t('Reset All Models')}
|
||||
message={$i18n.t('This will delete all models including custom models and cannot be undone.')}
|
||||
bind:show={showResetModal}
|
||||
onConfirm={async () => {
|
||||
const res = await deleteAllModels(localStorage.token);
|
||||
if (res) {
|
||||
toast.success($i18n.t('All models deleted successfully'));
|
||||
await init();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ModelSettingsModal bind:show={showConfigModal} initHandler={init} />
|
||||
<ManageModelsModal bind:show={showManageModal} />
|
||||
|
||||
@@ -552,7 +671,6 @@
|
||||
return { value: tag, label: tag };
|
||||
})}
|
||||
onChange={async () => {
|
||||
currentPage = 1;
|
||||
await init();
|
||||
}}
|
||||
/>
|
||||
@@ -608,6 +726,17 @@
|
||||
<div class="flex items-center">{$i18n.t('Manage')}</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={() => {
|
||||
showResetModal = true;
|
||||
}}
|
||||
>
|
||||
<GarbageBin className="size-3.5" />
|
||||
<div class="flex items-center">{$i18n.t('Reset')}</div>
|
||||
</button>
|
||||
|
||||
<hr class="mx-1 my-0.5 border-gray-100 dark:border-gray-800" />
|
||||
|
||||
<button
|
||||
@@ -666,16 +795,31 @@
|
||||
? 'overflow-y-auto scrollbar-hover pr-1.5'
|
||||
: 'overflow-hidden'}"
|
||||
id="model-list"
|
||||
bind:this={modelListElement}
|
||||
>
|
||||
{#if filteredModels.length > 0}
|
||||
{#each filteredModels.slice((currentPage - 1) * perPage, currentPage * perPage) as model, modelIdx (`${model.id}-${modelIdx}`)}
|
||||
{#each filteredModels 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}"
|
||||
>
|
||||
<div class="self-center pr-1 text-gray-400 dark:text-gray-600">
|
||||
<Tooltip
|
||||
content={canReorderModels
|
||||
? $i18n.t('Drag to reorder')
|
||||
: $i18n.t('Clear filters to reorder')}
|
||||
>
|
||||
<EllipsisVertical
|
||||
className="size-4 {canReorderModels
|
||||
? 'cursor-move model-item-handle'
|
||||
: 'opacity-40'}"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex group/item gap-2.5 w-full min-w-0 flex-1 text-left cursor-pointer"
|
||||
type="button"
|
||||
@@ -841,9 +985,23 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if filteredModels.length > perPage}
|
||||
<Pagination bind:page={currentPage} count={filteredModels.length} {perPage} />
|
||||
{/if}
|
||||
<div class="flex justify-end pt-6 text-sm font-normal">
|
||||
<button
|
||||
class="flex items-center gap-2 px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full disabled:cursor-not-allowed disabled:opacity-40"
|
||||
type="button"
|
||||
disabled={!modelOrderDirty || savingModelOrder}
|
||||
on:click={async () => {
|
||||
await saveModelOrder(modelOrderList);
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
{#if savingModelOrder}
|
||||
<span class="shrink-0">
|
||||
<Spinner />
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user