From 655157434ec8759f77cf7acd0778752b22b2ab8b Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 18 Nov 2025 06:46:50 +0800 Subject: [PATCH] feat(versions): add base filter toggle UI and styling Add CSS classes and JavaScript logic for the base filter toggle button in the versions toolbar. The filter allows users to switch between showing all versions or only versions matching the current base model. Includes styling for different states (active, hover, disabled) and accessibility features like screen reader support. --- locales/de.json | 12 + locales/en.json | 12 + locales/es.json | 12 + locales/fr.json | 12 + locales/he.json | 12 + locales/ja.json | 12 + locales/ko.json | 12 + locales/ru.json | 12 + locales/zh-CN.json | 12 + locales/zh-TW.json | 12 + static/css/components/lora-modal/versions.css | 52 ++++ .../js/components/shared/ModelVersionsTab.js | 262 ++++++++++++++---- .../components/modelVersionsTab.media.test.js | 139 +++++++++- 13 files changed, 521 insertions(+), 52 deletions(-) diff --git a/locales/de.json b/locales/de.json index 15b4cf47..e6a43fdc 100644 --- a/locales/de.json +++ b/locales/de.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Alle lokalen Versionen anzeigen", "viewLocalTooltip": "Demnächst verfügbar" }, + "filters": { + "label": "Basisfilter", + "state": { + "showAll": "Alle Versionen", + "showSameBase": "Gleiches Basismodell" + }, + "tooltip": { + "showAllVersions": "Wechseln, um alle Versionen anzuzeigen", + "showSameBaseVersions": "Wechseln, um nur Versionen mit demselben Basismodell anzuzeigen" + }, + "empty": "Keine Versionen entsprechen dem Filter für das aktuelle Basismodell." + }, "empty": "Noch keine Versionshistorie für dieses Modell vorhanden.", "error": "Versionen konnten nicht geladen werden.", "missingModelId": "Für dieses Modell ist keine Civitai-Model-ID vorhanden.", diff --git a/locales/en.json b/locales/en.json index 7ac844da..19ed4b4f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -938,6 +938,18 @@ "viewLocalVersions": "View all local versions", "viewLocalTooltip": "Coming soon" }, + "filters": { + "label": "Base filter", + "state": { + "showAll": "All versions", + "showSameBase": "Same base" + }, + "tooltip": { + "showAllVersions": "Switch to showing all versions", + "showSameBaseVersions": "Switch to showing only versions that match the current base model" + }, + "empty": "No versions match the current base model filter." + }, "empty": "No version history available for this model yet.", "error": "Failed to load versions.", "missingModelId": "This model is missing a Civitai model id.", diff --git a/locales/es.json b/locales/es.json index 89333ca8..4395c7e7 100644 --- a/locales/es.json +++ b/locales/es.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Ver todas las versiones locales", "viewLocalTooltip": "Disponible pronto" }, + "filters": { + "label": "Filtro base", + "state": { + "showAll": "Todas las versiones", + "showSameBase": "Mismo modelo base" + }, + "tooltip": { + "showAllVersions": "Cambiar para mostrar todas las versiones", + "showSameBaseVersions": "Cambiar para mostrar solo versiones del mismo modelo base" + }, + "empty": "Ninguna versión coincide con el filtro del modelo base actual." + }, "empty": "Aún no hay historial de versiones para este modelo.", "error": "No se pudieron cargar las versiones.", "missingModelId": "Este modelo no tiene un ID de modelo de Civitai.", diff --git a/locales/fr.json b/locales/fr.json index cf3c8ce3..68261795 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Voir toutes les versions locales", "viewLocalTooltip": "Bientôt disponible" }, + "filters": { + "label": "Filtre de base", + "state": { + "showAll": "Toutes les versions", + "showSameBase": "Même modèle de base" + }, + "tooltip": { + "showAllVersions": "Passer à l'affichage de toutes les versions", + "showSameBaseVersions": "Passer à l'affichage des versions du même modèle de base" + }, + "empty": "Aucune version ne correspond au filtre du modèle de base actuel." + }, "empty": "Aucun historique de versions n'est disponible pour ce modèle pour le moment.", "error": "Échec du chargement des versions.", "missingModelId": "Ce modèle ne possède pas d'identifiant de modèle Civitai.", diff --git a/locales/he.json b/locales/he.json index ac9571c6..ec33f7db 100644 --- a/locales/he.json +++ b/locales/he.json @@ -938,6 +938,18 @@ "viewLocalVersions": "הצג את כל הגרסאות המקומיות", "viewLocalTooltip": "יגיע בקרוב" }, + "filters": { + "label": "מסנן בסיס", + "state": { + "showAll": "כל הגרסאות", + "showSameBase": "אותו מודל בסיס" + }, + "tooltip": { + "showAllVersions": "החלף להצגת כל הגרסאות", + "showSameBaseVersions": "החלף להצגת גרסאות עם אותו מודל בסיס" + }, + "empty": "אין גרסאות התואמות את המסנן של מודל הבסיס הנוכחי." + }, "empty": "אין עדיין היסטוריית גרסאות למודל זה.", "error": "טעינת הגרסאות נכשלה.", "missingModelId": "למודל זה אין מזהה מודל של Civitai.", diff --git a/locales/ja.json b/locales/ja.json index ad5b300f..45ff3c10 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -938,6 +938,18 @@ "viewLocalVersions": "ローカルの全バージョンを表示", "viewLocalTooltip": "近日対応予定" }, + "filters": { + "label": "ベースフィルター", + "state": { + "showAll": "すべてのバージョン", + "showSameBase": "同じベース" + }, + "tooltip": { + "showAllVersions": "すべてのバージョンを表示する", + "showSameBaseVersions": "同じベースモデルのバージョンのみ表示する" + }, + "empty": "現在のベースモデルフィルターに一致するバージョンがありません。" + }, "empty": "このモデルにはまだバージョン履歴がありません。", "error": "バージョンの読み込みに失敗しました。", "missingModelId": "このモデルにはCivitaiのモデルIDがありません。", diff --git a/locales/ko.json b/locales/ko.json index 2fef79b2..ccedfc57 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -938,6 +938,18 @@ "viewLocalVersions": "로컬 버전 모두 보기", "viewLocalTooltip": "곧 제공 예정" }, + "filters": { + "label": "기본 필터", + "state": { + "showAll": "모든 버전", + "showSameBase": "같은 베이스" + }, + "tooltip": { + "showAllVersions": "모든 버전을 표시하도록 전환", + "showSameBaseVersions": "같은 베이스 모델 버전만 표시하도록 전환" + }, + "empty": "현재 베이스 모델 필터와 일치하는 버전이 없습니다." + }, "empty": "이 모델에는 아직 버전 기록이 없습니다.", "error": "버전을 불러오지 못했습니다.", "missingModelId": "이 모델에는 Civitai 모델 ID가 없습니다.", diff --git a/locales/ru.json b/locales/ru.json index 2d739730..d703ca4e 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Показать все локальные версии", "viewLocalTooltip": "Скоро появится" }, + "filters": { + "label": "Фильтр по базе", + "state": { + "showAll": "Все версии", + "showSameBase": "Тот же базовый" + }, + "tooltip": { + "showAllVersions": "Переключиться на отображение всех версий", + "showSameBaseVersions": "Переключиться на отображение только версий с тем же базовым" + }, + "empty": "Нет версий, соответствующих текущему фильтру базовой модели." + }, "empty": "Для этой модели пока нет истории версий.", "error": "Не удалось загрузить версии.", "missingModelId": "У этой модели отсутствует идентификатор модели Civitai.", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 1742ab69..e3804de8 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -938,6 +938,18 @@ "viewLocalVersions": "查看所有本地版本", "viewLocalTooltip": "敬请期待" }, + "filters": { + "label": "基础筛选", + "state": { + "showAll": "全部版本", + "showSameBase": "相同基模型" + }, + "tooltip": { + "showAllVersions": "切换为显示所有版本", + "showSameBaseVersions": "仅显示与当前基模型匹配的版本" + }, + "empty": "没有与当前基模型筛选匹配的版本。" + }, "empty": "该模型还没有版本历史。", "error": "加载版本失败。", "missingModelId": "该模型缺少 Civitai 模型 ID。", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index c8404531..9416fd8a 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -938,6 +938,18 @@ "viewLocalVersions": "檢視所有本地版本", "viewLocalTooltip": "敬請期待" }, + "filters": { + "label": "基礎篩選", + "state": { + "showAll": "所有版本", + "showSameBase": "相同基礎模型" + }, + "tooltip": { + "showAllVersions": "切換為顯示所有版本", + "showSameBaseVersions": "僅顯示與目前基礎模型相符的版本" + }, + "empty": "沒有符合目前基礎模型篩選的版本。" + }, "empty": "此模型尚無版本歷史。", "error": "載入版本失敗。", "missingModelId": "此模型缺少 Civitai 模型 ID。", diff --git a/static/css/components/lora-modal/versions.css b/static/css/components/lora-modal/versions.css index 1eaa9eac..929bf6d4 100644 --- a/static/css/components/lora-modal/versions.css +++ b/static/css/components/lora-modal/versions.css @@ -24,12 +24,29 @@ color: var(--text-color); } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + .versions-toolbar-info p { margin: 0; font-size: 0.85rem; color: var(--text-muted); } +.versions-toolbar-info-heading { + display: flex; + align-items: center; + gap: var(--space-2); +} + .versions-toolbar-actions { display: flex; flex-wrap: wrap; @@ -68,6 +85,41 @@ color: var(--text-color); } +.versions-filter-toggle { + appearance: none; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + padding: 0; + margin-bottom: 4px; + width: 30px; + height: 30px; + background: color-mix(in oklch, var(--card-bg) 80%, var(--bg-color)); + align-self: center; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, transform 0.2s ease; + position: relative; + cursor: pointer; +} + +.versions-filter-toggle i { + font-size: 1rem; +} + +.versions-filter-toggle:hover:not(:disabled) { + border-color: var(--text-color); + color: var(--text-color); + transform: translateY(-1px); +} + +.versions-filter-toggle[data-filter-active="true"] { + border-color: color-mix(in oklch, var(--lora-accent) 65%, transparent); + color: var(--lora-accent); + background: color-mix(in oklch, var(--lora-accent) 20%, var(--card-bg) 80%); +} + .versions-toolbar-btn:disabled { opacity: 0.6; cursor: not-allowed; diff --git a/static/js/components/shared/ModelVersionsTab.js b/static/js/components/shared/ModelVersionsTab.js index 8aff2efd..8b04e25c 100644 --- a/static/js/components/shared/ModelVersionsTab.js +++ b/static/js/components/shared/ModelVersionsTab.js @@ -152,6 +152,81 @@ function buildBadge(label, tone) { return `${escapeHtml(label)}`; } +const DISPLAY_FILTER_MODES = Object.freeze({ + SAME_BASE: 'same_base', + ANY: 'any', +}); + +const FILTER_LABEL_KEY = 'modals.model.versions.filters.label'; +const FILTER_STATE_KEYS = { + [DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.state.showSameBase', + [DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.state.showAll', +}; +const FILTER_TOOLTIP_KEYS = { + [DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.tooltip.showAllVersions', + [DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.tooltip.showSameBaseVersions', +}; + +function normalizeBaseModelName(value) { + if (typeof value !== 'string') { + return null; + } + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + return trimmed.toLowerCase(); +} + +function getToggleLabelText() { + return translate(FILTER_LABEL_KEY, {}, 'Base filter'); +} + +function getToggleStateText(mode) { + const key = FILTER_STATE_KEYS[mode] || FILTER_STATE_KEYS[DISPLAY_FILTER_MODES.ANY]; + const fallback = + mode === DISPLAY_FILTER_MODES.SAME_BASE ? 'Same base' : 'All versions'; + return translate(key, {}, fallback); +} + +function getToggleTooltipText(mode) { + const key = + FILTER_TOOLTIP_KEYS[mode] || FILTER_TOOLTIP_KEYS[DISPLAY_FILTER_MODES.ANY]; + const fallback = + mode === DISPLAY_FILTER_MODES.SAME_BASE + ? 'Switch to showing all versions' + : 'Switch to showing only versions with the current base model'; + return translate(key, {}, fallback); +} + +function getDefaultDisplayMode() { + const strategy = state?.global?.settings?.update_flag_strategy; + return strategy === DISPLAY_FILTER_MODES.SAME_BASE + ? DISPLAY_FILTER_MODES.SAME_BASE + : DISPLAY_FILTER_MODES.ANY; +} + +function getCurrentVersionBaseModel(record, versionId) { + if (!record || typeof versionId !== 'number' || !Array.isArray(record.versions)) { + return { + normalized: null, + raw: null, + }; + } + const currentVersion = record.versions.find(v => v.versionId === versionId); + if (!currentVersion) { + return { + normalized: null, + raw: null, + }; + } + const baseModelRaw = currentVersion.baseModel ?? null; + return { + normalized: normalizeBaseModelName(baseModelRaw), + raw: baseModelRaw, + }; +} + function getAutoplaySetting() { try { return Boolean(state?.global?.settings?.autoplay_on_hover); @@ -314,7 +389,7 @@ function getLatestLibraryVersionId(record) { return Math.max(...record.inLibraryVersionIds); } -function renderToolbar(record) { +function renderToolbar(record, toolbarState = {}) { const ignoreText = record.shouldIgnore ? translate('modals.model.versions.actions.resumeModelUpdates', {}, 'Resume updates for this model') : translate('modals.model.versions.actions.ignoreModelUpdates', {}, 'Ignore updates for this model'); @@ -325,10 +400,23 @@ function renderToolbar(record) { 'Track and manage every version of this model in one place.' ); + const displayMode = toolbarState.displayMode || DISPLAY_FILTER_MODES.ANY; + const toggleLabel = getToggleLabelText(); + const toggleState = getToggleStateText(displayMode); + const toggleTooltip = getToggleTooltipText(displayMode); + const filterActive = toolbarState.isFilteringActive ? 'true' : 'false'; + const screenReaderText = [toggleLabel, toggleState].filter(Boolean).join(': '); + return `
-

${translate('modals.model.versions.heading', {}, 'Model versions')}

+
+

${translate('modals.model.versions.heading', {}, 'Model versions')}

+ +

${escapeHtml(infoText)}

@@ -353,6 +441,20 @@ function renderEmptyState(container) { `; } +function renderFilteredEmptyState(baseModelLabel) { + const message = translate( + 'modals.model.versions.filters.empty', + { baseModel: baseModelLabel }, + 'No versions match the current base model filter.' + ); + return ` +
+ +

${escapeHtml(message)}

+
+ `; +} + function renderErrorState(container, message) { const fallback = translate('modals.model.versions.error', {}, 'Failed to load versions.'); container.innerHTML = ` @@ -391,6 +493,8 @@ export function initVersionsTab({ record: null, }; + let displayMode = getDefaultDisplayMode(); + let apiClient; function ensureClient() { @@ -414,55 +518,92 @@ export function initVersionsTab({ `; } - function render(record) { - controller.record = record; - controller.hasLoaded = true; +function render(record) { + controller.record = record; + controller.hasLoaded = true; - if (!record || !Array.isArray(record.versions) || record.versions.length === 0) { - renderEmptyState(container); - return; - } - - const latestLibraryVersionId = getLatestLibraryVersionId(record); - let dividerInserted = false; - - const sortedVersions = [...record.versions].sort( - (a, b) => Number(b.versionId) - Number(a.versionId) - ); - - const rowsMarkup = sortedVersions - .map(version => { - const isNewer = - typeof latestLibraryVersionId === 'number' && - version.versionId > latestLibraryVersionId; - let markup = ''; - if ( - !dividerInserted && - typeof latestLibraryVersionId === 'number' && - !isNewer - ) { - dividerInserted = true; - markup += ''; - } - markup += renderRow(version, { - latestLibraryVersionId, - currentVersionId: normalizedCurrentVersionId, - modelId: record?.modelId ?? modelId, - }); - return markup; - }) - .join(''); - - container.innerHTML = ` - ${renderToolbar(record)} -
- ${rowsMarkup} -
- `; - - setupMediaHoverInteractions(container); + if (!record || !Array.isArray(record.versions) || record.versions.length === 0) { + renderEmptyState(container); + return; } + const latestLibraryVersionId = getLatestLibraryVersionId(record); + const { normalized: currentBaseModelNormalized, raw: currentBaseModelLabel } = + getCurrentVersionBaseModel(record, normalizedCurrentVersionId); + const isFilteringActive = + displayMode === DISPLAY_FILTER_MODES.SAME_BASE && + Boolean(currentBaseModelNormalized); + + const sortedVersions = [...record.versions].sort( + (a, b) => Number(b.versionId) - Number(a.versionId) + ); + + const filteredVersions = sortedVersions.filter(version => { + if (!isFilteringActive) { + return true; + } + return normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized; + }); + + const dividerThresholdVersionId = (() => { + if (!isFilteringActive) { + return latestLibraryVersionId; + } + const baseLocalVersionIds = record.versions + .filter( + version => + version.isInLibrary && + normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized && + typeof version.versionId === 'number' + ) + .map(version => version.versionId); + if (!baseLocalVersionIds.length) { + return null; + } + return Math.max(...baseLocalVersionIds); + })(); + + let dividerInserted = false; + + const rowsMarkup = filteredVersions + .map(version => { + const isNewer = + typeof latestLibraryVersionId === 'number' && + version.versionId > latestLibraryVersionId; + let markup = ''; + if ( + !dividerInserted && + typeof dividerThresholdVersionId === 'number' && + !(version.versionId > dividerThresholdVersionId) + ) { + dividerInserted = true; + markup += ''; + } + markup += renderRow(version, { + latestLibraryVersionId, + currentVersionId: normalizedCurrentVersionId, + modelId: record?.modelId ?? modelId, + }); + return markup; + }) + .join(''); + + const listContent = + rowsMarkup || renderFilteredEmptyState(currentBaseModelLabel); + + container.innerHTML = ` + ${renderToolbar(record, { + displayMode, + isFilteringActive, + })} +
+ ${listContent} +
+ `; + + setupMediaHoverInteractions(container); +} + async function loadVersions({ forceRefresh = false, eager = false } = {}) { if (controller.isLoading) { return; @@ -531,6 +672,17 @@ export function initVersionsTab({ } } + function handleToggleVersionDisplayMode() { + displayMode = + displayMode === DISPLAY_FILTER_MODES.SAME_BASE + ? DISPLAY_FILTER_MODES.ANY + : DISPLAY_FILTER_MODES.SAME_BASE; + if (!controller.record) { + return; + } + render(controller.record); + } + async function handleToggleVersionIgnore(button, versionId) { if (!controller.record) { return; @@ -799,9 +951,17 @@ export function initVersionsTab({ const toolbarAction = event.target.closest('[data-versions-action]'); if (toolbarAction) { const action = toolbarAction.dataset.versionsAction; - if (action === 'toggle-model-ignore') { - event.preventDefault(); - await handleToggleModelIgnore(toolbarAction); + switch (action) { + case 'toggle-model-ignore': + event.preventDefault(); + await handleToggleModelIgnore(toolbarAction); + break; + case 'toggle-version-display-mode': + event.preventDefault(); + handleToggleVersionDisplayMode(); + break; + default: + break; } return; } diff --git a/tests/frontend/components/modelVersionsTab.media.test.js b/tests/frontend/components/modelVersionsTab.media.test.js index b8b3a287..a4c6af7c 100644 --- a/tests/frontend/components/modelVersionsTab.media.test.js +++ b/tests/frontend/components/modelVersionsTab.media.test.js @@ -28,7 +28,14 @@ vi.mock(UI_HELPERS_MODULE, () => ({ showToast: vi.fn(), })); -const stateMock = { global: { settings: { autoplay_on_hover: false } } }; +const stateMock = { + global: { + settings: { + autoplay_on_hover: false, + update_flag_strategy: 'any', + }, + }, +}; vi.mock(STATE_MODULE, () => ({ state: stateMock, })); @@ -59,6 +66,7 @@ describe('ModelVersionsTab media rendering', () => {
`; stateMock.global.settings.autoplay_on_hover = false; + stateMock.global.settings.update_flag_strategy = 'any'; ({ getModelApiClient } = await import(API_FACTORY_MODULE)); fetchModelUpdateVersions = vi.fn(); getModelApiClient.mockReturnValue({ @@ -146,4 +154,133 @@ describe('ModelVersionsTab media rendering', () => { expect(imageElement?.getAttribute('src')).toBe(previewUrl); expect(document.querySelector('.version-media video')).toBeFalsy(); }); + + it('shows a stable label with a short state indicator', async () => { + stateMock.global.settings.update_flag_strategy = 'any'; + fetchModelUpdateVersions.mockResolvedValue({ + success: true, + record: { + shouldIgnore: false, + inLibraryVersionIds: [5], + versions: [ + { + versionId: 5, + name: 'base', + baseModel: 'SDXL', + previewUrl: '/api/lm/previews/v-base.png', + sizeBytes: 1024, + isInLibrary: true, + shouldIgnore: false, + }, + ], + }, + }); + + const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE); + const controller = initVersionsTab({ + modalId: 'model-versions-modal', + modelType: 'loras', + modelId: 321, + currentVersionId: 5, + }); + + await controller.load(); + + const toggleText = document.querySelector('.versions-filter-toggle .sr-only'); + expect(toggleText?.textContent?.trim()).toBe('Base filter: All versions'); + }); + + it('filters versions to the current base model when strategy is same_base', async () => { + stateMock.global.settings.update_flag_strategy = 'same_base'; + fetchModelUpdateVersions.mockResolvedValue({ + success: true, + record: { + shouldIgnore: false, + inLibraryVersionIds: [10], + versions: [ + { + versionId: 10, + name: 'v1.0', + baseModel: 'SDXL', + previewUrl: '/api/lm/previews/v1.png', + sizeBytes: 1024, + isInLibrary: true, + shouldIgnore: false, + }, + { + versionId: 11, + name: 'v1.1', + baseModel: 'Realistic', + previewUrl: '/api/lm/previews/v1-1.png', + sizeBytes: 2048, + isInLibrary: false, + shouldIgnore: false, + }, + ], + }, + }); + + const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE); + const controller = initVersionsTab({ + modalId: 'model-versions-modal', + modelType: 'loras', + modelId: 789, + currentVersionId: 10, + }); + + await controller.load(); + + expect(document.querySelectorAll('.model-version-row').length).toBe(1); + }); + + it('toggle button can switch to display all versions', async () => { + stateMock.global.settings.update_flag_strategy = 'same_base'; + fetchModelUpdateVersions.mockResolvedValue({ + success: true, + record: { + shouldIgnore: false, + inLibraryVersionIds: [10], + versions: [ + { + versionId: 10, + name: 'v1.0', + baseModel: 'SDXL', + previewUrl: '/api/lm/previews/v1.png', + sizeBytes: 1024, + isInLibrary: true, + shouldIgnore: false, + }, + { + versionId: 11, + name: 'v1.1', + baseModel: 'Realistic', + previewUrl: '/api/lm/previews/v1-1.png', + sizeBytes: 2048, + isInLibrary: false, + shouldIgnore: false, + }, + ], + }, + }); + + const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE); + const controller = initVersionsTab({ + modalId: 'model-versions-modal', + modelType: 'loras', + modelId: 987, + currentVersionId: 10, + }); + + await controller.load(); + + expect(document.querySelectorAll('.model-version-row').length).toBe(1); + const toggleButton = document.querySelector('[data-versions-action="toggle-version-display-mode"]'); + expect(toggleButton).toBeTruthy(); + const toggleTextBefore = document.querySelector('.versions-filter-toggle .sr-only'); + expect(toggleTextBefore?.textContent?.trim()).toContain('Same base'); + toggleButton?.click(); + expect(document.querySelectorAll('.model-version-row').length).toBe(2); + const toggleTextAfter = document.querySelector('.versions-filter-toggle .sr-only'); + expect(toggleTextAfter?.textContent?.trim()).toContain('All versions'); + }); });