Compare commits
3 Commits
ce4b0dcd4d
...
8b0c69327b
| Author | SHA1 | Date |
|---|---|---|
|
|
8b0c69327b | |
|
|
875cfa7e7b | |
|
|
081adb34b3 |
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
|
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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
||||||
|
|
@ -44,6 +44,7 @@ export default function ChatPage() {
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
const [settings, setSettings] = useLocalSettings();
|
const [settings, setSettings] = useLocalSettings();
|
||||||
const { setOpen: setSidebarOpen } = useSidebar();
|
const { setOpen: setSidebarOpen } = useSidebar();
|
||||||
|
const router = useRouter();
|
||||||
const {
|
const {
|
||||||
artifacts,
|
artifacts,
|
||||||
open: artifactsOpen,
|
open: artifactsOpen,
|
||||||
|
|
@ -51,9 +52,12 @@ export default function ChatPage() {
|
||||||
setArtifacts,
|
setArtifacts,
|
||||||
select: selectArtifact,
|
select: selectArtifact,
|
||||||
selectedArtifact,
|
selectedArtifact,
|
||||||
|
deselect: deselectArtifact,
|
||||||
|
setFullscreen: setArtifactsFullscreen,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
} = useArtifacts();
|
} = useArtifacts();
|
||||||
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
// History render rules:
|
// History render rules:
|
||||||
// - /workspace/chats/{thread_id}: always render history
|
// - /workspace/chats/{thread_id}: always render history
|
||||||
|
|
@ -62,9 +66,13 @@ export default function ChatPage() {
|
||||||
!isNewThread ||
|
!isNewThread ||
|
||||||
searchParams.get("xclaw_used")?.trim().toLowerCase() === "true";
|
searchParams.get("xclaw_used")?.trim().toLowerCase() === "true";
|
||||||
|
|
||||||
// Submission strategy:
|
// Submission strategy: always reuse the thread id from query; never create new.
|
||||||
|
const createNewSession = false;
|
||||||
|
/*
|
||||||
|
// Original 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: 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(() => {
|
const createNewSession = useMemo(() => {
|
||||||
if (!isNewThread) {
|
if (!isNewThread) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -81,6 +89,7 @@ export default function ChatPage() {
|
||||||
}
|
}
|
||||||
return searchParams.get("isnew")?.trim().toLowerCase() !== "false";
|
return searchParams.get("isnew")?.trim().toLowerCase() !== "false";
|
||||||
}, [isNewThread, searchParams]);
|
}, [isNewThread, searchParams]);
|
||||||
|
*/
|
||||||
const streamThreadId = useMemo(() => {
|
const streamThreadId = useMemo(() => {
|
||||||
return isNewThread && createNewSession ? undefined : threadId;
|
return isNewThread && createNewSession ? undefined : threadId;
|
||||||
}, [createNewSession, isNewThread, threadId]);
|
}, [createNewSession, isNewThread, threadId]);
|
||||||
|
|
@ -99,7 +108,9 @@ export default function ChatPage() {
|
||||||
isMock,
|
isMock,
|
||||||
onStart: (currentThreadId) => {
|
onStart: (currentThreadId) => {
|
||||||
setIsNewThread(false);
|
setIsNewThread(false);
|
||||||
history.replaceState(null, "", pathOfThread(currentThreadId));
|
// Keep /new in history so router.back() can return to it.
|
||||||
|
router.replace(`/workspace/chats/${currentThreadId}`);
|
||||||
|
// history.pushState(null, "", pathOfThread(currentThreadId));
|
||||||
},
|
},
|
||||||
onFinish: (state) => {
|
onFinish: (state) => {
|
||||||
if (document.hidden || !document.hasFocus()) {
|
if (document.hidden || !document.hasFocus()) {
|
||||||
|
|
@ -205,6 +216,25 @@ export default function ChatPage() {
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [thread]);
|
||||||
|
|
||||||
|
const resetNewSessionState = useCallback(() => {
|
||||||
|
setIsNewThread(true);
|
||||||
|
setHasSubmitted(false);
|
||||||
|
setHistoryCutoff(null);
|
||||||
|
setArtifacts([]);
|
||||||
|
deselectArtifact();
|
||||||
|
setArtifactsOpen(false);
|
||||||
|
setArtifactsFullscreen(false);
|
||||||
|
}, [
|
||||||
|
deselectArtifact,
|
||||||
|
setArtifacts,
|
||||||
|
setArtifactsFullscreen,
|
||||||
|
setArtifactsOpen,
|
||||||
|
setIsNewThread,
|
||||||
|
]);
|
||||||
|
// shouldRenderHistory || historyCutoff === null
|
||||||
|
// console.log('shouldRenderHistory', shouldRenderHistory, 'historyCutoff', historyCutoff);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
<ThreadContext.Provider value={{ thread }}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -474,8 +504,15 @@ export default function ChatPage() {
|
||||||
type: POST_MESSAGE_TYPES.XCLAW_USED,
|
type: POST_MESSAGE_TYPES.XCLAW_USED,
|
||||||
XClawUsed: false,
|
XClawUsed: false,
|
||||||
});
|
});
|
||||||
// 使用完整页面刷新确保组件重新挂载,isNewThread 为 true
|
resetNewSessionState();
|
||||||
window.location.reload();
|
// 始终复用 query 中的 thread_id。
|
||||||
|
const nextQuery = new URLSearchParams();
|
||||||
|
nextQuery.set("isnew", "false");
|
||||||
|
nextQuery.set("xclaw_used", "false");
|
||||||
|
if (threadId && threadId !== "new") {
|
||||||
|
nextQuery.set("thread_id", threadId);
|
||||||
|
}
|
||||||
|
router.replace(`/workspace/chats/new?${nextQuery.toString()}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确定
|
确定
|
||||||
|
|
@ -513,7 +550,7 @@ export default function ChatPage() {
|
||||||
</DevDialog>
|
</DevDialog>
|
||||||
|
|
||||||
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
||||||
{process.env.NODE_ENV !== "production" && <IframeTestPanel />}
|
{/* {process.env.NODE_ENV !== "production" && <IframeTestPanel />} */}
|
||||||
</div>
|
</div>
|
||||||
</ThreadContext.Provider>
|
</ThreadContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -468,7 +468,7 @@ export function ArtifactFileDetail({
|
||||||
{previewable &&
|
{previewable &&
|
||||||
viewMode === "preview" &&
|
viewMode === "preview" &&
|
||||||
(language === "markdown" || language === "html") && (
|
(language === "markdown" || language === "html") && (
|
||||||
<div className="min-h-full mb-[150px] rounded-b-[10px] bg-white p-0 mb-0">
|
<div className="min-h-full mb-[180px] rounded-b-[10px] bg-white p-0 mb-0">
|
||||||
<ArtifactFilePreview
|
<ArtifactFilePreview
|
||||||
content={displayContent}
|
content={displayContent}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
|
|
@ -477,7 +477,7 @@ export function ArtifactFileDetail({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isCodeFile && viewMode === "code" && (
|
{isCodeFile && viewMode === "code" && (
|
||||||
<div className="min-h-full mb-[150px] rounded-b-[10px] bg-white p-0 mb-0">
|
<div className="min-h-full mb-[180px] rounded-b-[10px] bg-white p-0 mb-0">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="size-full resize-none rounded-none border-none py-[20px]"
|
className="size-full resize-none rounded-none border-none py-[20px]"
|
||||||
value={displayContent ?? ""}
|
value={displayContent ?? ""}
|
||||||
|
|
@ -487,7 +487,7 @@ export function ArtifactFileDetail({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isCodeFile && (
|
{!isCodeFile && (
|
||||||
<div className="h-full mb-[150px] ">
|
<div className="h-full mb-[180px] ">
|
||||||
<iframe
|
<iframe
|
||||||
className="size-full border-0"
|
className="size-full border-0"
|
||||||
srcDoc={artifactViewerSrcDoc}
|
srcDoc={artifactViewerSrcDoc}
|
||||||
|
|
|
||||||
|
|
@ -4,50 +4,96 @@ import { useParams, usePathname, useRouter, useSearchParams } from "next/navigat
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function useThreadChat() {
|
export function useThreadChat() {
|
||||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const params = useParams<{ thread_id?: string }>();
|
||||||
|
// 兜底:当 params 还未就绪时,从 pathname 解析 thread_id。
|
||||||
|
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;
|
||||||
|
})();
|
||||||
|
// 优先使用 params;如果是 "new",则回退到 pathname 解析出的 id。
|
||||||
|
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);
|
||||||
|
// 持久化兜底:用于处理首屏水合或 params 时序问题。
|
||||||
|
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 searchParams = useSearchParams();
|
||||||
|
// 读取 query 的 thread_id(先用 hook,必要时用 window 兜底)。
|
||||||
|
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);
|
||||||
|
// 归一化:当值为 "new" 时,替换为 query 中的 thread_id(如果存在)。
|
||||||
|
const normalizeThreadId = (value?: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value === "new" ? queryThreadIdFromParams : value;
|
||||||
|
};
|
||||||
const xClawUsedFromQuery = searchParams.get("xclaw_used");
|
const xClawUsedFromQuery = searchParams.get("xclaw_used");
|
||||||
const isNewFromQuery =
|
const isNewFromQuery =
|
||||||
searchParams.get("isnew")?.trim().toLowerCase() === "false";
|
searchParams.get("isnew")?.trim().toLowerCase() === "false";
|
||||||
const queryThreadIdFromParams = searchParams.get("thread_id")?.trim();
|
const effectiveThreadIdFromPath =
|
||||||
const shouldUseQueryThreadId =
|
normalizeThreadId(threadIdFromPath) ?? readStoredThreadId();
|
||||||
pathname.startsWith("/workspace/chats/") &&
|
// console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath);
|
||||||
!!queryThreadIdFromParams &&
|
|
||||||
(xClawUsedFromQuery === "true" || isNewFromQuery);
|
|
||||||
const [threadId, setThreadId] = useState(() => {
|
const [threadId, setThreadId] = useState(() => {
|
||||||
if (threadIdFromPath === "new") {
|
return effectiveThreadIdFromPath ?? undefined;
|
||||||
return shouldUseQueryThreadId ? queryThreadIdFromParams : undefined;
|
|
||||||
}
|
|
||||||
return threadIdFromPath;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// /new 或缺少 query 的 thread_id 时,视为新会话状态。 但是这个并不是新会话的意思,而是说当前处在对话状态。
|
||||||
const [isNewThread, setIsNewThread] = useState(
|
const [isNewThread, setIsNewThread] = useState(
|
||||||
() => threadIdFromPath === "new",
|
() => threadIdFromPath === "new" || !queryThreadIdFromParams,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 记住最近一次有效的 thread_id,供下次加载兜底使用。
|
||||||
|
if (threadId && threadId !== "new" && typeof window !== "undefined") {
|
||||||
|
window.sessionStorage.setItem("workspace.thread_id", threadId);
|
||||||
|
}
|
||||||
if (pathname.endsWith("/new")) {
|
if (pathname.endsWith("/new")) {
|
||||||
setIsNewThread(true);
|
setIsNewThread(true);
|
||||||
const nextQueryThreadId = searchParams.get("thread_id")?.trim();
|
const nextQueryThreadId = readQueryThreadId();
|
||||||
const nextIsNewFromQuery =
|
const nextIsNewFromQuery =
|
||||||
searchParams.get("isnew")?.trim().toLowerCase() === "false";
|
searchParams.get("isnew")?.trim().toLowerCase() === "false";
|
||||||
const nextXClawUsed = searchParams.get("xclaw_used");
|
const nextXClawUsed = searchParams.get("xclaw_used");
|
||||||
const nextShouldUseQueryThreadId =
|
setThreadId(nextQueryThreadId ?? undefined);
|
||||||
pathname.startsWith("/workspace/chats/") &&
|
|
||||||
!!nextQueryThreadId &&
|
|
||||||
(nextXClawUsed === "true" || nextIsNewFromQuery);
|
|
||||||
if (nextShouldUseQueryThreadId && nextQueryThreadId) {
|
|
||||||
router.replace(`/workspace/chats/${nextQueryThreadId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setThreadId(nextShouldUseQueryThreadId ? nextQueryThreadId : undefined);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsNewThread(false);
|
setIsNewThread(false);
|
||||||
setThreadId(threadIdFromPath);
|
// console.log("threadIdFromPath", threadIdFromPath, "normalized", normalizeThreadId(threadIdFromPath));
|
||||||
|
|
||||||
|
setThreadId(normalizeThreadId(threadIdFromPath));
|
||||||
}, [pathname, router, searchParams, threadIdFromPath]);
|
}, [pathname, router, searchParams, threadIdFromPath]);
|
||||||
const isMock = searchParams.get("mock") === "true";
|
const isMock = searchParams.get("mock") === "true";
|
||||||
return { threadId, isNewThread, setIsNewThread, isMock };
|
return { threadId, isNewThread, setIsNewThread, isMock };
|
||||||
|
|
|
||||||
|
|
@ -399,10 +399,10 @@
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
}
|
}
|
||||||
|
/* Chrome, Safari, Opera */
|
||||||
*::-webkit-scrollbar {
|
/* *::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari, Opera */
|
display: none;
|
||||||
}
|
} */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--container-width-xs: calc(var(--spacing) * 72);
|
--container-width-xs: calc(var(--spacing) * 72);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue