"use client"; import { useState, useEffect, useCallback, Suspense } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections"; import { Search, X, } from "lucide-react"; interface DocHit { slug: string; title: string; section: string; score: number; snippet: string; } type SearchMode = "hybrid" | "keyword" | "semantic"; function SearchContent() { const searchParams = useSearchParams(); const initialQ = searchParams.get("q") ?? ""; const initialMode = (searchParams.get("mode") as SearchMode) ?? "hybrid"; const [query, setQuery] = useState(initialQ); const [mode, setMode] = useState( ["hybrid", "keyword", "semantic"].includes(initialMode) ? initialMode : "hybrid", ); const [sections, setSections] = useState(DEFAULT_SECTIONS); const [selectedSection, setSelectedSection] = useState("all"); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { fetch("/api/sections") .then((r) => r.json()) .then((data) => { if (Array.isArray(data) && data.length > 0) { setSections(data); } }) .catch(() => {}); }, []); const handleSearch = useCallback(async (q: string, searchMode: SearchMode) => { if (!q.trim()) { setResults([]); return; } setLoading(true); try { const res = await fetch( `/api/search?q=${encodeURIComponent(q)}&mode=${searchMode}&limit=30`, ); const data = await res.json(); setResults(data.results || []); } catch { setResults([]); } finally { setLoading(false); } }, []); useEffect(() => { if (initialQ) { handleSearch(initialQ, mode); } }, [initialQ, handleSearch, mode]); function handleChange(e: React.ChangeEvent) { const val = e.target.value; setQuery(val); handleSearch(val, mode); } function handleModeChange(newMode: SearchMode) { setMode(newMode); if (query.trim()) { handleSearch(query, newMode); } } const filteredResults = selectedSection === "all" ? results : results.filter((r) => r.section === selectedSection); return (
{/* Header */}

Multi-Modal Search Console

Query across knowledge documents and CTF writeups using PostgreSQL Full-Text Search (tsvector), Cosine Vector Embeddings, or Reciprocal Rank Fusion (RRF).

{/* Search Box & Controls */}
{/* Search Input Bar */}
{query && ( )}
{/* Algorithm & Filter Controls */}
{/* Mode Selector */}
Algorithm: {[ { id: "hybrid", label: "Hybrid (RRF)", desc: "Reciprocal Rank Fusion (FTS + Vector)" }, { id: "keyword", label: "Keyword (FTS)", desc: "PostgreSQL GIN tsvector" }, { id: "semantic", label: "Semantic Vector", desc: "Cosine Similarity Embeddings" }, ].map((m) => ( ))}
{/* Section Filter Pills */}
{sections.map((s) => ( ))}
{/* Loading Spinner */} {loading && (
SEARCHING_VECTOR_SPACE...
)} {/* Empty State */} {!loading && query && filteredResults.length === 0 && (

No matching documents found

Try switching algorithm mode to Hybrid or Semantic, or search with different keywords.

)} {/* Initial Suggestions */} {!loading && !query && (

Popular Queries:

{[ "stack alignment", "ret2win exploit", "bullmq worker", "streamable http", "full-text search", "caddy reverse proxy", "pgvector cosine", ].map((term) => ( ))}
)} {/* Results List */} {!loading && filteredResults.length > 0 && (
Found {filteredResults.length} result{filteredResults.length !== 1 ? "s" : ""} Ranked by {mode} score
{filteredResults.map((r) => (
{r.section} /{r.slug}
{r.score.toFixed(3)}

{r.title}

{r.snippet && (

)} ))}

)}
); } export default function SearchPage() { return (

Search

Loading search module...

} > ); }