Search: async answer/results

This commit is contained in:
2026-02-14 01:53:34 -08:00
parent bec25aa943
commit 769cd6966a
4 changed files with 540 additions and 66 deletions

View File

@@ -4,7 +4,7 @@ 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 { createSearch, runSearchStream, type SearchDetail } from "@/lib/api";
import { useSessionAuth } from "@/hooks/use-session-auth";
function readQueryFromUrl() {
@@ -41,6 +41,7 @@ export default function SearchRoutePage() {
const [isRunning, setIsRunning] = useState(false);
const [error, setError] = useState<string | null>(null);
const requestCounterRef = useRef(0);
const streamAbortRef = useRef<AbortController | null>(null);
useEffect(() => {
const onPopState = () => {
@@ -52,6 +53,13 @@ export default function SearchRoutePage() {
return () => window.removeEventListener("popstate", onPopState);
}, []);
useEffect(() => {
return () => {
streamAbortRef.current?.abort();
streamAbortRef.current = null;
};
}, []);
const runQuery = async (query: string) => {
const trimmed = query.trim();
if (!trimmed) {
@@ -61,6 +69,9 @@ export default function SearchRoutePage() {
}
const requestId = ++requestCounterRef.current;
streamAbortRef.current?.abort();
const abortController = new AbortController();
streamAbortRef.current = abortController;
setError(null);
setIsRunning(true);
@@ -86,16 +97,78 @@ export default function SearchRoutePage() {
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);
}
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);
@@ -104,6 +177,7 @@ export default function SearchRoutePage() {
}
} finally {
if (requestId === requestCounterRef.current) {
streamAbortRef.current = null;
setIsRunning(false);
}
}