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

@@ -17,7 +17,7 @@ import {
listChats,
listSearches,
runCompletion,
runSearch,
runSearchStream,
type ChatDetail,
type ChatSummary,
type CompletionRequestMessage,
@@ -119,6 +119,8 @@ export default function App() {
const [error, setError] = useState<string | null>(null);
const transcriptEndRef = useRef<HTMLDivElement>(null);
const contextMenuRef = useRef<HTMLDivElement>(null);
const searchRunAbortRef = useRef<AbortController | null>(null);
const searchRunCounterRef = useRef(0);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const sidebarItems = useMemo(() => buildSidebarItems(chats, searches), [chats, searches]);
@@ -241,6 +243,13 @@ export default function App() {
transcriptEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
}, [draftKind, selectedChat?.messages.length, isSending, selectedItem?.kind]);
useEffect(() => {
return () => {
searchRunAbortRef.current?.abort();
searchRunAbortRef.current = null;
};
}, []);
const messages = selectedChat?.messages ?? [];
const selectedChatSummary = useMemo(() => {
@@ -411,6 +420,11 @@ export default function App() {
};
const handleSendSearch = async (query: string) => {
const runId = ++searchRunCounterRef.current;
searchRunAbortRef.current?.abort();
const abortController = new AbortController();
searchRunAbortRef.current = abortController;
let searchId = draftKind === "search" ? null : selectedItem?.kind === "search" ? selectedItem.id : null;
if (!searchId) {
@@ -460,15 +474,76 @@ export default function App() {
};
});
const search = await runSearch(searchId, {
query,
title: query.slice(0, 80),
type: "auto",
numResults: 10,
});
try {
await runSearchStream(
searchId,
{
query,
title: query.slice(0, 80),
type: "auto",
numResults: 10,
},
{
onSearchResults: (payload) => {
if (runId !== searchRunCounterRef.current) return;
setSelectedSearch((current) => {
if (!current || current.id !== searchId) return current;
return {
...current,
requestId: payload.requestId ?? current.requestId,
error: null,
results: payload.results,
};
});
},
onSearchError: (payload) => {
if (runId !== searchRunCounterRef.current) return;
setSelectedSearch((current) => {
if (!current || current.id !== searchId) return current;
return { ...current, error: payload.error };
});
},
onAnswer: (payload) => {
if (runId !== searchRunCounterRef.current) return;
setSelectedSearch((current) => {
if (!current || current.id !== searchId) return current;
return {
...current,
answerText: payload.answerText,
answerRequestId: payload.answerRequestId,
answerCitations: payload.answerCitations,
answerError: null,
};
});
},
onAnswerError: (payload) => {
if (runId !== searchRunCounterRef.current) return;
setSelectedSearch((current) => {
if (!current || current.id !== searchId) return current;
return { ...current, answerError: payload.error };
});
},
onDone: (payload) => {
if (runId !== searchRunCounterRef.current) return;
setSelectedSearch(payload.search);
setSelectedChat(null);
},
onError: (payload) => {
if (runId !== searchRunCounterRef.current) return;
setError(payload.message);
},
},
{ signal: abortController.signal }
);
} catch (err) {
if (abortController.signal.aborted) return;
throw err;
} finally {
if (runId === searchRunCounterRef.current) {
searchRunAbortRef.current = null;
}
}
setSelectedSearch(search);
setSelectedChat(null);
await refreshCollections({ kind: "search", id: searchId });
};