mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(autocomplete): group relative-path results by folder (#1091)
Autocomplete suggestions were ranked purely by relevance across the whole library, so same-named loras from different subfolders interleaved and were hard to tell apart. Results are now bucketed by folder (root first, then alphabetically, with nested paths sorting naturally) while keeping the existing relevance ordering within each folder group.
This commit is contained in:
@@ -1295,6 +1295,27 @@ class BaseModelService(ABC):
|
|||||||
path_for_sorting,
|
path_for_sorting,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _relative_path_folder_group_sort_key(
|
||||||
|
relative_path: str, include_terms: List[str]
|
||||||
|
) -> tuple:
|
||||||
|
"""Group paths by folder, then sort by relevance within each group.
|
||||||
|
|
||||||
|
Folders are ordered alphabetically (case-insensitive) by their full
|
||||||
|
folder path, with root-level files (empty folder) first. Within a
|
||||||
|
folder, paths keep the relevance ordering of
|
||||||
|
``_relative_path_sort_key``. This keeps same-folder entries together
|
||||||
|
in the autocomplete dropdown instead of interleaving them by filename.
|
||||||
|
"""
|
||||||
|
path_for_sorting = BaseModelService._remove_model_extension(
|
||||||
|
relative_path.lower()
|
||||||
|
)
|
||||||
|
folder = path_for_sorting.rpartition(os.sep)[0]
|
||||||
|
|
||||||
|
return (folder,) + BaseModelService._relative_path_sort_key(
|
||||||
|
relative_path, include_terms
|
||||||
|
)
|
||||||
|
|
||||||
async def search_relative_paths(
|
async def search_relative_paths(
|
||||||
self,
|
self,
|
||||||
search_term: str,
|
search_term: str,
|
||||||
@@ -1404,9 +1425,13 @@ class BaseModelService(ABC):
|
|||||||
):
|
):
|
||||||
matching_paths.append(relative_path)
|
matching_paths.append(relative_path)
|
||||||
|
|
||||||
# Sort by relevance (prefix and earliest hits first, then by length and alphabetically)
|
# Group by folder (root first, then alphabetically) and sort by
|
||||||
|
# relevance (prefix and earliest hits, then length and alphabetically)
|
||||||
|
# within each folder group.
|
||||||
matching_paths.sort(
|
matching_paths.sort(
|
||||||
key=lambda relative: self._relative_path_sort_key(relative, include_terms)
|
key=lambda relative: self._relative_path_folder_group_sort_key(
|
||||||
|
relative, include_terms
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply offset and limit
|
# Apply offset and limit
|
||||||
|
|||||||
@@ -49,9 +49,83 @@ async def test_search_relative_paths_supports_multiple_tokens():
|
|||||||
|
|
||||||
matching = await service.search_relative_paths("flux detail")
|
matching = await service.search_relative_paths("flux detail")
|
||||||
|
|
||||||
|
# Folder grouping takes precedence over cross-folder relevance:
|
||||||
|
# the "detail" folder sorts before "flux" alphabetically.
|
||||||
assert matching == [
|
assert matching == [
|
||||||
f"flux{os.sep}detail-model.safetensors",
|
|
||||||
f"detail{os.sep}flux-trained.safetensors",
|
f"detail{os.sep}flux-trained.safetensors",
|
||||||
|
f"flux{os.sep}detail-model.safetensors",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_search_relative_paths_groups_by_folder_alphabetically():
|
||||||
|
"""Same-folder entries cluster together; folders sort alphabetically."""
|
||||||
|
scanner = FakeScanner(
|
||||||
|
[
|
||||||
|
{"file_path": "/models/zeta/model-z1.safetensors"},
|
||||||
|
{"file_path": "/models/alpha/model-a1.safetensors"},
|
||||||
|
{"file_path": "/models/zeta/model-z2.safetensors"},
|
||||||
|
{"file_path": "/models/alpha/model-a2.safetensors"},
|
||||||
|
{"file_path": "/models/model-root.safetensors"},
|
||||||
|
],
|
||||||
|
["/models"],
|
||||||
|
)
|
||||||
|
service = DummyService("stub", scanner, BaseModelMetadata)
|
||||||
|
|
||||||
|
matching = await service.search_relative_paths("model")
|
||||||
|
|
||||||
|
assert matching == [
|
||||||
|
# Root-level files (empty folder) come first
|
||||||
|
"model-root.safetensors",
|
||||||
|
f"alpha{os.sep}model-a1.safetensors",
|
||||||
|
f"alpha{os.sep}model-a2.safetensors",
|
||||||
|
f"zeta{os.sep}model-z1.safetensors",
|
||||||
|
f"zeta{os.sep}model-z2.safetensors",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_search_relative_paths_relevance_within_folder_group():
|
||||||
|
"""Within a folder group, the relevance ordering still applies."""
|
||||||
|
scanner = FakeScanner(
|
||||||
|
[
|
||||||
|
{"file_path": "/models/flux/x-detail-model.safetensors"},
|
||||||
|
{"file_path": "/models/flux/detail-model.safetensors"},
|
||||||
|
{"file_path": "/models/flux/a-very-long-detail-model-name.safetensors"},
|
||||||
|
],
|
||||||
|
["/models"],
|
||||||
|
)
|
||||||
|
service = DummyService("stub", scanner, BaseModelMetadata)
|
||||||
|
|
||||||
|
matching = await service.search_relative_paths("flux detail")
|
||||||
|
|
||||||
|
assert matching == [
|
||||||
|
# Prefix hit on the full path wins
|
||||||
|
f"flux{os.sep}detail-model.safetensors",
|
||||||
|
# Then earliest match position, then shorter path
|
||||||
|
f"flux{os.sep}x-detail-model.safetensors",
|
||||||
|
f"flux{os.sep}a-very-long-detail-model-name.safetensors",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_search_relative_paths_nested_folders_sort_naturally():
|
||||||
|
scanner = FakeScanner(
|
||||||
|
[
|
||||||
|
{"file_path": "/models/styles/anime/model-b.safetensors"},
|
||||||
|
{"file_path": "/models/styles/model-a.safetensors"},
|
||||||
|
{"file_path": "/models/other/model-c.safetensors"},
|
||||||
|
],
|
||||||
|
["/models"],
|
||||||
|
)
|
||||||
|
service = DummyService("stub", scanner, BaseModelMetadata)
|
||||||
|
|
||||||
|
matching = await service.search_relative_paths("model")
|
||||||
|
|
||||||
|
assert matching == [
|
||||||
|
f"other{os.sep}model-c.safetensors",
|
||||||
|
f"styles{os.sep}model-a.safetensors",
|
||||||
|
f"styles{os.sep}anime{os.sep}model-b.safetensors",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1398,7 +1398,8 @@ class AutoComplete {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Merge and deduplicate results while preserving order from backend
|
// Merge and deduplicate results while preserving order from backend
|
||||||
// Backend returns results sorted by relevance, so we maintain that order
|
// Backend returns results grouped by folder and sorted by relevance
|
||||||
|
// within each group, so we maintain that order
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const mergedItems = [];
|
const mergedItems = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user