Files
Sybil-2/web/src/pages/search-route-page.tsx

162 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-02-14 00:22:19 -08:00
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, runSearch, 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<SearchDetail | null>(null);
const [isRunning, setIsRunning] = useState(false);
const [error, setError] = useState<string | null>(null);
const requestCounterRef = useRef(0);
useEffect(() => {
const onPopState = () => {
const next = readQueryFromUrl();
setRouteQuery(next);
setQueryInput(next);
};
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
}, []);
const runQuery = async (query: string) => {
const trimmed = query.trim();
if (!trimmed) {
setSearch(null);
setError(null);
return;
}
const requestId = ++requestCounterRef.current;
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),
});
const result = await runSearch(created.id, {
query: trimmed,
title: trimmed.slice(0, 80),
type: "auto",
numResults: 10,
});
if (requestId === requestCounterRef.current) {
setSearch(result);
}
} catch (err) {
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) {
setIsRunning(false);
}
}
};
useEffect(() => {
if (!isAuthenticated) return;
void runQuery(routeQuery);
}, [isAuthenticated, routeQuery]);
if (isCheckingSession) {
return (
<div className="flex h-full items-center justify-center">
<p className="text-sm text-muted-foreground">Checking session...</p>
</div>
);
}
if (!isAuthenticated) {
return (
<AuthScreen
authTokenInput={authTokenInput}
setAuthTokenInput={setAuthTokenInput}
isSigningIn={isSigningIn}
authError={authError}
onSignIn={handleSignIn}
/>
);
}
return (
<div className="h-full overflow-y-auto px-3 py-6 md:px-6">
<div className="mx-auto w-full max-w-4xl space-y-5">
<form
className="flex items-center gap-2 rounded-xl border bg-background p-2 shadow-sm"
onSubmit={(event) => {
event.preventDefault();
const next = queryInput.trim();
pushSearchQuery(next);
setRouteQuery(next);
}}
>
<Input value={queryInput} onInput={(event) => setQueryInput(event.currentTarget.value)} placeholder="Search the web" />
<Button type="submit" size="icon" disabled={!queryInput.trim() || isRunning}>
<Search className="h-4 w-4" />
</Button>
</form>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<SearchResultsPanel search={search} isLoading={false} isRunning={isRunning} showPrompt={false} className="w-full" />
</div>
</div>
);
}