From 07abcb46de55d6fdbc2f5308be5a2a9a74d32d26 Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Thu, 26 Mar 2026 11:40:11 +0100 Subject: [PATCH] fix: normalize search matching for recommended models and LoRA picker (#4615) Recommended models matching the query were filtered from HF results but the Recommended section was hidden during search, causing them to vanish entirely. - Show filtered recommended models during search by introducing `filteredRecommendedIds` - Switch `recommendedSet` to use filtered IDs when searching so dedup against HF results is correct - Hide empty "Hugging Face" label when recommended matches cover the query - Add `normalizeForSearch` helper to strip separators (spaces, hyphens, underscores, dots) so queries like "llama 3" match "Llama-3.2-1B" and "qwen 2.5" matches "Qwen2.5-7B" in both the recommended model filter and the LoRA adapter filter --- .../assistant-ui/model-selector/pickers.tsx | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx index 3ca9aadb1f..8d4b6ae0d6 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx @@ -47,6 +47,11 @@ function dedupe(values: string[]): string[] { return [...new Set(values.filter(Boolean))]; } +/** Normalize a string for fuzzy search: lowercase, strip separators. */ +function normalizeForSearch(s: string): string { + return s.toLowerCase().replace(/[\s\-_\.]/g, ""); +} + function ListLabel({ children }: { children: ReactNode }) { return (
@@ -492,7 +497,18 @@ export function HubModelPicker({ useRecommendedModelVram(recommendedIds); const showHfSection = debouncedQuery.trim().length > 0; - const recommendedSet = useMemo(() => new Set(visibleRecommendedIds), [visibleRecommendedIds]); + + // Recommended models that match the current search query + const filteredRecommendedIds = useMemo(() => { + if (!showHfSection) return []; + const q = normalizeForSearch(debouncedQuery.trim()); + return recommendedIds.filter((id) => normalizeForSearch(id).includes(q)); + }, [showHfSection, debouncedQuery, recommendedIds]); + + const recommendedSet = useMemo( + () => new Set(showHfSection ? filteredRecommendedIds : visibleRecommendedIds), + [showHfSection, filteredRecommendedIds, visibleRecommendedIds], + ); const hfIds = useMemo(() => { if (!showHfSection) return []; @@ -543,7 +559,8 @@ export function HubModelPicker({ string, { est: number; status: VramFitStatus | null; detail: string | null } >(); - for (const id of visibleRecommendedIds) { + const ids = showHfSection ? filteredRecommendedIds : visibleRecommendedIds; + for (const id of ids) { const totalParams = recommendedParamCountById.get(id); if (totalParams) { const est = estimateLoadingVram(totalParams, "qlora"); @@ -555,7 +572,7 @@ export function HubModelPicker({ } } return map; - }, [visibleRecommendedIds, recommendedParamCountById, gpu]); + }, [showHfSection, filteredRecommendedIds, visibleRecommendedIds, recommendedParamCountById, gpu]); const { scrollRef, sentinelRef } = useInfiniteScroll(fetchMore, results.length); @@ -712,13 +729,44 @@ export function HubModelPicker({ ) : null} + {showHfSection && filteredRecommendedIds.length > 0 ? ( + <> + {"\uD83E\uDDA5"} Recommended + {filteredRecommendedIds.map((id) => { + const vram = recommendedVramMap.get(id); + return ( +
+ handleModelClick(id)} + vramStatus={isGgufRepo(id) ? null : vram?.status ?? null} + vramEst={isGgufRepo(id) ? undefined : vram?.est} + gpuGb={gpu.available ? gpu.memoryTotalGb : undefined} + /> + {expandedGguf === id && ( + + )} +
+ ); + })} + + ) : null} + {showHfSection ? ( <> - Hugging Face + {(hfIds.length > 0 || isLoading) && Hugging Face} {hfIds.length === 0 && !isLoading ? ( -
- No matching models. -
+ filteredRecommendedIds.length === 0 ? ( +
+ No matching models. +
+ ) : null ) : ( hfIds.map((id) => { const vram = vramMap.get(id); @@ -809,11 +857,11 @@ export function LoraModelPicker({ ); const grouped = useMemo(() => { - const needle = query.trim().toLowerCase(); + const needle = normalizeForSearch(query.trim()); const out = new Map(); for (const model of normalized) { - const searchText = `${model.name} ${model.baseModel} ${model.id}`.toLowerCase(); + const searchText = normalizeForSearch(`${model.name} ${model.baseModel} ${model.id}`); if (needle && !searchText.includes(needle)) continue; const key = model.baseModel || "Unknown base model";