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