diff --git a/locales/en.json b/locales/en.json
index b55ff9bb..9b1d95ae 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1465,7 +1465,7 @@
"resumeModelUpdates": "Resume updates for this model",
"ignoreModelUpdates": "Ignore updates for this model",
"viewLocalVersions": "View all local versions",
- "viewLocalTooltip": "Coming soon"
+ "viewLocalTooltip": "Show all local versions of this model on the main page"
},
"filters": {
"label": "Base filter",
diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py
index 916864b7..8e21415d 100644
--- a/py/routes/handlers/model_handlers.py
+++ b/py/routes/handlers/model_handlers.py
@@ -373,6 +373,14 @@ class ModelListingHandler:
request.query.get("group_by_model", "false").lower() == "true"
)
+ # View-local-versions filter: show all local versions of a specific model
+ civitai_model_id = request.query.get("civitai_model_id")
+ if civitai_model_id is not None:
+ try:
+ civitai_model_id = int(civitai_model_id)
+ except (TypeError, ValueError):
+ civitai_model_id = None
+
return {
"page": page,
"page_size": page_size,
@@ -397,6 +405,7 @@ class ModelListingHandler:
"name_pattern_exclude": name_pattern_exclude,
"name_pattern_use_regex": name_pattern_use_regex,
"group_by_model": group_by_model,
+ "civitai_model_id": civitai_model_id,
**self._parse_specific_params(request),
}
diff --git a/py/services/base_model_service.py b/py/services/base_model_service.py
index 13f3bfbe..e7ba379b 100644
--- a/py/services/base_model_service.py
+++ b/py/services/base_model_service.py
@@ -104,9 +104,17 @@ class BaseModelService(ABC):
fetch_duration = time.perf_counter() - t0
initial_count = len(sorted_data)
+ # Optionally filter by civitai model ID (shows all local versions of a specific model)
+ civitai_model_id = kwargs.get("civitai_model_id")
+ if civitai_model_id is not None:
+ sorted_data = [
+ item for item in sorted_data
+ if self._extract_model_id(item) == civitai_model_id
+ ]
+
# Optionally group by civitai modelId, showing only the latest version per model
dedup_lost = 0
- if kwargs.get("group_by_model"):
+ if kwargs.get("group_by_model") and civitai_model_id is None:
dedup_map = {} # modelId -> (item, version_id)
standalone = []
for item in sorted_data:
diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js
index d32bf601..68761bc7 100644
--- a/static/js/api/baseModelApi.js
+++ b/static/js/api/baseModelApi.js
@@ -1271,8 +1271,9 @@ export class BaseModelApiClient {
params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false');
- // Pass group-by-model mode to backend
- if (state.global.settings.group_by_model) {
+ // Pass group-by-model mode to backend (skip when showing all versions of a specific model)
+ const vlmModelId = getSessionItem('vlm_model_id');
+ if (state.global.settings.group_by_model && !vlmModelId) {
params.append('group_by_model', 'true');
}
@@ -1357,6 +1358,17 @@ export class BaseModelApiClient {
}
_addModelSpecificParams(params, pageState) {
+ // Check for View Local Versions filter (takes priority over recipe filters)
+ const vlmModelId = getSessionItem('vlm_model_id');
+ if (vlmModelId) {
+ params.append('civitai_model_id', vlmModelId);
+ const vlmBaseModel = getSessionItem('vlm_base_model');
+ if (vlmBaseModel) {
+ params.append('base_model', vlmBaseModel);
+ }
+ return;
+ }
+
if (this.modelType === 'loras') {
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js
index d93cb1a8..9a10518d 100644
--- a/static/js/api/loraApi.js
+++ b/static/js/api/loraApi.js
@@ -9,6 +9,13 @@ export class LoraApiClient extends BaseModelApiClient {
* Add LoRA-specific parameters to query
*/
_addModelSpecificParams(params, pageState) {
+ // Let parent handle View Local Versions filter first
+ super._addModelSpecificParams(params, pageState);
+ // If VLM filter was applied, skip recipe-specific filters
+ if (params.has('civitai_model_id')) {
+ return;
+ }
+
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
diff --git a/static/js/components/controls/CheckpointsControls.js b/static/js/components/controls/CheckpointsControls.js
index 079037ff..5bdb62e6 100644
--- a/static/js/components/controls/CheckpointsControls.js
+++ b/static/js/components/controls/CheckpointsControls.js
@@ -95,6 +95,16 @@ export class CheckpointsControls extends PageControls {
* Clear checkpoint custom filter and reload
*/
async clearCustomFilter() {
+ // Check for View Local Versions filter first
+ const vlmModelId = getSessionItem('vlm_model_id');
+ if (vlmModelId) {
+ removeSessionItem('vlm_model_id');
+ removeSessionItem('vlm_model_name');
+ removeSessionItem('vlm_base_model');
+ window.location.reload();
+ return;
+ }
+
removeSessionItem('recipe_to_checkpoint_filterHash');
removeSessionItem('recipe_to_checkpoint_filterHashes');
removeSessionItem('filterCheckpointRecipeName');
diff --git a/static/js/components/controls/LorasControls.js b/static/js/components/controls/LorasControls.js
index 725a960a..8ddc166b 100644
--- a/static/js/components/controls/LorasControls.js
+++ b/static/js/components/controls/LorasControls.js
@@ -112,6 +112,16 @@ export class LorasControls extends PageControls {
* Clear the custom filter and reload the page
*/
async clearCustomFilter() {
+ // Check for View Local Versions filter first (handles VLM and reloads)
+ const vlmModelId = getSessionItem('vlm_model_id');
+ if (vlmModelId) {
+ removeSessionItem('vlm_model_id');
+ removeSessionItem('vlm_model_name');
+ removeSessionItem('vlm_base_model');
+ window.location.reload();
+ return;
+ }
+
console.log("Clearing custom filter...");
// Remove filter parameters from session storage
removeSessionItem('recipe_to_lora_filterLoraHash');
diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js
index 801a2bd0..c8431916 100644
--- a/static/js/components/controls/PageControls.js
+++ b/static/js/components/controls/PageControls.js
@@ -1,6 +1,6 @@
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js';
-import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js';
+import { getStorageItem, setStorageItem, getSessionItem, setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
import { showToast, openCivitaiByMetadata } from '../../utils/uiHelpers.js';
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
import { sidebarManager } from '../SidebarManager.js';
@@ -129,6 +129,9 @@ export class PageControls {
clearFilterBtn.addEventListener('click', () => this.clearCustomFilter());
}
+ // Check for View Local Versions filter
+ this.checkVlmFilter();
+
// Page-specific event listeners
this.initPageSpecificListeners();
}
@@ -459,15 +462,57 @@ export class PageControls {
this.api.toggleBulkMode();
}
+ /**
+ * Clear custom filter
+ */
+ /**
+ * Check for View Local Versions filter in sessionStorage
+ */
+ checkVlmFilter() {
+ const vlmModelId = getSessionItem('vlm_model_id');
+ const vlmModelName = getSessionItem('vlm_model_name');
+ const vlmBaseModel = getSessionItem('vlm_base_model');
+
+ if (vlmModelId && vlmModelName) {
+ const indicator = document.getElementById('customFilterIndicator');
+ const filterText = indicator?.querySelector('.customFilterText');
+
+ if (indicator && filterText) {
+ indicator.classList.remove('hidden');
+
+ const prefix = vlmBaseModel
+ ? 'Showing same-base versions from'
+ : 'Showing all versions from';
+ const displayText = `${prefix}: ${vlmModelName}`;
+
+ filterText.textContent = this._truncateText(displayText, 40);
+ filterText.setAttribute('title', displayText);
+ }
+ }
+ }
+
/**
* Clear custom filter
*/
async clearCustomFilter() {
+ // Check for View Local Versions filter first
+ const vlmModelId = getSessionItem('vlm_model_id');
+ if (vlmModelId) {
+ removeSessionItem('vlm_model_id');
+ removeSessionItem('vlm_model_name');
+ removeSessionItem('vlm_base_model');
+
+ // Full page reload to restore initial state (mirrors the "set" action)
+ window.location.reload();
+ return;
+ }
+
+ // Otherwise delegate to subclass for recipe filters
if (!this.api) {
console.error('API methods not registered');
return;
}
-
+
try {
await this.api.clearCustomFilter();
} catch (error) {
@@ -475,6 +520,14 @@ export class PageControls {
showToast('toast.controls.clearFilterFailed', { message: error.message }, 'error');
}
}
+
+ /**
+ * Truncate text with ellipsis
+ */
+ _truncateText(text, maxLength) {
+ if (!text) return '';
+ return text.length > maxLength ? `${text.substring(0, maxLength - 3)}...` : text;
+ }
/**
* Initialize the favorites filter button state
diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js
index 04493932..f530a09a 100644
--- a/static/js/components/shared/ModelModal.js
+++ b/static/js/components/shared/ModelModal.js
@@ -752,6 +752,7 @@ export async function showModelModal(model, modelType) {
modelId: civitaiModelId,
currentVersionId: civitaiVersionId,
currentBaseModel: modelWithFullData.base_model,
+ modelName: model.model_name,
onUpdateStatusChange: handleUpdateStatusChange,
});
setupEditableFields(modelWithFullData.file_path, modelType);
diff --git a/static/js/components/shared/ModelVersionsTab.js b/static/js/components/shared/ModelVersionsTab.js
index c00a2e98..6c8b1881 100644
--- a/static/js/components/shared/ModelVersionsTab.js
+++ b/static/js/components/shared/ModelVersionsTab.js
@@ -6,6 +6,7 @@ import { translate } from '../../utils/i18nHelpers.js';
import { state } from '../../state/index.js';
import { buildCivitaiModelUrl } from '../../utils/civitaiUtils.js';
import { formatFileSize } from './utils.js';
+import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv'];
const PREVIEW_PLACEHOLDER_URL = '/loras_static/images/no-preview.png';
@@ -744,7 +745,7 @@ function renderToolbar(record, toolbarState = {}) {
-