feat(vlm): sort versions by newest first in VLM view, with disabled sort dropdown

When viewing all versions of a model (VLM mode via 'x versions' button):
- Backend always sorts by version ID descending, ignoring current sort_by
- A temporary 'Newest version first' option is injected into the sort
  dropdown (removed on exit, not a permanent option)
- The sort dropdown is disabled (greyed out) while VLM is active
- On clearing VLM, the previous sort preference is restored and the
  dropdown re-enabled
- Handles stale VLM state (e.g. after page reload with leftover session)
- Covers all three model page types: loras, checkpoints, embeddings

Also fixes review nits:
- Correct i18n call pattern (defaultValue in options object)
- Shared _restoreSortAfterVlm() helper to avoid triple duplication
This commit is contained in:
Will Miao
2026-06-24 16:25:14 +08:00
parent 71a459422f
commit 7a71b34b54
15 changed files with 127 additions and 10 deletions

View File

@@ -682,7 +682,8 @@
"usageAsc": "Wenigste", "usageAsc": "Wenigste",
"versionsCount": "Lokale Versionen", "versionsCount": "Lokale Versionen",
"versionsCountDesc": "Meiste Versionen zuerst", "versionsCountDesc": "Meiste Versionen zuerst",
"versionsCountAsc": "Wenigste Versionen zuerst" "versionsCountAsc": "Wenigste Versionen zuerst",
"versionIdDesc": "Neueste Version zuerst"
}, },
"refresh": { "refresh": {
"title": "Modelliste aktualisieren", "title": "Modelliste aktualisieren",

View File

@@ -682,7 +682,8 @@
"usageAsc": "Least", "usageAsc": "Least",
"versionsCount": "Local Versions", "versionsCount": "Local Versions",
"versionsCountDesc": "Most versions first", "versionsCountDesc": "Most versions first",
"versionsCountAsc": "Fewest versions first" "versionsCountAsc": "Fewest versions first",
"versionIdDesc": "Newest version first"
}, },
"refresh": { "refresh": {
"title": "Refresh model list", "title": "Refresh model list",

View File

@@ -682,7 +682,8 @@
"usageAsc": "Menos", "usageAsc": "Menos",
"versionsCount": "Versiones locales", "versionsCount": "Versiones locales",
"versionsCountDesc": "Más versiones primero", "versionsCountDesc": "Más versiones primero",
"versionsCountAsc": "Menos versiones primero" "versionsCountAsc": "Menos versiones primero",
"versionIdDesc": "Versión más nueva primero"
}, },
"refresh": { "refresh": {
"title": "Actualizar lista de modelos", "title": "Actualizar lista de modelos",

View File

@@ -682,7 +682,8 @@
"usageAsc": "Moins", "usageAsc": "Moins",
"versionsCount": "Versions locales", "versionsCount": "Versions locales",
"versionsCountDesc": "Plus de versions d'abord", "versionsCountDesc": "Plus de versions d'abord",
"versionsCountAsc": "Moins de versions d'abord" "versionsCountAsc": "Moins de versions d'abord",
"versionIdDesc": "Version la plus récente d'abord"
}, },
"refresh": { "refresh": {
"title": "Actualiser la liste des modèles", "title": "Actualiser la liste des modèles",

View File

@@ -682,7 +682,8 @@
"usageAsc": "הכי פחות", "usageAsc": "הכי פחות",
"versionsCount": "גרסאות מקומיות", "versionsCount": "גרסאות מקומיות",
"versionsCountDesc": "הכי הרבה גרסאות ראשונות", "versionsCountDesc": "הכי הרבה גרסאות ראשונות",
"versionsCountAsc": "הכי מעט גרסאות ראשונות" "versionsCountAsc": "הכי מעט גרסאות ראשונות",
"versionIdDesc": "גרסה חדשה ביותר ראשונה"
}, },
"refresh": { "refresh": {
"title": "רענן רשימת מודלים", "title": "רענן רשימת מודלים",

View File

@@ -682,7 +682,8 @@
"usageAsc": "少ない", "usageAsc": "少ない",
"versionsCount": "ローカルバージョン数", "versionsCount": "ローカルバージョン数",
"versionsCountDesc": "バージョン数の多い順", "versionsCountDesc": "バージョン数の多い順",
"versionsCountAsc": "バージョン数の少ない順" "versionsCountAsc": "バージョン数の少ない順",
"versionIdDesc": "最新バージョン順"
}, },
"refresh": { "refresh": {
"title": "モデルリストを更新", "title": "モデルリストを更新",

View File

@@ -682,7 +682,8 @@
"usageAsc": "적은 순", "usageAsc": "적은 순",
"versionsCount": "로컬 버전 수", "versionsCount": "로컬 버전 수",
"versionsCountDesc": "버전 수 많은 순", "versionsCountDesc": "버전 수 많은 순",
"versionsCountAsc": "버전 수 적은 순" "versionsCountAsc": "버전 수 적은 순",
"versionIdDesc": "최신 버전순"
}, },
"refresh": { "refresh": {
"title": "모델 목록 새로고침", "title": "모델 목록 새로고침",

View File

@@ -682,7 +682,8 @@
"usageAsc": "Меньше", "usageAsc": "Меньше",
"versionsCount": "Локальные версии", "versionsCount": "Локальные версии",
"versionsCountDesc": "Сначала больше версий", "versionsCountDesc": "Сначала больше версий",
"versionsCountAsc": "Сначала меньше версий" "versionsCountAsc": "Сначала меньше версий",
"versionIdDesc": "Сначала новые версии"
}, },
"refresh": { "refresh": {
"title": "Обновить список моделей", "title": "Обновить список моделей",

View File

@@ -682,7 +682,8 @@
"usageAsc": "最少", "usageAsc": "最少",
"versionsCount": "本地版本数", "versionsCount": "本地版本数",
"versionsCountDesc": "版本数从多到少", "versionsCountDesc": "版本数从多到少",
"versionsCountAsc": "版本数从少到多" "versionsCountAsc": "版本数从少到多",
"versionIdDesc": "最新版本优先"
}, },
"refresh": { "refresh": {
"title": "刷新模型列表", "title": "刷新模型列表",

View File

@@ -682,7 +682,8 @@
"usageAsc": "最少", "usageAsc": "最少",
"versionsCount": "本地版本數", "versionsCount": "本地版本數",
"versionsCountDesc": "版本數從多到少", "versionsCountDesc": "版本數從多到少",
"versionsCountAsc": "版本數從少到多" "versionsCountAsc": "版本數從少到多",
"versionIdDesc": "最新版本優先"
}, },
"refresh": { "refresh": {
"title": "重新整理模型列表", "title": "重新整理模型列表",

View File

@@ -111,6 +111,12 @@ class BaseModelService(ABC):
item for item in sorted_data item for item in sorted_data
if self._extract_model_id(item) == civitai_model_id if self._extract_model_id(item) == civitai_model_id
] ]
# VLM mode: always sort by version ID descending (newest version first),
# regardless of the current sort_by preference.
sorted_data.sort(
key=lambda x: self._extract_version_id(x) or 0,
reverse=True,
)
# Optionally group by civitai modelId, showing only the latest version per model # Optionally group by civitai modelId, showing only the latest version per model
dedup_lost = 0 dedup_lost = 0

