From 88598fb9fb92c94dec9ba3e02170f4230cc210fb Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 16 Mar 2026 15:10:55 +0800 Subject: [PATCH] feat(web): memory ui upgrade --- web/src/views/EmotionEngine/index.tsx | 283 +++++++++------- web/src/views/MemoryManagement/index.tsx | 111 ++++--- web/src/views/UserMemory/index.tsx | 46 +-- .../components/ActivationMetricsPieCard.tsx | 123 +------ .../components/AudioPlayer.tsx | 152 +++++++++ .../components/EmotionTags.tsx | 42 ++- .../UserMemoryDetail/components/Habits.tsx | 77 +++-- .../UserMemoryDetail/components/Health.tsx | 158 ++++----- .../components/InterestAreas.tsx | 71 ++-- .../components/PerceptualLastInfo.tsx | 132 +++++--- .../UserMemoryDetail/components/Portrait.tsx | 27 +- .../components/Preferences.tsx | 88 +++-- .../components/RecentTrendsLineCard.tsx | 80 ++--- .../components/Suggestions.tsx | 34 +- .../UserMemoryDetail/components/Timeline.tsx | 51 +-- .../UserMemoryDetail/components/WordCloud.tsx | 54 ++- .../UserMemoryDetail/pages/EpisodicDetail.tsx | 311 ++++++++++-------- .../UserMemoryDetail/pages/ExplicitDetail.tsx | 164 ++++++--- .../UserMemoryDetail/pages/ForgetDetail.tsx | 238 +++++++++----- .../UserMemoryDetail/pages/ImplicitDetail.tsx | 52 ++- .../pages/PerceptualDetail.tsx | 43 +-- .../pages/ShortTermDetail.tsx | 241 ++++++++++---- .../pages/StatementDetail.tsx | 24 +- .../UserMemoryDetail/pages/WorkingDetail.tsx | 232 +++++++------ .../UserMemoryDetail/pages/index.module.css | 10 + .../views/UserMemoryDetail/pages/index.tsx | 74 ++--- 26 files changed, 1732 insertions(+), 1186 deletions(-) create mode 100644 web/src/views/UserMemoryDetail/components/AudioPlayer.tsx create mode 100644 web/src/views/UserMemoryDetail/pages/index.module.css diff --git a/web/src/views/EmotionEngine/index.tsx b/web/src/views/EmotionEngine/index.tsx index d6866b5a..3a9cadc0 100644 --- a/web/src/views/EmotionEngine/index.tsx +++ b/web/src/views/EmotionEngine/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:56:54 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:57:17 + * @Last Modified time: 2026-03-12 16:58:14 */ /** * Emotion Engine Configuration Page @@ -11,18 +11,21 @@ */ import React, { useState, useEffect } from 'react'; -import { Row, Col, Form, Slider, Button, Alert, message, Space } from 'antd'; +import { Row, Col, Form, Button, message, Space, Flex, Tooltip } from 'antd'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import clsx from 'clsx'; import RbCard from '@/components/RbCard/Card'; -import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg' import { getMemoryEmotionConfig, updateMemoryEmotionConfig } from '@/api/memory' import type { ConfigForm } from './types' import CustomSelect from '@/components/CustomSelect'; import { getModelListUrl } from '@/api/models' -import Tag from '@/components/Tag' import SwitchFormItem from '@/components/FormItem/SwitchFormItem' +import LabelWrapper from '@/components/FormItem/LabelWrapper' +import DescWrapper from '@/components/FormItem/DescWrapper' +import RbSlider from '@/components/RbSlider'; +import RbAlert from '@/components/RbAlert'; /** * Configuration field definitions @@ -40,10 +43,11 @@ const configList = [ }, { key: 'emotion_min_intensity', - type: 'slider', + type: 'decimal', min: 0, max: 1, - step: 0.05 + step: 0.05, + range: [0, 1], }, { key: 'emotion_extract_keywords', @@ -119,12 +123,15 @@ const EmotionEngine: React.FC = () => { - - {t('emotionEngine.emotionEngineConfig')} - - } + title={t('emotionEngine.emotionEngineConfig')} + headerType="borderless" + headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold" + extra={ + + + } + className="rb:h-[calc(100vh-76px)]!" + bodyClassName="rb:h-[calc(100%-54px)] rb:overflow-y-auto! rb:p-3! rb:pt-0!" >
{ lambda_mem: 0.03, }} > - {configList.map(config => { - if (config.type === 'slider') { + + {configList.map(config => { + if (config.type === 'decimal') { + return ( +
+ + {t(`emotionEngine.${config.key}`)} + +
+
+
+ + {t(`forgettingEngine.range`)}: {config.range?.join('-')} | {t(`forgettingEngine.type`)}: {config.type} + } + />} + className="rb:mb-0!" + > + {t('emotionEngine.currentValue')}:} + inputClassName="rb:w-[155px]!" + /> + +
+ ) + } + if (config.type === 'customSelect') { + return ( +
+ + + + + + +
+ ) + } return ( -
-
- {t(`emotionEngine.${config.key}`)} -
-
- {t(`emotionEngine.${config.key}_desc`)} -
- - - -
- - <>{t('emotionEngine.currentValue')}: {values?.[config.key as keyof ConfigForm] || 0} -
-
+ + {config.hasSubTitle &&
{t(`emotionEngine.${config.key}_subTitle`)}
} +
{t(`emotionEngine.${config.key}_desc`)}
+ } + disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} + className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3!" + /> ) - } - if (config.type === 'customSelect') { - return ( -
-
- {t(`emotionEngine.${config.key}`)} -
- - - -
- ) - } - return ( - - {config.hasSubTitle &&
{t(`emotionEngine.${config.key}_subTitle`)}
} -
{t(`emotionEngine.${config.key}_desc`)}
- } - className="rb:mb-6" - disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} - /> - ) - })} - - - - - - - - + })} +
-
{t('emotionEngine.question')}
-
{t('emotionEngine.answer')}
-
{t('emotionEngine.differentTitle')}
- - - {['low', 'middle', 'high'].map((key, index) => ( - -
- {t(`emotionEngine.${key}_title`)} - {t(`emotionEngine.${key}_tag`)} -
- -
{t('emotionEngine.advantage')}: {t(`emotionEngine.${key}_advantage`)}
-
{t('emotionEngine.shortcoming')}: {t(`emotionEngine.${key}_shortcoming`)}
-
{t('emotionEngine.scene')}: {t(`emotionEngine.${key}_scene`)}
-
- - } - /> - ))} -
- -
{t('emotionEngine.configSuggest')}
- - {['first', 'customer_service', 'data_analysis', 'risk_warning'].map(key => ( -
{t(`emotionEngine.${key}`)}: {t(`emotionEngine.${key}_desc`)}
- ))} -
- -
{t('emotionEngine.actual_case')}
- -
- {t('emotionEngine.user_input')}: - {t('emotionEngine.user_input_message')} -
- {['neutral_emotion', 'minor_dissatisfaction', 'expect_improvement'].map((key, index) => ( -
-
- {t(`emotionEngine.${key}`)} - {t('emotionEngine.confidence')}: {key === 'neutral_emotion' ? 0.85 : key === 'minor_dissatisfaction' ? 0.45 : 0.32} -
- - {t(`emotionEngine.${key}_tag`)} + +
+
{t('emotionEngine.question')}
+
+ {t('emotionEngine.answer')}
- ))} - +
+ +
+
{t('emotionEngine.differentTitle')}
+ + + {['low', 'middle', 'high'].map((key, index) => ( + + + + {t(`emotionEngine.${key}_title`)} + + + {t(`emotionEngine.${key}_tag`)} + + +
{t('emotionEngine.advantage')}: {t(`emotionEngine.${key}_advantage`)}
+
{t('emotionEngine.shortcoming')}: {t(`emotionEngine.${key}_shortcoming`)}
+
{t('emotionEngine.scene')}: {t(`emotionEngine.${key}_scene`)}
+
+
+ ))} +
+
+ +
+
{t('emotionEngine.configSuggest')}
+ + {['first', 'customer_service', 'data_analysis', 'risk_warning'].map(key => ( +
{t(`emotionEngine.${key}`)}: {t(`emotionEngine.${key}_desc`)}
+ ))} +
+
+ +
+
{t('emotionEngine.actual_case')}
+ +
+
+ {t('emotionEngine.user_input')}: + {t('emotionEngine.user_input_message')} +
+ + + {['neutral_emotion', 'minor_dissatisfaction', 'expect_improvement'].map((key, index) => ( + + + {t(`emotionEngine.${key}`)} + {t('emotionEngine.confidence')}: {key === 'neutral_emotion' ? 0.85 : key === 'minor_dissatisfaction' ? 0.45 : 0.32} + + + {t(`emotionEngine.${key}_tag`)} + + ))} + +
+
+
{contextHolder} diff --git a/web/src/views/MemoryManagement/index.tsx b/web/src/views/MemoryManagement/index.tsx index fdc272e5..00885516 100644 --- a/web/src/views/MemoryManagement/index.tsx +++ b/web/src/views/MemoryManagement/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 17:33:15 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-06 13:53:53 + * @Last Modified time: 2026-03-12 15:44:32 */ /** * Memory Management Page @@ -11,10 +11,9 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { List, Button, Space, App, Tooltip } from 'antd'; +import { Button, Space, App, Flex, Row, Col } from 'antd'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import clsx from 'clsx' import MemoryForm from './components/MemoryForm'; import type { Memory, MemoryFormRef } from '@/views/MemoryManagement/types' @@ -22,7 +21,6 @@ import RbCard from '@/components/RbCard/Card' import { getMemoryConfigList, deleteMemoryConfig } from '@/api/memory' import BodyWrapper from '@/components/Empty/BodyWrapper' import { formatDateTime } from '@/utils/format'; -import RbAlert from '@/components/RbAlert' const MemoryManagement: React.FC = () => { const { t } = useTranslation(); @@ -102,68 +100,69 @@ const MemoryManagement: React.FC = () => {
- ( - - + {data.map((item) => ( + + {item.is_system_default && -
+
{t('common.default')}
} - -
{item.config_desc}
-
- -
- {t('memory.scene_id')}: - - {item.scene_name || '-'} - + +
+ {t('memory.scene_id')}: {item.scene_name || '-'}
- -
- {['memoryExtractionEngine', 'forgottenEngine', 'emotionEngine', 'reflectionEngine'].map((key) => ( -
handleClick(item.config_id, key)} - > - {t(`memory.${key}`)} - - {/* */} -
+ {['memoryExtractionEngine', 'forgottenEngine', 'emotionEngine', 'reflectionEngine'].map((key) => ( + handleClick(item.config_id, key)} + > + {t(`memory.${key}`)} +
- -
- ))} -
-
- {formatDateTime(item.updated_at, 'YYYY-MM-DD HH:mm:ss')} - -
handleEdit(item)} - >
- {!item.is_system_default &&
handleDelete(item)} - >
} -
-
+ + ))} +
+ + + {formatDateTime(item.updated_at, 'YYYY-MM-DD HH:mm:ss')} + +
+
handleEdit(item)} + >
+
+ {!item.is_system_default && +
handleDelete(item)} + >
+ } +
+
+
- - )} - /> + + ))} + : filterData.length > 0 ? ( - + {filterData.map((item, index) => { const { end_user, memory_num, memory_config } = item as Data; const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id return ( {name[0]}
} - title={name || '-'} - extra={
} + title={ +
{name[0]}
+ +
{name || '-'}
+
} + headerType="border" + headerClassName="rb:h-[48px]! rb:mx-4!" + bodyClassName="rb:py-3! rb:px-4!" className="rb:cursor-pointer" onClick={() => handleViewDetail(end_user.id)} > - -
{t('userMemory.capacity')}
-
{memory_num?.total || 0} {t('userMemory.memoryNum')}
-
- -
{t('userMemory.type')}
-
{t(`userMemory.${item.type || 'person'}`)}
-
+ + + + + + + + -
- +
+ {t('userMemory.memory_config_name')}
-
{memory_config?.memory_config_name || '-'}
+
{memory_config?.memory_config_name || '-'}
diff --git a/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx b/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx index 8f2c85c6..759a4bd7 100644 --- a/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx +++ b/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx @@ -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>; 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 = ({ chartData, loading }) => { const { t } = useTranslation() - const chartRef = useRef(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 ( {loading - ? - : !chartData || chartData.length === 0 - ? - : + : } diff --git a/web/src/views/UserMemoryDetail/components/AudioPlayer.tsx b/web/src/views/UserMemoryDetail/components/AudioPlayer.tsx new file mode 100644 index 00000000..98dabfdb --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/AudioPlayer.tsx @@ -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 + * + */ +const AudioPlayer: FC = ({ src, fileName, fileSize }) => { + const { t } = useTranslation() + const audioRef = useRef(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:
, + label: t('common.download'), + onClick: handleDownload, + }, + { + key: 'speed', + icon:
, + label: t('perceptualDetail.playbackSpeed'), + children: SPEEDS.map(s => ({ + key: String(s), + label: {s === 1 ? 'normal' : s}, + onClick: () => setPlaybackSpeed(s), + })), + }, + ], + } + + return ( +
+