Merge pull request #974 from SuanmoSuanyangTechnology/feature/memory_zy

feat(web): explicit memory api
This commit is contained in:
yingzhao
2026-04-22 17:35:20 +08:00
committed by GitHub
4 changed files with 167 additions and 65 deletions

View File

@@ -87,11 +87,11 @@ export const getUserSummary = (end_user_id: string) => {
export const getNodeStatistics = (end_user_id: string) => { export const getNodeStatistics = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id }) return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id })
} }
// 查询用户别名及信息 // Get user alias and info
export const getEndUserInfo = (end_user_id: string) => { export const getEndUserInfo = (end_user_id: string) => {
return request.get(`/memory-storage/end_user_info`, { end_user_id }) return request.get(`/memory-storage/end_user_info`, { end_user_id })
} }
// 更新用户别名及信息 // Update user alias and info
export const updatedEndUserInfo = (values: EndUser) => { export const updatedEndUserInfo = (values: EndUser) => {
return request.post(`/memory-storage/end_user_info/updated`, values) return request.post(`/memory-storage/end_user_info/updated`, values)
} }
@@ -154,7 +154,7 @@ export const analyticsRefresh = (end_user_id: string) => {
export const getForgetStats = (end_user_id: string) => { export const getForgetStats = (end_user_id: string) => {
return request.get(`/memory/forget-memory/stats`, { end_user_id }) return request.get(`/memory/forget-memory/stats`, { end_user_id })
} }
// 获取带遗忘节点列表 // Get pending forgetting nodes list
export const getForgetPendingNodesUrl = '/memory/forget-memory/pending-nodes' export const getForgetPendingNodesUrl = '/memory/forget-memory/pending-nodes'
// Implicit Memory - Preferences // Implicit Memory - Preferences
export const getImplicitPreferences = (end_user_id: string) => { export const getImplicitPreferences = (end_user_id: string) => {
@@ -218,6 +218,24 @@ export const getTimelineMemories = (data: { id: string; label: string; }) => {
export const getExplicitMemory = (end_user_id: string) => { export const getExplicitMemory = (end_user_id: string) => {
return request.post(`/memory/explicit-memory/overview`, { end_user_id }) return request.post(`/memory/explicit-memory/overview`, { end_user_id })
} }
export type EpisodicMemoryType = "conversation" | "project_work" | "learning" | "decision" | "important_event"
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; }) => { export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => {
return request.post(`/memory/explicit-memory/details`, data) return request.post(`/memory/explicit-memory/details`, data)
} }

View File

@@ -2947,6 +2947,12 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
emotion: 'Emotion', emotion: 'Emotion',
core_definition: 'Core Definition', core_definition: 'Core Definition',
detailed_notes: 'Detailed Notes', detailed_notes: 'Detailed Notes',
episodic_type: 'Episodic Type',
conversation: 'Conversation',
project_work: 'Project/Work',
learning: 'Learning',
decision: 'Decision',
important_event: 'Important Event',
}, },
workingDetail: { workingDetail: {
conversation: 'Conversation', conversation: 'Conversation',

View File

@@ -2911,6 +2911,12 @@ export const zh = {
emotion: '情绪', emotion: '情绪',
core_definition: '核心定义', core_definition: '核心定义',
detailed_notes: '详细笔记', detailed_notes: '详细笔记',
episodic_type: '情景类型',
conversation: '对话',
project_work: '项目/工作',
learning: '学习',
decision: '决策',
important_event: '重要事件',
}, },
workingDetail: { workingDetail: {
conversation: '对话', conversation: '对话',

View File

@@ -7,25 +7,31 @@
import { type FC, useEffect, useState, useRef } from 'react' import { type FC, useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' 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 type { Dayjs } from 'dayjs'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import 'echarts-wordcloud' import 'echarts-wordcloud'
import clsx from 'clsx'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import { import {
getExplicitMemory, getSemanticsMemory,
getEpisodicMemory,
type EpisodicMemoryQuery,
type EpisodicMemoryType,
} from '@/api/memory' } from '@/api/memory'
import { formatDateTime } from '@/utils/format' import { formatDateTime } from '@/utils/format'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import ExplicitDetailModal from '../components/ExplicitDetailModal' import ExplicitDetailModal from '../components/ExplicitDetailModal'
/** An episodic (event-based) memory entry with a title and free-text content. */ /** An episodic (event-based) memory entry with a title and free-text content. */
export interface EpisodicMemory { export interface EpisodicMemory {
id: string; id: string;
title: string; title: string;
content: string; content: string;
created_at: number; created_at: number;
memory_type: EpisodicMemoryType;
} }
/** A semantic (concept-based) memory entry extracted as a named entity. */ /** A semantic (concept-based) memory entry extracted as a named entity. */
@@ -37,7 +43,6 @@ export interface SemanticMemory {
entity_type: string; entity_type: string;
/** Brief definition or description of the entity. */ /** Brief definition or description of the entity. */
core_definition: string; core_definition: string;
created_at: number;
} }
/** Combined API response containing both memory categories. */ /** Combined API response containing both memory categories. */
@@ -51,10 +56,12 @@ interface Data {
export interface ExplicitDetailModalRef { export interface ExplicitDetailModalRef {
handleOpen: (vo: EpisodicMemory | SemanticMemory) => void; handleOpen: (vo: EpisodicMemory | SemanticMemory) => void;
} }
interface PaginationConfig { pagesize?: number; page?: number; }
/** Rotating colour palette used for word-cloud text. */ /** Rotating colour palette used for word-cloud text. */
const DEFAULT_COLORS = ['#FF8A4C', '#FF5D34', '#155EEF', '#9C6FFF', '#4DA8FF', '#369F21'] const DEFAULT_COLORS = ['#FF8A4C', '#FF5D34', '#155EEF', '#9C6FFF', '#4DA8FF', '#369F21']
const PAGE_SIZE = 10
/** /**
* ExplicitDetail Two-column view of a user's explicit memories. * 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. */ /** Keeps a stable reference to the ECharts instance for cleanup. */
const chartInstance = useRef<echarts.ECharts | null>(null) const chartInstance = useRef<echarts.ECharts | null>(null)
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<Data>({ episodic_memories: [], semantic_memories: [], total: 0 }) const [semanticsMemory, setSemanticsMemory] = useState<SemanticMemory[]>([])
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null] | null>(null)
const [page, setPage] = useState(1)
const PAGE_SIZE = 10
const filteredEpisodic = dateRange?.[0] && dateRange?.[1] const [form] = Form.useForm<EpisodicMemoryQuery & { range?: [Dayjs, Dayjs] | null }>()
? data.episodic_memories.filter(item => { const values = Form.useWatch([], form)
const ts = item.created_at const [episodicLoading, setEpisodicLoading] = useState(false)
return ts >= dateRange[0]!.startOf('day').valueOf() && ts <= dateRange[1]!.endOf('day').valueOf() const [episodicMemories, setEpisodicMemories] = useState<Data['episodic_memories']>([])
const [currentPagination, setCurrentPagination] = useState<PaginationConfig>({
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[1]!.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. */ /* Fetch data whenever the route user ID changes. */
useEffect(() => { useEffect(() => {
@@ -97,9 +147,8 @@ const ExplicitDetail: FC = () => {
const getData = () => { const getData = () => {
if (!id) return if (!id) return
setLoading(true) setLoading(true)
getExplicitMemory(id).then((res) => { getSemanticsMemory(id).then((res) => {
const response = res as Data setSemanticsMemory(res as SemanticMemory[])
setData(response)
setLoading(false) setLoading(false)
}) })
.finally(() => { .finally(() => {
@@ -117,7 +166,7 @@ const ExplicitDetail: FC = () => {
* The chart instance is disposed on cleanup to prevent memory leaks. * The chart instance is disposed on cleanup to prevent memory leaks.
*/ */
useEffect(() => { useEffect(() => {
if (!wordCloudRef.current || !data.semantic_memories?.length) return if (!wordCloudRef.current || !semanticsMemory?.length) return
if (chartInstance.current) chartInstance.current.dispose() if (chartInstance.current) chartInstance.current.dispose()
chartInstance.current = echarts.init(wordCloudRef.current) chartInstance.current = echarts.init(wordCloudRef.current)
chartInstance.current.setOption({ chartInstance.current.setOption({
@@ -131,7 +180,7 @@ const ExplicitDetail: FC = () => {
height: '100%', height: '100%',
textStyle: { fontFamily: 'sans-serif', fontWeight: 'bold' }, textStyle: { fontFamily: 'sans-serif', fontWeight: 'bold' },
emphasis: { textStyle: { shadowBlur: 10, shadowColor: '#333' } }, emphasis: { textStyle: { shadowBlur: 10, shadowColor: '#333' } },
data: data.semantic_memories.map((item, index) => ({ data: semanticsMemory.map((item, index) => ({
name: item.name, name: item.name,
value: 50 + (index % 5) * 10, value: 50 + (index % 5) * 10,
itemIndex: index, itemIndex: index,
@@ -140,11 +189,11 @@ const ExplicitDetail: FC = () => {
}] }]
}) })
chartInstance.current.on('click', (params) => { 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) if (item) handleView(item)
}) })
return () => { chartInstance.current?.dispose(); chartInstance.current = null } return () => { chartInstance.current?.dispose(); chartInstance.current = null }
}, [data.semantic_memories]) }, [semanticsMemory])
/* Redraw the word cloud when the container dimensions change. */ /* Redraw the word cloud when the container dimensions change. */
useEffect(() => { useEffect(() => {
@@ -168,55 +217,78 @@ const ExplicitDetail: FC = () => {
<Col span={12} className="rb:h-full!"> <Col span={12} className="rb:h-full!">
<RbCard <RbCard
title={() => <span className="rb:font-[MiSans-Bold] rb:font-bold">{t('explicitDetail.episodic_memories')}</span>} title={() => <span className="rb:font-[MiSans-Bold] rb:font-bold">{t('explicitDetail.episodic_memories')}</span>}
extra={<span className="rb:text-[#5B6167]">{t('table.totalRecords', { total: data.total })}</span>} extra={<span className="rb:text-[#5B6167]">{t('table.totalRecords', { total: allEpisodicTotal })}</span>}
headerType="borderless" headerType="borderless"
headerClassName="rb:min-h-[50px]!" headerClassName="rb:min-h-[50px]!"
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-50px)]" bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-50px)]"
className="rb:h-full!" className="rb:h-full!"
> >
{loading ? <Flex vertical gap={12} className="rb:h-full!">
<Skeleton active /> <Form form={form} initialValues={{ episodic_type: null }}>
: ( <Row gutter={12}>
<Flex gap={12} vertical className="rb:h-full!"> <Col span={12}>
<Row gutter={12}> <Form.Item name="range" noStyle>
<Col span={12}>
<DatePicker.RangePicker <DatePicker.RangePicker
value={dateRange}
onChange={(val) => { setDateRange(val); setPage(1) }}
allowClear allowClear
className="rb:w-full!"
/> />
</Col> </Form.Item>
</Row> </Col>
<div className="rb:max-h-[calc(100%-92px)] rb:overflow-y-auto"> <Col span={12}>
{pagedEpisodic.length > 0 ? pagedEpisodic.map(item => ( <Form.Item name="episodic_type" noStyle>
<div <Select
key={item.id} options={[
className="rb:cursor-pointer rb:bg-[#F6F6F6] rb:rounded-xl rb:pt-2.5 rb:px-3 rb:pb-3" { value: null, label: t('common.all') },
onClick={() => handleView(item)} ...["conversation", "project_work", "learning", "decision", "important_event"].map(type => ({
> value: type, label: t(`explicitDetail.${type}`)
<Flex align="center" justify="space-between"> }))
<span className="rb:font-medium rb:pl-1">{item.title}</span> ]}
<div className="rb:text-[#5B6167] rb:leading-4.25 rb:text-[12px]">{formatDateTime(item.created_at)}</div> placeholder={t('explicitDetail.episodic_type')}
</Flex> className="rb:w-full!"
<div className="rb:bg-white rb:rounded-lg rb:py-2.5 rb:px-3 rb:mt-2.5 rb:leading-5">{item.content}</div> />
</div> </Form.Item>
)) : <Empty />} </Col>
</div> </Row>
{filteredEpisodic.length > PAGE_SIZE && ( </Form>
<Pagination {episodicLoading ?
current={page} <Skeleton active />
pageSize={PAGE_SIZE} : (
total={filteredEpisodic.length} <>
onChange={setPage} <Flex vertical gap={12} className={clsx(" rb:overflow-y-auto", {
size="small" 'rb:max-h-[calc(100%-92px)]!': total > PAGE_SIZE,
showSizeChanger={true} 'rb:max-h-[calc(100%-36px)]!': total <= PAGE_SIZE && episodicMemories.length > 0,
showQuickJumper={true} 'rb:h-full!': episodicMemories.length === 0
className="rb:mt-1!" })}>
/> {episodicMemories.length > 0 ? episodicMemories.map(item => (
)} <div
</Flex> key={item.id}
) className="rb:cursor-pointer rb:bg-[#F6F6F6] rb:rounded-xl rb:pt-2.5 rb:px-3 rb:pb-3"
} onClick={() => handleView(item)}
>
<Flex align="center" justify="space-between">
<span className="rb:font-medium rb:pl-1">{item.title}</span>
<div className="rb:text-[#5B6167] rb:leading-4.25 rb:text-[12px]">{formatDateTime(item.created_at)}</div>
</Flex>
<div className="rb:bg-white rb:rounded-lg rb:py-2.5 rb:px-3 rb:mt-2.5 rb:leading-5">{item.content}</div>
</div>
)) : <Empty className="rb:h-full!" />}
</Flex>
{total > PAGE_SIZE && (
<Pagination
current={currentPagination.page}
pageSize={PAGE_SIZE}
total={total}
onChange={handlePageChange}
size="small"
showSizeChanger={true}
showQuickJumper={true}
className="rb:mt-1!"
/>
)}
</>
)
}
</Flex>
</RbCard> </RbCard>
</Col> </Col>
<Col span={12} className="rb:h-full!"> <Col span={12} className="rb:h-full!">
@@ -229,7 +301,7 @@ const ExplicitDetail: FC = () => {
> >
{loading ? {loading ?
<Skeleton active /> <Skeleton active />
: data.semantic_memories?.length > 0 : semanticsMemory?.length > 0
? <div ref={wordCloudRef} className="rb:h-full rb:w-full rb:cursor-pointer" /> ? <div ref={wordCloudRef} className="rb:h-full rb:w-full rb:cursor-pointer" />
: <Empty /> : <Empty />
} }