import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"; import { Search } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from "@/components/ui/dialog"; import { deriveTitle } from "@/lib/format"; import { cn } from "@/lib/utils"; import type { ChatSummary } from "@/lib/types"; interface SessionSearchDialogProps { open: boolean; sessions: ChatSummary[]; activeKey: string | null; loading: boolean; titleOverrides?: Record; onOpenChange: (open: boolean) => void; onSelect: (key: string) => void; } export function SessionSearchDialog({ open, sessions, activeKey, loading, titleOverrides = {}, onOpenChange, onSelect, }: SessionSearchDialogProps) { const { t } = useTranslation(); const inputRef = useRef(null); const itemRefs = useRef>([]); const [query, setQuery] = useState(""); const [highlightedIndex, setHighlightedIndex] = useState(0); const normalizedQuery = query.trim().toLowerCase(); const sessionResults = useMemo(() => { if (!open) return []; if (!normalizedQuery) return sessions; const terms = normalizedQuery.split(/\s+/).filter(Boolean); return sessions.filter((session) => sessionMatchesTerms(session, terms, titleOverrides[session.key]), ); }, [normalizedQuery, open, sessions, titleOverrides]); const itemCount = sessionResults.length; useEffect(() => { if (!open) return; setQuery(""); setHighlightedIndex(0); window.setTimeout(() => inputRef.current?.focus(), 0); }, [open]); useEffect(() => { setHighlightedIndex(0); }, [normalizedQuery]); useEffect(() => { setHighlightedIndex((index) => itemCount === 0 ? 0 : Math.min(index, itemCount - 1), ); }, [itemCount]); useEffect(() => { itemRefs.current = itemRefs.current.slice(0, itemCount); }, [itemCount]); useEffect(() => { if (!open) return; itemRefs.current[highlightedIndex]?.scrollIntoView({ block: "nearest", inline: "nearest", }); }, [highlightedIndex, open]); const handleSelect = (key: string) => { onOpenChange(false); onSelect(key); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "ArrowDown") { event.preventDefault(); setHighlightedIndex((index) => itemCount === 0 ? 0 : (index + 1) % itemCount, ); return; } if (event.key === "ArrowUp") { event.preventDefault(); setHighlightedIndex((index) => itemCount === 0 ? 0 : (index - 1 + itemCount) % itemCount, ); return; } if (event.key === "Enter") { const highlighted = sessionResults[highlightedIndex]; if (!highlighted) return; event.preventDefault(); handleSelect(highlighted.key); } }; const emptyLabel = normalizedQuery ? t("sidebar.noSearchResults") : t("chat.noSessions"); const sectionLabel = normalizedQuery ? t("sidebar.searchResults") : t("sidebar.recent"); if (!open) return null; return ( {t("sidebar.searchAria")} {t("sidebar.searchPlaceholder")}
setQuery(event.target.value)} onKeyDown={handleKeyDown} placeholder={t("sidebar.searchPlaceholder")} aria-label={t("sidebar.searchAria")} className="h-full min-w-0 flex-1 bg-transparent text-[19px] font-normal leading-none text-foreground outline-none placeholder:text-muted-foreground" />
{sectionLabel}
{loading && sessions.length === 0 ? (
{t("chat.loading")}
) : sessionResults.length === 0 ? (
{emptyLabel}
) : (
    {sessionResults.map((session, index) => { const title = titleOverrides[session.key]?.trim() || session.title?.trim() || deriveTitle(session.preview, t("chat.newChat")); const preview = session.preview.trim(); const showPreview = preview.length > 0 && preview.toLowerCase() !== title.trim().toLowerCase(); const highlighted = index === highlightedIndex; const active = session.key === activeKey; return (
  • ); })}
)}
); } function sessionMatchesTerms( session: ChatSummary, terms: string[], titleOverride?: string, ) { const haystack = [ titleOverride, session.title, session.preview, ] .filter(Boolean) .join(" ") .toLowerCase(); return terms.every((term) => haystack.includes(term)); }