From 27ab446926d5e6cdfb7ea5290e6e5752a76af529 Mon Sep 17 00:00:00 2001 From: ls-root Date: Wed, 4 Feb 2026 13:27:42 +0100 Subject: [PATCH] fix(frontend): implement weighted search scoring for command menu (#11534) * fix(frontend): implement weighted search scoring for command menu The previous cmdk search was filtered correctly but then sorted by category order rather than match strength. This caused "Dokpoly" to appear above "Traefik", if you searched for Traefik, simply because its category (Containers) comes before Traefik's category alphabetically. - Flatten search results into a single "Search Results" group. - Implement scoring to prioritize name matches over descriptions. - Revert to category grouping only when the search query is empty. * perf(frontend): optimize search logic Limiting the results to 20 helps a lot. --- frontend/src/components/command-menu.tsx | 119 ++++++++++++++++++++--- 1 file changed, 104 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/command-menu.tsx b/frontend/src/components/command-menu.tsx index 7596b8307..081cade24 100644 --- a/frontend/src/components/command-menu.tsx +++ b/frontend/src/components/command-menu.tsx @@ -23,6 +23,34 @@ import { Button } from "./ui/button"; import { Badge } from "./ui/badge"; import Link from "next/link"; +function search(scripts: Script[], query: string): Script[] { + const queryLower = query.toLowerCase().trim(); + const searchWords = queryLower.split(/\s+/).filter(Boolean); + + return scripts + .map(script => { + const nameLower = script.name.toLowerCase(); + const descriptionLower = (script.description || "").toLowerCase(); + + let score = 0; + + for (const word of searchWords) { + if (nameLower.includes(word)) { + score += 10; + } + if (descriptionLower.includes(word)) { + score += 5; + } + } + + return { script, score }; + }) + .filter(({ score }) => score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, 20) + .map(({ script }) => script); +} + export function formattedBadge(type: string) { switch (type) { case "vm": @@ -51,9 +79,11 @@ function getRandomScript(categories: Category[], previouslySelected: Set } function CommandMenu() { + const [query, setQuery] = React.useState(""); const [open, setOpen] = React.useState(false); const [links, setLinks] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); + const [results, setResults] = React.useState([]); const [selectedScripts, setSelectedScripts] = React.useState>(new Set()); const router = useRouter(); @@ -70,6 +100,27 @@ function CommandMenu() { }); }; + React.useEffect(() => { + if (query.trim() === "") { + fetchSortedCategories(); + } + else { + const scriptMap = new Map(); + + for (const category of links) { + for (const script of category.scripts || []) { + if (!scriptMap.has(script.slug)) { + scriptMap.set(script.slug, script); + } + } + } + + const uniqueScripts = Array.from(scriptMap.values()); + const filteredResults = search(uniqueScripts, query); + setResults(filteredResults); + } + }, [query]); + React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { @@ -197,20 +248,20 @@ function CommandMenu() { { - const searchLower = search.toLowerCase().trim(); - if (!searchLower) - return 1; - const valueLower = value.toLowerCase(); - const searchWords = searchLower.split(/\s+/).filter(Boolean); - // All search words must appear somewhere in the value (name + description) - const allWordsMatch = searchWords.every((word: string) => valueLower.includes(word)); - return allWordsMatch ? 1 : 0; + onOpenChange={(open) => { + setOpen(open); + if (open) { + setQuery(""); + setResults([]); + } }} > Search scripts - + {isLoading ? ( @@ -233,9 +284,10 @@ function CommandMenu() { )} - {Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => ( - - {scripts.map(script => ( + + {results.length > 0 ? ( + + {results.map(script => ( ))} - ))} + ) : ( // When no search results, show all scripts grouped by category + Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => ( + + {scripts.map(script => ( + { + setOpen(false); + router.push(`/scripts?id=${script.slug}`); + }} + tabIndex={0} + aria-label={`Open script ${script.name}`} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setOpen(false); + router.push(`/scripts?id=${script.slug}`); + } + }} + > +
setOpen(false)}> + ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)} + unoptimized + width={16} + height={16} + alt="" + className="h-5 w-5" + /> + {script.name} + {formattedBadge(script.type)} +
+
+ ))} +
+ )) + )}