Merge pull request #974 from SuanmoSuanyangTechnology/feature/memory_zy
feat(web): explicit memory api
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: '对话',
|
||||||
|
|||||||
@@ -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 />
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user