Persist streamed assistant answers even if the client disconnects
The first message in a new conversation navigated to /c/:id, which remounted ChatScreen (two separate routes) and aborted the in-flight /stream-query fetch. Server-side that surfaced as asyncio.CancelledError, which isn't an Exception, so the except branch missed it and skipped the add_message_to_conversation save — the fully-streamed answer was never persisted. Backend: handle CancelledError and persist via a detached, shielded task so a disconnect at any point (navigation, tab close, network blip) still commits the answer. Frontend: serve "/" and "/c/:id" from a single catch-all ChatScreen route so first-message navigation no longer remounts and aborts the stream; derive the conversation id via useMatch instead of useParams. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-10
@@ -1,4 +1,4 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import "./App.css";
|
||||
import { AuthProvider } from "./contexts/AuthContext";
|
||||
import { ChatScreen } from "./components/ChatScreen";
|
||||
@@ -31,16 +31,16 @@ const AppContainer = () => {
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<ChatScreen setAuthenticated={setAuthenticated} isAdmin={isAdmin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/c/:conversationId"
|
||||
element={<ChatScreen setAuthenticated={setAuthenticated} isAdmin={isAdmin} />}
|
||||
/>
|
||||
<Route path="/conversations" element={<ConversationsPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
{/* One ChatScreen instance serves both the home ("/") and per-conversation
|
||||
("/c/:id") routes. Using a single catch-all route keeps that instance
|
||||
mounted across the navigate() that fires when the first message creates
|
||||
a conversation — a remount here would abort the in-flight SSE stream
|
||||
(and drop the answer before it's persisted). */}
|
||||
<Route
|
||||
path="*"
|
||||
element={<ChatScreen setAuthenticated={setAuthenticated} isAdmin={isAdmin} />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useMatch } from "react-router-dom";
|
||||
import { conversationService } from "../api/conversationService";
|
||||
import type { Conversation } from "./useConversations";
|
||||
|
||||
@@ -42,7 +42,10 @@ export function useChat({
|
||||
onSessionExpired,
|
||||
scrollToBottom,
|
||||
}: UseChatOptions) {
|
||||
const { conversationId } = useParams();
|
||||
// ChatScreen renders under a single catch-all route so it isn't remounted
|
||||
// when the first message navigates to /c/:id (which would abort the stream),
|
||||
// so the id comes from matching the URL rather than useParams.
|
||||
const conversationId = useMatch("/c/:conversationId")?.params.conversationId;
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [messagesLoading, setMessagesLoading] = useState(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMatch, useNavigate } from "react-router-dom";
|
||||
import { conversationService } from "../api/conversationService";
|
||||
|
||||
export type Conversation = {
|
||||
@@ -13,7 +13,9 @@ const SIDEBAR_LIMIT = 10;
|
||||
|
||||
export function useConversations() {
|
||||
const navigate = useNavigate();
|
||||
const { conversationId } = useParams();
|
||||
// ChatScreen now renders under a single catch-all route, so useParams no
|
||||
// longer captures the id. Derive it from the URL directly instead.
|
||||
const conversationId = useMatch("/c/:conversationId")?.params.conversationId;
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
|
||||
const refreshConversations = useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user