SearchModal.tsx (2315B)
1 import { useState, useCallback, useEffect, useRef } from "react"; 2 import { api } from "../../lib/api"; 3 import { SearchBar } from "./SearchBar"; 4 import { SearchResults } from "./SearchResults"; 5 import { useSessionStore } from "../../store/session"; 6 7 interface SearchResultMessage { 8 id: string; 9 content: string; 10 role: string; 11 } 12 13 interface SearchResultGroup { 14 session_id: string; 15 session_name: string | null; 16 messages: SearchResultMessage[]; 17 } 18 19 interface SearchModalProps { 20 isOpen: boolean; 21 onClose: () => void; 22 onSelectSession: (sessionId: string) => void; 23 } 24 25 export function SearchModal({ isOpen, onClose, onSelectSession }: SearchModalProps) { 26 const [results, setResults] = useState<SearchResultGroup[]>([]); 27 const [isSearching, setIsSearching] = useState(false); 28 const modalRef = useRef<HTMLDivElement>(null); 29 30 const handleSearch = useCallback(async (query: string) => { 31 if (!query) { 32 setResults([]); 33 return; 34 } 35 setIsSearching(true); 36 try { 37 const data = await api.get<{ results: SearchResultGroup[] }>( 38 `/search?q=${encodeURIComponent(query)}`, 39 ); 40 setResults(data.results ?? []); 41 } catch { 42 setResults([]); 43 } finally { 44 setIsSearching(false); 45 } 46 }, []); 47 48 const handleSelectMessage = useCallback( 49 (sessionId: string, _messageId: string) => { 50 useSessionStore.getState().setActiveSession(sessionId); 51 onSelectSession(sessionId); 52 }, 53 [onSelectSession], 54 ); 55 56 useEffect(() => { 57 if (!isOpen) return; 58 const handleKeyDown = (e: KeyboardEvent) => { 59 if (e.key === "Escape") onClose(); 60 }; 61 document.addEventListener("keydown", handleKeyDown); 62 return () => document.removeEventListener("keydown", handleKeyDown); 63 }, [isOpen, onClose]); 64 65 if (!isOpen) return null; 66 67 return ( 68 <div className="fixed inset-0 z-50 flex items-start justify-center bg-black/60 pt-[15vh]"> 69 <div ref={modalRef} className="w-full max-w-lg rounded-xl border border-gray-700 bg-gray-900 shadow-2xl"> 70 <div className="p-3"> 71 <SearchBar onSearch={handleSearch} isSearching={isSearching} /> 72 </div> 73 <SearchResults 74 results={results} 75 onSelectMessage={handleSelectMessage} 76 onClose={onClose} 77 /> 78 </div> 79 </div> 80 ); 81 }