refactor(search): preserve workflow path context and standardize tooltips

This commit is contained in:
Dariusz L
2026-07-20 12:12:34 +02:00
parent a4460afa65
commit 2f7a2c5869
3 changed files with 387 additions and 67 deletions
+185 -5
View File
@@ -188,7 +188,8 @@ test('auto base model uses Any model for standalone SAM and Ultralytics models',
assert.equal(getMissingAutoBaseModel.call(dialog, missing), '');
assert.equal(getSearchBaseModelLabel.call(dialog, 'auto', missing), 'Auto (Any model)');
assert.equal(getSearchBaseModelContext.call(dialog, missing), '');
assert.match(getMissingAutoBaseModelInfo.call(dialog, missing).message, /Auto uses Any model/);
assert.match(getMissingAutoBaseModelInfo.call(dialog, missing).message, /Selected automatically: Any model/);
assert.match(getMissingAutoBaseModelInfo.call(dialog, missing).message, /no model filter is needed/);
}
state.selectedBaseModel = 'SDXL 1.0';
@@ -199,6 +200,54 @@ test('auto base model uses Any model for standalone SAM and Ultralytics models',
);
});
test('auto base model keeps workflow path context when a delayed search result disagrees', () => {
const getMissingAutoBaseModelInfo = eval(`(${extractMethod(searchPanelMethodsSource, 'getMissingAutoBaseModelInfo')})`);
const missing = {
original_path: 'KREA2\\KNPV3_1.safetensors',
civitai_search_result: { base_model: 'Flux.1 D' }
};
const dialog = {
getBaseModelIndependentSearchType() {
return '';
},
getCachedSearchSuggestionData() {
return { base_model: 'Flux.1 D' };
},
getSavedDownloadTargetSelection() {
return {
subfolder: 'FLUX',
subfolderTouched: false,
subfolderSuggestionAppliedBy: 'auto'
};
},
getMissingLocalBaseModel() {
return '';
},
resolveBaseModelAlias(value = '') {
if (value === 'Flux.1 D') return 'Flux.1 D';
if (value === 'Krea 2') return 'Krea 2';
return '';
},
resolveBaseModelAliasExact(value = '') {
return this.resolveBaseModelAlias(value);
},
resolveBaseModelAliasFromPath(value = '') {
return /(^|[\\/])KREA2([\\/]|$)/i.test(String(value)) ? 'Krea 2' : '';
},
getDominantWorkflowBaseModel() {
return 'Flux.1 D';
}
};
const info = getMissingAutoBaseModelInfo.call(dialog, missing);
assert.equal(info.value, 'Krea 2');
assert.equal(info.source, 'missing model path');
assert.match(info.message, /Selected automatically: Krea 2/);
assert.match(info.message, /Why: The model path saved in this workflow/);
assert.match(info.message, /To change: Choose a different option in the Model field/);
});
test('base model path mapping ignores conflicting full-path base model', () => {
const normalizeBaseModelToken = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeBaseModelToken')})`);
const getBaseModelTokenVariants = eval(`(${extractMethod(searchPanelMethodsSource, 'getBaseModelTokenVariants')})`);
@@ -286,8 +335,26 @@ test('download subfolder tooltip explains automatic suggestion source', () => {
const tooltip = getDownloadSubfolderTooltip.call(dialog, { node_id: 1 }, 'loras', 'SDXL/Style');
assert.match(tooltip, /Auto selected SDXL\\Style/);
assert.match(tooltip, /base model \(SDXL\) and tag \(style\)/);
assert.match(tooltip, /Suggested subfolder: SDXL\\Style/);
assert.match(tooltip, /identified as SDXL and tagged/);
});
test('download subfolder tooltip identifies a folder taken from the workflow model path', () => {
const getDownloadSubfolderSuggestionReason = eval(`(${extractMethod(downloadTargetMethodsSource, 'getDownloadSubfolderSuggestionReason')})`);
const dialog = {
normalizeFolderToken(value = '') {
return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
}
};
const reason = getDownloadSubfolderSuggestionReason.call(dialog, {
suggestionSource: 'model_name',
matchedCandidate: 'KREA2'
}, {
original_path: 'KREA2\\KNPV3_1.safetensors'
});
assert.match(reason, /model path saved in this workflow.*folder.*KREA2/);
});
test('download subfolder tooltip explains Suggest button choice', () => {
@@ -333,8 +400,8 @@ test('download subfolder tooltip explains Suggest button choice', () => {
const tooltip = getDownloadSubfolderTooltip.call(dialog, { node_id: 1 }, 'loras', 'Pony\\Styles', { saved });
assert.match(tooltip, /because you clicked Suggest/);
assert.match(tooltip, /path template \(\{base_model\}\/\{first_tag\}\)/);
assert.match(tooltip, /Selected with Suggest: Pony\\Styles/);
assert.match(tooltip, /saved folder rule.*\{base_model\}\/\{first_tag\}/);
});
test('post-search subfolder suggestion can prefer path template metadata', () => {
@@ -500,6 +567,119 @@ test('search suggestion metadata prefers exact matching base model over weaker a
assert.equal(merged.filename, 'snofs_krea_v1.safetensors');
});
test('search suggestion does not let a delayed incompatible result replace workflow path context', () => {
const getCachedSearchSuggestionData = eval(`(${extractMethod(downloadTargetMethodsSource, 'getCachedSearchSuggestionData')})`);
const getFirstSearchResult = eval(`(${extractMethod(downloadTargetMethodsSource, 'getFirstSearchResult')})`);
const getSearchSuggestionPreferredBaseModel = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSearchSuggestionPreferredBaseModel')})`);
const baseModelMatchesSearchSuggestionPreference = eval(`(${extractMethod(downloadTargetMethodsSource, 'baseModelMatchesSearchSuggestionPreference')})`);
const getCompatibleCivitaiSearchResult = eval(`(${extractMethod(downloadTargetMethodsSource, 'getCompatibleCivitaiSearchResult')})`);
const getSearchSuggestionResultScore = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSearchSuggestionResultScore')})`);
const state = {
selectedBaseModel: 'auto',
results: {
civitai: {
base_model: 'Flux.1 D',
filename: 'KNPV3_1.safetensors',
match_type: 'similar',
confidence: 80
}
}
};
const dialog = {
searchResultCache: new Map([['missing-key', state]]),
getCachedSearchSuggestionData,
getFirstSearchResult,
getSearchSuggestionPreferredBaseModel,
baseModelMatchesSearchSuggestionPreference,
getCompatibleCivitaiSearchResult,
getSearchSuggestionResultScore,
getMissingSearchKey() {
return 'missing-key';
},
getSavedDownloadTargetSelection() {
return null;
},
getMissingLocalBaseModel() {
return '';
},
resolveBaseModelAliasFromPath(value = '') {
return /(^|[\\/])KREA2([\\/]|$)/i.test(String(value)) ? 'Krea 2' : '';
},
normalizeBaseModelToken(value = '') {
return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
},
resolveBaseModelAlias(value = '') {
const token = this.normalizeBaseModelToken(value);
if (token === 'krea2') return 'Krea 2';
if (token === 'flux1d') return 'Flux.1 D';
return '';
},
resolveBaseModelAliasExact(value = '') {
return this.resolveBaseModelAlias(value);
},
getSourceResultDownloadCategory() {
return 'loras';
}
};
const missing = {
category: 'loras',
original_path: 'KREA2\\KNPV3_1.safetensors',
civitai_search_result: { base_model: 'Flux.1 D' }
};
assert.equal(getSearchSuggestionPreferredBaseModel.call(dialog, missing), 'Krea 2');
assert.deepEqual(getCompatibleCivitaiSearchResult.call(dialog, missing), {});
assert.deepEqual(getCachedSearchSuggestionData.call(dialog, missing), {});
});
test('subfolder suggestion ignores hidden local matches below the visible confidence threshold', () => {
const getSuggestedModelSubfolderCandidates = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSuggestedModelSubfolderCandidates')})`);
const getSuggestedExistingSubfolderByModelName = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSuggestedExistingSubfolderByModelName')})`);
const dialog = {
getSuggestedModelSubfolderCandidates,
getSuggestedExistingSubfolderByModelName,
getBestLocalMatch(missing, minConfidence) {
return (missing.matches || [])
.filter(match => Number(match.confidence) >= minConfidence)
.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0))[0] || null;
},
getCompatibleCivitaiSearchResult() {
return {};
},
getCachedSearchSuggestionData() {
return {};
},
normalizeFolderToken(value = '') {
return String(value || '')
.toLowerCase()
.replace(/[\\/]+/g, ' ')
.replace(/[^a-z0-9]+/g, '');
}
};
const missing = {
original_path: 'KNPV3_1.safetensors',
name: 'KNPV3_1.safetensors',
matches: [{
confidence: 55,
model: {
filename: 'KNPV4.1_pre.safetensors',
relative_path: 'FLUX\\KREA\\concept\\KNPV4.1_pre.safetensors'
}
}]
};
const folders = [{
value: 'FLUX',
segments: ['FLUX'],
normalizedSegments: ['flux']
}];
const candidates = getSuggestedModelSubfolderCandidates.call(dialog, missing);
const suggestion = getSuggestedExistingSubfolderByModelName.call(dialog, missing, folders);
assert.equal(candidates.some(candidate => candidate.normalized === 'flux'), false);
assert.equal(suggestion, null);
});
test('auto-link 100 percent applies visible exact matches without re-analyzing', async () => {
const autoResolve100Percent = eval(`(${extractMethod(resolveDownloadMethodsSource, 'autoResolve100Percent')})`);
const getExactLocalMatchSelections = eval(`(${extractMethod(resolveDownloadMethodsSource, 'getExactLocalMatchSelections')})`);
+138 -33
View File
@@ -445,6 +445,9 @@ export const downloadTargetMethods = {
},
getDownloadCategoryTooltip(missing = {}, selectedCategory = '', options = {}) {
const formatFolderTooltip = (heading, value, reason, action = 'Choose another option in the Folder field.') => (
`${heading}: ${value}\n\nWhy: ${reason}\n\nTo change: ${action}`
);
const category = this.normalizeDownloadCategory(selectedCategory || 'checkpoints');
const label = this.getCategoryDisplayName(category);
const saved = options.saved || this.getSavedDownloadTargetSelection(missing);
@@ -452,27 +455,47 @@ export const downloadTargetMethods = {
const preserveSavedCategory = options.preserveSavedCategory ?? this.shouldPreserveSavedDownloadCategory(missing, saved, inferredCategory);
if (preserveSavedCategory) {
return `Folder is set to ${label} because you manually changed it for this missing model. Model Resolver remembers that choice for this workflow entry.`;
return formatFolderTooltip(
'Selected manually',
label,
'You changed this folder earlier, so Model Resolver kept your choice for this workflow.'
);
}
const nodeCategory = this.getMissingNodeTypeDownloadCategory(missing);
if (nodeCategory && this.normalizeDownloadCategory(nodeCategory) === category) {
const nodeType = missing.locate_node_type || missing.promoted_inner_node_type || missing.node_type || 'this node';
return `Auto selected ${label} because the node type (${nodeType}) uses this kind of model folder.`;
return formatFolderTooltip(
'Selected automatically',
label,
`The workflow node “${nodeType}” uses this type of model.`
);
}
const rawMissingCategory = missing.category || missing.directory || '';
const missingCategory = rawMissingCategory ? this.normalizeDownloadCategory(rawMissingCategory) : '';
if (missingCategory && missingCategory === category) {
return `Auto selected ${label} from the model category detected in the workflow: ${rawMissingCategory}.`;
return formatFolderTooltip(
'Selected automatically',
label,
`The workflow identifies this as a ${this.getCategoryDisplayName(missingCategory)} model.`
);
}
const defaultCategory = this.normalizeDownloadCategory(options.defaultCategory || 'checkpoints');
if (defaultCategory && defaultCategory === category) {
return `Auto selected ${label} from the download source or default folder for this result.`;
return formatFolderTooltip(
'Selected automatically',
label,
'This is the folder recommended by the selected download source or your default settings.'
);
}
return `Auto selected ${label} as the best available download folder. If this is wrong, choose another folder manually.`;
return formatFolderTooltip(
'Selected automatically',
label,
'This is the closest matching download folder available for this model.'
);
},
getSubfolderSuggestionTrackingPatch(suggestion = null, appliedBy = '') {
@@ -497,28 +520,40 @@ export const downloadTargetMethods = {
};
},
getDownloadSubfolderSuggestionReason(suggestion = {}) {
getDownloadSubfolderSuggestionReason(suggestion = {}, missing = {}) {
const source = suggestion?.suggestionSource || '';
if (source === 'template') {
const template = suggestion.template ? ` (${suggestion.template})` : '';
return `your path template${template} matched the available model metadata`;
const template = suggestion.template ? ` “${suggestion.template}”` : '';
return `Your saved folder rule${template} produced this subfolder from the available model information.`;
}
if (source === 'lora_metadata') {
const baseModel = suggestion.matchedBaseModel || '';
const tag = suggestion.matchedTag || '';
if (baseModel && tag) {
return `this LoRA's base model (${baseModel}) and tag (${tag}) match an existing folder`;
return `The LoRA is identified as ${baseModel} and tagged “${tag}”; an existing folder matches both.`;
}
if (baseModel) {
return `this LoRA's base model (${baseModel}) matches an existing folder`;
return `The LoRA is identified as ${baseModel}, and an existing folder has the same name.`;
}
return `this LoRA's metadata matches an existing folder`;
return `The available LoRA information matches an existing folder.`;
}
if (source === 'model_name') {
const candidate = suggestion.matchedCandidate || '';
const candidateToken = this.normalizeFolderToken(candidate);
const workflowPathParts = String(missing?.original_path || '')
.split(/[/\\]+/)
.map(part => part.trim())
.filter(Boolean);
const workflowDirectoryParts = workflowPathParts.slice(0, -1);
const matchingWorkflowDirectory = workflowDirectoryParts.find(part => (
candidateToken && this.normalizeFolderToken(part) === candidateToken
));
if (matchingWorkflowDirectory) {
return `The model path saved in this workflow already contains the folder “${matchingWorkflowDirectory}”, and the same download folder exists on this computer.`;
}
return candidate
? `the model name or filename (${candidate}) matches an existing folder`
: `the model name or filename matches an existing folder`;
? `Part of the model name, “${candidate}”, matches an existing download folder.`
: `The model name matches an existing download folder.`;
}
return '';
},
@@ -560,6 +595,9 @@ export const downloadTargetMethods = {
},
getDownloadSubfolderTooltip(missing = {}, selectedCategory = '', selectedSubfolder = '', options = {}) {
const formatSubfolderTooltip = (heading, value, reason, finalLabel = 'To change', finalText = 'Type or choose another subfolder.') => (
`${heading}: ${value}\n\nWhy: ${reason}\n\n${finalLabel}: ${finalText}`
);
const category = this.normalizeDownloadCategory(selectedCategory || 'checkpoints');
const categoryLabel = this.getCategoryDisplayName(category);
const saved = options.saved || this.getSavedDownloadTargetSelection(missing);
@@ -568,33 +606,67 @@ export const downloadTargetMethods = {
const mode = this.getDownloadPathMode();
const savedSuggestion = this.getSavedDownloadSubfolderSuggestion(saved, subfolder, baseDirectory);
const currentSuggestion = savedSuggestion || this.getCurrentDownloadSubfolderSuggestion(missing, category, subfolder, baseDirectory);
const suggestionReason = this.getDownloadSubfolderSuggestionReason(currentSuggestion);
const suggestionReason = this.getDownloadSubfolderSuggestionReason(currentSuggestion, missing);
if (saved?.subfolderTouched) {
if (!subfolder) {
return `Subfolder is empty because you manually cleared it for this missing model. The file will download directly into the ${categoryLabel} folder.`;
return formatSubfolderTooltip(
'Subfolder',
'None',
'You cleared this field manually, so Model Resolver kept it empty.',
'Save location',
`The file will be saved directly in the ${categoryLabel} folder.`
);
}
if (saved?.subfolderSuggestionAppliedBy === 'button' && suggestionReason) {
return `Subfolder is set to ${subfolder} because you clicked Suggest. The suggestion was chosen because ${suggestionReason}. Model Resolver remembers that choice for this workflow entry.`;
return formatSubfolderTooltip(
'Selected with Suggest',
subfolder,
suggestionReason
);
}
return `Subfolder is set to ${subfolder} because you manually changed it for this missing model. Model Resolver remembers that choice for this workflow entry.`;
return formatSubfolderTooltip(
'Selected manually',
subfolder,
'You changed this subfolder earlier, so Model Resolver kept your choice for this workflow.'
);
}
if (subfolder) {
return suggestionReason
? `Auto selected ${subfolder} because ${suggestionReason}. If this is wrong, type or pick another subfolder manually.`
: `Auto selected ${subfolder} as the best available subfolder for this model. If this is wrong, type or pick another subfolder manually.`;
return formatSubfolderTooltip(
'Suggested subfolder',
subfolder,
suggestionReason || 'This is the closest matching existing folder for the available model information.'
);
}
if (mode === 'manual') {
return `Subfolder is empty because Subfolder fill mode is Manual. Type a path or click Suggest to choose one for this download.`;
return formatSubfolderTooltip(
'Subfolder',
'None',
'Automatic subfolder suggestions are set to Manual in Options.',
'To add one',
'Type a subfolder or click Suggest.'
);
}
if (!this.isAutoFillSubfolderEnabled()) {
return `Subfolder is empty because Auto-fill subfolder is disabled. Type a path or click Suggest to choose one for this download.`;
return formatSubfolderTooltip(
'Subfolder',
'None',
'Automatic subfolder suggestions are disabled in Options.',
'To add one',
'Type a subfolder or click Suggest.'
);
}
return `No subfolder was selected automatically because Model Resolver did not find a reliable metadata, tag, filename, or existing-folder match. The file will download directly into the ${categoryLabel} folder.`;
return formatSubfolderTooltip(
'Subfolder',
'None',
'Model Resolver did not find enough reliable information to choose one.',
'Save location',
`The file will be saved directly in the ${categoryLabel} folder.`
);
},
refreshDownloadTargetHelp(container, missing, categoryEl = null, subfolderEl = null) {
@@ -1231,9 +1303,10 @@ export const downloadTargetMethods = {
getDownloadPathMetadata(missing = {}, source = {}) {
const sourceData = source && typeof source === 'object' ? source : {};
const searchSuggestion = this.getCachedSearchSuggestionData(missing);
const compatibleCivitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {};
const merged = {
...(missing?.civitai_info || {}),
...(missing?.civitai_search_result || {}),
...compatibleCivitaiSearch,
...(missing?.download_source || {}),
...(searchSuggestion || {}),
...sourceData
@@ -1269,9 +1342,10 @@ export const downloadTargetMethods = {
getDownloadMetadata(missing = {}, source = {}, options = {}) {
const sourceData = source && typeof source === 'object' ? source : {};
const searchSuggestion = this.getCachedSearchSuggestionData(missing);
const compatibleCivitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {};
const merged = {
...(missing?.civitai_info || {}),
...(missing?.civitai_search_result || {}),
...compatibleCivitaiSearch,
...(missing?.download_source || {}),
...(searchSuggestion || {}),
...sourceData
@@ -1453,13 +1527,17 @@ export const downloadTargetMethods = {
if (selectedBaseModel && selectedBaseModel !== 'auto' && selectedBaseModel !== 'none') {
addCandidate(selectedBaseModel);
}
addCandidate(missing?.civitai_search_result?.base_model);
addCandidate(missing?.civitai_search_result?.baseModel);
addCandidate(missing?.civitai_info?.base_model);
addCandidate(missing?.civitai_info?.baseModel);
addCandidate(missing?.download_source?.base_model);
addCandidate(missing?.download_source?.baseModel);
addCandidate(missing?.civitai_info?.base_model);
addCandidate(missing?.civitai_info?.baseModel);
addCandidate(this.getMissingLocalBaseModel?.(missing, 70));
const savedTarget = this.getSavedDownloadTargetSelection?.(missing) || {};
if (savedTarget.subfolderTouched) {
addCandidate(this.resolveBaseModelAliasFromPath?.(savedTarget.subfolder));
}
addCandidate(this.resolveBaseModelAliasFromPath?.(missing?.original_path));
addCandidate(this.resolveBaseModelAliasFromPath?.(missing?.name));
for (const candidate of candidates) {
const canonical = this.resolveBaseModelAlias?.(candidate)
@@ -1489,6 +1567,22 @@ export const downloadTargetMethods = {
return Boolean(preferredToken && resultToken && preferredToken === resultToken);
},
getCompatibleCivitaiSearchResult(missing = {}) {
const result = missing?.civitai_search_result;
if (!result || typeof result !== 'object') return {};
const preferredBaseModel = this.getSearchSuggestionPreferredBaseModel(missing);
const resultBaseModel = result.base_model || result.baseModel || '';
if (
preferredBaseModel
&& resultBaseModel
&& !this.baseModelMatchesSearchSuggestionPreference(resultBaseModel, preferredBaseModel)
) {
return {};
}
return result;
},
getSearchSuggestionResultScore(source = '', result = {}, preferredBaseModel = '') {
if (!result || typeof result !== 'object') return -Infinity;
@@ -1574,6 +1668,12 @@ export const downloadTargetMethods = {
result: this.getFirstSearchResult(results[source])
}))
.filter(candidate => candidate.result && typeof candidate.result === 'object')
.filter(candidate => {
if (!preferredBaseModel) return true;
const resultBaseModel = candidate.result.base_model || candidate.result.baseModel || '';
return !resultBaseModel
|| this.baseModelMatchesSearchSuggestionPreference(resultBaseModel, preferredBaseModel);
})
.map(candidate => ({
...candidate,
score: this.getSearchSuggestionResultScore(candidate.source, candidate.result, preferredBaseModel)
@@ -1626,10 +1726,11 @@ export const downloadTargetMethods = {
}
const searchSuggestion = this.getCachedSearchSuggestionData(missing);
const compatibleCivitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {};
const civitaiData = {
...(missing?.civitai_info || {}),
...(searchSuggestion || {}),
...(missing?.civitai_search_result || {}),
...compatibleCivitaiSearch,
...(missing?.download_source || {})
};
const baseModel = civitaiData.base_model || '';
@@ -1685,11 +1786,15 @@ export const downloadTargetMethods = {
getSuggestedModelSubfolderCandidates(missing = {}) {
const source = missing.download_source || {};
const civitaiInfo = missing.civitai_info || {};
const civitaiSearch = missing.civitai_search_result || {};
const civitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {};
const searchSuggestion = this.getCachedSearchSuggestionData(missing);
const localMatches = Array.isArray(missing.matches) ? missing.matches : [];
const bestLocalMatch = localMatches
.filter(match => match && typeof match === 'object')
const bestLocalMatch = this.getBestLocalMatch?.(missing, 70) || localMatches
.filter(match => (
match
&& typeof match === 'object'
&& Number(match.confidence) >= 70
))
.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0))[0] || null;
const bestLocalModel = bestLocalMatch?.model || {};
const bestLocalPath = bestLocalModel.relative_path
+64 -29
View File
@@ -226,24 +226,28 @@ export const searchPanelMethods = {
},
getMissingAutoBaseModelInfo(missing = {}) {
const formatAutoMessage = (selected, reason, action = 'Choose a different option in the Model field.') => (
`Selected automatically: ${selected}\n\nWhy: ${reason}\n\nTo change: ${action}`
);
const independentType = this.getBaseModelIndependentSearchType(missing);
if (independentType) {
return {
value: '',
source: 'independent model type',
usesAnyModel: true,
message: `${independentType} models are not tied to a base model, so Auto uses Any model.`
message: formatAutoMessage(
'Any model',
`${independentType} models are not tied to one base model, so no model filter is needed.`
)
};
}
const searchSuggestion = this.getCachedSearchSuggestionData?.(missing) || {};
const directCandidates = [
{ value: missing?.civitai_info?.base_model, source: 'model metadata' },
{ value: missing?.civitai_info?.baseModel, source: 'model metadata' },
{ value: missing?.civitai_search_result?.base_model, source: 'search result metadata' },
{ value: missing?.civitai_search_result?.baseModel, source: 'search result metadata' },
{ value: missing?.download_source?.base_model, source: 'selected download source' },
{ value: missing?.download_source?.baseModel, source: 'selected download source' }
{ value: missing?.download_source?.baseModel, source: 'selected download source' },
{ value: missing?.civitai_info?.base_model, source: 'model metadata' },
{ value: missing?.civitai_info?.baseModel, source: 'model metadata' }
];
for (const { value, source } of directCandidates) {
@@ -252,15 +256,23 @@ export const searchPanelMethods = {
return {
value: canonical,
source,
message: `Auto selected ${canonical} from ${source}.`
message: formatAutoMessage(
canonical,
source === 'selected download source'
? `The download source you selected identifies this model as ${canonical}.`
: `The model information saved with this workflow identifies it as ${canonical}.`
)
};
}
}
const savedTarget = this.getSavedDownloadTargetSelection?.(missing) || {};
const explicitlySelectedSubfolder = savedTarget.subfolderTouched
? savedTarget.subfolder
: '';
const pathCandidates = [
{ value: this.getMissingLocalBaseModel(missing, 70), source: 'best local match' },
{ value: savedTarget.subfolder, source: 'selected subfolder' }
{ value: explicitlySelectedSubfolder, source: 'selected subfolder' }
];
for (const { value, source } of pathCandidates) {
@@ -270,22 +282,12 @@ export const searchPanelMethods = {
return {
value: canonical,
source,
message: `Auto selected ${canonical} from the ${source}.`
};
}
}
const cachedCandidates = [
{ value: searchSuggestion.base_model, source: 'cached search result' },
{ value: searchSuggestion.baseModel, source: 'cached search result' }
];
for (const { value, source } of cachedCandidates) {
const canonical = this.resolveBaseModelAlias(value);
if (canonical) {
return {
value: canonical,
source,
message: `Auto selected ${canonical} from ${source} metadata.`
message: formatAutoMessage(
canonical,
source === 'best local match'
? `A reliable local match is stored in a folder associated with ${canonical}.`
: `The subfolder you selected is associated with ${canonical}.`
)
};
}
}
@@ -301,7 +303,32 @@ export const searchPanelMethods = {
return {
value: canonical,
source,
message: `Auto selected ${canonical} from the ${source}.`
message: formatAutoMessage(
canonical,
source === 'missing model path'
? `The model path saved in this workflow points to a folder associated with ${canonical}.`
: source === 'missing model name'
? `The model name saved in this workflow contains a name associated with ${canonical}.`
: `A previously found model path points to a folder associated with ${canonical}.`
)
};
}
}
const cachedCandidates = [
{ value: searchSuggestion.base_model, source: 'cached search result' },
{ value: searchSuggestion.baseModel, source: 'cached search result' }
];
for (const { value, source } of cachedCandidates) {
const canonical = this.resolveBaseModelAlias(value);
if (canonical) {
return {
value: canonical,
source,
message: formatAutoMessage(
canonical,
`A compatible search result identifies this model as ${canonical}.`
)
};
}
}
@@ -311,14 +338,22 @@ export const searchPanelMethods = {
return {
value: workflowFallback,
source: 'workflow fallback',
message: `This model was not recognized in the Base Models list, so Auto is using the workflow-wide fallback: ${workflowFallback}. If this looks wrong, open Options and update the local database / Base Models list.`
message: formatAutoMessage(
workflowFallback,
`This model could not be identified directly, so Model Resolver used the most common model type in this workflow.`,
'Choose another model here, or update the Base Models list in Options.'
)
};
}
return {
value: '',
source: 'none',
message: 'Auto could not detect a base model for this entry. Open Options and update the local database / Base Models list, or choose a model manually.'
message: formatAutoMessage(
'No model filter',
'There is not enough reliable information to identify the model type.',
'Choose a model here, or update the Base Models list in Options.'
)
};
},
@@ -326,10 +361,10 @@ export const searchPanelMethods = {
const state = this.getSearchState(missing);
const selected = state.selectedBaseModel || this.getDefaultSearchBaseModel();
if (selected === 'none') {
return 'Search will use Any model and will not filter by base model.';
return 'Selected: Any model\n\nWhy: Searches will not be limited to one model type.\n\nTo change: Choose Auto or a specific model.';
}
if (selected && selected !== 'auto') {
return `Search model filter is manually set to ${selected}.`;
return `Selected manually: ${selected}\n\nWhy: Searches are limited to this model type.\n\nTo change: Choose Auto, Any model, or another model.`;
}
return this.getMissingAutoBaseModelInfo(missing).message;
},