import { useEffect, useRef, useState } from "preact/hooks"; import { Search } from "lucide-preact"; import { AuthScreen } from "@/components/auth/auth-screen"; import { SearchResultsPanel } from "@/components/search/search-results-panel"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { createSearch, runSearchStream, type SearchDetail } from "@/lib/api"; import { useSessionAuth } from "@/hooks/use-session-auth"; function readQueryFromUrl() { const params = new URLSearchParams(window.location.search); return params.get("q")?.trim() ?? ""; } function pushSearchQuery(query: string) { const params = new URLSearchParams(window.location.search); if (query.trim()) { params.set("q", query.trim()); } else { params.delete("q"); } const next = `${window.location.pathname}${params.toString() ? `?${params.toString()}` : ""}`; window.history.pushState({}, "", next); } export default function SearchRoutePage() { const { authTokenInput, setAuthTokenInput, isCheckingSession, isSigningIn, isAuthenticated, authError, handleAuthFailure, handleSignIn, } = useSessionAuth(); const [queryInput, setQueryInput] = useState(readQueryFromUrl()); const [routeQuery, setRouteQuery] = useState(readQueryFromUrl()); const [search, setSearch] = useState(null); const [isRunning, setIsRunning] = useState(false); const [error, setError] = useState(null); const requestCounterRef = useRef(0); const streamAbortRef = useRef(null); useEffect(() => { const onPopState = () => { const next = readQueryFromUrl(); setRouteQuery(next); setQueryInput(next); }; window.addEventListener("popstate", onPopState); return () => window.removeEventListener("popstate", onPopState); }, []); useEffect(() => { document.title = routeQuery ? `${routeQuery} — Sybil` : "Sybil"; }, [routeQuery]); useEffect(() => { return () => { document.title = "Sybil"; }; }, []); useEffect(() => { return () => { streamAbortRef.current?.abort(); streamAbortRef.current = null; }; }, []); const runQuery = async (query: string) => { const trimmed = query.trim(); if (!trimmed) { setSearch(null); setError(null); return; } const requestId = ++requestCounterRef.current; streamAbortRef.current?.abort(); const abortController = new AbortController(); streamAbortRef.current = abortController; setError(null); setIsRunning(true); const nowIso = new Date().toISOString(); setSearch({ id: `temp-search-${requestId}`, title: trimmed.slice(0, 80), query: trimmed, createdAt: nowIso, updatedAt: nowIso, requestId: null, latencyMs: null, error: null, answerText: null, answerRequestId: null, answerCitations: null, answerError: null, results: [], }); try { const created = await createSearch({ query: trimmed, title: trimmed.slice(0, 80), }); if (requestId !== requestCounterRef.current) return; setSearch((current) => current ? { ...current, id: created.id, title: created.title, query: created.query, createdAt: created.createdAt, updatedAt: created.updatedAt, } : current ); await runSearchStream( created.id, { query: trimmed, title: trimmed.slice(0, 80), type: "auto", numResults: 10, }, { onSearchResults: (payload) => { if (requestId !== requestCounterRef.current) return; setSearch((current) => current ? { ...current, requestId: payload.requestId ?? current.requestId, error: null, results: payload.results, } : current ); }, onSearchError: (payload) => { if (requestId !== requestCounterRef.current) return; setSearch((current) => (current ? { ...current, error: payload.error } : current)); }, onAnswer: (payload) => { if (requestId !== requestCounterRef.current) return; setSearch((current) => current ? { ...current, answerText: payload.answerText, answerRequestId: payload.answerRequestId, answerCitations: payload.answerCitations, answerError: null, } : current ); }, onAnswerError: (payload) => { if (requestId !== requestCounterRef.current) return; setSearch((current) => (current ? { ...current, answerError: payload.error } : current)); }, onDone: (payload) => { if (requestId !== requestCounterRef.current) return; setSearch(payload.search); }, onError: (payload) => { if (requestId !== requestCounterRef.current) return; setError(payload.message); }, }, { signal: abortController.signal } ); } catch (err) { if (abortController.signal.aborted) return; const message = err instanceof Error ? err.message : String(err); if (message.includes("bearer token")) { handleAuthFailure(message); } else if (requestId === requestCounterRef.current) { setError(message); } } finally { if (requestId === requestCounterRef.current) { streamAbortRef.current = null; setIsRunning(false); } } }; useEffect(() => { if (!isAuthenticated) return; void runQuery(routeQuery); }, [isAuthenticated, routeQuery]); if (isCheckingSession) { return (

Checking session...

); } if (!isAuthenticated) { return ( ); } return (
{ event.preventDefault(); const next = queryInput.trim(); pushSearchQuery(next); setRouteQuery(next); }} > setQueryInput(event.currentTarget.value)} placeholder="Search the web" />
{error ?

{error}

: null}
); }