mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-05 12:28:37 +00:00
fix(frontend): reset new chat on client-side navigation (#3673)
* fix(frontend): reset new chat on client-side navigation Drive the chat reset effect with Next.js's reactive pathname instead of the render-time window.location-derived flag. During App Router transitions, window.location may still point to the previous thread until commit, leaving chat state stale until another UI interaction triggers a render. Preserve the stale 'new' param guard so created thread UUIDs are not overwritten. * test(frontend): add e2e to cover history-only new chat reset * docs(frontend): clarify new chat pathname synchronization
This commit is contained in:
parent
4572217038
commit
5ddf698895
@ -27,6 +27,9 @@ export function resetThreadChatAfterDelete(detail: ThreadChatResetDetail) {
|
|||||||
export function useThreadChat() {
|
export function useThreadChat() {
|
||||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
// Render-time values use the committed browser URL. The sync effect below
|
||||||
|
// intentionally watches the reactive pathname so client navigation still
|
||||||
|
// schedules a reset when window.location is stale during render.
|
||||||
const actualPathname =
|
const actualPathname =
|
||||||
typeof window === "undefined" ? pathname : window.location.pathname;
|
typeof window === "undefined" ? pathname : window.location.pathname;
|
||||||
const isNewPath = actualPathname.endsWith("/new");
|
const isNewPath = actualPathname.endsWith("/new");
|
||||||
@ -57,7 +60,7 @@ export function useThreadChat() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNewPath) {
|
if (pathname.endsWith("/new")) {
|
||||||
const nextThreadId = newThreadIdRef.current ?? uuid();
|
const nextThreadId = newThreadIdRef.current ?? uuid();
|
||||||
newThreadIdRef.current = nextThreadId;
|
newThreadIdRef.current = nextThreadId;
|
||||||
setIsNewThreadState(true);
|
setIsNewThreadState(true);
|
||||||
@ -65,17 +68,15 @@ export function useThreadChat() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newThreadIdRef.current = null;
|
newThreadIdRef.current = null;
|
||||||
// Guard: after history.replaceState updates the URL from /chats/new to
|
// Native history updates the canonical pathname but preserves the route
|
||||||
// /chats/{UUID}, Next.js useParams may still return the stale "new" value
|
// tree, so useParams may still return the stale "new" value. Avoid passing
|
||||||
// because replaceState does not trigger router updates. Avoid propagating
|
// it to downstream hooks (e.g. useStream), which would cause a 422.
|
||||||
// this invalid thread ID to downstream hooks (e.g. useStream), which would
|
|
||||||
// cause a 422 from LangGraph Server.
|
|
||||||
if (threadIdFromPath === "new") {
|
if (threadIdFromPath === "new") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsNewThreadState(false);
|
setIsNewThreadState(false);
|
||||||
setThreadIdState(threadIdFromPath);
|
setThreadIdState(threadIdFromPath);
|
||||||
}, [isNewPath, threadIdFromPath]);
|
}, [pathname, threadIdFromPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleReset = (event: Event) => {
|
const handleReset = (event: Event) => {
|
||||||
|
|||||||
@ -202,6 +202,43 @@ test.describe("Thread history", () => {
|
|||||||
await expect(page.getByPlaceholder(/how can i assist you/i)).toBeVisible();
|
await expect(page.getByPlaceholder(/how can i assist you/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("new chat resets immediately after a history-only thread URL update", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
mockLangGraphAPI(page);
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||||
|
await textarea.fill("Message that must disappear in the next new chat");
|
||||||
|
await textarea.press("Enter");
|
||||||
|
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// A newly created chat changes the URL with history.replaceState so the
|
||||||
|
// active stream is not remounted. Reproduce that history-only transition:
|
||||||
|
// the canonical pathname becomes the UUID while useParams can stay "new".
|
||||||
|
await page.evaluate((threadId) => {
|
||||||
|
history.replaceState(null, "", `/workspace/chats/${threadId}`);
|
||||||
|
}, MOCK_THREAD_ID);
|
||||||
|
|
||||||
|
const newChatLink = page.locator(
|
||||||
|
"[data-sidebar='sidebar'] a[href='/workspace/chats/new']",
|
||||||
|
);
|
||||||
|
await expect(page).toHaveURL(
|
||||||
|
new RegExp(`/workspace/chats/${MOCK_THREAD_ID}$`),
|
||||||
|
);
|
||||||
|
await expect(newChatLink).toHaveAttribute("data-active", "false");
|
||||||
|
|
||||||
|
// One click must reset the chat without a second click or unrelated UI
|
||||||
|
// interaction forcing another render.
|
||||||
|
await newChatLink.click();
|
||||||
|
await expect(page).toHaveURL(/\/workspace\/chats\/new$/);
|
||||||
|
await expect(page.getByText("Hello from DeerFlow!")).toHaveCount(0);
|
||||||
|
await expect(textarea).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("deleting the active newly created chat returns to the new chat screen", async ({
|
test("deleting the active newly created chat returns to the new chat screen", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user