fix(autocomplete): sync active filters via server-side store (#1091)

The LoRA Manager page kept its active filters in localStorage, which the
ComfyUI-side autocomplete read directly. When the two run in different
browsers, origins, or the ComfyUI Desktop Electron shell, localStorage is
not shared and the active-filters search silently did nothing.

The manager page now mirrors its filter state to a server-side in-memory
store (PUT /api/lm/{prefix}/active-filters), pushed on every change via a
storage-listener hook and once on page load. The autocomplete widget sends
only use_active_filters=true, and the relative-paths endpoint injects the
stored filters into the search, with explicit query params taking
precedence.
This commit is contained in:
Will Miao
2026-09-02 14:33:44 +08:00
parent 6b41c3bbb4
commit 00095a5398
14 changed files with 952 additions and 112 deletions
+4
View File
@@ -3,6 +3,7 @@ import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } fr
import { createPageControls } from './components/controls/index.js';
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
import { MODEL_TYPES } from './api/apiConfig.js';
import { initActiveFiltersSync } from './utils/activeFiltersSync.js';
// Initialize the Checkpoints page
export class CheckpointsPageManager {
@@ -32,6 +33,9 @@ export class CheckpointsPageManager {
// Initialize common page features (including context menus)
appCore.initializePageFeatures();
// Mirror active filters to the backend for the ComfyUI-side autocomplete
initActiveFiltersSync(MODEL_TYPES.CHECKPOINT);
console.log('Checkpoints Manager initialized');
}
}
+4
View File
@@ -3,6 +3,7 @@ import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } fr
import { createPageControls } from './components/controls/index.js';
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
import { MODEL_TYPES } from './api/apiConfig.js';
import { initActiveFiltersSync } from './utils/activeFiltersSync.js';
// Initialize the Embeddings page
class EmbeddingsPageManager {
@@ -32,6 +33,9 @@ class EmbeddingsPageManager {
// Initialize common page features (including context menus)
appCore.initializePageFeatures();
// Mirror active filters to the backend for the ComfyUI-side autocomplete
initActiveFiltersSync(MODEL_TYPES.EMBEDDING);
console.log('Embeddings Manager initialized');
}
}
+4
View File
@@ -4,6 +4,7 @@ import { updateCardsForBulkMode } from './components/shared/ModelCard.js';
import { createPageControls } from './components/controls/index.js';
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
import { initActiveFiltersSync } from './utils/activeFiltersSync.js';
// Initialize the LoRA page
export class LoraPageManager {
@@ -41,6 +42,9 @@ export class LoraPageManager {
// Initialize common page features (including context menus and virtual scroll)
appCore.initializePageFeatures();
// Mirror active filters to the backend for the ComfyUI-side autocomplete
initActiveFiltersSync('loras');
}
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Mirrors the manager page's active filter state to the backend's in-memory
* store, so the ComfyUI-side autocomplete can apply it even when the manager
* page and ComfyUI run in different browsers/origins (localStorage is not
* shared there).
*/
import { getStorageItem, setActiveFiltersListener } from './storageHelpers.js';
import { debounce } from './debounce.js';
const SYNC_DEBOUNCE_MS = 300;
const debouncedPushByPage = {};
function buildActiveFiltersPayload(pageType) {
const activeFolder = getStorageItem(`${pageType}_activeFolder`);
const recursiveSearch = getStorageItem(`${pageType}_recursiveSearch`, true);
const filters = getStorageItem(`${pageType}_filters`);
return {
// null stays null; legacy "null" string is normalized to null
activeFolder: activeFolder && activeFolder !== 'null' ? activeFolder : null,
recursiveSearch: recursiveSearch !== false,
filters: filters && typeof filters === 'object' ? filters : null,
};
}
export async function pushActiveFilters(pageType) {
try {
const response = await fetch(`/api/lm/${pageType}/active-filters`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildActiveFiltersPayload(pageType)),
});
if (!response.ok) {
console.warn(`[Lora Manager] Failed to sync active filters for ${pageType}: HTTP ${response.status}`);
}
} catch (error) {
console.warn(`[Lora Manager] Failed to sync active filters for ${pageType}:`, error);
}
}
export function syncActiveFilters(pageType) {
if (!debouncedPushByPage[pageType]) {
debouncedPushByPage[pageType] = debounce(() => {
pushActiveFilters(pageType);
}, SYNC_DEBOUNCE_MS);
}
debouncedPushByPage[pageType]();
}
/**
* Register the storage listener and push the current (restored) state once.
* The initial push covers server restarts, where the backend store is empty
* until the manager page re-publishes its localStorage-restored filters.
* @param {string} pageType - 'loras' | 'checkpoints' | 'embeddings'
*/
export function initActiveFiltersSync(pageType) {
setActiveFiltersListener((changedPageType) => syncActiveFilters(changedPageType));
pushActiveFilters(pageType);
}
+30 -1
View File
@@ -6,6 +6,31 @@
// Namespace prefix for all localStorage keys
const STORAGE_PREFIX = 'lora_manager_';
// Matches keys that carry the manager page's active filter state
// (e.g. 'loras_activeFolder', 'checkpoints_filters').
const ACTIVE_FILTER_KEY_PATTERN = /^(loras|checkpoints|embeddings)_(activeFolder|recursiveSearch|filters)$/;
let activeFiltersListener = null;
/**
* Register a listener invoked with the page type whenever one of the
* active-filter storage keys changes. Used to mirror filter state to the
* backend so the ComfyUI-side autocomplete can pick it up across
* browsers/origins where localStorage is not shared.
* @param {function(string): void} listener
*/
export function setActiveFiltersListener(listener) {
activeFiltersListener = listener;
}
function notifyActiveFiltersChanged(key) {
if (!activeFiltersListener) return;
const match = ACTIVE_FILTER_KEY_PATTERN.exec(key);
if (match) {
activeFiltersListener(match[1]);
}
}
/**
* Get an item from localStorage with namespace support and fallback to legacy keys
* @param {string} key - The key without prefix
@@ -51,13 +76,15 @@ export function getStorageItem(key, defaultValue = null) {
*/
export function setStorageItem(key, value) {
const prefixedKey = STORAGE_PREFIX + key;
// Convert objects and arrays to JSON strings
if (typeof value === 'object' && value !== null) {
localStorage.setItem(prefixedKey, JSON.stringify(value));
} else {
localStorage.setItem(prefixedKey, value);
}
notifyActiveFiltersChanged(key);
}
/**
@@ -67,6 +94,8 @@ export function setStorageItem(key, value) {
export function removeStorageItem(key) {
localStorage.removeItem(STORAGE_PREFIX + key);
localStorage.removeItem(key); // Also remove legacy key
notifyActiveFiltersChanged(key);
}
/**