diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 47177136..3c0fe6fa 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -204,8 +204,9 @@ export const getConversationMessages = (end_user: string, conversation_id: strin export const getConversationDetail = (end_user: string, conversation_id: string) => { return request.get(`/memory/work/${end_user}/detail`, { conversation_id }) } - - +export const forgetTrigger = (data: { max_merge_batch_size: number; min_days_since_access: number; end_user_id: string;}) => { + return request.post(`/memory/forget/trigger`, data) +} /*************** end 用户记忆 相关接口 ******************************/ /****************** 记忆管理 相关接口 *******************************/ diff --git a/web/src/assets/images/logout_hover.svg b/web/src/assets/images/logout_hover.svg new file mode 100644 index 00000000..d77ab292 --- /dev/null +++ b/web/src/assets/images/logout_hover.svg @@ -0,0 +1,17 @@ + + + 退出 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 1f4871d0..05d3d879 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1244,6 +1244,10 @@ export const en = { MemorySummary: 'Long-term Accumulation', Statement: 'Emotional Memory', ExtractedEntity: 'Episodic Memory', + positive: 'Positive Emotion', + negative: 'Negative Emotion', + neutral: 'Neutral Emotion', + interactionCountData: 'Interaction Count', }, space: { createSpace: 'Create Space', @@ -2203,6 +2207,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re node_type: 'Node Type', last_access_time: 'Last Activation Time', activation_value: 'Current Activation Value', + refreshSuccess: 'Forgetting Execution Successful', }, episodicDetail: { title: 'Record every important scene you have truly experienced', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 32188ede..b065a19a 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1324,6 +1324,10 @@ export const zh = { MemorySummary: '长期沉淀', Statement: '情绪记忆', ExtractedEntity: '情景记忆', + positive: '正向情绪', + negative: '负向情绪', + neutral: '中性情绪', + interactionCountData: '互动次数', }, space: { createSpace: '创建空间', @@ -2302,6 +2306,7 @@ export const zh = { node_type: '节点类型', last_access_time: '最后激活时间', activation_value: '当前激活值', + refreshSuccess: '遗忘执行成功', }, episodicDetail: { title: '记录你真实经历过的每一个重要场景', diff --git a/web/src/views/UserMemoryDetail/components/EmotionLine.tsx b/web/src/views/UserMemoryDetail/components/EmotionLine.tsx index 3652e7c5..c62fbfb9 100644 --- a/web/src/views/UserMemoryDetail/components/EmotionLine.tsx +++ b/web/src/views/UserMemoryDetail/components/EmotionLine.tsx @@ -4,6 +4,7 @@ import ReactEcharts from 'echarts-for-react'; import Empty from '@/components/Empty' import Loading from '@/components/Empty/Loading' import type { Emotion } from './GraphDetail' +import { format } from 'echarts'; interface EmotionLineProps { chartData: Emotion[]; @@ -26,7 +27,7 @@ const EmotionLine: FC = ({ chartData, loading }) => { const seriesData = timePoints.map(time => dataMap.get(time) || 0) return { - name: emotionType, + name: t(`userMemory.${emotionType}`), type: 'line', smooth: true, lineStyle: { @@ -71,7 +72,7 @@ const EmotionLine: FC = ({ chartData, loading }) => { formatter: function(params: any) { let result = `${params[0].axisValue}
` params.forEach((param: any) => { - result += `${param.marker}${param.seriesName}: ${param.value}
` + result += `${param.marker}${param.seriesName}: ${param.value}%
` }) return result } @@ -92,7 +93,7 @@ const EmotionLine: FC = ({ chartData, loading }) => { }, grid: { top: 16, - left: 30, + left: 40, right: 36, bottom: 48, // containLabel: false @@ -103,7 +104,7 @@ const EmotionLine: FC = ({ chartData, loading }) => { boundaryGap: false, axisLabel: { color: '#A8A9AA', - fontFamily: 'PingFangSC, PingFang SC' + fontFamily: 'PingFangSC, PingFang SC', }, axisLine: { show: true, @@ -130,7 +131,8 @@ const EmotionLine: FC = ({ chartData, loading }) => { type: 'value', axisLabel: { color: '#A8A9AA', - fontFamily: 'PingFangSC, PingFang SC' + fontFamily: 'PingFangSC, PingFang SC', + formatter: '{value}%' }, axisLine: { show: true, @@ -152,7 +154,7 @@ const EmotionLine: FC = ({ chartData, loading }) => { type: 'solid' } }, - max: 1, + max: 100, min: 0 }, series: getSeries() diff --git a/web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx b/web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx new file mode 100644 index 00000000..1d0974e3 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx @@ -0,0 +1,113 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { useParams } from 'react-router-dom' +import { Form, Slider } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import RbModal from '@/components/RbModal' +import { forgetTrigger } from '@/api/memory' +import type { ForgetRefreshModalRef } from '../pages/ForgetDetail' + +interface ForgetRefreshModalProps { + refresh: (flag: boolean) => void; +} + +const ForgetRefreshModal = forwardRef(({ + refresh +}, ref) => { + const { t } = useTranslation(); + const { id } = useParams() + const [visible, setVisible] = useState(false); + const [form] = Form.useForm<{ max_merge_batch_size: number; min_days_since_access: number; }>(); + const [loading, setLoading] = useState(false) + const values = Form.useWatch([], form); + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + const handleOpen = () => { + form.resetFields(); + setVisible(true); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + if(!id) return + form + .validateFields() + .then((values) => { + setLoading(true) + forgetTrigger({ + ...values, + end_user_id: id + }) + .then(() => { + refresh(true) + handleClose() + }) + .finally(() => { + setLoading(false) + }) + }) + .catch((err) => { + console.log('err', err) + }); + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
+
+
+ {t(`forgettingEngine.max_merge_batch_size`)} +
+ + + + +
+ {t(`forgettingEngine.range`)}: {[1, 1000]?.join('-')} + {t('forgettingEngine.CurrentValue')}: {values?.min_days_since_access || 0} +
+
+
+
+ {t(`forgettingEngine.min_days_since_access`)} +
+ + + + +
+ {t(`forgettingEngine.range`)}: {[1, 365]?.join('-')} + {t('forgettingEngine.CurrentValue')}: {values?.min_days_since_access || 0} +
+
+
+
+ ); +}); + +export default ForgetRefreshModal; \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/InteractionBar.tsx b/web/src/views/UserMemoryDetail/components/InteractionBar.tsx index 51224676..0db33b6f 100644 --- a/web/src/views/UserMemoryDetail/components/InteractionBar.tsx +++ b/web/src/views/UserMemoryDetail/components/InteractionBar.tsx @@ -1,4 +1,4 @@ -import { type FC } from 'react' +import { type FC, useMemo } from 'react' import { useTranslation } from 'react-i18next' import ReactEcharts from 'echarts-for-react' import Empty from '@/components/Empty' @@ -14,11 +14,13 @@ const Colors = ['#155EEF', '#369F21', '#FF5D34'] const InteractionBar: FC = ({ chartData, loading }) => { const { t } = useTranslation() - const series = [{ - name: 'Interaction Count', - type: 'bar', - data: chartData.map(item => item.count) - }] + const series = useMemo(() => { + return [{ + name: t('userMemory.interactionCountData'), + type: 'bar', + data: chartData.map(item => item.count) + }] + }, [chartData, t]) return ( <> @@ -80,6 +82,7 @@ const InteractionBar: FC = ({ chartData, loading }) => { }, yAxis: { type: 'value', + minInterval: 1, axisLabel: { color: '#A8A9AA', fontFamily: 'PingFangSC, PingFang SC' @@ -104,8 +107,6 @@ const InteractionBar: FC = ({ chartData, loading }) => { type: 'solid' } }, - max: 1, - min: 0 }, series }} diff --git a/web/src/views/UserMemoryDetail/components/PageHeader.tsx b/web/src/views/UserMemoryDetail/components/PageHeader.tsx index 56da70e0..68cdada8 100644 --- a/web/src/views/UserMemoryDetail/components/PageHeader.tsx +++ b/web/src/views/UserMemoryDetail/components/PageHeader.tsx @@ -1,20 +1,22 @@ import { type FC, type ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Layout } from 'antd'; +import { Layout, Space, Button } from 'antd'; import { useTranslation } from 'react-i18next'; -import logoutIcon from '@/assets/images/logout.svg' +import logoutIcon from '@/assets/images/logout_hover.svg' const { Header } = Layout; interface ConfigHeaderProps { name?: string; operation?: ReactNode; - source?: 'detail' | 'node' + source?: 'detail' | 'node'; + extra?: ReactNode; } const PageHeader: FC = ({ name, operation, - source = 'detail' + source = 'detail', + extra }) => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -33,10 +35,13 @@ const PageHeader: FC = ({ {operation} -
- - {t('common.return')} -
+ + + {extra} + ); }; diff --git a/web/src/views/UserMemoryDetail/components/Timeline.tsx b/web/src/views/UserMemoryDetail/components/Timeline.tsx index d7b9b273..e2d9446f 100644 --- a/web/src/views/UserMemoryDetail/components/Timeline.tsx +++ b/web/src/views/UserMemoryDetail/components/Timeline.tsx @@ -9,6 +9,7 @@ import { } from '@/api/memory' import { formatDateTime } from '@/utils/format'; import Empty from '@/components/Empty' +import Tag from '@/components/Tag' interface TimelineItem { id: string; @@ -18,6 +19,9 @@ interface TimelineItem { summary: string; storage_type: number; created_time: string | number; + domain: string; + topic: string; + keywords: string[] } const KEYS = { @@ -68,9 +72,14 @@ const Timeline: FC = () => { {formatDateTime(vo.created_time)} {index !== data.length - 1 && } -
-
{vo.summary}
-
{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}
+
+
+
{vo.summary}
+
{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}
+
+
{[vo.domain, vo.topic].join(' | ')}
+ + {vo.keywords.map(tag => {tag})}
))} diff --git a/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx index 602dbf25..9a19f055 100644 --- a/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx @@ -1,7 +1,7 @@ -import { type FC, useEffect, useState, useMemo } from 'react' +import { useEffect, useState, useMemo, forwardRef, useImperativeHandle, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' -import { Row, Col, Progress } from 'antd' +import { Row, Col, Progress, App } from 'antd' import RbCard from '@/components/RbCard/Card' import { getForgetStats, @@ -12,6 +12,7 @@ import RecentTrendsLineCard from '../components/RecentTrendsLineCard' import Table from '@/components/Table' import { formatDateTime } from '@/utils/format' import StatusTag from '@/components/StatusTag' +import ForgetRefreshModal from '../components/ForgetRefreshModal' const statusTagColors: Record = { statement: 'success', @@ -20,24 +21,33 @@ const statusTagColors: Record { +export interface ForgetRefreshModalRef { + handleOpen: () => void; +} + +const ForgetDetail = forwardRef((_props, ref) => { const { t } = useTranslation() const { id } = useParams() + const { message } = App.useApp() const [loading, setLoading] = useState(false) const [data, setData] = useState({} as ForgetData) + const forgetRefreshModalRef = useRef(null) useEffect(() => { if (!id) return getData() }, [id]) - const getData = () => { + const getData = (flag: boolean = false) => { if (!id) return setLoading(true) getForgetStats(id).then((res) => { const response = res as ForgetData setData(response) setLoading(false) + if (flag) { + message.success(t('forgetDetail.refreshSuccess')) + } }) .finally(() => { setLoading(false) @@ -67,6 +77,14 @@ const ForgetDetail: FC = () => { } }, [data.recent_trends]) + const handleRefresh = () => { + forgetRefreshModalRef.current?.handleOpen() + } + + useImperativeHandle(ref, () => ({ + handleRefresh + })); + return (
{t('forgetDetail.title')}
@@ -152,7 +170,12 @@ const ForgetDetail: FC = () => { ]} pagination={false} /> + +
) -} +}) export default ForgetDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/index.tsx b/web/src/views/UserMemoryDetail/pages/index.tsx index d29a8fad..8f5ee146 100644 --- a/web/src/views/UserMemoryDetail/pages/index.tsx +++ b/web/src/views/UserMemoryDetail/pages/index.tsx @@ -1,7 +1,7 @@ -import { type FC, useEffect, useState, useMemo } from 'react' +import { type FC, useEffect, useState, useMemo, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { Dropdown } from 'antd' +import { Dropdown, Space, Button } from 'antd' import PageHeader from '../components/PageHeader' import StatementDetail from './StatementDetail' @@ -15,12 +15,15 @@ import WorkingDetail from './WorkingDetail' import { getEndUserProfile, } from '@/api/memory' +import refreshIcon from '@/assets/images/refresh_hover.svg' const Detail: FC = () => { const { t } = useTranslation() const { id, type } = useParams() const navigate = useNavigate() const [name, setName] = useState('') + const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null) + useEffect(() => { if (!id) return getData() @@ -40,6 +43,9 @@ const Detail: FC = () => { const onClick = ({ key }: { key: string }) => { navigate(`/user-memory/detail/${id}/${key}`, { replace: true }) } + const handleRefresh = () => { + forgetDetailRef.current?.handleRefresh() + } return (
@@ -49,17 +55,22 @@ const Detail: FC = () => { operation={
- - {type ? t(`userMemory.${type}`) : ''} + - {type ? t(`userMemory.${type}`) : ''}
-
+
} + extra={type === 'FORGETTING_MANAGEMENT' && + } />
{type === 'EMOTIONAL_MEMORY' && } - {type === 'FORGETTING_MANAGEMENT' && } + {type === 'FORGETTING_MANAGEMENT' && } {type === 'IMPLICIT_MEMORY' && } {type === 'SHORT_TERM_MEMORY' && } {type === 'PERCEPTUAL_MEMORY' && }