diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index de5ed9f..fa24a7b 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -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: [], diff --git a/tests/test_missing_model_state_methods.mjs b/tests/test_missing_model_state_methods.mjs index a6ab52b..f17edea 100644 --- a/tests/test_missing_model_state_methods.mjs +++ b/tests/test_missing_model_state_methods.mjs @@ -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 { diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index 0d3f566..4268a02 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -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, diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 6aaf181..1736bec 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -268,138 +268,6 @@ export const missingBrowserMethods = { return `${content}`; }, - 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( diff --git a/web/resolver/views/missing_model_state_methods.js b/web/resolver/views/missing_model_state_methods.js new file mode 100644 index 0000000..1bd2ba7 --- /dev/null +++ b/web/resolver/views/missing_model_state_methods.js @@ -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); + }, +};