From f8ef32c1dd1d2b7a10678d39044f14bf6d229363 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 22 Apr 2026 17:26:29 +0800 Subject: [PATCH 1/2] feat(web): explicit memory api --- web/src/api/memory.ts | 23 +- web/src/i18n/en.ts | 6 + web/src/i18n/zh.ts | 6 + .../UserMemoryDetail/pages/ExplicitDetail.tsx | 196 ++++++++++++------ 4 files changed, 166 insertions(+), 65 deletions(-) diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 77801c63..c5120cb2 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -21,6 +21,7 @@ import type { import type { ConfigForm as SelfReflectionEngineConfig } from '@/views/SelfReflectionEngine/types' +import type { EpisodicMemoryType } from '@/views/UserMemoryDetail/pages/ExplicitDetail' import type { TestParams } from '@/views/MemoryConversation' import type { EndUser } from '@/views/UserMemoryDetail/types' import { handleSSE, type SSEMessage } from '@/utils/stream' @@ -87,11 +88,11 @@ export const getUserSummary = (end_user_id: string) => { export const getNodeStatistics = (end_user_id: string) => { return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id }) } -// 查询用户别名及信息 +// Get user alias and info export const getEndUserInfo = (end_user_id: string) => { return request.get(`/memory-storage/end_user_info`, { end_user_id }) } -// 更新用户别名及信息 +// Update user alias and info export const updatedEndUserInfo = (values: EndUser) => { return request.post(`/memory-storage/end_user_info/updated`, values) } @@ -154,7 +155,7 @@ export const analyticsRefresh = (end_user_id: string) => { export const getForgetStats = (end_user_id: string) => { return request.get(`/memory/forget-memory/stats`, { end_user_id }) } -// 获取带遗忘节点列表 +// Get pending forgetting nodes list export const getForgetPendingNodesUrl = '/memory/forget-memory/pending-nodes' // Implicit Memory - Preferences export const getImplicitPreferences = (end_user_id: string) => { @@ -218,6 +219,22 @@ export const getTimelineMemories = (data: { id: string; label: string; }) => { export const getExplicitMemory = (end_user_id: string) => { return request.post(`/memory/explicit-memory/overview`, { end_user_id }) } +export interface EpisodicMemoryQuery { + end_user_id?: string; + page?: number; + pagesize?: number; + start_date?: number; + end_date?: number; + episodic_type?: EpisodicMemoryType; +} +// Explicit Memory - Episodic memory paginated query +export const getEpisodicMemory = (data: EpisodicMemoryQuery) => { + return request.get(`/memory/explicit-memory/episodics`, data) +} +// Explicit Memory - Get user semantic memory list +export const getSemanticsMemory = (end_user_id: string) => { + return request.get(`/memory/explicit-memory/semantics`, { end_user_id }) +} export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => { return request.post(`/memory/explicit-memory/details`, data) } diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index cbdf921d..032b63db 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2947,6 +2947,12 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re emotion: 'Emotion', core_definition: 'Core Definition', detailed_notes: 'Detailed Notes', + episodic_type: 'Episodic Type', + conversation: 'Conversation', + project_work: 'Project/Work', + learning: 'Learning', + decision: 'Decision', + important_event: 'Important Event', }, workingDetail: { conversation: 'Conversation', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 7bd39034..505d9a74 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2911,6 +2911,12 @@ export const zh = { emotion: '情绪', core_definition: '核心定义', detailed_notes: '详细笔记', + episodic_type: '情景类型', + conversation: '对话', + project_work: '项目/工作', + learning: '学习', + decision: '决策', + important_event: '重要事件', }, workingDetail: { conversation: '对话', diff --git a/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx index 55623c7e..a8ea9368 100644 --- a/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx @@ -7,25 +7,31 @@ import { type FC, useEffect, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' -import { Skeleton, Row, Col, Flex, DatePicker, Pagination } from 'antd' +import { Skeleton, Row, Col, Flex, DatePicker, Pagination, Form, Select } from 'antd' import type { Dayjs } from 'dayjs' import * as echarts from 'echarts' import 'echarts-wordcloud' +import clsx from 'clsx' import RbCard from '@/components/RbCard/Card' import { - getExplicitMemory, + getSemanticsMemory, + getEpisodicMemory, + type EpisodicMemoryQuery, } from '@/api/memory' import { formatDateTime } from '@/utils/format' import Empty from '@/components/Empty' import ExplicitDetailModal from '../components/ExplicitDetailModal' /** An episodic (event-based) memory entry with a title and free-text content. */ + +export type EpisodicMemoryType = "conversation" | "project_work" | "learning" | "decision" | "important_event" export interface EpisodicMemory { id: string; title: string; content: string; created_at: number; + memory_type: EpisodicMemoryType; } /** A semantic (concept-based) memory entry extracted as a named entity. */ @@ -37,7 +43,6 @@ export interface SemanticMemory { entity_type: string; /** Brief definition or description of the entity. */ core_definition: string; - created_at: number; } /** Combined API response containing both memory categories. */ @@ -51,10 +56,12 @@ interface Data { export interface ExplicitDetailModalRef { handleOpen: (vo: EpisodicMemory | SemanticMemory) => void; } +interface PaginationConfig { pagesize?: number; page?: number; } /** Rotating colour palette used for word-cloud text. */ const DEFAULT_COLORS = ['#FF8A4C', '#FF5D34', '#155EEF', '#9C6FFF', '#4DA8FF', '#369F21'] +const PAGE_SIZE = 10 /** * ExplicitDetail – Two-column view of a user's explicit memories. * @@ -73,19 +80,62 @@ const ExplicitDetail: FC = () => { /** Keeps a stable reference to the ECharts instance for cleanup. */ const chartInstance = useRef(null) const [loading, setLoading] = useState(false) - const [data, setData] = useState({ episodic_memories: [], semantic_memories: [], total: 0 }) - const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null] | null>(null) - const [page, setPage] = useState(1) - const PAGE_SIZE = 10 + const [semanticsMemory, setSemanticsMemory] = useState([]) - const filteredEpisodic = dateRange?.[0] && dateRange?.[1] - ? data.episodic_memories.filter(item => { - const ts = item.created_at - return ts >= dateRange[0]!.startOf('day').valueOf() && ts <= dateRange[1]!.endOf('day').valueOf() + const [form] = Form.useForm() + const values = Form.useWatch([], form) + const [episodicLoading, setEpisodicLoading] = useState(false) + const [episodicMemories, setEpisodicMemories] = useState([]) + const [currentPagination, setCurrentPagination] = useState({ + page: 1, + pagesize: PAGE_SIZE, + }); + const [total, setTotal] = useState(0); + const [allEpisodicTotal, setAllEpisodicTotal] = useState(0) + + useEffect(() => { + getEpisodicMemoryList({ page: 1 }) + }, [values]) + + const getEpisodicMemoryList = (pagination?: PaginationConfig) => { + if (!id) return + if (pagination) { + setCurrentPagination({ + ...currentPagination, + ...pagination, }) - : data.episodic_memories + } - const pagedEpisodic = filteredEpisodic.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE) + const { range, ...rest } = values || {}; + const params = { + end_user_id: id, + ...currentPagination, + ...pagination, + ...rest + } + + if (range && range.length === 2) { + params.start_date = range[0]!.startOf('day').valueOf() + params.end_date = range[0]!.endOf('day').valueOf() + } + setEpisodicLoading(true) + getEpisodicMemory(params) + .then(res => { + const response = res as { total: number; items: EpisodicMemory[]; page: { hasnext: boolean; pagesize: number; total: number; } } + setEpisodicMemories(response.items) + setTotal(response.page.total) + setAllEpisodicTotal(response.total) + }) + .finally(() => { + setEpisodicLoading(false) + }) + } + const handlePageChange = (page: number, pagesize: number) => { + getEpisodicMemoryList({ + page: page, + pagesize + }) + } /* Fetch data whenever the route user ID changes. */ useEffect(() => { @@ -97,9 +147,8 @@ const ExplicitDetail: FC = () => { const getData = () => { if (!id) return setLoading(true) - getExplicitMemory(id).then((res) => { - const response = res as Data - setData(response) + getSemanticsMemory(id).then((res) => { + setSemanticsMemory(res as SemanticMemory[]) setLoading(false) }) .finally(() => { @@ -117,7 +166,7 @@ const ExplicitDetail: FC = () => { * The chart instance is disposed on cleanup to prevent memory leaks. */ useEffect(() => { - if (!wordCloudRef.current || !data.semantic_memories?.length) return + if (!wordCloudRef.current || !semanticsMemory?.length) return if (chartInstance.current) chartInstance.current.dispose() chartInstance.current = echarts.init(wordCloudRef.current) chartInstance.current.setOption({ @@ -131,7 +180,7 @@ const ExplicitDetail: FC = () => { height: '100%', textStyle: { fontFamily: 'sans-serif', fontWeight: 'bold' }, emphasis: { textStyle: { shadowBlur: 10, shadowColor: '#333' } }, - data: data.semantic_memories.map((item, index) => ({ + data: semanticsMemory.map((item, index) => ({ name: item.name, value: 50 + (index % 5) * 10, itemIndex: index, @@ -140,11 +189,11 @@ const ExplicitDetail: FC = () => { }] }) chartInstance.current.on('click', (params) => { - const item = data.semantic_memories[(params.data as any).itemIndex] + const item = semanticsMemory[(params.data as any).itemIndex] if (item) handleView(item) }) return () => { chartInstance.current?.dispose(); chartInstance.current = null } - }, [data.semantic_memories]) + }, [semanticsMemory]) /* Redraw the word cloud when the container dimensions change. */ useEffect(() => { @@ -168,55 +217,78 @@ const ExplicitDetail: FC = () => { {t('explicitDetail.episodic_memories')}} - extra={{t('table.totalRecords', { total: data.total })}} + extra={{t('table.totalRecords', { total: allEpisodicTotal })}} headerType="borderless" headerClassName="rb:min-h-[50px]!" bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-50px)]" className="rb:h-full!" > - {loading ? - - : ( - - - + +
+ + + { setDateRange(val); setPage(1) }} allowClear + className="rb:w-full!" /> - - -
- {pagedEpisodic.length > 0 ? pagedEpisodic.map(item => ( -
handleView(item)} - > - - {item.title} -
{formatDateTime(item.created_at)}
-
-
{item.content}
-
- )) : } -
- {filteredEpisodic.length > PAGE_SIZE && ( - - )} - - ) - } + + + + +