fix(autocomplete): strip file extensions from model names in search suggestions

Remove .safetensors/.ckpt/.pt/.bin extensions from model names in autocomplete
suggestions to improve UX and search relevance:

Frontend (web/comfyui/autocomplete.js):
- Add _getDisplayText() helper to strip extensions from model paths
- Update _matchItem() to match against filename without extension
- Update render() and createItemElement() to display clean names

Backend (py/services/base_model_service.py):
- Add _remove_model_extension() helper method
- Update _relative_path_matches_tokens() to ignore extensions in matching
- Update _relative_path_sort_key() to sort based on names without extensions

Tests (tests/services/test_relative_path_search.py):
- Add tests to verify 's' and 'safe' queries don't match all .safetensors files

Fixes issue where typing 's' would match all .safetensors files and cluttered
suggestions with redundant extension names.
This commit is contained in:
Will Miao
2026-03-07 23:07:10 +08:00
parent a802a89ff9
commit 43f6bfab36
3 changed files with 95 additions and 12 deletions

View File

@@ -689,6 +689,22 @@ class AutoComplete {
return Array.from(variations).filter(v => v.length >= this.options.minChars);
}
/**
* Get display text for an item (without extension for models)
* @param {string|Object} item - Item to get display text from
* @returns {string} - Display text without extension
*/
_getDisplayText(item) {
const itemText = typeof item === 'object' && item.tag_name ? item.tag_name : String(item);
// Remove extension for models to avoid matching/displaying .safetensors etc.
if (this.modelType === 'loras' || this.searchType === 'embeddings') {
return removeLoraExtension(itemText);
} else if (this.modelType === 'embeddings') {
return removeGeneralExtension(itemText);
}
return itemText;
}
/**
* Check if an item matches a search term
* Supports both string items and enriched items with tag_name property
@@ -697,7 +713,7 @@ class AutoComplete {
* @returns {Object} - { matched: boolean, isExactMatch: boolean }
*/
_matchItem(item, searchTerm) {
const itemText = typeof item === 'object' && item.tag_name ? item.tag_name : String(item);
const itemText = this._getDisplayText(item);
const itemTextLower = itemText.toLowerCase();
const searchTermLower = searchTerm.toLowerCase();
@@ -1070,7 +1086,9 @@ class AutoComplete {
// to prevent flex layout from breaking up the text
const nameSpan = document.createElement('span');
nameSpan.className = 'lm-autocomplete-name';
nameSpan.innerHTML = this.highlightMatch(displayText, this.currentSearchTerm);
// Use display text without extension for cleaner UI
const displayTextWithoutExt = this._getDisplayText(displayText);
nameSpan.innerHTML = this.highlightMatch(displayTextWithoutExt, this.currentSearchTerm);
nameSpan.style.cssText = `
flex: 1;
min-width: 0;
@@ -1522,7 +1540,9 @@ class AutoComplete {
} else {
const nameSpan = document.createElement('span');
nameSpan.className = 'lm-autocomplete-name';
nameSpan.innerHTML = this.highlightMatch(displayText, this.currentSearchTerm);
// Use display text without extension for cleaner UI
const displayTextWithoutExt = this._getDisplayText(displayText);
nameSpan.innerHTML = this.highlightMatch(displayTextWithoutExt, this.currentSearchTerm);
nameSpan.style.cssText = `
flex: 1;
min-width: 0;