Files
Azornes-Comfyui-Model-Resolver/web/resolver/shell/workflow_state_methods.js
T
Dariusz L b7c4f47e90 feat: add auto-fill base model toggle to control default search behavior
Adds a new 'Auto-fill model filter' setting under Search Defaults that
controls whether new searches default to 'Auto' (detecting the workflow
base model) or 'none' (Any model). The preference is persisted in
localStorage as ModelResolver.autoFillBaseModel and is respected across
search panel initialization, base model dropdown defaults, workflow state
cloning, and dirty-state checks.
2026-06-07 18:02:42 +02:00

403 lines
15 KiB
JavaScript

import { app } from "../../../../../scripts/app.js";
import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
export const workflowStateMethods = {
getWorkflowSignature(workflow) {
if (!workflow) return null;
try {
return JSON.stringify(this.getWorkflowSignatureData(workflow));
} catch (error) {
console.warn('Model Resolver: workflow signature generation failed', error);
return null;
}
},
getWorkflowSignatureData(workflow) {
const normalizeNode = (node = {}) => ({
id: node.id,
type: node.type,
mode: node.mode,
flags: node.flags,
inputs: Array.isArray(node.inputs)
? node.inputs.map(input => ({
name: input?.name,
type: input?.type,
link: input?.link
}))
: [],
outputs: Array.isArray(node.outputs)
? node.outputs.map(output => ({
name: output?.name,
type: output?.type,
links: output?.links || []
}))
: [],
widgets_values: node.widgets_values || [],
properties: node.properties || {}
});
const normalizeDefinition = (definition = {}) => ({
nodes: Array.isArray(definition.nodes)
? definition.nodes.map(normalizeNode).sort((a, b) => String(a.id).localeCompare(String(b.id)))
: [],
links: definition.links || []
});
const definitions = workflow.definitions && typeof workflow.definitions === 'object'
? Object.fromEntries(
Object.entries(workflow.definitions)
.sort(([a], [b]) => String(a).localeCompare(String(b)))
.map(([key, definition]) => [key, normalizeDefinition(definition)])
)
: {};
return {
nodes: Array.isArray(workflow.nodes)
? workflow.nodes.map(normalizeNode).sort((a, b) => String(a.id).localeCompare(String(b.id)))
: [],
links: workflow.links || [],
definitions
};
},
getActiveWorkflowRouteKey() {
try {
return window.location?.hash || '';
} catch (error) {
return '';
}
},
rememberActiveWorkflow(workflow = null) {
const currentWorkflow = workflow || this.getCurrentWorkflow();
this.activeWorkflowRouteKey = this.getActiveWorkflowRouteKey();
this.activeWorkflowSignature = this.getWorkflowSignature(currentWorkflow);
},
getWorkflowScopedQueueKey(route = this.activeWorkflowRouteKey, signature = this.activeWorkflowSignature) {
if (route && signature) return `${route}\n${signature}`;
return route || signature || null;
},
clonePendingResolutions(list = this.pendingResolutions) {
try {
return JSON.parse(JSON.stringify(Array.isArray(list) ? list : []));
} catch (error) {
console.warn('Model Resolver: failed to clone queued selections', error);
return Array.isArray(list) ? list.map(item => ({ ...item })) : [];
}
},
savePendingQueueForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
const list = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : [];
if (list.length) {
this.workflowPendingSelections.set(key, this.clonePendingResolutions(list));
} else {
this.workflowPendingSelections.delete(key);
}
},
restorePendingQueueForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? this.workflowPendingSelections.get(key) : null;
this.pendingResolutions = saved ? this.clonePendingResolutions(saved) : [];
this.rebuildPendingIndex();
this.updateApplyPendingButton?.();
this.updateQueuePanel?.();
this.updateQueueVisibility?.();
},
cloneAnalysisData(data = null) {
if (!data) return null;
try {
return JSON.parse(JSON.stringify(data));
} catch (error) {
console.warn('Model Resolver: failed to clone analysis data', error);
return data;
}
},
saveAnalysisCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
if (this.cachedWorkflowSignature && this.cachedAnalysisData) {
this.workflowAnalysisCaches.set(key, {
signature: this.cachedWorkflowSignature,
data: this.cloneAnalysisData(this.cachedAnalysisData)
});
} else {
this.workflowAnalysisCaches.delete(key);
}
},
restoreAnalysisCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? this.workflowAnalysisCaches.get(key) : null;
this.cachedWorkflowSignature = saved?.signature || null;
this.cachedAnalysisData = saved?.data
? this.cloneAnalysisData(saved.data)
: null;
},
saveLoadedModelsCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
if (this.cachedLoadedModelsSignature && this.cachedLoadedModelsData) {
this.workflowLoadedModelCaches.set(key, {
signature: this.cachedLoadedModelsSignature,
data: this.cloneAnalysisData(this.cachedLoadedModelsData)
});
} else {
this.workflowLoadedModelCaches.delete(key);
}
},
restoreLoadedModelsCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? this.workflowLoadedModelCaches.get(key) : null;
this.cachedLoadedModelsSignature = saved?.signature || null;
this.cachedLoadedModelsData = saved?.data
? this.cloneAnalysisData(saved.data)
: null;
},
invalidateLoadedModelsCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
this.cachedLoadedModelsSignature = null;
this.cachedLoadedModelsData = null;
if (key) {
this.workflowLoadedModelCaches.delete(key);
}
},
cloneSearchState(state = {}, { preserveActive = false } = {}) {
let clone = {};
try {
clone = JSON.parse(JSON.stringify(state || {}));
} catch (error) {
console.warn('Model Resolver: failed to clone search state', error);
clone = { ...state };
}
clone.selectedSource = clone.selectedSource || 'all';
clone.selectedBaseModel = clone.selectedBaseModel || this.getDefaultSearchBaseModel?.() || 'auto';
clone.results = this.mergeSearchResults({}, clone.results || {});
clone.lastAttemptSources = Array.isArray(clone.lastAttemptSources)
? clone.lastAttemptSources
: [];
clone.lastAttemptBaseModelContext = clone.lastAttemptBaseModelContext || '';
clone.lastAttemptFound = clone.lastAttemptFound ?? null;
clone.lastAttemptError = clone.lastAttemptError || null;
clone.sourceProgress = clone.sourceProgress || {};
clone.activeSearchRunId = preserveActive ? (clone.activeSearchRunId || null) : null;
if (!preserveActive) {
for (const progress of Object.values(clone.sourceProgress)) {
if (!progress || (progress.status !== 'pending' && progress.status !== 'running')) continue;
progress.status = 'error';
progress.percent = 100;
progress.message = 'Search interrupted';
}
}
return clone;
},
cloneSearchResultCache(cache = this.searchResultCache, { preserveActive = false, workflowKey = null } = {}) {
const cloned = new Map();
for (const [missingSearchKey, state] of cache.entries()) {
if (!this.isPersistableSearchState(state)) {
continue;
}
const preserveEntryActive = Boolean(
preserveActive
&& workflowKey
&& state?.activeSearchRunId
&& this.hasBackgroundSearchJob?.(workflowKey, missingSearchKey, state.activeSearchRunId)
);
cloned.set(
missingSearchKey,
this.cloneSearchState(state, { preserveActive: preserveEntryActive })
);
}
return cloned;
},
isPersistableSearchState(state = {}) {
if ((state?.selectedSource || 'all') !== 'all') return true;
const defaultBaseModel = this.getDefaultSearchBaseModel?.() || 'auto';
if ((state?.selectedBaseModel || defaultBaseModel) !== defaultBaseModel) return true;
if (state?.lastAttemptError) return true;
if (this.hasSearchResults(state?.results || {})) return true;
const progressEntries = Object.values(state?.sourceProgress || {});
return Boolean(state?.activeSearchRunId) || progressEntries.some(progress => progress?.status);
},
saveSearchCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
const cloned = this.cloneSearchResultCache(this.searchResultCache, {
preserveActive: true,
workflowKey: key
});
if (cloned.size) {
this.workflowSearchResultCaches.set(key, cloned);
} else {
this.workflowSearchResultCaches.delete(key);
}
},
restoreSearchCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? this.workflowSearchResultCaches.get(key) : null;
this.searchResultCache = saved
? this.cloneSearchResultCache(saved, { preserveActive: true, workflowKey: key })
: new Map();
},
persistSearchStateForActiveWorkflow() {
this.saveSearchCacheForActiveWorkflow();
},
clearAllSearchProgressTimers() {
for (const timer of this.searchProgressTimers.values()) {
clearInterval(timer);
}
this.searchProgressTimers.clear();
},
syncWorkflowScopedQueue(workflow = null) {
const currentWorkflow = workflow || this.getCurrentWorkflow();
const nextRoute = this.getActiveWorkflowRouteKey();
const nextSignature = this.getWorkflowSignature(currentWorkflow);
const previousKey = this.getWorkflowScopedQueueKey();
const nextKey = this.getWorkflowScopedQueueKey(nextRoute, nextSignature);
if (!nextKey || nextKey === previousKey) {
this.activeWorkflowRouteKey = nextRoute;
this.activeWorkflowSignature = nextSignature;
return;
}
this.savePendingQueueForActiveWorkflow();
this.saveAnalysisCacheForActiveWorkflow();
this.saveLoadedModelsCacheForActiveWorkflow();
this.saveSearchCacheForActiveWorkflow();
this.clearWorkflowScopedState();
this.activeWorkflowRouteKey = nextRoute;
this.activeWorkflowSignature = nextSignature;
this.restorePendingQueueForActiveWorkflow();
this.restoreAnalysisCacheForActiveWorkflow();
this.restoreLoadedModelsCacheForActiveWorkflow();
this.restoreSearchCacheForActiveWorkflow();
},
clearWorkflowScopedState() {
const key = this.getWorkflowScopedQueueKey();
this.cachedWorkflowSignature = null;
this.cachedAnalysisData = null;
this.cachedLoadedModelsSignature = null;
this.cachedLoadedModelsData = null;
for (const [missingSearchKey, state] of this.searchResultCache.entries()) {
if (!this.hasBackgroundSearchJob?.(key, missingSearchKey, state.activeSearchRunId)) {
state.activeSearchRunId = null;
}
}
this.clearAllSearchProgressTimers();
this.searchResultCache.clear();
this.urnResolvePromises.clear();
this.urnLocalMatchPromises.clear();
this.downloadTargetSelections?.clear();
this.pendingResolutions = [];
this.rebuildPendingIndex();
this.updateApplyPendingButton?.();
this.updateQueuePanel?.();
this.updateQueueVisibility();
},
scheduleActiveWorkflowRefresh(reason = 'workflow-change') {
if (!this.isVisible()) return;
this._workflowRefreshExpectedRoute = this.getActiveWorkflowRouteKey();
this._workflowRefreshPreviousSignature = this.activeWorkflowSignature;
if (this._workflowRefreshTimer) {
clearTimeout(this._workflowRefreshTimer);
}
this._workflowRefreshTimer = setTimeout(() => {
this._workflowRefreshTimer = null;
this.refreshForActiveWorkflowChange({
reason,
expectedRoute: this._workflowRefreshExpectedRoute,
previousSignature: this._workflowRefreshPreviousSignature,
attempt: 0
});
}, 180);
},
async refreshForActiveWorkflowChange({ reason = 'workflow-change', expectedRoute = '', previousSignature = null, attempt = 0 } = {}) {
if (!this.isVisible()) return;
const currentRoute = this.getActiveWorkflowRouteKey();
if (expectedRoute && currentRoute !== expectedRoute) {
this.scheduleActiveWorkflowRefresh(reason);
return;
}
const workflow = this.getCurrentWorkflow();
const signature = this.getWorkflowSignature(workflow);
const routeChanged = currentRoute !== this.activeWorkflowRouteKey;
const signatureChanged = signature && signature !== this.activeWorkflowSignature;
const graphStillLooksOld = routeChanged && previousSignature && signature === previousSignature;
if ((!signature || graphStillLooksOld) && attempt < 8) {
setTimeout(() => {
this.refreshForActiveWorkflowChange({
reason,
expectedRoute,
previousSignature,
attempt: attempt + 1
});
}, 180 + (attempt * 120));
return;
}
if (!routeChanged && !signatureChanged) return;
console.log('Model Resolver: active workflow changed, refreshing current tab', {
reason,
route: currentRoute,
tab: this.activeTab
});
this.savePendingQueueForActiveWorkflow();
this.saveAnalysisCacheForActiveWorkflow();
this.saveLoadedModelsCacheForActiveWorkflow();
this.saveSearchCacheForActiveWorkflow();
this.clearWorkflowScopedState();
this.activeWorkflowRouteKey = currentRoute;
this.activeWorkflowSignature = signature;
this.restorePendingQueueForActiveWorkflow();
this.restoreAnalysisCacheForActiveWorkflow();
this.restoreLoadedModelsCacheForActiveWorkflow();
this.restoreSearchCacheForActiveWorkflow();
if (this.activeTab === 'missing') {
if (this.contentElement) this.contentElement.style.overflowY = 'auto';
await this.loadWorkflowData(workflow);
} else if (this.activeTab === 'loaded') {
if (this.contentElement) this.contentElement.style.overflowY = 'auto';
await this.loadLoadedModels(workflow);
}
}
};