From e11c1bb2332b58dfb5da6aefbb0b38297077ed20 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Fri, 26 Dec 2025 19:14:26 +0800 Subject: [PATCH] feat(web): neo4j type user memory detail --- web/src/assets/images/fullScreen.svg | 24 +++ web/src/assets/images/fullScreen_hover.svg | 24 +++ web/src/assets/images/refresh.svg | 2 +- web/src/assets/images/refresh_hover.svg | 15 ++ .../assets/images/userMemory/detail_empty.png | Bin 0 -> 4184 bytes web/src/components/Layout/AuthLayout.tsx | 3 +- web/src/components/Layout/BasicLayout.tsx | 5 +- web/src/i18n/en.ts | 7 +- web/src/i18n/zh.ts | 9 +- web/src/routes/index.tsx | 1 + web/src/routes/routes.json | 3 +- web/src/store/menu.json | 14 +- web/src/views/UserMemory/index.tsx | 11 +- web/src/views/UserMemoryDetail/Neo4j.tsx | 197 ++++++------------ .../UserMemoryDetail/components/AboutMe.tsx | 56 +++++ .../components/EndUserProfile.tsx | 55 +++-- .../{PieCard.tsx => InterestDistribution.tsx} | 56 +++-- .../components/MemoryInsight.tsx | 15 +- .../components/NodeStatistics copy.tsx | 88 ++++++++ .../components/NodeStatistics.tsx | 41 ++-- .../components/PageHeader.tsx | 38 ++++ .../components/RelationshipNetwork.tsx | 121 +++++------ web/src/views/UserMemoryDetail/types.ts | 11 +- 23 files changed, 516 insertions(+), 280 deletions(-) create mode 100644 web/src/assets/images/fullScreen.svg create mode 100644 web/src/assets/images/fullScreen_hover.svg create mode 100644 web/src/assets/images/refresh_hover.svg create mode 100644 web/src/assets/images/userMemory/detail_empty.png create mode 100644 web/src/views/UserMemoryDetail/components/AboutMe.tsx rename web/src/views/UserMemoryDetail/components/{PieCard.tsx => InterestDistribution.tsx} (70%) create mode 100644 web/src/views/UserMemoryDetail/components/NodeStatistics copy.tsx create mode 100644 web/src/views/UserMemoryDetail/components/PageHeader.tsx diff --git a/web/src/assets/images/fullScreen.svg b/web/src/assets/images/fullScreen.svg new file mode 100644 index 00000000..a2fae058 --- /dev/null +++ b/web/src/assets/images/fullScreen.svg @@ -0,0 +1,24 @@ + + + 全屏 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/fullScreen_hover.svg b/web/src/assets/images/fullScreen_hover.svg new file mode 100644 index 00000000..fe3b8361 --- /dev/null +++ b/web/src/assets/images/fullScreen_hover.svg @@ -0,0 +1,24 @@ + + + 全屏 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/refresh.svg b/web/src/assets/images/refresh.svg index 7fb0dc34..c592feff 100644 --- a/web/src/assets/images/refresh.svg +++ b/web/src/assets/images/refresh.svg @@ -2,7 +2,7 @@ 刷新 - + diff --git a/web/src/assets/images/refresh_hover.svg b/web/src/assets/images/refresh_hover.svg new file mode 100644 index 00000000..1d4dcf7c --- /dev/null +++ b/web/src/assets/images/refresh_hover.svg @@ -0,0 +1,15 @@ + + + 刷新 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/userMemory/detail_empty.png b/web/src/assets/images/userMemory/detail_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..676662351f68f280bca7e14fb59a23c051b854cf GIT binary patch literal 4184 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 literal 0 HcmV?d00001 diff --git a/web/src/components/Layout/AuthLayout.tsx b/web/src/components/Layout/AuthLayout.tsx index a969298d..94d28e11 100644 --- a/web/src/components/Layout/AuthLayout.tsx +++ b/web/src/components/Layout/AuthLayout.tsx @@ -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() } }, []); diff --git a/web/src/components/Layout/BasicLayout.tsx b/web/src/components/Layout/BasicLayout.tsx index 76ab1690..6b3d2904 100644 --- a/web/src/components/Layout/BasicLayout.tsx +++ b/web/src/components/Layout/BasicLayout.tsx @@ -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 (
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index f77955f5..8ab3765a 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -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', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 5efcdfa6..f664fa14 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -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', diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index a4c20163..9a2ea17d 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -38,6 +38,7 @@ const componentMap: Record>> = 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')), diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index a414185d..9d3e9cb8 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -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" } ] }, { diff --git a/web/src/store/menu.json b/web/src/store/menu.json index 1c4cec9d..8311e27d 100644 --- a/web/src/store/menu.json +++ b/web/src/store/menu.json @@ -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 } ] }, diff --git a/web/src/views/UserMemory/index.tsx b/web/src/views/UserMemory/index.tsx index b325c52d..af7db5e1 100644 --- a/web/src/views/UserMemory/index.tsx +++ b/web/src/views/UserMemory/index.tsx @@ -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 = { export default function UserMemory() { const { t } = useTranslation(); const navigate = useNavigate() + const { storageType } = useUser() const configModalRef = useRef(null) const [loading, setLoading] = useState(false); const [data, setData] = useState([]); @@ -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 diff --git a/web/src/views/UserMemoryDetail/Neo4j.tsx b/web/src/views/UserMemoryDetail/Neo4j.tsx index 784f962e..75cde3f9 100644 --- a/web/src/views/UserMemoryDetail/Neo4j.tsx +++ b/web/src/views/UserMemoryDetail/Neo4j.tsx @@ -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 = ({ type, title, icon, t, expanded, onClick }) => ( -
- - - {title} - - - onClick(type)}> - {t(`userMemory.${expanded ? 'foldUp' : 'expanded'}`)} - - -
-) +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(null) const memoryInsightRef = useRef(null) - const [expanded, setExpanded] = useState(['aboutUs', 'interestDistribution', 'importantRelationships', 'importantMomentsInLife']) - const [summary, setSummary] = useState(null) - const [loading, setLoading] = useState>({ - summary: false, - refresh: false - }) + const aboutMeRef = useRef(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 ( -
- - - - - - - - - - - - {/* 关于我 */} - <> - - {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> ) } diff --git a/web/src/views/UserMemoryDetail/components/AboutMe.tsx b/web/src/views/UserMemoryDetail/components/AboutMe.tsx new file mode 100644 index 00000000..ba7e68fe --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/AboutMe.tsx @@ -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 \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx b/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx index 144beff7..ac98d9aa 100644 --- a/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx +++ b/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx @@ -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 \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/PieCard.tsx b/web/src/views/UserMemoryDetail/components/InterestDistribution.tsx similarity index 70% rename from web/src/views/UserMemoryDetail/components/PieCard.tsx rename to web/src/views/UserMemoryDetail/components/InterestDistribution.tsx index 982a8be6..8a1edccb 100644 --- a/web/src/views/UserMemoryDetail/components/PieCard.tsx +++ b/web/src/views/UserMemoryDetail/components/InterestDistribution.tsx @@ -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 diff --git a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx index 983e8a41..0c0751f3 100644 --- a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx +++ b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx @@ -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} /> } diff --git a/web/src/views/UserMemoryDetail/components/NodeStatistics copy.tsx b/web/src/views/UserMemoryDetail/components/NodeStatistics copy.tsx new file mode 100644 index 00000000..f5f7df34 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/NodeStatistics copy.tsx @@ -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 \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx b/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx index 7bfd19f3..288e11c0 100644 --- a/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx +++ b/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx @@ -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} /> } diff --git a/web/src/views/UserMemoryDetail/components/PageHeader.tsx b/web/src/views/UserMemoryDetail/components/PageHeader.tsx new file mode 100644 index 00000000..a1a0a2ea --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/PageHeader.tsx @@ -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; \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx index c2e588ef..db7c9e57 100644 --- a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx +++ b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx @@ -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包装组件,避免不必要的渲染 diff --git a/web/src/views/UserMemoryDetail/types.ts b/web/src/views/UserMemoryDetail/types.ts index 2c698b38..8fd050a9 100644 --- a/web/src/views/UserMemoryDetail/types.ts +++ b/web/src/views/UserMemoryDetail/types.ts @@ -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 } \ No newline at end of file