refactor(frontend): extract missing model state

This commit is contained in:
Dariusz L
2026-08-02 13:46:38 +02:00
parent 2a27538f7d
commit c025f9bdba
5 changed files with 173 additions and 212 deletions
+12 -8
View File
@@ -20,6 +20,7 @@ import {
import { startSplitterDrag } from '../web/resolver/utils/splitter_drag.js';
import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js';
import { searchHashMethods } from '../web/resolver/search/search_hash_methods.js';
import { missingModelStateMethods } from '../web/resolver/views/missing_model_state_methods.js';
import {
getCustomNodeModelAdapter,
getCustomNodeModelCategory,
@@ -2576,8 +2577,7 @@ test('workflow model selection patches the existing browser and centers its row'
});
test('Local Database source ignores installed local model matches before search', () => {
const hasMissingSourceSearchAttempt = eval(`(${extractMethod(missingBrowserMethodsSource, 'hasMissingSourceSearchAttempt')})`);
const getMissingSourceResultStatus = eval(`(${extractMethod(missingBrowserMethodsSource, 'getMissingSourceResultStatus')})`);
const { hasMissingSourceSearchAttempt, getMissingSourceResultStatus } = missingModelStateMethods;
const dialog = {
hasMissingSourceSearchAttempt,
getSearchResultStatusLevel: () => 'exact'
@@ -2602,9 +2602,11 @@ test('Local Database source ignores installed local model matches before search'
});
test('Local Database source reports its result after Local Database or Everything search', () => {
const hasMissingSourceSearchAttempt = eval(`(${extractMethod(missingBrowserMethodsSource, 'hasMissingSourceSearchAttempt')})`);
const getMissingSourceResultStatus = eval(`(${extractMethod(missingBrowserMethodsSource, 'getMissingSourceResultStatus')})`);
const getSearchResultStatusLevel = eval(`(${extractMethod(missingBrowserMethodsSource, 'getSearchResultStatusLevel')})`);
const {
hasMissingSourceSearchAttempt,
getMissingSourceResultStatus,
getSearchResultStatusLevel,
} = missingModelStateMethods;
const dialog = { hasMissingSourceSearchAttempt, getSearchResultStatusLevel };
for (const attemptedSource of ['local', 'all']) {
@@ -2624,9 +2626,11 @@ test('Local Database source reports its result after Local Database or Everythin
});
test('automatic Local Database download source stays hidden until that source is searched', () => {
const hasMissingSourceSearchAttempt = eval(`(${extractMethod(missingBrowserMethodsSource, 'hasMissingSourceSearchAttempt')})`);
const isLocalDatabaseDownloadSource = eval(`(${extractMethod(missingBrowserMethodsSource, 'isLocalDatabaseDownloadSource')})`);
const shouldDisplayKnownDownloadSource = eval(`(${extractMethod(missingBrowserMethodsSource, 'shouldDisplayKnownDownloadSource')})`);
const {
hasMissingSourceSearchAttempt,
isLocalDatabaseDownloadSource,
shouldDisplayKnownDownloadSource,
} = missingModelStateMethods;
const state = {
selectedSource: 'all',
explicitSearchSources: [],
+2 -48
View File
@@ -1,54 +1,8 @@
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import assert from 'node:assert/strict';
import { missingModelStateMethods } from '../web/resolver/views/missing_model_state_methods.js';
const projectRoot = path.resolve(import.meta.dirname, '..');
const missingBrowserMethodsSource = fs.readFileSync(
path.join(projectRoot, 'web/resolver/views/missing_browser_methods.js'),
'utf8'
);
function extractMethod(source, methodName, paramsPattern = '[^)]*') {
const signatureRegex = new RegExp(`\\n\\s+(async\\s+)?${methodName}\\s*\\(${paramsPattern}\\)\\s*\\{`);
const match = signatureRegex.exec(source);
assert.ok(match, `Could not find ${methodName}`);
const isAsync = Boolean(match[1]);
const parenStart = source.indexOf('(', match.index);
const parenEnd = source.indexOf(')', parenStart);
const params = source.slice(parenStart + 1, parenEnd);
const braceStart = source.indexOf('{', parenEnd);
let depth = 0;
for (let i = braceStart; i < source.length; i += 1) {
const char = source[i];
if (char === '{') depth += 1;
if (char === '}') depth -= 1;
if (depth === 0) {
return `${isAsync ? 'async ' : ''}function ${methodName}(${params}) ${source.slice(braceStart, i + 1)}`;
}
}
throw new Error(`Could not parse ${methodName}`);
}
const methodNames = [
'getBestLocalMatch',
'getMissingModelSummaryStats',
'getMissingSourceStatus',
'hasMissingSourceSearchAttempt',
'isLocalDatabaseDownloadSource',
'shouldDisplayKnownDownloadSource',
'getMissingSourceResultStatus',
'getSearchResultStatusLevel',
'hasRenderableSearchState',
'isMissingModelResolved',
'isAutoDownloadModel',
'getResolvedMissingCount',
];
const methods = Object.fromEntries(methodNames.map(methodName => [
methodName,
eval(`(${extractMethod(missingBrowserMethodsSource, methodName)})`),
]));
const methods = missingModelStateMethods;
function createDialog() {
return {
+2
View File
@@ -16,6 +16,7 @@ import { searchSourceMethods } from "./search/search_source_methods.js";
import { baseModelAliasMethods } from "./search/base_model_alias_methods.js";
import { searchHashMethods } from "./search/search_hash_methods.js";
import { searchStateMethods } from "./search/search_state_methods.js";
import { missingModelStateMethods } from "./views/missing_model_state_methods.js";
import { queueMethods } from "./actions/queue_methods.js";
import { queueStorageMethods } from "./actions/queue_storage_methods.js";
import { downloadProgressMethods } from "./actions/download_progress_methods.js";
@@ -341,6 +342,7 @@ applyDialogMethods(
baseModelAliasMethods,
searchHashMethods,
searchStateMethods,
missingModelStateMethods,
searchPanelMethods,
modelInfoMethods,
renderFormatMethods,
@@ -268,138 +268,6 @@ export const missingBrowserMethods = {
return `<span class="${classes}" data-tooltip="${this.escapeHtml(tooltip)}" tabindex="0" aria-label="Auto download capable">${content}</span>`;
},
getBestLocalMatch(missing = {}, minConfidence = 0) {
const matches = Array.isArray(missing.matches) ? missing.matches : [];
return matches
.filter(match => Number(match.confidence) >= minConfidence)
.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0))[0] || null;
},
getMissingModelSummaryStats(missingModels = []) {
return missingModels.reduce((stats, missing) => {
const best = this.getBestLocalMatch(missing, 70);
if (best?.confidence === 100) {
stats.exact += 1;
} else if (best) {
stats.partial += 1;
} else {
stats.none += 1;
}
return stats;
}, { exact: 0, partial: 0, none: 0 });
},
getMissingSourceStatus(missing = {}, source = '') {
const state = this.searchResultCache.get(this.getMissingSearchKey(missing));
const progress = state?.sourceProgress?.[source];
const resultStatus = this.getMissingSourceResultStatus(missing, source, state);
if (progress?.status === 'found') return resultStatus || 'found';
if (progress?.status) return progress.status;
if (resultStatus) return resultStatus;
return 'idle';
},
hasMissingSourceSearchAttempt(missing = {}, source = '', state = null) {
const searchState = state || this.searchResultCache?.get(this.getMissingSearchKey(missing));
const explicitSources = new Set(
Array.isArray(searchState?.explicitSearchSources) ? searchState.explicitSearchSources : []
);
return explicitSources.has(source) || explicitSources.has('all');
},
isLocalDatabaseDownloadSource(downloadSource = {}) {
const source = String(downloadSource?.source || '')
.trim()
.toLowerCase()
.replace(/-/g, '_');
return source === 'local' || source === 'model_list' || source === 'popular';
},
shouldDisplayKnownDownloadSource(missing = {}, downloadSource = {}, state = null) {
if (!downloadSource?.url) return false;
if (!this.isLocalDatabaseDownloadSource(downloadSource)) return true;
return this.hasMissingSourceSearchAttempt(missing, 'local', state);
},
getMissingSourceResultStatus(missing = {}, source = '', state = null) {
const results = state?.results || {};
const candidates = [];
if (source === 'local') {
if (!this.hasMissingSourceSearchAttempt(missing, source, state)) return '';
candidates.push(results.model_list, results.popular);
} else if (source === 'huggingface') {
candidates.push(results.huggingface);
} else if (source === 'civitai') {
candidates.push(results.civitai);
} else if (source === 'civarchive') {
candidates.push(results.civarchive);
} else if (source === 'lora_manager_archive') {
candidates.push(results.lora_manager_archive);
}
if (['huggingface', 'civitai', 'civarchive'].includes(source)) {
const customResults = Array.isArray(results.custom) ? results.custom : [];
customResults
.filter(result => String(result?.source || '').toLowerCase().replace(/-/g, '_') === source)
.forEach(result => candidates.push(result));
}
const downloadSource = missing.download_source || {};
const mappedDownloadSource = {
model_list: 'local',
popular: 'local',
workflow: 'civitai'
}[downloadSource.source] || downloadSource.source;
if (mappedDownloadSource === source && downloadSource.url) {
candidates.push(downloadSource);
}
return this.getSearchResultStatusLevel(candidates);
},
getSearchResultStatusLevel(resultOrResults) {
const results = Array.isArray(resultOrResults)
? resultOrResults.flatMap(result => Array.isArray(result) ? result : [result])
: [resultOrResults];
let hasPartial = false;
let hasUnknownFound = false;
for (const result of results) {
if (!result) continue;
const confidence = Number(result.confidence);
const hasConfidence = Number.isFinite(confidence);
const matchType = String(result.match_type || '').toLowerCase();
if (hasConfidence) {
if (confidence >= 100) return 'exact';
if (confidence > 0) {
hasPartial = true;
continue;
}
}
if (matchType === 'exact' || result.source === 'popular') {
return 'exact';
}
if (matchType === 'partial' || matchType === 'fuzzy' || matchType === 'similar') {
hasPartial = true;
continue;
}
if (result.url || result.download_url) {
hasUnknownFound = true;
}
}
if (hasPartial) return 'partial';
if (hasUnknownFound) return 'found';
return '';
},
renderMissingSourcesSummary(missing = {}) {
const sourceItems = this.getEnabledSearchSources().map(source => ({ source }));
@@ -424,15 +292,6 @@ export const missingBrowserMethods = {
}).join('');
},
hasRenderableSearchState(state = {}) {
return Boolean(
(state.lastAttemptSources || []).length
|| Object.keys(state.sourceProgress || {}).length
|| this.hasSearchResults(state.results || {})
|| state.lastAttemptError
);
},
getMissingModelsListLayout(missingModels = []) {
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
@@ -456,11 +315,6 @@ export const missingBrowserMethods = {
};
},
isMissingModelResolved(missing = {}) {
if (!missing) return false;
return Boolean(missing.__isExistingResolved);
},
getResolvedWorkflowModels(data = {}) {
return (data.resolved_models || []).map(model => this.normalizeResolvedWorkflowModel(model));
},
@@ -579,16 +433,6 @@ export const missingBrowserMethods = {
return selected;
},
isAutoDownloadModel(missing = {}) {
return Boolean(missing?.auto_download_capable || missing?.auto_download_candidate);
},
getResolvedMissingCount(missingModels = []) {
return missingModels.reduce((count, missing) => (
count + (this.isMissingModelResolved(missing) ? 1 : 0)
), 0);
},
refreshMissingModelsBrowserFromCache() {
if (this.activeTab !== 'missing' || !this.contentElement || !this.cachedAnalysisData) return;
this.displayMissingModels(
@@ -0,0 +1,157 @@
export const missingModelStateMethods = {
getBestLocalMatch(missing = {}, minConfidence = 0) {
const matches = Array.isArray(missing.matches) ? missing.matches : [];
return matches
.filter(match => Number(match.confidence) >= minConfidence)
.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0))[0] || null;
},
getMissingModelSummaryStats(missingModels = []) {
return missingModels.reduce((stats, missing) => {
const best = this.getBestLocalMatch(missing, 70);
if (best?.confidence === 100) {
stats.exact += 1;
} else if (best) {
stats.partial += 1;
} else {
stats.none += 1;
}
return stats;
}, { exact: 0, partial: 0, none: 0 });
},
getMissingSourceStatus(missing = {}, source = '') {
const state = this.searchResultCache.get(this.getMissingSearchKey(missing));
const progress = state?.sourceProgress?.[source];
const resultStatus = this.getMissingSourceResultStatus(missing, source, state);
if (progress?.status === 'found') return resultStatus || 'found';
if (progress?.status) return progress.status;
if (resultStatus) return resultStatus;
return 'idle';
},
hasMissingSourceSearchAttempt(missing = {}, source = '', state = null) {
const searchState = state || this.searchResultCache?.get(this.getMissingSearchKey(missing));
const explicitSources = new Set(
Array.isArray(searchState?.explicitSearchSources) ? searchState.explicitSearchSources : []
);
return explicitSources.has(source) || explicitSources.has('all');
},
isLocalDatabaseDownloadSource(downloadSource = {}) {
const source = String(downloadSource?.source || '')
.trim()
.toLowerCase()
.replace(/-/g, '_');
return source === 'local' || source === 'model_list' || source === 'popular';
},
shouldDisplayKnownDownloadSource(missing = {}, downloadSource = {}, state = null) {
if (!downloadSource?.url) return false;
if (!this.isLocalDatabaseDownloadSource(downloadSource)) return true;
return this.hasMissingSourceSearchAttempt(missing, 'local', state);
},
getMissingSourceResultStatus(missing = {}, source = '', state = null) {
const results = state?.results || {};
const candidates = [];
if (source === 'local') {
if (!this.hasMissingSourceSearchAttempt(missing, source, state)) return '';
candidates.push(results.model_list, results.popular);
} else if (source === 'huggingface') {
candidates.push(results.huggingface);
} else if (source === 'civitai') {
candidates.push(results.civitai);
} else if (source === 'civarchive') {
candidates.push(results.civarchive);
} else if (source === 'lora_manager_archive') {
candidates.push(results.lora_manager_archive);
}
if (['huggingface', 'civitai', 'civarchive'].includes(source)) {
const customResults = Array.isArray(results.custom) ? results.custom : [];
customResults
.filter(result => String(result?.source || '').toLowerCase().replace(/-/g, '_') === source)
.forEach(result => candidates.push(result));
}
const downloadSource = missing.download_source || {};
const mappedDownloadSource = {
model_list: 'local',
popular: 'local',
workflow: 'civitai'
}[downloadSource.source] || downloadSource.source;
if (mappedDownloadSource === source && downloadSource.url) {
candidates.push(downloadSource);
}
return this.getSearchResultStatusLevel(candidates);
},
getSearchResultStatusLevel(resultOrResults) {
const results = Array.isArray(resultOrResults)
? resultOrResults.flatMap(result => Array.isArray(result) ? result : [result])
: [resultOrResults];
let hasPartial = false;
let hasUnknownFound = false;
for (const result of results) {
if (!result) continue;
const confidence = Number(result.confidence);
const hasConfidence = Number.isFinite(confidence);
const matchType = String(result.match_type || '').toLowerCase();
if (hasConfidence) {
if (confidence >= 100) return 'exact';
if (confidence > 0) {
hasPartial = true;
continue;
}
}
if (matchType === 'exact' || result.source === 'popular') {
return 'exact';
}
if (matchType === 'partial' || matchType === 'fuzzy' || matchType === 'similar') {
hasPartial = true;
continue;
}
if (result.url || result.download_url) {
hasUnknownFound = true;
}
}
if (hasPartial) return 'partial';
if (hasUnknownFound) return 'found';
return '';
},
hasRenderableSearchState(state = {}) {
return Boolean(
(state.lastAttemptSources || []).length
|| Object.keys(state.sourceProgress || {}).length
|| this.hasSearchResults(state.results || {})
|| state.lastAttemptError
);
},
isMissingModelResolved(missing = {}) {
if (!missing) return false;
return Boolean(missing.__isExistingResolved);
},
isAutoDownloadModel(missing = {}) {
return Boolean(missing?.auto_download_capable || missing?.auto_download_candidate);
},
getResolvedMissingCount(missingModels = []) {
return missingModels.reduce((count, missing) => (
count + (this.isMissingModelResolved(missing) ? 1 : 0)
), 0);
},
};