View File

@@ -264,6 +264,23 @@
box-shadow: 0 0 0 2px oklch(var(--lora-accent) / 0.15); box-shadow: 0 0 0 2px oklch(var(--lora-accent) / 0.15);
} }
/* Disabled sort dropdown — used when VLM custom filter is active */
.control-group select:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--bg-color);
border-color: var(--border-color);
box-shadow: none;
transform: none;
}
.control-group select:disabled:hover {
border-color: var(--border-color);
background-color: var(--bg-color);
transform: none;
box-shadow: none;
}
/* Ensure hidden class works properly */ /* Ensure hidden class works properly */
.hidden { .hidden {
display: none !important; display: none !important;

View File

@@ -102,6 +102,7 @@ export class CheckpointsControls extends PageControls {
removeSessionItem('vlm_model_name'); removeSessionItem('vlm_model_name');
removeSessionItem('vlm_base_model'); removeSessionItem('vlm_base_model');
removeSessionItem('vlm_page_type'); removeSessionItem('vlm_page_type');
this._restoreSortAfterVlm();
// Hide the indicator // Hide the indicator
const indicator = document.getElementById('customFilterIndicator'); const indicator = document.getElementById('customFilterIndicator');
if (indicator) { if (indicator) {

View File

@@ -119,6 +119,7 @@ export class LorasControls extends PageControls {
removeSessionItem('vlm_model_name'); removeSessionItem('vlm_model_name');
removeSessionItem('vlm_base_model'); removeSessionItem('vlm_base_model');
removeSessionItem('vlm_page_type'); removeSessionItem('vlm_page_type');
this._restoreSortAfterVlm();
const indicator = document.getElementById('customFilterIndicator'); const indicator = document.getElementById('customFilterIndicator');
if (indicator) { if (indicator) {
indicator.classList.add('hidden'); indicator.classList.add('hidden');

View File

@@ -465,6 +465,60 @@ export class PageControls {
/** /**
* Clear custom filter * Clear custom filter
*/ */
/**
* Dynamically add the VLM sort option (version_id:desc) to the sort dropdown.
* It is not a permanent option — only present while VLM is active.
*/
_addVlmSortOption() {
const sortSelect = document.getElementById('sortSelect');
if (!sortSelect) return;
// Only add if not already present
if (sortSelect.querySelector('option[value="version_id:desc"]')) return;
const opt = document.createElement('option');
opt.value = 'version_id:desc';
opt.textContent = this._t('loras.controls.sort.versionIdDesc', 'Newest version first');
sortSelect.appendChild(opt);
}
/**
* Remove the VLM sort option from the sort dropdown.
*/
_removeVlmSortOption() {
const sortSelect = document.getElementById('sortSelect');
if (!sortSelect) return;
const opt = sortSelect.querySelector('option[value="version_id:desc"]');
if (opt) opt.remove();
}
/**
* Look up a translation key via the global i18n helper, falling back to
* a plain-text default when the key is missing or i18n is unavailable.
*/
_t(key, fallback) {
if (typeof window.i18n?.t === 'function') {
return window.i18n.t(key, { defaultValue: fallback });
}
return fallback;
}
/**
* Restore the sort dropdown state after VLM is cleared.
* Shared by PageControls.clearCustomFilter() and subclass overrides.
*/
_restoreSortAfterVlm() {
const prevSort = getSessionItem('vlm_prev_sort');
removeSessionItem('vlm_prev_sort');
const restoredSort = prevSort || 'name:asc';
this.pageState.sortBy = restoredSort;
this.saveSortPreference(restoredSort);
this._removeVlmSortOption();
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
sortSelect.value = restoredSort;
sortSelect.disabled = false;
}
}
/** /**
* Trigger View Local Versions without page reload * Trigger View Local Versions without page reload
* Sets sessionStorage and reloads data via the API. * Sets sessionStorage and reloads data via the API.
@@ -479,6 +533,17 @@ export class PageControls {
} else { } else {
removeSessionItem('vlm_base_model'); removeSessionItem('vlm_base_model');
} }
// Save current sort preference so it can be restored when VLM is cleared
setSessionItem('vlm_prev_sort', this.pageState.sortBy);
// Inject the temporary sort option and force version_id:desc
this._addVlmSortOption();
this.pageState.sortBy = 'version_id:desc';
this.saveSortPreference('version_id:desc');
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
sortSelect.value = 'version_id:desc';
sortSelect.disabled = true;
}
// Reload data via API (no page reload) // Reload data via API (no page reload)
this.resetAndReload(true).then(() => { this.resetAndReload(true).then(() => {
// Show the VLM indicator after data loads // Show the VLM indicator after data loads
@@ -533,6 +598,7 @@ export class PageControls {
checkVlmFilter() { checkVlmFilter() {
const vlmModelId = getSessionItem('vlm_model_id'); const vlmModelId = getSessionItem('vlm_model_id');
const vlmPageType = getSessionItem('vlm_page_type'); const vlmPageType = getSessionItem('vlm_page_type');
const sortSelect = document.getElementById('sortSelect');
// Only show VLM indicator when it belongs to the current page type // Only show VLM indicator when it belongs to the current page type
if (vlmModelId && vlmPageType !== this.pageType) { if (vlmModelId && vlmPageType !== this.pageType) {
@@ -541,6 +607,9 @@ export class PageControls {
removeSessionItem('vlm_model_name'); removeSessionItem('vlm_model_name');
removeSessionItem('vlm_base_model'); removeSessionItem('vlm_base_model');
removeSessionItem('vlm_page_type'); removeSessionItem('vlm_page_type');
removeSessionItem('vlm_prev_sort');
this._removeVlmSortOption();
if (sortSelect) sortSelect.disabled = false;
return; return;
} }
@@ -548,6 +617,13 @@ export class PageControls {
const vlmBaseModel = getSessionItem('vlm_base_model'); const vlmBaseModel = getSessionItem('vlm_base_model');
if (vlmModelId && vlmModelName) { if (vlmModelId && vlmModelName) {
// VLM is active — inject sort option, disable dropdown, show indicator
this._addVlmSortOption();
if (sortSelect) {
sortSelect.value = 'version_id:desc';
sortSelect.disabled = true;
}
const indicator = document.getElementById('customFilterIndicator'); const indicator = document.getElementById('customFilterIndicator');
const filterText = indicator?.querySelector('.customFilterText'); const filterText = indicator?.querySelector('.customFilterText');
@@ -562,6 +638,10 @@ export class PageControls {
filterText.textContent = this._truncateText(displayText, 40); filterText.textContent = this._truncateText(displayText, 40);
filterText.setAttribute('title', displayText); filterText.setAttribute('title', displayText);
} }
} else {
// No VLM — ensure sort option is removed and dropdown is enabled
this._removeVlmSortOption();
if (sortSelect) sortSelect.disabled = false;
} }
} }
@@ -577,6 +657,8 @@ export class PageControls {
removeSessionItem('vlm_base_model'); removeSessionItem('vlm_base_model');
removeSessionItem('vlm_page_type'); removeSessionItem('vlm_page_type');
this._restoreSortAfterVlm();
// Hide the indicator // Hide the indicator
const indicator = document.getElementById('customFilterIndicator'); const indicator = document.getElementById('customFilterIndicator');
if (indicator) { if (indicator) {