Implement streaming

This commit is contained in:
2026-02-14 21:15:54 -08:00
parent 0c892d0ffa
commit 642d0ba460
3 changed files with 184 additions and 11 deletions

View File

@@ -16,7 +16,7 @@ import {
getSearch,
listChats,
listSearches,
runCompletion,
runCompletionStream,
runSearchStream,
type ModelCatalogResponse,
type Provider,
@@ -268,6 +268,7 @@ export default function App() {
const [error, setError] = useState<string | null>(null);
const transcriptEndRef = useRef<HTMLDivElement>(null);
const contextMenuRef = useRef<HTMLDivElement>(null);
const selectedItemRef = useRef<SidebarSelection | null>(null);
const searchRunAbortRef = useRef<AbortController | null>(null);
const searchRunCounterRef = useRef(0);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
@@ -404,6 +405,10 @@ export default function App() {
const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null;
useEffect(() => {
selectedItemRef.current = selectedItem;
}, [selectedItem]);
useEffect(() => {
if (!isAuthenticated) {
setSelectedChat(null);
@@ -438,6 +443,13 @@ export default function App() {
const messages = selectedChat?.messages ?? [];
const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search";
const isSearchRunning = isSending && isSearchMode;
const isSendingActiveChat =
isSending &&
!isSearchMode &&
!!pendingChatState &&
!!pendingChatState.chatId &&
selectedItem?.kind === "chat" &&
selectedItem.id === pendingChatState.chatId;
const displayMessages = useMemo(() => {
if (!pendingChatState) return messages;
if (pendingChatState.chatId) {
@@ -606,14 +618,62 @@ export default function App() {
throw new Error("No model available for selected provider");
}
await runCompletion({
chatId,
provider,
model: selectedModel,
messages: requestMessages,
});
let streamErrorMessage: string | null = null;
await Promise.all([refreshCollections({ kind: "chat", id: chatId }), refreshChat(chatId)]);
await runCompletionStream(
{
chatId,
provider,
model: selectedModel,
messages: requestMessages,
},
{
onMeta: (payload) => {
if (payload.chatId !== chatId) return;
setPendingChatState((current) => (current ? { ...current, chatId: payload.chatId } : current));
},
onDelta: (payload) => {
if (!payload.text) return;
setPendingChatState((current) => {
if (!current) return current;
let updated = false;
const nextMessages = current.messages.map((message, index, all) => {
const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
if (!isTarget) return message;
updated = true;
return { ...message, content: message.content + payload.text };
});
return updated ? { ...current, messages: nextMessages } : current;
});
},
onDone: (payload) => {
setPendingChatState((current) => {
if (!current) return current;
let updated = false;
const nextMessages = current.messages.map((message, index, all) => {
const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
if (!isTarget) return message;
updated = true;
return { ...message, content: payload.text };
});
return updated ? { ...current, messages: nextMessages } : current;
});
},
onError: (payload) => {
streamErrorMessage = payload.message;
},
}
);
if (streamErrorMessage) {
throw new Error(streamErrorMessage);
}
await refreshCollections();
const currentSelection = selectedItemRef.current;
if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
await refreshChat(chatId);
}
setPendingChatState(null);
};
@@ -914,7 +974,7 @@ export default function App() {
<div className="flex-1 overflow-y-auto px-3 py-6 md:px-10">
{!isSearchMode ? (
<ChatMessagesPanel messages={displayMessages} isLoading={isLoadingSelection} isSending={isSending} />
<ChatMessagesPanel messages={displayMessages} isLoading={isLoadingSelection} isSending={isSendingActiveChat} />
) : (
<SearchResultsPanel search={selectedSearch} isLoading={isLoadingSelection} isRunning={isSearchRunning} />
)}