+
{!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 {