refactor: unify local match classification

This commit is contained in:
Dariusz L
2026-08-06 20:01:20 +02:00
parent ac5f22736d
commit 2ef86fed3b
5 changed files with 169 additions and 25 deletions
+1
View File
@@ -10,6 +10,7 @@ import {
syncElementAttributes,
} from '../web/resolver/utils/dom_patch_utils.js';
import * as domPatchUtils from '../web/resolver/utils/dom_patch_utils.js';
import { classifyLocalMatches } from '../web/resolver/utils/local_match_utils.js';
const projectRoot = path.resolve(import.meta.dirname, '..');
const resolveDownloadMethodsSource = fs.readFileSync(
+125
View File
@@ -9,6 +9,7 @@ import {
safeStorage,
} from '../web/resolver/utils/html_utils.js';
import { getModelCardUrl } from '../web/resolver/utils/url_utils.js';
import { classifyLocalMatches } from '../web/resolver/utils/local_match_utils.js';
import { extractComfyWorkflow } from '../web/resolver/utils/workflow_metadata.js';
import {
buildModelResolverNodeMenu,
@@ -6020,6 +6021,130 @@ test('local match rows preserve best-match and alternative rendering contracts',
assert.equal((html.match(/class="status-group"/g) || []).length, 2);
});
test('local match classification preserves the confidence threshold and alternative partition', () => {
globalThis.getSvgIcon = () => '<svg></svg>';
const renderLocalMatchRow = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchRow')})`);
const renderLocalMatchesContent = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchesContent')})`);
const dialog = {
renderLocalMatchRow,
getHashMatchLabelMap() {
return null;
},
buildContextMenuModelData() {
return { model: 'context' };
},
getLocalMatchContextData() {
return { source: 'local' };
},
getLocalMatchIdentity(match) {
return match.identity;
},
escapeHtml(value) {
return String(value).replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[char]));
},
getContextMenuAttrs() {
return '';
},
getConfidenceBadge(confidence) {
return `<span class="confidence">${confidence}</span>`;
},
getModelPreviewTooltipAttrs() {
return '';
},
renderLocalMatchStatusGroup() {
return '';
},
areLocalMatchAlternativesCollapsed() {
return false;
},
};
const html = renderLocalMatchesContent.call(dialog, {
node_id: 1,
widget_index: 2,
matches: [
{ confidence: 69, identity: 'below-threshold', model: { relative_path: 'below.safetensors' } },
{ confidence: 70, identity: 'minimum-threshold', model: { relative_path: 'minimum.safetensors' } },
{ confidence: 80, identity: 'alternative', model: { relative_path: 'alternative.safetensors' } },
{ confidence: 100, identity: 'perfect', model: { relative_path: 'perfect.safetensors' } },
],
}, 0);
assert.match(html, /perfect\.safetensors/);
assert.match(html, /minimum\.safetensors/);
assert.match(html, /alternative\.safetensors/);
assert.doesNotMatch(html, /below\.safetensors/);
assert.match(html, /Alternatives \(2\)/);
});
test('local match classification limits visible non-perfect matches to the five highest confidences', () => {
globalThis.getSvgIcon = () => '<svg></svg>';
const renderLocalMatchRow = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchRow')})`);
const renderLocalMatchesContent = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchesContent')})`);
const dialog = {
renderLocalMatchRow,
getHashMatchLabelMap() {
return null;
},
buildContextMenuModelData() {
return { model: 'context' };
},
getLocalMatchContextData() {
return { source: 'local' };
},
getLocalMatchIdentity(match) {
return match.identity;
},
escapeHtml(value) {
return String(value).replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[char]));
},
getContextMenuAttrs() {
return '';
},
getConfidenceBadge(confidence) {
return `<span class="confidence">${confidence}</span>`;
},
getModelPreviewTooltipAttrs() {
return '';
},
renderLocalMatchStatusGroup() {
return '';
},
areLocalMatchAlternativesCollapsed() {
return false;
},
};
const html = renderLocalMatchesContent.call(dialog, {
node_id: 1,
widget_index: 2,
matches: [
{ confidence: 69, identity: 'below-threshold', model: { relative_path: 'below.safetensors' } },
...[70, 75, 80, 85, 90, 95].map(confidence => ({
confidence,
identity: `match-${confidence}`,
model: { relative_path: `match-${confidence}.safetensors` },
})),
],
}, 0);
assert.match(html, /match-95\.safetensors/);
assert.match(html, /match-75\.safetensors/);
assert.doesNotMatch(html, /match-70\.safetensors/);
assert.doesNotMatch(html, /below\.safetensors/);
assert.equal((html.match(/class="mr-match-row/g) || []).length, 5);
});
test('URN resolution preserves the download panel container', () => {
const resolveUrnAsync = extractMethod(resolveDownloadMethodsSource, 'resolveUrnAsync');
+13 -13
View File
@@ -5,6 +5,7 @@ import { getCivitaiModelUrl } from "../globals.js";
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
import { getSha256Field } from "../utils/hash_utils.js";
import { bindEventOnce } from "../utils/dom_patch_utils.js";
import { classifyLocalMatches } from "../utils/local_match_utils.js";
const localStorage = safeStorage;
export const searchPanelMethods = {
isAutoFillBaseModelEnabled() {
@@ -2399,18 +2400,19 @@ export const searchPanelMethods = {
renderLocalMatchesContent(missing, missingIndex = 0) {
const allMatches = missing.matches || [];
const filteredMatches = allMatches.filter(m => m.confidence >= 70);
const hasMatches = filteredMatches.length > 0;
const perfectMatches = filteredMatches.filter(m => m.confidence === 100);
const otherMatches = filteredMatches.filter(m => m.confidence < 100 && m.confidence >= 70);
const {
filteredMatches,
hasMatches,
perfectMatches,
otherMatches,
visibleMatches,
} = classifyLocalMatches(allMatches);
const hashLabelMap = this.getHashMatchLabelMap?.(missing) || null;
let html = '';
if (hasMatches) {
const matchesToShow = perfectMatches.length > 0
? perfectMatches
: otherMatches.sort((a, b) => b.confidence - a.confidence).slice(0, 5);
const matchesToShow = perfectMatches.length > 0 ? perfectMatches : visibleMatches;
const sortedMatches = matchesToShow.sort((a, b) => {
if (a.confidence === 100 && b.confidence !== 100) return -1;
@@ -2479,12 +2481,10 @@ export const searchPanelMethods = {
const matchIndex = Number(row?.dataset.localMatchIndex);
if (!row || !Number.isInteger(matchIndex) || matchIndex < 0) return;
const filteredMatches = (currentMissing.matches || []).filter(match => match.confidence >= 70);
const perfectMatches = filteredMatches.filter(match => match.confidence === 100);
const otherMatches = filteredMatches.filter(match => match.confidence < 100 && match.confidence >= 70);
const visibleMatches = perfectMatches.length > 0
? perfectMatches
: otherMatches.sort((left, right) => right.confidence - left.confidence).slice(0, 5);
const {
otherMatches,
visibleMatches,
} = classifyLocalMatches(currentMissing.matches || []);
const match = row.dataset.localMatchAlternative === 'true'
? otherMatches[matchIndex]
: visibleMatches[matchIndex];
+21
View File
@@ -0,0 +1,21 @@
export function classifyLocalMatches(
matches = [],
{ minConfidence = 70, perfectConfidence = 100, visibleLimit = 5 } = {},
) {
const filteredMatches = matches.filter(match => match.confidence >= minConfidence);
const perfectMatches = filteredMatches.filter(match => match.confidence === perfectConfidence);
const otherMatches = filteredMatches.filter(
match => match.confidence < perfectConfidence && match.confidence >= minConfidence,
);
const visibleMatches = perfectMatches.length > 0
? perfectMatches
: [...otherMatches].sort((left, right) => right.confidence - left.confidence).slice(0, visibleLimit);
return {
filteredMatches,
perfectMatches,
otherMatches,
visibleMatches,
hasMatches: filteredMatches.length > 0,
};
}
+9 -12
View File
@@ -4,6 +4,7 @@ import { startSplitterDrag } from "../utils/splitter_drag.js";
import { getCivitaiModelUrl } from "../globals.js";
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
import { matchesWorkflowModelReference } from "../node_context_menu.js";
import { classifyLocalMatches } from "../utils/local_match_utils.js";
const MISSING_ROW_FALLBACK_HEIGHT = 70;
const MISSING_VIRTUAL_INITIAL_ROWS = 24;
@@ -2409,7 +2410,7 @@ export const missingBrowserMethods = {
// Check if there are active downloads
const activeCount = Object.keys(this.activeDownloads).length;
const hasAny100Match = visibleMissingModels.some(missing =>
(missing.matches || []).some(match => match.confidence === 100)
classifyLocalMatches(missing.matches || []).perfectMatches.length > 0
);
const hasAnyModelsToDisplay = rawMissingCount > 0 || resolvedModels.length > 0;
@@ -2481,13 +2482,13 @@ export const missingBrowserMethods = {
const aMatches = a.matches || [];
const bMatches = b.matches || [];
// Filter to 70%+ confidence
const aFiltered = aMatches.filter(m => m.confidence >= 70);
const bFiltered = bMatches.filter(m => m.confidence >= 70);
const aClassification = classifyLocalMatches(aMatches);
const bClassification = classifyLocalMatches(bMatches);
const aFiltered = aClassification.filteredMatches;
const bFiltered = bClassification.filteredMatches;
// Check if they have 100% matches
const aHas100 = aFiltered.some(m => m.confidence === 100);
const bHas100 = bFiltered.some(m => m.confidence === 100);
const aHas100 = aClassification.perfectMatches.length > 0;
const bHas100 = bClassification.perfectMatches.length > 0;
// If one has 100% and the other doesn't, prioritize the one with 100%
if (aHas100 && !bHas100) return -1;
@@ -2612,11 +2613,7 @@ export const missingBrowserMethods = {
renderMissingModel(missing, missingIndex = 0) {
const allMatches = missing.matches || [];
// Filter out matches below 70% confidence threshold
const filteredMatches = allMatches.filter(m => m.confidence >= 70);
// Calculate 100% matches upfront (needed for download section)
const perfectMatches = filteredMatches.filter(m => m.confidence === 100);
const { perfectMatches } = classifyLocalMatches(allMatches);
const missingFilename = this.getMissingFilename(missing);