"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import type { FAQQuickResult } from "@/features/agent/services/faqApi"; import { quickSearchFAQs } from "@/features/agent/services/faqApi"; import { Loader2, Search } from "lucide-react"; import { useI18n } from "@/lib/i18n/provider"; interface FAQSearchDropdownProps { open: boolean; onClose: () => void; onSelect: (faq: FAQQuickResult) => void; } export function FAQSearchDropdown({ open, onClose, onSelect, }: FAQSearchDropdownProps) { const { t } = useI18n(); const inputRef = useRef(null); const debounceRef = useRef | null>(null); const containerRef = useRef(null); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedIndex, setSelectedIndex] = useState(-1); useEffect(() => { if (open) { setQuery(""); setResults([]); setError(null); setSelectedIndex(-1); setTimeout(() => inputRef.current?.focus(), 30); } }, [open]); useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, []); const doSearch = useCallback(async (q: string) => { if (!q.trim()) { setResults([]); setError(null); setSelectedIndex(-1); return; } setLoading(true); setError(null); try { const data = await quickSearchFAQs(q.trim(), 10); setResults(data); setSelectedIndex(data.length > 0 ? 0 : -1); } catch (err) { setError((err as Error).message || "搜索失败"); setResults([]); setSelectedIndex(-1); } finally { setLoading(false); } }, []); const handleInputChange = useCallback( (value: string) => { setQuery(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => doSearch(value), 300); }, [doSearch] ); const handleSelect = useCallback( (faq: FAQQuickResult) => { onSelect(faq); }, [onSelect] ); const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { switch (event.key) { case "ArrowDown": event.preventDefault(); setSelectedIndex((prev) => results.length === 0 ? -1 : Math.min(results.length - 1, prev + 1) ); break; case "ArrowUp": event.preventDefault(); setSelectedIndex((prev) => Math.max(0, prev - 1)); break; case "Enter": if (selectedIndex >= 0 && selectedIndex < results.length) { event.preventDefault(); handleSelect(results[selectedIndex]); } break; case "Escape": event.preventDefault(); onClose(); break; } }, [results, selectedIndex, handleSelect, onClose] ); useEffect(() => { if (!open) return; const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { onClose(); } }; const timer = setTimeout(() => { document.addEventListener("mousedown", handleClickOutside); }, 100); return () => { clearTimeout(timer); document.removeEventListener("mousedown", handleClickOutside); }; }, [open, onClose]); if (!open) return null; return (
handleInputChange(e.target.value)} onKeyDown={handleKeyDown} placeholder={t("agent.faqs.quickSearch.placeholder")} className="flex-1 bg-transparent border-none outline-none text-sm placeholder:text-muted-foreground" /> Esc
{loading && (
{t("agent.faqs.quickSearch.searching")}
)} {error && (
{error}
)} {!loading && !error && query.trim() && results.length === 0 && (
{t("agent.faqs.quickSearch.noResults")}
)} {!loading && results.length > 0 && (
{results.map((faq, index) => ( ))}
)} {!loading && !query.trim() && (
{t("agent.faqs.quickSearch.startTyping")}
)}
); }