diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index baa57e4d..abb0be97 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -1542,6 +1542,7 @@ export const en = {
MemorySummary: 'Long-term Accumulation',
Statement: 'Emotional Memory',
ExtractedEntity: 'Episodic Memory',
+ Perceptual: 'Perceptual Memory',
positive: 'Positive Emotion',
negative: 'Negative Emotion',
neutral: 'Neutral Emotion',
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index 16cfe4e0..b5bbbfcd 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -1540,6 +1540,7 @@ export const zh = {
MemorySummary: '长期沉淀',
Statement: '情绪记忆',
ExtractedEntity: '情景记忆',
+ Perceptual: '感知记忆',
positive: '正向情绪',
negative: '负向情绪',
neutral: '中性情绪',
diff --git a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
index 6272a432..99609b58 100644
--- a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
+++ b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:23
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-20 11:07:02
+ * @Last Modified time: 2026-03-24 11:36:22
*/
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -12,6 +12,7 @@ import clsx from 'clsx'
import RbCard from '@/components/RbCard/Card'
import AudioPlayer from './AudioPlayer'
+import VideoPlayer from './VideoPlayer'
import {
getPerceptualLastVisual,
getPerceptualLastListen,
@@ -107,7 +108,7 @@ const PerceptualLastInfo: FC = () => {
})
}
- const handleDownload = async () => {
+ const handleDownload = () => {
if (!data.file_path) return
window.open(data.file_path, '_blank')
}
@@ -139,9 +140,7 @@ const PerceptualLastInfo: FC = () => {
{/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name)
?
: /\.(mp4|webm|ogg|mov)$/i.test(data.file_name)
- ?
-
-
+ ?
: /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name)
?
:
diff --git a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx
index df7d639e..a2467ff4 100644
--- a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx
+++ b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:00
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-20 12:14:43
+ * @Last Modified time: 2026-03-24 12:19:12
*/
/**
* Relationship Network Component
@@ -13,7 +13,7 @@
import React, { type FC, useEffect, useState, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams, useNavigate } from 'react-router-dom'
-import { Space, Flex, Divider, type SegmentedProps } from 'antd'
+import { Space, Flex, Divider, type SegmentedProps, Image } from 'antd'
import dayjs from 'dayjs'
import clsx from 'clsx'
@@ -27,6 +27,8 @@ import Tag from '@/components/Tag'
import GraphNetworkChart, { type Node, type Edge } from '@/components/Charts/GraphNetworkChart'
import CommunityNetwork from './CommunityNetwork'
import PageTabs from '@/components/PageTabs'
+import AudioPlayer from './AudioPlayer'
+import VideoPlayer from './VideoPlayer'
const RelationshipNetwork: FC = () => {
const { t } = useTranslation()
@@ -149,6 +151,26 @@ const RelationshipNetwork: FC = () => {
setSelectedNode(null)
}
+ const [fileSize, setFileSize] = useState('')
+ useEffect(() => {
+ setFileSize('')
+ if (selectedNode && 'file_path' in selectedNode.properties && selectedNode.properties.file_path) {
+ fetch(selectedNode.properties.file_path, { method: 'HEAD' })
+ .then(r => {
+ const bytes = Number(r.headers.get('content-length'))
+ if (!bytes) return
+ setFileSize(bytes < 1024 * 1024
+ ? `${(bytes / 1024).toFixed(1)} KB`
+ : `${(bytes / 1024 / 1024).toFixed(1)} MB`)
+ })
+ .catch(() => {})
+ }
+ }, [selectedNode])
+ const handleDownload = () => {
+ if (!selectedNode?.properties?.file_path) return
+ window.open(selectedNode?.properties?.file_path, '_blank')
+ }
+
return (
@@ -229,6 +251,8 @@ const RelationshipNetwork: FC = () => {
? selectedNode.properties.description
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
? selectedNode.properties.statement
+ : selectedNode.label === 'Perceptual' && 'summary' in selectedNode.properties
+ ? selectedNode.properties.summary
: ''
}
@@ -285,6 +309,30 @@ const RelationshipNetwork: FC = () => {
return null
})}
>}
+ {selectedNode.label === 'Perceptual' && <>
+ {selectedNode.properties.file_type.includes('image')
+ ?
+ : selectedNode.properties.file_type.includes('video')
+ ?
+ : selectedNode.properties.file_type.includes('audio')
+ ?
+ :
+
+
+
+
{selectedNode.properties.file_name}
+
+ {fileSize || '-'}
+
+
+
+
+
+ }
+ >}
>}
diff --git a/web/src/views/UserMemoryDetail/components/VideoPlayer.tsx b/web/src/views/UserMemoryDetail/components/VideoPlayer.tsx
new file mode 100644
index 00000000..63b369af
--- /dev/null
+++ b/web/src/views/UserMemoryDetail/components/VideoPlayer.tsx
@@ -0,0 +1,62 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-03-24 12:21:56
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-03-24 12:21:56
+ */
+import { type FC, useRef, useState } from 'react'
+import { CloseOutlined } from '@ant-design/icons'
+interface VideoPlayerProps {
+ src: string
+}
+
+const VideoPlayer: FC = ({ src }) => {
+ const [open, setOpen] = useState(false)
+ const videoRef = useRef(null)
+
+ const handleOpen = () => setOpen(true)
+
+ const handleClose = () => {
+ videoRef.current?.pause()
+ setOpen(false)
+ }
+
+ return (
+ <>
+ {/* Thumbnail with play overlay */}
+
+
+ {/* Fullscreen modal */}
+ {open && (
+
+
+
+ )}
+ >
+ )
+}
+
+export default VideoPlayer