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
This commit is contained in:
Wasim Yousef Said 2026-03-26 11:40:11 +01:00 committed by GitHub
commit 07abcb46de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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 (
<div className="px-2.5 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
@ -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 ? (
<>
<ListLabel>{"\uD83E\uDDA5"} Recommended</ListLabel>
{filteredRecommendedIds.map((id) => {
const vram = recommendedVramMap.get(id);
return (
<div key={id}>
<ModelRow
label={id}
meta={
isGgufRepo(id)
? "GGUF"
: vram?.detail ?? extractParamLabel(id)
}
selected={value === id}
onClick={() => handleModelClick(id)}
vramStatus={isGgufRepo(id) ? null : vram?.status ?? null}
vramEst={isGgufRepo(id) ? undefined : vram?.est}
gpuGb={gpu.available ? gpu.memoryTotalGb : undefined}
/>
{expandedGguf === id && (
<GgufVariantExpander repoId={id} onSelect={onSelect} gpuGb={gpu.available ? gpu.memoryTotalGb : undefined} systemRamGb={gpu.available ? gpu.systemRamAvailableGb : undefined} />
)}
</div>
);
})}
</>
) : null}
{showHfSection ? (
<>
<ListLabel>Hugging Face</ListLabel>
{(hfIds.length > 0 || isLoading) && <ListLabel>Hugging Face</ListLabel>}
{hfIds.length === 0 && !isLoading ? (
<div className="px-2.5 py-2 text-xs text-muted-foreground">
No matching models.
</div>
filteredRecommendedIds.length === 0 ? (
<div className="px-2.5 py-2 text-xs text-muted-foreground">
No matching models.
</div>
) : 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<string, LoraModelOption[]>();
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";