/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' import { Check, ChevronsUpDown } from 'lucide-react' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { Input } from '@/components/ui/input' export type ComboboxInputOption = { value: string label: string icon?: React.ReactNode } interface ComboboxInputProps { options: ComboboxInputOption[] value?: string onValueChange: (value: string) => void placeholder?: string emptyText?: string className?: string id?: string } export function ComboboxInput({ options, value = '', onValueChange, placeholder = 'Select or type...', emptyText = 'No option found.', className, id, }: ComboboxInputProps) { const { t } = useTranslation() const [open, setOpen] = React.useState(false) const [highlightedIndex, setHighlightedIndex] = React.useState(-1) const containerRef = React.useRef(null) const inputRef = React.useRef(null) const listRef = React.useRef(null) const filteredOptions = React.useMemo(() => { if (!value.trim()) return options const search = value.toLowerCase().trim() return options.filter( (option) => option.label.toLowerCase().includes(search) || option.value.toLowerCase().includes(search) ) }, [options, value]) // Reset highlight when filtered options change React.useEffect(() => { setHighlightedIndex(-1) }, [filteredOptions]) // Handle click outside to close React.useEffect(() => { if (!open) return const handleClickOutside = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { setOpen(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [open]) const handleSelect = (selectedValue: string) => { onValueChange(selectedValue) setOpen(false) inputRef.current?.focus() } const handleKeyDown = (e: React.KeyboardEvent) => { if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { setOpen(true) return } if (!open) return switch (e.key) { case 'ArrowDown': e.preventDefault() setHighlightedIndex((prev) => prev < filteredOptions.length - 1 ? prev + 1 : 0 ) break case 'ArrowUp': e.preventDefault() setHighlightedIndex((prev) => prev > 0 ? prev - 1 : filteredOptions.length - 1 ) break case 'Enter': e.preventDefault() if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) { handleSelect(filteredOptions[highlightedIndex].value) } else { // No highlighted option, just close the dropdown and keep current value setOpen(false) } break case 'Escape': e.preventDefault() setOpen(false) break } } // Scroll highlighted item into view React.useEffect(() => { if (highlightedIndex < 0 || !listRef.current) return const item = listRef.current.children[highlightedIndex] as HTMLElement item?.scrollIntoView({ block: 'nearest' }) }, [highlightedIndex]) const showDropdown = open && (filteredOptions.length > 0 || value.trim()) return (
{ onValueChange(e.target.value) if (!open) setOpen(true) }} onFocus={() => setOpen(true)} onKeyDown={handleKeyDown} className={cn('pr-9', className)} /> {showDropdown && (
{filteredOptions.length > 0 ? (
    {filteredOptions.map((option, index) => (
  • setHighlightedIndex(index)} onMouseDown={(e) => { e.preventDefault() // Prevent blur handleSelect(option.value) }} > {option.icon && {option.icon}} {option.label}
  • ))}
) : (
{emptyText} {value.trim() && (
{t('Press Enter to use "{{value}}"', { value: value.trim() })}
)}
)}
)}
) }