feat(web): neo4j type user memory detail
This commit is contained in:
24
web/src/assets/images/fullScreen.svg
Normal file
24
web/src/assets/images/fullScreen.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>全屏</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="红熊空间-记忆管理-个人记忆" transform="translate(-967, -349)" stroke="#5B6167">
|
||||
<g id="图谱" transform="translate(432, 334)">
|
||||
<g id="编组-18" transform="translate(535, 13)">
|
||||
<g id="全屏" transform="translate(0, 2)">
|
||||
<g id="编组-6" transform="translate(2, 2)">
|
||||
<polyline id="路径" points="0 3 0 0 3 0"></polyline>
|
||||
<polyline id="路径" transform="translate(10.5, 1.5) scale(-1, 1) translate(-10.5, -1.5)" points="9 3 9 0 12 0"></polyline>
|
||||
<polyline id="路径" transform="translate(1.5, 10.5) scale(1, -1) translate(-1.5, -10.5)" points="0 12 0 9 3 9"></polyline>
|
||||
<polyline id="路径" transform="translate(10.5, 10.5) scale(-1, -1) translate(-10.5, -10.5)" points="9 12 9 9 12 9"></polyline>
|
||||
<line x1="5.30274338e-05" y1="0" x2="4.00960414" y2="4.02323899" id="路径-3"></line>
|
||||
<line x1="12" y1="0" x2="8.01466597" y2="4.01783837" id="路径-4"></line>
|
||||
<line x1="0" y1="11.9995998" x2="4.00871245" y2="8.01746225" id="路径-5"></line>
|
||||
<line x1="11.9977093" y1="12" x2="8.01643138" y2="8.01746225" id="路径-7"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
24
web/src/assets/images/fullScreen_hover.svg
Normal file
24
web/src/assets/images/fullScreen_hover.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>全屏</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="红熊空间-记忆管理-个人记忆" transform="translate(-967, -349)" stroke="#212332">
|
||||
<g id="图谱" transform="translate(432, 334)">
|
||||
<g id="编组-18" transform="translate(535, 13)">
|
||||
<g id="全屏" transform="translate(0, 2)">
|
||||
<g id="编组-6" transform="translate(2, 2)">
|
||||
<polyline id="路径" points="0 3 0 0 3 0"></polyline>
|
||||
<polyline id="路径" transform="translate(10.5, 1.5) scale(-1, 1) translate(-10.5, -1.5)" points="9 3 9 0 12 0"></polyline>
|
||||
<polyline id="路径" transform="translate(1.5, 10.5) scale(1, -1) translate(-1.5, -10.5)" points="0 12 0 9 3 9"></polyline>
|
||||
<polyline id="路径" transform="translate(10.5, 10.5) scale(-1, -1) translate(-10.5, -10.5)" points="9 12 9 9 12 9"></polyline>
|
||||
<line x1="5.30274338e-05" y1="0" x2="4.00960414" y2="4.02323899" id="路径-3"></line>
|
||||
<line x1="12" y1="0" x2="8.01466597" y2="4.01783837" id="路径-4"></line>
|
||||
<line x1="0" y1="11.9995998" x2="4.00871245" y2="8.01746225" id="路径-5"></line>
|
||||
<line x1="11.9977093" y1="12" x2="8.01643138" y2="8.01746225" id="路径-7"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -2,7 +2,7 @@
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>刷新</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="应用管理--API" transform="translate(-1071, -490)" fill="#212332" fill-rule="nonzero">
|
||||
<g id="应用管理--API" transform="translate(-1071, -490)" fill="#5B6167" fill-rule="nonzero">
|
||||
<g id="2" transform="translate(220, 336)">
|
||||
<g id="主操作" transform="translate(839, 144)">
|
||||
<g id="刷新" transform="translate(12, 10)">
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
15
web/src/assets/images/refresh_hover.svg
Normal file
15
web/src/assets/images/refresh_hover.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>刷新</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="应用管理--API" transform="translate(-1071, -490)" fill="#155EEF" fill-rule="nonzero">
|
||||
<g id="2" transform="translate(220, 336)">
|
||||
<g id="主操作" transform="translate(839, 144)">
|
||||
<g id="刷新" transform="translate(12, 10)">
|
||||
<path d="M14.5,6.60760714 L14.5,3.35760714 L13.4384813,4.41757143 C12.2346397,2.59629629 10.195406,1.50029795 8.00999678,1.5 C4.41487535,1.5 1.5,4.41014286 1.5,8.00046429 C1.5,11.5907857 4.41487535,14.5009287 8.00999678,14.5009287 C10.6602392,14.5014558 13.046297,12.8977788 14.0434028,10.4458571 C14.1184045,10.261489 14.0892053,10.0511714 13.9668045,9.89412914 C13.8444036,9.73708685 13.6473966,9.65717825 13.4499941,9.68450414 C13.2525917,9.71183003 13.0847838,9.84223896 13.0097822,10.0266071 C12.1832078,12.0581943 10.2060581,13.38691 8.00999678,13.3866429 C5.03095604,13.3866429 2.61591974,10.9751429 2.61591974,8.00046429 C2.61591974,5.02578571 5.03095604,2.61428571 8.00999678,2.61428571 C9.93449337,2.61428571 11.6706785,3.63107143 12.6308344,5.22357143 L11.2452341,6.60760714 L14.5,6.60760714 Z" id="路径"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
web/src/assets/images/userMemory/detail_empty.png
Normal file
BIN
web/src/assets/images/userMemory/detail_empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -13,7 +13,7 @@ const { Content } = Layout;
|
||||
|
||||
// 认证布局组件,使用useRouteGuard hook进行路由鉴权
|
||||
const AuthLayout: FC = () => {
|
||||
const { getUserInfo } = useUser();
|
||||
const { getUserInfo, getStorageType } = useUser();
|
||||
// 使用路由守卫hook处理认证和权限检查
|
||||
useRouteGuard('manage');
|
||||
// 自动更新面包屑导航
|
||||
@@ -24,6 +24,7 @@ const AuthLayout: FC = () => {
|
||||
window.location.href = `/#/login`;
|
||||
} else {
|
||||
getUserInfo()
|
||||
getStorageType()
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useUser } from '@/store/user';
|
||||
|
||||
// 基础布局组件,用于展示内容并保留用户信息获取功能
|
||||
const BasicLayout: FC = () => {
|
||||
const { getUserInfo } = useUser();
|
||||
const { getUserInfo, getStorageType } = useUser();
|
||||
|
||||
// 获取用户信息
|
||||
useEffect(() => {
|
||||
getUserInfo();
|
||||
}, [getUserInfo]);
|
||||
getStorageType()
|
||||
}, [getUserInfo, getStorageType]);
|
||||
|
||||
return (
|
||||
<div className="rb:relative rb:h-full rb:w-full">
|
||||
|
||||
@@ -329,7 +329,8 @@ export const en = {
|
||||
publicApiCannotRefreshToken: 'Public API cannot refresh token',
|
||||
refreshTokenNotExist: 'Refresh token does not exist',
|
||||
reset: 'Reset',
|
||||
refresh: 'Refresh'
|
||||
refresh: 'Refresh',
|
||||
return: 'Return',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -1023,7 +1024,7 @@ export const en = {
|
||||
drag: 'Drag and drop to move nodes',
|
||||
zoom: 'Scroll zoom view',
|
||||
memoryDetailEmpty: 'Please select a memory node',
|
||||
memoryDetailEmptyDesc: 'Click on any node in the above view to view detailed information',
|
||||
memoryDetailEmptyDesc: 'Click on the node in the left graph to view the details of entity memory',
|
||||
|
||||
totalNumOfMemories: 'Total Number of Memories',
|
||||
footprintCity: 'Footprint City',
|
||||
@@ -1067,6 +1068,8 @@ export const en = {
|
||||
hire_date: 'Hire Date',
|
||||
memoryContent: 'Memory Content',
|
||||
created_at: 'Created At',
|
||||
|
||||
memoryWindow: "{{name}}'s Window of Memory"
|
||||
},
|
||||
space: {
|
||||
createSpace: 'Create Space',
|
||||
|
||||
@@ -805,7 +805,8 @@ export const zh = {
|
||||
publicApiCannotRefreshToken: '公共接口不能刷新token',
|
||||
refreshTokenNotExist: '刷新token不存在',
|
||||
reset: '重置',
|
||||
refresh: '刷新'
|
||||
refresh: '刷新',
|
||||
return: '返回',
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -1105,7 +1106,7 @@ export const zh = {
|
||||
drag: '拖放移动节点',
|
||||
zoom: '滚动缩放视图',
|
||||
memoryDetailEmpty: '请选择一个记忆节点',
|
||||
memoryDetailEmptyDesc: '点击上方视图中的任何节点以查看详细信息',
|
||||
memoryDetailEmptyDesc: '点击左侧图表中的节点查看实体记忆详情',
|
||||
|
||||
totalNumOfMemories: '记忆总数',
|
||||
footprintCity: '足迹城市',
|
||||
@@ -1151,6 +1152,8 @@ export const zh = {
|
||||
hire_date: '入职时间',
|
||||
memoryContent: '记忆内容',
|
||||
created_at: '创建时间',
|
||||
updated_at: '最后更新时间',
|
||||
fullScreen: '全屏'
|
||||
},
|
||||
space: {
|
||||
createSpace: '创建空间',
|
||||
@@ -1476,7 +1479,7 @@ export const zh = {
|
||||
auth: '认证',
|
||||
requestHeader: '请求头',
|
||||
config: '配置',
|
||||
auth_Type: '认证方式',
|
||||
auth_type: '认证方式',
|
||||
none: '无需认证',
|
||||
api_key: 'API Key',
|
||||
basic_auth: 'Basic Auth',
|
||||
|
||||
@@ -38,6 +38,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
Home: lazy(() => import('@/views/Home')),
|
||||
UserMemory: lazy(() => import('@/views/UserMemory')),
|
||||
UserMemoryDetail: lazy(() => import('@/views/UserMemoryDetail')),
|
||||
Neo4jUserMemoryDetail: lazy(() => import('@/views/UserMemoryDetail/Neo4j')),
|
||||
MemberManagement: lazy(() => import('@/views/MemberManagement')),
|
||||
MemoryManagement: lazy(() => import('@/views/MemoryManagement')),
|
||||
ForgettingEngine: lazy(() => import('@/views/ForgettingEngine')),
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"element": "BasicLayout",
|
||||
"children": [
|
||||
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
|
||||
{ "path": "/conversation/:token", "element": "Conversation" }
|
||||
{ "path": "/conversation/:token", "element": "Conversation" },
|
||||
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
"code": "userMemoryDetail",
|
||||
"label": "记忆详情",
|
||||
"i18nKey": "menu.userMemoryDetail",
|
||||
"path": "/user-memory/:id",
|
||||
"path": "/user-memory/neo4j/:id",
|
||||
"enable": true,
|
||||
"display": false,
|
||||
"level": 2,
|
||||
@@ -304,6 +304,18 @@
|
||||
"subs": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 81,
|
||||
"parent": 8,
|
||||
"code": "userMemoryDetail",
|
||||
"label": "记忆详情",
|
||||
"i18nKey": "menu.userMemoryDetail",
|
||||
"path": "/user-memory/:id",
|
||||
"enable": true,
|
||||
"display": false,
|
||||
"level": 2,
|
||||
"sort": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ import onlineNum from '@/assets/images/memory/onlineNum.svg'
|
||||
import Table from '@/components/Table'
|
||||
import { getTotalEndUsers, userMemoryListUrl, getUserMemoryList } from '@/api/memory';
|
||||
import ConfigModal from './components/ConfigModal';
|
||||
import { useUser } from '@/store/user'
|
||||
|
||||
const bgList = [
|
||||
'linear-gradient( 180deg, #F1F6FE 0%, #FBFDFF 100%)',
|
||||
@@ -31,6 +32,7 @@ const IconList: Record<string, string> = {
|
||||
export default function UserMemory() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { storageType } = useUser()
|
||||
const configModalRef = useRef<ConfigModalRef>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [data, setData] = useState<Data[]>([]);
|
||||
@@ -58,8 +60,15 @@ export default function UserMemory() {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
console.log('storageType', storageType)
|
||||
const handleViewDetail = (id: string | number) => {
|
||||
navigate(`/user-memory/${id}`)
|
||||
switch (storageType) {
|
||||
case 'neo4j':
|
||||
navigate(`/user-memory/neo4j/${id}`)
|
||||
break;
|
||||
default:
|
||||
navigate(`/user-memory/${id}`)
|
||||
}
|
||||
}
|
||||
const handleChangeLayout = (e: RadioChangeEvent) => {
|
||||
const type = e.target.value
|
||||
|
||||
@@ -1,168 +1,87 @@
|
||||
import { type FC, useEffect, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx'
|
||||
import { Row, Col, Skeleton, Flex, Button } from 'antd'
|
||||
import { type FC, useEffect, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import aboutUs from '@/assets/images/userMemory/aboutUs.svg'
|
||||
import down from '@/assets/images/userMemory/down.svg'
|
||||
import interestDistribution from '@/assets/images/userMemory/interestDistribution.svg'
|
||||
import PieCard from './components/PieCard'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getUserSummary,
|
||||
analyticsRefresh
|
||||
} from '@/api/memory'
|
||||
import type { MemoryInsightRef } from './types'
|
||||
import { Row, Col, Space, Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PageHeader from './components/PageHeader'
|
||||
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 Empty from '@/components/Empty'
|
||||
|
||||
import NodeStatistics from './components/NodeStatistics'
|
||||
import EndUserProfile from './components/EndUserProfile'
|
||||
|
||||
interface TitleProps {
|
||||
type: string;
|
||||
title: string
|
||||
icon: string
|
||||
t: (key: string) => string;
|
||||
expanded: boolean;
|
||||
onClick: (type: string) => void;
|
||||
}
|
||||
const Title: FC<TitleProps> = ({ type, title, icon, t, expanded, onClick }) => (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-4.25 rb:border-b rb:border-[#DFE4ED] rb:text-[16px] rb:font-semibold rb:leading-5.5">
|
||||
<span className="rb:flex rb:items-center">
|
||||
<img src={icon} className="rb:w-5 rb:h-5 rb:mr-2" />
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<span className="rb:flex rb:items-center rb:cursor-pointer rb:text-[#5B6167] rb:text-[14px] rb:font-regular rb:leading-5" onClick={() => onClick(type)}>
|
||||
{t(`userMemory.${expanded ? 'foldUp' : 'expanded'}`)}
|
||||
<img src={down} className={clsx("rb:w-4 rb:h-4 rb:ml-1", {
|
||||
'rb:rotate-180': !expanded,
|
||||
})} />
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
import type { EndUserProfileRef, MemoryInsightRef, AboutMeRef } from './types'
|
||||
import {
|
||||
analyticsRefresh,
|
||||
} from '@/api/memory'
|
||||
|
||||
const Neo4j: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const ref = useRef<EndUserProfileRef>(null)
|
||||
const memoryInsightRef = useRef<MemoryInsightRef>(null)
|
||||
const [expanded, setExpanded] = useState<string[]>(['aboutUs', 'interestDistribution', 'importantRelationships', 'importantMomentsInLife'])
|
||||
const [summary, setSummary] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({
|
||||
summary: false,
|
||||
refresh: false
|
||||
})
|
||||
const aboutMeRef = useRef<AboutMeRef>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getSummary()
|
||||
}, [id])
|
||||
const handleNameUpdate = (data: { other_name?: string; id: string }) => {
|
||||
setName(data.other_name ?? data.id)
|
||||
}
|
||||
|
||||
const handleTitleClick = (key: string) => {
|
||||
setExpanded(expanded.includes(key) ? expanded.filter((item) => item !== key) : [...expanded, key])
|
||||
}
|
||||
// 用户摘要
|
||||
const getSummary = () => {
|
||||
if (!id) return
|
||||
setLoading(prev => ({ ...prev, summary: true }))
|
||||
getUserSummary(id).then((res) => {
|
||||
setSummary((res as { summary?: string }).summary || null)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(prev => ({ ...prev, summary: false }))
|
||||
})
|
||||
}
|
||||
const handleRefresh = () => {
|
||||
setLoading(prev => ({ ...prev, refresh: true }))
|
||||
setLoading(true)
|
||||
analyticsRefresh(id as string)
|
||||
.then(res => {
|
||||
const response = res as { insight_success: boolean; summary_success: boolean; }
|
||||
if (response.insight_success) {
|
||||
memoryInsightRef.current?.getInsightReport()
|
||||
memoryInsightRef.current?.getData()
|
||||
}
|
||||
if (response.summary_success) {
|
||||
getSummary()
|
||||
memoryInsightRef.current?.getData()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(prev => ({ ...prev, refresh: false }))
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify="flex-end">
|
||||
<Button type="primary" loading={loading.refresh} className="rb:mb-3" onClick={handleRefresh}>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Row gutter={[16, 16]} className="rb:pb-6">
|
||||
<Col span={8}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<EndUserProfile />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<RbCard>
|
||||
{/* 关于我 */}
|
||||
<>
|
||||
<Title
|
||||
type="aboutUs"
|
||||
title={t('userMemory.aboutMe')}
|
||||
icon={aboutUs}
|
||||
t={t}
|
||||
expanded={expanded.includes('aboutUs')}
|
||||
onClick={handleTitleClick}
|
||||
/>
|
||||
{expanded.includes('aboutUs') && (
|
||||
<>
|
||||
{loading.summary
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: summary
|
||||
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
|
||||
{summary || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
{/* 兴趣分布 */}
|
||||
<>
|
||||
<Title
|
||||
type="interestDistribution"
|
||||
title={t('userMemory.interestDistribution')}
|
||||
icon={interestDistribution}
|
||||
t={t}
|
||||
expanded={expanded.includes('interestDistribution')}
|
||||
onClick={handleTitleClick}
|
||||
/>
|
||||
|
||||
{expanded.includes('interestDistribution') && (
|
||||
<PieCard />
|
||||
)}
|
||||
</>
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<PageHeader
|
||||
name={name}
|
||||
operation={(
|
||||
<Button
|
||||
loading={loading}
|
||||
className="rb:group rb:h-7! rb:bg-transparent! rb:border-[#5B6167] rb:text-[#5B6167] rb:ml-3"
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{!loading && <div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh.svg')] rb:group-hover:bg-[url('@/assets/images/refresh_hover.svg')]"
|
||||
></div>}
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<EndUserProfile ref={ref} onDataLoaded={handleNameUpdate} />
|
||||
<AboutMe ref={aboutMeRef} />
|
||||
<InterestDistribution />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<NodeStatistics />
|
||||
</Col>
|
||||
{/* 记忆洞察 */}
|
||||
<Col span={24}>
|
||||
<RelationshipNetwork />
|
||||
<MemoryInsight ref={memoryInsightRef} />
|
||||
</Col>
|
||||
{/* 关系网络 + 记忆详情 */}
|
||||
<RelationshipNetwork />
|
||||
</Row>
|
||||
</Col>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
56
web/src/views/UserMemoryDetail/components/AboutMe.tsx
Normal file
56
web/src/views/UserMemoryDetail/components/AboutMe.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { type FC, useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton } from 'antd';
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
getUserSummary,
|
||||
} from '@/api/memory'
|
||||
import type { AboutMeRef } from '../types'
|
||||
|
||||
const AboutMe = forwardRef<AboutMeRef>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getUserSummary(id)
|
||||
.then((res) => {
|
||||
setData((res as { summary?: string }).summary || null)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.aboutMe')}
|
||||
>
|
||||
{loading
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: data
|
||||
? <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167]">
|
||||
{data || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
})
|
||||
export default AboutMe
|
||||
@@ -1,17 +1,21 @@
|
||||
import { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { forwardRef, useImperativeHandle, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Descriptions, Button } from 'antd';
|
||||
import { Skeleton } from 'antd';
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
getEndUserProfile,
|
||||
} from '@/api/memory'
|
||||
import EndUserProfileModal from './EndUserProfileModal'
|
||||
import type { EndUser, EndUserProfileModalRef } from '../types'
|
||||
import type { EndUser, EndUserProfileModalRef, EndUserProfileRef } from '../types'
|
||||
|
||||
const EndUserProfile:FC = () => {
|
||||
interface EndUserProfileProps {
|
||||
onDataLoaded?: (data: { other_name?: string; id: string }) => void
|
||||
}
|
||||
|
||||
const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ onDataLoaded }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const endUserProfileModalRef = useRef<EndUserProfileModalRef>(null)
|
||||
@@ -28,7 +32,12 @@ const EndUserProfile:FC = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getEndUserProfile(id).then((res) => {
|
||||
setData(res as EndUser)
|
||||
const userData = res as EndUser
|
||||
setData(userData)
|
||||
onDataLoaded?.({
|
||||
other_name: userData.other_name,
|
||||
id: userData.id
|
||||
})
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -36,31 +45,45 @@ const EndUserProfile:FC = () => {
|
||||
})
|
||||
}
|
||||
const formatItems = useCallback(() => {
|
||||
if (!data) return []
|
||||
return ['other_name', 'position', 'department', 'contact', 'phone', 'hire_date'].map(key => ({
|
||||
key,
|
||||
label: t(`userMemory.${key}`),
|
||||
children: key === 'hire_date' && data[key] ? dayjs(data[key as keyof EndUser]).format('YYYY-MM-DD') : String(data[key as keyof EndUser] || ''),
|
||||
children: key === 'hire_date' && data?.[key] ? dayjs(data[key as keyof EndUser]).format('YYYY-MM-DD') : String(data?.[key as keyof EndUser] || '-'),
|
||||
}))
|
||||
}, [data])
|
||||
const handleEdit = () => {
|
||||
if (!data) return
|
||||
endUserProfileModalRef.current?.handleOpen(data)
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
data
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.endUserProfile')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
extra={
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={handleEdit}
|
||||
></div>
|
||||
}
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data
|
||||
? <div className="rb:flex rb:flex-wrap rb:justify-between rb:h-full">
|
||||
<Descriptions column={1} items={formatItems()} classNames={{ label: 'rb:w-24' }} />
|
||||
<Button className="rb:mt-3" block onClick={handleEdit}>{t('common.edit')}</Button>
|
||||
: <div className="rb:flex rb:flex-col rb:justify-between rb:gap-3 rb:h-full">
|
||||
{formatItems().map(vo => (
|
||||
<div key={vo.key} className="rb:flex rb:justify-between rb:items-center rb:gap-3 rb:leading-5">
|
||||
<div className="rb:text-[#5B6167]">{vo.label}</div>
|
||||
<div className="">{vo.children}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="rb:border-t rb:border-t-[#DFE4ED] rb:pt-4 rb:text-[#5B6167] rb:text-[12px] rb:leading-4">
|
||||
{t('userMemory.updated_at')}: {data?.updatetime_profile ? dayjs(data?.updatetime_profile).format('YYYY/MM/DD HH:mm:ss') : ''}
|
||||
</div>
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
<EndUserProfileModal
|
||||
ref={endUserProfileModalRef}
|
||||
@@ -68,5 +91,5 @@ const EndUserProfile:FC = () => {
|
||||
/>
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
})
|
||||
export default EndUserProfile
|
||||
@@ -1,18 +1,24 @@
|
||||
import { type FC, useRef, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import { Space } from 'antd'
|
||||
|
||||
import { getHotMemoryTagsByUser } from '@/api/memory';
|
||||
import Empty from '@/components/Empty';
|
||||
import Loading from '@/components/Empty/Loading';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
|
||||
const Colors = ['#155EEF', '#4DA8FF', '#03BDFF', '#31E8FF', '#AD88FF', '#FFB048']
|
||||
|
||||
const PieCard: FC = () => {
|
||||
const InterestDistribution: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
const resizeScheduledRef = useRef(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<Array<Record<string, string | number>>>([])
|
||||
const totalValue = data.reduce((sum, item) => sum + Number(item.value), 0)
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
@@ -55,12 +61,14 @@ const PieCard: FC = () => {
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<>
|
||||
<RbCard
|
||||
title={t('userMemory.interestDistribution')}
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !data || data.length === 0
|
||||
? <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
: data && data.length > 0 &&
|
||||
: data && data.length > 0 && <>
|
||||
<ReactEcharts
|
||||
option={{
|
||||
color: Colors,
|
||||
@@ -80,19 +88,7 @@ const PieCard: FC = () => {
|
||||
extraCssText: 'width: 36px; height: 36px; box-shadow: 0px 2px 4px 0px rgba(33,35,50,0.12);border-radius: 36px;'
|
||||
},
|
||||
legend: {
|
||||
type: data.length > 8 ? 'scroll' : 'plain',
|
||||
bottom: 0,
|
||||
left: 16,
|
||||
padding: 0,
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
// orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
show: false
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -102,9 +98,9 @@ const PieCard: FC = () => {
|
||||
avoidLabelOverlap: false,
|
||||
percentPrecision: 0,
|
||||
padAngle: 0,
|
||||
width: 220,
|
||||
height: 220,
|
||||
top: 32,
|
||||
width: 200,
|
||||
height: 200,
|
||||
top: 18,
|
||||
left: 'center',
|
||||
itemStyle: {
|
||||
borderRadius: 0
|
||||
@@ -129,13 +125,27 @@ const PieCard: FC = () => {
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '340px', width: '100%' }}
|
||||
style={{ height: '250px', width: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className="rb:relative rb:flex rb:items-center rb:justify-between rb:px-4 rb:py-2.5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:font-regular rb:leading-5 rb:rounded-md">
|
||||
<div className="rb:pl-3.5 rb:relative">
|
||||
<span
|
||||
className="rb:absolute rb:left-0 rb:top-[calc(50%-4px)] rb:w-2 rb:h-2 rb:rounded-full"
|
||||
style={{ backgroundColor: Colors[index % Colors.length] }}
|
||||
/>
|
||||
{item.name}
|
||||
</div>
|
||||
<div className="rb:font-medium">{totalValue > 0 ? Math.round((Number(item.value) / totalValue) * 100) : 0}%</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</>}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default PieCard
|
||||
export default InterestDistribution
|
||||
@@ -17,11 +17,11 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getInsightReport()
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getInsightReport = () => {
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getMemoryInsightReport(id).then((res) => {
|
||||
@@ -34,23 +34,18 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
||||
}
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
getInsightReport,
|
||||
getData,
|
||||
}));
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.memoryInsight')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bgColor="linear-gradient(180deg,#F1F9FE 0%, #FBFCFF 100%)"
|
||||
height="100%"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: report
|
||||
? <div className="rb:flex rb:flex-wrap rb:justify-between rb:h-full">
|
||||
<div className="rb:leading-5.5">
|
||||
{report|| '-'}
|
||||
</div>
|
||||
? <div className="rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3 rb:px-4 rb:text-[#5B6167] rb:leading-5">
|
||||
{report || '-'}
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Skeleton } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
getNodeStatistics,
|
||||
} from '@/api/memory'
|
||||
import type { NodeStatisticsItem } from '../types'
|
||||
|
||||
|
||||
const NodeStatistics: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [total, setTotal] = useState<number>(0)
|
||||
const [data, setData] = useState<NodeStatisticsItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getNodeStatistics(id).then((res) => {
|
||||
const response = res as NodeStatisticsItem[]
|
||||
setData(response)
|
||||
// 计算count总计
|
||||
const totalCount = response.reduce((sum, item) => sum + (item.count || 0), 0)
|
||||
setTotal(totalCount)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleViewDetail = (type: string) => {
|
||||
switch (type) {
|
||||
case 'EMOTIONAL_MEMORY':
|
||||
navigate(`/statement/${id}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<RbCard
|
||||
title={<>{t('userMemory.nodeStatistics')}<div>{t('userMemory.total')}: {total}</div></>}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data && data.length > 0
|
||||
? <div className={`rb:w-full rb:grid rb:grid-cols-3 rb:gap-2`}>
|
||||
{data.map(vo => (
|
||||
<div
|
||||
key={vo.type}
|
||||
className={clsx("rb:group rb:border rb:border-[#DFE4ED] rb:p-0 rb:rounded-xl rb:hover:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)]", {
|
||||
'rb:cursor-pointer': vo.type === 'EMOTIONAL_MEMORY'
|
||||
})}
|
||||
onClick={() => handleViewDetail(vo.type)}
|
||||
>
|
||||
<div className="rb:gap-0.5 rb:p-3 rb:leading-4 rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border-b rb:border-[#DFE4ED]">
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{t(`userMemory.${vo.type}`)}</div>
|
||||
{vo.type === 'EMOTIONAL_MEMORY' && <div
|
||||
className="rb:w-3 rb:h-3 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/home/arrow_top_right.svg')] rb:group-hover:bg-[url('@/assets/images/home/arrow_top_right_hover.svg')]"
|
||||
></div>}
|
||||
</div>
|
||||
|
||||
<div className="rb:p-3 rb:flex rb:justify-between rb:items-center rb:font-bold rb:text-[20px] rb:text-[#212332] rb:text-left">
|
||||
{vo.count ?? 0}
|
||||
<div className="rb:text-right rb:font-normal rb:text-[14px] rb:text-[#5F6266] rb:leading-4 rb:gap-1">
|
||||
{vo.percentage ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default NodeStatistics
|
||||
@@ -11,6 +11,17 @@ import {
|
||||
import type { NodeStatisticsItem } from '../types'
|
||||
|
||||
|
||||
const BG_LIST = [
|
||||
'rb:bg-[linear-gradient(316deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(316deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(314deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(314deg,rgba(255,93,52,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[]',
|
||||
]
|
||||
|
||||
const NodeStatistics: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation()
|
||||
@@ -49,40 +60,32 @@ const NodeStatistics: FC = () => {
|
||||
}
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.nodeStatistics')}
|
||||
extra={<div>{t('userMemory.total')}: {total}</div>}
|
||||
title={<>{t('userMemory.nodeStatistics')} <span className="rb:text-[#5B6167] rb:font-normal!">({t('userMemory.total')}: {total})</span></>}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bgColor="linear-gradient(180deg,#F1F9FE 0%, #FBFCFF 100%)"
|
||||
height="100%"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data && data.length > 0
|
||||
? <div className={`rb:w-full rb:grid rb:grid-cols-3 rb:gap-2`}>
|
||||
{data.map(vo => (
|
||||
? <div className={`rb:w-full rb:grid rb:grid-cols-8 rb:gap-3`}>
|
||||
{data.map((vo, index) => (
|
||||
<div
|
||||
key={vo.type}
|
||||
className={clsx("rb:group rb:border rb:border-[#DFE4ED] rb:p-0 rb:rounded-xl rb:hover:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)]", {
|
||||
className={clsx("rb:flex rb:flex-col rb:justify-between rb:group rb:border rb:border-[#DFE4ED] rb:h-45 rb:rounded-lg rb:pt-3 rb:px-4 rb:pb-5", {
|
||||
'rb:cursor-pointer': vo.type === 'EMOTIONAL_MEMORY'
|
||||
})}
|
||||
}, BG_LIST[index])}
|
||||
onClick={() => handleViewDetail(vo.type)}
|
||||
>
|
||||
<div className="rb:gap-0.5 rb:p-3 rb:leading-4 rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border-b rb:border-[#DFE4ED]">
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{t(`userMemory.${vo.type}`)}</div>
|
||||
<div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">
|
||||
{t(`userMemory.${vo.type}`)}
|
||||
</div>
|
||||
{vo.type === 'EMOTIONAL_MEMORY' && <div
|
||||
className="rb:w-3 rb:h-3 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/home/arrow_top_right.svg')] rb:group-hover:bg-[url('@/assets/images/home/arrow_top_right_hover.svg')]"
|
||||
></div>}
|
||||
</div>
|
||||
|
||||
<div className="rb:p-3 rb:flex rb:justify-between rb:items-center rb:font-bold rb:text-[20px] rb:text-[#212332] rb:text-left">
|
||||
{vo.count ?? 0}
|
||||
<div className="rb:text-right rb:font-normal rb:text-[14px] rb:text-[#5F6266] rb:leading-4 rb:gap-1">
|
||||
{vo.percentage ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:text-[28px] rb:leading-8.75 rb:font-extrabold">{vo.count ?? 0}</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
|
||||
38
web/src/views/UserMemoryDetail/components/PageHeader.tsx
Normal file
38
web/src/views/UserMemoryDetail/components/PageHeader.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Layout } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import logoutIcon from '@/assets/images/logout.svg'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
interface ConfigHeaderProps {
|
||||
name?: string;
|
||||
operation: ReactNode
|
||||
}
|
||||
const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
name,
|
||||
operation
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goBack = () => {
|
||||
navigate('/user-memory', { replace: true })
|
||||
}
|
||||
return (
|
||||
<Header className="rb:w-full rb:h-16 rb:flex rb:justify-between rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
||||
<div className="rb:h-8 rb:flex rb:items-center rb:font-medium">
|
||||
{t('userMemory.memoryWindow', { name: name })}
|
||||
{operation}
|
||||
</div>
|
||||
|
||||
<div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goBack}>
|
||||
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
|
||||
{t('common.return')}
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageHeader;
|
||||
@@ -1,25 +1,18 @@
|
||||
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Col } from 'antd'
|
||||
import { Col, Row } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import zoom from '@/assets/images/userMemory/zoom.svg'
|
||||
import drag from '@/assets/images/userMemory/drag.svg'
|
||||
import pointer from '@/assets/images/userMemory/pointer.svg'
|
||||
import empty from '@/assets/images/userMemory/empty.svg'
|
||||
import detailEmpty from '@/assets/images/userMemory/detail_empty.png'
|
||||
import type { Node, Edge, GraphData } from '../types'
|
||||
import {
|
||||
getMemorySearchEdges,
|
||||
} from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const operations = [
|
||||
{ name: 'click', icon: pointer },
|
||||
{ name: 'drag', icon: drag },
|
||||
{ name: 'zoom', icon: zoom },
|
||||
]
|
||||
const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048']
|
||||
const RelationshipNetwork:FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -30,6 +23,7 @@ const RelationshipNetwork:FC = () => {
|
||||
const [links, setLinks] = useState<Edge[]>([])
|
||||
const [categories, setCategories] = useState<{ name: string }[]>([])
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
|
||||
// const [fullScreen, setFullScreen] = useState<boolean>(false)
|
||||
|
||||
console.log('categories', categories)
|
||||
// 关系网络
|
||||
@@ -136,16 +130,30 @@ const RelationshipNetwork:FC = () => {
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
// const handleFullScreen = () => {
|
||||
// setFullScreen(prev => !prev)
|
||||
// }
|
||||
|
||||
console.log('selectedNode', selectedNode)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
{/* 关系网络 */}
|
||||
<Col span={24}>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={t('userMemory.relationshipNetwork')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
// extra={
|
||||
// <div
|
||||
// onClick={handleFullScreen}
|
||||
// className="rb:group rb:cursor-pointer rb:hover:text-[#212332] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:flex rb:items-center rb:gap-1"
|
||||
// >
|
||||
// <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/fullScreen.svg')] rb:hover:bg-[url('@/assets/images/fullScreen_hover.svg')]"></div>
|
||||
// {t('userMemory.fullScreen')}
|
||||
// </div>
|
||||
// }
|
||||
>
|
||||
<div className="rb:h-124">
|
||||
<div className="rb:h-129.5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm">
|
||||
{nodes.length === 0 ? (
|
||||
<Empty className="rb:h-full" />
|
||||
) : (
|
||||
@@ -157,7 +165,7 @@ const RelationshipNetwork:FC = () => {
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
bottom: 20,
|
||||
bottom: 12,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -207,12 +215,12 @@ const RelationshipNetwork:FC = () => {
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '496px', width: '100%' }}
|
||||
style={{ height: '518px', width: '100%' }}
|
||||
notMerge={false}
|
||||
lazyUpdate={true}
|
||||
onEvents={{
|
||||
// 节点点击事件处理
|
||||
click: (params: { dataType: string; data: Node }) => {
|
||||
click: (params: { dataType: string; data: Node; name: string }) => {
|
||||
if (params.dataType === 'node') {
|
||||
// 处理节点点击事件
|
||||
console.log('Node clicked:', params.data);
|
||||
@@ -224,57 +232,52 @@ const RelationshipNetwork:FC = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:flex rb:items-center rb:gap-6 rb:rounded-[0px_0px_12px_12px] rb:p-[14px_40px] rb:m-[0_-20px_-16px_-16px]">
|
||||
{operations.map((item) => (
|
||||
<div key={item.name} className="rb:flex rb:items-center rb:text-[#5B6167] rb:leading-5">
|
||||
<img src={item.icon} className="rb:w-5 rb:h-5 rb:mr-1" />
|
||||
{t(`userMemory.${item.name}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
{/* 记忆详情 */}
|
||||
<Col span={24}>
|
||||
<Col span={8}>
|
||||
<RbCard
|
||||
title={t('userMemory.memoryDetails')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bodyClassName='rb:p-0!'
|
||||
>
|
||||
{!selectedNode
|
||||
? <Empty
|
||||
url={empty}
|
||||
title={t('userMemory.memoryDetailEmpty')}
|
||||
subTitle={t('userMemory.memoryDetailEmptyDesc')}
|
||||
className="rb:mb-3"
|
||||
size={88}
|
||||
/>
|
||||
: <>
|
||||
|
||||
<div className="rb:font-medium rb:mb-2">
|
||||
{t('userMemory.memoryContent')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">
|
||||
{['Chunk', 'Dialogue', 'MemorySummary'].includes(selectedNode.label) && 'content' in selectedNode.properties
|
||||
? selectedNode.properties.content
|
||||
: selectedNode.label === 'ExtractedEntity' && 'description' in selectedNode.properties
|
||||
? selectedNode.properties.description
|
||||
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
|
||||
? selectedNode.properties.statement
|
||||
: ''
|
||||
}
|
||||
<div className="rb:h-133.5">
|
||||
{!selectedNode
|
||||
? <Empty
|
||||
url={detailEmpty}
|
||||
subTitle={t('userMemory.memoryDetailEmptyDesc')}
|
||||
className="rb:h-full rb:mx-10 rb:text-center"
|
||||
size={90}
|
||||
/>
|
||||
: <>
|
||||
<div className="rb:bg-[#F6F8FC] rb:border-t rb:border-b rb:border-[#DFE4ED] rb:font-medium rb:py-2 rb:px-4 rb:h-10">{selectedNode.name}</div>
|
||||
<div className="rb:p-4">
|
||||
<>
|
||||
<div className="rb:font-medium rb:leading-5">{t('userMemory.memoryContent')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
{['Chunk', 'Dialogue', 'MemorySummary'].includes(selectedNode.label) && 'content' in selectedNode.properties
|
||||
? selectedNode.properties.content
|
||||
: selectedNode.label === 'ExtractedEntity' && 'description' in selectedNode.properties
|
||||
? selectedNode.properties.description
|
||||
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
|
||||
? selectedNode.properties.statement
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
<div className="rb:font-medium rb:mb-2 rb:mt-4">
|
||||
<div className="rb:font-medium rb:leading-5">{t('userMemory.created_at')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4">
|
||||
{dayjs(selectedNode?.properties.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:font-medium rb:mb-2">
|
||||
{t('userMemory.created_at')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">
|
||||
{dayjs(selectedNode?.properties.created_at).format('YYYY/MM/DD HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
</>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
// 使用React.memo包装组件,避免不必要的渲染
|
||||
|
||||
@@ -121,16 +121,23 @@ export interface NodeStatisticsItem {
|
||||
export interface EndUser {
|
||||
end_user_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
other_name: string;
|
||||
position: string;
|
||||
department: string;
|
||||
contact: string;
|
||||
phone: string;
|
||||
hire_date: string | number | Dayjs | null;
|
||||
updatetime_profile?: number;
|
||||
}
|
||||
export interface EndUserProfileModalRef {
|
||||
handleOpen: (vo: EndUser) => void;
|
||||
}
|
||||
export interface MemoryInsightRef {
|
||||
getInsightReport: () => void
|
||||
getData: () => void
|
||||
}
|
||||
export interface AboutMeRef {
|
||||
getData: () => void
|
||||
}
|
||||
export interface EndUserProfileRef {
|
||||
data: EndUser | null
|
||||
}
|
||||
Reference in New Issue
Block a user