"use client"; /** * SearchAutocomplete — type-as-you-go dropdown on the /search input. * * Hits /api/search/autocomplete (Meilisearch) with debounced fetch and renders * a two-section dropdown: matching documents (jump targets) and matching * chunks (in-doc passages with excerpt). Sub-30ms target. Keyboard navigation * via Up/Down + Enter. Esc closes. */ import { useEffect, useRef, useState } from "react"; import Link from "next/link"; interface DocSuggestion { doc_id: string; title: string; collection?: string; href: string; } interface ChunkSuggestion { chunk_id: string; doc_id: string; page: number; type: string; excerpt: string; ufo_anomaly: boolean; href: string; } interface ApiResponse { q: string; duration_ms?: number; documents: DocSuggestion[]; chunks: ChunkSuggestion[]; } export function SearchAutocomplete({ query, onPick }: { query: string; onPick?: () => void }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const timer = useRef | null>(null); const abort = useRef(null); useEffect(() => { const q = query.trim(); if (q.length < 2) { setData(null); setOpen(false); return; } if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(async () => { abort.current?.abort(); abort.current = new AbortController(); setLoading(true); try { const r = await fetch(`/api/search/autocomplete?q=${encodeURIComponent(q)}`, { signal: abort.current.signal, }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const j = (await r.json()) as ApiResponse; setData(j); setOpen(j.documents.length + j.chunks.length > 0); } catch (e) { if ((e as Error).name === "AbortError") return; setData(null); setOpen(false); } finally { setLoading(false); } }, 150); return () => { if (timer.current) clearTimeout(timer.current); }; }, [query]); if (!open || !data) return null; return (
⚡ autocomplete · {data.documents.length} docs · {data.chunks.length} trechos {loading ? "…" : `${data.duration_ms ?? "?"}ms`}
{data.documents.length > 0 && (
documentos
    {data.documents.map((d) => (
  • {d.title}
    {d.doc_id} {d.collection && · {d.collection}}
  • ))}
)} {data.chunks.length > 0 && (
trechos
    {data.chunks.map((c) => (
  • {c.chunk_id} p{c.page} {c.type} {c.ufo_anomaly && 🛸} {c.doc_id}
    {c.excerpt}
  • ))}
)}
); }