📢 fix: Better Prompt Search Result Announcements (#10848)

This commit is contained in:
Dustin Healy 2025-12-09 17:11:23 -08:00 committed by Danny Avila
parent b97d72e51a
commit 885508fc74
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956

View file

@ -1,11 +1,10 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react'; import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { ListFilter, User, Share2 } from 'lucide-react'; import { ListFilter, User, Share2 } from 'lucide-react';
import { SystemCategories } from 'librechat-data-provider'; import { SystemCategories } from 'librechat-data-provider';
import { Dropdown, AnimatedSearchInput } from '@librechat/client'; import { Dropdown, AnimatedSearchInput } from '@librechat/client';
import type { Option } from '~/common'; import type { Option } from '~/common';
import type { TranslationKeys } from '~/hooks'; import { useLocalize, useCategories, useDebounce } from '~/hooks';
import { useLocalize, useCategories } from '~/hooks';
import { usePromptGroupsContext } from '~/Providers'; import { usePromptGroupsContext } from '~/Providers';
import { cn } from '~/utils'; import { cn } from '~/utils';
import store from '~/store'; import store from '~/store';
@ -14,10 +13,10 @@ export default function FilterPrompts({ className = '' }: { className?: string }
const localize = useLocalize(); const localize = useLocalize();
const { name, setName, hasAccess, promptGroups } = usePromptGroupsContext(); const { name, setName, hasAccess, promptGroups } = usePromptGroupsContext();
const { categories } = useCategories({ className: 'h-4 w-4', hasAccess }); const { categories } = useCategories({ className: 'h-4 w-4', hasAccess });
const [displayName, setDisplayName] = useState(name || ''); const [searchTerm, setSearchTerm] = useState(name || '');
const [isSearching, setIsSearching] = useState(false);
const [categoryFilter, setCategory] = useRecoilState(store.promptsCategory); const [categoryFilter, setCategory] = useRecoilState(store.promptsCategory);
const [searchResultsAnnouncement, setSearchResultsAnnouncement] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 500);
const prevNameRef = useRef(name);
const filterOptions = useMemo(() => { const filterOptions = useMemo(() => {
const baseOptions: Option[] = [ const baseOptions: Option[] = [
@ -62,53 +61,35 @@ export default function FilterPrompts({ className = '' }: { className?: string }
[setCategory], [setCategory],
); );
// Sync displayName with name prop when it changes externally // Sync searchTerm with name prop when it changes externally
useEffect(() => { useEffect(() => {
setDisplayName(name || ''); if (prevNameRef.current !== name) {
prevNameRef.current = name;
setSearchTerm(name || '');
}
}, [name]); }, [name]);
useEffect(() => { useEffect(() => {
if (displayName === '') { setName(debouncedSearchTerm);
// Clear immediately when empty }, [debouncedSearchTerm, setName]);
setName('');
setIsSearching(false);
return;
}
setIsSearching(true); const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const timeout = setTimeout(() => { setSearchTerm(e.target.value);
setIsSearching(false); }, []);
setName(displayName); // Debounced setName call
}, 500);
return () => clearTimeout(timeout);
}, [displayName, setName]);
useEffect(() => { const isSearching = searchTerm !== debouncedSearchTerm;
if (!displayName.trim() || isSearching) {
setSearchResultsAnnouncement('');
return;
}
const resultCount = promptGroups?.length ?? 0; const resultCount = promptGroups?.length ?? 0;
const announcement = const searchResultsAnnouncement = useMemo(() => {
resultCount === 1 if (!debouncedSearchTerm.trim()) {
? localize('com_ui_result_found' as TranslationKeys, { return '';
count: resultCount, }
}) return resultCount === 1 ? `${resultCount} result found` : `${resultCount} results found`;
: localize('com_ui_results_found' as TranslationKeys, { }, [debouncedSearchTerm, resultCount]);
count: resultCount,
});
const timeout = setTimeout(() => {
setSearchResultsAnnouncement(announcement);
}, 300);
return () => clearTimeout(timeout);
}, [promptGroups?.length, displayName, isSearching, localize]);
return ( return (
<div className={cn('flex w-full gap-2 text-text-primary', className)}> <div role="search" className={cn('flex w-full gap-2 text-text-primary', className)}>
<div aria-live="polite" className="sr-only"> <div aria-live="polite" aria-atomic="true" className="sr-only">
{searchResultsAnnouncement} {searchResultsAnnouncement}
</div> </div>
<Dropdown <Dropdown
@ -122,10 +103,8 @@ export default function FilterPrompts({ className = '' }: { className?: string }
iconOnly iconOnly
/> />
<AnimatedSearchInput <AnimatedSearchInput
value={displayName} value={searchTerm}
onChange={(e) => { onChange={handleSearchChange}
setDisplayName(e.target.value);
}}
isSearching={isSearching} isSearching={isSearching}
placeholder={localize('com_ui_filter_prompts_name')} placeholder={localize('com_ui_filter_prompts_name')}
/> />