feat(web): memory ui upgrade
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:34:16
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:34:16
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 11:36:02
|
||||
*/
|
||||
import { type FC, useRef, useEffect } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import PieChart from '@/components/Charts/PieChart'
|
||||
|
||||
/**
|
||||
* Props for ActivationMetricsPieCard component
|
||||
@@ -21,117 +20,27 @@ interface ActivationMetricsPieCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
loading: boolean;
|
||||
}
|
||||
const Colors = ['#155EEF', '#FFB048', '#FF5D34']
|
||||
|
||||
/**
|
||||
* ActivationMetricsPieCard Component
|
||||
* Displays activation value distribution as a donut chart with legend
|
||||
* Shows percentage distribution of different activation levels
|
||||
*/
|
||||
const ActivationMetricsPieCard: FC<ActivationMetricsPieCardProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
const resizeScheduledRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (chartRef.current && !resizeScheduledRef.current) {
|
||||
resizeScheduledRef.current = true
|
||||
requestAnimationFrame(() => {
|
||||
chartRef.current?.getEchartsInstance().resize();
|
||||
resizeScheduledRef.current = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize)
|
||||
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (chartElement) {
|
||||
resizeObserver.observe(chartElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [chartData])
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('forgetDetail.activationValueDistribution')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[46px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-46px)]"
|
||||
className="rb:h-full!"
|
||||
>
|
||||
{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: 'item',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontSize: 12,
|
||||
width: 27,
|
||||
height: 16,
|
||||
},
|
||||
formatter: '{d}%',
|
||||
padding: [8, 5],
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#DFE4ED',
|
||||
extraCssText: 'width: 36px; height: 36px; box-shadow: 0px 2px 4px 0px rgba(33,35,50,0.12);border-radius: 36px;'
|
||||
},
|
||||
legend: {
|
||||
bottom: 14 ,
|
||||
padding: 0,
|
||||
itemGap: 24,
|
||||
itemWidth: 40,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: ['50%', '90%'],
|
||||
avoidLabelOverlap: false,
|
||||
percentPrecision: 0,
|
||||
padAngle: 4,
|
||||
width: 200,
|
||||
height: 200,
|
||||
left: 143,
|
||||
itemStyle: {
|
||||
borderRadius: 0
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#212332',
|
||||
formatter: '{d}%\n{b}',
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '400px' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
? <Loading size={249} />
|
||||
: <PieChart
|
||||
chartData={chartData as { name: string; value: number }[]}
|
||||
height={214}
|
||||
seriesWidth={150}
|
||||
seriesHeight={150}
|
||||
itemGap={14}
|
||||
seriesLabel={false}
|
||||
seriesTop={5}
|
||||
/>
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
152
web/src/views/UserMemoryDetail/components/AudioPlayer.tsx
Normal file
152
web/src/views/UserMemoryDetail/components/AudioPlayer.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-16 15:00:07
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:00:07
|
||||
*/
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { Flex, Dropdown, type MenuProps, Slider } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/** Available playback speed options. */
|
||||
const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
||||
|
||||
/** Format seconds into "MM:SS" display string. */
|
||||
const fmt = (s: number) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`
|
||||
|
||||
/**
|
||||
* Props for the AudioPlayer component.
|
||||
* @property src - Audio file URL to play.
|
||||
* @property fileName - Display name shown beside the file icon.
|
||||
* @property fileSize - Human-readable file size string (e.g. "3.2 MB").
|
||||
*/
|
||||
interface AudioPlayerProps {
|
||||
src: string
|
||||
fileName: string
|
||||
fileSize: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AudioPlayer – A compact inline audio player with playback controls.
|
||||
*
|
||||
* Displays file metadata (name & size), a play/pause toggle, a seekable
|
||||
* progress slider, elapsed/total time, and a dropdown menu for downloading
|
||||
* the file or changing playback speed.
|
||||
*
|
||||
* @example
|
||||
* <AudioPlayer src="/audio/demo.mp3" fileName="demo.mp3" fileSize="3.2 MB" />
|
||||
*/
|
||||
const AudioPlayer: FC<AudioPlayerProps> = ({ src, fileName, fileSize }) => {
|
||||
const { t } = useTranslation()
|
||||
const audioRef = useRef<HTMLAudioElement>(null)
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [current, setCurrent] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const [speed, setSpeed] = useState(1)
|
||||
|
||||
/* Bind native audio events to sync React state; re-binds when src changes. */
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
const onTime = () => setCurrent(audio.currentTime)
|
||||
const onMeta = () => setDuration(audio.duration)
|
||||
const onEnd = () => setPlaying(false)
|
||||
audio.addEventListener('timeupdate', onTime)
|
||||
audio.addEventListener('loadedmetadata', onMeta)
|
||||
audio.addEventListener('ended', onEnd)
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', onTime)
|
||||
audio.removeEventListener('loadedmetadata', onMeta)
|
||||
audio.removeEventListener('ended', onEnd)
|
||||
}
|
||||
}, [src])
|
||||
|
||||
/** Toggle between play and pause. */
|
||||
const togglePlay = () => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
if (playing) { audio.pause(); setPlaying(false) }
|
||||
else { audio.play(); setPlaying(true) }
|
||||
}
|
||||
|
||||
/** Seek to a specific position (in seconds) on the audio timeline. */
|
||||
const handleSeek = (val: number) => {
|
||||
if (audioRef.current) audioRef.current.currentTime = val
|
||||
setCurrent(val)
|
||||
}
|
||||
|
||||
/** Update playback speed on both React state and the native audio element. */
|
||||
const setPlaybackSpeed = (s: number) => {
|
||||
setSpeed(s)
|
||||
if (audioRef.current) audioRef.current.playbackRate = s
|
||||
}
|
||||
|
||||
/** Open the audio source URL in a new tab to trigger download. */
|
||||
const handleDownload = () => window.open(src, '_blank')
|
||||
|
||||
/** Dropdown menu items: download and playback speed sub-menu. */
|
||||
const mainMenu: MenuProps = {
|
||||
items: [
|
||||
{
|
||||
key: 'download',
|
||||
icon: <div className="rb:size-6 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/userMemory/download.svg')]" />,
|
||||
label: t('common.download'),
|
||||
onClick: handleDownload,
|
||||
},
|
||||
{
|
||||
key: 'speed',
|
||||
icon: <div className="rb:size-6 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/userMemory/play_speed.svg')]" />,
|
||||
label: t('perceptualDetail.playbackSpeed'),
|
||||
children: SPEEDS.map(s => ({
|
||||
key: String(s),
|
||||
label: <span className={s === speed ? 'rb:font-bold rb:text-[#171719]' : ''}>{s === 1 ? 'normal' : s}</span>,
|
||||
onClick: () => setPlaybackSpeed(s),
|
||||
})),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3 rb:w-full">
|
||||
<audio ref={audioRef} src={src} preload="metadata" />
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2">
|
||||
<Flex align="center" gap={12}>
|
||||
<div className="rb:w-7.5 rb:h-9 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/mp3.svg')]" />
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:font-medium rb:leading-5 rb:text-[14px]">{fileName}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">{fileSize || '-'}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex align="center" gap={12}>
|
||||
<div
|
||||
className={clsx("rb:cursor-pointer rb:size-6", {
|
||||
"rb:bg-[url('@/assets/images/userMemory/play.svg')]": !playing,
|
||||
"rb:bg-[url('@/assets/images/userMemory/pause.svg')]": playing,
|
||||
})}
|
||||
onClick={togglePlay}
|
||||
></div>
|
||||
|
||||
<Dropdown menu={mainMenu} trigger={['click']} placement="bottomRight">
|
||||
<div className="rb:cursor-pointer rb:size-6 rb:bg-[url('@/assets/images/common/more.svg')] rb:hover:bg-[url('@/assets/images/common/more_hover.svg')]"></div>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex align="center" gap={8} className="rb:mt-3!">
|
||||
<Slider
|
||||
min={0}
|
||||
max={duration || 0}
|
||||
step={0.1}
|
||||
value={current}
|
||||
onChange={handleSeek}
|
||||
tooltip={{ formatter: null }}
|
||||
className="rb:flex-1 rb:m-0!"
|
||||
styles={{ track: { background: '#171719' }, rail: { background: '#E4E4E4' }, handle: { display: 'none' } }}
|
||||
/>
|
||||
<span className="rb:text-[12px] rb:leading-4.5 rb:text-[#5B6167] rb:whitespace-nowrap">{fmt(current)} / {fmt(duration)}</span>
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AudioPlayer
|
||||
@@ -2,13 +2,14 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:33:39
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:33:39
|
||||
* @Last Modified time: 2026-03-16 15:01:39
|
||||
*/
|
||||
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 { Flex, Divider } from 'antd';
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -57,12 +58,12 @@ const EmotionTags: FC = () => {
|
||||
*/
|
||||
const getEmotionColor = (emotionType: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
joy: '#52c41a',
|
||||
anger: '#ff4d4f',
|
||||
sadness: '#1890ff',
|
||||
fear: '#fa8c16',
|
||||
neutral: '#8c8c8c',
|
||||
surprise: '#722ed1'
|
||||
joy: '#FF8A4C',
|
||||
anger: '#FF5D34',
|
||||
sadness: '#155EEF',
|
||||
fear: '#9C6FFF',
|
||||
neutral: '#4DA8FF',
|
||||
surprise: '#369F21'
|
||||
}
|
||||
return colors[emotionType] || '#8c8c8c'
|
||||
}
|
||||
@@ -126,21 +127,26 @@ const EmotionTags: FC = () => {
|
||||
<RbCard
|
||||
title={t('statementDetail.emotionTags')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
|
||||
headerClassName="rb:min-h-[46px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-0!"
|
||||
className="rb:h-full!"
|
||||
>
|
||||
{data?.keywords && data?.keywords.length > 0
|
||||
? <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]) => {
|
||||
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:leading-5">{t(`statementDetail.${type || 'neutral'}`)} ({count}{t('statementDetail.item')})</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div ref={chartRef} className="rb:mt-4 rb:px-3" style={{ height: '212px', width: '100%' }} />
|
||||
|
||||
<div className="rb:px-4 rb:pb-2.5">
|
||||
<Divider className="rb:my-2.5!" />
|
||||
<Flex wrap justify="space-between" className="rb:px-1.5!">
|
||||
{Object.keys(emotionStats).map((type) => {
|
||||
return (
|
||||
<Flex key={type} align="center" gap={4}>
|
||||
<div className="rb:size-1 rb:rounded-full" style={{ backgroundColor: getEmotionColor(type) }}></div>
|
||||
<span className="rb:leading-5">{t(`statementDetail.${type || 'neutral'}`)}</span>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
: <Empty size={88} className="rb:h-full rb:mb-4" />
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:33:06
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:33:06
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 14:05:10
|
||||
*/
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Space, Progress } from 'antd';
|
||||
import { Skeleton, Space, Progress, Tooltip, Flex } from 'antd';
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty'
|
||||
import {
|
||||
getImplicitHabits,
|
||||
} from '@/api/memory'
|
||||
import styles from '../pages/index.module.css'
|
||||
|
||||
/**
|
||||
* Habits item data structure
|
||||
@@ -72,40 +73,46 @@ const Habits = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('implicitDetail.habits')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.habitsSubTitle')}</div>
|
||||
<RbCard>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: data.length === 0
|
||||
? <Empty size={88} />
|
||||
: <Space size={12} direction="vertical" className="rb:w-full!">
|
||||
{data.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div>
|
||||
<div className="rb:mb-1">{vo.habit_description}</div>
|
||||
<div className="rb:mb-1 rb:text-[#5B6167]">{vo.time_context}</div>
|
||||
</div>
|
||||
<div className="rb:text-[24px] rb:font-medium">{vo.confidence_level}%</div>
|
||||
<RbCard
|
||||
title={() => (<Space size={4}>
|
||||
{t('implicitDetail.habits')}
|
||||
<Tooltip title={t('implicitDetail.habitsSubTitle')}>
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/userMemory/question.svg')]"></div>
|
||||
</Tooltip>
|
||||
</Space>)}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-54px)] rb:overflow-y-auto!"
|
||||
className="rb:h-[calc(100vh-88px)]!"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: data.length === 0
|
||||
? <Empty size={88} />
|
||||
: <Flex gap={12} vertical>
|
||||
{data.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb-border rb:rounded-xl rb:p-3">
|
||||
<Flex gap={30} align="center" justify="space-between">
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:mb-2.5 rb:font-medium rb:text-[#212332]">{vo.habit_description}</div>
|
||||
<div className="rb:text-[#5B6167]">{vo.time_context}</div>
|
||||
</div>
|
||||
<Progress type="circle" strokeWidth={10} percent={vo.confidence_level} className={styles.progressCustom} />
|
||||
</Flex>
|
||||
|
||||
{vo.specific_examples.length > 0 && <>
|
||||
<div className="rb:mt-3 rb:mb-2">{t('implicitDetail.specific_examples')}</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
{vo.specific_examples.map((item, index) => (
|
||||
<div key={index} className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">- {item}</div>
|
||||
))}
|
||||
</div>
|
||||
</>}
|
||||
<Progress percent={vo.confidence_level} showInfo={false} className="rb:mt-3" />
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
</RbCard>
|
||||
</>
|
||||
{vo.specific_examples.length > 0 && <div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:py-2.5 rb:px-3 rb:mt-2.5">
|
||||
<div className="rb:font-medium rb:mb-1">{t('implicitDetail.specific_examples')}</div>
|
||||
<ul className="rb:list-disc rb:ml-4">
|
||||
{vo.specific_examples.map((item, index) => (
|
||||
<li key={index} className="rb:text-[#5B6167]">{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>}
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
})
|
||||
export default Habits
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:33:01
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:33:01
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 14:58:25
|
||||
*/
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Progress } from 'antd'
|
||||
import { Flex } from 'antd'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
@@ -79,96 +79,74 @@ const Health: FC = () => {
|
||||
<RbCard
|
||||
title={t('statementDetail.health')}
|
||||
headerType="borderless"
|
||||
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]!"
|
||||
headerClassName="rb:min-h-[46px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:px-[25px]! rb:pb-[30px]! rb:pt-0!"
|
||||
>
|
||||
{health?.health_score && health?.health_score > 0
|
||||
? <>
|
||||
<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>
|
||||
? <Flex vertical align="center" justify="center">
|
||||
<ReactEcharts
|
||||
option={{
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['75%', '90%'],
|
||||
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: '180px', width: '180px' }}
|
||||
/>
|
||||
|
||||
</>
|
||||
{health.dimensions && <Flex justify="space-between" className="rb:w-full rb:mt-7!">
|
||||
{['positivity_rate', 'stability', 'resilience'].map(key => (
|
||||
<div key={key} className="rb:text-[12px] rb:leading-4.5 rb:text-[#5B6167]">
|
||||
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[#212332] rb:text-[14px] rb:leading-4.75 rb:mb-1">{health.dimensions[key as keyof typeof health.dimensions].score}%</div>
|
||||
{t(`statementDetail.${key}`)}
|
||||
</div>
|
||||
))}
|
||||
</Flex>}
|
||||
|
||||
</Flex>
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:32:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:32:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 14:27:12
|
||||
*/
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Progress } from 'antd';
|
||||
import { Skeleton } from 'antd';
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getImplicitInterestAreas,
|
||||
} from '@/api/memory'
|
||||
|
||||
/** Default color palette for area line series */
|
||||
const Colors = ['#9C6FFF', '#FFB048', '#4DA8FF', '#369F21']
|
||||
const keys = ['art', 'music', 'tech', 'lifestyle'] as const
|
||||
/**
|
||||
* Interest category item structure
|
||||
* @property {string} category_name - Category name
|
||||
@@ -58,6 +61,7 @@ const InterestAreas = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<InterestAreasItem>({} as InterestAreasItem)
|
||||
const chartRef = useRef<ReactEcharts>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
@@ -81,27 +85,46 @@ const InterestAreas = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =
|
||||
handleRefresh: getData
|
||||
}));
|
||||
return (
|
||||
<RbCard
|
||||
title={t('implicitDetail.interestAreas')}
|
||||
headerType="borderless"
|
||||
>
|
||||
<div className="rb-border rb:p-4 rb:rounded-xl rb:mt-4">
|
||||
<div className="rb:text-[#212332] rb:font-medium rb:leading-5 rb:mb-4">{t('implicitDetail.interestAreas')}</div>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div>
|
||||
{(['art', 'music', 'tech', 'lifestyle'] as const).map((key) => {
|
||||
return (
|
||||
<div key={key} >
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-1">{t(`implicitDetail.${key}`)}</div>
|
||||
{data[key]?.percentage ?? 0}%
|
||||
</div>
|
||||
<Progress percent={data[key]?.percentage || 0} showInfo={false} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
: <ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: Colors,
|
||||
grid: { top: 8, left: 38, right: 8, bottom: 24 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: keys.map(k => t(`implicitDetail.${k}`)),
|
||||
axisLabel: { color: '#5B6167', fontSize: 12, fontFamily: 'PingFangSC, PingFang SC', interval: 0, overflow: 'break-word', width: 60 },
|
||||
axisLine: { lineStyle: { color: '#EBEBEB' } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: { color: '#A8A9AA', fontSize: 12, fontFamily: 'PingFangSC, PingFang SC', formatter: '{value}%' },
|
||||
splitLine: { lineStyle: { color: '#EBEBEB' } },
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
barMaxWidth: 40,
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
data: keys.map((k, i) => ({
|
||||
value: data[k]?.percentage ?? 0,
|
||||
itemStyle: { color: Colors[i] }
|
||||
})),
|
||||
label: { show: true, position: 'top', formatter: '{c}%', color: '#5B6167', fontSize: 10 },
|
||||
}]
|
||||
}}
|
||||
style={{ height: '200px', width: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
export default InterestAreas
|
||||
@@ -2,19 +2,22 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:32:23
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:32:23
|
||||
* @Last Modified time: 2026-03-16 15:01:50
|
||||
*/
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Space, Tooltip, Image } from 'antd';
|
||||
import { Skeleton, Image, Flex } from 'antd';
|
||||
import clsx from 'clsx'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import AudioPlayer from './AudioPlayer'
|
||||
import {
|
||||
getPerceptualLastVisual,
|
||||
getPerceptualLastListen,
|
||||
getPerceptualLastText,
|
||||
} from '@/api/memory'
|
||||
import Empty from '@/components/Empty';
|
||||
|
||||
/**
|
||||
* Perceptual last info item structure
|
||||
@@ -51,7 +54,7 @@ interface PerceptualLastInfoItem {
|
||||
/**
|
||||
* Field keys for different perceptual types
|
||||
*/
|
||||
const KEYS = {
|
||||
const KEYS: Record<string, string[]> = {
|
||||
last_visual: ['summary', 'keywords', 'topic', 'domain', 'scene'],
|
||||
last_listen: ['summary', 'keywords', 'topic', 'domain', 'speaker_count'],
|
||||
last_text: ['summary', 'keywords', 'topic', 'domain', 'section_count'],
|
||||
@@ -62,11 +65,13 @@ const KEYS = {
|
||||
* Displays the last perceptual memory (visual, audio, or text)
|
||||
* Shows file preview and metadata based on perceptual type
|
||||
*/
|
||||
const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text' }> = ({ type }) => {
|
||||
const PerceptualLastInfo: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<PerceptualLastInfoItem>({} as PerceptualLastInfoItem)
|
||||
const [type, setType] = useState('last_visual')
|
||||
const [fileSize, setFileSize] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
@@ -75,6 +80,7 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
|
||||
const getData = () => {
|
||||
if (!id || !type) return
|
||||
setLoading(true)
|
||||
setFileSize('')
|
||||
const request = type === 'last_visual'
|
||||
? getPerceptualLastVisual(id)
|
||||
: type === 'last_listen'
|
||||
@@ -83,7 +89,18 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
|
||||
request.then((res) => {
|
||||
const response = res as PerceptualLastInfoItem
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
setLoading(false)
|
||||
if (response.file_path) {
|
||||
fetch(response.file_path, { method: 'HEAD' })
|
||||
.then(r => {
|
||||
const bytes = Number(r.headers.get('content-length'))
|
||||
if (!bytes) return
|
||||
setFileSize(bytes < 1024 * 1024
|
||||
? `${(bytes / 1024).toFixed(1)} KB`
|
||||
: `${(bytes / 1024 / 1024).toFixed(1)} MB`)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
@@ -99,56 +116,73 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
|
||||
<RbCard
|
||||
title={t(`perceptualDetail.${type}`)}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[50px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-4! rb:pt-0! rb:h-[calc(100%-50px)] rb:overflow-y-auto"
|
||||
className="rb:h-[calc(100vh-88px)]! rb:w-full!"
|
||||
>
|
||||
<Flex align="center" gap={8} className="rb:mb-4!">
|
||||
{Object.keys(KEYS).map(key => (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx("rb:text-[12px] rb:rounded-[14px] rb:py-1 rb:pl-2 rb:pr-3 rb:cursor-pointer", {
|
||||
'rb:bg-[#171719] rb:text-white': type === key,
|
||||
'rb:bg-[#F6F6F6]': type !== key
|
||||
})}
|
||||
onClick={() => setType(key)}
|
||||
>{key}</div>))}
|
||||
</Flex>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:h-36 rb:rounded-sm rb:flex rb:items-center rb:justify-center rb:overflow-hidden">
|
||||
{data.file_path ? (
|
||||
type === 'last_visual' ? (
|
||||
/\.(mp4|webm|ogg|mov)$/i.test(data.file_name) ? (
|
||||
<video controls className="rb:max-w-full rb:max-h-full">
|
||||
<source src={data.file_path} />
|
||||
</video>
|
||||
) : /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name) ? (
|
||||
<Image src={data.file_path} alt={data.file_name} />
|
||||
// <img src={data.file_path} alt={data.file_name} className="rb:max-w-full rb:max-h-full rb:object-contain" />
|
||||
) : (
|
||||
<div className="rb:text-[#5B6167]">{data.file_name}</div>
|
||||
)
|
||||
) : type === 'last_listen' && /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name) ? (
|
||||
<audio controls className="rb:w-full">
|
||||
<source src={data.file_path} />
|
||||
</audio>
|
||||
) : (
|
||||
<div className="rb:text-[#5B6167] rb:cursor-pointer" onClick={handleDownload}>{data.file_name}</div>
|
||||
)
|
||||
) : (
|
||||
<div className="rb:text-[#5B6167]">{t('empty.tableEmpty')}</div>
|
||||
)}
|
||||
</div>
|
||||
<Space size={4} direction="vertical" className="rb:w-full rb:mt-3">
|
||||
{KEYS[type].map(key => {
|
||||
const value = (data as any)[key]
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:justify-between rb:items-center rb:gap-3">
|
||||
<div className="rb:text-[#5B6167]">{t(`perceptualDetail.${key}`)}</div>
|
||||
{key === 'summary' ? (
|
||||
<Tooltip title={value}>
|
||||
<div className="rb:flex-1 rb:text-right rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'}
|
||||
: <Flex vertical gap={16} className="rb:w-108">
|
||||
{data.file_path
|
||||
? <>
|
||||
{/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name)
|
||||
? <Image src={data.file_path} alt={data.file_name} width={432} className="rb:rounded-xl rb:h-45!" />
|
||||
: /\.(mp4|webm|ogg|mov)$/i.test(data.file_name)
|
||||
? <Flex align="center" justify="space-between" className="rb:bg-[#F6F6F6] rb:min-h-15.5! rb:rounded-xl rb:p-3!">
|
||||
|
||||
</Flex>
|
||||
: /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name)
|
||||
? <AudioPlayer src={data.file_path} fileName={data.file_name} fileSize={fileSize} />
|
||||
: <Flex gap={11} align="center" justify="space-between" className="rb:bg-[#F6F6F6] rb:min-h-15.5! rb:rounded-xl rb:p-3!">
|
||||
<Flex gap={12} align="center">
|
||||
<div className="rb:w-7.5 rb:h-9 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/file.svg')]"></div>
|
||||
<div>
|
||||
<div className="rb:leading-5 rb:font-medium rb:mb-1">{data.file_name}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">
|
||||
{fileSize || '-'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
: <div className="rb:flex-1 rb:text-right">
|
||||
{typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'}
|
||||
</div>
|
||||
</Flex>
|
||||
<div
|
||||
className="rb:size-6 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/userMemory/download.svg')] rb:hover:bg-[url('@/assets/images/userMemory/download_hover.svg')]"
|
||||
onClick={handleDownload}
|
||||
></div>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
: <div className="rb:bg-[#F6F6F6] rb:min-h-15.5! rb:rounded-xl rb:p-3!">
|
||||
<Empty size={44} />
|
||||
</div>
|
||||
}
|
||||
{KEYS[type].map(key => {
|
||||
const value = (data as any)[key]
|
||||
return (
|
||||
<div key={key} className="rb:leading-5">
|
||||
<div className="rb:text-[#5B6167] rb:mb-1">{t(`perceptualDetail.${key}`)}</div>
|
||||
|
||||
{typeof value === 'string'
|
||||
? <div>{value}</div>
|
||||
: Array.isArray(value)
|
||||
? <Flex wrap gap={11}>
|
||||
{value.map((vo, index) => <div key={index} className="rb:bg-[#F6F6F6] rb:rounded-[13px] rb:py-1 rb:px-2 rb:text-[12px] rb:font-medium rb:leading-4.5">{vo}</div>)}
|
||||
</Flex>
|
||||
: '-'
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:32:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:32:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 14:19:15
|
||||
*/
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Progress } from 'antd';
|
||||
import { Skeleton, Progress, Flex } from 'antd';
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getImplicitPortrait,
|
||||
} from '@/api/memory'
|
||||
@@ -85,28 +84,26 @@ const Portrait = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
handleRefresh: getData
|
||||
}));
|
||||
return (
|
||||
<RbCard
|
||||
title={t('implicitDetail.portrait')}
|
||||
headerType="borderless"
|
||||
>
|
||||
<div className="rb-border rb:p-4 rb:pb-2.25 rb:rounded-xl">
|
||||
<div className="rb:text-[#212332] rb:font-medium rb:leading-5 rb:mb-4">{t('implicitDetail.portrait')}</div>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div className="rb:mt-1">
|
||||
: <Flex vertical gap={14} className="rb:mt-1!">
|
||||
{(['aesthetic', 'creativity', 'literature', 'technology'] as const).map((key) => {
|
||||
const item = data[key] as Item
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-1">{t(`implicitDetail.${key}`)}</div>
|
||||
<Flex align="center" justify="space-between" className="rb:leading-5">
|
||||
<div className="rb:text-[#5B6167]">{t(`implicitDetail.${key}`)}</div>
|
||||
{item?.percentage ?? 0}%
|
||||
</div>
|
||||
<Progress percent={item?.percentage || 0} showInfo={false} />
|
||||
</Flex>
|
||||
<Progress percent={item?.percentage || 0} showInfo={false} strokeColor="#171719" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Flex>
|
||||
}
|
||||
</RbCard>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
export default Portrait
|
||||
@@ -1,20 +1,19 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:32:12
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:32:12
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 14:30:25
|
||||
*/
|
||||
import { useEffect, useState, useRef, useMemo, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Skeleton } from 'antd'
|
||||
import { Skeleton } from 'antd'
|
||||
import * as echarts from 'echarts'
|
||||
import 'echarts-wordcloud'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getImplicitPreferences } from '@/api/memory'
|
||||
import detailEmpty from '@/assets/images/userMemory/detail_empty.png'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
|
||||
/**
|
||||
* Preference item structure
|
||||
@@ -41,7 +40,7 @@ interface PreferenceItem {
|
||||
/**
|
||||
* Default color palette for categories
|
||||
*/
|
||||
const DEFAULT_COLORS = ['#FF5D34', '#155EEF', '#9C6FFF', '#369F21', '#4DA8FF', '#FF8C00', '#32CD32', '#FF69B4', '#20B2AA', '#DDA0DD']
|
||||
const DEFAULT_COLORS = ['#FF8A4C', '#FF5D34', '#155EEF', '#9C6FFF', '#4DA8FF', '#369F21']
|
||||
|
||||
/**
|
||||
* Generate color mapping for categories
|
||||
@@ -161,9 +160,6 @@ const Preferences = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =>
|
||||
}
|
||||
}, [data])
|
||||
|
||||
|
||||
console.log(selectedWord, data)
|
||||
|
||||
const detailTitle = useMemo(() => {
|
||||
return selectedWord !== null && data[selectedWord].tag_name ? <>{data[selectedWord].tag_name}{t('implicitDetail.preferencesDetail')}</> : ''
|
||||
}, [selectedWord, data, t])
|
||||
@@ -173,48 +169,40 @@ const Preferences = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =>
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.overviewTitle')}</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={t('implicitDetail.preferences')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bodyClassName='rb:p-0! rb:pb-3! rb:relative rb:h-[350px]'
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active className="rb:px-4" />
|
||||
: data && data.length > 0
|
||||
? <div ref={chartRef} className="rb:mt-6 rb:px-6" style={{ height: '350px' }} />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard
|
||||
title={detailTitle}
|
||||
headerType="borderless"
|
||||
height="100%"
|
||||
bodyClassName='rb:p-3! rb:h-[326px]'
|
||||
>
|
||||
{selectedWord === null
|
||||
? <Empty
|
||||
url={detailEmpty}
|
||||
subTitle={t('implicitDetail.wordEmpty')}
|
||||
className="rb:h-full rb:mx-10 rb:text-center"
|
||||
size={[197.81, 150]}
|
||||
/>
|
||||
: <>
|
||||
<div className="rb:leading-5 rb:mb-1 rb:font-medium">{t('implicitDetail.context_details')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{data[selectedWord].context_details}</div>
|
||||
<RbAlert color="orange">{t('implicitDetail.preferencesTip')}</RbAlert>
|
||||
<div className="rb-border rb:rounded-xl rb:h-60 rb:my-3">
|
||||
{loading
|
||||
? <Skeleton active className="rb:px-4" />
|
||||
: data && data.length > 0
|
||||
? <div ref={chartRef} className="rb:px-3 rb:h-full" />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</div>
|
||||
<div className="rb:h-[calc(100%-296px)] rb:overflow-y-auto">
|
||||
{selectedWord === null
|
||||
? <Empty
|
||||
subTitle={t('implicitDetail.wordEmpty')}
|
||||
size={96}
|
||||
className="rb:h-full"
|
||||
/>
|
||||
: <>
|
||||
<div className="rb:px-1 rb:pt-1 rb:pb-3 rb:font-medium rb:leading-5">{detailTitle}</div>
|
||||
<div className="rb:bg-[#F6F6F6] rb:rounded-lg rb:px-3 rb:py-2.5">
|
||||
<div className="rb:leading-5 rb:mb-2 rb:font-medium">{t('implicitDetail.context_details')}</div>
|
||||
<div className="rb:leading-5">{data[selectedWord].context_details}</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:leading-5 rb:mt-3 rb:font-medium">{t('implicitDetail.supporting_evidence')}</div>
|
||||
{data[selectedWord].supporting_evidence.map((vo, index) => <div key={index} className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">-{vo}</div>)}
|
||||
</>
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="rb:bg-[#F6F6F6] rb:rounded-lg rb:px-3 rb:py-2.5 rb:mt-3">
|
||||
<div className="rb:leading-5 rb:mb-2 rb:font-medium">{t('implicitDetail.supporting_evidence')}</div>
|
||||
<ul className="rb:list-disc rb:ml-4">
|
||||
{data[selectedWord].supporting_evidence.map((vo, index) => (
|
||||
<li key={index} className="rb:text-[#5B6167]">{vo}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:32:07
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:32:07
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 11:49:29
|
||||
*/
|
||||
import { type FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -26,6 +26,13 @@ interface RecentTrendsLineCardProps {
|
||||
|
||||
const Colors = ['#155EEF', '#FF5D34']
|
||||
|
||||
const axisLabelConfig = {
|
||||
color: '#5B6167',
|
||||
fontSize: 10,
|
||||
lineHeight: 14,
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
formatter: '{value}'
|
||||
}
|
||||
/**
|
||||
* RecentTrendsLineCard Component
|
||||
* Displays forgetting trends with dual Y-axis line chart
|
||||
@@ -60,6 +67,9 @@ const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, series
|
||||
<RbCard
|
||||
title={t('forgetDetail.forgettingTrend')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[46px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-46px)]"
|
||||
className="rb:h-full!"
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
@@ -92,16 +102,20 @@ const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, series
|
||||
legend: {
|
||||
bottom: 2,
|
||||
padding: 0,
|
||||
itemGap: 24,
|
||||
itemWidth: 40,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
itemGap: 8,
|
||||
itemWidth: 12,
|
||||
itemHeight: 6,
|
||||
icon: 'roundRect',
|
||||
orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
textStyle: axisLabelConfig,
|
||||
data: seriesList.map((key, index) => ({
|
||||
name: key === 'merged_count' ? t('forgetDetail.merged_count') : t('forgetDetail.average_activation'),
|
||||
itemStyle: {
|
||||
color: Colors[index] + '14',
|
||||
borderColor: Colors[index],
|
||||
borderWidth: 1,
|
||||
}
|
||||
}))
|
||||
},
|
||||
grid: {
|
||||
top: 16,
|
||||
@@ -114,39 +128,29 @@ const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, series
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.date),
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLabel: axisLabelConfig,
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB'
|
||||
color: '#DFE4ED'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
color: '#DFE4ED',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
color: Colors[0],
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLabel: axisLabelConfig,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: Colors[0]
|
||||
@@ -155,14 +159,7 @@ const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, series
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
color: '#DFE4ED',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
@@ -170,11 +167,7 @@ const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, series
|
||||
{
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: Colors[1],
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
formatter: '{value}'
|
||||
},
|
||||
axisLabel: axisLabelConfig,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: Colors[1]
|
||||
@@ -183,20 +176,13 @@ const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, series
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
max: 1,
|
||||
min: 0
|
||||
}
|
||||
],
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%' }}
|
||||
style={{ height: '214px', width: '100%', minWidth: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:31:50
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:22:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:02:00
|
||||
*/
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { App } from 'antd'
|
||||
import { App, Flex } from 'antd'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -82,25 +82,27 @@ const Suggestions = forwardRef<{ handleRefresh: () => void; }, { refresh: () =>
|
||||
<RbCard
|
||||
title={t('statementDetail.suggestions')}
|
||||
headerType="borderless"
|
||||
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]!"
|
||||
headerClassName="rb:min-h-[46px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:pt-0! rb:h-[740px]"
|
||||
>
|
||||
{suggestions?.suggestions && suggestions?.suggestions.length > 0
|
||||
? <>
|
||||
<RbAlert className="rb:mb-3">{suggestions.health_summary}</RbAlert>
|
||||
<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>
|
||||
? <Flex vertical gap={16} className="rb:h-full! rb:overflow-y-auto!">
|
||||
<RbAlert className="rb:text-[14px] rb:py-2.5! rb:px-3! rb:leading-4">{suggestions.health_summary}</RbAlert>
|
||||
|
||||
{suggestions.suggestions.map((item, index) => (
|
||||
<div key={index} className="rb:leading-5">
|
||||
<div className="rb:font-medium rb:mb-2">{index + 1}. {item.title}</div>
|
||||
|
||||
<ul className="rb:list-disc rb:ml-4 rb:text-[12px] rb:text-[#5B6167] rb:leading-5">
|
||||
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3">
|
||||
<div className="rb:mb-2">{item.content}</div>
|
||||
|
||||
<ul className="rb:list-disc rb:ml-4">
|
||||
{item.actionable_steps.map((vo, idx) => <li key={idx}>{vo}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
: <Empty size={88} subTitle={t(loading ? 'statementDetail.suggestionLoading' : 'empty.tableEmpty')} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:31:36
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:31:36
|
||||
* @Last Modified time: 2026-03-16 15:02:11
|
||||
*/
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Space, Divider } from 'antd';
|
||||
import { Skeleton, Row, Col, Flex } from 'antd';
|
||||
import clsx from 'clsx'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import Empty from '@/components/Empty'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
/**
|
||||
* Timeline item structure
|
||||
@@ -81,30 +81,41 @@ const Timeline: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard>
|
||||
<RbCard
|
||||
title={t('perceptualDetail.timeLine')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:pl-5! rb:pt-0! rb:pr-3! rb:pb-4! rb:h-[calc(100%-54px)] rb:overflow-y-auto"
|
||||
className="rb:h-[calc(100vh-88px)]!"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: data.length === 0
|
||||
? <Empty />
|
||||
: <Space size={8} direction="vertical" className="rb:w-full">
|
||||
: <Flex gap={12} vertical>
|
||||
{data.map((vo, index) => (
|
||||
<div key={vo.id} className="rb:flex rb:gap-6 rb:min-h-16">
|
||||
<div className="rb:text-[#155EEF] rb:leading-5 rb:font-medium rb:flex rb:flex-col rb:gap-2 rb:items-center">
|
||||
{formatDateTime(vo.created_time)}
|
||||
{index !== data.length - 1 && <Divider type="vertical" className="rb:flex-1 rb:w-px rb:border-[#155EEF]!" />}
|
||||
</div>
|
||||
<div className="rb:flex-1 rb:pb-4">
|
||||
<div className="rb:flex rb:justify-between">
|
||||
<div className="rb:w-150 rb:leading-5 rb:font-medium">{vo.summary}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:flex-1 rb:text-right">{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}</div>
|
||||
</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-2">{[vo.domain, vo.topic].join(' | ')}</div>
|
||||
<Row key={vo.id}className="rb:flex rb:gap-6 rb:min-h-16">
|
||||
<Col flex="90px" className="rb:leading-5 rb:font-semibold">
|
||||
<Flex vertical gap={12} align="center" justify="center" className="rb:h-full!">
|
||||
<span className="rb:text-center">{formatDateTime(vo.created_time)}</span>
|
||||
<div className={clsx("rb:flex-1 rb:w-px", {
|
||||
'rb:bg-[#5B6167]!': index !== data.length - 1
|
||||
})} />
|
||||
</Flex>
|
||||
</Col>
|
||||
<Col flex="1" className="rb:mb-1! rb:bg-[#F6F6F6] rb:rounded-xl rb:py-3 rb:px-4">
|
||||
<div className="rb:leading-4.5 rb:font-bold rb:text-[12px] rb:font-[MiSans-Bold]">{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}</div>
|
||||
|
||||
<Space size={8} className="rb:mt-2">{vo.keywords.map(tag => <Tag>{tag}</Tag>)}</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:leading-5 rb:mt-2">{vo.summary}</div>
|
||||
<div className="rb:leading-5 rb:text-[#5B6167] rb:mt-2">{[vo.domain, vo.topic].join(' | ')}</div>
|
||||
|
||||
<Flex gap={8} wrap className="rb:mt-2!">
|
||||
{vo.keywords.map((tag, index) => <div key={index} className="rb:bg-white rb:rounded-[13px] rb:py-1 rb:px-2 rb:font-medium rb:leading-4.5 rb:text-[12px]">{tag}</div>)}
|
||||
</Flex>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Space>
|
||||
</Flex>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:31:24
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:31:24
|
||||
* @Last Modified time: 2026-03-16 15:02:21
|
||||
*/
|
||||
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, Row, Col } from 'antd'
|
||||
import { Progress, Row, Col, Flex} from 'antd'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -88,6 +88,7 @@ const WordCloud: FC = () => {
|
||||
}))
|
||||
|
||||
return {
|
||||
color: ['#155EEF'],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
@@ -100,7 +101,24 @@ const WordCloud: FC = () => {
|
||||
indicator: radarData.map(item => ({
|
||||
name: t(`statementDetail.${item.name}`),
|
||||
max: 100,
|
||||
min: 1
|
||||
min: 1,
|
||||
color: '#5B6167',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#EBEBEB'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: '#A8A9AA',
|
||||
fontSize: 10,
|
||||
customValues: [20, 40, 60, 80, 100],
|
||||
align: 'center',
|
||||
margin: 0,
|
||||
}
|
||||
}))
|
||||
},
|
||||
series: [{
|
||||
@@ -108,7 +126,8 @@ const WordCloud: FC = () => {
|
||||
name: 'Emotion Intensity',
|
||||
data: [{
|
||||
value: radarData.map(item => item.value),
|
||||
name: 'Emotion Intensity'
|
||||
name: 'Emotion Intensity',
|
||||
symbol: 'circle'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -118,33 +137,38 @@ const WordCloud: FC = () => {
|
||||
<RbCard
|
||||
title={t('statementDetail.wordCloud')}
|
||||
headerType="borderless"
|
||||
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]!"
|
||||
headerClassName="rb:min-h-[50px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName='rb:px-[22px]! rb:pb-[28px]! rb:pt-0! rb:h-[calc(100%-54px)]'
|
||||
className="rb:h-full!"
|
||||
>
|
||||
{wordCloud?.total_count && wordCloud?.total_count > 0
|
||||
? <Row gutter={50}>
|
||||
? <Row gutter={58}>
|
||||
<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 />
|
||||
<ReactEcharts
|
||||
ref={chartRef}
|
||||
option={radarOption}
|
||||
style={{ width: '100%', height: 'calc(100% - 88px)' }}
|
||||
/>
|
||||
<div className="rb:text-center rb:bg-[#F6F6F6] rb:rounded-lg rb:p-2.5 rb:mt-4">
|
||||
<span className="rb:font-[MiSans-Heavy] rb:font-bold rb:text-[24px] 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">
|
||||
<Flex vertical gap={20} className="rb:pt-1!">
|
||||
{wordCloud.tags.map(item => (
|
||||
<div key={item.emotion_type}>
|
||||
<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>
|
||||
<span className="rb:font-medium rb:text-[#212332]">{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 className="rb:font-medium">{item.percentage.toFixed(1)}%</div>
|
||||
</div>
|
||||
<Progress strokeColor="#155EEF" percent={item.percentage} showInfo={false} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
</Col>
|
||||
</Row>
|
||||
: <Empty size={88} />
|
||||
|
||||
Reference in New Issue
Block a user