From 09e6871118fdbb6a5e3d71ad18e7f04ade4b41f4 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sun, 13 Apr 2025 16:51:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Popups/ShortMemoryPopup.tsx | 195 ++++ src/renderer/src/i18n/locales/en-us.json | 79 +- src/renderer/src/i18n/locales/ja-jp.json | 103 +- src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 73 +- src/renderer/src/i18n/locales/zh-tw.json | 103 +- src/renderer/src/pages/home/Navbar.tsx | 51 +- .../settings/MemorySettings/CenterNode.tsx | 18 +- .../CollapsibleShortMemoryManager.tsx | 299 ++++++ .../MemoryDeduplicationPanel.tsx | 440 ++++++++ .../settings/MemorySettings/MemoryNode.tsx | 48 +- .../pages/settings/MemorySettings/index.tsx | 947 ++++++++++++++---- .../src/pages/settings/SettingsPage.tsx | 42 + src/renderer/src/pages/settings/index.tsx | 39 +- .../src/providers/AiProvider/BaseProvider.ts | 10 +- .../src/providers/AiProvider/index.ts | 10 +- .../services/MemoryDeduplicationService.ts | 279 ++++++ src/renderer/src/services/MemoryService.ts | 315 +++++- src/renderer/src/store/memory.ts | 48 +- 19 files changed, 2678 insertions(+), 422 deletions(-) create mode 100644 src/renderer/src/components/Popups/ShortMemoryPopup.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/CollapsibleShortMemoryManager.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx create mode 100644 src/renderer/src/services/MemoryDeduplicationService.ts diff --git a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx new file mode 100644 index 0000000000..9764517c06 --- /dev/null +++ b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx @@ -0,0 +1,195 @@ +import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons' +import { Box } from '@renderer/components/Layout' +import { TopView } from '@renderer/components/TopView' +import { addShortMemoryItem, analyzeAndAddShortMemories } from '@renderer/services/MemoryService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { deleteShortMemory } from '@renderer/store/memory' +import { Button, Empty, Input, List, Modal, Tooltip } from 'antd' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +const { confirm } = Modal + +const ButtonGroup = styled.div` + display: flex; + gap: 8px; + margin-top: 8px; +` + +const MemoryContent = styled.div` + word-break: break-word; +` + +interface ShowParams { + topicId: string +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ topicId, resolve }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const [open, setOpen] = useState(true) + + // 获取短记忆状态 + const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false) + const shortMemories = useAppSelector((state) => { + const allShortMemories = state.memory?.shortMemories || [] + // 只显示当前话题的短记忆 + return topicId ? allShortMemories.filter((memory) => memory.topicId === topicId) : [] + }) + + // 添加短记忆的状态 + const [newMemoryContent, setNewMemoryContent] = useState('') + const [isAnalyzing, setIsAnalyzing] = useState(false) + + // 添加新的短记忆 + const handleAddMemory = () => { + if (newMemoryContent.trim() && topicId) { + addShortMemoryItem(newMemoryContent.trim(), topicId) + setNewMemoryContent('') // 清空输入框 + } + } + + // 手动分析对话内容 + const handleAnalyzeConversation = async () => { + if (!topicId || !shortMemoryActive) return + + setIsAnalyzing(true) + try { + const result = await analyzeAndAddShortMemories(topicId) + if (result) { + // 如果有新的短期记忆被添加 + Modal.success({ + title: t('settings.memory.shortMemoryAnalysisSuccess') || '分析成功', + content: t('settings.memory.shortMemoryAnalysisSuccessContent') || '已成功提取并添加重要信息到短期记忆' + }) + } else { + // 如果没有新的短期记忆被添加 + Modal.info({ + title: t('settings.memory.shortMemoryAnalysisNoNew') || '无新信息', + content: t('settings.memory.shortMemoryAnalysisNoNewContent') || '未发现新的重要信息或所有信息已存在' + }) + } + } catch (error) { + console.error('Failed to analyze conversation:', error) + Modal.error({ + title: t('settings.memory.shortMemoryAnalysisError') || '分析失败', + content: t('settings.memory.shortMemoryAnalysisErrorContent') || '分析对话内容时出错' + }) + } finally { + setIsAnalyzing(false) + } + } + + // 删除短记忆 + const handleDeleteMemory = (id: string) => { + confirm({ + title: t('settings.memory.confirmDelete'), + icon: , + content: t('settings.memory.confirmDeleteContent'), + onOk() { + dispatch(deleteShortMemory(id)) + } + }) + } + + const onClose = () => { + setOpen(false) + } + + const afterClose = () => { + resolve({}) + } + + ShortMemoryPopup.hide = onClose + + return ( + + + setNewMemoryContent(e.target.value)} + placeholder={t('settings.memory.addShortMemoryPlaceholder')} + autoSize={{ minRows: 2, maxRows: 4 }} + disabled={!shortMemoryActive || !topicId} + /> + + + + + + + + {shortMemories.length > 0 ? ( + ( + + + + + ) + } + + // 切换折叠状态 + const toggleExpand = () => { + setIsExpanded(!isExpanded) + } + + return ( + + + + {title || t(`${translationPrefix}.title`)} + {isExpanded ? : } + + + + {isExpanded && ( + + {description || t(`${translationPrefix}.description`)} + + + {!isShortMemory ? ( +
+ {t(`${translationPrefix}.selectList`)} + +
+ ) : ( +
+ {t(`${translationPrefix}.selectTopic`) || '选择话题'} + +
+ )} + +
+ + {t(`${translationPrefix}.similarityThreshold`)}: {threshold} + + +
+
+ + + + + + + + {isLoading ? ( + + + {t(`${translationPrefix}.analyzing`)} + + ) : ( + {renderResult()} + )} +
+ )} +
+ ) +} + +const StyledCard = styled(Card)` + margin-bottom: 24px; + border-radius: 8px; + overflow: hidden; +` + +const CollapsibleHeader = styled.div` + cursor: pointer; + padding: 12px 16px; + background-color: var(--color-background-secondary, #f5f5f5); + border-bottom: 1px solid var(--color-border, #e8e8e8); + transition: background-color 0.3s; + + &:hover { + background-color: var(--color-background-hover, #e6f7ff); + } +` + +const HeaderContent = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` + +const CollapsibleContent = styled.div` + padding: 16px; +` + +const ControlsContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 24px; + margin-bottom: 16px; +` + +const ButtonContainer = styled.div` + display: flex; + gap: 8px; + margin-bottom: 24px; +` + +const LoadingContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 0; + gap: 16px; +` + +const ResultContainer = styled.div` + margin-top: 16px; +` + +// ApplyButtonContainer seems unused, removing it. +// const ApplyButtonContainer = styled.div` +// margin-top: 16px; +// text-align: center; +// ` + +const Select = styled.select` + display: block; + width: 100%; + margin-top: 8px; + padding: 8px; + border: 1px solid var(--color-border); + border-radius: 4px; + background-color: var(--color-background); + color: var(--color-text); +` + +export default MemoryDeduplicationPanel diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx index 82aa08d553..e285843948 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx @@ -1,19 +1,19 @@ -import { DeleteOutlined, EditOutlined, TagOutlined } from '@ant-design/icons'; -import { Memory } from '@renderer/store/memory'; -import { Button, Card, Tag, Tooltip, Typography } from 'antd'; -import { Handle, Position } from '@xyflow/react'; -import styled from 'styled-components'; +import { DeleteOutlined, EditOutlined, TagOutlined } from '@ant-design/icons' +import { Memory } from '@renderer/store/memory' +import { Handle, Position } from '@xyflow/react' +import { Button, Card, Tag, Tooltip, Typography } from 'antd' +import styled from 'styled-components' interface MemoryNodeProps { data: { - memory: Memory; - onEdit: (id: string) => void; - onDelete: (id: string) => void; - }; + memory: Memory + onEdit: (id: string) => void + onDelete: (id: string) => void + } } const MemoryNode: React.FC = ({ data }) => { - const { memory, onEdit, onDelete } = data; + const { memory, onEdit, onDelete } = data return ( @@ -35,43 +35,31 @@ const MemoryNode: React.FC = ({ data }) => { extra={
-
- } - > + }> {new Date(memory.createdAt).toLocaleString()} {memory.source && {memory.source}}
- ); -}; + ) +} const NodeContainer = styled.div` width: 220px; -`; +` const MemoryMeta = styled.div` display: flex; flex-direction: column; font-size: 12px; color: var(--color-text-secondary); -`; +` -export default MemoryNode; +export default MemoryNode diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx index 3df80dea12..61b0177d2b 100644 --- a/src/renderer/src/pages/settings/MemorySettings/index.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -8,7 +8,7 @@ import { } from '@ant-design/icons' import { useTheme } from '@renderer/context/ThemeProvider' import { TopicManager } from '@renderer/hooks/useTopic' -import { useMemoryService } from '@renderer/services/MemoryService' +import { analyzeAndAddShortMemories, useMemoryService } from '@renderer/services/MemoryService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { addMemory, @@ -16,12 +16,14 @@ import { deleteMemory, editMemory, setAnalyzeModel, + setAnalyzing, setAutoAnalyze, - setMemoryActive + setMemoryActive, + setShortMemoryAnalyzeModel } from '@renderer/store/memory' import { Topic } from '@renderer/types' -import { Button, Empty, Input, List, message, Modal, Radio, Select, Switch, Tag, Tooltip } from 'antd' -import { FC, useEffect, useMemo, useState } from 'react' +import { Button, Empty, Input, List, message, Modal, Radio, Select, Switch, Tabs, Tag, Tooltip } from 'antd' +import { FC, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -34,9 +36,10 @@ import { SettingRowTitle, SettingTitle } from '..' +import CollapsibleShortMemoryManager from './CollapsibleShortMemoryManager' +import MemoryDeduplicationPanel from './MemoryDeduplicationPanel' import MemoryListManager from './MemoryListManager' import MemoryMindMap from './MemoryMindMap' -import ShortMemoryManager from './ShortMemoryManager' const MemorySettings: FC = () => { const { t } = useTranslation() @@ -51,6 +54,8 @@ const MemorySettings: FC = () => { const isActive = useAppSelector((state) => state.memory?.isActive || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) + const shortMemoryAnalyzeModel = useAppSelector((state) => state.memory?.shortMemoryAnalyzeModel || null) + const isAnalyzing = useAppSelector((state) => state.memory?.isAnalyzing || false) // 从 Redux 获取所有模型,不仅仅是可用的模型 const providers = useAppSelector((state) => state.llm?.providers || []) @@ -196,233 +201,561 @@ const MemorySettings: FC = () => { dispatch(setAutoAnalyze(checked)) } - // 处理选择分析模型 + // 处理选择长期记忆分析模型 const handleSelectModel = (modelId: string) => { dispatch(setAnalyzeModel(modelId)) } + // 处理选择短期记忆分析模型 + const handleSelectShortMemoryModel = (modelId: string) => { + dispatch(setShortMemoryAnalyzeModel(modelId)) + } + // 手动触发分析 - const handleManualAnalyze = () => { - if (isActive && analyzeModel) { - message.info(t('settings.memory.startingAnalysis') || '开始分析...') - // 如果选择了话题,则分析选定的话题,否则分析当前话题 - analyzeAndAddMemories(selectedTopicId || undefined) - } else { + const handleManualAnalyze = async (isShortMemory: boolean = false) => { + if (!isActive) { message.warning(t('settings.memory.cannotAnalyze') || '无法分析,请检查设置') + return + } + + // 如果没有选择话题,提示用户 + if (!selectedTopicId) { + message.warning(t('settings.memory.selectTopicFirst') || '请先选择要分析的话题') + return + } + + message.info(t('settings.memory.startingAnalysis') || '开始分析...') + + if (isShortMemory) { + // 短期记忆分析 + if (!shortMemoryAnalyzeModel) { + message.warning(t('settings.memory.noShortMemoryModel') || '未设置短期记忆分析模型') + return + } + + try { + // 调用短期记忆分析函数 + const result = await analyzeAndAddShortMemories(selectedTopicId) + + if (result) { + message.success(t('settings.memory.shortMemoryAnalysisSuccess') || '短期记忆分析成功') + } else { + message.info(t('settings.memory.shortMemoryAnalysisNoNew') || '未发现新的短期记忆') + } + } catch (error) { + console.error('Failed to analyze short memories:', error) + message.error(t('settings.memory.shortMemoryAnalysisError') || '短期记忆分析失败') + } + } else { + // 长期记忆分析 + if (!analyzeModel) { + message.warning(t('settings.memory.noAnalyzeModel') || '未设置长期记忆分析模型') + return + } + + // 调用长期记忆分析函数 + analyzeAndAddMemories(selectedTopicId) } } + // 重置分析状态 + const handleResetAnalyzingState = () => { + dispatch(setAnalyzing(false)) + message.success(t('settings.memory.resetAnalyzingState') || '分析状态已重置') + } + + // 添加滚动检测 + const containerRef = useRef(null) + const listContainerRef = useRef(null) + + // 检测滚动状态并添加类 + useEffect(() => { + const container = containerRef.current + const listContainer = listContainerRef.current + if (!container || !listContainer) return + + const checkMainScroll = () => { + if (container.scrollHeight > container.clientHeight) { + container.classList.add('scrollable') + } else { + container.classList.remove('scrollable') + } + } + + const checkListScroll = () => { + if (listContainer.scrollHeight > listContainer.clientHeight) { + listContainer.classList.add('scrollable') + } else { + listContainer.classList.remove('scrollable') + } + } + + // 初始检查 + checkMainScroll() + checkListScroll() + + // 监听窗口大小变化 + window.addEventListener('resize', () => { + checkMainScroll() + checkListScroll() + }) + + // 监听内容变化(使用MutationObserver) + const mainObserver = new MutationObserver(checkMainScroll) + mainObserver.observe(container, { childList: true, subtree: true }) + + const listObserver = new MutationObserver(checkListScroll) + listObserver.observe(listContainer, { childList: true, subtree: true }) + + // 主容器始终保持可滚动状态 + container.style.overflowY = 'auto' + + // 添加滚动指示器 + const addScrollIndicator = () => { + const scrollIndicator = document.createElement('div') + scrollIndicator.className = 'scroll-indicator' + scrollIndicator.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + width: 40px; + height: 40px; + border-radius: 50%; + background-color: var(--color-primary); + opacity: 0.7; + pointer-events: none; + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: opacity 0.3s ease; + ` + + // 添加箭头图标 + scrollIndicator.innerHTML = `` + + document.body.appendChild(scrollIndicator) + + // 2秒后淡出 + setTimeout(() => { + scrollIndicator.style.opacity = '0' + setTimeout(() => { + document.body.removeChild(scrollIndicator) + }, 300) + }, 2000) + } + + // 首次加载时显示滚动指示器 + if (container.scrollHeight > container.clientHeight) { + addScrollIndicator() + } + + // 添加滚动事件监听器,当用户滚动时显示滚动指示器 + let scrollTimeout: NodeJS.Timeout | null = null + const handleContainerScroll = () => { + // 清除之前的定时器 + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + + // 如果容器可滚动,显示滚动指示器 + if (container.scrollHeight > container.clientHeight) { + // 如果已经滚动到底部,不显示指示器 + if (container.scrollHeight - container.scrollTop - container.clientHeight > 20) { + // 设置定时器,延迟显示滚动指示器 + scrollTimeout = setTimeout(() => { + addScrollIndicator() + }, 500) + } + } + } + + container.addEventListener('scroll', handleContainerScroll) + + return () => { + window.removeEventListener('resize', checkMainScroll) + mainObserver.disconnect() + listObserver.disconnect() + // 移除滚动事件监听器 + container.removeEventListener('scroll', handleContainerScroll) + // 清除定时器 + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + } + }, []) + return ( - - - {t('settings.memory.title')} - {t('settings.memory.description')} - + + {/* 1. 将 TabsContainer 移到 SettingContainer 顶部 */} + + + + {t('settings.memory.shortMemory') || '短期记忆'} + + ), + children: ( + // 将原来...中的内容放在这里 + + {t('settings.memory.title')} + {t('settings.memory.description')} + - {/* 记忆功能开关 */} - - {t('settings.memory.enableMemory')} - - + {t('settings.memory.shortMemorySettings')} + {t('settings.memory.shortMemoryDescription')} + - {/* 自动分析开关 */} - - {t('settings.memory.enableAutoAnalyze')} - - + {/* 保留原有的短期记忆设置 */} + + {t('settings.memory.enableMemory')} + + + + {t('settings.memory.enableAutoAnalyze')} + + - {/* 分析模型选择 */} - {autoAnalyze && isActive && ( - - {t('settings.memory.analyzeModel')} - + + )} - {/* 话题选择 */} - {isActive && ( - - {t('settings.memory.selectTopic') || '选择话题'} - setSelectedTopicId(value)} + placeholder={t('settings.memory.selectTopicPlaceholder') || '选择要分析的话题'} + allowClear + showSearch + filterOption={(input, option) => + (option?.label as string).toLowerCase().includes(input.toLowerCase()) + } + options={topics.map((topic) => ({ + label: topic.name || `话题 ${topic.id.substring(0, 8)}`, + value: topic.id + }))} + popupMatchSelectWidth={false} + /> + + )} - {/* 手动分析按钮 */} - {isActive && ( - - {t('settings.memory.manualAnalyze') || '手动分析'} - - - )} - - - - {/* 短记忆管理器 */} - - - - - {/* 记忆列表管理器 */} - { - // 当选择了一个记忆列表时,重置分类筛选器 - setCategoryFilter(null) - }} - /> - - - - {/* 记忆列表标题和操作按钮 */} - - {t('settings.memory.memoriesList')} - - setViewMode(e.target.value)} - buttonStyle="solid" - style={{ marginRight: 16 }}> - - {t('settings.memory.listView')} - - - {t('settings.memory.mindmapView')} - - - - - - - - {/* 分类筛选器 */} - {memories.length > 0 && ( - - {t('settings.memory.filterByCategory') || '按分类筛选:'} -
- setCategoryFilter(null)}> - {t('settings.memory.allCategories') || '全部'} - - {Array.from(new Set(memories.filter((m) => m.category).map((m) => m.category))).map((category) => ( - setCategoryFilter(category || null)}> - {category || t('settings.memory.uncategorized') || '未分类'} - - ))} -
-
- )} - - {/* 记忆列表 */} - - {viewMode === 'list' ? ( - memories.length > 0 ? ( - (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter)} - renderItem={(memory) => ( - + {/* 手动分析按钮 */} + {isActive && ( + + {t('settings.memory.manualAnalyze') || '手动分析'} + + {isAnalyzing && ( + + )} + + + )} + + + + {/* 短期记忆去重与合并面板 */} + + + + + {/* 短记忆管理器 */} + +
+ ) + }, + { + key: 'longMemory', + label: ( + + + {t('settings.memory.longMemory') || '长期记忆'} + + ), + children: ( + // 将原来...中的内容放在这里 + + {t('settings.memory.title')} + {t('settings.memory.description')} + + + {t('settings.memory.longMemorySettings')} + {t('settings.memory.longMemoryDescription')} + + + {/* 保留原有的长期记忆设置 */} + + {t('settings.memory.enableMemory')} + + + + {t('settings.memory.enableAutoAnalyze')} + + + + {/* 长期记忆分析模型选择 */} + {autoAnalyze && isActive && ( + + {t('settings.memory.analyzeModel') || '长期记忆分析模型'} + setSelectedTopicId(value)} + placeholder={t('settings.memory.selectTopicPlaceholder') || '选择要分析的话题'} + allowClear + showSearch + filterOption={(input, option) => + (option?.label as string).toLowerCase().includes(input.toLowerCase()) + } + options={topics.map((topic) => ({ + label: topic.name || `话题 ${topic.id.substring(0, 8)}`, + value: topic.id + }))} + popupMatchSelectWidth={false} + /> + + )} + + {/* 手动分析按钮 */} + {isActive && ( + + {t('settings.memory.manualAnalyze') || '手动分析'} + + {isAnalyzing && ( + + )} + + + )} + + + + {/* 记忆列表管理器 */} + { + // 当选择了一个记忆列表时,重置分类筛选器 + setCategoryFilter(null) + }} + // disabled={!isActive} // 移除此属性 + /> + + + + {/* 长期记忆去重与合并面板 */} + + + + + {/* 记忆列表标题和操作按钮 */} + + {t('settings.memory.memoriesList')} + + setViewMode(e.target.value)} + buttonStyle="solid" + disabled={!isActive}> + + {t('settings.memory.listView')} + + + {t('settings.memory.mindmapView')} + + + + + + + + {/* 分类筛选器 */} + {memories.length > 0 && isActive && ( + + {t('settings.memory.filterByCategory') || '按分类筛选:'} +
+ setCategoryFilter(null)}> + {t('settings.memory.allCategories') || '全部'} + + {Array.from(new Set(memories.filter((m) => m.category).map((m) => m.category))).map( + (category) => ( + setCategoryFilter(category || null)}> + {category || t('settings.memory.uncategorized') || '未分类'} + + ) + )} +
+
+ )} + + {/* 记忆列表 */} + + {viewMode === 'list' ? ( + memories.length > 0 && isActive ? ( + (currentListId ? memory.listId === currentListId : true)) + .filter((memory) => categoryFilter === null || memory.category === categoryFilter)} + renderItem={(memory) => ( + +