feat: 重置会话时新增checkbox,清除当前会话的memory
This commit is contained in:
parent
dd98337a92
commit
63563ce6a3
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Ticker } from "@tombcato/smart-ticker";
|
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 { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -23,20 +23,20 @@ import {
|
|||||||
} from "@/components/workspace/artifacts";
|
} from "@/components/workspace/artifacts";
|
||||||
import { useThreadChat } from "@/components/workspace/chats";
|
import { useThreadChat } from "@/components/workspace/chats";
|
||||||
// import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
// import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
||||||
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
|
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
|
||||||
import { Tooltip } from "@/components/workspace/tooltip";
|
import { Tooltip } from "@/components/workspace/tooltip";
|
||||||
import { useSpecificChatMode } from "@/components/workspace/use-chat-mode";
|
import { useSpecificChatMode } from "@/components/workspace/use-chat-mode";
|
||||||
import { Welcome } from "@/components/workspace/welcome";
|
import { Welcome } from "@/components/workspace/welcome";
|
||||||
import { getAPIClient } from "@/core/api";
|
import { getAPIClient } from "@/core/api";
|
||||||
import { sanitizeArtifactPaths } from "@/core/artifacts/utils";
|
import { sanitizeArtifactPaths } from "@/core/artifacts/utils";
|
||||||
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
|
import { clearThreadMemoryOnExit } from "@/core/threads/exit-thread-memory";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
@ -60,8 +60,6 @@ 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, showWelcomeStyle } =
|
const { threadId, isNewThread, setIsNewThread, isMock, showWelcomeStyle } =
|
||||||
@ -303,6 +301,8 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
const todoListCollapsed = true;
|
const todoListCollapsed = true;
|
||||||
const [showExitDialog, setShowExitDialog] = useState(false);
|
const [showExitDialog, setShowExitDialog] = useState(false);
|
||||||
|
const [clearMemoryOnExit, setClearMemoryOnExit] = useState(false);
|
||||||
|
const [isConfirmingExit, setIsConfirmingExit] = useState(false);
|
||||||
const isStreaming = isUploading || thread.isLoading;
|
const isStreaming = isUploading || thread.isLoading;
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (message: Parameters<typeof sendMessage>[1]) => {
|
async (message: Parameters<typeof sendMessage>[1]) => {
|
||||||
@ -627,7 +627,16 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 退出确认对话框 */}
|
{/* 退出确认对话框 */}
|
||||||
<DevDialog open={showExitDialog} onOpenChange={setShowExitDialog}>
|
<DevDialog
|
||||||
|
open={showExitDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setShowExitDialog(open);
|
||||||
|
if (!open) {
|
||||||
|
setClearMemoryOnExit(false);
|
||||||
|
setIsConfirmingExit(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DevDialogContent>
|
<DevDialogContent>
|
||||||
<DevDialogHeader>
|
<DevDialogHeader>
|
||||||
<DevDialogTitle>{t.chatPage.exitDialogTitle}</DevDialogTitle>
|
<DevDialogTitle>{t.chatPage.exitDialogTitle}</DevDialogTitle>
|
||||||
@ -635,11 +644,22 @@ export default function ChatPage() {
|
|||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
{t.chatPage.exitDialogDescription}
|
{t.chatPage.exitDialogDescription}
|
||||||
</p>
|
</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>
|
<DevDialogFooter>
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-ws-surface-subtle hover:bg-ws-interactive-primary hover:text-primary-foreground"
|
className="w-full bg-ws-surface-subtle hover:bg-ws-interactive-primary hover:text-primary-foreground"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setShowExitDialog(false)}
|
onClick={() => setShowExitDialog(false)}
|
||||||
|
disabled={isConfirmingExit}
|
||||||
>
|
>
|
||||||
{t.common.cancel}
|
{t.common.cancel}
|
||||||
</Button>
|
</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"
|
className="w-full bg-ws-surface-subtle hover:bg-ws-interactive-primary hover:text-primary-foreground"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// 如果正在生成,先终止再退出
|
setIsConfirmingExit(true);
|
||||||
|
try {
|
||||||
if (thread.isLoading) {
|
if (thread.isLoading) {
|
||||||
await handleStop();
|
await handleStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await clearThreadMemoryOnExit({
|
||||||
|
backendBaseURL: getBackendBaseURL(),
|
||||||
|
threadId: safeThreadId,
|
||||||
|
shouldClearMemory: clearMemoryOnExit,
|
||||||
|
});
|
||||||
|
|
||||||
setShowExitDialog(false);
|
setShowExitDialog(false);
|
||||||
sendToParent({
|
sendToParent({
|
||||||
type: POST_MESSAGE_TYPES.IS_CHATTING,
|
type: POST_MESSAGE_TYPES.IS_CHATTING,
|
||||||
isChatting: false,
|
isChatting: false,
|
||||||
});
|
});
|
||||||
// 始终复用 query 中的 thread_id。
|
router.replace(`/workspace/chats/new?thread_id=${threadId}`);
|
||||||
const nextQuery = new URLSearchParams();
|
} catch {
|
||||||
if (threadId && threadId !== "new") {
|
toast.error(t.threadMemoryPanel.toastDeleteFailed);
|
||||||
nextQuery.set("thread_id", threadId);
|
} finally {
|
||||||
|
setIsConfirmingExit(false);
|
||||||
}
|
}
|
||||||
// /workspace/chats/${threadId}?is_chatting=false
|
|
||||||
router.replace(
|
|
||||||
`/workspace/chats/new?thread_id=${threadId}`,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
|
disabled={isConfirmingExit}
|
||||||
>
|
>
|
||||||
{t.chatPage.exitDialogConfirm}
|
{t.chatPage.exitDialogConfirm}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -298,6 +298,7 @@ export const enUS: Translations = {
|
|||||||
exitDialogTitle: "Notice",
|
exitDialogTitle: "Notice",
|
||||||
exitDialogDescription:
|
exitDialogDescription:
|
||||||
"Chat history is automatically deleted every seven days. You will return to the welcome page now. Continue?",
|
"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",
|
exitDialogConfirm: "Confirm",
|
||||||
selectedSkillLoadFailed: "Failed to load skill",
|
selectedSkillLoadFailed: "Failed to load skill",
|
||||||
unknownErrorRetry: "An unknown error occurred. Please try again later.",
|
unknownErrorRetry: "An unknown error occurred. Please try again later.",
|
||||||
|
|||||||
@ -227,6 +227,7 @@ export interface Translations {
|
|||||||
noArtifactSelectedDescription: string;
|
noArtifactSelectedDescription: string;
|
||||||
exitDialogTitle: string;
|
exitDialogTitle: string;
|
||||||
exitDialogDescription: string;
|
exitDialogDescription: string;
|
||||||
|
exitDialogClearMemory: string;
|
||||||
exitDialogConfirm: string;
|
exitDialogConfirm: string;
|
||||||
selectedSkillLoadFailed: string;
|
selectedSkillLoadFailed: string;
|
||||||
unknownErrorRetry: string;
|
unknownErrorRetry: string;
|
||||||
|
|||||||
@ -323,6 +323,7 @@ export const zhCN: Translations = {
|
|||||||
exitDialogTitle: "提示",
|
exitDialogTitle: "提示",
|
||||||
exitDialogDescription:
|
exitDialogDescription:
|
||||||
"每七天自动删除。现在将返回欢迎页且清空聊天消息,是否继续?",
|
"每七天自动删除。现在将返回欢迎页且清空聊天消息,是否继续?",
|
||||||
|
exitDialogClearMemory: "同时清除当前会话的记忆",
|
||||||
exitDialogConfirm: "确定",
|
exitDialogConfirm: "确定",
|
||||||
selectedSkillLoadFailed: "技能加载失败",
|
selectedSkillLoadFailed: "技能加载失败",
|
||||||
unknownErrorRetry: "发生了未知错误,请稍后重试。",
|
unknownErrorRetry: "发生了未知错误,请稍后重试。",
|
||||||
|
|||||||
45
frontend/src/core/threads/exit-thread-memory.test.ts
Normal file
45
frontend/src/core/threads/exit-thread-memory.test.ts
Normal 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);
|
||||||
|
});
|
||||||
24
frontend/src/core/threads/exit-thread-memory.ts
Normal file
24
frontend/src/core/threads/exit-thread-memory.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user