import { useState, useCallback, useMemo } from "react"; import { Search, X } from "lucide-react"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; import { useDebounce } from "~/hooks/use-debounce"; import type { RawTriplet } from "./type"; interface SpaceSearchProps { triplets: RawTriplet[]; searchQuery: string; onSearchChange: (query: string) => void; placeholder?: string; } export function SpaceSearch({ triplets, searchQuery, onSearchChange, placeholder = "Search in episodes...", }: SpaceSearchProps) { const [inputValue, setInputValue] = useState(searchQuery); // Debounce the search to avoid too many re-renders const debouncedSearchQuery = useDebounce(inputValue, 300); // Update parent component when debounced value changes useMemo(() => { if (debouncedSearchQuery !== searchQuery) { onSearchChange(debouncedSearchQuery); } }, [debouncedSearchQuery, searchQuery, onSearchChange]); // Helper to determine if a node is an episode const isEpisodeNode = useCallback((node: any) => { // Check if node has content attribute (indicates it's an episode) return ( node.attributes?.content || node.attributes?.episodeUuid || (node.labels && node.labels.includes("Episode")) ); }, []); // Count episode nodes that match the search const matchingEpisodes = useMemo(() => { if (!debouncedSearchQuery.trim()) return 0; const query = debouncedSearchQuery.toLowerCase(); const episodes: Record = {}; triplets.forEach((triplet) => { // Check if source node is an episode and matches if ( isEpisodeNode(triplet.sourceNode) && triplet.sourceNode.attributes?.content?.toLowerCase().includes(query) ) { episodes[triplet.sourceNode.uuid] = 1; } // Check if target node is an episode and matches if ( isEpisodeNode(triplet.targetNode) && triplet.targetNode.attributes?.content?.toLowerCase().includes(query) ) { episodes[triplet.targetNode.uuid] = 1; } }); return Object.keys(episodes).length; }, [triplets, debouncedSearchQuery]); const handleInputChange = (event: React.ChangeEvent) => { setInputValue(event.target.value); }; const handleClear = () => { setInputValue(""); onSearchChange(""); }; const hasSearchQuery = inputValue.trim().length > 0; return (
{hasSearchQuery && ( )}
{/* Show search results count */} {debouncedSearchQuery.trim() && (
{matchingEpisodes} episode{matchingEpisodes !== 1 ? "s" : ""}
)}
); }