From a1e8d858a28db95e9079d986ddbcd94059f7c2d2 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 7 Jan 2026 20:37:34 +0800 Subject: [PATCH 1/2] feat(web): forgetting memory --- web/src/api/memory.ts | 4 + .../images/userMemory/arrow_right_hover.svg | 14 ++ web/src/components/StatusTag/index.tsx | 10 +- web/src/i18n/en.ts | 32 +++ web/src/i18n/zh.ts | 27 ++- web/src/routes/index.tsx | 2 + web/src/routes/routes.json | 3 +- web/src/views/ApplicationConfig/Cluster.tsx | 2 +- web/src/views/ApplicationConfig/types.ts | 4 +- web/src/views/UserMemoryDetail/Neo4j.tsx | 2 +- .../components/ActivationMetricsPieCard.tsx | 124 ++++++++++++ .../components/NodeStatistics.tsx | 95 ++++++--- .../components/PageHeader.tsx | 2 +- .../components/RecentTrendsLineCard.tsx | 191 ++++++++++++++++++ .../UserMemoryDetail/pages/ForgetDetail.tsx | 159 +++++++++++++++ .../pages/StatementDetail.tsx | 53 ++--- .../views/UserMemoryDetail/pages/index.tsx | 42 ++++ web/src/views/UserMemoryDetail/types.ts | 34 ++++ 18 files changed, 717 insertions(+), 83 deletions(-) create mode 100644 web/src/assets/images/userMemory/arrow_right_hover.svg create mode 100644 web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx create mode 100644 web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/index.tsx diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 750f559c..1ead7548 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -134,6 +134,10 @@ export const getEmotionSuggestions = (group_id: string) => { export const analyticsRefresh = (end_user_id: string) => { return request.post('/memory-storage/analytics/generate_cache', { end_user_id }) } +export const getForgetStats = (group_id: string) => { + return request.get(`/memory/forget/stats`, { group_id }) +} + /*************** end 用户记忆 相关接口 ******************************/ diff --git a/web/src/assets/images/userMemory/arrow_right_hover.svg b/web/src/assets/images/userMemory/arrow_right_hover.svg new file mode 100644 index 00000000..0fed7c6b --- /dev/null +++ b/web/src/assets/images/userMemory/arrow_right_hover.svg @@ -0,0 +1,14 @@ + + + 编组 5 + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/StatusTag/index.tsx b/web/src/components/StatusTag/index.tsx index 70b720b5..273fff1b 100644 --- a/web/src/components/StatusTag/index.tsx +++ b/web/src/components/StatusTag/index.tsx @@ -3,23 +3,27 @@ import { Tag } from 'antd'; import clsx from 'clsx'; interface StatusTagProps { - status: 'success' | 'error' | 'warning', + status: 'success' | 'error' | 'warning' | 'default' | 'lightBlue' | 'purple', text: string; } const Colors = { success: 'rb:bg-[#369F21]', error: 'rb:bg-[#FF5D34]', warning: 'rb:bg-[#FF8A4C]', + default: 'rb:bg-[#155EEF]', + lightBlue: 'rb:bg-[#4DA8FF]', + purple: 'rb:bg-[#9C6FFF]' } const StatusTag: FC = ({ status, text }) => { + console.log('status', status) return ( - - + + { text } diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index a01feb34..75afc6fc 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2150,5 +2150,37 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re orderPayInfo: 'Payment Information', create_time: 'Creation Time', }, + forgetDetail: { + title: 'The forgetting management system helps AI intelligently manage memory lifecycle by automatically identifying low-value memories, setting forgetting strategies, and performing regular cleanup to optimize memory storage space and improve retrieval efficiency.', + overviewTitle: 'Core Metrics Overview', + totalMemory: 'Total Memory', + MemoryHealth: 'Memory Health', + riskOfForgetting: 'Risk of Forgetting', + statement_count: 'Statement', + entity_count: 'Entity', + summary_count: 'Summary', + chunk_count: 'Chunk', + healthStatus: 'Health Status', + average: 'Average Activation Value', + threshold: 'Threshold Reference:', + unhealthy: 'Unhealthy', + healthy: 'Healthy', + low_nodes: 'Low Activation Nodes', + memoryHealthVisualization: 'Memory Health Visualization', + activationValueDistribution: 'Activation Value Distribution', + forgettingTrend: 'Forgetting Trend (Last 7 Days)', + + nodes_without_activation: 'Observation Zone', + low_activation_nodes: 'Forgetting Zone', + health_nodes: 'Healthy Zone', + average_activation: 'Average Activation Value', + merged_count: 'Daily Merged Nodes Count', + + pending_nodes: 'Risk Node Forgetting Pool', + content_summary: 'Content Summary', + node_type: 'Node Type', + last_access_time: 'Last Activation Time', + activation_value: 'Current Activation Value', + }, }, }; diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 8da2e36c..b05b6224 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2256,6 +2256,31 @@ export const zh = { totalMemory: '记忆总量', MemoryHealth: '记忆健康度', riskOfForgetting: '遗忘风险', - } + statement_count: '陈述', + entity_count: '实体', + summary_count: '摘要', + chunk_count: '片段', + healthStatus: '健康状态', + average: '平均激活值', + threshold: '阈值参考:', + unhealthy: '不健康', + healthy: '健康', + low_nodes: '低激活节点', + memoryHealthVisualization: '记忆健康可视化', + activationValueDistribution: '激活值分布', + forgettingTrend: '遗忘趋势(近7天)', + + nodes_without_activation: '观察区', + low_activation_nodes: '遗忘区', + health_nodes: '健康区', + average_activation: '平均激活值', + merged_count: '每日融合节点数', + + pending_nodes: '风险节点遗忘池', + content_summary: '内容摘要', + node_type: '节点类型', + last_access_time: '最后激活时间', + activation_value: '当前激活值', + }, }, } \ No newline at end of file diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index bac985bf..f3bd9c2d 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -59,6 +59,8 @@ const componentMap: Record>> = ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')), EmotionEngine: lazy(() => import('@/views/EmotionEngine')), StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')), + ForgetDetail: lazy(() => import('@/views/UserMemoryDetail/pages/ForgetDetail')), + MemoryNodeDetail: lazy(() => import('@/views/UserMemoryDetail/pages/index')), SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')), OrderPayment: lazy(() => import('@/views/OrderPayment')), OrderHistory: lazy(() => import('@/views/OrderHistory')), diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index a29d1c63..ca6a3271 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -43,7 +43,8 @@ { "path": "/application/config/:id", "element": "ApplicationConfig" }, { "path": "/conversation/:token", "element": "Conversation" }, { "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" }, - { "path": "/statement/:id", "element": "StatementDetail" } + { "path": "/statement/:id", "element": "StatementDetail" }, + { "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" } ] }, { diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index ce639d62..66245446 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -138,7 +138,7 @@ const Cluster = forwardRef((_props, ref) => {
- + { const aboutMeRef = useRef(null) const handleNameUpdate = (data: { other_name?: string; id: string }) => { - setName(data.other_name ?? data.id) + setName(data.other_name && data.other_name !== '' ? data.other_name : data.id) } const handleRefresh = () => { diff --git a/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx b/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx new file mode 100644 index 00000000..eedd3e7e --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx @@ -0,0 +1,124 @@ +import { type FC, useRef, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import ReactEcharts from 'echarts-for-react'; +import Loading from '@/components/Empty/Loading' +import Empty from '@/components/Empty' +import RbCard from '@/components/RbCard/Card' + +interface ActivationMetricsPieCardProps { + chartData: Array>; + loading: boolean; +} +const Colors = ['#155EEF', '#FFB048', '#FF5D34'] + +const ActivationMetricsPieCard: FC = ({ chartData, loading }) => { + const { t } = useTranslation() + const chartRef = useRef(null); + const resizeScheduledRef = useRef(false) + + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [chartData]) + + return ( + + {loading + ? + : !chartData || chartData.length === 0 + ? + : + } + + ) +} + +export default ActivationMetricsPieCard diff --git a/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx b/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx index 288e11c0..8815c66d 100644 --- a/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx +++ b/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next' import { useParams, useNavigate } from 'react-router-dom' import { Skeleton } from 'antd'; import RbCard from '@/components/RbCard/Card' -import Empty from '@/components/Empty'; import { getNodeStatistics, } from '@/api/memory' @@ -15,11 +14,25 @@ const BG_LIST = [ 'rb:bg-[linear-gradient(316deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]', 'rb:bg-[linear-gradient(316deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]', 'rb:bg-[linear-gradient(314deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]', - 'rb:bg-[linear-gradient(314deg,rgba(255,93,52,0.06)_0%,rgba(251,253,255,0)_100%)]', - 'rb:bg-[linear-gradient(180deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]', - 'rb:bg-[linear-gradient(180deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]', - 'rb:bg-[linear-gradient(180deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]', - 'rb:bg-[]', + 'rb:bg-[linear-gradient(332deg,rgba(255,93,52,0.06)_0%,rgba(251,253,255,0)_100%)]', + 'rb:bg-[linear-gradient(313deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]', + 'rb:bg-[linear-gradient(332deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]', +] +const typeList = [ + { key: 'PERCEPTUAL_MEMORY', bg: 0 }, + { key: 'WORKING_MEMORY', bg: 1 }, + { key: 'EMOTIONAL_MEMORY', bg: 2 }, + { key: 'SHORT_TERM_MEMORY', bg: 3 }, + { + key: 'LONG_TERM_MEMORY', + bg: 4, + children: [ + { key: 'IMPLICIT_MEMORY' }, + { key: 'EPISODIC_MEMORY' }, + { key: 'EXPLICIT_MEMORY' } + ] + }, + { key: 'FORGETTING_MANAGEMENT', bg: 5 }, ] const NodeStatistics: FC = () => { @@ -52,43 +65,59 @@ const NodeStatistics: FC = () => { }) } const handleViewDetail = (type: string) => { - switch (type) { - case 'EMOTIONAL_MEMORY': - navigate(`/statement/${id}`) - break - } + navigate(`/user-memory/detail/${id}/${type}`) } + const renderCard = (key: string, bgIndex: number | null, isChild: boolean = false) => { + const item = data.find((item) => item.type === key) + return ( +
handleViewDetail(key)} + > +
+
+ {t(`userMemory.${key}`)} +
+
+
+
{item?.count ?? 0}
+
+ ) + } + return ( {t('userMemory.nodeStatistics')} ({t('userMemory.total')}: {total})} headerType="borderless" > {loading - ? - : data && data.length > 0 - ?
- {data.map((vo, index) => ( -
handleViewDetail(vo.type)} - > -
-
- {t(`userMemory.${vo.type}`)} -
- {vo.type === 'EMOTIONAL_MEMORY' &&
} + ? + :
+ {typeList.map((vo) => { + if (!vo.children) { + return
{renderCard(vo.key, vo.bg)}
+ } + return ( +
+
{t(`userMemory.${vo.key}`)}
+
+ {vo.children.map((child) =>
{renderCard(child.key, null, true)}
)}
-
{vo.count ?? 0}
- ))} + ) + })}
- : - } + } ) } diff --git a/web/src/views/UserMemoryDetail/components/PageHeader.tsx b/web/src/views/UserMemoryDetail/components/PageHeader.tsx index dda60e7e..56da70e0 100644 --- a/web/src/views/UserMemoryDetail/components/PageHeader.tsx +++ b/web/src/views/UserMemoryDetail/components/PageHeader.tsx @@ -9,7 +9,7 @@ const { Header } = Layout; interface ConfigHeaderProps { name?: string; operation?: ReactNode; - source?: 'detail' | 'statement' + source?: 'detail' | 'node' } const PageHeader: FC = ({ name, diff --git a/web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx b/web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx new file mode 100644 index 00000000..1d6bd213 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx @@ -0,0 +1,191 @@ +import { type FC, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import ReactEcharts from 'echarts-for-react'; +import * as echarts from 'echarts'; +import Empty from '@/components/Empty' +import Loading from '@/components/Empty/Loading' +import RbCard from '@/components/RbCard/Card' + +interface RecentTrendsLineCardProps { + chartData: Array>; + seriesList: string[]; + loading?: boolean; +} + +const Colors = ['#155EEF', '#FF5D34'] + +const RecentTrendsLineCard: FC = ({ chartData, seriesList, loading }) => { + const { t } = useTranslation() + const chartRef = useRef(null); + + const getSeries = () => { + return seriesList.map((key, index) => ({ + name: key === 'merged_count' ? t('forgetDetail.merged_count') : t('forgetDetail.average_activation'), + type: 'line', + yAxisIndex: key === 'merged_count' ? 0 : 1, + smooth: true, + lineStyle: { + width: 3, + color: Colors[index] + }, + itemStyle: { + color: Colors[index] + }, + areaStyle: { + color: Colors[index], + opacity: 0.08 + }, + data: chartData.map(item => item[key]) + })) + } + + return ( + + {loading + ? + : !chartData || chartData.length === 0 + ? + : ` + params.forEach((param: any) => { + result += `${param.marker}${param.seriesName}: ${param.value}
` + }) + return result + } + }, + legend: { + bottom: 2, + padding: 0, + itemGap: 24, + itemWidth: 40, + itemHeight: 12, + borderRadius: 2, + orient: 'horizontal', + textStyle: { + color: '#5B6167', + fontFamily: 'PingFangSC, PingFang SC', + lineHeight: 16, + } + }, + grid: { + top: 16, + left: 30, + right: 36, + bottom: 48, + // containLabel: false + }, + xAxis: { + type: 'category', + data: chartData.map(item => item.date), + boundaryGap: false, + axisLabel: { + color: '#A8A9AA', + fontFamily: 'PingFangSC, PingFang SC' + }, + axisLine: { + show: true, + lineStyle: { + color: '#EBEBEB' + } + }, + splitLine: { + show: true, + lineStyle: { + color: '#EBEBEB', + type: 'solid' + } + }, + axisTick: { + show: true, + lineStyle: { + color: '#EBEBEB', + type: 'solid' + } + } + }, + yAxis: [ + { + type: 'value', + position: 'left', + axisLabel: { + color: Colors[0], + fontFamily: 'PingFangSC, PingFang SC' + }, + axisLine: { + lineStyle: { + color: Colors[0] + } + }, + splitLine: { + show: true, + lineStyle: { + color: '#EBEBEB', + type: 'solid' + } + }, + axisTick: { + show: true, + lineStyle: { + color: '#EBEBEB', + type: 'solid' + } + }, + }, + { + type: 'value', + position: 'right', + axisLabel: { + color: Colors[1], + fontFamily: 'PingFangSC, PingFang SC', + formatter: '{value}' + }, + axisLine: { + lineStyle: { + color: Colors[1] + } + }, + splitLine: { + show: false, + }, + axisTick: { + show: true, + lineStyle: { + color: '#EBEBEB', + type: 'solid' + } + }, + max: 1, + min: 0 + } + ], + series: getSeries() + }} + style={{ height: '265px', width: '100%', minWidth: '100%' }} + notMerge={true} + lazyUpdate={true} + /> + } +
+ ) +} + +export default RecentTrendsLineCard diff --git a/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx new file mode 100644 index 00000000..6a59d41a --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx @@ -0,0 +1,159 @@ +import { type FC, useEffect, useState, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Row, Col, Progress } from 'antd' +import RbCard from '@/components/RbCard/Card' +import { + getForgetStats, +} from '@/api/memory' +import type { ForgetData } from '../types' +import ActivationMetricsPieCard from '../components/ActivationMetricsPieCard' +import RecentTrendsLineCard from '../components/RecentTrendsLineCard' +import Table from '@/components/Table' +import { formatDateTime } from '@/utils/format' +import StatusTag from '@/components/StatusTag' + +const statusTagColors: Record = { + statement: 'success', + entity: 'purple', + summary: 'default', + chunk: 'warning', +} + +const ForgetOverview: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as ForgetData) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + // 记忆洞察 + const getData = () => { + if (!id) return + setLoading(true) + getForgetStats(id).then((res) => { + const response = res as ForgetData + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + const chartData = useMemo(() => { + const { activation_metrics } = data + if (!activation_metrics) return [] + + let health_nodes = (activation_metrics.total_nodes || 0) - (activation_metrics.low_activation_nodes || 0) - (activation_metrics.nodes_without_activation || 0) + + return [ + { name: t('forgetDetail.health_nodes'), value: health_nodes }, + { name: t('forgetDetail.nodes_without_activation'), value: activation_metrics.nodes_without_activation || 0 }, + { name: t('forgetDetail.low_activation_nodes'), value: activation_metrics.low_activation_nodes || 0 }, + ] + + }, [data.activation_metrics, t]) + + const seriesList = useMemo(() => { + const { recent_trends = [] } = data + if (!recent_trends || recent_trends.length === 0) return { chartData: [], seriesList: [] } + + return { + chartData: recent_trends, + seriesList: ['merged_count', 'average_activation'] + } + }, [data.recent_trends]) + + return ( +
+
{t('forgetDetail.title')}
+
{t('forgetDetail.overviewTitle')}
+ + + +
{t('forgetDetail.totalMemory')}
+
{data?.activation_metrics?.total_nodes ?? 0}
+
+ {['statement_count', 'entity_count', 'summary_count', 'chunk_count'].map((key, index) => ( +
+
{data?.node_distribution?.[key as keyof typeof data.node_distribution] ?? 0}
+
{t(`forgetDetail.${key}`)}
+
+ ))} +
+
+ + + +
{t('forgetDetail.MemoryHealth')}
+
{data?.activation_metrics?.average_activation_value ?? 0}
+ +
+
{t('forgetDetail.healthStatus')}
+
{data?.activation_metrics?.average_activation_value > data.activation_metrics?.forgetting_threshold ? t('forgetDetail.healthy') : t('forgetDetail.unhealthy')}
+
+ {t('forgetDetail.average')}
+ {t('forgetDetail.threshold')}{data.activation_metrics?.forgetting_threshold ?? 0} +
+
+
+ + + +
{t('forgetDetail.riskOfForgetting')}
+
{data.activation_metrics?.low_activation_nodes ?? 0}
+
{t('forgetDetail.low_nodes')}
+
+ +
+ +
{t('forgetDetail.memoryHealthVisualization')}
+ + + + + + + + +
{t('forgetDetail.pending_nodes')}
+
{content_summary}
+ }, + { + title: t('forgetDetail.node_type'), + dataIndex: 'node_type', + key: 'node_type', + render: (node_type: string) => { + return } + }, + { + title: t('forgetDetail.last_access_time'), + dataIndex: 'last_access_time', + key: 'last_access_time', + render: (last_access_time) => formatDateTime(last_access_time, 'YYYY-MM-DD HH:mm') + }, + { + title: t('forgetDetail.activation_value'), + dataIndex: 'activation_value', + key: 'activation_value', + }, + ]} + pagination={false} + /> + + ) +} +export default ForgetOverview \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx b/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx index 744c244d..e6ddfd20 100644 --- a/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx @@ -1,53 +1,26 @@ -import { type FC, useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' +import { type FC } from 'react' import { Row, Col, Space } from 'antd'; import WordCloud from '../components/WordCloud' import EmotionTags from '../components/EmotionTags' import Health from '../components/Health' import Suggestions from '../components/Suggestions' -import PageHeader from '../components/PageHeader' -import { - getEndUserProfile, -} from '@/api/memory' const StatementDetail: FC = () => { - const { id } = useParams() - const [name, setName] = useState('') - useEffect(() => { - if (!id) return - getData() - }, [id]) - - const getData = () => { - if (!id) return - getEndUserProfile(id).then((res) => { - const response = res as { other_name: string; id: string; } - setName(response.other_name ?? response.id) - }) - } return ( -
- -
- -
- - - - - - - - - - - - + + + + + + + + + + + + ) } diff --git a/web/src/views/UserMemoryDetail/pages/index.tsx b/web/src/views/UserMemoryDetail/pages/index.tsx new file mode 100644 index 00000000..6b78a210 --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/index.tsx @@ -0,0 +1,42 @@ +import { type FC, useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' + +import PageHeader from '../components/PageHeader' +import StatementDetail from './StatementDetail' +import ForgetDetail from './ForgetDetail' +import { + getEndUserProfile, +} from '@/api/memory' + +const Detail: FC = () => { + const { id, type } = useParams() + const [name, setName] = useState('') + useEffect(() => { + if (!id) return + getData() + }, [id]) + + const getData = () => { + if (!id) return + getEndUserProfile(id).then((res) => { + const response = res as { other_name: string; id: string; } + setName(response.other_name || response.id) + }) + } + + console.log('Detail', name) + return ( +
+ +
+ {type === 'EMOTIONAL_MEMORY' && } + {type === 'FORGETTING_MANAGEMENT' && } +
+
+ ) +} + +export default Detail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/types.ts b/web/src/views/UserMemoryDetail/types.ts index 8fd050a9..77dd653e 100644 --- a/web/src/views/UserMemoryDetail/types.ts +++ b/web/src/views/UserMemoryDetail/types.ts @@ -140,4 +140,38 @@ export interface AboutMeRef { } export interface EndUserProfileRef { data: EndUser | null +} + + +export interface ForgetData { + activation_metrics: { + total_nodes: number; + nodes_with_activation: number; + nodes_without_activation: number; + average_activation_value: number; + low_activation_nodes: number; + timestamp: number; + forgetting_threshold: number; + }, + node_distribution: { + statement_count: number; + entity_count: number; + summary_count: number; + chunk_count: number; + }, + recent_trends: { + date: string; + merged_count: number; + average_activation: number; + total_nodes: number; + execution_time: number; + }[], + pending_nodes: { + node_id: string; + node_type: string; + content_summary: string; + activation_value: number; + last_access_time: number; + }[], + timestamp: number; } \ No newline at end of file From 81508a25a8c7bed8d47ca4843adc954f72010f76 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 8 Jan 2026 19:46:02 +0800 Subject: [PATCH 2/2] feat(web): user memory detail --- web/package.json | 1 + web/src/api/memory.ts | 44 +++ .../assets/images/userMemory/shortTerm.png | Bin 0 -> 28977 bytes .../assets/images/userMemory/up_border.svg | 14 + web/src/assets/images/userMemory/view.svg | 19 ++ .../assets/images/userMemory/view_hover.svg | 19 ++ web/src/i18n/en.ts | 101 ++++++- web/src/i18n/zh.ts | 87 ++++++ web/src/routes/index.tsx | 1 + web/src/routes/routes.json | 3 +- web/src/views/UserMemory/index.tsx | 22 +- web/src/views/UserMemoryDetail/Neo4j.tsx | 2 +- .../UserMemoryDetail/components/Habits.tsx | 85 ++++++ .../components/InterestAreas.tsx | 75 ++++++ .../components/PerceptualLastInfo.tsx | 120 +++++++++ .../UserMemoryDetail/components/Portrait.tsx | 77 ++++++ .../components/Preferences.tsx | 183 +++++++++++++ .../components/RelationshipNetwork.tsx | 63 ++++- .../UserMemoryDetail/components/Timeline.tsx | 82 ++++++ .../UserMemoryDetail/components/WordCloud.tsx | 2 +- .../UserMemoryDetail/pages/EpisodicDetail.tsx | 250 ++++++++++++++++++ .../UserMemoryDetail/pages/ForgetDetail.tsx | 4 +- .../UserMemoryDetail/pages/GraphDetail.tsx | 14 + .../UserMemoryDetail/pages/ImplicitDetail.tsx | 34 +++ .../pages/PerceptualDetail.tsx | 32 +++ .../pages/ShortTermDetail.tsx | 114 ++++++++ .../views/UserMemoryDetail/pages/index.tsx | 34 ++- web/src/views/UserMemoryDetail/types.ts | 12 +- 28 files changed, 1463 insertions(+), 31 deletions(-) create mode 100644 web/src/assets/images/userMemory/shortTerm.png create mode 100644 web/src/assets/images/userMemory/up_border.svg create mode 100644 web/src/assets/images/userMemory/view.svg create mode 100644 web/src/assets/images/userMemory/view_hover.svg create mode 100644 web/src/views/UserMemoryDetail/components/Habits.tsx create mode 100644 web/src/views/UserMemoryDetail/components/InterestAreas.tsx create mode 100644 web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx create mode 100644 web/src/views/UserMemoryDetail/components/Portrait.tsx create mode 100644 web/src/views/UserMemoryDetail/components/Preferences.tsx create mode 100644 web/src/views/UserMemoryDetail/components/Timeline.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/GraphDetail.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx diff --git a/web/package.json b/web/package.json index 9d157982..e28e8b56 100644 --- a/web/package.json +++ b/web/package.json @@ -30,6 +30,7 @@ "dayjs": "^1.11.18", "echarts": "^5.6.0", "echarts-for-react": "^3.0.2", + "echarts-wordcloud": "^2.1.0", "i18next": "^25.6.0", "js-yaml": "^4.1.1", "lexical": "^0.39.0", diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 1ead7548..39136da3 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -134,9 +134,53 @@ export const getEmotionSuggestions = (group_id: string) => { export const analyticsRefresh = (end_user_id: string) => { return request.post('/memory-storage/analytics/generate_cache', { end_user_id }) } +// 遗忘 export const getForgetStats = (group_id: string) => { return request.get(`/memory/forget/stats`, { group_id }) } +// 隐性记忆-偏好 +export const getImplicitPreferences = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/preferences/${end_user_id}`) +} +// 隐性记忆-核心特质 +export const getImplicitPortrait = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/portrait/${end_user_id}`) +} +// 隐性记忆-兴趣领域分布 +export const getImplicitInterestAreas = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/interest-areas/${end_user_id}`) +} +// 隐性记忆-用户习惯分析 +export const getImplicitHabits = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/habits/${end_user_id}`) +} +// 短期记忆 +export const getShortTerm = (end_user_id: string) => { + return request.get(`/memory/short/short_term`, { end_user_id }) +} +// 感知记忆-视觉记忆 +export const getPerceptualLastVisual = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/last_visual`) +} +// 感知记忆-音频记忆 +export const getPerceptualLastListen = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/last_listen`) +} +// 感知记忆-文本记忆 +export const getPerceptualLastText = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/last_text`) +} +// 感知记忆-感知记忆时间线 +export const getPerceptualTimeline = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/timeline`) +} +// 情景记忆-总览 +export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => { + return request.post(`/memory-storage/classifications/episodic-memory`, data) +} +export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => { + return request.post(`/memory-storage/classifications/episodic-memory-details`, data) +} /*************** end 用户记忆 相关接口 ******************************/ diff --git a/web/src/assets/images/userMemory/shortTerm.png b/web/src/assets/images/userMemory/shortTerm.png new file mode 100644 index 0000000000000000000000000000000000000000..37a880ecb37f3fd49da27c59fa513acdb525eef9 GIT binary patch literal 28977 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 Tk;AJH|0|pv71|#Bm*W2cMj$GW literal 0 HcmV?d00001 diff --git a/web/src/assets/images/userMemory/up_border.svg b/web/src/assets/images/userMemory/up_border.svg new file mode 100644 index 00000000..a7fe9978 --- /dev/null +++ b/web/src/assets/images/userMemory/up_border.svg @@ -0,0 +1,14 @@ + + + 下拉备份 + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/userMemory/view.svg b/web/src/assets/images/userMemory/view.svg new file mode 100644 index 00000000..642841ae --- /dev/null +++ b/web/src/assets/images/userMemory/view.svg @@ -0,0 +1,19 @@ + + + 查看 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/userMemory/view_hover.svg b/web/src/assets/images/userMemory/view_hover.svg new file mode 100644 index 00000000..642841ae --- /dev/null +++ b/web/src/assets/images/userMemory/view_hover.svg @@ -0,0 +1,19 @@ + + + 查看 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 75afc6fc..c7c9a937 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1223,6 +1223,23 @@ export const en = { growth_trajectory: 'Growth Trajectory', personality: 'Personality Traits', core_values: 'Core Values', + + Statement_emotion_keywords: 'Emotion Keywords', + Statement_emotion_type: 'Emotion Type', + Statement_emotion_subject: 'Emotion Subject', + Statement_importance_score: 'Importance Score', + + ExtractedEntity_description: 'Description', + ExtractedEntity_name: 'Content', + ExtractedEntity_entity_type: 'Type', + ExtractedEntity_created_at: 'Created At', + ExtractedEntity_aliases: 'Aliases', + ExtractedEntity_connect_strngth: 'Connection Strength', + ExtractedEntity_importance_score: 'Importance Score', + + associative_memory: 'Associative Memory', + unix: 'items', + completeMemory: 'Complete Memory', }, space: { createSpace: 'Create Space', @@ -2151,15 +2168,15 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re create_time: 'Creation Time', }, forgetDetail: { - title: 'The forgetting management system helps AI intelligently manage memory lifecycle by automatically identifying low-value memories, setting forgetting strategies, and performing regular cleanup to optimize memory storage space and improve retrieval efficiency.', + title: 'The forgetting management system helps AI intelligently manage memory lifecycle by automatically identifying low-value memories, setting forgetting strategies, and executing regular cleanup to optimize memory storage space and improve retrieval efficiency.', overviewTitle: 'Core Metrics Overview', totalMemory: 'Total Memory', MemoryHealth: 'Memory Health', - riskOfForgetting: 'Risk of Forgetting', - statement_count: 'Statement', - entity_count: 'Entity', - summary_count: 'Summary', - chunk_count: 'Chunk', + riskOfForgetting: 'Forgetting Risk', + statement_count: 'Statements', + entity_count: 'Entities', + summary_count: 'Summaries', + chunk_count: 'Chunks', healthStatus: 'Health Status', average: 'Average Activation Value', threshold: 'Threshold Reference:', @@ -2174,7 +2191,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re low_activation_nodes: 'Forgetting Zone', health_nodes: 'Healthy Zone', average_activation: 'Average Activation Value', - merged_count: 'Daily Merged Nodes Count', + merged_count: 'Daily Merged Node Count', pending_nodes: 'Risk Node Forgetting Pool', content_summary: 'Content Summary', @@ -2182,5 +2199,75 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re last_access_time: 'Last Activation Time', activation_value: 'Current Activation Value', }, + episodicDetail: { + title: 'Record every important scene you have truly experienced', + total_all: 'Total Episodic Memories', + all: "All", + today: 'Today', + this_week: 'This Week', + this_month: 'This Month', + conversation: "Conversation", + project_work: "Project/Work", + learning: "Learning", + decision: "Decision", + important_event: "Important Event", + titleKeywordPlaceholder: 'Search episode title or content', + curResult: 'Current Filter Results', + unix: 'items', + created: 'Occurrence Time', + episodic_type: 'Episode Type', + involved_objects: 'Involved Objects', + content_records: 'Episode Content Records', + emotion: 'Emotion and State Records', + }, + implicitDetail: { + title: 'The invisible forces that shaped me', + preferences: 'My Subconscious Preferences', + preferencesDetail: 'Association Network', + portraitTitle: 'My Subconscious Portrait', + portraitSubTitle: 'Personalized insights generated by AI based on your preference tags', + portrait: 'Core Traits', + aesthetic: 'Aesthetic Driven', + creativity: 'Creative Thinking', + literature: 'Cultural Sensitivity', + technology: 'Technology Affinity', + interestAreas: 'Interest Area Distribution', + art: 'Art & Design', + music: 'Music & Culture', + tech: 'Technology & Future', + lifestyle: 'Lifestyle', + habits: 'User Habit Analysis', + habitsSubTitle: 'Habit characteristics identified based on your behavior patterns', + context_details: 'Preference Details', + supporting_evidence: 'Preference Source', + specific_examples: 'Source', + }, + shortTermDetail: { + title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.', + retrieval_number: 'Retrieval Count', + entity: 'Extracted Entities', + long_term_number: 'Long-term Candidates', + shortTermTitle: 'Deep Retrieval & Extended Answer Area', + shortTermSubTitle: 'Stores deep information retrieval performed to answer questions and the extended answers generated from it, including original questions, retrieved information, and generated answers.', + longTermTitle: 'Long-term Memory Candidate Pool', + longTermSubTitle: 'Aggregates short-term memory, filters and prepares content for storage in long-term memory. This is the "transfer station" and "filter" from short-term to long-term memory.', + answer: 'Answer', + query: 'Question', + noAnswer: 'No reply yet', + }, + perceptualDetail: { + last_visual: 'Visual Perception Stream', + last_listen: 'Auditory Perception Stream', + last_text: 'Text Perception', + summary: 'Summary', + keywords: 'Keywords', + topic: 'Topic', + domain: 'Domain', + scene: 'Scene', + speaker_count: 'Number of Speakers', + section_count: 'Number of Sections', + timeLine: 'Perception Timeline', + lastInfo: 'Real-time Perception Dashboard', + } }, }; diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index b05b6224..d858b643 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1304,6 +1304,23 @@ export const zh = { growth_trajectory: '成长轨迹', personality: '性格特点', core_values: '核心价值观', + + Statement_emotion_keywords: '情感关键词', + Statement_emotion_type: '情感类型', + Statement_emotion_subject: '情感主体', + Statement_importance_score: '重要性评分', + + ExtractedEntity_description: '描述', + ExtractedEntity_name: '内容', + ExtractedEntity_entity_type: '类型', + ExtractedEntity_created_at: '创建时间', + ExtractedEntity_aliases: '别名', + ExtractedEntity_connect_strngth: '连接强度', + ExtractedEntity_importance_score: '重要性评分', + + associative_memory: '关联记忆', + unix: '个', + completeMemory: '完整记忆', }, space: { createSpace: '创建空间', @@ -2282,5 +2299,75 @@ export const zh = { last_access_time: '最后激活时间', activation_value: '当前激活值', }, + episodicDetail: { + title: '记录你真实经历过的每一个重要场景', + total_all: '情景记忆总数', + all: "全部", + today: '今天', + this_week: '本周', + this_month: '本月', + conversation: "对话", + project_work: "项目/工作", + learning: "学习", + decision: "决策", + important_event: "重要事件", + titleKeywordPlaceholder: '搜索情景标题或内容', + curResult: '当前筛选结果', + unix: '条', + created: '发生时间', + episodic_type: '情景类型', + involved_objects: '涉及对象', + content_records: '情景内容记录', + emotion: '情绪与状态记录', + }, + implicitDetail: { + title: '那些塑造了我的无形力量', + preferences: '我的潜意识偏好', + preferencesDetail: '的联想网络', + portraitTitle: '我的潜意识画像', + portraitSubTitle: '基于您的偏好标签,AI为您生成的个性化洞察', + portrait: '核心特质', + aesthetic: '审美驱动', + creativity: '创造性思维', + literature: '文化敏感度', + technology: '技术亲和力', + interestAreas: '兴趣领域分布', + art: '艺术与设计', + music: '音乐与文化', + tech: '科技与未来', + lifestyle: '生活方式', + habits: '用户习惯分析', + habitsSubTitle: '基于您的行为模式识别的习惯特征', + context_details: '偏好详情', + supporting_evidence: '偏好来源', + specific_examples: '来源', + }, + shortTermDetail: { + title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。', + retrieval_number: '检索次数', + entity: '提取实体', + long_term_number: '长期候选', + shortTermTitle: '深度检索与扩展答案区', + shortTermSubTitle: '存放为回答问题而进行的深度信息检索和由此生成的扩展答案,包含原始问题、检索信息和生成答案。', + longTermTitle: '长期记忆候选池', + longTermSubTitle: '聚合短期记忆,筛选并准备存入长期记忆的内容。这是从短时记忆到长时记忆的"中转站"和"过滤器"。', + answer: '回答', + query: '问题', + noAnswer: '暂无回复', + }, + perceptualDetail: { + last_visual: '视觉感知流', + last_listen: '听觉感知流', + last_text: '文本感知', + summary: '摘要', + keywords: '关键词', + topic: '主题', + domain: '领域', + scene: '场景', + speaker_count: '对话人数', + section_count: '段落数', + timeLine: '感知时间线', + lastInfo: '实时感知仪表盘', + } }, } \ No newline at end of file diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index f3bd9c2d..bc2f61e6 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -61,6 +61,7 @@ const componentMap: Record>> = StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')), ForgetDetail: lazy(() => import('@/views/UserMemoryDetail/pages/ForgetDetail')), MemoryNodeDetail: lazy(() => import('@/views/UserMemoryDetail/pages/index')), + GraphDetail: lazy(() => import('@/views/UserMemoryDetail/pages/GraphDetail')), SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')), OrderPayment: lazy(() => import('@/views/OrderPayment')), OrderHistory: lazy(() => import('@/views/OrderHistory')), diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index ca6a3271..2f332b72 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -44,7 +44,8 @@ { "path": "/conversation/:token", "element": "Conversation" }, { "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" }, { "path": "/statement/:id", "element": "StatementDetail" }, - { "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" } + { "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" }, + { "path": "/graph/:id", "element": "GraphDetail" } ] }, { diff --git a/web/src/views/UserMemory/index.tsx b/web/src/views/UserMemory/index.tsx index af7db5e1..7065f036 100644 --- a/web/src/views/UserMemory/index.tsx +++ b/web/src/views/UserMemory/index.tsx @@ -104,15 +104,15 @@ export default function UserMemory() { return (
- + {countList.map(key => (
-
-
+
+
{countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''} - +
-
{t(`userMemory.${key}`)}
+
{t(`userMemory.${key}`)}
))} @@ -140,22 +140,22 @@ export default function UserMemory() { return (
handleViewDetail(end_user.id)} >
-
{name[0]}
-
+
{name[0]}
+
{name || '-'}
-
+
-
{memory_num.total || 0}
-
{t(`userMemory.knowledgeEntryCount`)}
+
{memory_num.total || 0}
+
{t(`userMemory.knowledgeEntryCount`)}
diff --git a/web/src/views/UserMemoryDetail/Neo4j.tsx b/web/src/views/UserMemoryDetail/Neo4j.tsx index 2425b503..a6dedd8b 100644 --- a/web/src/views/UserMemoryDetail/Neo4j.tsx +++ b/web/src/views/UserMemoryDetail/Neo4j.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useRef, useState } from 'react' +import { type FC, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import { Row, Col, Space, Button } from 'antd' import { useTranslation } from 'react-i18next'; diff --git a/web/src/views/UserMemoryDetail/components/Habits.tsx b/web/src/views/UserMemoryDetail/components/Habits.tsx new file mode 100644 index 00000000..746d7164 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Habits.tsx @@ -0,0 +1,85 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Space, Progress } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import Empty from '@/components/Empty' +import { + getImplicitHabits, +} from '@/api/memory' + +interface HabitsItem { + habit_description: string; + frequency_pattern: string; + time_context: string; + confidence_level: string; + supporting_summaries: string[]; + first_observed: string; + last_observed: string; + is_current: boolean; + specific_examples: string[]; +} + +const Habits: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState([]) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + // 记忆洞察 + const getData = () => { + if (!id) return + setLoading(true) + getImplicitHabits(id).then((res) => { + const response = res as HabitsItem[] + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + <> +
{t('implicitDetail.habits')}
+
{t('implicitDetail.habitsSubTitle')}
+ + {loading + ? + : data.length === 0 + ? + : + {data.map((vo, voIdx) => ( +
+
+
+
{vo.habit_description}
+
{vo.time_context}
+
+
{vo.confidence_level}%
+
+ + {vo.specific_examples.length > 0 && <> +
{t('implicitDetail.specific_examples')}
+
+ {vo.specific_examples.map((item, index) => ( +
- {item}
+ ))} +
+ } + +
+ ))} +
+ } +
+ + ) +} +export default Habits \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/InterestAreas.tsx b/web/src/views/UserMemoryDetail/components/InterestAreas.tsx new file mode 100644 index 00000000..357336f4 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/InterestAreas.tsx @@ -0,0 +1,75 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Progress } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getImplicitInterestAreas, +} from '@/api/memory' + +interface Item { + category_name: string; + percentage: number; + evidence: string[]; + trending_direction: string | null; +} +interface InterestAreasItem { + user_id: string; + analysis_timestamp: number | string; + total_summaries_analyzed: number; + tech: Item; + lifestyle: Item; + music: Item; + art: Item; +} + +const InterestAreas: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as InterestAreasItem) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + // 记忆洞察 + const getData = () => { + if (!id) return + setLoading(true) + getImplicitInterestAreas(id).then((res) => { + const response = res as InterestAreasItem + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + :
+ {(['art', 'music', 'tech', 'lifestyle'] as const).map((key) => { + return ( +
+
+
{t(`implicitDetail.${key}`)}
+ {data[key]?.percentage ?? 0}% +
+ +
+ ) + })} +
+ } +
+ ) +} +export default InterestAreas \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx new file mode 100644 index 00000000..d3788a74 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx @@ -0,0 +1,120 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Space, Tooltip, Image } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getPerceptualLastVisual, + getPerceptualLastListen, + getPerceptualLastText, +} from '@/api/memory' + +interface PerceptualLastInfoItem { + id: string; + file_name: string; + file_ext: string; + file_path: string; + storage_type: number; + summary: string; + keywords: string[]; + topic: string; + domain: string; + created_time: number | string; + scene: string[] + speaker_count: number; + section_count: number; +} + +const KEYS = { + last_visual: ['summary', 'keywords', 'topic', 'domain', 'scene'], + last_listen: ['summary', 'keywords', 'topic', 'domain', 'speaker_count'], + last_text: ['summary', 'keywords', 'topic', 'domain', 'section_count'], +} + +const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text' }> = ({ type }) => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as PerceptualLastInfoItem) + + useEffect(() => { + if (!id) return + getData() + }, [id, type]) + const getData = () => { + if (!id || !type) return + setLoading(true) + const request = type === 'last_visual' + ? getPerceptualLastVisual(id) + : type === 'last_listen' + ? getPerceptualLastListen(id) + : getPerceptualLastText(id) + request.then((res) => { + const response = res as PerceptualLastInfoItem + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + :
+
+ {data.file_path ? ( + type === 'last_visual' ? ( + /\.(mp4|webm|ogg|mov)$/i.test(data.file_name) ? ( + + ) : /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name) ? ( + + // {data.file_name} + ) : ( +
{data.file_name}
+ ) + ) : type === 'last_listen' && /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name) ? ( + + ) : ( +
{data.file_name}
+ ) + ) : ( +
No file
+ )} +
+ + {KEYS[type].map(key => { + const value = (data as any)[key] + return ( +
+
{t(`perceptualDetail.${key}`)}
+ {key === 'summary' ? ( + +
+ {typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'} +
+
+ ) + :
+ {typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'} +
+ } +
+ ) + })} +
+
+ } +
+ ) +} +export default PerceptualLastInfo \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/Portrait.tsx b/web/src/views/UserMemoryDetail/components/Portrait.tsx new file mode 100644 index 00000000..3164ae06 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Portrait.tsx @@ -0,0 +1,77 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Progress } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getImplicitPortrait, +} from '@/api/memory' + +interface Item { + dimension_name: string; + percentage: number; + evidence: string[]; + reasoning: string; + confidence_level: string; +} +interface PortraitItem { + user_id: string; + analysis_timestamp: number | string; + total_summaries_analyzed: number; + historical_trends: null; + creativity: Item; + aesthetic: Item; + technology: Item; + literature: Item; +} + +const Portrait: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as PortraitItem) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + const getData = () => { + if (!id) return + setLoading(true) + getImplicitPortrait(id).then((res) => { + const response = res as PortraitItem + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + :
+ {(['aesthetic', 'creativity', 'literature', 'technology'] as const).map((key) => { + const item = data[key] as Item + return ( +
+
+
{t(`implicitDetail.${key}`)}
+ {item?.percentage ?? 0}% +
+ +
+ ) + })} +
+ } +
+ ) +} +export default Portrait \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/Preferences.tsx b/web/src/views/UserMemoryDetail/components/Preferences.tsx new file mode 100644 index 00000000..3d1372de --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Preferences.tsx @@ -0,0 +1,183 @@ +import { type FC, useEffect, useState, useRef, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Row, Col, Skeleton } from 'antd' +import * as echarts from 'echarts' +import 'echarts-wordcloud' + +import Empty from '@/components/Empty' +import RbCard from '@/components/RbCard/Card' +import { getImplicitPreferences } from '@/api/memory' + +interface PreferenceItem { + tag_name: string; + confidence_score: number; + supporting_evidence: string[]; + context_details: string; + created_at: number | string; // TODO + updated_at: number | string; // TODO + conversation_references: string[]; + category: string; +} + +const DEFAULT_COLORS = ['#FF5D34', '#155EEF', '#9C6FFF', '#369F21', '#4DA8FF', '#FF8C00', '#32CD32', '#FF69B4', '#20B2AA', '#DDA0DD'] + +const generateCategoryColors = (categories: string[]) => { + const colors: Record = {} + categories.forEach((category, index) => { + colors[category] = DEFAULT_COLORS[index % DEFAULT_COLORS.length] + }) + return colors +} + +const Preferences: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const chartRef = useRef(null) + const chartInstance = useRef(null) + const [selectedWord, setSelectedWord] = useState(null) + const [loading, setLoading] = useState(false) + const [data, setData] = useState([]) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + const getData = () => { + if (!id) { + return + } + setLoading(true) + setSelectedWord(null) + getImplicitPreferences(id) + .then((res) => { + setData(res as PreferenceItem[]) + }) + .finally(() => { + setLoading(false) + }) + } + + const uniqueCategories = [...new Set(data.map(item => item.category).filter(Boolean))] + const categoryColors = generateCategoryColors(uniqueCategories) + + const getCategoryColor = (category: string) => { + return categoryColors[category] || '#4DA8FF' + } + + useEffect(() => { + if (!chartRef.current || !data.length) return + + if (chartInstance.current) { + chartInstance.current.dispose() + } + + chartInstance.current = echarts.init(chartRef.current) + + const wordCloudData = data.map((item, index) => ({ + name: item.tag_name, + value: Math.round(item.confidence_score * 100), + itemIndex: index, + textStyle: { + color: getCategoryColor(item.category) + } + })) + + const option = { + series: [{ + type: 'wordCloud', + gridSize: 8, + sizeRange: [14, 60], + rotationRange: [-45, 45], + shape: 'pentagon', + width: '100%', + height: '100%', + textStyle: { + fontFamily: 'sans-serif', + fontWeight: 'bold' + }, + emphasis: { + textStyle: { + shadowBlur: 10, + shadowColor: '#333' + } + }, + data: wordCloudData + }] + } + + chartInstance.current.setOption(option) + + chartInstance.current.on('click', (params) => { + const clickedIndex = (params.data as any).itemIndex + if (selectedWord !== clickedIndex) { + setSelectedWord(clickedIndex) + } + + // Highlight selected word without redrawing + chartInstance.current?.dispatchAction({ + type: 'highlight', + dataIndex: clickedIndex + }) + }) + + return () => { + if (chartInstance.current) { + chartInstance.current.dispose() + chartInstance.current = null + } + } + }, [data]) + + + console.log(selectedWord, data) + + const detailTitle = useMemo(() => { + return selectedWord !== null && data[selectedWord].tag_name ? <>{data[selectedWord].tag_name}{t('implicitDetail.preferencesDetail')} : '' + }, [selectedWord, data, t]) + + return ( + <> +
{t('forgetDetail.overviewTitle')}
+ +
+ + {loading + ? + : data && data.length > 0 + ?
+ : + } + + +
+ + {selectedWord === null + ? + : <> +
{t('implicitDetail.context_details')}
+
{data[selectedWord].context_details}
+ +
{t('implicitDetail.supporting_evidence')}
+ {data[selectedWord].supporting_evidence.map((vo, index) =>
-{vo}
)} + + } +
+ + + + ) +} + +export default Preferences \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx index db7c9e57..4cce1100 100644 --- a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx +++ b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx @@ -1,17 +1,18 @@ import React, { type FC, useEffect, useState, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' -import { Col, Row } from 'antd' +import { Col, Row, Space, Button } from 'antd' import dayjs from 'dayjs' import RbCard from '@/components/RbCard/Card' import ReactEcharts from 'echarts-for-react' import detailEmpty from '@/assets/images/userMemory/detail_empty.png' -import type { Node, Edge, GraphData } from '../types' +import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties } from '../types' import { getMemorySearchEdges, } from '@/api/memory' import Empty from '@/components/Empty' +import Tag from '@/components/Tag' const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048'] const RelationshipNetwork:FC = () => { @@ -136,6 +137,11 @@ const RelationshipNetwork:FC = () => { console.log('selectedNode', selectedNode) + const handleViewAll = () => { + if (!selectedNode) return + window.open(`/#/graph/${selectedNode.id}`); + } + return ( {/* 关系网络 */} @@ -240,8 +246,14 @@ const RelationshipNetwork:FC = () => { title={t('userMemory.memoryDetails')} headerType="borderless" bodyClassName='rb:p-0!' + extra={selectedNode && } > -
+
{!selectedNode ? {
{t('userMemory.created_at')}
-
+
{dayjs(selectedNode?.properties.created_at).format('YYYY-MM-DD HH:mm:ss')}
+ + {selectedNode?.properties.associative_memory > 0 &&
+
{t('userMemory.associative_memory')}
+
+ {selectedNode?.properties.associative_memory} {t('userMemory.unix')}{t('userMemory.associative_memory')} +
+
} + + {selectedNode.label === 'Statement' && <> + {(['emotion_keywords', 'emotion_type', 'emotion_subject', 'importance_score'] as const).map(key => { + const statementProps = selectedNode.properties as StatementNodeProperties; + if ((key === 'emotion_keywords' && statementProps[key]?.length > 0) || statementProps[key]) { + return ( +
+ {t(`userMemory.Statement_${key}`)} +
+ {key === 'emotion_keywords' + ? {statementProps.emotion_keywords.map((vo, index) => {vo})} + : statementProps[key] + } +
+
+ ) + } + return null + })} + } + {selectedNode.label === 'ExtractedEntity' && <> + {(['name', 'entity_type', 'aliases', 'connect_strngth', 'importance_score'] as const).map(key => { + const entityProps = selectedNode.properties as ExtractedEntityNodeProperties; + if (entityProps[key]) { + return ( +
+ {t(`userMemory.ExtractedEntity_${key}`)} +
+ {entityProps[key]} +
+
+ ) + } + return null + })} + }
diff --git a/web/src/views/UserMemoryDetail/components/Timeline.tsx b/web/src/views/UserMemoryDetail/components/Timeline.tsx new file mode 100644 index 00000000..d7b9b273 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Timeline.tsx @@ -0,0 +1,82 @@ +import { type FC, useEffect, useState } from 'react' +import clsx from 'clsx' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Progress, Space, Tooltip, Divider } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getPerceptualTimeline +} from '@/api/memory' +import { formatDateTime } from '@/utils/format'; +import Empty from '@/components/Empty' + +interface TimelineItem { + id: string; + perceptual_type: number; + file_path: string; + file_name: string; + summary: string; + storage_type: number; + created_time: string | number; +} + +const KEYS = { + last_visual: ['summary', 'keywords', 'topic', 'domain', 'scene'], + last_listen: ['summary', 'keywords', 'topic', 'domain', 'speaker_count'], + last_text: ['summary', 'keywords', 'topic', 'domain', 'section_count'], +} + +const perceptual_type: Record = { + 1: 'last_visual', + 2: 'last_listen', + 3: 'last_text', +} + +const Timeline: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState([]) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + const getData = () => { + if (!id) return + setLoading(true) + getPerceptualTimeline(id).then((res) => { + const response = res as { memories: TimelineItem[] } + setData(response.memories || []) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + : data.length === 0 + ? + : + {data.map((vo, index) => ( +
+
+ {formatDateTime(vo.created_time)} + {index !== data.length - 1 && } +
+
+
{vo.summary}
+
{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}
+
+
+ ))} +
+ } +
+ ) +} +export default Timeline \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/WordCloud.tsx b/web/src/views/UserMemoryDetail/components/WordCloud.tsx index 64f2bfaa..b249a03a 100644 --- a/web/src/views/UserMemoryDetail/components/WordCloud.tsx +++ b/web/src/views/UserMemoryDetail/components/WordCloud.tsx @@ -114,7 +114,7 @@ const WordCloud: FC = () => {
{t(`statementDetail.${item.emotion_type}`)} -
{item.count}{t('statementDetail.pieces')}
+
{item.count} {t('statementDetail.pieces')}
diff --git a/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx b/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx new file mode 100644 index 00000000..4a7e4b1f --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx @@ -0,0 +1,250 @@ +import { type FC, useEffect, useState } from 'react' +import clsx from 'clsx' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Row, Col, Select, Form, Space, Skeleton, Input } from 'antd' +import RbCard from '@/components/RbCard/Card' +import { + getEpisodicOverview, + getEpisodicDetail, +} from '@/api/memory' +import { formatDateTime } from '@/utils/format' +import Tag from '@/components/Tag' +import RbAlert from '@/components/RbAlert' +import Empty from '@/components/Empty' + +interface EpisodicMemory { + id: string; + title: string; + type: string; + created_at: number; +} +interface EpisodicOverviewData { + total: number; + total_all: number; + episodic_memories: EpisodicMemory[] +} +interface EpisodicMemoryDetail { + id: string; + created_at: number; + involved_objects: string[]; + episodic_type: string; + content_records: string[]; + emotion: string; +} + +const TAG_COLORS: Record = { + conversation: "processing", + project_work: "success", + learning: "warning", + decision: "warning", + important_event: "error", +} +const BG_COLORS: Record = { + conversation: "rb:bg-[#155EEF]", + project_work: "rb:bg-[#369F21]", + learning: "rb:bg-[#FF5D34]", + decision: "rb:bg-[#FF5D34]", + important_event: "rb:bg-[#5B6167]", +} + +// Map display types to internal keys +const getTypeKey = (type: string): string => { + const typeMap: Record = { + 'Learning': 'learning', + 'Project/Work': 'project_work', + 'Conversation': 'conversation', + 'Decision': 'decision', + 'Important Event': 'important_event', + } + return typeMap[type] || type.toLowerCase().replace(/[^a-z0-9]/g, '_') +} +const EpisodicDetail: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as EpisodicOverviewData) + const values = Form.useWatch([], form) + const [detailLoading, setDetailLoading] = useState(false) + const [detail, setDetail] = useState(null) + const [selected, setSelected] = useState(null) + + useEffect(() => { + if (!id) return + // getData() + }, [id]) + + // 记忆洞察 + const getData = () => { + if (!id) return + setLoading(true) + setSelected(null) + setDetail(null) + getEpisodicOverview({ + end_user_id: id, + ...values + }).then((res) => { + const response = res as EpisodicOverviewData + setData(response) + if (response.episodic_memories.length > 0) { + setSelected(response.episodic_memories[0]) + } + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + useEffect(() => { + getData() + }, [values]) + + useEffect(() => { + getDetail() + }, [selected]) + + const getDetail = () => { + if (!selected || !selected.id) return + + setDetailLoading(true) + getEpisodicDetail({ + end_user_id: id as string, + summary_id: selected.id + }) + .then(res => { + setDetail(res as EpisodicMemoryDetail) + }) + .finally(() => { + setDetailLoading(false) + }) + } + + return ( +
+
+
{t('episodicDetail.title')}
+ +
+
+
{data.total_all ?? 0}
+ {t(`episodicDetail.total_all`)} +
+
+
+ + + +
+ + + + + + + + + + + + + + + {t('episodicDetail.curResult')} ({data.total || 0}{t('episodicDetail.unix')})} + headerType="borderless" + > + {loading + ? + : !data.episodic_memories || data.episodic_memories.length === 0 + ? + : ( + + {data.episodic_memories.map((vo, index) => ( +
setSelected(vo)} + > +
{index + 1}
+
+
{vo.title} {t(`episodicDetail.${getTypeKey(vo.type)}`)}
+
{formatDateTime(vo.created_at)}
+
+
+ ))} +
+ ) + } + +
+ +
+ + {detailLoading + ? + : !selected || !detail + ? + : ( + +
+ +
+
{t('episodicDetail.created')}
{formatDateTime(detail.created_at)}
+ +
+
{t('episodicDetail.episodic_type')}
{detail.episodic_type}
+ + {detail.involved_objects.length > 0 &&
+
{t('episodicDetail.involved_objects')}
+ {detail.involved_objects.map((vo, index) => {vo})} + } + + +
+
{t('episodicDetail.content_records')}
+ {detail.content_records.map((vo, index) =>
- {vo}
)} +
+ + {t('episodicDetail.emotion')}: {t(`statementDetail.${detail.emotion}`)} + + + ) + } + + + + + ) +} +export default EpisodicDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx index 6a59d41a..f0ba04ff 100644 --- a/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx @@ -20,7 +20,7 @@ const statusTagColors: Record { +const ForgetDetail: FC = () => { const { t } = useTranslation() const { id } = useParams() const [loading, setLoading] = useState(false) @@ -156,4 +156,4 @@ const ForgetOverview: FC = () => { ) } -export default ForgetOverview \ No newline at end of file +export default ForgetDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx new file mode 100644 index 00000000..f3cf716c --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx @@ -0,0 +1,14 @@ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Row, Col } from 'antd' + +const GraphDetail: FC = () => { + const { t } = useTranslation() + + return ( +
+ GraphDetail +
+ ) +} +export default GraphDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx new file mode 100644 index 00000000..ef23463a --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx @@ -0,0 +1,34 @@ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Row, Col } from 'antd' + +import Preferences from '../components/Preferences' +import Portrait from '../components/Portrait' +import InterestAreas from '../components/InterestAreas' +import Habits from '../components/Habits' + +const ImplicitDetail: FC = () => { + const { t } = useTranslation() + + return ( +
+
{t('implicitDetail.title')}
+ + + +
{t('implicitDetail.portraitTitle')}
+
{t('implicitDetail.portraitSubTitle')}
+ +
+ + + + + + + + + + ) +} +export default ImplicitDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx b/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx new file mode 100644 index 00000000..7e2d5353 --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx @@ -0,0 +1,32 @@ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Row, Col } from 'antd' + +import PerceptualLastInfo from '../components/PerceptualLastInfo' +import Timeline from '../components/Timeline' + +const PerceptualDetail: FC = () => { + const { t } = useTranslation() + + return ( +
+
{t('perceptualDetail.lastInfo')}
+ + +
+ + + + + + + + + + +
{t('perceptualDetail.timeLine')}
+ + + ) +} +export default PerceptualDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx b/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx new file mode 100644 index 00000000..6cc8eafc --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx @@ -0,0 +1,114 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Space, Skeleton } from 'antd' +import { + getShortTerm, +} from '@/api/memory' +import Empty from '@/components/Empty' + +interface ShortTermItem { + retrieval: Array<{ query: string; retrieval: string[]; }>; + message: string; + answer: string; +} +interface LongTermItem { + query: string; + retrieval: string; +} +interface ShortData { + short_term: ShortTermItem[]; + long_term: LongTermItem[]; + entity: number; + retrieval_number: number; + long_term_number: number; +} +const ShortTermDetail: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as ShortData) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + const getData = () => { + if (!id) return + setLoading(true) + getShortTerm(id).then((res) => { + const response = res as ShortData + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( +
+
+
{t('shortTermDetail.title')}
+ +
+ {(['retrieval_number', 'entity', 'long_term_number'] as const).map(key => ( +
+
{(data as any)[key] ?? 0}
+ {t(`shortTermDetail.${key}`)} +
+ ))} +
+
+ + +
{t('shortTermDetail.shortTermTitle')}
+
{t('shortTermDetail.shortTermSubTitle')}
+ + {loading + ? + : !data.short_term || data.short_term.length === 0 + ? + :data.short_term?.map((vo, voIdx) => ( +
+
{vo.message}
+ + {vo.retrieval.map((item, index) => ( +
+
{t('shortTermDetail.query')}: {item.query}
+
{t('shortTermDetail.answer')}:
+ {item.retrieval.length > 0 ? item.retrieval.map((retrieval, retrievalIdx) => ( +
- {retrieval}
+ )) :
{t('shortTermDetail.noAnswer')}
} +
+ ))} +
+
{t('shortTermDetail.answer')}
+
{vo.answer}
+
+
+
+ )) + } +
+ +
{t('shortTermDetail.longTermTitle')}
+
{t('shortTermDetail.shortTermSubTitle')}
+ + {loading + ? + : !data.long_term || data.long_term.length === 0 + ? + : data.long_term?.map((vo, voIdx) => ( +
+
{vo.query}
+
{vo.retrieval}
+
+ )) + } +
+
+ ) +} +export default ShortTermDetail \ 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 6b78a210..da62c14e 100644 --- a/web/src/views/UserMemoryDetail/pages/index.tsx +++ b/web/src/views/UserMemoryDetail/pages/index.tsx @@ -1,15 +1,23 @@ -import { type FC, useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' +import { type FC, useEffect, useState, useMemo } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { Dropdown } from 'antd' import PageHeader from '../components/PageHeader' import StatementDetail from './StatementDetail' import ForgetDetail from './ForgetDetail' +import ImplicitDetail from './ImplicitDetail' +import ShortTermDetail from './ShortTermDetail' +import PerceptualDetail from './PerceptualDetail' +import EpisodicDetail from './EpisodicDetail' import { getEndUserProfile, } from '@/api/memory' const Detail: FC = () => { + const { t } = useTranslation() const { id, type } = useParams() + const navigate = useNavigate() const [name, setName] = useState('') useEffect(() => { if (!id) return @@ -23,17 +31,37 @@ const Detail: FC = () => { setName(response.other_name || response.id) }) } + const items = useMemo(() => { + return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGETTING_MANAGEMENT'] + .map(key => ({ key, label: t(`userMemory.${key}`) })) + }, [t]) + const onClick = ({ key }: { key: string }) => { + navigate(`/user-memory/detail/${id}/${key}`, { replace: true }) + } - console.log('Detail', name) return (
+
+ - {type ? t(`userMemory.${type}`) : ''} +
+
+ + } />
{type === 'EMOTIONAL_MEMORY' && } {type === 'FORGETTING_MANAGEMENT' && } + {type === 'IMPLICIT_MEMORY' && } + {type === 'SHORT_TERM_MEMORY' && } + {type === 'PERCEPTUAL_MEMORY' && } {/** TODO */} + {type === 'EPISODIC_MEMORY' && }
) diff --git a/web/src/views/UserMemoryDetail/types.ts b/web/src/views/UserMemoryDetail/types.ts index 77dd653e..263494d0 100644 --- a/web/src/views/UserMemoryDetail/types.ts +++ b/web/src/views/UserMemoryDetail/types.ts @@ -44,6 +44,7 @@ export interface Data { export interface BaseProperties { content: string; created_at: number; + associative_memory: number; } export interface StatementNodeProperties { temporal_info: string; @@ -51,12 +52,21 @@ export interface StatementNodeProperties { statement: string; valid_at: string; created_at: number; + emotion_keywords: string[]; + emotion_type: string; + emotion_subject: string; + importance_score: number; + associative_memory: number; } export interface ExtractedEntityNodeProperties { description: string; name: string; entity_type: string; created_at: number; + aliases: string; + connect_strngth: string; + importance_score: number; + associative_memory: number; } export interface MemorySummaryNode { id: string; @@ -72,7 +82,7 @@ export interface MemorySummaryNode { created_at: number; } caption: string; - + associative_memory: number; } export interface Node {