feat(web): neo4j type user memory detail
This commit is contained in:
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包装组件,避免不必要的渲染
|
||||
|
||||
Reference in New Issue
Block a user