From 63563ce6a3d0b97ace843a83b42c2ff4b7d1dd9c Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Tue, 2 Jun 2026 10:21:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E7=BD=AE=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E6=97=B6=E6=96=B0=E5=A2=9Echeckbox=EF=BC=8C=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E4=BC=9A=E8=AF=9D=E7=9A=84memory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/workspace/chats/[thread_id]/page.tsx | 72 +++++++++++++------ frontend/src/core/i18n/locales/en-US.ts | 1 + frontend/src/core/i18n/locales/types.ts | 1 + frontend/src/core/i18n/locales/zh-CN.ts | 1 + .../core/threads/exit-thread-memory.test.ts | 45 ++++++++++++ .../src/core/threads/exit-thread-memory.ts | 24 +++++++ 6 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 frontend/src/core/threads/exit-thread-memory.test.ts create mode 100644 frontend/src/core/threads/exit-thread-memory.ts 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}`); + } +}