feat(web): add graph detail page

This commit is contained in:
zhaoying
2026-01-13 14:04:28 +08:00
parent 2f13cb4cbc
commit 1ebab759b1
5 changed files with 86 additions and 75 deletions

View File

@@ -3,8 +3,7 @@ import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react'; import ReactEcharts from 'echarts-for-react';
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import Loading from '@/components/Empty/Loading' import Loading from '@/components/Empty/Loading'
import type { Emotion } from './GraphDetail' import type { Emotion } from '../pages/GraphDetail'
import { format } from 'echarts';
interface EmotionLineProps { interface EmotionLineProps {
chartData: Emotion[]; chartData: Emotion[];

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react' import ReactEcharts from 'echarts-for-react'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import Loading from '@/components/Empty/Loading' import Loading from '@/components/Empty/Loading'
import type { Interaction } from './GraphDetail' import type { Interaction } from '../pages/GraphDetail'
interface InteractionBarProps { interface InteractionBarProps {
chartData: Interaction[]; chartData: Interaction[];

View File

@@ -1,19 +1,18 @@
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react' import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { Col, Row, Space, Button } from 'antd' import { Col, Row, Space, Button } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import ReactEcharts from 'echarts-for-react' import ReactEcharts from 'echarts-for-react'
import detailEmpty from '@/assets/images/userMemory/detail_empty.png' import detailEmpty from '@/assets/images/userMemory/detail_empty.png'
import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties, GraphDetailRef } from '../types' import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties } from '../types'
import { import {
getMemorySearchEdges, getMemorySearchEdges,
} from '@/api/memory' } from '@/api/memory'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import GraphDetail from '../components/GraphDetail'
const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048'] const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048']
const RelationshipNetwork:FC = () => { const RelationshipNetwork:FC = () => {
@@ -26,7 +25,7 @@ const RelationshipNetwork:FC = () => {
const [categories, setCategories] = useState<{ name: string }[]>([]) const [categories, setCategories] = useState<{ name: string }[]>([])
const [selectedNode, setSelectedNode] = useState<Node | null>(null) const [selectedNode, setSelectedNode] = useState<Node | null>(null)
// const [fullScreen, setFullScreen] = useState<boolean>(false) // const [fullScreen, setFullScreen] = useState<boolean>(false)
const graphDetailRef = useRef<GraphDetailRef>(null) const navigate = useNavigate()
console.log('categories', categories) console.log('categories', categories)
// 关系网络 // 关系网络
@@ -133,15 +132,14 @@ const RelationshipNetwork:FC = () => {
} }
}, [nodes]) }, [nodes])
// const handleFullScreen = () => {
// setFullScreen(prev => !prev)
// }
console.log('selectedNode', selectedNode)
const handleViewAll = () => { const handleViewAll = () => {
if (!selectedNode) return if (!selectedNode) return
graphDetailRef.current?.handleOpen(selectedNode) const params = new URLSearchParams({
nodeId: selectedNode.id,
nodeLabel: selectedNode.label,
nodeName: selectedNode.name || ''
})
navigate(`/user-memory/detail/${id}/GRAPH?${params.toString()}`)
} }
return ( return (
@@ -336,8 +334,6 @@ const RelationshipNetwork:FC = () => {
</div> </div>
</RbCard> </RbCard>
</Col> </Col>
<GraphDetail ref={graphDetailRef} />
</Row> </Row>
) )
} }

View File

