162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|