Files
MemoryBear/web/src/views/UserMemoryDetail/Neo4j.tsx
2026-03-07 14:59:58 +08:00

171 lines
7.0 KiB
TypeScript

/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:57:26
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 14:28:34
*/
/**
* Neo4j User Memory Detail View
* Displays user memory details using Neo4j graph storage
* Shows profile, interests, node statistics, relationships, and insights
*/
import { type FC, useRef, useState, type MouseEvent } from 'react'
import clsx from 'clsx'
import { useParams, useNavigate } from 'react-router-dom'
import { Flex, Popover } from 'antd'
import { useTranslation } from 'react-i18next';
import EndUserProfile from './components/EndUserProfile'
import AboutMe from './components/AboutMe'
import InterestDistribution from './components/InterestDistribution'
import NodeStatistics from './components/NodeStatistics'
import RelationshipNetwork from './components/RelationshipNetwork'
import MemoryInsight from './components/MemoryInsight'
import type { EndUserProfileRef, MemoryInsightRef, AboutMeRef } from './types'
import {
analyticsRefresh,
} from '@/api/memory'
const Neo4j: FC = () => {
const { id } = useParams()
const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false)
const [name, setName] = useState('')
const ref = useRef<EndUserProfileRef>(null)
const memoryInsightRef = useRef<MemoryInsightRef>(null)
const aboutMeRef = useRef<AboutMeRef>(null)
const [selectedKey, setSelectedKey] = useState<string | null>(null)
/** Update displayed name */
const handleNameUpdate = (data: { other_name?: string; id: string }) => {
setName(data.other_name && data.other_name !== '' ? data.other_name : data.id)
}
/** Navigate back */
const goBack = () => {
navigate('/user-memory', { replace: true })
}
/** Refresh analytics data */
const handleRefresh = () => {
if (loading) return;
setLoading(true)
analyticsRefresh(id as string)
.then(res => {
const response = res as { insight_success: boolean; summary_success: boolean; }
if (response.insight_success) {
memoryInsightRef.current?.getData()
}
if (response.summary_success) {
aboutMeRef.current?.getData()
}
})
.finally(() => {
setLoading(false)
})
}
const onOpenChange = (e: MouseEvent, type: string) => {
e.preventDefault();
e.stopPropagation();
setSelectedKey(type)
}
return (
<div className="rb:h-screen rb:w-screen rb:p-3 rb:relative" onClick={() => setSelectedKey(null)}>
<Flex className="rb:h-[calc(100vh-24px)]" gap={12}>
<Flex gap={15} vertical justify="space-between" align="center" className="rb:h-full! rb:px-4! rb:pt-6! rb:pb-5! rb:bg-white rb:w-20 rb:rounded-xl">
<Flex gap={15} vertical>
<Popover
content={t('userMemory.memoryWindow', { name: name })}
placement="right"
arrow={false}
trigger="hover"
>
<div className="rb:mb-4.25! rb:size-12 rb:rounded-xl rb:bg-cover rb:bg-[url('@/assets/images/userMemory/logo.png')]"></div>
</Popover>
<Flex
align="center"
justify="center"
className={clsx("rb:cursor-pointer rb:size-12 rb:rounded-xl rb:group", {
'rb:bg-[#155EEF]': selectedKey === 'userProfile',
'rb:hover:bg-[#F0F3F8]': selectedKey !== 'userProfile',
})}
onClick={(e) => onOpenChange(e, 'userProfile')}
>
<div className={clsx("rb:size-6 rb:bg-cover", {
"rb:bg-[url('@/assets/images/userMemory/userProfile.svg')]": selectedKey !== 'userProfile',
"rb:bg-[url('@/assets/images/userMemory/userProfile_active.svg')]": selectedKey === 'userProfile'
})}></div>
</Flex>
<Flex
align="center"
justify="center"
className={clsx("rb:cursor-pointer rb:size-12 rb:rounded-xl rb:group", {
'rb:bg-[#155EEF]': selectedKey === 'aboutMe',
'rb:hover:bg-[#F0F3F8]': selectedKey !== 'aboutMe',
})}
onClick={(e) => onOpenChange(e, 'aboutMe')}
>
<div className={clsx("rb:size-6 rb:bg-cover", {
"rb:bg-[url('@/assets/images/userMemory/aboutMe.svg')]": selectedKey !== 'aboutMe',
"rb:bg-[url('@/assets/images/userMemory/aboutMe_active.svg')]": selectedKey === 'aboutMe'
})}></div>
</Flex>
<Flex
align="center"
justify="center"
className={clsx("rb:cursor-pointer rb:size-12 rb:rounded-xl rb:group", {
'rb:bg-[#155EEF]': selectedKey === 'interestDistribution',
'rb:hover:bg-[#F0F3F8]': selectedKey !== 'interestDistribution',
})}
onClick={(e) => onOpenChange(e, 'interestDistribution')}
>
<div className={clsx("rb:size-6 rb:bg-cover", {
"rb:bg-[url('@/assets/images/userMemory/interestDistribution.svg')]": selectedKey !== 'interestDistribution',
"rb:bg-[url('@/assets/images/userMemory/interestDistribution_active.svg')]": selectedKey === 'interestDistribution'
})}></div>
</Flex>
<Flex
align="center"
justify="center"
className={clsx("rb:cursor-pointer rb:size-12 rb:rounded-xl rb:group", {
'rb:bg-[#155EEF]': selectedKey === 'memoryInsight',
'rb:hover:bg-[#F0F3F8]': selectedKey !== 'memoryInsight',
})}
onClick={(e) => onOpenChange(e, 'memoryInsight')}
>
<div className={clsx("rb:size-6 rb:bg-cover", {
"rb:bg-[url('@/assets/images/userMemory/memoryInsight.svg')]": selectedKey !== 'memoryInsight',
"rb:bg-[url('@/assets/images/userMemory/memoryInsight_active.svg')]": selectedKey === 'memoryInsight'
})}></div>
</Flex>
</Flex>
<Flex vertical gap={24}>
<div className={clsx("rb:cursor-pointer rb:size-6 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/refresh.svg')]", {
"rb:animate-spin": loading
})} onClick={handleRefresh}></div>
<div className="rb:cursor-pointer rb:size-6 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/logout.svg')]" onClick={goBack}></div>
</Flex>
</Flex>
<Flex vertical className="rb:flex-1">
<NodeStatistics />
<RelationshipNetwork />
</Flex>
</Flex>
<EndUserProfile ref={ref} onDataLoaded={handleNameUpdate} className={selectedKey === 'userProfile' ? 'rb:block!' : 'rb:hidden!'} />
<AboutMe ref={aboutMeRef} className={selectedKey === 'aboutMe' ? 'rb:block!' : 'rb:hidden!'} />
<InterestDistribution className={selectedKey === 'interestDistribution' ? 'rb:block!' : 'rb:hidden!'} />
<MemoryInsight ref={memoryInsightRef} className={selectedKey === 'memoryInsight' ? 'rb:block!' : 'rb:hidden!'} />
</div>
)
}
export default Neo4j