import * as React from "react" import { Command, CommandGroup, CommandItem, CommandList, } from "@/components/ui/command" import { Command as CommandPrimitive } from "cmdk" import { motion, AnimatePresence } from "framer-motion" import { cn, searchSafeRegExp, shuffleArray } from "@/lib/utils" import { useIsMounted } from "@/hooks/use-is-mounted" interface GraphNode { name: string prettyName: string connectedTopics: string[] } interface AutocompleteProps { topics: GraphNode[] onSelect: (topic: string) => void onInputChange: (value: string) => void } export function Autocomplete({ topics = [], onSelect, onInputChange, }: AutocompleteProps): JSX.Element { const inputRef = React.useRef(null) const [open, setOpen] = React.useState(false) const isMounted = useIsMounted() const [inputValue, setInputValue] = React.useState("") const [hasInteracted, setHasInteracted] = React.useState(false) const [initialTopics, setInitialTopics] = React.useState([]) React.useEffect(() => { setInitialTopics(shuffleArray(topics).slice(0, 5)) }, [topics]) const filteredTopics = React.useMemo(() => { if (!inputValue) { return initialTopics } const regex = searchSafeRegExp(inputValue) return topics .filter( (topic) => regex.test(topic.name) || regex.test(topic.prettyName) || topic.connectedTopics.some((connectedTopic) => regex.test(connectedTopic), ), ) .sort((a, b) => a.prettyName.localeCompare(b.prettyName)) .slice(0, 10) }, [inputValue, topics, initialTopics]) const handleSelect = React.useCallback( (topic: GraphNode) => { setOpen(false) onSelect(topic.name) }, [onSelect], ) const handleInputChange = React.useCallback( (value: string) => { setInputValue(value) setOpen(true) setHasInteracted(true) onInputChange(value) }, [onInputChange], ) const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { if ((event.key === "ArrowDown" || event.key === "ArrowUp") && !open) { event.preventDefault() setOpen(true) setHasInteracted(true) } }, [open], ) const commandKey = React.useMemo(() => { return filteredTopics .map( (topic) => `${topic.name}:${topic.prettyName}:${topic.connectedTopics.join(",")}`, ) .join("__") }, [filteredTopics]) React.useEffect(() => { if (inputRef.current && isMounted() && hasInteracted) { inputRef.current.focus() } }, [commandKey, isMounted, hasInteracted]) return (
{ setTimeout(() => setOpen(false), 100) }} onFocus={() => setHasInteracted(true)} onClick={() => { setOpen(true) setHasInteracted(true) }} placeholder={filteredTopics[0]?.prettyName} className={cn( "min-h-10 flex-1 bg-transparent px-3 py-1 outline-none placeholder:text-muted-foreground sm:px-4 sm:py-3", )} autoFocus />
{open && hasInteracted && ( {filteredTopics.map((topic, index) => ( handleSelect(topic)} className="min-h-10 rounded-none px-3 py-1.5" > {topic.prettyName} {topic.connectedTopics.length > 0 && topic.connectedTopics.join(", ")} ))} )}
) } export default Autocomplete