import { useEffect, useRef, useState } from "preact/hooks"; import type { SearchDetail } from "@/lib/api"; import { MarkdownContent } from "@/components/markdown/markdown-content"; import { cn } from "@/lib/utils"; function formatHost(url: string) { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return url; } } function normalizeHref(href: string) { try { const parsed = new URL(href); parsed.hash = ""; const normalized = parsed.toString(); return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized; } catch { return href.trim().replace(/\/$/, ""); } } type Props = { search: SearchDetail | null; isLoading: boolean; isRunning: boolean; className?: string; enableKeyboardNavigation?: boolean; }; export function SearchResultsPanel({ search, isLoading, isRunning, className, enableKeyboardNavigation = false }: Props) { const ANSWER_COLLAPSED_HEIGHT_CLASS = "h-[3rem]"; const [isAnswerExpanded, setIsAnswerExpanded] = useState(false); const [canExpandAnswer, setCanExpandAnswer] = useState(false); const [activeResultIndex, setActiveResultIndex] = useState(-1); const answerBodyRef = useRef(null); useEffect(() => { setIsAnswerExpanded(false); setCanExpandAnswer(false); }, [search?.id, search?.answerText]); useEffect(() => { const el = answerBodyRef.current; if (!el || !search?.answerText) { setCanExpandAnswer(false); return; } if (isAnswerExpanded) { setCanExpandAnswer(true); return; } setCanExpandAnswer(el.scrollHeight - el.clientHeight > 1); }, [search?.answerText, isAnswerExpanded]); useEffect(() => { setActiveResultIndex(search?.results.length ? 0 : -1); }, [search?.id, search?.results.length]); useEffect(() => { if (!enableKeyboardNavigation) return; const onKeyDown = (event: KeyboardEvent) => { if (!search?.results.length || event.metaKey || event.ctrlKey || event.altKey) return; const target = event.target; if ( target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || (target instanceof HTMLElement && target.isContentEditable) ) { return; } if (event.key === "ArrowLeft") { event.preventDefault(); setActiveResultIndex((current) => Math.max(0, (current < 0 ? 0 : current) - 1)); return; } if (event.key === "ArrowRight") { event.preventDefault(); setActiveResultIndex((current) => Math.min(search.results.length - 1, current + 1)); return; } if (event.key !== "Enter") return; const result = search.results[activeResultIndex >= 0 ? activeResultIndex : 0]; if (!result?.url) return; event.preventDefault(); window.open(result.url, "_blank", "noopener,noreferrer"); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [activeResultIndex, enableKeyboardNavigation, search]); const citationEntries = (search?.answerCitations ?? []) .map((citation, index) => { const href = citation.url || citation.id || ""; if (!href) return null; return { href, normalizedHref: normalizeHref(href), index: index + 1, label: citation.title?.trim() || formatHost(href), }; }) .filter((entry): entry is { href: string; normalizedHref: string; index: number; label: string } => !!entry); const resolveCitationIndex = (href: string) => { const normalized = normalizeHref(href); return citationEntries.find((entry) => entry.normalizedHref === normalized)?.index; }; const hasAnswerText = !!search?.answerText; const isAnswerLoading = isRunning && !hasAnswerText; const hasCitations = citationEntries.length > 0; const isExpandable = hasAnswerText && (canExpandAnswer || hasCitations); return (
{search?.query ? (

Results for

{search.query}

{search.results.length} result{search.results.length === 1 ? "" : "s"} {search.latencyMs ? ` • ${search.latencyMs} ms` : ""}

) : null} {(isRunning || !!search?.answerText || !!search?.answerError) && (

Answer

{(isAnswerLoading || hasAnswerText) ? (
{isAnswerLoading ? (
Generating answer...
) : ( )}
{!isAnswerExpanded && (isExpandable || isAnswerLoading) ? (
) : null}
{isExpandable ? ( ) : null}
) : null} {search?.answerError ?

{search.answerError}

: null} {isAnswerExpanded && !!citationEntries.length && (
{citationEntries.slice(0, 8).map((citation) => { return ( {citation.index} {citation.label} ); })}
)}
)} {(isLoading || isRunning) && !search?.results.length ? (

{isRunning ? "Searching Exa..." : "Loading search..."}

) : null} {!isLoading && !isRunning && !!search?.query && search.results.length === 0 ? (

No results found.

) : null}
{search?.results.map((result, index) => { return (

{formatHost(result.url)}

{result.title || result.url} {(result.publishedDate || result.author) && (

{[result.publishedDate, result.author].filter(Boolean).join(" • ")}

)} {result.url ?

{result.url}

: null}
); })}
{search?.error ?

{search.error}

: null}
); }