import React, { forwardRef, useState, useCallback, useMemo, useEffect, useRef } from 'react'; import debounce from 'lodash/debounce'; import { useRecoilState } from 'recoil'; import { Search, X } from 'lucide-react'; import { QueryKeys } from 'librechat-data-provider'; import { useQueryClient } from '@tanstack/react-query'; import { useLocation, useNavigate } from 'react-router-dom'; import { useLocalize, useNewConvo } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; type SearchBarProps = { isSmallScreen?: boolean; }; const SearchBar = forwardRef((props: SearchBarProps, ref: React.Ref) => { const localize = useLocalize(); const location = useLocation(); const queryClient = useQueryClient(); const navigate = useNavigate(); const { isSmallScreen } = props; const [text, setText] = useState(''); const inputRef = useRef(null); const [showClearIcon, setShowClearIcon] = useState(false); const { newConversation: newConvo } = useNewConvo(); const [search, setSearchState] = useRecoilState(store.search); const clearSearch = useCallback( (pathname?: string) => { if (pathname?.includes('/search') || pathname === '/c/new') { queryClient.removeQueries([QueryKeys.messages]); newConvo({ disableFocus: true }); navigate('/c/new'); } }, [newConvo, navigate, queryClient], ); const clearText = useCallback( (pathname?: string) => { setShowClearIcon(false); setText(''); setSearchState((prev) => ({ ...prev, query: '', debouncedQuery: '', isTyping: false, })); clearSearch(pathname); inputRef.current?.focus(); }, [setSearchState, clearSearch], ); const handleKeyUp = useCallback( (e: React.KeyboardEvent) => { const { value } = e.target as HTMLInputElement; if (e.key === 'Backspace' && value === '') { clearText(location.pathname); } }, [clearText, location.pathname], ); const sendRequest = useCallback( (value: string) => { if (!value) { return; } queryClient.invalidateQueries([QueryKeys.messages]); }, [queryClient], ); const debouncedSetDebouncedQuery = useMemo( () => debounce((value: string) => { setSearchState((prev) => ({ ...prev, debouncedQuery: value, isTyping: false })); sendRequest(value); }, 500), [setSearchState, sendRequest], ); const onChange = (e: React.ChangeEvent) => { const value = e.target.value; setShowClearIcon(value.length > 0); setText(value); setSearchState((prev) => ({ ...prev, query: value, isTyping: true, })); debouncedSetDebouncedQuery(value); if (value.length > 0 && location.pathname !== '/search') { navigate('/search', { replace: true }); } }; // Automatically set isTyping to false when loading is done and debouncedQuery matches query // (prevents stuck loading state if input is still focused) useEffect(() => { if (search.isTyping && !search.isSearching && search.debouncedQuery === search.query) { setSearchState((prev) => ({ ...prev, isTyping: false })); } }, [search.isTyping, search.isSearching, search.debouncedQuery, search.query, setSearchState]); return (
{ e.code === 'Space' ? e.stopPropagation() : null; }} aria-label={localize('com_nav_search_placeholder')} placeholder={localize('com_nav_search_placeholder')} onKeyUp={handleKeyUp} onFocus={() => setSearchState((prev) => ({ ...prev, isSearching: true }))} onBlur={() => setSearchState((prev) => ({ ...prev, isSearching: false }))} autoComplete="off" dir="auto" />
); }); export default SearchBar;