feat(web): user memory

This commit is contained in:
zhaoying
2026-01-12 14:42:02 +08:00
parent 24ace52e27
commit d56cbed0bf
14 changed files with 648 additions and 223 deletions

View File

@@ -1,7 +1,6 @@
import { type FC, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts';
import Empty from '@/components/Empty'
import Loading from '@/components/Empty/Loading'
import type { Emotion } from './GraphDetail'
@@ -100,10 +99,7 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
},
xAxis: {
type: 'category',
data: [...new Set(chartData.map(item => item.created_at))].sort().map(time => {
const date = new Date(time)
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}`
}),
data: [...new Set(chartData.map(item => item.created_at))].sort(),
boundaryGap: false,
axisLabel: {
color: '#A8A9AA',

View File

@@ -1,6 +1,8 @@
import { type FC, useEffect, useState } from 'react'
import { type FC, useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import * as echarts from 'echarts'
import 'echarts-wordcloud'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
@@ -13,40 +15,23 @@ interface TagList {
const EmotionTags: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const chartRef = useRef<HTMLDivElement>(null)
const chartInstance = useRef<echarts.ECharts | null>(null)
const [data, setData] = useState<TagList | null>(null)
useEffect(() => {
if (!id) return
getEmotionTagData()
}, [id])
const getEmotionTagData = () => {
if (!id) {
return
}
if (!id) return
getWordCloud(id)
.then((res) => {
setData(res as TagList)
})
}
const [visibleCount, setVisibleCount] = useState(0)
useEffect(() => {
if (!data || data?.keywords.length === 0) return
const timer = setInterval(() => {
setVisibleCount(prev => {
if (prev >= data?.keywords.length) {
clearInterval(timer)
return prev
}
return prev + 1
})
}, 200)
return () => clearInterval(timer)
}, [data?.keywords.length])
const getEmotionColor = (emotionType: string) => {
const colors: Record<string, string> = {
joy: '#52c41a',
@@ -59,6 +44,56 @@ const EmotionTags: FC = () => {
return colors[emotionType] || '#8c8c8c'
}
useEffect(() => {
if (!chartRef.current || !data?.keywords.length) return
if (chartInstance.current) {
chartInstance.current.dispose()
}
chartInstance.current = echarts.init(chartRef.current)
const wordCloudData = data.keywords.map((item) => ({
name: item.keyword,
value: item.frequency,
textStyle: {
color: getEmotionColor(item.emotion_type)
}
}))
const option = {
series: [{
type: 'wordCloud',
gridSize: 8,
sizeRange: [14, 60],
rotationRange: [-45, 45],
shape: 'pentagon',
width: '100%',
height: '100%',
textStyle: {
fontFamily: 'sans-serif',
fontWeight: 'bold'
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: '#333'
}
},
data: wordCloudData
}]
}
chartInstance.current.setOption(option)
return () => {
if (chartInstance.current) {
chartInstance.current.dispose()
chartInstance.current = null
}
}
}, [data])
const emotionStats = data?.keywords.reduce((acc, item) => {
acc[item.emotion_type] = (acc[item.emotion_type] || 0) + item.frequency
return acc
@@ -68,41 +103,25 @@ const EmotionTags: FC = () => {
<RbCard
title={t('statementDetail.emotionTags')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
bodyClassName='rb:p-0! rb:pb-3! rb:relative'
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:p-0!"
>
{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]">
? <div>
<div ref={chartRef} className="rb:mt-6 rb:px-6" style={{ height: '320px', width: '100%' }} />
<div className="rb:flex rb:flex-wrap rb:items-center rb:justify-center rb:gap-10 rb:text-sm rb:mt-3 rb:p-3 rb:bg-[#F0F3F8] rb:rounded-[0_0_8px_8px]">
{Object.entries(emotionStats).map(([type, count]) => {
console.log(type)
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(`statementDetail.${type || 'neutral'}`)} ({count})</span>
<span className="rb:leading-5">{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">
{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"
style={{
backgroundColor: getEmotionColor(item.emotion_type),
fontSize: `${12 + item.avg_intensity * 8}px`,
animationDelay: `${index * 200}ms`,
height: `${20 + item.avg_intensity * 20}px`,
transition: 'all 0.3s ease-in-out'
}}
>
{item.keyword}
</div>
))}
</div>
</>
: <Empty size={88} />
</div>
: <Empty size={88} className="rb:h-full" />
}
</RbCard>
)

View File

@@ -1,13 +1,16 @@
import { type FC, useState, forwardRef, useImperativeHandle } from 'react'
import { useState, forwardRef, useImperativeHandle, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Row, Col, Tabs } from 'antd'
import { Row, Col, Tabs, Space, Skeleton } from 'antd'
import { getRelationshipEvolution, getTimelineMemories } from '@/api/memory'
import type { Node, GraphDetailRef } from '../types'
import RbDrawer from '@/components/RbDrawer'
import RbCard from '@/components/RbCard/Card'
import EmotionLine from './EmotionLine'
import { formatDateTime } from '@/utils/format'
import Tag from '@/components/Tag'
import InteractionBar from './InteractionBar'
import Empty from '@/components/Empty'
export interface Emotion {
emotion_intensity: number;
@@ -15,141 +18,72 @@ export interface Emotion {
created_at: string | number;
}
export interface Interaction {
name: string;
importance_score: number;
interaction_count: number;
created_at: string | number;
count: number;
}
interface TimelineMemory {
text: string;
type: string;
created_at: number | string;
}
interface Timeline {
MemorySummary: TimelineMemory[];
Statement: TimelineMemory[];
ExtractedEntity: TimelineMemory[];
timelines_memory: TimelineMemory[];
}
const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
const [open, setOpen] = useState(false);
const [vo, setVo] = useState<Node | null>(null)
const [emotionData, setEmotionData] = useState<Emotion[]>([
{
"emotion_intensity": 0.1,
"emotion_type": "neutral",
"created_at": "2026-01-07 19:14:34"
},
{
"emotion_intensity": 0.2,
"emotion_type": "neutral",
"created_at": "2026-02-08 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "neutral",
"created_at": "2026-03-09 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "neutral",
"created_at": "2026-04-10 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "sadness",
"created_at": "2026-01-07 19:14:34"
},
{
"emotion_intensity": 0.2,
"emotion_type": "sadness",
"created_at": "2026-02-08 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "sadness",
"created_at": "2026-03-09 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "sadness",
"created_at": "2026-04-10 19:14:34"
},
])
const [interactionData, setInteractionData] = useState<Interaction[]>([
{
"name": "小蓝",
"importance_score": 0.5,
"interaction_count": 1
}
])
const [timelineMemories, setTimelineMemories] = useState({
"code": 0,
"msg": "共同记忆时间线",
"data": {
"success": true,
"data": {
"MemorySummary": [
"小蓝今天原计划与小明野餐、与小绿看电影,但最终选择与姐姐小红一起看戏。",
"用户小明喜欢喝咖啡,每天都要喝拿铁。"
],
"Statement": [
"小蓝对是否去野餐或看电影感到犹豫。",
"小蓝和她姐姐小红出去看戏。",
"小明喜欢喝咖啡。",
"小明每天都要喝拿铁。",
"小明今天约小蓝出去野餐。"
],
"ExtractedEntity": [
"小明",
"咖啡",
"拿铁",
"小蓝",
"野餐"
],
"timelines_memory": [
"小蓝今天原计划与小明野餐、与小绿看电影,但最终选择与姐姐小红一起看戏。",
"用户小明喜欢喝咖啡,每天都要喝拿铁。",
"小蓝对是否去野餐或看电影感到犹豫。",
"小蓝和她姐姐小红出去看戏。",
"小明喜欢喝咖啡。",
"小明每天都要喝拿铁。",
"小明今天约小蓝出去野餐。",
"小明",
"咖啡",
"拿铁",
"小蓝",
"野餐"
]
}
},
"error": "",
"time": 1767852781464
})
const [loading, setLoading] = useState(false)
const [emotionData, setEmotionData] = useState<Emotion[]>([])
const [interactionData, setInteractionData] = useState<Interaction[]>([])
const [activeTab, setActiveTab] = useState('timelines_memory')
const [timelineLoading, setTimelineLoading] = useState(false)
const [timelineMemories, setTimelineMemories] = useState<Timeline>({ timelines_memory: [], MemorySummary: [], Statement: [], ExtractedEntity: []})
const handleCancel = () => {
setVo(null)
setOpen(false)
}
const handleOpen = (vo: Node) => {
setActiveTab('timelines_memory')
setOpen(true)
setVo(vo)
getRelationshipEvolutionData(vo)
getTimelineMemoriesData(vo)
}
const getRelationshipEvolutionData = (vo: Node) => {
if (!id || !vo.label) return
getRelationshipEvolution({ id: id as string, label: vo.label })
if (!vo.id || !vo.label) return
setLoading(true)
getRelationshipEvolution({ id: vo.id as string, label: vo.label })
.then(res => {
const { emotion, interaction } = res as { emotion: { data: Emotion[]}; interaction: {data: Interaction[]} } || {}
setEmotionData(emotion?.data)
setInteractionData(interaction?.data)
const { emotion, interaction } = res as { emotion: Emotion[]; interaction: Interaction[] } || {}
setEmotionData(emotion)
setInteractionData(interaction)
})
.finally(() => setLoading(false))
}
const getTimelineMemoriesData = (vo: Node) => {
if (!id || !vo.label) return
getTimelineMemories({ id: id as string, label: vo.label })
if (!vo.id || !vo.label) return
setTimelineLoading(true)
getTimelineMemories({ id: vo.id as string, label: vo.label })
.then(res => {
setTimelineMemories(res as Timeline)
})
.finally(() => setTimelineLoading(false))
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
const activeContent = useMemo(() => {
return timelineMemories[activeTab as keyof Timeline] || []
}, [activeTab, timelineMemories])
return (
<RbDrawer
title={vo?.name}
@@ -157,17 +91,49 @@ const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
onClose={handleCancel}
width={1000}
>
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('useMemory.relationshipEvolution')}</div>
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('userMemory.relationshipEvolution')}</div>
<RbCard>
<Row gutter={16}>
<Col span={12}>
<EmotionLine chartData={emotionData} />
<EmotionLine chartData={emotionData} loading={loading} />
</Col>
<Col span={12}>
<div>{t('userMemory.interaction')}</div>
<InteractionBar chartData={interactionData} loading={loading} />
</Col>
</Row>
</RbCard>
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3 rb:mt-6">{t('userMemory.timelineMemories')}</div>
<RbCard>
<Tabs
activeKey={activeTab}
items={['timelines_memory', 'ExtractedEntity', 'Statement', 'MemorySummary'].map(key => ({
label: t(`userMemory.${key}`),
key
}))}
onChange={(key: string) => setActiveTab(key)}
/>
{timelineLoading
? <Skeleton active />
: !activeContent || activeContent.length === 0
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
: <Space size={16} direction="vertical" className="rb:w-full">
{activeContent.map((vo, index) => (
<RbCard
key={index}
headerType="borderL"
headerClassName="rb:before:bg-[#155EEF]!"
title={vo.text}
>
<div className="rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4">{formatDateTime(vo.created_at)}</div>
<Tag className="rb:mt-2">{vo.type}</Tag>
</RbCard>
))}
</Space>
}
</RbCard>
</RbDrawer>
)
})

View File

@@ -1,7 +1,8 @@
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Progress } from 'antd'
import { Row, Col, Progress } from 'antd'
import ReactEcharts from 'echarts-for-react'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
@@ -58,37 +59,95 @@ const Health: FC = () => {
<RbCard
title={t('statementDetail.health')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:px-[28px]! rb:py-[16px]!"
>
{health?.health_score && health?.health_score > 0
? <>
<div className="rb:flex rb:justify-center rb:items-center">
<Progress
size={250}
type="circle"
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
percent={health.health_score}
format={(percent) => `${percent}(${health.level})`}
/>
</div>
<Row gutter={59}>
<Col span={12}>
<div className="rb:flex rb:justify-center rb:items-center">
<ReactEcharts
option={{
series: [{
type: 'pie',
radius: ['65%', '80%'],
center: ['50%', '50%'],
startAngle: 90,
data: [
{
value: health.health_score,
name: health.level,
itemStyle: {
color: '#155EEF',
borderRadius: [10, 10, 10, 10]
}
},
{
value: 100 - health.health_score,
name: '',
itemStyle: {
color: '#DFE4ED',
borderRadius: [10, 10, 10, 10]
}
}
],
label: {
show: true,
position: 'center',
formatter: '{score|' + health.health_score + '}\n{level|' + health.level + '}',
rich: {
score: {
fontSize: 36,
fontWeight: 'bold',
color: '#212332',
lineHeight: 36
},
level: {
fontSize: 14,
color: '#5B6167',
lineHeight: 20
}
}
},
labelLine: { show: false },
emphasis: { disabled: true },
itemStyle: {
borderRadius: 10
}
}]
}}
style={{ height: '200px', width: '200px' }}
/>
</div>
</Col>
<Col span={12}>
{health.dimensions && <div className="rb:space-y-7">
<div>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
{t('statementDetail.positivity_rate')}
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.positivity_rate.score}%</div>
</div>
<Progress strokeColor="#155EEF" percent={health.dimensions.positivity_rate.score} showInfo={false} />
</div>
<div>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
{t('statementDetail.stability')}
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.stability.score}%</div>
</div>
<Progress strokeColor="#155EEF" percent={health.dimensions.stability.score} showInfo={false} />
</div>
<div>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
{t('statementDetail.resilience')}
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.resilience.score}%</div>
</div>
<Progress strokeColor="#155EEF" percent={health.dimensions.resilience.score} showInfo={false} />
</div>
</div>}
</Col>
</Row>
{health.dimensions && <>
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
<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('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('statementDetail.resilience')}</div>
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.resilience.score} />
</div>
</>}
</>
: <Empty size={88} className="rb:h-full" />
}

View File

@@ -0,0 +1,119 @@
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react'
import Empty from '@/components/Empty'
import Loading from '@/components/Empty/Loading'
import type { Interaction } from './GraphDetail'
interface InteractionBarProps {
chartData: Interaction[];
loading?: boolean;
}
const Colors = ['#155EEF', '#369F21', '#FF5D34']
const InteractionBar: FC<InteractionBarProps> = ({ chartData, loading }) => {
const { t } = useTranslation()
const series = [{
name: 'Interaction Count',
type: 'bar',
data: chartData.map(item => item.count)
}]
return (
<>
<div>{t('userMemory.interaction')}</div>
{loading
? <Loading size={249} />
: !chartData || chartData.length === 0
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
: <ReactEcharts
option={{
color: Colors,
tooltip: {
trigger: 'axis',
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
axisPointer: {
type: 'line',
crossStyle: {
color: '#5F6266',
},
lineStyle: {
color: '#5F6266',
}
},
},
grid: {
top: 16,
left: 30,
right: 36,
bottom: 48,
// containLabel: false
},
xAxis: {
type: 'category',
data: chartData.map(item => item.created_at),
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC'
},
axisLine: {
show: true,
lineStyle: {
color: '#EBEBEB'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
},
axisTick: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC'
},
axisLine: {
show: true,
lineStyle: {
color: '#EBEBEB'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
},
axisTick: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
},
max: 1,
min: 0
},
series
}}
style={{ height: '265px', width: '100%' }}
/>
}
</>
)
}
export default InteractionBar

View File

@@ -41,18 +41,24 @@ const Suggestions: FC = () => {
<RbCard
title={t('statementDetail.suggestions')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:px-[16px]! rb:pt-[20px]! rb:pb-[24px]!"
>
{suggestions?.suggestions && suggestions?.suggestions.length > 0
? <>
<RbAlert className="rb:mb-3">{suggestions.health_summary}</RbAlert>
{suggestions.suggestions.map((item, index) => (
<div key={index} className="rb:mb-3">
<div className="rb:font-medium">{index + 1}. {item.title}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-1 rb:mb-2">{item.content}</div>
{item.actionable_steps.map((vo, idx) => <div key={idx} className="rb:ml-6 rb:text-[12px] rb:text-[#5B6167] rb:mt-1">- {vo}</div>)}
</div>
))}
<div className="rb:space-y-8">
{suggestions.suggestions.map((item, index) => (
<div key={index}>
<div className="rb:font-medium">{index + 1}. {item.title}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:mb-2 rb:leading-5">{item.content}</div>
<ul className="rb:list-disc rb:ml-4 rb:text-[12px] rb:text-[#5B6167] rb:leading-5">
{item.actionable_steps.map((vo, idx) => <li key={idx}>{vo}</li>)}
</ul>
</div>
))}
</div>
</>
: <Empty size={88} className="rb:h-full" />
}

View File

@@ -2,7 +2,7 @@ import { type FC, useEffect, useState, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import ReactEcharts from 'echarts-for-react'
import { Progress } from 'antd'
import { Progress, Row, Col } from 'antd'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
@@ -101,27 +101,35 @@ const WordCloud: FC = () => {
<RbCard
title={t('statementDetail.wordCloud')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
height="100%"
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:px-[28px]! rb:py-[16px]!"
>
{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">
<div className="rb:text-[18px] rb:font-medium rb:mb-4">{wordCloud.total_count}</div>
<div className="rb:space-y-3">
? <Row gutter={50}>
<Col span={12}>
<ReactEcharts ref={chartRef} option={radarOption} style={{ width: '100%', height: 'calc(100% - 100px)' }} />
<div className="rb:mb-4 rb:text-center rb:bg-[#F5F7FC] rb:rounded-lg rb:p-2.5 rb:mt-4">
<span className="rb:text-[#155EEF] rb:text-[28px] rb:font-bold rb:leading-8">{wordCloud.total_count}</span><br />
<span className="rb:text-[#5B6167] rb:leading-5">{t('statementDetail.totalCount')}</span>
</div>
</Col>
<Col span={12}>
<div className="rb:space-y-5">
{wordCloud.tags.map(item => (
<div key={item.emotion_type}>
<div className="rb:flex rb:items-center rb:justify-between rb:font-medium">
{t(`statementDetail.${item.emotion_type}`)}
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count} {t('statementDetail.pieces')}</div>
<div className="rb:flex rb:items-center rb:justify-between">
<div>
<span className="rb:font-medium">{t(`statementDetail.${item.emotion_type}`)}</span>
<span className="rb:font-regular rb:text-[#5B6167]"> ( {item.count} {t('statementDetail.pieces')} )</span>
</div>
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{item.percentage.toFixed(1)}%</div>
</div>
<Progress size="small" percent={item.percentage} />
<Progress strokeColor="#155EEF" percent={item.percentage} showInfo={false} />
</div>
))}
</div>
</div>
</div>
</Col>
</Row>
: <Empty size={88} />
}
</RbCard>