diff --git a/web/package-lock.json b/web/package-lock.json index de42a1ac7..77f0bfc0f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -34,6 +34,7 @@ "axios": "^1.7.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "embla-carousel-react": "^8.2.0", @@ -3722,6 +3723,384 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/web/package.json b/web/package.json index bb15ea4b8..f8f4bc306 100644 --- a/web/package.json +++ b/web/package.json @@ -40,6 +40,7 @@ "axios": "^1.7.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "embla-carousel-react": "^8.2.0", diff --git a/web/src/components/input/InputWithTags.tsx b/web/src/components/input/InputWithTags.tsx index 0d85b3323..a8cd2bce3 100644 --- a/web/src/components/input/InputWithTags.tsx +++ b/web/src/components/input/InputWithTags.tsx @@ -1,10 +1,16 @@ import { useState, useRef, useEffect, useMemo, useCallback } from "react"; -import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { LuX, LuFilter, LuImage } from "react-icons/lu"; import { FilterType, SearchFilter, SearchSource } from "@/types/search"; -import { DropdownMenuSeparator } from "../ui/dropdown-menu"; import useSuggestions from "@/hooks/use-suggestions"; +import { + Command, + CommandInput, + CommandList, + CommandGroup, + CommandItem, +} from "@/components/ui/command"; +import { cn } from "@/lib/utils"; const convertMMDDYYToTimestamp = (dateString: string): number => { const match = dateString.match(/^(\d{2})(\d{2})(\d{2})$/); @@ -33,16 +39,13 @@ export default function InputWithTags({ allSuggestions, }: InputWithTagsProps) { const [inputValue, setInputValue] = useState(search || ""); - const [showSuggestions, setShowSuggestions] = useState(false); - const [showFilters, setShowFilters] = useState(false); const [currentFilterType, setCurrentFilterType] = useState( null, ); + const [inputFocused, setInputFocused] = useState(false); const [isSimilaritySearch, setIsSimilaritySearch] = useState(false); const inputRef = useRef(null); - const containerRef = useRef(null); - const suggestionRef = useRef(null); - const filterRef = useRef(null); + const commandRef = useRef(null); // TODO: search history from browser storage @@ -53,12 +56,11 @@ export default function InputWithTags({ // suggestions - const { - suggestions, - selectedSuggestionIndex, - setSelectedSuggestionIndex, - updateSuggestions, - } = useSuggestions(filters, allSuggestions, searchHistory); + const { suggestions, updateSuggestions } = useSuggestions( + filters, + allSuggestions, + searchHistory, + ); const resetSuggestions = useCallback( (value: string) => { @@ -68,6 +70,21 @@ export default function InputWithTags({ [updateSuggestions], ); + const filterSuggestions = useCallback( + (current_suggestions: string[]) => { + if (!inputValue || currentFilterType) return suggestions; + const words = inputValue.split(/[\s,]+/); + const lastNonEmptyWordIndex = words + .map((word) => word.trim()) + .lastIndexOf(words.filter((word) => word.trim() !== "").pop() || ""); + const currentWord = words[lastNonEmptyWordIndex]; + return current_suggestions.filter((suggestion) => + suggestion.toLowerCase().includes(currentWord.toLowerCase()), + ); + }, + [inputValue, suggestions, currentFilterType], + ); + const removeFilter = useCallback( (filterType: FilterType, filterValue: string | number) => { const newFilters = { ...filters }; @@ -137,7 +154,6 @@ export default function InputWithTags({ setFilters(newFilters); setInputValue((prev) => prev.replace(`${type}:${value}`, "").trim()); setCurrentFilterType(null); - setShowSuggestions(false); } }, [filters, setFilters, allSuggestions], @@ -172,8 +188,11 @@ export default function InputWithTags({ ); const handleInputChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; + (value: string) => { + if (value === "") { + return; + } + setInputValue(value); const words = value.split(/[\s,]+/); @@ -219,55 +238,32 @@ export default function InputWithTags({ } else { resetSuggestions(value); } - - // Reset suggestion state - setSelectedSuggestionIndex(-1); - setShowSuggestions(true); }, - [ - updateSuggestions, - resetSuggestions, - allSuggestions, - handleFilterCreation, - setSelectedSuggestionIndex, - ], + [updateSuggestions, resetSuggestions, allSuggestions, handleFilterCreation], ); const handleInputFocus = useCallback(() => { - setShowSuggestions(true); - setShowFilters(true); - updateSuggestions(inputValue, currentFilterType); - setSelectedSuggestionIndex(-1); - }, [ - inputValue, - currentFilterType, - updateSuggestions, - setSelectedSuggestionIndex, - ]); + setInputFocused(true); + }, []); const handleClearInput = useCallback(() => { - setInputValue(""); + setInputFocused(false); + // setInputValue(""); + resetSuggestions(""); + setSearch(""); + inputRef?.current?.blur(); setFilters({}); setCurrentFilterType(null); - updateSuggestions("", null); - setShowFilters(false); - setShowSuggestions(false); setIsSimilaritySearch(false); - setSearch(""); - }, [setFilters, updateSuggestions, setSearch]); + }, [setFilters, resetSuggestions, setSearch]); - const handleInputBlur = useCallback(() => { - setTimeout(() => { - if (!containerRef.current?.contains(document.activeElement)) { - setShowSuggestions(false); - setShowFilters(false); - setSelectedSuggestionIndex(-1); - } - }, 0); - }, [setSelectedSuggestionIndex]); - - const toggleFilters = useCallback(() => { - setShowFilters((prev) => !prev); + const handleInputBlur = useCallback((e: React.FocusEvent) => { + if ( + commandRef.current && + !commandRef.current.contains(e.relatedTarget as Node) + ) { + setInputFocused(false); + } }, []); const handleSuggestionClick = useCallback( @@ -292,72 +288,36 @@ export default function InputWithTags({ [createFilter, currentFilterType, allSuggestions], ); - const handleKeyDown = useCallback( + const handleSearch = useCallback( + (value: string) => { + setSearch(value); + setInputFocused(false); + inputRef?.current?.blur(); + }, + [setSearch], + ); + + const handleInputKeyDown = useCallback( (e: React.KeyboardEvent) => { - if (e.key === "ArrowDown") { + if ( + e.key === "Enter" && + inputValue.trim() !== "" && + filterSuggestions(suggestions).length == 0 + ) { e.preventDefault(); - setSelectedSuggestionIndex((prev) => (prev + 1) % suggestions.length); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedSuggestionIndex( - (prev) => (prev - 1 + suggestions.length) % suggestions.length, - ); - } else if (e.key === "Enter" && selectedSuggestionIndex !== -1) { - e.preventDefault(); - handleSuggestionClick(suggestions[selectedSuggestionIndex]); - } else if (e.key === "Enter" && currentFilterType) { - e.preventDefault(); - const currentWord = inputValue.split(/[\s,]+/).pop() || ""; - handleFilterCreation(currentFilterType, currentWord); - } else if (e.key === "Enter" && !currentFilterType) { - e.preventDefault(); - setSearch(inputValue); - inputRef.current?.blur(); - handleInputBlur(); + + handleSearch(inputValue); } }, - [ - suggestions, - selectedSuggestionIndex, - handleSuggestionClick, - currentFilterType, - inputValue, - handleFilterCreation, - setSelectedSuggestionIndex, - setSearch, - handleInputBlur, - ], + [inputValue, handleSearch, filterSuggestions, suggestions], ); // effects - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - suggestionRef.current && - !suggestionRef.current.contains(event.target as Node) && - containerRef.current && - !containerRef.current.contains(event.target as Node) - ) { - setShowSuggestions(false); - setShowFilters(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); - useEffect(() => { updateSuggestions(inputValue, currentFilterType); }, [currentFilterType, inputValue, updateSuggestions]); - useEffect(() => { - setInputValue(search || ""); - }, [search]); - useEffect(() => { if (search?.startsWith("similarity:")) { setIsSimilaritySearch(true); @@ -369,32 +329,26 @@ export default function InputWithTags({ }, [search]); return ( -
-
- +
+
{(Object.keys(filters).length > 0 || isSimilaritySearch) && ( )} - {(inputValue || Object.keys(filters).length > 0) && ( + {(search || Object.keys(filters).length > 0) && (
- {((showFilters && - (Object.keys(filters).length > 0 || isSimilaritySearch)) || - showSuggestions) && ( -
- {showFilters && - (Object.keys(filters).length > 0 || isSimilaritySearch) && ( -
- {isSimilaritySearch && ( - - Similarity Search - - - )} - {Object.entries(filters).map(([filterType, filterValues]) => - Array.isArray(filterValues) ? ( - filterValues.map((value, index) => ( - - {filterType}:{value} - - - )) - ) : ( + + + {(Object.keys(filters).length > 0 || isSimilaritySearch) && ( + +
+ {isSimilaritySearch && ( + + Similarity Search + + + )} + {Object.entries(filters).map(([filterType, filterValues]) => + Array.isArray(filterValues) ? ( + filterValues.map((value, index) => ( - {filterType}: - {filterType === "before" || filterType === "after" - ? new Date(filterValues as number).toLocaleDateString() - : filterValues} + {filterType}:{value} - ), - )} -
- )} - {showSuggestions && ( -
- {!currentFilterType && searchHistory.length > 0 && ( - <> -

- Previous Searches -

- {searchHistory.map((suggestion, index) => ( - - ))} - - - )} -

- {currentFilterType ? "Filter Values" : "Filters"} -

- {suggestions - .filter((item) => !searchHistory.includes(item)) - .map((suggestion, index) => ( - - ))} + {filterType}: + {filterType === "before" || filterType === "after" + ? new Date(filterValues as number).toLocaleDateString() + : filterValues} + + + ), + )}
- )} -
- )} -
+ + )} + {!currentFilterType && !inputValue && searchHistory.length > 0 && ( + + {searchHistory.map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + > + {suggestion} + + ))} + + )} + + {filterSuggestions(suggestions) + .filter((item) => !searchHistory.includes(item)) + .map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + > + {suggestion} + + ))} + + + ); } diff --git a/web/src/components/ui/command.tsx b/web/src/components/ui/command.tsx new file mode 100644 index 000000000..64be5e01a --- /dev/null +++ b/web/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/web/src/hooks/use-suggestions.ts b/web/src/hooks/use-suggestions.ts index 91cf0080d..702bb1711 100644 --- a/web/src/hooks/use-suggestions.ts +++ b/web/src/hooks/use-suggestions.ts @@ -15,7 +15,6 @@ export default function useSuggestions( searchHistory: string[], ) { const [suggestions, setSuggestions] = useState([]); - const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1); const updateSuggestions = useCallback( (value: string, currentFilterType: FilterType | null) => { @@ -59,8 +58,6 @@ export default function useSuggestions( return { suggestions, - selectedSuggestionIndex, - setSelectedSuggestionIndex, updateSuggestions, }; } diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 03d5fcf6a..673374d4b 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -260,7 +260,7 @@ export default function SearchView({