@@ -1,16 +1,17 @@
import { useState, forwardRef, useImperativeHandle, useMemo } from 'react' import { useState, forwardRef, useImperativeHandle, useMemo, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'react-router-dom'
import { Row, Col, Tabs, Space, Skeleton } from 'antd' import { Row, Col, Tabs, Space, Skeleton } from 'antd'
import { getRelationshipEvolution, getTimelineMemories } from '@/api/memory' import { getRelationshipEvolution, getTimelineMemories } from '@/api/memory'
import type { Node, GraphDetailRef } from '../types' import type { Node, GraphDetailRef } from '../types'
import RbDrawer from '@/components/RbDrawer'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import EmotionLine from './EmotionLine' import EmotionLine from '../components/EmotionLine'
import { formatDateTime } from '@/utils/format' import { formatDateTime } from '@/utils/format'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import InteractionBar from './InteractionBar' import InteractionBar from '../components/InteractionBar'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import PageHeader from '../components/PageHeader'
export interface Emotion { export interface Emotion {
emotion_intensity: number; emotion_intensity: number;
@@ -35,7 +36,7 @@ interface Timeline {
const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => { const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false); const [searchParams] = useSearchParams()
const [vo, setVo] = useState<Node | null>(null) const [vo, setVo] = useState<Node | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [emotionData, setEmotionData] = useState<Emotion[]>([]) const [emotionData, setEmotionData] = useState<Emotion[]>([])
@@ -43,14 +44,23 @@ const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
const [activeTab, setActiveTab] = useState('timelines_memory') const [activeTab, setActiveTab] = useState('timelines_memory')
const [timelineLoading, setTimelineLoading] = useState(false) const [timelineLoading, setTimelineLoading] = useState(false)
const [timelineMemories, setTimelineMemories] = useState<Timeline>({ timelines_memory: [], MemorySummary: [], Statement: [], ExtractedEntity: []}) const [timelineMemories, setTimelineMemories] = useState<Timeline>({ timelines_memory: [], MemorySummary: [], Statement: [], ExtractedEntity: []})
useEffect(() => {
const nodeId = searchParams.get('nodeId')
const nodeLabel = searchParams.get('nodeLabel')
const nodeName = searchParams.get('nodeName')
if (nodeId && nodeLabel) {
const nodeFromUrl = {
id: nodeId,
label: nodeLabel,
name: nodeName || nodeLabel
}
handleOpen(nodeFromUrl as Node)
}
}, [searchParams])
const handleCancel = () => {
setVo(null)
setOpen(false)
}
const handleOpen = (vo: Node) => { const handleOpen = (vo: Node) => {
setActiveTab('timelines_memory') setActiveTab('timelines_memory')
setOpen(true)
setVo(vo) setVo(vo)
getRelationshipEvolutionData(vo) getRelationshipEvolutionData(vo)
getTimelineMemoriesData(vo) getTimelineMemoriesData(vo)
@@ -85,56 +95,57 @@ const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
}, [activeTab, timelineMemories]) }, [activeTab, timelineMemories])
return ( return (
<RbDrawer <>
title={vo?.name} <PageHeader
open={open} name={vo?.name}
onClose={handleCancel} source="node"
width={1000} />
> <div className="rb:h-full rb:max-w-266 rb:mx-auto">
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('userMemory.relationshipEvolution')}</div> <div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('userMemory.relationshipEvolution')}</div>
<RbCard> <RbCard>
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<EmotionLine chartData={emotionData} loading={loading} /> <EmotionLine chartData={emotionData} loading={loading} />
</Col> </Col>
<Col span={12}> <Col span={12}>
<InteractionBar chartData={interactionData} loading={loading} /> <InteractionBar chartData={interactionData} loading={loading} />
</Col> </Col>
</Row> </Row>
</RbCard> </RbCard>
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3 rb:mt-6">{t('userMemory.timelineMemories')}</div> <div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3 rb:mt-6">{t('userMemory.timelineMemories')}</div>
<RbCard> <RbCard>
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
items={['timelines_memory', 'ExtractedEntity', 'Statement', 'MemorySummary'].map(key => ({ items={['timelines_memory', 'ExtractedEntity', 'Statement', 'MemorySummary'].map(key => ({
label: t(`userMemory.${key}`), label: t(`userMemory.${key}`),
key key
}))} }))}
onChange={(key: string) => setActiveTab(key)} onChange={(key: string) => setActiveTab(key)}
/> />
{timelineLoading {timelineLoading
? <Skeleton active /> ? <Skeleton active />
: !activeContent || activeContent.length === 0 : !activeContent || activeContent.length === 0
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" /> ? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
: <Space size={16} direction="vertical" className="rb:w-full"> : <Space size={16} direction="vertical" className="rb:w-full">
{activeContent.map((vo, index) => ( {activeContent.map((vo, index) => (
<RbCard <RbCard
key={index} key={index}
headerType="borderL" headerType="borderL"
headerClassName="rb:before:bg-[#155EEF]!" headerClassName="rb:before:bg-[#155EEF]!"
title={vo.text} title={vo.text}
> >
<div className="rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4">{formatDateTime(vo.created_at)}</div> <div className="rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4">{formatDateTime(vo.created_at)}</div>
<Tag className="rb:mt-2">{vo.type}</Tag> <Tag className="rb:mt-2">{vo.type}</Tag>
</RbCard> </RbCard>
))} ))}
</Space> </Space>
} }
</RbCard> </RbCard>
</RbDrawer> </div>
</>
) )
}) })
export default GraphDetail export default GraphDetail

View File

@@ -1,7 +1,7 @@
import { type FC, useEffect, useState, useMemo, useRef } from 'react' import { type FC, useEffect, useState, useMemo, useRef } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Dropdown, Space, Button } from 'antd' import { Dropdown, Button } from 'antd'
import PageHeader from '../components/PageHeader' import PageHeader from '../components/PageHeader'
import StatementDetail from './StatementDetail' import StatementDetail from './StatementDetail'
@@ -16,6 +16,7 @@ import {
getEndUserProfile, getEndUserProfile,
} from '@/api/memory' } from '@/api/memory'
import refreshIcon from '@/assets/images/refresh_hover.svg' import refreshIcon from '@/assets/images/refresh_hover.svg'
import GraphDetail from './GraphDetail'
const Detail: FC = () => { const Detail: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -47,6 +48,10 @@ const Detail: FC = () => {
forgetDetailRef.current?.handleRefresh() forgetDetailRef.current?.handleRefresh()
} }
if (type === 'GRAPH') {
return <GraphDetail />
}
return ( return (
<div className="rb:h-full rb:w-full"> <div className="rb:h-full rb:w-full">
<PageHeader <PageHeader