refactor: unify local match classification
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
}[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 => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
}[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');
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user