/** * SearchPanel — full hybrid_search page with form controls and rich result cards. * * Syncs URL params so results are shareable. Click a result to open its chunk * anchor in the V2 view. */ "use client"; import Image from "next/image"; import Link from "next/link"; import { useEffect, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { SearchAutocomplete } from "./search-autocomplete"; interface Hit { chunk_id: string; doc_id: string; page: number; type: string; bbox: { x: number; y: number; w: number; h: number } | null; classification: string | null; snippet: string; score: number; href: string; } export function SearchPanel({ initialQ, initialLang, initialType, initialDocId, }: { initialQ: string; initialLang: "pt" | "en"; initialType: string; initialDocId: string; }) { const router = useRouter(); const params = useSearchParams(); const [q, setQ] = useState(initialQ); const [lang, setLang] = useState<"pt" | "en">(initialLang); const [type, setType] = useState(initialType); const [docId, setDocId] = useState(initialDocId); const [hits, setHits] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Initial fetch if URL had params useEffect(() => { if (initialQ) doSearch(initialQ, initialLang, initialType, initialDocId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function doSearch(qStr: string, l: "pt" | "en", t: string, d: string) { if (!qStr.trim()) return; setLoading(true); setError(null); const sp = new URLSearchParams({ q: qStr, lang: l, top_k: "25" }); if (t) sp.set("type", t); if (d) sp.set("doc_id", d); try { const r = await fetch(`/api/search/hybrid?${sp}`); if (!r.ok) { const j = await r.json().catch(() => ({})); setError(j.message ?? `HTTP ${r.status}`); setHits([]); return; } const j = (await r.json()) as { hits?: Hit[] }; setHits(j.hits ?? []); } catch (e) { setError((e as Error).message); setHits([]); } finally { setLoading(false); } } function submit(e: React.FormEvent) { e.preventDefault(); // Sync URL (shareable) const sp = new URLSearchParams(params.toString()); sp.set("q", q); sp.set("lang", lang); if (type) sp.set("type", type); else sp.delete("type"); if (docId) sp.set("doc_id", docId); else sp.delete("doc_id"); router.replace(`/search?${sp}`); doSearch(q, lang, type, docId); } return (
setQ(e.target.value)} placeholder="ex. objetos esféricos avistados em Kansas, MJ-12, Roswell..." className="w-full bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-3 py-2 font-mono text-sm text-[#c8d4e6] outline-none" autoFocus /> setQ("")} />
{(["pt", "en"] as const).map((l) => ( ))}
setDocId(e.target.value)} placeholder="opcional: doc-id exato" className="w-full bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1 font-mono text-xs text-[#c8d4e6] outline-none" />
{error && (
retrieval indisponível: {error}
)}
{hits.map((h) => { const cropUrl = h.bbox ? `/api/crop?doc=${encodeURIComponent(h.doc_id)}&page=${h.page}` + `&x=${h.bbox.x}&y=${h.bbox.y}&w=${h.bbox.w}&h=${h.bbox.h}&w_px=320` : null; return ( {cropUrl && ( )}
{h.chunk_id} p{h.page} {h.type} {h.classification && ( {h.classification} )} {h.score.toFixed(3)}

{h.snippet}

{h.doc_id}
); })}
{!loading && !error && initialQ && hits.length === 0 && (
nenhum resultado
)}
); }