@@ -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');
+ });
});