"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Search, RefreshCw, Copy as CopyIcon, Database, Files } from "lucide-react"; import { api } from "@/lib/client/api"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge, Empty, Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/primitives"; import { toast } from "@/components/ui/toast"; import { timeAgo, cn } from "@/lib/utils"; export function SearchPage({ initialQuery }: { initialQuery: string }) { const [q, setQ] = useState(initialQuery); const [dq, setDq] = useState(initialQuery); const [types, setTypes] = useState([]); const [semantic, setSemantic] = useState(true); const qc = useQueryClient(); useEffect(() => { const t = setTimeout(() => setDq(q), 300); return () => clearTimeout(t); }, [q]); const { data: hits = [], isFetching } = useQuery({ queryKey: ["search-full", dq, types, semantic], queryFn: () => api(`/api/search?mode=hybrid&q=${encodeURIComponent(dq)}&limit=40&semantic=${semantic ? 1 : 0}${types.length ? `&types=${types.join(",")}` : ""}`), enabled: dq.trim().length > 1 }); const { data: stats } = useQuery({ queryKey: ["index-stats"], queryFn: () => api("/api/search/index-state"), refetchInterval: 10_000 }); const { data: dups = [] } = useQuery({ queryKey: ["duplicates"], queryFn: () => api("/api/search/duplicates") }); const toggleType = (t: string) => setTypes((s) => (s.includes(t) ? s.filter((x) => x !== t) : [...s, t])); return (

Search

setQ(e.target.value)} placeholder="Search content semantically and by keyword…" className="h-10 flex-1 min-w-[16rem] text-[15px]" /> {[["page", "Pages", ], ["attachment", "Files", ], ["row", "Rows", ]].map(([t, l, ic]) => )}
Results {dq && `(${hits.length})`}IndexDuplicates {dups.length ? `(${dups.length})` : ""} {!dq.trim() ? } title="Type to search your workspace" hint="Hybrid retrieval: keyword (full-text) fused with semantic (pgvector) similarity." /> : isFetching && !hits.length ?
Searching…
: !hits.length ? : (hits as any[]).map((h) => (
{h.icon ?? (h.sourceType === "attachment" ? "📎" : "📄")}{h.title}{h.heading && › {h.heading}}{h.semantic > 0 && sem {(h.semantic * 100).toFixed(0)}%}{h.keyword > 0 && kw}
{h.content}
))}
{stats &&
{[["Pages", stats.pages], ["Files with text", stats.attachments], ["Chunks", stats.chunks], ["Embedded chunks", stats.embedded], ["Indexed sources", stats.indexed_sources], ["Stale (old model)", stats.stale]].map(([l, v]) =>
{l}
{String(v)}
)}
}
Embedding model {stats?.model} v{stats?.version}{stats?.last_indexed && · last indexed {timeAgo(stats.last_indexed)}}
{!dups.length ? } title="No near-duplicate pages" hint="Pairs of pages whose content embeddings are ≥ 93 % similar appear here." /> : (dups as any[]).map((d, i) =>
{(d.similarity * 100).toFixed(0)}%{d.a.icon ?? "📄"} {d.a.title}≈{d.b.icon ?? "📄"} {d.b.title}{d.a.wordCount} / {d.b.wordCount} words
)}
); }