refactor(frontend): extract missing model state
This commit is contained in:
@@ -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: [],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user