"use client"; import { useAtomValue } from "jotai"; import { Plus, WandSparkles } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { forwardRef, useCallback, useDeferredValue, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react"; import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms"; import { ComposerSuggestionGroup, ComposerSuggestionGroupHeading, ComposerSuggestionItem, ComposerSuggestionList, ComposerSuggestionMessage, ComposerSuggestionSeparator, ComposerSuggestionSkeleton, } from "@/components/new-chat/composer-suggestion-popup"; export interface PromptPickerRef { selectHighlighted: () => void; moveUp: () => void; moveDown: () => void; } interface PromptPickerProps { onSelect: (action: { name: string; prompt: string; mode: "transform" | "explore" }) => void; onDone: () => void; externalSearch?: string; } export const PromptPicker = forwardRef(function PromptPicker( { onSelect, onDone, externalSearch = "" }, ref ) { const router = useRouter(); const params = useParams(); const { data: prompts, isLoading, isError } = useAtomValue(promptsAtom); const [highlightedIndex, setHighlightedIndex] = useState(0); const scrollContainerRef = useRef(null); 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 (!deferredSearch) return list; return list.filter((a) => a.name.toLowerCase().includes(deferredSearch.toLowerCase())); }, [prompts, deferredSearch]); // Reset highlight when the deferred (filtered) search changes const prevSearchRef = useRef(deferredSearch); if (prevSearchRef.current !== deferredSearch) { prevSearchRef.current = deferredSearch; if (highlightedIndex !== 0) { setHighlightedIndex(0); } } const createPromptIndex = filtered.length; const totalItems = filtered.length + 1; const searchSpaceId = Array.isArray(params?.search_space_id) ? params.search_space_id[0] : params?.search_space_id; const handleSelect = useCallback( (index: number) => { if (index === createPromptIndex) { onDone(); if (searchSpaceId) { router.push(`/dashboard/${searchSpaceId}/user-settings/prompts`); } return; } const action = filtered[index]; if (!action) return; onSelect({ name: action.name, prompt: action.prompt, mode: action.mode }); }, [filtered, onSelect, createPromptIndex, onDone, router, searchSpaceId] ); useEffect(() => { if (!shouldScrollRef.current) return; shouldScrollRef.current = false; const rafId = requestAnimationFrame(() => { const item = itemRefs.current.get(highlightedIndex); const container = scrollContainerRef.current; if (item && container) { const itemRect = item.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); if (itemRect.top < containerRect.top || itemRect.bottom > containerRect.bottom) { item.scrollIntoView({ block: "nearest" }); } } }); return () => cancelAnimationFrame(rafId); }, [highlightedIndex]); useImperativeHandle( ref, () => ({ selectHighlighted: () => handleSelect(highlightedIndex), moveUp: () => { shouldScrollRef.current = true; setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1)); }, moveDown: () => { shouldScrollRef.current = true; setHighlightedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0)); }, }), [totalItems, highlightedIndex, handleSelect] ); return ( {isLoading ? ( ) : isError ? ( Failed to load prompts ) : filtered.length === 0 ? ( No matching prompts ) : ( Saved Prompts {filtered.map((action, index) => ( { if (el) itemRefs.current.set(index, el); else itemRefs.current.delete(index); }} icon={} selected={index === highlightedIndex} onClick={() => handleSelect(index)} onMouseEnter={() => setHighlightedIndex(index)} > {action.name} ))} { if (el) itemRefs.current.set(createPromptIndex, el); else itemRefs.current.delete(createPromptIndex); }} icon={} muted selected={highlightedIndex === createPromptIndex} onClick={() => handleSelect(createPromptIndex)} onMouseEnter={() => setHighlightedIndex(createPromptIndex)} > Create prompt )} ); });