feat(web): add perceptual node
This commit is contained in:
@@ -62,8 +62,8 @@
|
|||||||
</g>
|
</g>
|
||||||
<path d="M21,0 L30,9 L23,9 C21.8954305,9 21,8.1045695 21,7 L21,0 L21,0 Z" id="矩形" fill="url(#linearGradient-7)"></path>
|
<path d="M21,0 L30,9 L23,9 C21.8954305,9 21,8.1045695 21,7 L21,0 L21,0 Z" id="矩形" fill="url(#linearGradient-7)"></path>
|
||||||
<rect id="矩形" fill="#155EEF" x="2" y="25" width="26" height="9" rx="3"></rect>
|
<rect id="矩形" fill="#155EEF" x="2" y="25" width="26" height="9" rx="3"></rect>
|
||||||
<text id="DOC" font-family="MiSans-Semibold, MiSans" font-size="8" font-weight="400" line-spacing="8" fill="#FFFFFF">
|
<text id="text" font-family="MiSans-Semibold, MiSans" font-size="8" font-weight="400" line-spacing="8" fill="#FFFFFF">
|
||||||
<tspan x="5" y="33.5">DOC</tspan>
|
<tspan x="4" y="33.5">TEXT</tspan>
|
||||||
</text>
|
</text>
|
||||||
<g id="矩形" fill-rule="nonzero">
|
<g id="矩形" fill-rule="nonzero">
|
||||||
<use fill="black" fill-opacity="1" filter="url(#filter-10)" xlink:href="#path-9"></use>
|
<use fill="black" fill-opacity="1" filter="url(#filter-10)" xlink:href="#path-9"></use>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
15
web/src/assets/images/userMemory/play_opacity.svg
Normal file
15
web/src/assets/images/userMemory/play_opacity.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<title>播放</title>
|
||||||
|
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="记忆库-个人记忆-感知记忆-视觉" transform="translate(-232, -238)" fill-rule="nonzero">
|
||||||
|
<g id="视觉" transform="translate(28, 168)">
|
||||||
|
<g id="播放" transform="translate(204, 70)">
|
||||||
|
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="40" height="40"></rect>
|
||||||
|
<path d="M20,0 C9,0 0,9 0,20 C0,31 9,40 20,40 C31,40 40,31 40,20 C40,9 31,0 20,0 Z" id="路径" fill-opacity="0.5" fill="#171719"></path>
|
||||||
|
<path d="M26.25,17.25 L19.5833203,12.8333203 C17.3333203,11.3333203 15.4166797,12.3333203 15.4166797,15.0833203 L15.4166797,25.0833203 C15.4166797,27.8333203 17.25,28.8333203 19.5833203,27.3333203 L26.25,22.9166797 C28.5,21.25 28.5,18.75 26.25,17.25 Z" id="路径" fill="#FFFFFF"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,8 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-03-13 15:17:06
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-03-24 12:19:57
|
||||||
|
*/
|
||||||
import React, { useState, useRef, useMemo, useEffect, type FC } from 'react'
|
import React, { useState, useRef, useMemo, useEffect, type FC } from 'react'
|
||||||
import Empty from '@/components/Empty'
|
|
||||||
import { GRAPH_COLORS, initCommunityGraph } from './utils'
|
import { GRAPH_COLORS, initCommunityGraph } from './utils'
|
||||||
import { useD3Graph } from './hooks'
|
import { useD3Graph } from './hooks'
|
||||||
import type { CommunityD3Node, D3Link, CommunityGraphProps } from './types'
|
import type { CommunityD3Node, D3Link, CommunityGraphProps } from './types'
|
||||||
|
import PageEmpty from '@/components/Empty/PageEmpty'
|
||||||
|
|
||||||
// ─── Component ────────────────────────────────────────────────────────────────
|
// ─── Component ────────────────────────────────────────────────────────────────
|
||||||
// Renders a D3-powered community graph with optional tooltip and legend.
|
// Renders a D3-powered community graph with optional tooltip and legend.
|
||||||
@@ -51,7 +58,7 @@ const CommunityGraph: FC<CommunityGraphProps> = ({
|
|||||||
? renderTooltipRef.current(tooltip.node)
|
? renderTooltipRef.current(tooltip.node)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (isEmpty) return <Empty className="rb:h-full" />
|
if (isEmpty) return <PageEmpty className="rb:h-full" />
|
||||||
return (
|
return (
|
||||||
<div className="rb:w-full rb:h-full rb:relative">
|
<div className="rb:w-full rb:h-full rb:relative">
|
||||||
<div ref={containerRef} className="rb:w-full rb:h-full" />
|
<div ref={containerRef} className="rb:w-full rb:h-full" />
|
||||||
|
|||||||
@@ -1542,6 +1542,7 @@ export const en = {
|
|||||||
MemorySummary: 'Long-term Accumulation',
|
MemorySummary: 'Long-term Accumulation',
|
||||||
Statement: 'Emotional Memory',
|
Statement: 'Emotional Memory',
|
||||||
ExtractedEntity: 'Episodic Memory',
|
ExtractedEntity: 'Episodic Memory',
|
||||||
|
Perceptual: 'Perceptual Memory',
|
||||||
positive: 'Positive Emotion',
|
positive: 'Positive Emotion',
|
||||||
negative: 'Negative Emotion',
|
negative: 'Negative Emotion',
|
||||||
neutral: 'Neutral Emotion',
|
neutral: 'Neutral Emotion',
|
||||||
|
|||||||
@@ -1540,6 +1540,7 @@ export const zh = {
|
|||||||
MemorySummary: '长期沉淀',
|
MemorySummary: '长期沉淀',
|
||||||
Statement: '情绪记忆',
|
Statement: '情绪记忆',
|
||||||
ExtractedEntity: '情景记忆',
|
ExtractedEntity: '情景记忆',
|
||||||
|
Perceptual: '感知记忆',
|
||||||
positive: '正向情绪',
|
positive: '正向情绪',
|
||||||
negative: '负向情绪',
|
negative: '负向情绪',
|
||||||
neutral: '中性情绪',
|
neutral: '中性情绪',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 18:32:23
|
* @Date: 2026-02-03 18:32:23
|
||||||
* @Last Modified by: ZhaoYing
|
* @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 { type FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -12,6 +12,7 @@ import clsx from 'clsx'
|
|||||||
|
|
||||||
import RbCard from '@/components/RbCard/Card'
|
import RbCard from '@/components/RbCard/Card'
|
||||||
import AudioPlayer from './AudioPlayer'
|
import AudioPlayer from './AudioPlayer'
|
||||||
|
import VideoPlayer from './VideoPlayer'
|
||||||
import {
|
import {
|
||||||
getPerceptualLastVisual,
|
getPerceptualLastVisual,
|
||||||
getPerceptualLastListen,
|
getPerceptualLastListen,
|
||||||
@@ -107,7 +108,7 @@ const PerceptualLastInfo: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = () => {
|
||||||
if (!data.file_path) return
|
if (!data.file_path) return
|
||||||
window.open(data.file_path, '_blank')
|
window.open(data.file_path, '_blank')
|
||||||
}
|
}
|
||||||
@@ -139,9 +140,7 @@ const PerceptualLastInfo: FC = () => {
|
|||||||
{/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name)
|
{/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name)
|
||||||
? <Image src={data.file_path} alt={data.file_name} width={432} className="rb:rounded-xl rb:h-45!" />
|
? <Image src={data.file_path} alt={data.file_name} width={432} className="rb:rounded-xl rb:h-45!" />
|
||||||
: /\.(mp4|webm|ogg|mov)$/i.test(data.file_name)
|
: /\.(mp4|webm|ogg|mov)$/i.test(data.file_name)
|
||||||
? <Flex align="center" justify="space-between" className="rb:bg-[#F6F6F6] rb:min-h-15.5! rb:rounded-xl rb:p-3!">
|
? <VideoPlayer src={data.file_path} />
|
||||||
|
|
||||||
</Flex>
|
|
||||||
: /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name)
|
: /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name)
|
||||||
? <AudioPlayer src={data.file_path} fileName={data.file_name} fileSize={fileSize} />
|
? <AudioPlayer src={data.file_path} fileName={data.file_name} fileSize={fileSize} />
|
||||||
: <Flex gap={11} align="center" justify="space-between" className="rb:bg-[#F6F6F6] rb:min-h-15.5! rb:rounded-xl rb:p-3!">
|
: <Flex gap={11} align="center" justify="space-between" className="rb:bg-[#F6F6F6] rb:min-h-15.5! rb:rounded-xl rb:p-3!">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 18:32:00
|
* @Date: 2026-02-03 18:32:00
|
||||||
* @Last Modified by: ZhaoYing
|
* @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
|
* Relationship Network Component
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
import React, { type FC, useEffect, useState, useCallback, useRef } from 'react'
|
import React, { type FC, useEffect, useState, useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
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 dayjs from 'dayjs'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ import Tag from '@/components/Tag'
|
|||||||
import GraphNetworkChart, { type Node, type Edge } from '@/components/Charts/GraphNetworkChart'
|
import GraphNetworkChart, { type Node, type Edge } from '@/components/Charts/GraphNetworkChart'
|
||||||
import CommunityNetwork from './CommunityNetwork'
|
import CommunityNetwork from './CommunityNetwork'
|
||||||
import PageTabs from '@/components/PageTabs'
|
import PageTabs from '@/components/PageTabs'
|
||||||
|
import AudioPlayer from './AudioPlayer'
|
||||||
|
import VideoPlayer from './VideoPlayer'
|
||||||
|
|
||||||
const RelationshipNetwork: FC = () => {
|
const RelationshipNetwork: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -149,6 +151,26 @@ const RelationshipNetwork: FC = () => {
|
|||||||
setSelectedNode(null)
|
setSelectedNode(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [fileSize, setFileSize] = useState<string>('')
|
||||||
|
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 (
|
return (
|
||||||
<div className="rb:flex-1 rb:relative">
|
<div className="rb:flex-1 rb:relative">
|
||||||
<div className="rb:absolute rb:z-111 rb:bottom-10 rb:left-[calc(50%-96px)] rb:transition-transform-[translateX(-50%]">
|
<div className="rb:absolute rb:z-111 rb:bottom-10 rb:left-[calc(50%-96px)] rb:transition-transform-[translateX(-50%]">
|
||||||
@@ -229,6 +251,8 @@ const RelationshipNetwork: FC = () => {
|
|||||||
? selectedNode.properties.description
|
? selectedNode.properties.description
|
||||||
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
|
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
|
||||||
? selectedNode.properties.statement
|
? selectedNode.properties.statement
|
||||||
|
: selectedNode.label === 'Perceptual' && 'summary' in selectedNode.properties
|
||||||
|
? selectedNode.properties.summary
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -285,6 +309,30 @@ const RelationshipNetwork: FC = () => {
|
|||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
</>}
|
</>}
|
||||||
|
{selectedNode.label === 'Perceptual' && <>
|
||||||
|
{selectedNode.properties.file_type.includes('image')
|
||||||
|
? <Image src={selectedNode.properties.file_path} alt={selectedNode.properties.file_name} className="rb:rounded-xl rb:h-45! rb:w-full" />
|
||||||
|
: selectedNode.properties.file_type.includes('video')
|
||||||
|
? <VideoPlayer src={selectedNode.properties.file_path} />
|
||||||
|
: selectedNode.properties.file_type.includes('audio')
|
||||||
|
? <AudioPlayer src={selectedNode.properties.file_path} fileName={selectedNode.properties.file_name} fileSize={fileSize} />
|
||||||
|
: <Flex gap={11} align="center" justify="space-between" className="rb:bg-[#F6F6F6] rb:min-h-15.5! rb:rounded-xl rb:p-3!">
|
||||||
|
<Flex gap={12} align="center">
|
||||||
|
<div className="rb:w-7.5 rb:h-9 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/file.svg')]"></div>
|
||||||
|
<div>
|
||||||
|
<div className="rb:leading-5 rb:font-medium rb:mb-1">{selectedNode.properties.file_name}</div>
|
||||||
|
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">
|
||||||
|
{fileSize || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
<div
|
||||||
|
className="rb:size-6 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/userMemory/download.svg')] rb:hover:bg-[url('@/assets/images/userMemory/download_hover.svg')]"
|
||||||
|
onClick={handleDownload}
|
||||||
|
></div>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
</>}
|
||||||
</Flex>
|
</Flex>
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
62
web/src/views/UserMemoryDetail/components/VideoPlayer.tsx
Normal file
62
web/src/views/UserMemoryDetail/components/VideoPlayer.tsx
Normal file
@@ -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<VideoPlayerProps> = ({ src }) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
|
||||||
|
const handleOpen = () => setOpen(true)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
videoRef.current?.pause()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Thumbnail with play overlay */}
|
||||||
|
<div
|
||||||
|
className="rb:relative rb:w-full rb:h-45 rb:rounded-xl rb:overflow-hidden rb:cursor-pointer rb:group"
|
||||||
|
onClick={handleOpen}
|
||||||
|
>
|
||||||
|
<video src={src} className="rb:w-full rb:h-full rb:object-cover" preload="metadata" />
|
||||||
|
<div className="rb:absolute rb:inset-0 rb:bg-black/20 rb:flex rb:items-center rb:justify-center rb:transition-colors group-hover:rb:bg-black/30">
|
||||||
|
<div className="rb:size-10 rb:rounded-full rb:bg-white/80 rb:flex rb:items-center rb:justify-center">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path d="M5 3.5L14.5 9L5 14.5V3.5Z" fill="#171719" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fullscreen modal */}
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className="rb:fixed rb:inset-0 rb:z-1000 rb:bg-black/80 rb:flex rb:items-center rb:justify-center"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<button className="ant-image-preview-close"><CloseOutlined /></button>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={src}
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
className="rb:max-w-[90vw] rb:max-h-[90vh] rb:rounded-xl"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoPlayer
|
||||||
Reference in New Issue
Block a user