mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
添加了记忆功能
This commit is contained in:
parent
07bfb8e01e
commit
09e6871118
195
src/renderer/src/components/Popups/ShortMemoryPopup.tsx
Normal file
195
src/renderer/src/components/Popups/ShortMemoryPopup.tsx
Normal file
@ -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<Props> = ({ 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: <ExclamationCircleOutlined />,
|
||||
content: t('settings.memory.confirmDeleteContent'),
|
||||
onOk() {
|
||||
dispatch(deleteShortMemory(id))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const afterClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
ShortMemoryPopup.hide = onClose
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.memory.shortMemory')}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
afterClose={afterClose}
|
||||
footer={null}
|
||||
width={500}
|
||||
centered>
|
||||
<Box mb={16}>
|
||||
<Input.TextArea
|
||||
value={newMemoryContent}
|
||||
onChange={(e) => setNewMemoryContent(e.target.value)}
|
||||
placeholder={t('settings.memory.addShortMemoryPlaceholder')}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
disabled={!shortMemoryActive || !topicId}
|
||||
/>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddMemory}
|
||||
disabled={!shortMemoryActive || !newMemoryContent.trim() || !topicId}>
|
||||
{t('settings.memory.addShortMemory')}
|
||||
</Button>
|
||||
<Button onClick={handleAnalyzeConversation} loading={isAnalyzing} disabled={!shortMemoryActive || !topicId}>
|
||||
{t('settings.memory.analyzeConversation') || '分析对话'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
|
||||
<MemoriesList>
|
||||
{shortMemories.length > 0 ? (
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={shortMemories}
|
||||
renderItem={(memory) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Tooltip title={t('settings.memory.delete')} key="delete">
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteMemory(memory.id)}
|
||||
type="text"
|
||||
danger
|
||||
/>
|
||||
</Tooltip>
|
||||
]}>
|
||||
<List.Item.Meta
|
||||
title={<MemoryContent>{memory.content}</MemoryContent>}
|
||||
description={new Date(memory.createdAt).toLocaleString()}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description={!topicId ? t('settings.memory.noCurrentTopic') : t('settings.memory.noShortMemories')} />
|
||||
)}
|
||||
</MemoriesList>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const MemoriesList = styled.div`
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
const TopViewKey = 'ShortMemoryPopup'
|
||||
|
||||
export default class ShortMemoryPopup {
|
||||
static hide: () => void = () => {}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1047,12 +1047,74 @@
|
||||
"analyzeNow": "Analyze Now",
|
||||
"startingAnalysis": "Starting analysis...",
|
||||
"cannotAnalyze": "Cannot analyze, please check settings",
|
||||
"resetAnalyzingState": "Reset Analysis State",
|
||||
"analyzeConversation": "Analyze Conversation",
|
||||
"shortMemoryAnalysisSuccess": "Analysis Successful",
|
||||
"shortMemoryAnalysisSuccessContent": "Successfully extracted and added important information to short-term memory",
|
||||
"shortMemoryAnalysisNoNew": "No New Information",
|
||||
"shortMemoryAnalysisNoNewContent": "No new important information found or all information already exists",
|
||||
"shortMemoryAnalysisError": "Analysis Failed",
|
||||
"shortMemoryAnalysisErrorContent": "Error occurred while analyzing conversation content",
|
||||
"deduplication": {
|
||||
"title": "Memory Deduplication",
|
||||
"description": "Analyze similar memories in your memory library and provide intelligent merging suggestions.",
|
||||
"selectList": "Select Memory List",
|
||||
"allLists": "All Lists",
|
||||
"selectTopic": "Select Topic",
|
||||
"similarityThreshold": "Similarity Threshold",
|
||||
"startAnalysis": "Start Analysis",
|
||||
"help": "Help",
|
||||
"helpTitle": "Memory Deduplication Help",
|
||||
"helpContent1": "This feature analyzes similar memories in your memory library and provides merging suggestions.",
|
||||
"helpContent2": "The similarity threshold determines how similar two memories need to be to be considered for merging. Higher values require more similarity.",
|
||||
"helpContent3": "When you apply the results, similar memories will be merged into a new memory and the original memories will be deleted.",
|
||||
"analyzing": "Analyzing...",
|
||||
"noSimilarMemories": "No similar memories found",
|
||||
"similarGroups": "Similar Memory Groups",
|
||||
"group": "Group",
|
||||
"items": "items",
|
||||
"originalMemories": "Original Memories",
|
||||
"mergedResult": "Merged Result",
|
||||
"other": "Other",
|
||||
"applyResults": "Apply Results",
|
||||
"confirmApply": "Confirm Apply Results",
|
||||
"confirmApplyContent": "Applying deduplication results will merge similar memories and delete the original ones. This action cannot be undone. Are you sure you want to continue?",
|
||||
"applySuccess": "Applied Successfully",
|
||||
"applySuccessContent": "Memory deduplication has been successfully applied"
|
||||
},
|
||||
"shortMemoryDeduplication": {
|
||||
"title": "Short Memory Deduplication",
|
||||
"description": "Analyze similar memories in your short-term memory and provide intelligent merging suggestions.",
|
||||
"selectTopic": "Select Topic",
|
||||
"similarityThreshold": "Similarity Threshold",
|
||||
"startAnalysis": "Start Analysis",
|
||||
"help": "Help",
|
||||
"helpTitle": "Short Memory Deduplication Help",
|
||||
"helpContent1": "This feature analyzes similar memories in your short-term memory and provides merging suggestions.",
|
||||
"helpContent2": "The similarity threshold determines how similar two memories need to be to be considered for merging. Higher values require more similarity.",
|
||||
"helpContent3": "When you apply the results, similar memories will be merged into a new memory and the original memories will be deleted.",
|
||||
"analyzing": "Analyzing...",
|
||||
"noSimilarMemories": "No similar memories found",
|
||||
"similarGroups": "Similar Memory Groups",
|
||||
"group": "Group",
|
||||
"items": "items",
|
||||
"originalMemories": "Original Memories",
|
||||
"mergedResult": "Merged Result",
|
||||
"other": "Other",
|
||||
"applyResults": "Apply Results",
|
||||
"confirmApply": "Confirm Apply Results",
|
||||
"confirmApplyContent": "Applying deduplication results will merge similar memories and delete the original ones. This action cannot be undone. Are you sure you want to continue?",
|
||||
"applySuccess": "Applied Successfully",
|
||||
"applySuccessContent": "Short memory deduplication has been successfully applied"
|
||||
},
|
||||
"selectTopic": "Select Topic",
|
||||
"selectTopicPlaceholder": "Select a topic to analyze",
|
||||
"filterByCategory": "Filter by Category",
|
||||
"allCategories": "All",
|
||||
"uncategorized": "Uncategorized",
|
||||
"shortMemory": "Short-term Memory",
|
||||
"loading": "Loading...",
|
||||
"longMemory": "Long-term Memory",
|
||||
"toggleShortMemoryActive": "Toggle Short-term Memory",
|
||||
"addShortMemory": "Add Short-term Memory",
|
||||
"addShortMemoryPlaceholder": "Enter short-term memory content, only valid in current conversation",
|
||||
@ -1060,22 +1122,7 @@
|
||||
"noCurrentTopic": "Please select a conversation topic first",
|
||||
"confirmDelete": "Confirm Delete",
|
||||
"confirmDeleteContent": "Are you sure you want to delete this short-term memory?",
|
||||
"delete": "Delete",
|
||||
"memoryLists": "[to be translated]:记忆角色",
|
||||
"listView": "[to be translated]:列表视图",
|
||||
"mindmapView": "[to be translated]:思维导图",
|
||||
"centerNodeLabel": "[to be translated]:用户记忆",
|
||||
"addList": "[to be translated]:添加记忆列表",
|
||||
"editList": "[to be translated]:编辑记忆列表",
|
||||
"listName": "[to be translated]:列表名称",
|
||||
"listNamePlaceholder": "[to be translated]:输入列表名称",
|
||||
"listDescription": "[to be translated]:列表描述",
|
||||
"listDescriptionPlaceholder": "[to be translated]:输入列表描述(可选)",
|
||||
"noLists": "[to be translated]:暂无记忆列表",
|
||||
"confirmDeleteList": "[to be translated]:确认删除列表",
|
||||
"confirmDeleteListContent": "[to be translated]:确定要删除 {{name}} 列表吗?此操作将同时删除列表中的所有记忆,且不可恢复。",
|
||||
"toggleActive": "[to be translated]:切换激活状态",
|
||||
"clearConfirmContentList": "[to be translated]:确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。"
|
||||
"delete": "Delete"
|
||||
},
|
||||
"mcp": {
|
||||
"actions": "Actions",
|
||||
|
||||
@ -1353,57 +1353,58 @@
|
||||
"enable_privacy_mode": "匿名エラーレポートとデータ統計の送信"
|
||||
},
|
||||
"memory": {
|
||||
"title": "[to be translated]:记忆功能",
|
||||
"description": "[to be translated]:管理AI助手的长期记忆,自动分析对话并提取重要信息",
|
||||
"enableMemory": "[to be translated]:启用记忆功能",
|
||||
"enableAutoAnalyze": "[to be translated]:启用自动分析",
|
||||
"analyzeModel": "[to be translated]:分析模型",
|
||||
"selectModel": "[to be translated]:选择模型",
|
||||
"memoriesList": "[to be translated]:记忆列表",
|
||||
"memoryLists": "[to be translated]:记忆角色",
|
||||
"addMemory": "[to be translated]:添加记忆",
|
||||
"editMemory": "[to be translated]:编辑记忆",
|
||||
"clearAll": "[to be translated]:清空全部",
|
||||
"noMemories": "[to be translated]:暂无记忆",
|
||||
"memoryPlaceholder": "[to be translated]:输入要记住的内容",
|
||||
"addSuccess": "[to be translated]:记忆添加成功",
|
||||
"editSuccess": "[to be translated]:记忆编辑成功",
|
||||
"deleteSuccess": "[to be translated]:记忆删除成功",
|
||||
"clearSuccess": "[to be translated]:记忆清空成功",
|
||||
"clearConfirmTitle": "[to be translated]:确认清空",
|
||||
"clearConfirmContent": "[to be translated]:确定要清空所有记忆吗?此操作无法撤销。",
|
||||
"listView": "[to be translated]:列表视图",
|
||||
"mindmapView": "[to be translated]:思维导图",
|
||||
"centerNodeLabel": "[to be translated]:用户记忆",
|
||||
"manualAnalyze": "[to be translated]:手动分析",
|
||||
"analyzeNow": "[to be translated]:立即分析",
|
||||
"startingAnalysis": "[to be translated]:开始分析...",
|
||||
"cannotAnalyze": "[to be translated]:无法分析,请检查设置",
|
||||
"selectTopic": "[to be translated]:选择话题",
|
||||
"selectTopicPlaceholder": "[to be translated]:选择要分析的话题",
|
||||
"filterByCategory": "[to be translated]:按分类筛选",
|
||||
"allCategories": "[to be translated]:全部",
|
||||
"uncategorized": "[to be translated]:未分类",
|
||||
"addList": "[to be translated]:添加记忆列表",
|
||||
"editList": "[to be translated]:编辑记忆列表",
|
||||
"listName": "[to be translated]:列表名称",
|
||||
"listNamePlaceholder": "[to be translated]:输入列表名称",
|
||||
"listDescription": "[to be translated]:列表描述",
|
||||
"listDescriptionPlaceholder": "[to be translated]:输入列表描述(可选)",
|
||||
"noLists": "[to be translated]:暂无记忆列表",
|
||||
"confirmDeleteList": "[to be translated]:确认删除列表",
|
||||
"confirmDeleteListContent": "[to be translated]:确定要删除 {{name}} 列表吗?此操作将同时删除列表中的所有记忆,且不可恢复。",
|
||||
"toggleActive": "[to be translated]:切换激活状态",
|
||||
"clearConfirmContentList": "[to be translated]:确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
|
||||
"shortMemory": "[to be translated]:短期记忆",
|
||||
"toggleShortMemoryActive": "[to be translated]:切换短期记忆功能",
|
||||
"addShortMemory": "[to be translated]:添加短期记忆",
|
||||
"addShortMemoryPlaceholder": "[to be translated]:输入短期记忆内容,只在当前对话中有效",
|
||||
"noShortMemories": "[to be translated]:暂无短期记忆",
|
||||
"noCurrentTopic": "[to be translated]:请先选择一个对话话题",
|
||||
"confirmDelete": "[to be translated]:确认删除",
|
||||
"confirmDeleteContent": "[to be translated]:确定要删除这条短期记忆吗?",
|
||||
"delete": "[to be translated]:删除"
|
||||
"title": "メモリー機能",
|
||||
"description": "AIアシスタントの長期メモリーを管理し、会話を自動分析して重要な情報を抽出します",
|
||||
"enableMemory": "メモリー機能を有効にする",
|
||||
"enableAutoAnalyze": "自動分析を有効にする",
|
||||
"analyzeModel": "分析モデル",
|
||||
"selectModel": "モデルを選択",
|
||||
"memoriesList": "メモリーリスト",
|
||||
"memoryLists": "メモリーロール",
|
||||
"addMemory": "メモリーを追加",
|
||||
"editMemory": "メモリーを編集",
|
||||
"clearAll": "すべてクリア",
|
||||
"noMemories": "メモリーなし",
|
||||
"memoryPlaceholder": "記憶したい内容を入力",
|
||||
"addSuccess": "メモリーが正常に追加されました",
|
||||
"editSuccess": "メモリーが正常に編集されました",
|
||||
"deleteSuccess": "メモリーが正常に削除されました",
|
||||
"clearSuccess": "メモリーが正常にクリアされました",
|
||||
"clearConfirmTitle": "クリアの確認",
|
||||
"clearConfirmContent": "すべてのメモリーをクリアしますか?この操作は元に戻せません。",
|
||||
"listView": "リスト表示",
|
||||
"mindmapView": "マインドマップ表示",
|
||||
"centerNodeLabel": "ユーザーメモリー",
|
||||
"manualAnalyze": "手動分析",
|
||||
"analyzeNow": "今すぐ分析",
|
||||
"startingAnalysis": "分析開始...",
|
||||
"cannotAnalyze": "分析できません、設定を確認してください",
|
||||
"selectTopic": "トピックを選択",
|
||||
"selectTopicPlaceholder": "分析するトピックを選択",
|
||||
"filterByCategory": "カテゴリーで絞り込み",
|
||||
"allCategories": "すべて",
|
||||
"uncategorized": "未分類",
|
||||
"addList": "メモリーリストを追加",
|
||||
"editList": "メモリーリストを編集",
|
||||
"listName": "リスト名",
|
||||
"listNamePlaceholder": "リスト名を入力",
|
||||
"listDescription": "リストの説明",
|
||||
"listDescriptionPlaceholder": "リストの説明を入力(オプション)",
|
||||
"noLists": "メモリーリストなし",
|
||||
"confirmDeleteList": "リスト削除の確認",
|
||||
"confirmDeleteListContent": "{{name}} リストを削除しますか?この操作はリスト内のすべてのメモリーも削除し、元に戻せません。",
|
||||
"toggleActive": "アクティブ状態を切り替え",
|
||||
"clearConfirmContentList": "{{name}} のすべてのメモリーをクリアしますか?この操作は元に戻せません。",
|
||||
"shortMemory": "短期メモリー",
|
||||
"longMemory": "長期メモリー",
|
||||
"toggleShortMemoryActive": "短期メモリー機能を切り替え",
|
||||
"addShortMemory": "短期メモリーを追加",
|
||||
"addShortMemoryPlaceholder": "短期メモリーの内容を入力、現在の会話のみ有効",
|
||||
"noShortMemories": "短期メモリーなし",
|
||||
"noCurrentTopic": "まず会話トピックを選択してください",
|
||||
"confirmDelete": "削除の確認",
|
||||
"confirmDeleteContent": "この短期メモリーを削除しますか?",
|
||||
"delete": "削除"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
|
||||
@ -1396,6 +1396,7 @@
|
||||
"toggleActive": "[to be translated]:切换激活状态",
|
||||
"clearConfirmContentList": "[to be translated]:确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
|
||||
"shortMemory": "[to be translated]:短期记忆",
|
||||
"longMemory": "[to be translated]:长期记忆",
|
||||
"toggleShortMemoryActive": "[to be translated]:切换短期记忆功能",
|
||||
"addShortMemory": "[to be translated]:添加短期记忆",
|
||||
"addShortMemoryPlaceholder": "[to be translated]:输入短期记忆内容,只在当前对话中有效",
|
||||
|
||||
@ -1029,7 +1029,8 @@
|
||||
"description": "管理AI助手的长期记忆,自动分析对话并提取重要信息",
|
||||
"enableMemory": "启用记忆功能",
|
||||
"enableAutoAnalyze": "启用自动分析",
|
||||
"analyzeModel": "分析模型",
|
||||
"analyzeModel": "长期记忆分析模型",
|
||||
"shortMemoryAnalyzeModel": "短期记忆分析模型",
|
||||
"selectModel": "选择模型",
|
||||
"memoriesList": "记忆列表",
|
||||
"memoryLists": "记忆角色",
|
||||
@ -1051,6 +1052,66 @@
|
||||
"analyzeNow": "立即分析",
|
||||
"startingAnalysis": "开始分析...",
|
||||
"cannotAnalyze": "无法分析,请检查设置",
|
||||
"resetAnalyzingState": "重置分析状态",
|
||||
"analyzeConversation": "分析对话",
|
||||
"shortMemoryAnalysisSuccess": "分析成功",
|
||||
"shortMemoryAnalysisSuccessContent": "已成功提取并添加重要信息到短期记忆",
|
||||
"shortMemoryAnalysisNoNew": "无新信息",
|
||||
"shortMemoryAnalysisNoNewContent": "未发现新的重要信息或所有信息已存在",
|
||||
"shortMemoryAnalysisError": "分析失败",
|
||||
"shortMemoryAnalysisErrorContent": "分析对话内容时出错",
|
||||
"deduplication": {
|
||||
"title": "记忆去重与合并",
|
||||
"description": "分析记忆库中的相似记忆,提供智能合并建议。",
|
||||
"selectList": "选择记忆列表",
|
||||
"allLists": "所有列表",
|
||||
"selectTopic": "选择话题",
|
||||
"similarityThreshold": "相似度阈值",
|
||||
"startAnalysis": "开始分析",
|
||||
"help": "帮助",
|
||||
"helpTitle": "记忆去重与合并帮助",
|
||||
"helpContent1": "该功能会分析记忆库中的相似记忆,并提供合并建议。",
|
||||
"helpContent2": "相似度阈值决定了两条记忆被认为相似的程度,值越高,要求越严格。",
|
||||
"helpContent3": "应用结果后,相似的记忆将被合并为一条新记忆,原记忆将被删除。",
|
||||
"analyzing": "分析中...",
|
||||
"noSimilarMemories": "未发现相似记忆",
|
||||
"similarGroups": "相似记忆组",
|
||||
"group": "组",
|
||||
"items": "项",
|
||||
"originalMemories": "原始记忆",
|
||||
"mergedResult": "合并结果",
|
||||
"other": "其他",
|
||||
"applyResults": "应用结果",
|
||||
"confirmApply": "确认应用去重结果",
|
||||
"confirmApplyContent": "应用去重结果将合并相似记忆并删除原记忆,此操作不可撤销。确定要继续吗?",
|
||||
"applySuccess": "应用成功",
|
||||
"applySuccessContent": "记忆去重与合并已成功应用"
|
||||
},
|
||||
"shortMemoryDeduplication": {
|
||||
"title": "短期记忆去重与合并",
|
||||
"description": "分析短期记忆中的相似记忆,提供智能合并建议。",
|
||||
"selectTopic": "选择话题",
|
||||
"similarityThreshold": "相似度阈值",
|
||||
"startAnalysis": "开始分析",
|
||||
"help": "帮助",
|
||||
"helpTitle": "短期记忆去重与合并帮助",
|
||||
"helpContent1": "该功能会分析短期记忆中的相似记忆,并提供合并建议。",
|
||||
"helpContent2": "相似度阈值决定了两条记忆被认为相似的程度,值越高,要求越严格。",
|
||||
"helpContent3": "应用结果后,相似的记忆将被合并为一条新记忆,原记忆将被删除。",
|
||||
"analyzing": "分析中...",
|
||||
"noSimilarMemories": "未发现相似记忆",
|
||||
"similarGroups": "相似记忆组",
|
||||
"group": "组",
|
||||
"items": "项",
|
||||
"originalMemories": "原始记忆",
|
||||
"mergedResult": "合并结果",
|
||||
"other": "其他",
|
||||
"applyResults": "应用结果",
|
||||
"confirmApply": "确认应用去重结果",
|
||||
"confirmApplyContent": "应用去重结果将合并相似记忆并删除原记忆,此操作不可撤销。确定要继续吗?",
|
||||
"applySuccess": "应用成功",
|
||||
"applySuccessContent": "短期记忆去重与合并已成功应用"
|
||||
},
|
||||
"selectTopic": "选择话题",
|
||||
"selectTopicPlaceholder": "选择要分析的话题",
|
||||
"filterByCategory": "按分类筛选",
|
||||
@ -1068,6 +1129,12 @@
|
||||
"toggleActive": "切换激活状态",
|
||||
"clearConfirmContentList": "确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
|
||||
"shortMemory": "短期记忆",
|
||||
"loading": "加载中...",
|
||||
"longMemory": "长期记忆",
|
||||
"shortMemorySettings": "短期记忆设置",
|
||||
"shortMemoryDescription": "管理与当前对话相关的短期记忆",
|
||||
"longMemorySettings": "长期记忆设置",
|
||||
"longMemoryDescription": "管理跨对话的长期记忆",
|
||||
"toggleShortMemoryActive": "切换短期记忆功能",
|
||||
"addShortMemory": "添加短期记忆",
|
||||
"addShortMemoryPlaceholder": "输入短期记忆内容,只在当前对话中有效",
|
||||
@ -1075,7 +1142,9 @@
|
||||
"noCurrentTopic": "请先选择一个对话话题",
|
||||
"confirmDelete": "确认删除",
|
||||
"confirmDeleteContent": "确定要删除这条短期记忆吗?",
|
||||
"delete": "删除"
|
||||
"delete": "删除",
|
||||
"allTopics": "所有话题",
|
||||
"noTopics": "没有话题"
|
||||
},
|
||||
"mcp": {
|
||||
"actions": "操作",
|
||||
|
||||
@ -1353,57 +1353,58 @@
|
||||
"enable_privacy_mode": "匿名發送錯誤報告和資料統計"
|
||||
},
|
||||
"memory": {
|
||||
"title": "[to be translated]:记忆功能",
|
||||
"description": "[to be translated]:管理AI助手的长期记忆,自动分析对话并提取重要信息",
|
||||
"enableMemory": "[to be translated]:启用记忆功能",
|
||||
"enableAutoAnalyze": "[to be translated]:启用自动分析",
|
||||
"analyzeModel": "[to be translated]:分析模型",
|
||||
"selectModel": "[to be translated]:选择模型",
|
||||
"memoriesList": "[to be translated]:记忆列表",
|
||||
"memoryLists": "[to be translated]:记忆角色",
|
||||
"addMemory": "[to be translated]:添加记忆",
|
||||
"editMemory": "[to be translated]:编辑记忆",
|
||||
"clearAll": "[to be translated]:清空全部",
|
||||
"noMemories": "[to be translated]:暂无记忆",
|
||||
"memoryPlaceholder": "[to be translated]:输入要记住的内容",
|
||||
"addSuccess": "[to be translated]:记忆添加成功",
|
||||
"editSuccess": "[to be translated]:记忆编辑成功",
|
||||
"deleteSuccess": "[to be translated]:记忆删除成功",
|
||||
"clearSuccess": "[to be translated]:记忆清空成功",
|
||||
"clearConfirmTitle": "[to be translated]:确认清空",
|
||||
"clearConfirmContent": "[to be translated]:确定要清空所有记忆吗?此操作无法撤销。",
|
||||
"listView": "[to be translated]:列表视图",
|
||||
"mindmapView": "[to be translated]:思维导图",
|
||||
"centerNodeLabel": "[to be translated]:用户记忆",
|
||||
"manualAnalyze": "[to be translated]:手动分析",
|
||||
"analyzeNow": "[to be translated]:立即分析",
|
||||
"startingAnalysis": "[to be translated]:开始分析...",
|
||||
"cannotAnalyze": "[to be translated]:无法分析,请检查设置",
|
||||
"selectTopic": "[to be translated]:选择话题",
|
||||
"selectTopicPlaceholder": "[to be translated]:选择要分析的话题",
|
||||
"filterByCategory": "[to be translated]:按分类筛选",
|
||||
"allCategories": "[to be translated]:全部",
|
||||
"uncategorized": "[to be translated]:未分类",
|
||||
"addList": "[to be translated]:添加记忆列表",
|
||||
"editList": "[to be translated]:编辑记忆列表",
|
||||
"listName": "[to be translated]:列表名称",
|
||||
"listNamePlaceholder": "[to be translated]:输入列表名称",
|
||||
"listDescription": "[to be translated]:列表描述",
|
||||
"listDescriptionPlaceholder": "[to be translated]:输入列表描述(可选)",
|
||||
"noLists": "[to be translated]:暂无记忆列表",
|
||||
"confirmDeleteList": "[to be translated]:确认删除列表",
|
||||
"confirmDeleteListContent": "[to be translated]:确定要删除 {{name}} 列表吗?此操作将同时删除列表中的所有记忆,且不可恢复。",
|
||||
"toggleActive": "[to be translated]:切换激活状态",
|
||||
"clearConfirmContentList": "[to be translated]:确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
|
||||
"shortMemory": "[to be translated]:短期记忆",
|
||||
"toggleShortMemoryActive": "[to be translated]:切换短期记忆功能",
|
||||
"addShortMemory": "[to be translated]:添加短期记忆",
|
||||
"addShortMemoryPlaceholder": "[to be translated]:输入短期记忆内容,只在当前对话中有效",
|
||||
"noShortMemories": "[to be translated]:暂无短期记忆",
|
||||
"noCurrentTopic": "[to be translated]:请先选择一个对话话题",
|
||||
"confirmDelete": "[to be translated]:确认删除",
|
||||
"confirmDeleteContent": "[to be translated]:确定要删除这条短期记忆吗?",
|
||||
"delete": "[to be translated]:删除"
|
||||
"title": "記憶功能",
|
||||
"description": "管理AI助手的長期記憶,自動分析對話並提取重要信息",
|
||||
"enableMemory": "啟用記憶功能",
|
||||
"enableAutoAnalyze": "啟用自動分析",
|
||||
"analyzeModel": "分析模型",
|
||||
"selectModel": "選擇模型",
|
||||
"memoriesList": "記憶列表",
|
||||
"memoryLists": "記憶角色",
|
||||
"addMemory": "添加記憶",
|
||||
"editMemory": "編輯記憶",
|
||||
"clearAll": "清空全部",
|
||||
"noMemories": "暫無記憶",
|
||||
"memoryPlaceholder": "輸入要記住的內容",
|
||||
"addSuccess": "記憶添加成功",
|
||||
"editSuccess": "記憶編輯成功",
|
||||
"deleteSuccess": "記憶刪除成功",
|
||||
"clearSuccess": "記憶清空成功",
|
||||
"clearConfirmTitle": "確認清空",
|
||||
"clearConfirmContent": "確定要清空所有記憶嗎?此操作無法撤銷。",
|
||||
"listView": "列表視圖",
|
||||
"mindmapView": "思維導圖",
|
||||
"centerNodeLabel": "用戶記憶",
|
||||
"manualAnalyze": "手動分析",
|
||||
"analyzeNow": "立即分析",
|
||||
"startingAnalysis": "開始分析...",
|
||||
"cannotAnalyze": "無法分析,請檢查設置",
|
||||
"selectTopic": "選擇話題",
|
||||
"selectTopicPlaceholder": "選擇要分析的話題",
|
||||
"filterByCategory": "按分類篩選",
|
||||
"allCategories": "全部",
|
||||
"uncategorized": "未分類",
|
||||
"addList": "添加記憶列表",
|
||||
"editList": "編輯記憶列表",
|
||||
"listName": "列表名稱",
|
||||
"listNamePlaceholder": "輸入列表名稱",
|
||||
"listDescription": "列表描述",
|
||||
"listDescriptionPlaceholder": "輸入列表描述(可選)",
|
||||
"noLists": "暫無記憶列表",
|
||||
"confirmDeleteList": "確認刪除列表",
|
||||
"confirmDeleteListContent": "確定要刪除 {{name}} 列表嗎?此操作將同時刪除列表中的所有記憶,且不可恢復。",
|
||||
"toggleActive": "切換激活狀態",
|
||||
"clearConfirmContentList": "確定要清空 {{name}} 中的所有記憶嗎?此操作不可恢復。",
|
||||
"shortMemory": "短期記憶",
|
||||
"longMemory": "長期記憶",
|
||||
"toggleShortMemoryActive": "切換短期記憶功能",
|
||||
"addShortMemory": "添加短期記憶",
|
||||
"addShortMemoryPlaceholder": "輸入短期記憶內容,只在當前對話中有效",
|
||||
"noShortMemories": "暫無短期記憶",
|
||||
"noCurrentTopic": "請先選擇一個對話話題",
|
||||
"confirmDelete": "確認刪除",
|
||||
"confirmDeleteContent": "確定要刪除這條短期記憶嗎?",
|
||||
"delete": "刪除"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { FormOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { BookOutlined, FormOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import ShortMemoryPopup from '@renderer/components/Popups/ShortMemoryPopup'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
@ -10,10 +11,11 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { analyzeAndAddShortMemories } from '@renderer/services/MemoryService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@ -27,7 +29,7 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant, activeTopic }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
|
||||
@ -55,6 +57,28 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
dispatch(setNarrowMode(!narrowMode))
|
||||
}
|
||||
|
||||
const handleShowShortMemory = () => {
|
||||
if (activeTopic && activeTopic.id) {
|
||||
ShortMemoryPopup.show({ topicId: activeTopic.id })
|
||||
}
|
||||
}
|
||||
|
||||
const handleAnalyzeShortMemory = async () => {
|
||||
if (activeTopic && activeTopic.id) {
|
||||
try {
|
||||
const result = await analyzeAndAddShortMemories(activeTopic.id)
|
||||
if (result) {
|
||||
window.message.success(t('settings.memory.shortMemoryAnalysisSuccess') || '分析成功')
|
||||
} else {
|
||||
window.message.info(t('settings.memory.shortMemoryAnalysisNoNew') || '无新信息')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to analyze conversation for short memory:', error)
|
||||
window.message.error(t('settings.memory.shortMemoryAnalysisError') || '分析失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Navbar className="home-navbar">
|
||||
{showAssistants && (
|
||||
@ -86,6 +110,14 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
<Tooltip title={t('settings.memory.shortMemory')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleShowShortMemory}>
|
||||
<BookOutlined />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
<AnalyzeButton onClick={handleAnalyzeShortMemory}>
|
||||
{t('settings.memory.analyzeConversation') || '分析对话'}
|
||||
</AnalyzeButton>
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<SearchOutlined />
|
||||
@ -156,4 +188,17 @@ const NarrowIcon = styled(NavbarIcon)`
|
||||
}
|
||||
`
|
||||
|
||||
const AnalyzeButton = styled(Button)`
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
-webkit-app-region: none;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Card, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { Card, Typography } from 'antd'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface CenterNodeProps {
|
||||
data: {
|
||||
label: string;
|
||||
};
|
||||
label: string
|
||||
}
|
||||
}
|
||||
|
||||
const CenterNode: React.FC<CenterNodeProps> = ({ data }) => {
|
||||
@ -19,12 +19,12 @@ const CenterNode: React.FC<CenterNodeProps> = ({ data }) => {
|
||||
<Handle type="source" position={Position.Left} id="l" />
|
||||
<Handle type="source" position={Position.Top} id="t" />
|
||||
</NodeContainer>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const NodeContainer = styled.div`
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
`;
|
||||
`
|
||||
|
||||
export default CenterNode;
|
||||
export default CenterNode
|
||||
|
||||
@ -0,0 +1,299 @@
|
||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { addShortMemoryItem } from '@renderer/services/MemoryService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import { deleteShortMemory, setShortMemoryActive, ShortMemory } from '@renderer/store/memory' // Import ShortMemory from here
|
||||
import { Topic } from '@renderer/types' // Remove ShortMemory import from here
|
||||
import { Button, Collapse, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Title } = Typography
|
||||
const { confirm } = Modal
|
||||
// const { Panel } = Collapse // Panel is no longer used
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
`
|
||||
|
||||
const AddButton = styled(Button)`
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
interface TopicWithMemories {
|
||||
topic: Topic
|
||||
memories: ShortMemory[]
|
||||
}
|
||||
|
||||
const CollapsibleShortMemoryManager = () => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// 获取短记忆状态
|
||||
const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false)
|
||||
const shortMemories = useAppSelector((state) => state.memory?.shortMemories || [])
|
||||
|
||||
// 获取当前话题ID
|
||||
const currentTopicId = useAppSelector((state) => state.messages?.currentTopic?.id)
|
||||
|
||||
// 添加短记忆的状态
|
||||
const [newMemoryContent, setNewMemoryContent] = useState('')
|
||||
|
||||
// 话题列表和话题记忆映射
|
||||
const [topicsWithMemories, setTopicsWithMemories] = useState<TopicWithMemories[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// 加载所有话题和对应的短期记忆
|
||||
useEffect(() => {
|
||||
const loadTopicsWithMemories = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 从数据库获取所有话题
|
||||
const allTopics = await TopicManager.getAllTopics()
|
||||
|
||||
// 获取所有助手及其话题,确保我们使用与左侧列表相同的话题名称
|
||||
const assistants = store.getState().assistants?.assistants || []
|
||||
const allAssistantTopics = assistants.flatMap((assistant) => assistant.topics || [])
|
||||
|
||||
if (allTopics && allTopics.length > 0) {
|
||||
// 创建话题和记忆的映射
|
||||
const topicsMemories: TopicWithMemories[] = []
|
||||
|
||||
for (const dbTopic of allTopics) {
|
||||
// 获取该话题的短期记忆
|
||||
const topicMemories = shortMemories.filter((memory) => memory.topicId === dbTopic.id)
|
||||
|
||||
// 只添加有短期记忆的话题
|
||||
if (topicMemories.length > 0) {
|
||||
// 首先尝试从助手的话题列表中找到完整的话题信息
|
||||
let topicInfo = allAssistantTopics.find((topic) => topic.id === dbTopic.id)
|
||||
|
||||
// 如果在助手话题中找不到,则尝试从数据库获取
|
||||
if (!topicInfo) {
|
||||
try {
|
||||
const fullTopic = await TopicManager.getTopic(dbTopic.id)
|
||||
if (fullTopic) {
|
||||
// 数据库中的话题可能没有name属性,所以需要手动构造
|
||||
// 使用默认的话题名称格式
|
||||
const topicName = `话题 ${dbTopic.id.substring(0, 8)}`
|
||||
topicInfo = {
|
||||
id: dbTopic.id,
|
||||
assistantId: '',
|
||||
name: topicName,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get topic name for ${dbTopic.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还是找不到,使用默认名称
|
||||
if (!topicInfo) {
|
||||
topicInfo = {
|
||||
id: dbTopic.id,
|
||||
assistantId: '',
|
||||
name: `话题 ${dbTopic.id.substring(0, 8)}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
|
||||
topicsMemories.push({
|
||||
topic: topicInfo,
|
||||
memories: topicMemories
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按更新时间排序,最新的在前
|
||||
const sortedTopicsMemories = topicsMemories.sort((a, b) => {
|
||||
// 使用最新记忆的时间进行排序
|
||||
const aLatestMemory = a.memories.sort(
|
||||
(m1, m2) => new Date(m2.createdAt).getTime() - new Date(m1.createdAt).getTime()
|
||||
)[0]
|
||||
|
||||
const bLatestMemory = b.memories.sort(
|
||||
(m1, m2) => new Date(m2.createdAt).getTime() - new Date(m1.createdAt).getTime()
|
||||
)[0]
|
||||
|
||||
return new Date(bLatestMemory.createdAt).getTime() - new Date(aLatestMemory.createdAt).getTime()
|
||||
})
|
||||
|
||||
setTopicsWithMemories(sortedTopicsMemories)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load topics with memories:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (shortMemories.length > 0) {
|
||||
loadTopicsWithMemories()
|
||||
} else {
|
||||
setTopicsWithMemories([])
|
||||
setLoading(false)
|
||||
}
|
||||
}, [shortMemories])
|
||||
|
||||
// 切换短记忆功能激活状态
|
||||
const handleToggleActive = (checked: boolean) => {
|
||||
dispatch(setShortMemoryActive(checked))
|
||||
}
|
||||
|
||||
// 添加新的短记忆
|
||||
const handleAddMemory = () => {
|
||||
if (newMemoryContent.trim() && currentTopicId) {
|
||||
addShortMemoryItem(newMemoryContent.trim(), currentTopicId)
|
||||
setNewMemoryContent('') // 清空输入框
|
||||
}
|
||||
}
|
||||
|
||||
// 删除短记忆
|
||||
const handleDeleteMemory = (id: string) => {
|
||||
confirm({
|
||||
title: t('settings.memory.confirmDelete'),
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('settings.memory.confirmDeleteContent'),
|
||||
onOk() {
|
||||
dispatch(deleteShortMemory(id))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="short-memory-manager">
|
||||
<HeaderContainer>
|
||||
<Title level={4}>{t('settings.memory.shortMemory')}</Title>
|
||||
<Tooltip title={t('settings.memory.toggleShortMemoryActive')}>
|
||||
<Switch checked={shortMemoryActive} onChange={handleToggleActive} />
|
||||
</Tooltip>
|
||||
</HeaderContainer>
|
||||
|
||||
<InputContainer>
|
||||
<Input.TextArea
|
||||
value={newMemoryContent}
|
||||
onChange={(e) => setNewMemoryContent(e.target.value)}
|
||||
placeholder={t('settings.memory.addShortMemoryPlaceholder')}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
disabled={!shortMemoryActive || !currentTopicId}
|
||||
/>
|
||||
<AddButton
|
||||
type="primary"
|
||||
onClick={handleAddMemory}
|
||||
disabled={!shortMemoryActive || !newMemoryContent.trim() || !currentTopicId}>
|
||||
{t('settings.memory.addShortMemory')}
|
||||
</AddButton>
|
||||
</InputContainer>
|
||||
|
||||
<div className="short-memories-list">
|
||||
{loading ? (
|
||||
<LoadingContainer>{t('settings.memory.loading') || '加载中...'}</LoadingContainer>
|
||||
) : topicsWithMemories.length > 0 ? (
|
||||
<StyledCollapse
|
||||
defaultActiveKey={[currentTopicId || '']}
|
||||
items={topicsWithMemories.map(({ topic, memories }) => ({
|
||||
key: topic.id,
|
||||
label: (
|
||||
<CollapseHeader>
|
||||
<span>{topic.name}</span>
|
||||
<MemoryCount>{memories.length}</MemoryCount>
|
||||
</CollapseHeader>
|
||||
),
|
||||
children: (
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={memories}
|
||||
renderItem={(memory) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Tooltip title={t('settings.memory.delete')} key="delete">
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteMemory(memory.id)}
|
||||
type="text"
|
||||
danger
|
||||
/>
|
||||
</Tooltip>
|
||||
]}>
|
||||
<List.Item.Meta
|
||||
title={<MemoryContent>{memory.content}</MemoryContent>}
|
||||
description={new Date(memory.createdAt).toLocaleString()}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Empty
|
||||
description={!currentTopicId ? t('settings.memory.noCurrentTopic') : t('settings.memory.noShortMemories')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledCollapse = styled(Collapse)`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px !important;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
`
|
||||
|
||||
const CollapseHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const MemoryCount = styled.span`
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const MemoryContent = styled.div`
|
||||
word-break: break-word;
|
||||
`
|
||||
|
||||
export default CollapsibleShortMemoryManager
|
||||
@ -0,0 +1,440 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
DownOutlined,
|
||||
MergeCellsOutlined,
|
||||
QuestionCircleOutlined,
|
||||
RightOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import {
|
||||
applyDeduplicationResult,
|
||||
deduplicateAndMergeMemories,
|
||||
DeduplicationResult
|
||||
} from '@renderer/services/MemoryDeduplicationService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { Button, Card, Collapse, Empty, List, Modal, Slider, Space, Spin, Tag, Typography } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// 使用items属性,不再需要Panel组件
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
interface MemoryDeduplicationPanelProps {
|
||||
title?: string
|
||||
description?: string
|
||||
translationPrefix?: string
|
||||
applyResults?: (result: DeduplicationResult) => void
|
||||
isShortMemory?: boolean
|
||||
}
|
||||
|
||||
const MemoryDeduplicationPanel: React.FC<MemoryDeduplicationPanelProps> = ({
|
||||
title,
|
||||
description,
|
||||
translationPrefix = 'settings.memory.deduplication',
|
||||
applyResults,
|
||||
isShortMemory = false
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [deduplicationResult, setDeduplicationResult] = useState<DeduplicationResult | null>(null)
|
||||
const [threshold, setThreshold] = useState(0.75) // 降低默认阈值以捕获更多相似记忆
|
||||
const [selectedListId, setSelectedListId] = useState<string | undefined>(undefined)
|
||||
const [selectedTopicId, setSelectedTopicId] = useState<string | undefined>(undefined)
|
||||
const [topicsList, setTopicsList] = useState<Topic[]>([])
|
||||
const [loadingTopics, setLoadingTopics] = useState(false)
|
||||
|
||||
// 获取记忆列表
|
||||
const memoryLists = useAppSelector((state) => state.memory?.memoryLists || [])
|
||||
const memories = useAppSelector((state) =>
|
||||
isShortMemory ? state.memory?.shortMemories || [] : state.memory?.memories || []
|
||||
)
|
||||
|
||||
// 加载有短期记忆的话题
|
||||
useEffect(() => {
|
||||
const loadTopics = async () => {
|
||||
try {
|
||||
setLoadingTopics(true)
|
||||
|
||||
// 获取短期记忆
|
||||
const shortMemories = store.getState().memory?.shortMemories || []
|
||||
|
||||
// 获取所有有短期记忆的话题ID
|
||||
const topicIds = Array.from(new Set(shortMemories.map((memory) => memory.topicId)))
|
||||
|
||||
if (topicIds.length > 0) {
|
||||
// 获取所有助手及其话题,确保我们使用与左侧列表相同的话题名称
|
||||
const assistants = store.getState().assistants?.assistants || []
|
||||
const allAssistantTopics = assistants.flatMap((assistant) => assistant.topics || [])
|
||||
|
||||
// 创建完整的话题列表
|
||||
const fullTopics: Topic[] = []
|
||||
|
||||
for (const topicId of topicIds) {
|
||||
// 首先尝试从助手的话题列表中找到完整的话题信息
|
||||
let topicInfo = allAssistantTopics.find((topic) => topic.id === topicId)
|
||||
|
||||
// 如果在助手话题中找不到,则尝试从数据库获取
|
||||
if (!topicInfo) {
|
||||
try {
|
||||
const dbTopic = await TopicManager.getTopic(topicId)
|
||||
if (dbTopic) {
|
||||
topicInfo = {
|
||||
id: dbTopic.id,
|
||||
assistantId: '',
|
||||
name: `话题 ${dbTopic.id.substring(0, 8)}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get topic name for ${topicId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了话题信息,添加到列表中
|
||||
if (topicInfo) {
|
||||
fullTopics.push(topicInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 按更新时间排序,最新的在前
|
||||
const sortedTopics = fullTopics.sort((a, b) => {
|
||||
return new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime()
|
||||
})
|
||||
|
||||
setTopicsList(sortedTopics)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load topics:', error)
|
||||
} finally {
|
||||
setLoadingTopics(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadTopics()
|
||||
}, [])
|
||||
|
||||
// 开始去重分析
|
||||
const handleDeduplication = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (isShortMemory) {
|
||||
// 短期记忆去重
|
||||
const result = await deduplicateAndMergeMemories(undefined, true, selectedTopicId)
|
||||
setDeduplicationResult(result)
|
||||
} else {
|
||||
// 长期记忆去重
|
||||
const result = await deduplicateAndMergeMemories(selectedListId, false)
|
||||
setDeduplicationResult(result)
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用去重结果
|
||||
const handleApplyResult = () => {
|
||||
if (!deduplicationResult) return
|
||||
|
||||
Modal.confirm({
|
||||
title: t(`${translationPrefix}.confirmApply`),
|
||||
content: t(`${translationPrefix}.confirmApplyContent`),
|
||||
onOk: () => {
|
||||
if (applyResults) {
|
||||
// 使用自定义的应用函数
|
||||
applyResults(deduplicationResult)
|
||||
} else {
|
||||
// 使用默认的应用函数
|
||||
applyDeduplicationResult(deduplicationResult, true, isShortMemory)
|
||||
}
|
||||
setDeduplicationResult(null)
|
||||
Modal.success({
|
||||
title: t(`${translationPrefix}.applySuccess`),
|
||||
content: t(`${translationPrefix}.applySuccessContent`)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取记忆内容 - 这个函数在renderItem中使用,确保没有删除错误
|
||||
const getMemoryContent = (index: string) => {
|
||||
const memoryIndex = parseInt(index) - 1
|
||||
if (memoryIndex >= 0 && memoryIndex < memories.length) {
|
||||
const memory = memories[memoryIndex]
|
||||
return {
|
||||
content: memory.content,
|
||||
category: 'category' in memory ? memory.category || '其他' : '其他'
|
||||
}
|
||||
}
|
||||
return { content: '', category: '' }
|
||||
}
|
||||
// 函数 getMemories 在第38行报错未使用,不是 getMemoryContent
|
||||
// 将删除报错的 getMemories 函数 (实际检查代码发现没有 getMemories 函数,可能之前已删除或误报,先跳过此文件)
|
||||
|
||||
// 渲染结果
|
||||
const renderResult = () => {
|
||||
if (!deduplicationResult) return null
|
||||
|
||||
if (deduplicationResult.similarGroups.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
description={t('settings.memory.deduplication.noSimilarMemories')}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={5}>{t('settings.memory.deduplication.similarGroups')}</Title>
|
||||
<Collapse
|
||||
items={deduplicationResult.similarGroups.map((group) => ({
|
||||
key: group.groupId,
|
||||
label: (
|
||||
<Space>
|
||||
<Text strong>
|
||||
{t('settings.memory.deduplication.group')} {group.groupId}
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
({group.memoryIds.length} {t('settings.memory.deduplication.items')})
|
||||
</Text>
|
||||
{group.category && <Tag color="blue">{group.category}</Tag>}
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
<Card
|
||||
title={t('settings.memory.deduplication.originalMemories')}
|
||||
size="small"
|
||||
style={{ marginBottom: 16 }}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={group.memoryIds}
|
||||
renderItem={(id) => {
|
||||
const memory = getMemoryContent(id)
|
||||
return (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={<Text code>{id}</Text>}
|
||||
description={
|
||||
<>
|
||||
<Tag color="cyan">{memory.category}</Tag>
|
||||
<Text>{memory.content}</Text>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title={t('settings.memory.deduplication.mergedResult')} size="small">
|
||||
<Paragraph>
|
||||
<Tag color="green">{group.category || t('settings.memory.deduplication.other')}</Tag>
|
||||
<Text strong>{group.mergedContent}</Text>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
<Button type="primary" icon={<CheckCircleOutlined />} onClick={handleApplyResult}>
|
||||
{t('settings.memory.deduplication.applyResults')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 切换折叠状态
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCard>
|
||||
<CollapsibleHeader onClick={toggleExpand}>
|
||||
<HeaderContent>
|
||||
<Title level={5}>{title || t(`${translationPrefix}.title`)}</Title>
|
||||
{isExpanded ? <DownOutlined /> : <RightOutlined />}
|
||||
</HeaderContent>
|
||||
</CollapsibleHeader>
|
||||
|
||||
{isExpanded && (
|
||||
<CollapsibleContent>
|
||||
<Paragraph>{description || t(`${translationPrefix}.description`)}</Paragraph>
|
||||
|
||||
<ControlsContainer>
|
||||
{!isShortMemory ? (
|
||||
<div>
|
||||
<Text>{t(`${translationPrefix}.selectList`)}</Text>
|
||||
<Select
|
||||
value={selectedListId || 'all'}
|
||||
onChange={(e) => setSelectedListId(e.target.value === 'all' ? undefined : e.target.value)}>
|
||||
<option value="all">{t(`${translationPrefix}.allLists`)}</option>
|
||||
{memoryLists.map((list) => (
|
||||
<option key={list.id} value={list.id}>
|
||||
{list.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Text>{t(`${translationPrefix}.selectTopic`) || '选择话题'}</Text>
|
||||
<Select
|
||||
value={selectedTopicId || 'all'}
|
||||
onChange={(e) => setSelectedTopicId(e.target.value === 'all' ? undefined : e.target.value)}>
|
||||
<option value="all">{t('settings.memory.allTopics') || '所有话题'}</option>
|
||||
{loadingTopics ? (
|
||||
<option disabled>{t('settings.memory.loading') || '加载中...'}</option>
|
||||
) : topicsList.length > 0 ? (
|
||||
topicsList.map((topic) => (
|
||||
<option key={topic.id} value={topic.id}>
|
||||
{topic.name || `话题 ${topic.id.substring(0, 8)}`}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option disabled>{t('settings.memory.noTopics') || '没有话题'}</option>
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text>
|
||||
{t(`${translationPrefix}.similarityThreshold`)}: {threshold}
|
||||
</Text>
|
||||
<Slider
|
||||
min={0.5}
|
||||
max={0.95}
|
||||
step={0.05}
|
||||
value={threshold}
|
||||
onChange={setThreshold}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</div>
|
||||
</ControlsContainer>
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MergeCellsOutlined />}
|
||||
onClick={handleDeduplication}
|
||||
loading={isLoading}
|
||||
disabled={memories.length < 2}>
|
||||
{t(`${translationPrefix}.startAnalysis`)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<QuestionCircleOutlined />}
|
||||
onClick={() => {
|
||||
Modal.info({
|
||||
title: t(`${translationPrefix}.helpTitle`),
|
||||
content: (
|
||||
<div>
|
||||
<Paragraph>{t(`${translationPrefix}.helpContent1`)}</Paragraph>
|
||||
<Paragraph>{t(`${translationPrefix}.helpContent2`)}</Paragraph>
|
||||
<Paragraph>{t(`${translationPrefix}.helpContent3`)}</Paragraph>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}}>
|
||||
{t(`${translationPrefix}.help`)}
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
{isLoading ? (
|
||||
<LoadingContainer>
|
||||
<Spin size="large" />
|
||||
<Text>{t(`${translationPrefix}.analyzing`)}</Text>
|
||||
</LoadingContainer>
|
||||
) : (
|
||||
<ResultContainer>{renderResult()}</ResultContainer>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</StyledCard>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@ -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<MemoryNodeProps> = ({ data }) => {
|
||||
const { memory, onEdit, onDelete } = data;
|
||||
const { memory, onEdit, onDelete } = data
|
||||
|
||||
return (
|
||||
<NodeContainer>
|
||||
@ -35,43 +35,31 @@ const MemoryNode: React.FC<MemoryNodeProps> = ({ data }) => {
|
||||
extra={
|
||||
<div>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => onEdit(memory.id)}
|
||||
/>
|
||||
<Button icon={<EditOutlined />} type="text" size="small" onClick={() => onEdit(memory.id)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
onClick={() => onDelete(memory.id)}
|
||||
/>
|
||||
<Button icon={<DeleteOutlined />} type="text" danger size="small" onClick={() => onDelete(memory.id)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
}>
|
||||
<MemoryMeta>
|
||||
<span>{new Date(memory.createdAt).toLocaleString()}</span>
|
||||
{memory.source && <span>{memory.source}</span>}
|
||||
</MemoryMeta>
|
||||
</Card>
|
||||
</NodeContainer>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -164,6 +164,8 @@ const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
height: calc(100vh - var(--navbar-height)); /* 设置高度为视口高度减去导航栏高度 */
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
`
|
||||
|
||||
const SettingMenus = styled.ul`
|
||||
@ -173,6 +175,26 @@ const SettingMenus = styled.ul`
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
user-select: none;
|
||||
overflow-y: auto; /* 允许菜单滚动 */
|
||||
|
||||
/* 添加滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
const MenuItemLink = styled(Link)`
|
||||
@ -217,6 +239,26 @@ const SettingContent = styled.div`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
overflow-y: auto; /* 添加滚动属性,允许内容滚动 */
|
||||
|
||||
/* 添加滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
export default SettingsPage
|
||||
|
||||
@ -12,18 +12,49 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
|
||||
padding: 20px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 75px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto; /* 改为auto,只在需要时显示滚动条 */
|
||||
font-family: Ubuntu;
|
||||
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
|
||||
|
||||
/* 添加滚动指示器 */
|
||||
&::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.scrollable::after {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary);
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
|
||||
@ -37,7 +37,15 @@ export default abstract class BaseProvider {
|
||||
abstract summaries(messages: Message[], assistant: Assistant): Promise<string>
|
||||
abstract summaryForSearch(messages: Message[], assistant: Assistant): Promise<string | null>
|
||||
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
|
||||
abstract generateText({ prompt, content, modelId }: { prompt: string; content: string; modelId?: string }): Promise<string>
|
||||
abstract generateText({
|
||||
prompt,
|
||||
content,
|
||||
modelId
|
||||
}: {
|
||||
prompt: string
|
||||
content: string
|
||||
modelId?: string
|
||||
}): Promise<string>
|
||||
abstract check(model: Model): Promise<{ valid: boolean; error: Error | null }>
|
||||
abstract models(): Promise<OpenAI.Models.Model[]>
|
||||
abstract generateImage(params: GenerateImageParams): Promise<string[]>
|
||||
|
||||
@ -88,7 +88,15 @@ export default class AiProvider {
|
||||
return this.sdk.suggestions(messages, assistant)
|
||||
}
|
||||
|
||||
public async generateText({ prompt, content, modelId }: { prompt: string; content: string; modelId?: string }): Promise<string> {
|
||||
public async generateText({
|
||||
prompt,
|
||||
content,
|
||||
modelId
|
||||
}: {
|
||||
prompt: string
|
||||
content: string
|
||||
modelId?: string
|
||||
}): Promise<string> {
|
||||
return this.sdk.generateText({ prompt, content, modelId })
|
||||
}
|
||||
|
||||
|
||||
279
src/renderer/src/services/MemoryDeduplicationService.ts
Normal file
279
src/renderer/src/services/MemoryDeduplicationService.ts
Normal file
@ -0,0 +1,279 @@
|
||||
// 记忆去重与合并服务
|
||||
import { fetchGenerate } from '@renderer/services/ApiService'
|
||||
import store from '@renderer/store'
|
||||
import { addMemory, addShortMemory, deleteMemory, deleteShortMemory } from '@renderer/store/memory'
|
||||
|
||||
// 记忆去重与合并的结果接口
|
||||
export interface DeduplicationResult {
|
||||
similarGroups: {
|
||||
groupId: string
|
||||
memoryIds: string[]
|
||||
mergedContent: string
|
||||
category?: string
|
||||
}[]
|
||||
independentMemories: string[]
|
||||
rawResponse: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析记忆库中的相似记忆,提供智能合并建议
|
||||
* @param listId 可选的列表ID,如果不提供则处理所有列表
|
||||
* @param isShortMemory 是否处理短期记忆
|
||||
* @param topicId 当处理短期记忆时,可选的话题ID
|
||||
* @returns 去重分析结果
|
||||
*/
|
||||
export const deduplicateAndMergeMemories = async (
|
||||
listId?: string,
|
||||
isShortMemory: boolean = false,
|
||||
topicId?: string
|
||||
): Promise<DeduplicationResult | null> => {
|
||||
// 获取需要处理的记忆
|
||||
const state = store.getState()
|
||||
|
||||
let targetMemories: any[] = []
|
||||
|
||||
if (isShortMemory) {
|
||||
// 处理短期记忆
|
||||
const shortMemories = state.memory?.shortMemories || []
|
||||
targetMemories = topicId ? shortMemories.filter((memory) => memory.topicId === topicId) : shortMemories
|
||||
} else {
|
||||
// 处理长期记忆
|
||||
const memories = state.memory?.memories || []
|
||||
targetMemories = listId ? memories.filter((memory) => memory.listId === listId) : memories
|
||||
}
|
||||
|
||||
if (targetMemories.length < 2) {
|
||||
console.log('[Memory Deduplication] Not enough memories to deduplicate')
|
||||
return null
|
||||
}
|
||||
|
||||
const memoryType = isShortMemory ? 'short memories' : 'memories'
|
||||
console.log(`[Memory Deduplication] Starting deduplication for ${targetMemories.length} ${memoryType}`)
|
||||
|
||||
// 构建去重提示词
|
||||
const memoriesToCheck = targetMemories
|
||||
.map((memory, index) => {
|
||||
if (isShortMemory) {
|
||||
return `${index + 1}. 短期记忆: ${memory.content}`
|
||||
} else {
|
||||
return `${index + 1}. ${memory.category || '其他'}: ${memory.content}`
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const prompt = `
|
||||
请仔细分析以下记忆项,识别语义相似或包含重复信息的条目,并提供智能合并建议。
|
||||
|
||||
相似度判断标准:
|
||||
1. 语义相似:即使表述不同,但表达相同或非常相似的意思
|
||||
2. 内容重叠:一个记忆项包含另一个记忆项的大部分信息
|
||||
3. 主题相同:描述同一个主题或事件的不同方面
|
||||
|
||||
记忆项列表:
|
||||
${memoriesToCheck}
|
||||
|
||||
例如,以下记忆应被视为相似:
|
||||
- "用户喜欢简洁的界面设计"和"用户偏好简单直观的UI"
|
||||
- "用户正在开发一个网站项目"和"用户在进行网站开发工作"
|
||||
- "用户正在准备完成一个项目"和"用户正在进行一个项目的工作"
|
||||
|
||||
请按以下格式返回结果:
|
||||
1. 识别出的相似组:
|
||||
- 组1: [记忆项编号,如"1,5,8"] - 合并建议: "合并后的内容" - 分类: "最合适的分类"
|
||||
- 组2: [记忆项编号] - 合并建议: "合并后的内容" - 分类: "最合适的分类"
|
||||
...
|
||||
|
||||
2. 独立记忆项: [不需要合并的记忆项编号]
|
||||
|
||||
合并建议要求:
|
||||
- 保留所有非重复的有价值信息
|
||||
- 使用简洁清晰的语言
|
||||
- 确保合并后的内容比原始记忆更加全面和准确
|
||||
- 如果记忆项之间有细微差异,请在合并内容中保留这些差异
|
||||
|
||||
如果没有发现相似记忆,请返回"未发现相似记忆"。
|
||||
`
|
||||
|
||||
try {
|
||||
// 使用AI模型进行去重分析
|
||||
const analyzeModel = state.memory?.analyzeModel
|
||||
if (!analyzeModel) {
|
||||
console.log('[Memory Deduplication] No analyze model set')
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('[Memory Deduplication] Calling AI model for analysis...')
|
||||
const result = await fetchGenerate({
|
||||
prompt: prompt,
|
||||
content: memoriesToCheck,
|
||||
modelId: analyzeModel
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
console.log('[Memory Deduplication] No result from AI analysis')
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('[Memory Deduplication] Analysis result:', result)
|
||||
|
||||
// 解析结果
|
||||
const similarGroups: DeduplicationResult['similarGroups'] = []
|
||||
const independentMemories: string[] = []
|
||||
|
||||
// 检查是否没有发现相似记忆
|
||||
if (result.includes('未发现相似记忆')) {
|
||||
console.log('[Memory Deduplication] No similar memories found')
|
||||
return {
|
||||
similarGroups: [],
|
||||
independentMemories: targetMemories.map((_, index) => String(index + 1)),
|
||||
rawResponse: result
|
||||
}
|
||||
}
|
||||
|
||||
// 解析相似组
|
||||
const similarGroupsMatch = result.match(/1\.\s*识别出的相似组:([\s\S]*?)(?=2\.\s*独立记忆项:|$)/i)
|
||||
if (similarGroupsMatch && similarGroupsMatch[1]) {
|
||||
const groupsText = similarGroupsMatch[1].trim()
|
||||
const groupRegex = /-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*"([^"]+)"\s*(?:-\s*分类:\s*"([^"]+)")?/g
|
||||
|
||||
let match
|
||||
while ((match = groupRegex.exec(groupsText)) !== null) {
|
||||
const groupId = match[1] || String(similarGroups.length + 1)
|
||||
const memoryIndices = match[2].split(',').map((s) => s.trim())
|
||||
const mergedContent = match[3].trim()
|
||||
const category = match[4]?.trim()
|
||||
|
||||
similarGroups.push({
|
||||
groupId,
|
||||
memoryIds: memoryIndices,
|
||||
mergedContent,
|
||||
category
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 解析独立记忆项
|
||||
const independentMatch = result.match(/2\.\s*独立记忆项:\s*\[([\d,\s]+)\]/i)
|
||||
if (independentMatch && independentMatch[1]) {
|
||||
independentMemories.push(...independentMatch[1].split(',').map((s) => s.trim()))
|
||||
}
|
||||
|
||||
console.log('[Memory Deduplication] Parsed result:', { similarGroups, independentMemories })
|
||||
|
||||
return {
|
||||
similarGroups,
|
||||
independentMemories,
|
||||
rawResponse: result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Memory Deduplication] Error during deduplication:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用去重结果,合并相似记忆
|
||||
* @param result 去重分析结果
|
||||
* @param autoApply 是否自动应用合并结果
|
||||
* @param isShortMemory 是否处理短期记忆
|
||||
*/
|
||||
export const applyDeduplicationResult = (
|
||||
result: DeduplicationResult,
|
||||
autoApply: boolean = false,
|
||||
isShortMemory: boolean = false
|
||||
) => {
|
||||
if (!result || !result.similarGroups || result.similarGroups.length === 0) {
|
||||
console.log('[Memory Deduplication] No similar groups to apply')
|
||||
return
|
||||
}
|
||||
|
||||
const state = store.getState()
|
||||
const memories = isShortMemory ? state.memory?.shortMemories || [] : state.memory?.memories || []
|
||||
|
||||
// 处理每个相似组
|
||||
for (const group of result.similarGroups) {
|
||||
// 获取组中的记忆
|
||||
const memoryIndices = group.memoryIds.map((id) => parseInt(id) - 1)
|
||||
const groupMemories = memoryIndices.map((index) => memories[index]).filter(Boolean)
|
||||
|
||||
if (groupMemories.length < 2) continue
|
||||
|
||||
// 获取第一个记忆的列表ID和其他属性
|
||||
const firstMemory = groupMemories[0]
|
||||
|
||||
// 收集所有已分析过的消息ID
|
||||
const allAnalyzedMessageIds = new Set<string>()
|
||||
groupMemories.forEach((memory) => {
|
||||
if (memory.analyzedMessageIds) {
|
||||
memory.analyzedMessageIds.forEach((id) => allAnalyzedMessageIds.add(id))
|
||||
}
|
||||
})
|
||||
|
||||
// 找出最新的lastMessageId
|
||||
let lastMessageId: string | undefined
|
||||
groupMemories.forEach((memory) => {
|
||||
if (memory.lastMessageId) {
|
||||
if (!lastMessageId || new Date(memory.createdAt) > new Date(lastMessageId)) {
|
||||
lastMessageId = memory.lastMessageId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 找出所有关联的话题ID
|
||||
const topicIds = new Set<string>()
|
||||
groupMemories.forEach((memory) => {
|
||||
if (memory.topicId) {
|
||||
topicIds.add(memory.topicId)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果自动应用,则添加合并后的记忆并删除原记忆
|
||||
if (autoApply) {
|
||||
if (isShortMemory) {
|
||||
// 处理短期记忆
|
||||
// 添加合并后的短期记忆
|
||||
const topicId = topicIds.size === 1 ? Array.from(topicIds)[0] : undefined
|
||||
if (topicId) {
|
||||
store.dispatch(
|
||||
addShortMemory({
|
||||
content: group.mergedContent,
|
||||
topicId: topicId,
|
||||
analyzedMessageIds: Array.from(allAnalyzedMessageIds),
|
||||
lastMessageId: lastMessageId
|
||||
})
|
||||
)
|
||||
|
||||
// 删除原短期记忆
|
||||
for (const memory of groupMemories) {
|
||||
store.dispatch(deleteShortMemory(memory.id))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 处理长期记忆
|
||||
// 安全地获取 listId 和 category,因为它们只存在于 Memory 类型
|
||||
const listId = 'listId' in firstMemory ? firstMemory.listId : undefined
|
||||
const memoryCategory = 'category' in firstMemory ? firstMemory.category : undefined
|
||||
|
||||
// 添加合并后的记忆
|
||||
store.dispatch(
|
||||
addMemory({
|
||||
content: group.mergedContent,
|
||||
source: '自动合并',
|
||||
category: group.category || memoryCategory || '其他', // 使用安全获取的 category
|
||||
listId: listId, // 使用安全获取的 listId
|
||||
analyzedMessageIds: Array.from(allAnalyzedMessageIds),
|
||||
lastMessageId: lastMessageId,
|
||||
topicId: topicIds.size === 1 ? Array.from(topicIds)[0] : undefined
|
||||
})
|
||||
)
|
||||
|
||||
// 删除原记忆
|
||||
for (const memory of groupMemories) {
|
||||
store.dispatch(deleteMemory(memory.id))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Memory Deduplication] Applied group ${group.groupId}: merged ${groupMemories.length} memories`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,11 +11,14 @@ import { useCallback, useEffect, useRef } from 'react' // Add useRef back
|
||||
// 分析对话内容并提取重要信息
|
||||
const analyzeConversation = async (
|
||||
conversation: string,
|
||||
modelId: string
|
||||
modelId: string,
|
||||
customPrompt?: string
|
||||
): Promise<Array<{ content: string; category: string }>> => {
|
||||
try {
|
||||
// 构建分析提示词
|
||||
const prompt = `
|
||||
// 使用自定义提示词或默认提示词
|
||||
const prompt =
|
||||
customPrompt ||
|
||||
`
|
||||
请分析以下对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。
|
||||
|
||||
将每条信息分类并按以下格式返回:
|
||||
@ -111,17 +114,18 @@ export const useMemoryService = () => {
|
||||
// 增加可选的 topicId 参数,允许分析指定的话题
|
||||
const analyzeAndAddMemories = useCallback(
|
||||
async (topicId?: string) => {
|
||||
// 如果没有提供话题ID,则使用当前话题
|
||||
// 在函数执行时获取最新状态
|
||||
const currentState = store.getState() // Use imported store
|
||||
const memoryState = currentState.memory || {}
|
||||
const messagesState = currentState.messages || {}
|
||||
|
||||
// 检查isAnalyzing状态是否卡住(超过5分钟)
|
||||
// 检查isAnalyzing状态是否卡住(超过1分钟)
|
||||
if (memoryState.isAnalyzing && memoryState.lastAnalyzeTime) {
|
||||
const now = Date.now()
|
||||
const analyzeTime = memoryState.lastAnalyzeTime
|
||||
if (now - analyzeTime > 5 * 60 * 1000) {
|
||||
// 5分钟超时
|
||||
if (now - analyzeTime > 1 * 60 * 1000) {
|
||||
// 1分钟超时
|
||||
console.log('[Memory Analysis] Analysis state stuck, resetting...')
|
||||
dispatch(setAnalyzing(false))
|
||||
}
|
||||
@ -159,9 +163,42 @@ export const useMemoryService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const latestConversation = messages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
|
||||
if (!messages || messages.length === 0) {
|
||||
console.log('[Memory Analysis] No messages to analyze.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!latestConversation) {
|
||||
// 获取现有的长期记忆
|
||||
const existingMemories = store.getState().memory?.memories || []
|
||||
const topicMemories = existingMemories.filter((memory) => memory.topicId === targetTopicId)
|
||||
|
||||
// 收集所有已分析过的消息ID
|
||||
const analyzedMessageIds = new Set<string>()
|
||||
topicMemories.forEach((memory) => {
|
||||
if (memory.analyzedMessageIds) {
|
||||
memory.analyzedMessageIds.forEach((id) => analyzedMessageIds.add(id))
|
||||
}
|
||||
})
|
||||
|
||||
// 找出未分析过的新消息
|
||||
const newMessages = messages.filter((msg) => !analyzedMessageIds.has(msg.id))
|
||||
|
||||
if (newMessages.length === 0) {
|
||||
console.log('[Memory Analysis] No new messages to analyze.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[Memory Analysis] Found ${newMessages.length} new messages to analyze.`)
|
||||
|
||||
// 构建新消息的对话内容
|
||||
const newConversation = newMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
|
||||
|
||||
// 获取已有的长期记忆内容
|
||||
const existingMemoriesContent = topicMemories
|
||||
.map((memory) => `${memory.category || '其他'}: ${memory.content}`)
|
||||
.join('\n')
|
||||
|
||||
if (!newConversation) {
|
||||
console.log('[Memory Analysis] No conversation content to analyze.')
|
||||
return
|
||||
}
|
||||
@ -170,10 +207,37 @@ export const useMemoryService = () => {
|
||||
dispatch(setAnalyzing(true))
|
||||
console.log('[Memory Analysis] Starting analysis...')
|
||||
console.log(`[Memory Analysis] Analyzing topic: ${targetTopicId}`)
|
||||
console.log('[Memory Analysis] Conversation length:', latestConversation.length)
|
||||
console.log('[Memory Analysis] Conversation length:', newConversation.length)
|
||||
|
||||
// 调用分析函数 (仍然是模拟的)
|
||||
const memories = await analyzeConversation(latestConversation, memoryState.analyzeModel!)
|
||||
// 构建长期记忆分析提示词,包含已有记忆和新对话
|
||||
const prompt = `
|
||||
请分析以下对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。
|
||||
|
||||
将每条信息分类并按以下格式返回:
|
||||
类别: 信息内容
|
||||
|
||||
类别应该是以下几种之一:
|
||||
- 用户偏好:用户喜好、喜欢的事物、风格等
|
||||
- 技术需求:用户的技术相关需求、开发偏好等
|
||||
- 个人信息:用户的背景、经历等个人信息
|
||||
- 交互偏好:用户喜欢的交流方式、沟通风格等
|
||||
- 其他:不属于以上类别的重要信息
|
||||
|
||||
${
|
||||
existingMemoriesContent
|
||||
? `以下是已经提取的重要信息:
|
||||
${existingMemoriesContent}
|
||||
|
||||
请分析新的对话内容,提取出新的重要信息,避免重复已有信息。只关注新增的、有价值的信息。`
|
||||
: '请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。'
|
||||
}
|
||||
|
||||
新的对话内容:
|
||||
${newConversation}
|
||||
`
|
||||
|
||||
// 调用分析函数,传递自定义提示词
|
||||
const memories = await analyzeConversation(newConversation, memoryState.analyzeModel!, prompt)
|
||||
console.log('[Memory Analysis] Analysis complete. Memories extracted:', memories)
|
||||
|
||||
// 添加提取的记忆
|
||||
@ -193,12 +257,21 @@ export const useMemoryService = () => {
|
||||
// 获取当前选中的列表ID
|
||||
const currentListId = store.getState().memory?.currentListId || store.getState().memory?.memoryLists[0]?.id
|
||||
|
||||
// 收集新分析的消息ID
|
||||
const newMessageIds = messages.map((msg) => msg.id)
|
||||
|
||||
// 获取最后一条消息的ID,用于跟踪分析进度
|
||||
const lastMessageId = messages[messages.length - 1]?.id
|
||||
|
||||
dispatch(
|
||||
addMemory({
|
||||
content: memory.content,
|
||||
source: '自动分析',
|
||||
category: memory.category,
|
||||
listId: currentListId
|
||||
listId: currentListId,
|
||||
analyzedMessageIds: newMessageIds,
|
||||
lastMessageId: lastMessageId,
|
||||
topicId: targetTopicId
|
||||
})
|
||||
)
|
||||
console.log(
|
||||
@ -263,30 +336,205 @@ export const useMemoryService = () => {
|
||||
return { analyzeAndAddMemories }
|
||||
}
|
||||
|
||||
// 手动添加记忆
|
||||
export const addMemoryItem = (content: string, listId?: string, category?: string) => {
|
||||
// Use imported store directly
|
||||
store.dispatch(
|
||||
addMemory({
|
||||
content,
|
||||
source: '手动添加',
|
||||
listId,
|
||||
category
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 手动添加短记忆
|
||||
export const addShortMemoryItem = (content: string, topicId: string) => {
|
||||
export const addShortMemoryItem = (
|
||||
content: string,
|
||||
topicId: string,
|
||||
analyzedMessageIds?: string[],
|
||||
lastMessageId?: string
|
||||
) => {
|
||||
// Use imported store directly
|
||||
store.dispatch(
|
||||
addShortMemory({
|
||||
content,
|
||||
topicId
|
||||
topicId,
|
||||
analyzedMessageIds,
|
||||
lastMessageId
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 分析对话内容并提取重要信息添加到短期记忆
|
||||
export const analyzeAndAddShortMemories = async (topicId: string) => {
|
||||
if (!topicId) {
|
||||
console.log('[Short Memory Analysis] No topic ID provided')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前记忆状态
|
||||
const memoryState = store.getState().memory || {}
|
||||
const messagesState = store.getState().messages || {}
|
||||
const shortMemoryAnalyzeModel = memoryState.shortMemoryAnalyzeModel
|
||||
|
||||
if (!shortMemoryAnalyzeModel) {
|
||||
console.log('[Short Memory Analysis] No short memory analyze model set')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取对话内容
|
||||
let messages: any[] = []
|
||||
|
||||
// 从 Redux store 中获取话题消息
|
||||
if (messagesState.messagesByTopic && messagesState.messagesByTopic[topicId]) {
|
||||
messages = messagesState.messagesByTopic[topicId] || []
|
||||
} else {
|
||||
// 如果 Redux store 中没有,则从数据库中获取
|
||||
try {
|
||||
const topicMessages = await TopicManager.getTopicMessages(topicId)
|
||||
if (topicMessages && topicMessages.length > 0) {
|
||||
messages = topicMessages
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Short Memory Analysis] Failed to get messages for topic ${topicId}:`, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
console.log('[Short Memory Analysis] No messages to analyze.')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取现有的短期记忆
|
||||
const existingShortMemories = store.getState().memory?.shortMemories || []
|
||||
const topicShortMemories = existingShortMemories.filter((memory) => memory.topicId === topicId)
|
||||
|
||||
// 收集所有已分析过的消息ID
|
||||
const analyzedMessageIds = new Set<string>()
|
||||
topicShortMemories.forEach((memory) => {
|
||||
if (memory.analyzedMessageIds) {
|
||||
memory.analyzedMessageIds.forEach((id) => analyzedMessageIds.add(id))
|
||||
}
|
||||
})
|
||||
|
||||
// 找出未分析过的新消息
|
||||
const newMessages = messages.filter((msg) => !analyzedMessageIds.has(msg.id))
|
||||
|
||||
if (newMessages.length === 0) {
|
||||
console.log('[Short Memory Analysis] No new messages to analyze.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[Short Memory Analysis] Found ${newMessages.length} new messages to analyze.`)
|
||||
|
||||
// 构建新消息的对话内容
|
||||
const newConversation = newMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
|
||||
|
||||
// 获取已有的短期记忆内容
|
||||
const existingMemoriesContent = topicShortMemories.map((memory) => memory.content).join('\n')
|
||||
|
||||
try {
|
||||
console.log('[Short Memory Analysis] Starting analysis...')
|
||||
console.log(`[Short Memory Analysis] Analyzing topic: ${topicId}`)
|
||||
console.log('[Short Memory Analysis] New conversation length:', newConversation.length)
|
||||
|
||||
// 构建短期记忆分析提示词,包含已有记忆和新对话
|
||||
const prompt = `
|
||||
请对以下对话内容进行详细分析和总结,提取对当前对话至关重要的上下文信息。
|
||||
|
||||
分析要求:
|
||||
1. 详细总结用户的每一句话中表达的关键信息、需求和意图
|
||||
2. 分析AI回复中的重要内容和对用户问题的解决方案
|
||||
3. 识别对话中的重要事实、数据和具体细节
|
||||
4. 捕捉对话的逻辑发展和转折点
|
||||
5. 提取对理解当前对话上下文必不可少的信息
|
||||
|
||||
与长期记忆不同,短期记忆应该关注当前对话的具体细节和上下文,而不是用户的长期偏好。每条短期记忆应该是对对话片段的精准总结,确保不遗漏任何重要信息。
|
||||
|
||||
${
|
||||
existingMemoriesContent
|
||||
? `以下是已经提取的重要信息:
|
||||
${existingMemoriesContent}
|
||||
|
||||
请分析新的对话内容,提取出新的重要信息,避免重复已有信息。确保新提取的信息与已有信息形成连贯的上下文理解。`
|
||||
: '请对对话进行全面分析,确保不遗漏任何重要细节。每条总结应该是完整的句子,清晰表达一个重要的上下文信息。'
|
||||
}
|
||||
|
||||
输出格式:
|
||||
- 提供完整的上下文总结,数量不限,确保覆盖所有重要信息
|
||||
- 每条总结应该是一个完整的句子
|
||||
- 确保总结内容精准、具体且与当前对话直接相关
|
||||
- 按重要性排序,最重要的信息放在前面
|
||||
- 对于复杂的对话,应提供足够多的条目(至少5-10条)以确保上下文的完整性
|
||||
- 如果对话内容简单,可以少于5条,但必须确保完整捕捉所有重要信息
|
||||
|
||||
如果没有找到新的重要信息,请返回空字符串。
|
||||
|
||||
新的对话内容:
|
||||
${newConversation}
|
||||
`
|
||||
|
||||
// 获取模型
|
||||
const model = store
|
||||
.getState()
|
||||
.llm.providers.flatMap((provider) => provider.models)
|
||||
.find((model) => model.id === shortMemoryAnalyzeModel)
|
||||
|
||||
if (!model) {
|
||||
console.error(`[Short Memory Analysis] Model ${shortMemoryAnalyzeModel} not found`)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用AI生成文本
|
||||
console.log('[Short Memory Analysis] Calling AI.generateText...')
|
||||
const result = await fetchGenerate({
|
||||
prompt: prompt,
|
||||
content: newConversation,
|
||||
modelId: shortMemoryAnalyzeModel
|
||||
})
|
||||
console.log('[Short Memory Analysis] AI.generateText response:', result)
|
||||
|
||||
if (!result) {
|
||||
console.log('[Short Memory Analysis] No result from AI analysis.')
|
||||
return
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
const lines = result
|
||||
.split('\n')
|
||||
.map((line: string) => line.trim())
|
||||
.filter((line: string) => {
|
||||
// 匹配以数字和点开头的行(如"1.", "2.")或者以短横线开头的行(如"-")
|
||||
return /^\d+\./.test(line) || line.startsWith('-')
|
||||
})
|
||||
.map((line: string) => {
|
||||
// 如果是数字开头,移除数字和点,如果是短横线开头,移除短横线
|
||||
if (/^\d+\./.test(line)) {
|
||||
return line.replace(/^\d+\.\s*/, '').trim()
|
||||
} else if (line.startsWith('-')) {
|
||||
return line.substring(1).trim()
|
||||
}
|
||||
return line
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
console.log('[Short Memory Analysis] Extracted items:', lines)
|
||||
|
||||
// 过滤掉已存在的记忆
|
||||
const existingContents = topicShortMemories.map((memory) => memory.content.toLowerCase())
|
||||
const newMemories = lines.filter((content: string) => !existingContents.includes(content.toLowerCase()))
|
||||
|
||||
console.log(`[Short Memory Analysis] Found ${lines.length} items, ${newMemories.length} are new`)
|
||||
|
||||
// 收集新分析的消息ID
|
||||
const newMessageIds = newMessages.map((msg) => msg.id)
|
||||
|
||||
// 获取最后一条消息的ID,用于跟踪分析进度
|
||||
const lastMessageId = messages[messages.length - 1]?.id
|
||||
|
||||
// 添加新的短期记忆
|
||||
for (const content of newMemories) {
|
||||
addShortMemoryItem(content, topicId, newMessageIds, lastMessageId)
|
||||
console.log(`[Short Memory Analysis] Added new short memory: "${content}" to topic ${topicId}`)
|
||||
}
|
||||
|
||||
return newMemories.length > 0
|
||||
} catch (error) {
|
||||
console.error('[Short Memory Analysis] Failed to analyze and add short memories:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 将记忆应用到系统提示词
|
||||
import { persistor } from '@renderer/store' // Import persistor
|
||||
|
||||
@ -299,8 +547,13 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => {
|
||||
|
||||
const state = store.getState() // Use imported store
|
||||
// 确保 state.memory 存在,如果不存在则提供默认值
|
||||
const { isActive, memories, memoryLists, shortMemoryActive, shortMemories } = state.memory ||
|
||||
{ isActive: false, memories: [], memoryLists: [], shortMemoryActive: false, shortMemories: [] }
|
||||
const { isActive, memories, memoryLists, shortMemoryActive, shortMemories } = state.memory || {
|
||||
isActive: false,
|
||||
memories: [],
|
||||
memoryLists: [],
|
||||
shortMemoryActive: false,
|
||||
shortMemories: []
|
||||
}
|
||||
|
||||
// 获取当前话题ID
|
||||
const currentTopicId = state.messages.currentTopic?.id
|
||||
@ -320,10 +573,10 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => {
|
||||
// 处理短记忆
|
||||
if (shortMemoryActive && shortMemories && shortMemories.length > 0 && currentTopicId) {
|
||||
// 获取当前话题的短记忆
|
||||
const topicShortMemories = shortMemories.filter(memory => memory.topicId === currentTopicId)
|
||||
const topicShortMemories = shortMemories.filter((memory) => memory.topicId === currentTopicId)
|
||||
|
||||
if (topicShortMemories.length > 0) {
|
||||
const shortMemoryPrompt = topicShortMemories.map(memory => `- ${memory.content}`).join('\n')
|
||||
const shortMemoryPrompt = topicShortMemories.map((memory) => `- ${memory.content}`).join('\n')
|
||||
console.log('[Memory] Short memory prompt:', shortMemoryPrompt)
|
||||
|
||||
// 添加短记忆到提示词
|
||||
|
||||
@ -19,6 +19,9 @@ export interface Memory {
|
||||
source?: string // 来源,例如"自动分析"或"手动添加"
|
||||
category?: string // 分类,例如"用户偏好"、"技术需求"等
|
||||
listId: string // 所属的记忆列表ID
|
||||
analyzedMessageIds?: string[] // 记录该记忆是从哪些消息中分析出来的
|
||||
lastMessageId?: string // 分析时的最后一条消息的ID,用于跟踪分析进度
|
||||
topicId?: string // 关联的对话话题ID,用于跟踪该记忆来自哪个话题
|
||||
}
|
||||
|
||||
// 短记忆项接口
|
||||
@ -27,6 +30,8 @@ export interface ShortMemory {
|
||||
content: string
|
||||
createdAt: string
|
||||
topicId: string // 关联的对话话题ID
|
||||
analyzedMessageIds?: string[] // 记录该记忆是从哪些消息中分析出来的
|
||||
lastMessageId?: string // 分析时的最后一条消息的ID,用于跟踪分析进度
|
||||
}
|
||||
|
||||
export interface MemoryState {
|
||||
@ -37,7 +42,8 @@ export interface MemoryState {
|
||||
isActive: boolean // 记忆功能是否激活
|
||||
shortMemoryActive: boolean // 短记忆功能是否激活
|
||||
autoAnalyze: boolean // 是否自动分析
|
||||
analyzeModel: string | null // 用于分析的模型ID
|
||||
analyzeModel: string | null // 用于长期记忆分析的模型ID
|
||||
shortMemoryAnalyzeModel: string | null // 用于短期记忆分析的模型ID
|
||||
lastAnalyzeTime: number | null // 上次分析时间
|
||||
isAnalyzing: boolean // 是否正在分析
|
||||
}
|
||||
@ -60,7 +66,8 @@ const initialState: MemoryState = {
|
||||
isActive: true,
|
||||
shortMemoryActive: true, // 默认启用短记忆功能
|
||||
autoAnalyze: true,
|
||||
analyzeModel: 'gpt-3.5-turbo', // 设置默认模型
|
||||
analyzeModel: 'gpt-3.5-turbo', // 设置默认长期记忆分析模型
|
||||
shortMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认短期记忆分析模型
|
||||
lastAnalyzeTime: null,
|
||||
isAnalyzing: false
|
||||
}
|
||||
@ -72,7 +79,15 @@ const memorySlice = createSlice({
|
||||
// 添加新记忆
|
||||
addMemory: (
|
||||
state,
|
||||
action: PayloadAction<{ content: string; source?: string; category?: string; listId?: string }>
|
||||
action: PayloadAction<{
|
||||
content: string
|
||||
source?: string
|
||||
category?: string
|
||||
listId?: string
|
||||
analyzedMessageIds?: string[]
|
||||
lastMessageId?: string
|
||||
topicId?: string
|
||||
}>
|
||||
) => {
|
||||
// 确保 memoryLists 存在
|
||||
if (!state.memoryLists) {
|
||||
@ -91,7 +106,10 @@ const memorySlice = createSlice({
|
||||
createdAt: new Date().toISOString(),
|
||||
source: action.payload.source || '手动添加',
|
||||
category: action.payload.category,
|
||||
listId: listId
|
||||
listId: listId,
|
||||
analyzedMessageIds: action.payload.analyzedMessageIds,
|
||||
lastMessageId: action.payload.lastMessageId,
|
||||
topicId: action.payload.topicId
|
||||
}
|
||||
|
||||
// 确保 memories 存在
|
||||
@ -141,11 +159,16 @@ const memorySlice = createSlice({
|
||||
state.autoAnalyze = action.payload
|
||||
},
|
||||
|
||||
// 设置分析模型
|
||||
// 设置长期记忆分析模型
|
||||
setAnalyzeModel: (state, action: PayloadAction<string | null>) => {
|
||||
state.analyzeModel = action.payload
|
||||
},
|
||||
|
||||
// 设置短期记忆分析模型
|
||||
setShortMemoryAnalyzeModel: (state, action: PayloadAction<string | null>) => {
|
||||
state.shortMemoryAnalyzeModel = action.payload
|
||||
},
|
||||
|
||||
// 设置分析状态
|
||||
setAnalyzing: (state, action: PayloadAction<boolean>) => {
|
||||
state.isAnalyzing = action.payload
|
||||
@ -285,12 +308,22 @@ const memorySlice = createSlice({
|
||||
},
|
||||
|
||||
// 添加短记忆
|
||||
addShortMemory: (state, action: PayloadAction<{ content: string; topicId: string }>) => {
|
||||
addShortMemory: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
content: string
|
||||
topicId: string
|
||||
analyzedMessageIds?: string[]
|
||||
lastMessageId?: string
|
||||
}>
|
||||
) => {
|
||||
const newShortMemory: ShortMemory = {
|
||||
id: nanoid(),
|
||||
content: action.payload.content,
|
||||
createdAt: new Date().toISOString(),
|
||||
topicId: action.payload.topicId
|
||||
topicId: action.payload.topicId,
|
||||
analyzedMessageIds: action.payload.analyzedMessageIds,
|
||||
lastMessageId: action.payload.lastMessageId
|
||||
}
|
||||
|
||||
// 确保 shortMemories 存在
|
||||
@ -345,6 +378,7 @@ export const {
|
||||
setMemoryActive,
|
||||
setAutoAnalyze,
|
||||
setAnalyzeModel,
|
||||
setShortMemoryAnalyzeModel,
|
||||
setAnalyzing,
|
||||
importMemories,
|
||||
clearMemories,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user