diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index 03f2bc982..61eda2a88 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -20,7 +20,7 @@ import { } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom"; import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item"; @@ -289,15 +289,14 @@ export function InboxSidebarContent({ [activeFilter] ); + // Defer non-urgent list updates so the search input stays responsive. + // The deferred snapshot lags one render behind the live value intentionally. + const deferredTabItems = useDeferredValue(activeSource.items); + const deferredSearchItems = useDeferredValue(searchResponse?.items ?? []); + // Two data paths: search mode (API) or default (per-tab data source) const filteredItems = useMemo(() => { - let tabItems: InboxItem[]; - - if (isSearchMode) { - tabItems = searchResponse?.items ?? []; - } else { - tabItems = activeSource.items; - } + const tabItems: InboxItem[] = isSearchMode ? deferredSearchItems : deferredTabItems; let result = tabItems; if (activeFilter !== "all") { @@ -310,8 +309,8 @@ export function InboxSidebarContent({ return result; }, [ isSearchMode, - searchResponse, - activeSource.items, + deferredSearchItems, + deferredTabItems, activeTab, activeFilter, selectedSource, diff --git a/surfsense_web/components/new-chat/document-mention-picker.tsx b/surfsense_web/components/new-chat/document-mention-picker.tsx index ee053a9f5..9c6521f31 100644 --- a/surfsense_web/components/new-chat/document-mention-picker.tsx +++ b/surfsense_web/components/new-chat/document-mention-picker.tsx @@ -4,6 +4,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { forwardRef, useCallback, + useDeferredValue, useEffect, useImperativeHandle, useMemo, @@ -81,6 +82,9 @@ export const DocumentMentionPicker = forwardRef< // Debounced search value to minimize API calls and prevent race conditions const search = externalSearch; const debouncedSearch = useDebounced(search, DEBOUNCE_MS); + // Deferred snapshot of debouncedSearch — client-side filtering uses this so it + // is treated as a non-urgent update, keeping the input responsive. + const deferredSearch = useDeferredValue(debouncedSearch); const [highlightedIndex, setHighlightedIndex] = useState(0); const itemRefs = useRef>(new Map()); const scrollContainerRef = useRef(null); @@ -245,12 +249,14 @@ export const DocumentMentionPicker = forwardRef< * Client-side filtering for single character searches. * Filters cached documents locally for instant feedback without additional API calls. * Server-side search is reserved for 2+ character queries to leverage database indexing. + * Uses deferredSearch (a deferred snapshot of debouncedSearch) so this memo is treated + * as non-urgent — React can interrupt it to keep the input responsive. */ const clientFilteredDocs = useMemo(() => { if (!isSingleCharSearch) return null; - const searchLower = debouncedSearch.trim().toLowerCase(); + const searchLower = deferredSearch.trim().toLowerCase(); return accumulatedDocuments.filter((doc) => doc.title.toLowerCase().includes(searchLower)); - }, [isSingleCharSearch, debouncedSearch, accumulatedDocuments]); + }, [isSingleCharSearch, deferredSearch, accumulatedDocuments]); // Select data source based on search length: client-filtered for single char, server results for 2+ const actualDocuments = isSingleCharSearch ? (clientFilteredDocs ?? []) : accumulatedDocuments; diff --git a/surfsense_web/components/new-chat/prompt-picker.tsx b/surfsense_web/components/new-chat/prompt-picker.tsx index 9fc435111..3e6457b8c 100644 --- a/surfsense_web/components/new-chat/prompt-picker.tsx +++ b/surfsense_web/components/new-chat/prompt-picker.tsx @@ -5,6 +5,7 @@ import { Plus, Zap } from "lucide-react"; import { forwardRef, useCallback, + useDeferredValue, useEffect, useImperativeHandle, useMemo, @@ -41,15 +42,19 @@ export const PromptPicker = forwardRef(funct const shouldScrollRef = useRef(false); const itemRefs = useRef>(new Map()); + // Defer the search value so filtering is non-urgent and the input stays responsive + const deferredSearch = useDeferredValue(externalSearch); + const filtered = useMemo(() => { const list = prompts ?? []; - if (!externalSearch) return list; - return list.filter((a) => a.name.toLowerCase().includes(externalSearch.toLowerCase())); - }, [prompts, externalSearch]); + if (!deferredSearch) return list; + return list.filter((a) => a.name.toLowerCase().includes(deferredSearch.toLowerCase())); + }, [prompts, deferredSearch]); - const prevSearchRef = useRef(externalSearch); - if (prevSearchRef.current !== externalSearch) { - prevSearchRef.current = externalSearch; + // Reset highlight when the deferred (filtered) search changes + const prevSearchRef = useRef(deferredSearch); + if (prevSearchRef.current !== deferredSearch) { + prevSearchRef.current = deferredSearch; if (highlightedIndex !== 0) { setHighlightedIndex(0); }