diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index b8e1f3c6..a3c64be1 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 { Ticker } from "@tombcato/smart-ticker"; -import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react"; +import { FilesIcon, XIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; @@ -23,20 +23,20 @@ import { } from "@/components/workspace/artifacts"; import { useThreadChat } from "@/components/workspace/chats"; // import { DevTodoList } from "@/components/workspace/dev-todo-list"; -import { IframeTestPanel } from "@/components/workspace/iframe-test-panel"; import { InputBox } from "@/components/workspace/input-box"; import { MessageList } from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; -import { ThreadTitle } from "@/components/workspace/thread-title"; import { Tooltip } from "@/components/workspace/tooltip"; import { useSpecificChatMode } from "@/components/workspace/use-chat-mode"; import { Welcome } from "@/components/workspace/welcome"; import { getAPIClient } from "@/core/api"; import { sanitizeArtifactPaths } from "@/core/artifacts/utils"; +import { getBackendBaseURL } from "@/core/config"; import { useI18n } from "@/core/i18n/hooks"; import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { useNotification } from "@/core/notification/hooks"; import { useLocalSettings } from "@/core/settings"; +import { clearThreadMemoryOnExit } from "@/core/threads/exit-thread-memory"; import { useThreadStream } from "@/core/threads/hooks"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; @@ -60,8 +60,6 @@ export default function ChatPage() { setArtifacts, select: selectArtifact, selectedArtifact, - deselect: deselectArtifact, - setFullscreen: setArtifactsFullscreen, fullscreen, } = useArtifacts(); const { threadId, isNewThread, setIsNewThread, isMock, showWelcomeStyle } = @@ -303,6 +301,8 @@ export default function ChatPage() { const todoListCollapsed = true; const [showExitDialog, setShowExitDialog] = useState(false); + const [clearMemoryOnExit, setClearMemoryOnExit] = useState(false); + const [isConfirmingExit, setIsConfirmingExit] = useState(false); const isStreaming = isUploading || thread.isLoading; const handleSubmit = useCallback( async (message: Parameters[1]) => { @@ -627,7 +627,16 @@ export default function ChatPage() { {/* 退出确认对话框 */} - + { + setShowExitDialog(open); + if (!open) { + setClearMemoryOnExit(false); + setIsConfirmingExit(false); + } + }} + > {t.chatPage.exitDialogTitle} @@ -635,11 +644,22 @@ export default function ChatPage() {

{t.chatPage.exitDialogDescription}

+ @@ -647,25 +667,31 @@ export default function ChatPage() { className="w-full bg-ws-surface-subtle hover:bg-ws-interactive-primary hover:text-primary-foreground" variant="ghost" onClick={async () => { - // 如果正在生成,先终止再退出 - if (thread.isLoading) { - await handleStop(); + setIsConfirmingExit(true); + try { + if (thread.isLoading) { + await handleStop(); + } + + await clearThreadMemoryOnExit({ + backendBaseURL: getBackendBaseURL(), + threadId: safeThreadId, + shouldClearMemory: clearMemoryOnExit, + }); + + setShowExitDialog(false); + sendToParent({ + type: POST_MESSAGE_TYPES.IS_CHATTING, + isChatting: false, + }); + router.replace(`/workspace/chats/new?thread_id=${threadId}`); + } catch { + toast.error(t.threadMemoryPanel.toastDeleteFailed); + } finally { + setIsConfirmingExit(false); } - setShowExitDialog(false); - sendToParent({ - type: POST_MESSAGE_TYPES.IS_CHATTING, - isChatting: false, - }); - // 始终复用 query 中的 thread_id。 - const nextQuery = new URLSearchParams(); - if (threadId && threadId !== "new") { - nextQuery.set("thread_id", threadId); - } - // /workspace/chats/${threadId}?is_chatting=false - router.replace( - `/workspace/chats/new?thread_id=${threadId}`, - ); }} + disabled={isConfirmingExit} > {t.chatPage.exitDialogConfirm} diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index 96b04171..07d2c7db 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -298,6 +298,7 @@ export const enUS: Translations = { exitDialogTitle: "Notice", exitDialogDescription: "Chat history is automatically deleted every seven days. You will return to the welcome page now. Continue?", + exitDialogClearMemory: "Also clear memory for this thread", exitDialogConfirm: "Confirm", selectedSkillLoadFailed: "Failed to load skill", unknownErrorRetry: "An unknown error occurred. Please try again later.", diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index 2f877b03..92408fe8 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -227,6 +227,7 @@ export interface Translations { noArtifactSelectedDescription: string; exitDialogTitle: string; exitDialogDescription: string; + exitDialogClearMemory: string; exitDialogConfirm: string; selectedSkillLoadFailed: string; unknownErrorRetry: string; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index a59ad2b0..7ca7a67b 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -323,6 +323,7 @@ export const zhCN: Translations = { exitDialogTitle: "提示", exitDialogDescription: "每七天自动删除。现在将返回欢迎页且清空聊天消息,是否继续?", + exitDialogClearMemory: "同时清除当前会话的记忆", exitDialogConfirm: "确定", selectedSkillLoadFailed: "技能加载失败", unknownErrorRetry: "发生了未知错误,请稍后重试。", diff --git a/frontend/src/core/threads/exit-thread-memory.test.ts b/frontend/src/core/threads/exit-thread-memory.test.ts new file mode 100644 index 00000000..7a91c45a --- /dev/null +++ b/frontend/src/core/threads/exit-thread-memory.test.ts @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const { clearThreadMemoryOnExit } = await import( + new URL("./exit-thread-memory.ts", import.meta.url).href +); + +void test("clears thread memory when checkbox is enabled", async () => { + const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []; + + globalThis.fetch = (async (input, init) => { + calls.push({ input, init }); + return new Response(null, { status: 204 }); + }) as typeof fetch; + + await clearThreadMemoryOnExit({ + backendBaseURL: "http://localhost:3000", + threadId: "thread-123", + shouldClearMemory: true, + }); + + assert.equal(calls.length, 1); + assert.equal( + calls[0]?.input, + "http://localhost:3000/api/threads/thread-123/memory", + ); + assert.equal(calls[0]?.init?.method, "DELETE"); +}); + +void test("skips clearing thread memory when checkbox is disabled", async () => { + let called = false; + + globalThis.fetch = (async () => { + called = true; + return new Response(null, { status: 204 }); + }) as typeof fetch; + + await clearThreadMemoryOnExit({ + backendBaseURL: "http://localhost:3000", + threadId: "thread-123", + shouldClearMemory: false, + }); + + assert.equal(called, false); +}); diff --git a/frontend/src/core/threads/exit-thread-memory.ts b/frontend/src/core/threads/exit-thread-memory.ts new file mode 100644 index 00000000..8c7030b9 --- /dev/null +++ b/frontend/src/core/threads/exit-thread-memory.ts @@ -0,0 +1,24 @@ +type ClearThreadMemoryOnExitParams = { + backendBaseURL?: string; + threadId?: string; + shouldClearMemory: boolean; +}; + +export async function clearThreadMemoryOnExit({ + backendBaseURL = "", + threadId, + shouldClearMemory, +}: ClearThreadMemoryOnExitParams) { + if (!threadId || !shouldClearMemory) { + return; + } + + const res = await fetch( + `${backendBaseURL}/api/threads/${encodeURIComponent(threadId)}/memory`, + { method: "DELETE" }, + ); + + if (!res.ok) { + throw new Error(`Failed to clear thread memory: HTTP ${res.status}`); + } +}