diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index dcd226e8..b8e1f3c6 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -27,7 +27,6 @@ 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 { ThreadMemoryTestPanel } from "@/components/workspace/thread-memory-test-panel"; import { ThreadTitle } from "@/components/workspace/thread-title"; import { Tooltip } from "@/components/workspace/tooltip"; import { useSpecificChatMode } from "@/components/workspace/use-chat-mode"; @@ -706,7 +705,6 @@ export default function ChatPage() { {/* MARK: 开发测试:iframe 通信功能测试面板 */} {/* {process.env.NODE_ENV !== "production" && } */} - {/* */} ); diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 2c93afe7..32f2347c 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -4,6 +4,7 @@ import type { ChatStatus } from "ai"; import { Tour } from "antd"; import { CheckIcon, + BrainIcon, GraduationCapIcon, LightbulbIcon, Loader2Icon, @@ -97,6 +98,7 @@ import { Suggestion, Suggestions } from "../ai-elements/suggestion"; import { ScrollArea } from "../ui/scroll-area"; import { ModeHoverGuide } from "./mode-hover-guide"; +import { ThreadMemoryPanel } from "./thread-memory-panel"; import { Tooltip } from "./tooltip"; @@ -280,6 +282,7 @@ export function InputBox({ null, ); const [isFocused, setIsFocused] = useState(false); + const [memoryPanelOpen, setMemoryPanelOpen] = useState(false); const [references, setReferences] = useState([]); const [mentionQuery, setMentionQuery] = useState(""); const [mentionOpen, setMentionOpen] = useState(false); @@ -293,7 +296,8 @@ export function InputBox({ const { data: referenceFilesData } = useReferenceFiles(threadIdFromProps); // Welcome 态下禁用收缩,始终保持展开 - const effectiveIsFocused = (showWelcomeStyle ?? false) || isFocused; + const effectiveIsFocused = + (showWelcomeStyle ?? false) || isFocused || memoryPanelOpen; const shouldShowSuggestionList = showWelcomeStyle && searchParams.get("mode") !== "skill"; @@ -965,6 +969,7 @@ export function InputBox({ /> )} + {/* {!showWelcomeStyle && ( + {/* 记忆按钮 */} + + + + + + + + + + + + (null); + const [loadingSummary, setLoadingSummary] = useState(false); + const [savingSummary, setSavingSummary] = useState(false); + const [deletingMemory, setDeletingMemory] = useState(false); + const { t } = useI18n(); + if (!threadId || threadId === "new") return null; + + const handleLoadMemorySummary = async () => { + setLoadingSummary(true); + try { + const res = await fetch( + `${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory-summary`, + ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { summary: string; memoryVersion: number }; + setMemorySummary(data.summary ?? ""); + setMemoryVersion(data.memoryVersion ?? 0); + toast.success(t.threadMemoryPanel.toastLoadSuccess); + } catch { + toast.error(t.threadMemoryPanel.toastLoadFailed); + } finally { + setLoadingSummary(false); + } + }; + + const handleSaveMemorySummary = async () => { + if (memoryVersion == null) return; + setSavingSummary(true); + try { + const res = await fetch( + `${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory-summary`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ summary: memorySummary, memoryVersion }), + }, + ); + if (res.status === 409) { + toast.error(t.threadMemoryPanel.toastConflict); + return; + } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { memoryVersion?: number }; + if (typeof data.memoryVersion === "number") setMemoryVersion(data.memoryVersion); + toast.success(t.threadMemoryPanel.toastSaveSuccess); + } catch { + toast.error(t.threadMemoryPanel.toastSaveFailed); + } finally { + setSavingSummary(false); + } + }; + + const handleDeleteMemory = async () => { + setDeletingMemory(true); + try { + const res = await fetch( + `${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory`, + { method: "DELETE" }, + ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setMemorySummary(""); + setMemoryVersion(0); + toast.success(t.threadMemoryPanel.toastDeleteSuccess); + } catch { + toast.error(t.threadMemoryPanel.toastDeleteFailed); + } finally { + setDeletingMemory(false); + } + }; + + return ( + + + {t.threadMemoryPanel.title} + + + + { + void handleLoadMemorySummary(); + }} + disabled={loadingSummary} + > + {loadingSummary ? t.threadMemoryPanel.loading : t.threadMemoryPanel.load} + + { + void handleSaveMemorySummary(); + }} + disabled={savingSummary || memoryVersion == null} + > + {savingSummary ? t.threadMemoryPanel.saving : t.threadMemoryPanel.save} + + { + void handleDeleteMemory(); + }} + disabled={deletingMemory} + > + {deletingMemory ? t.threadMemoryPanel.removing : t.threadMemoryPanel.remove} + + + + {t.threadMemoryPanel.threadId}: {threadId.slice(0, 8)}... |{" "} + {t.threadMemoryPanel.version}:{" "} + {memoryVersion == null ? t.threadMemoryPanel.unavailableVersion : memoryVersion} + + setMemorySummary(e.target.value)} + placeholder={t.threadMemoryPanel.summaryPlaceholder} + className="min-h-32 bg-white/80" + /> + + + ); +} diff --git a/frontend/src/components/workspace/thread-memory-test-panel.tsx b/frontend/src/components/workspace/thread-memory-test-panel.tsx deleted file mode 100644 index ea3fd11e..00000000 --- a/frontend/src/components/workspace/thread-memory-test-panel.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { toast } from "sonner"; - -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { getBackendBaseURL } from "@/core/config"; - -type ThreadMemoryTestPanelProps = { - threadId?: string; -}; - -export function ThreadMemoryTestPanel({ threadId }: ThreadMemoryTestPanelProps) { - const [memorySummary, setMemorySummary] = useState(""); - const [memoryVersion, setMemoryVersion] = useState(null); - const [loadingSummary, setLoadingSummary] = useState(false); - const [savingSummary, setSavingSummary] = useState(false); - const [deletingMemory, setDeletingMemory] = useState(false); - const [open, setOpen] = useState(true); - - if (!threadId || threadId === "new") return null; - - const handleLoadMemorySummary = async () => { - setLoadingSummary(true); - try { - const res = await fetch( - `${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory-summary`, - ); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = (await res.json()) as { summary: string; memoryVersion: number }; - setMemorySummary(data.summary ?? ""); - setMemoryVersion(data.memoryVersion ?? 0); - toast.success("已加载会话记忆"); - } catch { - toast.error("加载会话记忆失败"); - } finally { - setLoadingSummary(false); - } - }; - - const handleSaveMemorySummary = async () => { - if (memoryVersion == null) return; - setSavingSummary(true); - try { - const res = await fetch( - `${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory-summary`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ summary: memorySummary, memoryVersion }), - }, - ); - if (res.status === 409) { - toast.error("记忆已更新,请先重新加载再保存"); - return; - } - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = (await res.json()) as { memoryVersion?: number }; - if (typeof data.memoryVersion === "number") setMemoryVersion(data.memoryVersion); - toast.success("会话记忆已保存"); - } catch { - toast.error("保存会话记忆失败"); - } finally { - setSavingSummary(false); - } - }; - - const handleDeleteMemory = async () => { - setDeletingMemory(true); - try { - const res = await fetch( - `${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory`, - { method: "DELETE" }, - ); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - setMemorySummary(""); - setMemoryVersion(0); - toast.success("当前会话记忆已删除"); - } catch { - toast.error("删除会话记忆失败"); - } finally { - setDeletingMemory(false); - } - }; - - return ( - - - Thread Memory TestPanel - setOpen((v) => !v)}> - {open ? "收起" : "展开"} - - - {open && ( - - - { - void handleLoadMemorySummary(); - }} - disabled={loadingSummary} - > - {loadingSummary ? "加载中..." : "查看记忆"} - - { - void handleSaveMemorySummary(); - }} - disabled={savingSummary || memoryVersion == null} - > - {savingSummary ? "保存中..." : "保存记忆"} - - { - void handleDeleteMemory(); - }} - disabled={deletingMemory} - > - {deletingMemory ? "删除中..." : "删除记忆"} - - - - threadId: {threadId.slice(0, 8)}... | version:{" "} - {memoryVersion == null ? "-" : memoryVersion} - - setMemorySummary(e.target.value)} - placeholder="这里显示会话记忆总结,可编辑后保存" - className="min-h-32 bg-white/80" - /> - - )} - - ); -} diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index 19762e79..96b04171 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -264,6 +264,27 @@ export const enUS: Translations = { scrollToBottom: "Scroll to bottom", }, + threadMemoryPanel: { + title: "Thread Memory", + load: "Load memory", + loading: "Loading...", + save: "Save memory", + saving: "Saving...", + remove: "Delete memory", + removing: "Deleting...", + threadId: "Thread ID", + version: "Version", + unavailableVersion: "-", + summaryPlaceholder: "Thread memory summary is shown here. Edit it and save.", + toastLoadSuccess: "Thread memory loaded", + toastLoadFailed: "Failed to load thread memory", + toastConflict: "Memory changed. Please reload before saving.", + toastSaveSuccess: "Thread memory saved", + toastSaveFailed: "Failed to save thread memory", + toastDeleteSuccess: "Thread memory deleted", + toastDeleteFailed: "Failed to delete thread memory", + }, + // Workspace Chat Page chatPage: { defaultSlogan: "Let's study and work together", diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index 9fc67cc0..2f877b03 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -194,6 +194,27 @@ export interface Translations { scrollToBottom: string; }; + threadMemoryPanel: { + title: string; + load: string; + loading: string; + save: string; + saving: string; + remove: string; + removing: string; + threadId: string; + version: string; + unavailableVersion: string; + summaryPlaceholder: string; + toastLoadSuccess: string; + toastLoadFailed: string; + toastConflict: string; + toastSaveSuccess: string; + toastSaveFailed: string; + toastDeleteSuccess: string; + toastDeleteFailed: string; + }; + // Workspace Chat Page chatPage: { defaultSlogan: string; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index c69b34f0..977621b5 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -252,6 +252,27 @@ export const zhCN: Translations = { scrollToBottom: "滚动到底部", }, + threadMemoryPanel: { + title: "会话记忆", + load: "查看记忆", + loading: "加载中...", + save: "保存记忆", + saving: "保存中...", + remove: "删除记忆", + removing: "删除中...", + threadId: "threadId", + version: "版本", + unavailableVersion: "-", + summaryPlaceholder: "这里显示会话记忆总结,可编辑后保存", + toastLoadSuccess: "已加载会话记忆", + toastLoadFailed: "加载会话记忆失败", + toastConflict: "记忆已更新,请先重新加载再保存", + toastSaveSuccess: "会话记忆已保存", + toastSaveFailed: "保存会话记忆失败", + toastDeleteSuccess: "当前会话记忆已删除", + toastDeleteFailed: "删除会话记忆失败", + }, + // Workspace Chat Page chatPage: { defaultSlogan: "来,一起学习工作吧",