fixed page state while generating a message
This commit is contained in:
+48
-34
@@ -3,12 +3,11 @@
|
||||
======================================== */
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMatch, useNavigate } from "react-router-dom";
|
||||
import Sidebar from "./components/sidebar/Sidebar";
|
||||
import TopNav from "./components/top-nav/TopNav";
|
||||
import ChatHistory from "./components/chat-history/ChatHistory";
|
||||
import ChatInput from "./components/chat-input/ChatInput";
|
||||
import SelectorFlow from "./components/selector-flow/SelectorFlow";
|
||||
import { useAuth } from "./lib/auth/SupabaseAuth";
|
||||
import SignInModal from "./lib/auth/SignIn";
|
||||
import { resetAppState } from "./appState";
|
||||
@@ -94,9 +93,10 @@ const chapterData = {
|
||||
};
|
||||
|
||||
function App() {
|
||||
const { isAuthenticated, user, signIn, signUp, signOut, getToken } = useAuth();
|
||||
const { isAuthenticated, user, signIn, signUp, signOut, getToken } =
|
||||
useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { chatId: routeChatId } = useParams();
|
||||
const routeChatId = useMatch("/chat/:chatId")?.params.chatId ?? null;
|
||||
// Guards deep-link loading so it only fires once per route/authenticated change.
|
||||
const chatLoadHandledRef = useRef(null);
|
||||
const [messages, setMessages] = useState([]);
|
||||
@@ -166,7 +166,7 @@ function App() {
|
||||
chatLoadHandledRef.current = null;
|
||||
navigate("/");
|
||||
}
|
||||
}, [isAuthenticated, navigate, routeChatId]);
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
// Deep link: landing on /chat/:chatId loads that conversation (signed-in
|
||||
// users only — anonymous trials are in-memory, so a refreshed trial URL
|
||||
@@ -178,8 +178,6 @@ function App() {
|
||||
handleChatSelect(routeChatId);
|
||||
}, [routeChatId, isAuthenticated]);
|
||||
|
||||
|
||||
|
||||
// ─── Auth handlers ───
|
||||
const handleAuthError = (msg) => {
|
||||
setMessages((prev) => [
|
||||
@@ -196,7 +194,10 @@ function App() {
|
||||
const headers = {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/sessions`, { headers, credentials: "include" });
|
||||
const res = await fetch(`${API_BASE}/api/sessions`, {
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSessions(data);
|
||||
@@ -250,7 +251,10 @@ function App() {
|
||||
const headers = {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/chat/${sessionId}`, { headers, credentials: "include" });
|
||||
const res = await fetch(`${API_BASE}/api/chat/${sessionId}`, {
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
@@ -338,9 +342,9 @@ function App() {
|
||||
const body = {
|
||||
text: text.trim(),
|
||||
chatId: fetchChatId,
|
||||
grade: activeGrade !== DEFAULTS.grade ? activeGrade : undefined,
|
||||
subject: activeSubject !== DEFAULTS.subject ? activeSubject : undefined,
|
||||
chapter: activeChapter !== DEFAULTS.chapter ? activeChapter : undefined,
|
||||
grade: activeGrade === DEFAULTS.grade ? undefined : activeGrade,
|
||||
subject: activeSubject === DEFAULTS.subject ? undefined : activeSubject,
|
||||
chapter: activeChapter === DEFAULTS.chapter ? undefined : activeChapter,
|
||||
};
|
||||
|
||||
// Open SSE connection
|
||||
@@ -406,17 +410,19 @@ function App() {
|
||||
try {
|
||||
const data = JSON.parse(jsonStr);
|
||||
if (data.type === "session") {
|
||||
// Remember which session this conversation belongs to so
|
||||
// follow-up messages reuse it (history stays together).
|
||||
// For anonymous users this is the in-memory trial id.
|
||||
setActiveChat(data.id);
|
||||
// Only reflect signed-in sessions in the URL. An anonymous
|
||||
// trial id is a capability (no server ownership check), so
|
||||
// baking it into the URL would leak it via history/logs/
|
||||
// referer — keep it in in-memory state and the POST body only.
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate(`/chat/${data.id}`, { replace: true });
|
||||
// Persist the deep-link without dispatching a router
|
||||
// navigation. React Router navigation rerenders the route
|
||||
// tree during the active stream; history.replaceState only
|
||||
// updates the address bar, so chat state remains untouched.
|
||||
chatLoadHandledRef.current = data.id;
|
||||
window.history.replaceState(
|
||||
window.history.state,
|
||||
"",
|
||||
`/chat/${encodeURIComponent(data.id)}`,
|
||||
);
|
||||
}
|
||||
} else if (data.type === "limit") {
|
||||
// Anonymous free-message budget remaining (server truth).
|
||||
@@ -425,19 +431,15 @@ function App() {
|
||||
assistantText += data.content;
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, text: assistantText }
|
||||
: m
|
||||
)
|
||||
m.id === assistantId ? { ...m, text: assistantText } : m,
|
||||
),
|
||||
);
|
||||
} else if (data.type === "done") {
|
||||
// Mark as non-streaming
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, streaming: false }
|
||||
: m
|
||||
)
|
||||
m.id === assistantId ? { ...m, streaming: false } : m,
|
||||
),
|
||||
);
|
||||
} else if (data.type === "error") {
|
||||
setMessages((prev) =>
|
||||
@@ -448,8 +450,8 @@ function App() {
|
||||
text: `⚠️ Error: ${data.message}`,
|
||||
streaming: false,
|
||||
}
|
||||
: m
|
||||
)
|
||||
: m,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
@@ -461,7 +463,8 @@ function App() {
|
||||
setIsTyping(false);
|
||||
abortRef.current = null;
|
||||
|
||||
// Refresh sessions to update timestamps (signed-in users only)
|
||||
// Refresh the sidebar without changing routes. The active session id
|
||||
// already lives in state and is reused by the next message.
|
||||
if (isAuthenticated) loadSessions();
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -478,14 +481,23 @@ function App() {
|
||||
abortRef.current = null;
|
||||
});
|
||||
},
|
||||
[isTyping, activeChat, activeGrade, activeSubject, activeChapter, isAnonymous, anonRemaining, isAuthenticated]
|
||||
[
|
||||
isTyping,
|
||||
activeChat,
|
||||
activeGrade,
|
||||
activeSubject,
|
||||
activeChapter,
|
||||
isAnonymous,
|
||||
anonRemaining,
|
||||
isAuthenticated,
|
||||
],
|
||||
);
|
||||
|
||||
const handlePromptClick = useCallback(
|
||||
(promptText) => {
|
||||
handleSend(promptText);
|
||||
},
|
||||
[handleSend]
|
||||
[handleSend],
|
||||
);
|
||||
|
||||
/* ─── Selector step handlers ─── */
|
||||
@@ -619,7 +631,9 @@ function App() {
|
||||
onPromptClick={handlePromptClick}
|
||||
isTyping={isTyping}
|
||||
selectorStep={selectorStep}
|
||||
selectorData={selectorStep !== "chat" ? selectorData[selectorStep] : null}
|
||||
selectorData={
|
||||
selectorStep === "chat" ? null : selectorData[selectorStep]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user