feat(web): memory ui upgrade

This commit is contained in:
zhaoying
2026-03-16 15:10:55 +08:00
parent f09de3a11c
commit 88598fb9fb
26 changed files with 1732 additions and 1186 deletions

View File

@@ -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>

View 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

View File

@@ -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" />

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
)

View File

@@ -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

View File

@@ -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>
</>
)
})

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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} />