Merge branch 'feature/20251219_zy' into develop_web
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, App, Button, Switch, Space, Select } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg'
|
||||
import { getMemoryReflectionConfig, updateMemoryReflectionConfig, pilotRunMemoryReflectionConfig } from '@/api/memory'
|
||||
@@ -139,6 +140,9 @@ const SelfReflectionEngine: React.FC = () => {
|
||||
.then((res) => {
|
||||
setResult(res as Result)
|
||||
})
|
||||
.catch(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
|
||||
@@ -8,22 +8,15 @@ 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 type { Data } from './types'
|
||||
import {
|
||||
getUserSummary,
|
||||
getUserProfile,
|
||||
getTotalMemoryCountByUser,
|
||||
} from '@/api/memory'
|
||||
import RelationshipNetwork from './components/RelationshipNetwork'
|
||||
import MemoryInsight from './components/MemoryInsight'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
import WordCloud from './components/WordCloud'
|
||||
import EmotionTags from './components/EmotionTags'
|
||||
import Health from './components/Health'
|
||||
import Suggestions from './components/Suggestions'
|
||||
|
||||
const tagColors = ['21, 94, 239', '156, 111, 255', '255, 93, 52', '54, 159, 33']
|
||||
import NodeStatistics from './components/NodeStatistics'
|
||||
import EndUserProfile from './components/EndUserProfile'
|
||||
|
||||
interface TitleProps {
|
||||
type: string;
|
||||
@@ -34,15 +27,15 @@ interface TitleProps {
|
||||
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-[17px] rb:border-b-[1px] rb:border-[#DFE4ED] rb:text-[16px] rb:font-semibold rb:leading-[22px]">
|
||||
<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-[20px] rb:h-[20px] rb:mr-[8px]" />
|
||||
<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-[20px]" onClick={() => onClick(type)}>
|
||||
<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-[16px] rb:h-[16px] rb:ml-[4px]", {
|
||||
<img src={down} className={clsx("rb:w-4 rb:h-4 rb:ml-1", {
|
||||
'rb:rotate-180': !expanded,
|
||||
})} />
|
||||
</span>
|
||||
@@ -52,47 +45,20 @@ const Title: FC<TitleProps> = ({ type, title, icon, t, expanded, onClick }) => (
|
||||
const Neo4j: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [data, setData] = useState<Data | null>(null)
|
||||
const [expanded, setExpanded] = useState<string[]>(['aboutUs', 'interestDistribution', 'importantRelationships', 'importantMomentsInLife'])
|
||||
const [summary, setSummary] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({
|
||||
detail: false,
|
||||
summary: false,
|
||||
})
|
||||
const [memory, setMemory] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getMemory()
|
||||
getSummary()
|
||||
getDetail()
|
||||
}, [id])
|
||||
|
||||
const handleTitleClick = (key: string) => {
|
||||
setExpanded(expanded.includes(key) ? expanded.filter((item) => item !== key) : [...expanded, key])
|
||||
}
|
||||
// 用户记忆详情
|
||||
const getDetail = () => {
|
||||
if (!id) return
|
||||
setLoading(prev => ({ ...prev, detail: true }))
|
||||
getUserProfile(id).then((res) => {
|
||||
setData((res as Data))
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(prev => ({ ...prev, detail: false }))
|
||||
})
|
||||
}
|
||||
// 记忆总览
|
||||
const getMemory = () => {
|
||||
if (!id) return
|
||||
setLoading(prev => ({ ...prev, memory: true }))
|
||||
getTotalMemoryCountByUser(id).then((res) => {
|
||||
setMemory(res.total)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(prev => ({ ...prev, memory: false }))
|
||||
})
|
||||
}
|
||||
// 用户摘要
|
||||
const getSummary = () => {
|
||||
if (!id) return
|
||||
@@ -105,42 +71,15 @@ const Neo4j: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const name = loading.detail ? '' : data?.name && data?.name !== '' ? data.name : id
|
||||
return (
|
||||
<Row gutter={[16, 16]} className="rb:pb-6">
|
||||
<Col span={8}>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<EndUserProfile />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<RbCard>
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:flex-[0_0_auto] rb:w-[80px] rb:h-[80px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[80px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:ml-[16px]">
|
||||
{name}<br/>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] rb:mt-[8px]">{data?.tags?.join(' | ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:flex rb:gap-[8px] rb:mb-[8px] rb:flex-wrap rb:mt-[25px]">
|
||||
{data?.hot_tags?.map((tag, tagIndex) => (
|
||||
<span key={tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-[22px] rb:border"
|
||||
style={{
|
||||
backgroundColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.08)`,
|
||||
borderColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.3)`,
|
||||
color: `rgba(${tagColors[tagIndex % tagColors.length]}, 1)`,
|
||||
}}
|
||||
>
|
||||
{tag.name}({tag.frequency})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 记忆总量 */}
|
||||
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:mb-[25px]">
|
||||
{t('userMemory.totalNumOfMemories')}
|
||||
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-[30px] rb:mt-[8px]">{memory || 0}</div>
|
||||
</div>
|
||||
|
||||
{/* 关于我 */}
|
||||
<>
|
||||
<Title
|
||||
@@ -154,12 +93,12 @@ const Neo4j: FC = () => {
|
||||
{expanded.includes('aboutUs') && (
|
||||
<>
|
||||
{loading.summary
|
||||
? <Skeleton className="rb:mt-[16px]" />
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: summary
|
||||
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
|
||||
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
|
||||
{summary || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
@@ -182,28 +121,19 @@ const Neo4j: FC = () => {
|
||||
</>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<EmotionTags />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<NodeStatistics />
|
||||
</Col>
|
||||
{/* 记忆洞察 */}
|
||||
<Col span={24}>
|
||||
<MemoryInsight />
|
||||
</Col>
|
||||
{/* 关系网络 + 记忆详情 */}
|
||||
<RelationshipNetwork />
|
||||
<Col span={12}>
|
||||
<WordCloud />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Health />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Suggestions />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -28,15 +28,15 @@ interface TitleProps {
|
||||
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-[17px] rb:border-b-[1px] rb:border-[#DFE4ED] rb:text-[16px] rb:font-semibold rb:leading-[22px]">
|
||||
<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-[20px] rb:h-[20px] rb:mr-[8px]" />
|
||||
<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-[20px]" onClick={() => onClick(type)}>
|
||||
<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-[16px] rb:h-[16px] rb:ml-[4px]", {
|
||||
<img src={down} className={clsx("rb:w-4 rb:h-4 rb:ml-1", {
|
||||
'rb:rotate-180': !expanded,
|
||||
})} />
|
||||
</span>
|
||||
@@ -115,20 +115,20 @@ const Rag: FC = () => {
|
||||
}
|
||||
const name = loading.detail ? '' : data?.name && data?.name !== '' ? data.name : id
|
||||
return (
|
||||
<Row gutter={[16, 16]} className="rb:pb-[24px]">
|
||||
<Row gutter={[16, 16]} className="rb:pb-6">
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:flex-[0_0_auto] rb:w-[80px] rb:h-[80px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[80px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:ml-[16px]">
|
||||
<div className="rb:flex-[0_0_auto] rb:w-20 rb:h-20 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-20 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-8 rb:ml-4">
|
||||
{name}<br/>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] rb:mt-[8px]">{personas?.join(' | ')}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mt-2">{personas?.join(' | ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:flex rb:gap-[8px] rb:mb-[8px] rb:flex-wrap rb:mt-[25px]">
|
||||
<div className="rb:flex rb:gap-2 rb:mb-2 rb:flex-wrap rb:mt-6.25">
|
||||
{tags?.map((tag, tagIndex) => (
|
||||
<span key={tag.tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-[22px] rb:border"
|
||||
<span key={tag.tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-5.5 rb:border"
|
||||
style={{
|
||||
backgroundColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.08)`,
|
||||
borderColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.3)`,
|
||||
@@ -141,9 +141,9 @@ const Rag: FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 记忆总量 */}
|
||||
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:mb-[25px]">
|
||||
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-6.25">
|
||||
{t('userMemory.totalNumOfMemories')}
|
||||
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-[30px] rb:mt-[8px]">{memory || 0}</div>
|
||||
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-7.5 rb:mt-2">{memory || 0}</div>
|
||||
</div>
|
||||
|
||||
{/* 关于我 */}
|
||||
@@ -159,12 +159,12 @@ const Rag: FC = () => {
|
||||
{expanded.includes('aboutUs') && (
|
||||
<>
|
||||
{loading.summary
|
||||
? <Skeleton className="rb:mt-[16px]" />
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: summary
|
||||
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
|
||||
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
|
||||
{summary || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
@@ -182,12 +182,12 @@ const Rag: FC = () => {
|
||||
{expanded.includes('memoryInsight') && (
|
||||
<>
|
||||
{loading.insight
|
||||
? <Skeleton className="rb:mt-[16px]" />
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: insight
|
||||
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
|
||||
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
|
||||
{insight || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -10,11 +10,11 @@ interface CardProps {
|
||||
|
||||
const Card: FC<CardProps> = ({ title, children, theme = 'default', className }) => {
|
||||
return (
|
||||
<div className={clsx('rb:h-full rb:border rb:rounded-[12px] rb:p-[16px] rb:border-[#DFE4ED]', {
|
||||
<div className={clsx('rb:h-full rb:border rb:rounded-xl rb:p-4 rb:border-[#DFE4ED]', {
|
||||
'rb:bg-[#FBFDFF]': theme === 'default',
|
||||
'rb:bg-[linear-gradient(180deg,_#F1F9FE_0%,_#FBFCFF_100%)]': theme === 'custom',
|
||||
'rb:bg-[linear-gradient(180deg,#F1F9FE_0%,#FBFCFF_100%)]': theme === 'custom',
|
||||
}, className)}>
|
||||
{title && <div className="rb:text-[18px] rb:font-semibold rb:leading-[25px] rb:pb-[16px]">{title}</div>}
|
||||
{title && <div className="rb:text-[18px] rb:font-semibold rb:leading-6.25 rb:pb-4">{title}</div>}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ const ConversationMemory:FC = () => {
|
||||
<List.Item>
|
||||
<div
|
||||
key={index}
|
||||
className="rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:rounded-lg rb:mt-2 rb:text-gray-800 rb:text-sm"
|
||||
className="rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:mt-2 rb:text-gray-800 rb:text-sm"
|
||||
>
|
||||
<Markdown content={item} />
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ interface TagList {
|
||||
const EmotionTags: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [tagList, setTagList] = useState<TagList | null>(null)
|
||||
const [data, setData] = useState<TagList | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getEmotionTagData()
|
||||
@@ -25,18 +25,18 @@ const EmotionTags: FC = () => {
|
||||
}
|
||||
getWordCloud(id)
|
||||
.then((res) => {
|
||||
setTagList(res as TagList)
|
||||
setData(res as TagList)
|
||||
})
|
||||
}
|
||||
|
||||
const [visibleCount, setVisibleCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!tagList || tagList?.keywords.length === 0) return
|
||||
if (!data || data?.keywords.length === 0) return
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setVisibleCount(prev => {
|
||||
if (prev >= tagList?.keywords.length) {
|
||||
if (prev >= data?.keywords.length) {
|
||||
clearInterval(timer)
|
||||
return prev
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const EmotionTags: FC = () => {
|
||||
}, 200)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [tagList?.keywords.length])
|
||||
}, [data?.keywords.length])
|
||||
|
||||
const getEmotionColor = (emotionType: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
@@ -59,19 +59,19 @@ const EmotionTags: FC = () => {
|
||||
return colors[emotionType] || '#8c8c8c'
|
||||
}
|
||||
|
||||
const emotionStats = tagList?.keywords.reduce((acc, item) => {
|
||||
const emotionStats = data?.keywords.reduce((acc, item) => {
|
||||
acc[item.emotion_type] = (acc[item.emotion_type] || 0) + item.frequency
|
||||
return acc
|
||||
}, {} as Record<string, number>) ?? {}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.emotionTags')}
|
||||
title={t('statementDetail.emotionTags')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bodyClassName='rb:p-0! rb:relative'
|
||||
bodyClassName='rb:p-0! rb:pb-3! rb:relative'
|
||||
>
|
||||
{tagList
|
||||
{data?.keywords && data?.keywords.length > 0
|
||||
? <>
|
||||
<div className="rb:flex rb:flex-wrap rb:items-center rb:gap-6 rb:text-sm rb:mt-3 rb:p-3 rb:bg-[#F0F3F8]">
|
||||
{Object.entries(emotionStats).map(([type, count]) => {
|
||||
@@ -79,13 +79,13 @@ const EmotionTags: FC = () => {
|
||||
return (
|
||||
<div key={type} className="rb:flex rb:items-center rb:gap-2">
|
||||
<div className="rb:w-3 rb:h-3 rb:rounded-full" style={{ backgroundColor: getEmotionColor(type) }}></div>
|
||||
<span className="rb:text-gray-600">{t(`emotionDetail.${type || 'neutral'}`)} ({count}个)</span>
|
||||
<span className="rb:text-gray-600">{t(`statementDetail.${type || 'neutral'}`)} ({count}个)</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="rb:mt-6 rb:flex rb:items-center rb:flex-wrap rb:gap-3 rb:mb-3 rb:px-6">
|
||||
{tagList.keywords.slice(0, visibleCount).map((item, index) => (
|
||||
{data.keywords.slice(0, visibleCount).map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rb:flex rb:items-center rb:justify-center rb:animate-fadeIn rb:px-4 rb:py-2 rb:rounded-full rb:text-white rb:font-medium"
|
||||
@@ -102,7 +102,7 @@ const EmotionTags: FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
: <Empty />
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
72
web/src/views/UserMemoryDetail/components/EndUserProfile.tsx
Normal file
72
web/src/views/UserMemoryDetail/components/EndUserProfile.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Descriptions, Button } 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'
|
||||
|
||||
const EndUserProfile:FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const endUserProfileModalRef = useRef<EndUserProfileModalRef>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<EndUser | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getEndUserProfile(id).then((res) => {
|
||||
setData(res as EndUser)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const formatItems = useCallback(() => {
|
||||
if (!data) return []
|
||||
return ['name', 'position', 'department', 'contact', 'phone', 'hire_date'].map(key => ({
|
||||
key,
|
||||
label: t(`userMemory.${key}`),
|
||||
children: key === 'hire_date' ? 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)
|
||||
}
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.endUserProfile')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
{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>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
<EndUserProfileModal
|
||||
ref={endUserProfileModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default EndUserProfile
|
||||
@@ -0,0 +1,128 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, DatePicker } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { EndUser, EndUserProfileModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { updatedEndUserProfile, } from '@/api/memory'
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface EndUserProfileModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const EndUserProfileModal = forwardRef<EndUserProfileModalRef, EndUserProfileModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<EndUser>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (user: EndUser) => {
|
||||
form.setFieldsValue({
|
||||
...user,
|
||||
end_user_id: user.id,
|
||||
hire_date: dayjs(user.hire_date)
|
||||
});
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
updatedEndUserProfile({
|
||||
...values,
|
||||
hire_date: values.hire_date.valueOf()
|
||||
})
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
refresh()
|
||||
handleClose()
|
||||
message.success(t('common.saveSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('common.edit')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem name="end_user_id" hidden></FormItem>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('userMemory.name')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="position"
|
||||
label={t('userMemory.position')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="department"
|
||||
label={t('userMemory.department')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="contact"
|
||||
label={t('userMemory.contact')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="phone"
|
||||
label={t('userMemory.phone')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="hire_date"
|
||||
label={t('userMemory.hire_date')}
|
||||
>
|
||||
<DatePicker className="rb:w-full" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default EndUserProfileModal;
|
||||
@@ -56,12 +56,11 @@ const Health: FC = () => {
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.health')}
|
||||
title={t('statementDetail.health')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
height="100%"
|
||||
>
|
||||
{health
|
||||
{health?.health_score && health?.health_score > 0
|
||||
? <>
|
||||
<div className="rb:flex rb:justify-center rb:items-center">
|
||||
<Progress
|
||||
@@ -78,20 +77,20 @@ const Health: FC = () => {
|
||||
|
||||
{health.dimensions && <>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
|
||||
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.positivity_rate')}</div>
|
||||
<div className="rb:w-40 rb:mr-3">{t('statementDetail.positivity_rate')}</div>
|
||||
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.positivity_rate.score} />
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-3 rb:mt-3">
|
||||
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.stability')}</div>
|
||||
<div className="rb:w-40 rb:mr-3">{t('statementDetail.stability')}</div>
|
||||
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.stability.score} />
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-3 rb:mt-3">
|
||||
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.resilience')}</div>
|
||||
<div className="rb:w-40 rb:mr-3">{t('statementDetail.resilience')}</div>
|
||||
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.resilience.score} />
|
||||
</div>
|
||||
</>}
|
||||
</>
|
||||
: <Empty />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ const MemoryInsight:FC = () => {
|
||||
? <Skeleton />
|
||||
: report
|
||||
? <div className="rb:flex rb:flex-wrap rb:justify-between rb:h-full">
|
||||
<div className="rb:leading-[22px]">
|
||||
<div className="rb:leading-5.5">
|
||||
{report|| '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
90
web/src/views/UserMemoryDetail/components/NodeStatistics.tsx
Normal file
90
web/src/views/UserMemoryDetail/components/NodeStatistics.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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 { nodes: NodeStatisticsItem[], total: number }
|
||||
setData(response.nodes)
|
||||
setTotal(response.total)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleViewDetail = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Statement':
|
||||
navigate(`/statement/${id}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.nodeStatistics')}
|
||||
extra={<div>{t('userMemory.total')}: {total}</div>}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bgColor="linear-gradient(180deg,#F1F9FE 0%, #FBFCFF 100%)"
|
||||
height="100%"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data.length > 0
|
||||
? <div className={`rb:w-full rb:grid rb:grid-cols-${data.length} 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 === 'Statement'
|
||||
})}
|
||||
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 === 'Statement' && <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
|
||||
@@ -59,7 +59,7 @@ const PieCard: FC = () => {
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !data || data.length === 0
|
||||
? <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
? <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
: data && data.length > 0 &&
|
||||
<ReactEcharts
|
||||
option={{
|
||||
|
||||
@@ -8,11 +8,12 @@ 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 type { EdgeData, Node, Edge } from '../types'
|
||||
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 },
|
||||
@@ -35,89 +36,76 @@ const RelationshipNetwork:FC = () => {
|
||||
if (!id) return
|
||||
setSelectedNode(null)
|
||||
getMemorySearchEdges(id).then((res) => {
|
||||
const list = (res as { detials?: EdgeData[] }).detials || []
|
||||
const nodes: Node[] = [];
|
||||
const links: Edge[] = [];
|
||||
const categories: { name: string }[] = []
|
||||
const { nodes, edges, statistics } = res as GraphData
|
||||
const curNodes: Node[] = []
|
||||
const curEdges: Edge[] = []
|
||||
const curNodeTypes = Object.keys(statistics.node_types)
|
||||
|
||||
list.forEach(item => {
|
||||
if (item.edge && item.edge.target_id && item.edge.source_id) {
|
||||
links.push({
|
||||
...item.edge,
|
||||
target: item.edge.target_id,
|
||||
source: item.edge.source_id,
|
||||
})
|
||||
}
|
||||
if (item.sourceNode) {
|
||||
nodes.push(item.sourceNode)
|
||||
categories.push({name: item.sourceNode.entity_type || 'Unknown'})
|
||||
}
|
||||
if (item.targetNode) {
|
||||
nodes.push(item.targetNode)
|
||||
categories.push({name: item.targetNode.entity_type || 'Unknown'})
|
||||
}
|
||||
// 计算每个节点的连接数
|
||||
const connectionCount: Record<string, number> = {}
|
||||
edges.forEach(edge => {
|
||||
connectionCount[edge.source] = (connectionCount[edge.source] || 0) + 1
|
||||
connectionCount[edge.target] = (connectionCount[edge.target] || 0) + 1
|
||||
})
|
||||
|
||||
// 根据ID字段去重节点
|
||||
const uniqueNodes = nodes.filter((node, index, self) =>
|
||||
index === self.findIndex((n) => n.id === node.id && n.name === node.name)
|
||||
)
|
||||
const uniqueLinks = links.filter((node, index, self) =>
|
||||
index === self.findIndex((n) => n.target === node.target && n.source === node.source)
|
||||
)
|
||||
const uniqueCategories = categories.filter((node, index, self) =>
|
||||
index === self.findIndex((n) => n.name === node.name)
|
||||
)
|
||||
|
||||
setLinks(uniqueLinks)
|
||||
setCategories(uniqueCategories)
|
||||
|
||||
// Calculate node frequency based on appearance in links
|
||||
const nodeFrequency = new Map<string, number>()
|
||||
|
||||
// Count each node's appearance in links (both as source and target)
|
||||
uniqueLinks.forEach(link => {
|
||||
// Increment source node frequency (only if source exists and is a string)
|
||||
if (typeof link.source === 'string') {
|
||||
nodeFrequency.set(link.source, (nodeFrequency.get(link.source) || 0) + 1)
|
||||
}
|
||||
// Increment target node frequency (only if target exists and is a string)
|
||||
if (typeof link.target === 'string') {
|
||||
nodeFrequency.set(link.target, (nodeFrequency.get(link.target) || 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
// Set minimum frequency to 1 for nodes not in any links
|
||||
uniqueNodes.forEach(node => {
|
||||
if (node.id && typeof node.id === 'string') {
|
||||
if (!nodeFrequency.has(node.id)) {
|
||||
nodeFrequency.set(node.id, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
uniqueNodes.map(item => {
|
||||
const index = uniqueCategories.findIndex((n) => n.name === (item.entity_type || 'Unknown'))
|
||||
item.category = index
|
||||
// 处理节点数据
|
||||
nodes.forEach(node => {
|
||||
const connections = connectionCount[node.id] || 0
|
||||
const categoryIndex = curNodeTypes.indexOf(node.label)
|
||||
|
||||
// Get frequency for the node, ensuring id is a string
|
||||
const frequency = (item.id && typeof item.id === 'string') ? (nodeFrequency.get(item.id) || 1) : 1
|
||||
|
||||
// Set symbolSize based on frequency
|
||||
// Adjust these thresholds based on expected frequency ranges
|
||||
if (frequency <= 1) {
|
||||
item.symbolSize = 5
|
||||
} else if (frequency <= 10) {
|
||||
item.symbolSize = 10
|
||||
} else if (frequency <= 15) {
|
||||
item.symbolSize = 15
|
||||
} else if (frequency <= 20) {
|
||||
item.symbolSize = 25
|
||||
// 根据节点类型获取显示名称
|
||||
let displayName = ''
|
||||
switch (node.label) {
|
||||
case 'Statement':
|
||||
displayName = 'statement' in node.properties ? node.properties.statement?.slice(0, 5) || '' : ''
|
||||
break
|
||||
case 'ExtractedEntity':
|
||||
displayName = 'name' in node.properties ? node.properties.name || '' : ''
|
||||
break
|
||||
default:
|
||||
displayName = 'content' in node.properties ? node.properties.content?.slice(0, 5) || '' : ''
|
||||
break
|
||||
}
|
||||
let symbolSize = 0
|
||||
if (connections <= 1) {
|
||||
symbolSize = 5
|
||||
} else if (connections <= 10) {
|
||||
symbolSize = 10
|
||||
} else if (connections <= 15) {
|
||||
symbolSize = 15
|
||||
} else if (connections <= 20) {
|
||||
symbolSize = 25
|
||||
} else {
|
||||
item.symbolSize = 35
|
||||
symbolSize = 35
|
||||
}
|
||||
|
||||
curNodes.push({
|
||||
...node,
|
||||
name: displayName,
|
||||
category: categoryIndex >= 0 ? categoryIndex : 0,
|
||||
symbolSize: symbolSize, // 根据连接数调整节点大小
|
||||
itemStyle: {
|
||||
color: ['#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21', '#FF5D34', '#FF8A4C', '#FFB048'][categoryIndex % 8]
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(uniqueNodes)
|
||||
|
||||
// 处理边数据
|
||||
edges.forEach(edge => {
|
||||
curEdges.push({
|
||||
...edge,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
value: edge.weight || 1
|
||||
})
|
||||
})
|
||||
|
||||
// 设置分类
|
||||
const curCategories = curNodeTypes.map(type => ({ name: type }))
|
||||
|
||||
setNodes(curNodes)
|
||||
setLinks(curEdges)
|
||||
setCategories(curCategories)
|
||||
})
|
||||
}, [id])
|
||||
useEffect(() => {
|
||||
@@ -147,7 +135,6 @@ const RelationshipNetwork:FC = () => {
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
console.log('nodes', nodes)
|
||||
return (
|
||||
<>
|
||||
{/* 关系网络 */}
|
||||
@@ -157,7 +144,7 @@ const RelationshipNetwork:FC = () => {
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
<div className="rb:h-[496px]">
|
||||
<div className="rb:h-124">
|
||||
{nodes.length === 0 ? (
|
||||
<Empty className="rb:h-full" />
|
||||
) : (
|
||||
@@ -175,8 +162,13 @@ const RelationshipNetwork:FC = () => {
|
||||
links: links || [],
|
||||
categories: categories || [],
|
||||
roam: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
formatter: '{b}',
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'source',
|
||||
color: '#5B6167',
|
||||
curveness: 0.3
|
||||
},
|
||||
force: {
|
||||
@@ -218,19 +210,17 @@ const RelationshipNetwork:FC = () => {
|
||||
// 处理节点点击事件
|
||||
console.log('Node clicked:', params.data);
|
||||
// 使用函数式更新避免状态依赖问题
|
||||
setSelectedNode(prevSelected =>
|
||||
prevSelected?.id === params.data.id ? null : params.data
|
||||
)
|
||||
setSelectedNode(params.data)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:flex rb:items-center rb:gap-[24px] rb:rounded-[0px_0px_12px_12px] rb:p-[14px_40px] rb:m-[0_-20px_-16px_-16px]">
|
||||
<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-[20px]">
|
||||
<img src={item.icon} className="rb:w-[20px] rb:h-[20px] rb:mr-[4px]" />
|
||||
<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>
|
||||
))}
|
||||
@@ -244,27 +234,35 @@ const RelationshipNetwork:FC = () => {
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
{(!selectedNode || (!selectedNode?.description && !selectedNode?.entity_type))
|
||||
{!selectedNode
|
||||
? <Empty
|
||||
url={empty}
|
||||
title={t('userMemory.memoryDetailEmpty')}
|
||||
subTitle={t('userMemory.memoryDetailEmptyDesc')}
|
||||
className="rb:mb-[12px]"
|
||||
className="rb:mb-3"
|
||||
size={88}
|
||||
/>
|
||||
: <>
|
||||
{selectedNode?.description &&
|
||||
<div className="rb:font-medium rb:mb-[8px]">
|
||||
{t('userMemory.description')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[8px]"> {selectedNode.description}</div>
|
||||
|
||||
<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>
|
||||
}
|
||||
{selectedNode?.entity_type &&
|
||||
<div className="rb:font-medium rb:mb-[8px]">
|
||||
{t('userMemory.entityType')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[8px]"> {selectedNode.entity_type}</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>
|
||||
</>
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
@@ -39,11 +39,11 @@ const Suggestions: FC = () => {
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.suggestions')}
|
||||
title={t('statementDetail.suggestions')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
{suggestions
|
||||
{suggestions?.suggestions && suggestions?.suggestions.length > 0
|
||||
? <>
|
||||
<RbAlert className="rb:mb-3">{suggestions.health_summary}</RbAlert>
|
||||
{suggestions.suggestions.map((item, index) => (
|
||||
@@ -54,7 +54,7 @@ const Suggestions: FC = () => {
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
: <Empty />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ const WordCloud: FC = () => {
|
||||
},
|
||||
radar: {
|
||||
indicator: radarData.map(item => ({
|
||||
name: t(`emotionDetail.${item.name}`),
|
||||
name: t(`statementDetail.${item.name}`),
|
||||
max: 100,
|
||||
min: 1
|
||||
}))
|
||||
@@ -99,12 +99,12 @@ const WordCloud: FC = () => {
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.wordCloud')}
|
||||
title={t('statementDetail.wordCloud')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
height="100%"
|
||||
>
|
||||
{wordCloud
|
||||
{wordCloud?.total_count && wordCloud?.total_count > 0
|
||||
? <div className="rb:flex rb:h-100">
|
||||
<ReactEcharts ref={chartRef} option={radarOption} style={{ width: '50%', height: '100%' }} />
|
||||
<div className="rb:w-[50%] rb:pl-4 rb:flex rb:flex-col rb:justify-center">
|
||||
@@ -113,8 +113,8 @@ const WordCloud: FC = () => {
|
||||
{wordCloud.tags.map(item => (
|
||||
<div key={item.emotion_type}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:font-medium">
|
||||
{t(`emotionDetail.${item.emotion_type}`)}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count}{t('emotionDetail.pieces')}</div>
|
||||
{t(`statementDetail.${item.emotion_type}`)}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count}{t('statementDetail.pieces')}</div>
|
||||
</div>
|
||||
<Progress size="small" percent={item.percentage} />
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@ const WordCloud: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <Empty />
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type FC } from 'react'
|
||||
import { Row, Col } from 'antd';
|
||||
import { Row, Col, Space } from 'antd';
|
||||
|
||||
import WordCloud from '../components/WordCloud'
|
||||
import EmotionTags from '../components/EmotionTags'
|
||||
@@ -7,17 +7,15 @@ import Health from '../components/Health'
|
||||
import Suggestions from '../components/Suggestions'
|
||||
|
||||
|
||||
const EmotionDetail: FC = () => {
|
||||
const StatementDetail: FC = () => {
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<WordCloud />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<EmotionTags />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Health />
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<WordCloud />
|
||||
<EmotionTags />
|
||||
<Health />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions />
|
||||
@@ -26,4 +24,4 @@ const EmotionDetail: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default EmotionDetail
|
||||
export default StatementDetail
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Dayjs } from "dayjs";
|
||||
|
||||
export interface Data {
|
||||
id: string | number
|
||||
name: string;
|
||||
@@ -39,30 +41,93 @@ export interface Data {
|
||||
}[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface BaseProperties {
|
||||
content: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface StatementNodeProperties {
|
||||
temporal_info: string;
|
||||
stmt_type: string;
|
||||
statement: string;
|
||||
valid_at: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface ExtractedEntityNodeProperties {
|
||||
description: string;
|
||||
name: string;
|
||||
entity_type: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface MemorySummaryNode {
|
||||
id: string;
|
||||
label: 'MemorySummary';
|
||||
category: number;
|
||||
symbolSize: number;
|
||||
itemStyle: {
|
||||
color: string;
|
||||
}
|
||||
name: string;
|
||||
properties: {
|
||||
content: string;
|
||||
created_at: number;
|
||||
}
|
||||
caption: string;
|
||||
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
description?: string;
|
||||
label: 'Dialogue' | 'ExtractedEntity' | 'Chunk' | 'MemorySummary' | 'Statement';
|
||||
category: number;
|
||||
symbolSize: number;
|
||||
name: string;
|
||||
connect_strength?: string;
|
||||
entity_idx: number;
|
||||
entity_type?: string;
|
||||
fact_summary?: string[];
|
||||
category?: number;
|
||||
symbolSize?: number;
|
||||
itemStyle: {
|
||||
color: string;
|
||||
}
|
||||
properties: BaseProperties | StatementNodeProperties | ExtractedEntityNodeProperties
|
||||
caption: string;
|
||||
}
|
||||
export interface Edge {
|
||||
statement: string;
|
||||
rel_id: string;
|
||||
source_id: string;
|
||||
predicate: string;
|
||||
target_id: string;
|
||||
statement_id: string;
|
||||
target?: string;
|
||||
source?: string;
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
properties: {
|
||||
run_id: string;
|
||||
group_id: string;
|
||||
created_at: string;
|
||||
expired_at: string;
|
||||
}
|
||||
caption: string;
|
||||
value: number;
|
||||
weight: number;
|
||||
}
|
||||
export interface EdgeData {
|
||||
sourceNode: Node;
|
||||
edge: Edge;
|
||||
targetNode: Node;
|
||||
export interface GraphData {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
statistics: {
|
||||
total_nodes: number;
|
||||
total_edges: number;
|
||||
node_types: Record<string, number>;
|
||||
edge_types: Record<string, number>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NodeStatisticsItem {
|
||||
type: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
export interface EndUser {
|
||||
end_user_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
position: string;
|
||||
department: string;
|
||||
contact: string;
|
||||
phone: string;
|
||||
hire_date: string | number | Dayjs
|
||||
}
|
||||
export interface EndUserProfileModalRef {
|
||||
handleOpen: (vo: EndUser) => void;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
setChatList([])
|
||||
}
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen()
|
||||
variableConfigModalRef.current?.handleOpen(variables)
|
||||
}
|
||||
const handleSave = (values: StartVariableItem[]) => {
|
||||
setVariables([...values])
|
||||
@@ -91,7 +91,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
}])
|
||||
setChatList(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: message,
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
}])
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ interface VariableEditModalProps {
|
||||
|
||||
const VariableConfigModal = forwardRef<VariableEditModalRef, VariableEditModalProps>(({
|
||||
refresh,
|
||||
variables
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{variables: StartVariableItem[]}>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialValues, setInitialValues] = useState<StartVariableItem[]>([])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
@@ -26,9 +26,10 @@ const VariableConfigModal = forwardRef<VariableEditModalRef, VariableEditModalPr
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
|
||||
const handleOpen = (values: StartVariableItem[]) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue({variables: values})
|
||||
setInitialValues([...values])
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
@@ -59,18 +60,18 @@ const VariableConfigModal = forwardRef<VariableEditModalRef, VariableEditModalPr
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
initialValues={{ variables: variables }}
|
||||
>
|
||||
<Form.List name="variables">
|
||||
{(fields) => (
|
||||
<>
|
||||
{fields.map(({ name }, index) => {
|
||||
const field = variables[index]
|
||||
const field = initialValues[index]
|
||||
return (
|
||||
<Form.Item
|
||||
key={name}
|
||||
name={[name, 'value']}
|
||||
label={field.type === 'boolean' ? undefined : `${field.name}·${field.description}`}
|
||||
valuePropName={field.type === 'boolean' ? 'checked' : 'value'}
|
||||
rules={[
|
||||
{ required: field.required, message: field.type === 'boolean' ? t('common.pleaseSelect') : t('common.pleaseEnter') },
|
||||
]}
|
||||
|
||||
@@ -138,6 +138,15 @@ export const useWorkflowGraph = ({
|
||||
})
|
||||
graphRef.current.addEdges(edgeList.filter(vo => vo !== null))
|
||||
}
|
||||
|
||||
// 初始化完成后,将节点展示在可视区域内
|
||||
if (nodes.length > 0 || edges.length > 0) {
|
||||
setTimeout(() => {
|
||||
if (graphRef.current) {
|
||||
graphRef.current.centerContent()
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
const saveState = () => {
|
||||
|
||||
@@ -75,7 +75,7 @@ export interface WorkflowConfig {
|
||||
}
|
||||
|
||||
export interface VariableEditModalRef {
|
||||
handleOpen: (values?: StartVariableItem) => void;
|
||||
handleOpen: (values: StartVariableItem[]) => void;
|
||||
}
|
||||
export interface StartVariableItem {
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user