feat: 重置会话时新增checkbox,清除当前会话的memory

This commit is contained in:
肖应宇 2026-06-02 10:21:33 +08:00
parent dd98337a92
commit 63563ce6a3
6 changed files with 121 additions and 23 deletions

View File

@ -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<typeof sendMessage>[1]) => {
@ -627,7 +627,16 @@ export default function ChatPage() {
</div>
{/* 退出确认对话框 */}
<DevDialog open={showExitDialog} onOpenChange={setShowExitDialog}>
<DevDialog
open={showExitDialog}
onOpenChange={(open) => {
setShowExitDialog(open);
if (!open) {
setClearMemoryOnExit(false);
setIsConfirmingExit(false);
}
}}
>
<DevDialogContent>
<DevDialogHeader>
<DevDialogTitle>{t.chatPage.exitDialogTitle}</DevDialogTitle>
@ -635,11 +644,22 @@ export default function ChatPage() {
<p className="text-muted-foreground text-sm">
{t.chatPage.exitDialogDescription}
</p>
<label className="flex cursor-pointer items-center gap-2 text-sm text-ws-fg-primary">
<input
type="checkbox"
className="h-4 w-4 rounded border-ws-divider accent-ws-interactive-primary"
checked={clearMemoryOnExit}
onChange={(e) => setClearMemoryOnExit(e.target.checked)}
disabled={isConfirmingExit}
/>
<span>{t.chatPage.exitDialogClearMemory}</span>
</label>
<DevDialogFooter>
<Button
className="w-full bg-ws-surface-subtle hover:bg-ws-interactive-primary hover:text-primary-foreground"
variant="ghost"
onClick={() => setShowExitDialog(false)}
disabled={isConfirmingExit}
>
{t.common.cancel}
</Button>
@ -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}
</Button>

View File

@ -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.",

View File

@ -227,6 +227,7 @@ export interface Translations {
noArtifactSelectedDescription: string;
exitDialogTitle: string;
exitDialogDescription: string;
exitDialogClearMemory: string;
exitDialogConfirm: string;
selectedSkillLoadFailed: string;
unknownErrorRetry: string;

View File

@ -323,6 +323,7 @@ export const zhCN: Translations = {
exitDialogTitle: "提示",
exitDialogDescription:
"每七天自动删除。现在将返回欢迎页且清空聊天消息,是否继续?",
exitDialogClearMemory: "同时清除当前会话的记忆",
exitDialogConfirm: "确定",
selectedSkillLoadFailed: "技能加载失败",
unknownErrorRetry: "发生了未知错误,请稍后重试。",

View File

@ -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);
});

View File

@ -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}`);
}
}