diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index e16a7e37..e8be7565 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react"; -import { useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ConversationEmptyState } from "@/components/ai-elements/conversation"; @@ -44,6 +44,7 @@ export default function ChatPage() { useSpecificChatMode(); const [settings, setSettings] = useLocalSettings(); const { setOpen: setSidebarOpen } = useSidebar(); + const router = useRouter(); const { artifacts, open: artifactsOpen, @@ -51,9 +52,12 @@ export default function ChatPage() { setArtifacts, select: selectArtifact, selectedArtifact, + deselect: deselectArtifact, + setFullscreen: setArtifactsFullscreen, fullscreen, } = useArtifacts(); const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat(); + const searchParams = useSearchParams(); // History render rules: // - /workspace/chats/{thread_id}: always render history @@ -63,8 +67,9 @@ export default function ChatPage() { searchParams.get("xclaw_used")?.trim().toLowerCase() === "true"; // Submission strategy: + // - isnew=false + thread_id: reuse existing thread (explicit request from URL) // - xclaw_used=true: follow `isnew` (isnew=false => reuse existing thread) - // - xclaw_used!=true: always create/start a new session (no history) + // - otherwise: create/start a new session (no history) const createNewSession = useMemo(() => { if (!isNewThread) { return false; @@ -99,7 +104,8 @@ export default function ChatPage() { isMock, onStart: (currentThreadId) => { setIsNewThread(false); - history.replaceState(null, "", pathOfThread(currentThreadId)); + // Keep /new in history so router.back() can return to it. + history.pushState(null, "", pathOfThread(currentThreadId)); }, onFinish: (state) => { if (document.hidden || !document.hasFocus()) { @@ -205,6 +211,22 @@ export default function ChatPage() { await thread.stop(); }, [thread]); + const resetNewSessionState = useCallback(() => { + setIsNewThread(true); + setHasSubmitted(false); + setHistoryCutoff(null); + setArtifacts([]); + deselectArtifact(); + setArtifactsOpen(false); + setArtifactsFullscreen(false); + }, [ + deselectArtifact, + setArtifacts, + setArtifactsFullscreen, + setArtifactsOpen, + setIsNewThread, + ]); + return (
确定 @@ -513,7 +542,7 @@ export default function ChatPage() { {/* MARK: 开发测试:iframe 通信功能测试面板 */} - {process.env.NODE_ENV !== "production" && } + {/* {process.env.NODE_ENV !== "production" && } */}
); diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index 7009a368..d5fc4024 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -4,24 +4,64 @@ import { useParams, usePathname, useRouter, useSearchParams } from "next/navigat import { useEffect, useState } from "react"; export function useThreadChat() { - const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); const pathname = usePathname(); const router = useRouter(); + const params = useParams<{ thread_id?: string }>(); + const threadIdFromPathname = (() => { + const parts = pathname.split("?")[0]?.split("/") ?? []; + const idx = parts.lastIndexOf("chats"); + if (idx >= 0 && parts.length > idx + 1) { + return parts[idx + 1]; + } + return undefined; + })(); + const threadIdFromPath = params?.thread_id !== 'new' ? params?.thread_id : threadIdFromPathname; + console.log("[useThreadChat] pathname", pathname); + console.log("[useThreadChat] params.thread_id", params?.thread_id); + console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname); + console.log("[useThreadChat] threadIdFromPath", threadIdFromPath); + const readStoredThreadId = () => { + if (typeof window === "undefined") { + return undefined; + } + const stored = window.sessionStorage.getItem("workspace.thread_id"); + return stored && stored !== "new" ? stored : undefined; + }; const searchParams = useSearchParams(); + const readQueryThreadId = () => { + const fromHook = searchParams.get("thread_id")?.trim(); + if (fromHook && fromHook !== "new") { + return fromHook; + } + if (typeof window === "undefined") { + return undefined; + } + const fromLocation = new URLSearchParams(window.location.search).get( + "thread_id", + ); + if (fromLocation && fromLocation !== "new") { + return fromLocation.trim(); + } + return undefined; + }; + const queryThreadIdFromParams = readQueryThreadId(); + console.log("[useThreadChat] query.thread_id", queryThreadIdFromParams); + const normalizeThreadId = (value?: string | null) => { + if (!value) { + return undefined; + } + return value === "new" ? queryThreadIdFromParams : value; + }; const xClawUsedFromQuery = searchParams.get("xclaw_used"); const isNewFromQuery = searchParams.get("isnew")?.trim().toLowerCase() === "false"; - const queryThreadIdFromParams = searchParams.get("thread_id")?.trim(); - const shouldUseQueryThreadId = - pathname.startsWith("/workspace/chats/") && - !!queryThreadIdFromParams && - (xClawUsedFromQuery === "true" || isNewFromQuery); + const effectiveThreadIdFromPath = + normalizeThreadId(threadIdFromPath) ?? readStoredThreadId(); + console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath); + const [threadId, setThreadId] = useState(() => { - if (threadIdFromPath === "new") { - return shouldUseQueryThreadId ? queryThreadIdFromParams : undefined; - } - return threadIdFromPath; + return effectiveThreadIdFromPath ?? undefined; }); const [isNewThread, setIsNewThread] = useState( @@ -29,25 +69,22 @@ export function useThreadChat() { ); useEffect(() => { + if (threadId && threadId !== "new" && typeof window !== "undefined") { + window.sessionStorage.setItem("workspace.thread_id", threadId); + } if (pathname.endsWith("/new")) { setIsNewThread(true); - const nextQueryThreadId = searchParams.get("thread_id")?.trim(); + const nextQueryThreadId = readQueryThreadId(); const nextIsNewFromQuery = searchParams.get("isnew")?.trim().toLowerCase() === "false"; const nextXClawUsed = searchParams.get("xclaw_used"); - const nextShouldUseQueryThreadId = - pathname.startsWith("/workspace/chats/") && - !!nextQueryThreadId && - (nextXClawUsed === "true" || nextIsNewFromQuery); - if (nextShouldUseQueryThreadId && nextQueryThreadId) { - router.replace(`/workspace/chats/${nextQueryThreadId}`); - return; - } - setThreadId(nextShouldUseQueryThreadId ? nextQueryThreadId : undefined); + setThreadId(nextQueryThreadId ?? undefined); return; } setIsNewThread(false); - setThreadId(threadIdFromPath); + console.log("threadIdFromPath", threadIdFromPath, "normalized", normalizeThreadId(threadIdFromPath)); + + setThreadId(normalizeThreadId(threadIdFromPath)); }, [pathname, router, searchParams, threadIdFromPath]); const isMock = searchParams.get("mock") === "true"; return { threadId, isNewThread, setIsNewThread, isMock };