From ebafbe599df40c02a0683efc9b424bc8b75af3c3 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 29 Nov 2025 16:31:25 +0000 Subject: feat: autocomplete search terms (#2178) * refactor(web): split search autocomplete logic * some improvements * restructure the code * fix typesafety * add feed suggestions * fix --- .../components/dashboard/search/SearchInput.tsx | 122 +++++++++++---------- 1 file changed, 63 insertions(+), 59 deletions(-) (limited to 'apps/web/components/dashboard/search/SearchInput.tsx') diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx index 3f49a53c..644468e8 100644 --- a/apps/web/components/dashboard/search/SearchInput.tsx +++ b/apps/web/components/dashboard/search/SearchInput.tsx @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useImperativeHandle, - useMemo, useRef, useState, } from "react"; @@ -12,6 +11,7 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Command, + CommandEmpty, CommandGroup, CommandInput, CommandItem, @@ -25,14 +25,12 @@ import { import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search"; import { useTranslation } from "@/lib/i18n/client"; import { cn } from "@/lib/utils"; -import { History } from "lucide-react"; import { useSearchHistory } from "@karakeep/shared-react/hooks/search-history"; import { EditListModal } from "../lists/EditListModal"; import QueryExplainerTooltip from "./QueryExplainerTooltip"; - -const MAX_DISPLAY_SUGGESTIONS = 5; +import { useSearchAutocomplete } from "./useSearchAutocomplete"; function useFocusSearchOnKeyPress( inputRef: React.RefObject, @@ -85,7 +83,7 @@ const SearchInput = React.forwardRef< removeItem: (k: string) => localStorage.removeItem(k), }); - const [value, setValue] = React.useState(searchQuery); + const [value, setValue] = useState(searchQuery); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [newNestedListModalOpen, setNewNestedListModalOpen] = useState(false); @@ -119,19 +117,22 @@ const SearchInput = React.forwardRef< [debounceSearch], ); - const suggestions = useMemo(() => { - if (value.trim() === "") { - // Show recent items when not typing - return history.slice(0, MAX_DISPLAY_SUGGESTIONS); - } else { - // Show filtered items when typing - return history - .filter((item) => item.toLowerCase().includes(value.toLowerCase())) - .slice(0, MAX_DISPLAY_SUGGESTIONS); - } - }, [history, value]); + const { + suggestionGroups, + hasSuggestions, + isPopoverVisible, + handleSuggestionSelect, + handleCommandKeyDown, + } = useSearchAutocomplete({ + value, + onValueChange: handleValueChange, + inputRef, + isPopoverOpen, + setIsPopoverOpen, + t, + history, + }); - const isPopoverVisible = isPopoverOpen && suggestions.length > 0; const handleHistorySelect = useCallback( (term: string) => { isHistorySelected.current = true; @@ -141,28 +142,9 @@ const SearchInput = React.forwardRef< setIsPopoverOpen(false); inputRef.current?.blur(); }, - [doSearch], + [doSearch, addTerm], ); - const handleCommandKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === "Enter") { - const selectedItem = document.querySelector( - '[cmdk-item][data-selected="true"]', - ); - const isPlaceholderSelected = - selectedItem?.getAttribute("data-value") === "-"; - if (!selectedItem || isPlaceholderSelected) { - e.preventDefault(); - setIsPopoverOpen(false); - inputRef.current?.blur(); - } - } else if (e.key === "Escape") { - e.preventDefault(); - setIsPopoverOpen(false); - inputRef.current?.blur(); - } - }, []); - useFocusSearchOnKeyPress(inputRef, value, setValue, setIsPopoverOpen); useImperativeHandle(ref, () => inputRef.current!); @@ -242,28 +224,50 @@ const SearchInput = React.forwardRef< onOpenAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} > - - - {/* prevent cmdk auto select the first suggestion -> https://github.com/pacocoursey/cmdk/issues/171*/} - - {suggestions.map((term) => ( - handleHistorySelect(term)} - onMouseDown={() => { - isHistorySelected.current = true; - }} - className="cursor-pointer" - > - - {term} - - ))} - + + {t("search.no_suggestions")} + {hasSuggestions && } + {suggestionGroups.map((group) => ( + + {group.items.map((item) => { + if (item.type === "history") { + return ( + handleHistorySelect(item.term)} + onMouseDown={() => { + isHistorySelected.current = true; + }} + className="cursor-pointer" + > + + {item.label} + + ); + } + + return ( + handleSuggestionSelect(item)} + className="cursor-pointer" + > + +
+ {item.label} + {item.description && ( + + {item.description} + + )} +
+
+ ); + })} +
+ ))}
-- cgit v1.2.3-70-g09d2