feat(web): memory ui upgrade
This commit is contained in:
@@ -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 = () => {
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={
|
||||
<div className="rb:flex rb:items-center">
|
||||
<img src={strategyImpactSimulator} className="rb:w-5 rb:h-5 rb:mr-2" />
|
||||
{t('emotionEngine.emotionEngineConfig')}
|
||||
</div>
|
||||
}
|
||||
title={t('emotionEngine.emotionEngineConfig')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
extra={<Space>
|
||||
<Button block onClick={handleReset}>{t('common.reset')}</Button>
|
||||
<Button type="primary" loading={loading} block onClick={handleSave}>{t('common.save')}</Button>
|
||||
</Space>}
|
||||
className="rb:h-[calc(100vh-76px)]!"
|
||||
bodyClassName="rb:h-[calc(100%-54px)] rb:overflow-y-auto! rb:p-3! rb:pt-0!"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
@@ -135,130 +142,152 @@ const EmotionEngine: React.FC = () => {
|
||||
lambda_mem: 0.03,
|
||||
}}
|
||||
>
|
||||
{configList.map(config => {
|
||||
if (config.type === 'slider') {
|
||||
<Flex vertical gap={12}>
|
||||
{configList.map(config => {
|
||||
if (config.type === 'decimal') {
|
||||
return (
|
||||
<div key={config.key} className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3">
|
||||
<Flex align="center" gap={4} className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`emotionEngine.${config.key}`)}
|
||||
<Tooltip title={t(`emotionEngine.${config.key}_desc`)}>
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/question.svg')]"></div>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
extra={<DescWrapper
|
||||
desc={<>
|
||||
<span className="rb:text-[12px]">{t(`forgettingEngine.range`)}: {config.range?.join('-')}</span> | <span>{t(`forgettingEngine.type`)}: {config.type}</span>
|
||||
</>}
|
||||
/>}
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<RbSlider
|
||||
max={config.max}
|
||||
min={config.min}
|
||||
step={config.step}
|
||||
isInput={true}
|
||||
prefix={<span className="rb:text-[#5B6167]">{t('emotionEngine.currentValue')}:</span>}
|
||||
inputClassName="rb:w-[155px]!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (config.type === 'customSelect') {
|
||||
return (
|
||||
<div key={config.key} className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3">
|
||||
<LabelWrapper title={t(`emotionEngine.${config.key}`)} className="rb:mb-3">
|
||||
<DescWrapper desc={t(`emotionEngine.${config.key}_desc`)} className="rb:mt-1" />
|
||||
</LabelWrapper>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<CustomSelect
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
valueKey='id'
|
||||
labelKey='name'
|
||||
hasAll={false}
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div key={config.key} className=" rb:mb-6">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`emotionEngine.${config.key}`)}
|
||||
</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ">
|
||||
{t(`emotionEngine.${config.key}_desc`)}
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
>
|
||||
<Slider
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
tooltip={{ open: false }} max={config.max} min={config.min} step={config.step} style={{ margin: '0' }} />
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
|
||||
|
||||
<>{t('emotionEngine.currentValue')}: {values?.[config.key as keyof ConfigForm] || 0}</>
|
||||
</div>
|
||||
</div>
|
||||
<SwitchFormItem
|
||||
title={t(`emotionEngine.${config.key}`)}
|
||||
name={config.key}
|
||||
desc={<>
|
||||
{config.hasSubTitle && <div className="rb:mt-1 rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_subTitle`)}</div>}
|
||||
<div className="rb:mt-1 rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_desc`)}</div>
|
||||
</>}
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3!"
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (config.type === 'customSelect') {
|
||||
return (
|
||||
<div key={config.key}>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`emotionEngine.${config.key}`)}
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
extra={t(`emotionEngine.${config.key}_desc`)}
|
||||
>
|
||||
<CustomSelect
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
valueKey='id'
|
||||
labelKey='name'
|
||||
hasAll={false}
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SwitchFormItem
|
||||
title={t(`emotionEngine.${config.key}`)}
|
||||
name={config.key}
|
||||
desc={<>
|
||||
{config.hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_subTitle`)}</div>}
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_desc`)}</div>
|
||||
</>}
|
||||
className="rb:mb-6"
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Row gutter={16} className="rb:mt-3">
|
||||
<Col span={12}>
|
||||
<Button block onClick={handleReset}>{t('common.reset')}</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button type="primary" loading={loading} block onClick={handleSave}>{t('common.save')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
})}
|
||||
</Flex>
|
||||
</Form>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={t('emotionEngine.emotion_min_intensity_description')}
|
||||
title={t('emotionEngine.emotionEngineConfig')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
className="rb:h-[calc(100vh-76px)]!"
|
||||
bodyClassName="rb:h-[calc(100%-54px)] rb:overflow-y-auto! rb:p-3! rb:pt-0!"
|
||||
>
|
||||
<div className="rb:font-medium">{t('emotionEngine.question')}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mt-2">{t('emotionEngine.answer')}</div>
|
||||
<div className="rb:font-medium rb:mt-4 rb:mb-2">{t('emotionEngine.differentTitle')}</div>
|
||||
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{['low', 'middle', 'high'].map((key, index) => (
|
||||
<Alert
|
||||
key={key}
|
||||
type={(['warning', 'info', 'success'] as const)[index] as 'warning' | 'info' | 'success'}
|
||||
message={
|
||||
<div>
|
||||
<div className="rb:w-full rb:font-medium rb:flex rb:justify-between">
|
||||
{t(`emotionEngine.${key}_title`)}
|
||||
<Tag color={(['warning', 'processing', 'success'] as const)[index] as 'warning' | 'processing' | 'success'}>{t(`emotionEngine.${key}_tag`)}</Tag>
|
||||
</div>
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">
|
||||
<div><span className="rb:font-medium">{t('emotionEngine.advantage')}: </span>{t(`emotionEngine.${key}_advantage`)}</div>
|
||||
<div><span className="rb:font-medium">{t('emotionEngine.shortcoming')}: </span>{t(`emotionEngine.${key}_shortcoming`)}</div>
|
||||
<div><span className="rb:font-medium">{t('emotionEngine.scene')}: </span>{t(`emotionEngine.${key}_scene`)}</div>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:font-medium rb:mt-6 rb:mb-3">{t('emotionEngine.configSuggest')}</div>
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{['first', 'customer_service', 'data_analysis', 'risk_warning'].map(key => (
|
||||
<div className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">{t(`emotionEngine.${key}`)}: {t(`emotionEngine.${key}_desc`)}</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:font-medium rb:mt-6 rb:mb-3">{t('emotionEngine.actual_case')}</div>
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
<div className="rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
<span className="rb:font-medium">{t('emotionEngine.user_input')}: </span>
|
||||
{t('emotionEngine.user_input_message')}
|
||||
</div>
|
||||
{['neutral_emotion', 'minor_dissatisfaction', 'expect_improvement'].map((key, index) => (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
<div className="rb:w-[50%] rb:flex rb:items-center rb:justify-between rb:text-[12px]">
|
||||
{t(`emotionEngine.${key}`)}
|
||||
<span>{t('emotionEngine.confidence')}: {key === 'neutral_emotion' ? 0.85 : key === 'minor_dissatisfaction' ? 0.45 : 0.32}</span>
|
||||
</div>
|
||||
|
||||
<Tag color={(['success', 'warning', 'processing'] as const)[index] as 'warning' | 'processing' | 'success'}>{t(`emotionEngine.${key}_tag`)}</Tag>
|
||||
<Flex vertical gap={24} className="rb:text-[#212332]">
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:px-1 rb:mb-2.5">{t('emotionEngine.question')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:bg-[#F6F6F6] rb:px-3 rb:py-2.5 rb:font-regular rb:leading-5 rb:rounded-xl">
|
||||
{t('emotionEngine.answer')}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:px-1 rb:mb-2.5">{t('emotionEngine.differentTitle')}</div>
|
||||
|
||||
<Flex gap={10} vertical>
|
||||
{['low', 'middle', 'high'].map((key, index) => (
|
||||
<RbAlert
|
||||
key={key}
|
||||
color={(['orange', 'blue', 'green'] as const)[index] as 'orange' | 'blue' | 'green'}
|
||||
>
|
||||
<Flex gap={10} vertical className=" rb:text-[#5B6167] rb:text-[14px] rb:font-regular rb:leading-5">
|
||||
<Flex align="center" gap={8}>
|
||||
<span className="rb:font-medium rb:text-[#212332]">{t(`emotionEngine.${key}_title`)}</span>
|
||||
|
||||
<span className={clsx("rb:px-1 rb:rounded-sm rb:text-white rb:leading-4.5", ['rb:bg-[#FF5D34]', 'rb:bg-[#155EEF]', 'rb:bg-[#369F21]'][index])}>
|
||||
{t(`emotionEngine.${key}_tag`)}
|
||||
</span>
|
||||
</Flex>
|
||||
<div><span className="rb:font-medium rb:text-[#212332]">{t('emotionEngine.advantage')}: </span>{t(`emotionEngine.${key}_advantage`)}</div>
|
||||
<div><span className="rb:font-medium rb:text-[#212332]">{t('emotionEngine.shortcoming')}: </span>{t(`emotionEngine.${key}_shortcoming`)}</div>
|
||||
<div><span className="rb:font-medium rb:text-[#212332]">{t('emotionEngine.scene')}: </span>{t(`emotionEngine.${key}_scene`)}</div>
|
||||
</Flex>
|
||||
</RbAlert>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:px-1 rb:mb-2.5">{t('emotionEngine.configSuggest')}</div>
|
||||
<Flex gap={10} vertical>
|
||||
{['first', 'customer_service', 'data_analysis', 'risk_warning'].map(key => (
|
||||
<div className="rb:bg-[#F6F6F6] rb:px-3 rb:py-2.5 rb:rounded-xl">{t(`emotionEngine.${key}`)}: {t(`emotionEngine.${key}_desc`)}</div>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:px-1 rb:mb-2.5">{t('emotionEngine.actual_case')}</div>
|
||||
|
||||
<div className="rb:bg-[#F6F6F6] rb:px-3 rb:py-2.5 rb:font-regular rb:leading-5 rb:rounded-xl">
|
||||
<div className="rb:mb-2.5">
|
||||
<span className="rb:font-medium">{t('emotionEngine.user_input')}: </span>
|
||||
{t('emotionEngine.user_input_message')}
|
||||
</div>
|
||||
|
||||
<Flex vertical gap={4}>
|
||||
{['neutral_emotion', 'minor_dissatisfaction', 'expect_improvement'].map((key, index) => (
|
||||
<Flex gap={28} align="center" justify="space-between" className="rb:bg-white rb:px-3! rb:py-2! rb:rounded-lg">
|
||||
<Flex align="center" justify="space-between" className="rb:w-[55%]!">
|
||||
<span className="rb:font-medium">{t(`emotionEngine.${key}`)}</span>
|
||||
<span>{t('emotionEngine.confidence')}: {key === 'neutral_emotion' ? 0.85 : key === 'minor_dissatisfaction' ? 0.45 : 0.32}</span>
|
||||
</Flex>
|
||||
|
||||
<span className={clsx('rb:text-right rb:wrap-break-word rb:flex-1', ['rb:text-[#369F21]', 'rb:text-[#FF5D34]', 'rb:text-[#155EEF]'][index])}>{t(`emotionEngine.${key}_tag`)}</span>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</Col>
|
||||
{contextHolder}
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
|
||||
<BodyWrapper loading={loading} empty={data.length === 0}>
|
||||
<List
|
||||
grid={{ gutter: 16, column: 2 }}
|
||||
loading={loading}
|
||||
dataSource={data}
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.config_id}>
|
||||
<RbCard
|
||||
<Row gutter={[12, 12]}>
|
||||
{data.map((item) => (
|
||||
<Col key={item.config_id} span={12}>
|
||||
<RbCard
|
||||
title={item.config_name}
|
||||
className="rb:relative"
|
||||
className="rb:relative rb:hover:shadow-[0px_2px_8px_0px_rgba(23,23,25,0.16)]!"
|
||||
headerType="borderless"
|
||||
headerClassName="rb:h-[46px]"
|
||||
bodyClassName="rb:p-3! rb:pt-0!"
|
||||
>
|
||||
{item.is_system_default &&
|
||||
<div className="rb:absolute rb:-right-px rb:-top-px rb:bg-[#FF5D34] rb:rounded-[0px_7px_0px_8px] rb:text-[12px] rb:text-white rb:font-regular rb:leading-4 rb:py-0.5 rb:px-1">
|
||||
<div className="rb:absolute rb:right-0 rb:top-0 rb:bg-[#FF5D34] rb:rounded-[0px_12px_0px_12px] rb:text-[12px] rb:text-white rb:font-medium rb:leading-4 rb:py-0.75 rb:px-2">
|
||||
{t('common.default')}
|
||||
</div>
|
||||
}
|
||||
<Tooltip title={item.config_desc}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1 rb:h-4.25">{item.config_desc}</div>
|
||||
</Tooltip>
|
||||
<RbAlert className="rb:mt-3 ">
|
||||
<div className={clsx("rb:flex rb:gap-5 rb:font-regular rb:text-[14px]")}>
|
||||
<span className="rb:text-[#5B6167]">{t('memory.scene_id')}: </span>
|
||||
<span className="rb:font-medium">
|
||||
{item.scene_name || '-'}
|
||||
</span>
|
||||
<Flex vertical gap={12}>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.06)] rb:rounded-lg rb:text-[#155EEF] rb:font-medium rb:leading-5 rb:py-1.5 rb:px-2">
|
||||
{t('memory.scene_id')}: {item.scene_name || '-'}
|
||||
</div>
|
||||
</RbAlert>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-x-4 rb:gap-y-3 rb:mt-3">
|
||||
{['memoryExtractionEngine', 'forgottenEngine', 'emotionEngine', 'reflectionEngine'].map((key) => (
|
||||
<div key={key} className="rb:group rb:cursor-pointer rb:bg-[#F0F3F8] rb:h-10 rb:rounded-md rb:flex rb:items-center rb:justify-between rb:p-[0_8px_0_12px] rb:text-[#5B6167] rb:font-medium"
|
||||
onClick={() => handleClick(item.config_id, key)}
|
||||
>
|
||||
{t(`memory.${key}`)}
|
||||
<span className='rb:flex rb:items-center rb:justify-end'>
|
||||
{/* <StatusTag status={item[key] === 'active' ? 'success' : 'error'} text={item[key] === 'active' ? t('memory.active') : t('memory.inactive')} /> */}
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/memory/arrow_right.svg')] rb:group-hover:bg-[url('@/assets/images/memory/arrow_right_hover.svg')]"
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-x-3 rb:gap-y-2">
|
||||
{['memoryExtractionEngine', 'forgottenEngine', 'emotionEngine', 'reflectionEngine'].map((key) => (
|
||||
<Flex
|
||||
key={key}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
className="rb:cursor-pointer rb:bg-[#F6F6F6] rb:h-8 rb:rounded-lg rb:font-medium rb:leading-5 rb:pl-2! rb:pr-1! rb:hover:shadow-[0px_2px_8px_0px_rgba(23,23,25,0.16)]"
|
||||
onClick={() => handleClick(item.config_id, key)}
|
||||
>
|
||||
{t(`memory.${key}`)}
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/memory/arrow_right.svg')] rb:hover:shadow-[0px_2px_8px_0px_rgba(23,23,25,0.16)]"
|
||||
></div>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={clsx("rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center", {
|
||||
'rb:justify-between': item.updated_at,
|
||||
'rb:justify-end': !item.updated_at
|
||||
})}>
|
||||
{formatDateTime(item.updated_at, 'YYYY-MM-DD HH:mm:ss')}
|
||||
<Space size={16}>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(item)}
|
||||
></div>
|
||||
{!item.is_system_default && <div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>}
|
||||
</Space>
|
||||
</div>
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
justify={item.updated_at ? "space-between" : "flex-end"}
|
||||
className="rb:text-[12px] rb:leading-4.5 rb:font-regular rb:text-[#5B6167] rb:pl-1!"
|
||||
>
|
||||
{formatDateTime(item.updated_at, 'YYYY-MM-DD HH:mm:ss')}
|
||||
<Space size={8}>
|
||||
<div className="rb:size-4.5 rb:hover:bg-[#EBEBEB] rb:rounded-md">
|
||||
<div
|
||||
className="rb:size-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/edit.svg')]"
|
||||
onClick={() => handleEdit(item)}
|
||||
></div>
|
||||
</div>
|
||||
{!item.is_system_default &&
|
||||
<div
|
||||
className="rb:size-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/delete.svg')] rb:hover:bg-[url('@/assets/images/common/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</BodyWrapper>
|
||||
|
||||
<MemoryForm
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:53:44
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-10 17:52:35
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:01:27
|
||||
*/
|
||||
/**
|
||||
* User Memory Page
|
||||
@@ -12,7 +12,7 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Row, Col, Skeleton, Form, Flex } from 'antd';
|
||||
import { Row, Col, Skeleton, Form, Flex, Tooltip } from 'antd';
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import type { Data } from './types'
|
||||
@@ -20,6 +20,7 @@ import { getUserMemoryList } from '@/api/memory';
|
||||
import { useUser } from '@/store/user'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import RbStatistic from '@/components/RbStatistic';
|
||||
|
||||
export default function UserMemory() {
|
||||
const { t } = useTranslation();
|
||||
@@ -93,38 +94,41 @@ export default function UserMemory() {
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: filterData.length > 0 ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Row gutter={[12, 12]}>
|
||||
{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 (
|
||||
<Col key={index} span={8}>
|
||||
<RbCard
|
||||
avatar={<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF] rb:mr-2">{name[0]}</div>}
|
||||
title={name || '-'}
|
||||
extra={<div
|
||||
className="rb:w-7 rb:h-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/goto.svg')]"
|
||||
></div>}
|
||||
title={<Flex gap={4}>
|
||||
<div className="rb:size-6 rb:text-center rb:font-semibold rb:leading-6 rb:rounded-md rb:text-white rb:bg-[#155EEF]">{name[0]}</div>
|
||||
|
||||
<Tooltip title={name || '-'}><div className={`rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap`}>{name || '-'}</div></Tooltip>
|
||||
</Flex>}
|
||||
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)}
|
||||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
<div>{t('userMemory.capacity')}</div>
|
||||
<div>{memory_num?.total || 0} {t('userMemory.memoryNum')}</div>
|
||||
</Flex>
|
||||
<Flex align="center" justify="space-between" className="rb:mt-2.5!">
|
||||
<div>{t('userMemory.type')}</div>
|
||||
<div>{t(`userMemory.${item.type || 'person'}`)}</div>
|
||||
</Flex>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<RbStatistic title={t('userMemory.capacity')} value={memory_num?.total || 0} suffix={t('userMemory.memoryNum')} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RbStatistic title={t('userMemory.type')} value={t(`userMemory.${item.type || 'person'}`)} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="rb:relative rb:z-2 rb:mt-3 rb:bg-[#F6F8FC] rb:rounded-lg rb-border rb:py-2 rb:px-3" onClick={handleViewMemoryConfig}>
|
||||
<Flex align="center" justify="space-between" className="rb:text-[#5B6167] rb:leading-5">
|
||||
<div className="rb:relative rb:z-2 rb:mt-3 rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2 rb:px-3 rb:leading-5" onClick={handleViewMemoryConfig}>
|
||||
<Flex align="center" justify="space-between" className="rb:text-[#5B6167]">
|
||||
{t('userMemory.memory_config_name')}
|
||||
<div
|
||||
className="rb:w-7 rb:h-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/arrow_right.svg')]"
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/arrow_right_dark.svg')]"
|
||||
></div>
|
||||
</Flex>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mt-1">{memory_config?.memory_config_name || '-'}</div>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mt-1">{memory_config?.memory_config_name || '-'}</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-08 19:46:02
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:03:50
|
||||
*/
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Select, Form, Space, Skeleton, Input } from 'antd'
|
||||
import { Row, Col, Select, Form, Skeleton, Input, Flex } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getEpisodicOverview,
|
||||
@@ -10,29 +16,40 @@ import {
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Tag from '@/components/Tag'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
/** Single episodic memory item returned by the overview API. */
|
||||
interface EpisodicMemory {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
/** Response shape of the episodic overview API. */
|
||||
interface EpisodicOverviewData {
|
||||
/** Count of memories matching the current filter. */
|
||||
total: number;
|
||||
/** Total count of all episodic memories (unfiltered). */
|
||||
total_all: number;
|
||||
episodic_memories: EpisodicMemory[]
|
||||
}
|
||||
|
||||
/** Full detail of a single episodic memory entry. */
|
||||
interface EpisodicMemoryDetail {
|
||||
id: string;
|
||||
created_at: number;
|
||||
/** People or entities involved in this episode. */
|
||||
involved_objects: string[];
|
||||
/** Category such as conversation, learning, decision, etc. */
|
||||
episodic_type: string;
|
||||
/** Ordered list of content paragraphs describing the episode. */
|
||||
content_records: string[];
|
||||
/** Emotion label associated with this episode (e.g. "joy", "neutral"). */
|
||||
emotion: string;
|
||||
}
|
||||
|
||||
/** Maps episodic type keys to Ant Design Tag color presets. */
|
||||
const TAG_COLORS: Record<string, "processing" | "success" | "warning" | "error" | "default"> = {
|
||||
conversation: "processing",
|
||||
project_work: "success",
|
||||
@@ -41,16 +58,8 @@ const TAG_COLORS: Record<string, "processing" | "success" | "warning" | "error"
|
||||
important_event: "error",
|
||||
default: 'default'
|
||||
}
|
||||
const BG_COLORS: Record<string, string> = {
|
||||
conversation: "rb:bg-[#155EEF]",
|
||||
project_work: "rb:bg-[#369F21]",
|
||||
learning: "rb:bg-[#FF5D34]",
|
||||
decision: "rb:bg-[#FF5D34]",
|
||||
important_event: "rb:bg-[#5B6167]",
|
||||
default: 'rb:bg-[#F0F3F8] rb:text-[#5B6167]!'
|
||||
}
|
||||
|
||||
// Map display types to internal keys
|
||||
/** Normalise a display-friendly type string (e.g. "Project/Work") to its internal key (e.g. "project_work"). */
|
||||
const getTypeKey = (type: string): string => {
|
||||
if (!type) return 'default'
|
||||
const typeMap: Record<string, string> = {
|
||||
@@ -62,22 +71,34 @@ const getTypeKey = (type: string): string => {
|
||||
}
|
||||
return typeMap[type] || type.toLowerCase().replace(/[^a-z0-9]/g, '_')
|
||||
}
|
||||
/**
|
||||
* EpisodicDetail – Displays a user's episodic memories in a master-detail layout.
|
||||
*
|
||||
* Left panel: filterable & searchable list of episodic memory cards.
|
||||
* Right panel: full detail view of the selected episode including metadata,
|
||||
* content records and emotion label.
|
||||
*
|
||||
* Route param `id` is the end-user ID whose memories are being viewed.
|
||||
*/
|
||||
const EpisodicDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<EpisodicOverviewData>({} as EpisodicOverviewData)
|
||||
/** Reactive form values used as filter params (time_range, episodic_type, title_keyword). */
|
||||
const values = Form.useWatch([], form)
|
||||
const [detailLoading, setDetailLoading] = useState<boolean>(false)
|
||||
const [detail, setDetail] = useState<EpisodicMemoryDetail | null>(null)
|
||||
const [selected, setSelected] = useState<EpisodicMemory | null>(null)
|
||||
|
||||
/* Fetch overview when the route user ID changes. */
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
/** Fetch the episodic memory overview list with current filter values. */
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
@@ -99,14 +120,17 @@ const EpisodicDetail: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/* Re-fetch overview whenever filter form values change. */
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [values])
|
||||
|
||||
/* Load detail whenever a different memory card is selected. */
|
||||
useEffect(() => {
|
||||
getDetail()
|
||||
}, [selected])
|
||||
|
||||
/** Fetch full detail for the currently selected episodic memory. */
|
||||
const getDetail = () => {
|
||||
if (!selected || !selected.id) return
|
||||
|
||||
@@ -124,133 +148,148 @@ const EpisodicDetail: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:text-[#FFFFFF] rb:leading-5 rb:h-30 rb:p-5 rb:bg-[url('@/assets/images/userMemory/shortTerm.png')] rb:bg-cover rb:mb-6">
|
||||
<div className="rb:max-w-135">{t('episodicDetail.title')}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-4">
|
||||
<div className="rb:bg-[rgba(255,255,255,0.2)] rb:rounded-lg rb:p-3.5 rb:text-[12px] rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-8 rb:mb-1">{data.total_all ?? 0}</div>
|
||||
{t(`episodicDetail.total_all`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form form={form} initialValues={{ time_range: 'all', episodic_type: 'all' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Form.Item name="time_range">
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'today', label: t('episodicDetail.today') },
|
||||
{ value: 'this_week', label: t('episodicDetail.this_week') },
|
||||
{ value: 'this_month', label: t('episodicDetail.this_month') },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="episodic_type">
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'conversation', label: t('episodicDetail.conversation') },
|
||||
{ value: 'project_work', label: t('episodicDetail.project_work') },
|
||||
{ value: 'learning', label: t('episodicDetail.learning') },
|
||||
{ value: 'decision', label: t('episodicDetail.decision') },
|
||||
{ value: 'important_event', label: t('episodicDetail.important_event') },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="title_keyword">
|
||||
<Input placeholder={t('episodicDetail.titleKeywordPlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<RbCard
|
||||
title={<>{t('episodicDetail.curResult')}<span className="rb:text-[#5B6167] rb:font-regular!"> ({data.total || 0}{t('episodicDetail.unix')})</span></>}
|
||||
headerType="borderless"
|
||||
bodyClassName="rb:h-[calc(100vh-349px)] rb:overflow-y-auto"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.episodic_memories || data.episodic_memories.length === 0
|
||||
? <Empty />
|
||||
: (
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{data.episodic_memories.map((vo, index) => (
|
||||
<div
|
||||
key={vo.id}
|
||||
className={clsx("rb:cursor-pointer rb:flex rb:items-center rb:bg-[#FFFFFF] rb:border rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5", {
|
||||
'rb:border-[#DFE4ED] rb:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.16)]': selected?.id !== vo.id,
|
||||
'rb:border-[#155EEF]': selected?.id === vo.id,
|
||||
})}
|
||||
onClick={() => setSelected(vo)}
|
||||
>
|
||||
<div className={clsx("rb:rounded-lg rb:text-[#FFFFFF] rb:size-6 rb:text-[12px] rb:leading-6 rb:text-center rb:mr-3", BG_COLORS[getTypeKey(vo.type)])}>{index + 1}</div>
|
||||
<div className="rb:flex-1 rb:w-[calc(100%-36px)]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{vo.title}</div>
|
||||
{vo.type && <Tag color={TAG_COLORS[getTypeKey(vo.type)]}>{t(`episodicDetail.${getTypeKey(vo.type)}`)}</Tag>}
|
||||
</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px]">{formatDateTime(vo.created_at)}</div>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col flex="400px">
|
||||
<RbCard
|
||||
title={<div className="rb:leading-5.5!">
|
||||
<span className="rb:font-[MiSans-Bold] rb:font-bold">{t('episodicDetail.curResult')}</span>
|
||||
<span className="rb:text-[#5B6167] rb:font-regular!"> ({data.total || 0}{t('episodicDetail.unix')})</span>
|
||||
</div>}
|
||||
headerType="borderless"
|
||||
className="rb:h-[calc(100vh-88px)]!"
|
||||
headerClassName="rb:min-h-[38px]! rb:pt-3! rb:mb-0!"
|
||||
bodyClassName="rb:p-3! rb:pb-0! rb:h-[calc(100%-38px)]!"
|
||||
>
|
||||
<Form form={form} initialValues={{ time_range: 'all', episodic_type: 'all' }}>
|
||||
<Row gutter={[8, 8]} className="rb:mb-3">
|
||||
<Col span={12}>
|
||||
<Form.Item name="time_range" noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'today', label: t('episodicDetail.today') },
|
||||
{ value: 'this_week', label: t('episodicDetail.this_week') },
|
||||
{ value: 'this_month', label: t('episodicDetail.this_month') },
|
||||
]}
|
||||
className="rb:w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="episodic_type" noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'conversation', label: t('episodicDetail.conversation') },
|
||||
{ value: 'project_work', label: t('episodicDetail.project_work') },
|
||||
{ value: 'learning', label: t('episodicDetail.learning') },
|
||||
{ value: 'decision', label: t('episodicDetail.decision') },
|
||||
{ value: 'important_event', label: t('episodicDetail.important_event') },
|
||||
]}
|
||||
className="rb:w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="title_keyword" noStyle>
|
||||
<Input placeholder={t('episodicDetail.titleKeywordPlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.episodic_memories || data.episodic_memories.length === 0
|
||||
? <Empty />
|
||||
: (
|
||||
<Flex gap={12} vertical className="rb:overflow-y-auto rb:h-[calc(100%-84px)] rb:pb-3!">
|
||||
{data.episodic_memories.map((vo, index) => (
|
||||
<Flex
|
||||
key={vo.id}
|
||||
gap={12}
|
||||
align="center"
|
||||
className={clsx("rb:cursor-pointer rb:rounded-xl rb:px-3! rb:py-2!", {
|
||||
'rb-border': selected?.id !== vo.id,
|
||||
'rb:border rb:border-[#171719]': selected?.id === vo.id,
|
||||
})}
|
||||
onClick={() => setSelected(vo)}
|
||||
>
|
||||
<div className="rb:rounded-md rb:text-[#FFFFFF] rb:size-5 rb:text-[10px] rb:leading-3.5 rb:text-center rb:py-0.75 rb:bg-[#171719]">{index + 1}</div>
|
||||
<div className="rb:flex-1 rb:w-[calc(100%-36px)]">
|
||||
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1 rb:text-[#212332] rb:font-medium rb:leading-5 rb:mb-1">{vo.title}</div>
|
||||
<Flex align="center" justify="space-between" className="rb:text-[#5B6167] rb:text-[12px]">
|
||||
{formatDateTime(vo.created_at)}
|
||||
{vo.type && <Tag color={TAG_COLORS[getTypeKey(vo.type)]}>{t(`episodicDetail.${getTypeKey(vo.type)}`)}</Tag>}
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={selected?.title}
|
||||
headerType="borderless"
|
||||
bodyClassName="rb:h-[calc(100vh-349px)] rb:overflow-y-auto"
|
||||
>
|
||||
{detailLoading
|
||||
? <Skeleton active />
|
||||
: !selected || !detail
|
||||
? <Empty className="rb:mt-14" />
|
||||
: (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5">
|
||||
<Row gutter={[12, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="rb:text-[#5B6167]">{t('episodicDetail.created')}<br />{formatDateTime(detail.created_at)}</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="rb:text-[#5B6167]">{t('episodicDetail.episodic_type')}<br />{detail.episodic_type}</div>
|
||||
</Col>
|
||||
{detail.involved_objects.length > 0 && <Col span={24}>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('episodicDetail.involved_objects')}</div>
|
||||
<Space size={8}>{detail.involved_objects.map((vo, index) => <Tag key={index}>{vo}</Tag>)}</Space>
|
||||
</Col>}
|
||||
</Row>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<RbCard
|
||||
title={selected?.title}
|
||||
headerType="borderless"
|
||||
className="rb:h-[calc(100vh-88px)]!"
|
||||
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"
|
||||
>
|
||||
{detailLoading
|
||||
? <Skeleton active />
|
||||
: !selected || !detail
|
||||
? <Empty className="rb:mt-14" />
|
||||
: (
|
||||
<Flex gap={16} vertical>
|
||||
<div className="rb-border rb:rounded-xl rb:px-4 rb:py-3">
|
||||
<Row gutter={12}>
|
||||
<Col span={8}>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5">
|
||||
{t('episodicDetail.created')}
|
||||
<div className="rb:font-medium rb:mt-1 rb:text-[#171719]">{formatDateTime(detail.created_at)}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5">
|
||||
{t('episodicDetail.episodic_type')}
|
||||
<div className="rb:font-medium rb:mt-1 rb:text-[#171719]">{detail.episodic_type}</div>
|
||||
</div>
|
||||
</Col>
|
||||
{detail.involved_objects.length > 0 && <Col span={8}>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5">
|
||||
{t('episodicDetail.involved_objects')}
|
||||
<Flex gap={8} className="rb:mt-1!">{detail.involved_objects.map((vo, index) => <Tag key={index}>{vo}</Tag>)}</Flex>
|
||||
</div>
|
||||
</Col>}
|
||||
</Row>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-2 rb:pl-1">{t('episodicDetail.content_records')}</div>
|
||||
|
||||
<ul className="rb:leading-5.5 rb:list-disc rb-border rb:rounded-xl rb:pl-8 rb:pr-4 rb:py-3">
|
||||
{detail.content_records.map((vo, index) => <li key={index}>{vo}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-2 rb:pl-1">{t('episodicDetail.emotion')}</div>
|
||||
<div className="rb-border rb:rounded-xl rb:px-4 rb:py-3">
|
||||
{detail.emotion
|
||||
? t(`episodicDetail.${detail.emotion || 'none'}`)
|
||||
: <Empty size={96} className="rb:pt-1! rb:pb-3.5!" />
|
||||
}
|
||||
</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5">
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('episodicDetail.content_records')}</div>
|
||||
{detail.content_records.map((vo, index) => <div key={index} className="rb:text-[#5B6167] rb:leading-5">- {vo}</div>)}
|
||||
</div>
|
||||
<RbAlert>
|
||||
{t('episodicDetail.emotion')}: {t(`episodicDetail.${detail.emotion || 'none'}`)}
|
||||
</RbAlert>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
export default EpisodicDetail
|
||||
@@ -1,7 +1,16 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-10 17:35:17
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:05:06
|
||||
*/
|
||||
import { type FC, useEffect, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Row, Col } from 'antd'
|
||||
import { Skeleton, Row, Col, Flex } from 'antd'
|
||||
import * as echarts from 'echarts'
|
||||
import 'echarts-wordcloud'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getExplicitMemory,
|
||||
@@ -10,40 +19,67 @@ import { formatDateTime } from '@/utils/format'
|
||||
import Empty from '@/components/Empty'
|
||||
import ExplicitDetailModal from '../components/ExplicitDetailModal'
|
||||
|
||||
/** An episodic (event-based) memory entry with a title and free-text content. */
|
||||
export interface EpisodicMemory {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
/** A semantic (concept-based) memory entry extracted as a named entity. */
|
||||
export interface SemanticMemory {
|
||||
id: string;
|
||||
/** Entity name displayed in the word cloud. */
|
||||
name: string;
|
||||
/** Classification of the entity (e.g. person, location, concept). */
|
||||
entity_type: string;
|
||||
/** Brief definition or description of the entity. */
|
||||
core_definition: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
/** Combined API response containing both memory categories. */
|
||||
interface Data {
|
||||
episodic_memories: EpisodicMemory[];
|
||||
semantic_memories: SemanticMemory[]
|
||||
}
|
||||
|
||||
/** Imperative handle exposed by ExplicitDetailModal for opening the detail drawer. */
|
||||
export interface ExplicitDetailModalRef {
|
||||
handleOpen: (vo: EpisodicMemory | SemanticMemory) => void;
|
||||
}
|
||||
|
||||
/** Rotating colour palette used for word-cloud text. */
|
||||
const DEFAULT_COLORS = ['#FF8A4C', '#FF5D34', '#155EEF', '#9C6FFF', '#4DA8FF', '#369F21']
|
||||
|
||||
/**
|
||||
* ExplicitDetail – Two-column view of a user's explicit memories.
|
||||
*
|
||||
* Left column: scrollable list of episodic memory cards (title + content).
|
||||
* Right column: ECharts word cloud built from semantic memory entity names;
|
||||
* clicking a word opens the detail modal.
|
||||
*
|
||||
* Route param `id` is the end-user ID whose memories are displayed.
|
||||
*/
|
||||
const ExplicitDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const explicitDetailModalRef = useRef<ExplicitDetailModalRef>(null)
|
||||
/** Container element for the ECharts word-cloud instance. */
|
||||
const wordCloudRef = useRef<HTMLDivElement>(null)
|
||||
/** Keeps a stable reference to the ECharts instance for cleanup. */
|
||||
const chartInstance = useRef<echarts.ECharts | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<Data>({ episodic_memories: [], semantic_memories: [] })
|
||||
|
||||
/* Fetch data whenever the route user ID changes. */
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
|
||||
/** Load both episodic and semantic memories for the current user. */
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
@@ -56,54 +92,98 @@ const ExplicitDetail: FC = () => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
/** Open the detail modal for a given memory item. */
|
||||
const handleView = (item: EpisodicMemory | SemanticMemory) => {
|
||||
explicitDetailModalRef.current?.handleOpen(item)
|
||||
}
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-3 rb:rounded-md rb:mb-4">{t('explicitDetail.episodic_memories')}</div>
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: data.episodic_memories?.length > 0 ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{data.episodic_memories.map(item => (
|
||||
<Col key={item.id} span={6}>
|
||||
<RbCard
|
||||
title={item.title}
|
||||
className="rb:h-full! rb:cursor-pointer"
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
<div>{formatDateTime(item.created_at)}</div>
|
||||
<div>{item.content}</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : <Empty />}
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('explicitDetail.semantic_memories')}</div>
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: data.semantic_memories?.length > 0 ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{data.semantic_memories.map(item => (
|
||||
<Col key={item.id} span={6}>
|
||||
<RbCard
|
||||
title={item.name}
|
||||
className="rb:h-full! rb:cursor-pointer"
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
<div>{item.core_definition}</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : <Empty />}
|
||||
/**
|
||||
* Initialise / re-render the word cloud whenever semantic memories change.
|
||||
* Each word is clickable and opens the detail modal for that entity.
|
||||
* The chart instance is disposed on cleanup to prevent memory leaks.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!wordCloudRef.current || !data.semantic_memories?.length) return
|
||||
if (chartInstance.current) chartInstance.current.dispose()
|
||||
chartInstance.current = echarts.init(wordCloudRef.current)
|
||||
chartInstance.current.setOption({
|
||||
series: [{
|
||||
type: 'wordCloud',
|
||||
gridSize: 8,
|
||||
sizeRange: [14, 56],
|
||||
rotationRange: [-45, 45],
|
||||
shape: 'pentagon',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
textStyle: { fontFamily: 'sans-serif', fontWeight: 'bold' },
|
||||
emphasis: { textStyle: { shadowBlur: 10, shadowColor: '#333' } },
|
||||
data: data.semantic_memories.map((item, index) => ({
|
||||
name: item.name,
|
||||
value: 50 + (index % 5) * 10,
|
||||
itemIndex: index,
|
||||
textStyle: { color: DEFAULT_COLORS[index % DEFAULT_COLORS.length] }
|
||||
}))
|
||||
}]
|
||||
})
|
||||
chartInstance.current.on('click', (params) => {
|
||||
const item = data.semantic_memories[(params.data as any).itemIndex]
|
||||
if (item) handleView(item)
|
||||
})
|
||||
return () => { chartInstance.current?.dispose(); chartInstance.current = null }
|
||||
}, [data.semantic_memories])
|
||||
return (
|
||||
<Row gutter={12} className="rb:h-full">
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={t('explicitDetail.episodic_memories')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[50px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-50px)] rb:overflow-y-auto!"
|
||||
className="rb:h-[calc(100vh-88px)]!"
|
||||
>
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: data.episodic_memories?.length > 0 ? (
|
||||
<Flex gap={12} vertical>
|
||||
{data.episodic_memories.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="rb:cursor-pointer rb:bg-[#F6F6F6] rb:rounded-xl rb:pt-2.5 rb:px-3 rb:pb-3"
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
<span className="rb:font-medium rb:pl-1">{item.title}</span>
|
||||
<div className="rb:textt-[#5B6167] rb:leading-4.25 rb:text-[12px]">{formatDateTime(item.created_at)}</div>
|
||||
</Flex>
|
||||
<div className="rb:bg-white rb:rounded-lg rb:py-2.5 rb:px-3 rb:mt-2.5 rb:leading-5">{item.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
) : <Empty />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={t('explicitDetail.semantic_memories')}
|
||||
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.semantic_memories?.length > 0
|
||||
? <div ref={wordCloudRef} className="rb:h-full rb:w-full rb:cursor-pointer" />
|
||||
: <Empty />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
|
||||
<ExplicitDetailModal
|
||||
ref={explicitDetailModalRef}
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
export default ExplicitDetail
|
||||
@@ -1,7 +1,14 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-07 20:37:34
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:07:50
|
||||
*/
|
||||
import { useEffect, useState, useMemo, forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Progress, App } from 'antd'
|
||||
import { Row, Col, Progress, App, Table } from 'antd'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getForgetStats,
|
||||
@@ -9,11 +16,11 @@ import {
|
||||
import type { ForgetData } from '../types'
|
||||
import ActivationMetricsPieCard from '../components/ActivationMetricsPieCard'
|
||||
import RecentTrendsLineCard from '../components/RecentTrendsLineCard'
|
||||
import Table from '@/components/Table'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import StatusTag from '@/components/StatusTag'
|
||||
import ForgetRefreshModal from '../components/ForgetRefreshModal'
|
||||
import ForgetRefreshModal from '../components/ForgetRefreshModal';
|
||||
|
||||
/** Maps node type keys to StatusTag colour presets for the pending-nodes table. */
|
||||
const statusTagColors: Record<string, 'success' | 'purple' | 'default' | 'warning' | 'error' | 'lightBlue'> = {
|
||||
statement: 'success',
|
||||
entity: 'purple',
|
||||
@@ -21,10 +28,25 @@ const statusTagColors: Record<string, 'success' | 'purple' | 'default' | 'warnin
|
||||
chunk: 'warning',
|
||||
}
|
||||
|
||||
/** Imperative handle exposed by ForgetRefreshModal for triggering the refresh dialog. */
|
||||
export interface ForgetRefreshModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ForgetDetail – Dashboard for the forgetting engine of a single user.
|
||||
*
|
||||
* Layout (top → bottom):
|
||||
* 1. Overview row (3 metric cards): total memory nodes, memory health
|
||||
* (average activation vs threshold), and forgetting-risk count.
|
||||
* 2. Pie chart (activation distribution) + line chart (7-day trends).
|
||||
* 3. Pending-nodes table listing low-activation memories at risk of being forgotten.
|
||||
*
|
||||
* The parent can trigger a manual forgetting refresh via the imperative
|
||||
* `handleRefresh` method exposed through `forwardRef`.
|
||||
*
|
||||
* Route param `id` is the end-user ID.
|
||||
*/
|
||||
const ForgetDetail = forwardRef((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
@@ -33,11 +55,16 @@ const ForgetDetail = forwardRef((_props, ref) => {
|
||||
const [data, setData] = useState<ForgetData>({} as ForgetData)
|
||||
const forgetRefreshModalRef = useRef<ForgetRefreshModalRef>(null)
|
||||
|
||||
/* Fetch stats whenever the route user ID changes. */
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
/**
|
||||
* Load forgetting-engine statistics for the current user.
|
||||
* @param flag - When true, shows a success toast after loading (used after manual refresh).
|
||||
*/
|
||||
const getData = (flag: boolean = false) => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
@@ -53,6 +80,10 @@ const ForgetDetail = forwardRef((_props, ref) => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Derive pie-chart data from activation metrics.
|
||||
* Splits total nodes into three zones: healthy, observation (no activation), and forgetting (low activation).
|
||||
*/
|
||||
const chartData = useMemo(() => {
|
||||
const { activation_metrics } = data
|
||||
if (!activation_metrics) return []
|
||||
@@ -67,6 +98,10 @@ const ForgetDetail = forwardRef((_props, ref) => {
|
||||
|
||||
}, [data.activation_metrics, t])
|
||||
|
||||
/**
|
||||
* Prepare line-chart series from the recent 7-day trend data.
|
||||
* Returns merged_count (daily merged nodes) and average_activation as two series.
|
||||
*/
|
||||
const seriesList = useMemo(() => {
|
||||
const { recent_trends = [] } = data
|
||||
if (!recent_trends || recent_trends.length === 0) return { chartData: [], seriesList: [] }
|
||||
@@ -77,105 +112,138 @@ const ForgetDetail = forwardRef((_props, ref) => {
|
||||
}
|
||||
}, [data.recent_trends])
|
||||
|
||||
/** Open the forgetting-refresh confirmation modal. */
|
||||
const handleRefresh = () => {
|
||||
forgetRefreshModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
/* Expose handleRefresh to parent components via ref. */
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('forgetDetail.title')}</div>
|
||||
<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={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.totalMemory')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data?.activation_metrics?.total_nodes ?? 0}</div>
|
||||
<div className="rb:mt-4 rb:grid rb:grid-cols-2 rb:gap-x-2 rb:gap-y-5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2">
|
||||
{['statement_count', 'entity_count', 'summary_count', 'chunk_count'].map((key, index) => (
|
||||
<div key={index}>
|
||||
<div className="rb:text-[16px] rb:font-bold rb:leading-5.5">{data?.node_distribution?.[key as keyof typeof data.node_distribution] ?? 0}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mt-1">{t(`forgetDetail.${key}`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.MemoryHealth')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data?.activation_metrics?.average_activation_value ?? 0}</div>
|
||||
<Progress className="rb:mt-px" showInfo={false} percent={data?.activation_metrics?.average_activation_value ?? 0} />
|
||||
<div className="rb:mt-4 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{t('forgetDetail.healthStatus')}</div>
|
||||
<div className="rb:text-[20px] rb:font-semibold rb:leading-7">{data?.activation_metrics?.average_activation_value > data.activation_metrics?.forgetting_threshold ? t('forgetDetail.healthy') : t('forgetDetail.unhealthy')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:mt-2">
|
||||
{t('forgetDetail.average')}<br />
|
||||
{t('forgetDetail.threshold')}{data.activation_metrics?.forgetting_threshold ?? 0}
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={t('forgetDetail.overviewTitle')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:pt-0! rb:overflow-visible!"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-3">
|
||||
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-2 rb:pt-3">
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mb-1">{t('forgetDetail.totalMemory')}</div>
|
||||
<div className="rb:text-[18px] rb:font-[MiSans-Bold] rb:font-bold rb:leading-6 rb:mb-2">{data?.activation_metrics?.total_nodes ?? 0}</div>
|
||||
<div className="rb:bg-white rb:rounded-lg rb:p-3 rb:grid rb:grid-cols-2 rb:gap-x-2 rb:gap-y-6">
|
||||
{['statement_count', 'entity_count', 'summary_count', 'chunk_count'].map((key, index) => (
|
||||
<div key={index}>
|
||||
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:leading-4.75">{data?.node_distribution?.[key as keyof typeof data.node_distribution] ?? 0}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mt-1">{t(`forgetDetail.${key}`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.riskOfForgetting')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data.activation_metrics?.low_activation_nodes ?? 0}</div>
|
||||
<div className="rb:mb-31.5 rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4 rb:font-regular rb:mt-1">{t('forgetDetail.low_nodes')}</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-2 rb:pt-3">
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mb-1">{t('forgetDetail.MemoryHealth')}</div>
|
||||
<div className="rb:text-[18px] rb:font-[MiSans-Bold] rb:font-bold rb:leading-6">{data?.activation_metrics?.average_activation_value ?? 0}</div>
|
||||
<div className="rb:-mt-1 rb:mb-2">
|
||||
<Progress showInfo={false} percent={data?.activation_metrics?.average_activation_value ?? 0} />
|
||||
</div>
|
||||
|
||||
<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.memoryHealthVisualization')}</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<ActivationMetricsPieCard chartData={chartData} loading={loading} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RecentTrendsLineCard chartData={seriesList.chartData} seriesList={seriesList.seriesList} loading={loading} />
|
||||
</Col>
|
||||
</Row>
|
||||
<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.pending_nodes')}</div>
|
||||
<Table
|
||||
rowKey='node_id'
|
||||
initialData={data.pending_nodes ?? []}
|
||||
columns={[
|
||||
{
|
||||
title: t('forgetDetail.content_summary'),
|
||||
dataIndex: 'content_summary',
|
||||
key: 'content_summary',
|
||||
width: 340,
|
||||
render: (content_summary) => <div className="rb:wrap-break-word rb:line-clamp-2">{content_summary}</div>
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.node_type'),
|
||||
dataIndex: 'node_type',
|
||||
key: 'node_type',
|
||||
render: (node_type: string) => {
|
||||
return <StatusTag status={statusTagColors[node_type] || 'default'} text={node_type} />}
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.last_access_time'),
|
||||
dataIndex: 'last_access_time',
|
||||
key: 'last_access_time',
|
||||
render: (last_access_time) => formatDateTime(last_access_time, 'YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.activation_value'),
|
||||
dataIndex: 'activation_value',
|
||||
key: 'activation_value',
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
/>
|
||||
<div className="rb:bg-white rb:rounded-lg rb:p-3 rb:pt-2">
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mb-1">{t('forgetDetail.healthStatus')}</div>
|
||||
<div className="rb:text-[16px] rb:font-[MiSans-Bold] rb:font-bold rb:leading-6 rb:mb-3">
|
||||
{data?.activation_metrics?.average_activation_value > data.activation_metrics?.forgetting_threshold
|
||||
? t('forgetDetail.healthy')
|
||||
: t('forgetDetail.unhealthy')
|
||||
}
|
||||
</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">
|
||||
{t('forgetDetail.average')}<br />
|
||||
{t('forgetDetail.threshold')}{data.activation_metrics?.forgetting_threshold ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:p-2 rb:pt-3">
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mb-1">{t('forgetDetail.riskOfForgetting')}</div>
|
||||
<div className="rb:text-[18px] rb:font-[MiSans-Bold] rb:font-bold rb:leading-6">{data.activation_metrics?.low_activation_nodes ?? 0}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[10px] rb:leading-3.5">{t('forgetDetail.low_nodes')}</div>
|
||||
|
||||
<div className="rb:bg-white rb:rounded-lg rb:mt-2">
|
||||
<div className="rb:h-29.5 rb:w-full rb:bg-cover rb:bg-[url('@/assets/images/userMemory/forget.png')]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<ActivationMetricsPieCard chartData={chartData} loading={loading} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<RecentTrendsLineCard chartData={seriesList.chartData} seriesList={seriesList.seriesList} loading={loading} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<RbCard
|
||||
title={t('forgetDetail.pending_nodes')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:py-0! rb:h-[calc(100%-54px)]"
|
||||
className="rb:h-full!"
|
||||
>
|
||||
<Table
|
||||
rowKey='node_id'
|
||||
dataSource={data.pending_nodes ?? []}
|
||||
columns={[
|
||||
{
|
||||
title: t('forgetDetail.content_summary'),
|
||||
dataIndex: 'content_summary',
|
||||
key: 'content_summary',
|
||||
render: (content_summary) => <div className="rb:wrap-break-word rb:line-clamp-2">{content_summary}</div>
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.node_type'),
|
||||
dataIndex: 'node_type',
|
||||
key: 'node_type',
|
||||
width: '20%',
|
||||
render: (node_type: string) => {
|
||||
return <StatusTag status={statusTagColors[node_type] || 'default'} text={node_type} />
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.last_access_time'),
|
||||
dataIndex: 'last_access_time',
|
||||
key: 'last_access_time',
|
||||
width: '20%',
|
||||
render: (last_access_time) => <span className="rb:text-[#5B6167]">{formatDateTime(last_access_time, 'YYYY-MM-DD HH:mm')}</span>
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.activation_value'),
|
||||
dataIndex: 'activation_value',
|
||||
key: 'activation_value',
|
||||
width: '20%',
|
||||
render: (activation_value) => <span className="rb:text-[#5B6167]">{activation_value}</span>
|
||||
},
|
||||
]}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
showQuickJumper: true,
|
||||
className: 'rb:mt-5! rb:mb-5.75!'
|
||||
}}
|
||||
className="table-header-has-bg"
|
||||
/>
|
||||
</RbCard>
|
||||
</Col>
|
||||
|
||||
<ForgetRefreshModal
|
||||
ref={forgetRefreshModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('forgetDetail.title')}</div>
|
||||
|
||||
</div> */}
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
export default ForgetDetail
|
||||
@@ -2,9 +2,9 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-08 19:46:02
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:26:55
|
||||
* @Last Modified time: 2026-03-16 14:27:58
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react'
|
||||
import { forwardRef, useImperativeHandle, useRef, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Row, Col, App } from 'antd'
|
||||
import { useParams } from 'react-router-dom'
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
generateProfile,
|
||||
implicitCheckData,
|
||||
} from '@/api/memory'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import RadioGroupButton from '@/components/RadioGroupButton'
|
||||
|
||||
/**
|
||||
* ImplicitDetail Component - Displays user's implicit memory profile
|
||||
@@ -75,24 +77,46 @@ const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }, { refresh: ()
|
||||
handleRefresh
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('implicitDetail.title')}</div>
|
||||
|
||||
<Preferences ref={preferencesRef} />
|
||||
const [activeTab, setActiveTab] = useState('preferences')
|
||||
const handleChangeTab = (value: unknown) => {
|
||||
setActiveTab(value as string)
|
||||
}
|
||||
|
||||
<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.portraitTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.portraitSubTitle')}</div>
|
||||
<Row gutter={[16, 16]} className="rb:mt-4">
|
||||
return (
|
||||
<div className="rb:h-full">
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Portrait ref={portraitRef} />
|
||||
<RbCard
|
||||
title={t('implicitDetail.subconscious')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[50px]! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-54px)]"
|
||||
className="rb:h-[calc(100vh-84px)]!"
|
||||
>
|
||||
<RadioGroupButton
|
||||
value={activeTab}
|
||||
options={[
|
||||
{ value: 'preferences', label: t('implicitDetail.preferences') },
|
||||
{ value: 'portrait', label: t('implicitDetail.portrait') },
|
||||
]}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
|
||||
<div className="rb:mt-3 rb:h-[calc(100%-32px)]">
|
||||
{activeTab === 'preferences'
|
||||
? <Preferences ref={preferencesRef} />
|
||||
: <div className="rb:h-full rb:overflow-y-auto">
|
||||
<Portrait ref={portraitRef} />
|
||||
<InterestAreas ref={interestAreasRef} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InterestAreas ref={interestAreasRef} />
|
||||
<Habits ref={habitsRef} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Habits ref={habitsRef} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-08 19:46:02
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:09:12
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Row, Col } from 'antd'
|
||||
|
||||
import PerceptualLastInfo from '../components/PerceptualLastInfo'
|
||||
import Timeline from '../components/Timeline'
|
||||
|
||||
/**
|
||||
* PerceptualDetail – Two-column view of a user's perceptual memory.
|
||||
*
|
||||
* Left column (fixed 480px): real-time sensory dashboard showing the latest
|
||||
* visual, auditory and text perception streams (PerceptualLastInfo).
|
||||
* Right column (fluid): chronological perception timeline (Timeline).
|
||||
*
|
||||
* Route param `id` (consumed by child components) identifies the end-user.
|
||||
*/
|
||||
const PerceptualDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('perceptualDetail.lastInfo')}</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_visual" />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_listen" />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_text" />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('perceptualDetail.timeLine')}</div>
|
||||
<Timeline />
|
||||
</div>
|
||||
<Row gutter={12}>
|
||||
<Col flex="480px">
|
||||
<PerceptualLastInfo />
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Timeline />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
export default PerceptualDetail
|
||||
@@ -1,40 +1,78 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-08 19:46:02
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:09:49
|
||||
*/
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Space, Skeleton } from 'antd'
|
||||
import { Space, Skeleton, Row, Col, Flex, Tooltip } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import {
|
||||
getShortTerm,
|
||||
} from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
|
||||
/** A single deep-retrieval entry: the original query and its retrieved passages. */
|
||||
interface ShortTermItem {
|
||||
retrieval: Array<{ query: string; retrieval: string[]; }>;
|
||||
/** The user's original message that triggered the retrieval. */
|
||||
message: string;
|
||||
/** The generated answer based on retrieved passages. */
|
||||
answer: string;
|
||||
}
|
||||
|
||||
/** A candidate entry waiting to be promoted into long-term memory. */
|
||||
interface LongTermItem {
|
||||
query: string;
|
||||
/** Aggregated retrieval text (may contain newlines). */
|
||||
retrieval: string;
|
||||
}
|
||||
|
||||
/** Combined API response for the short-term memory page. */
|
||||
interface ShortData {
|
||||
short_term: ShortTermItem[];
|
||||
long_term: LongTermItem[];
|
||||
/** Total number of extracted entities. */
|
||||
entity: number;
|
||||
/** Total retrieval count. */
|
||||
retrieval_number: number;
|
||||
/** Number of long-term memory candidates. */
|
||||
long_term_number: number;
|
||||
}
|
||||
/**
|
||||
* ShortTermDetail – Displays the AI system's short-term "workbench" memory.
|
||||
*
|
||||
* Layout:
|
||||
* - Top-left: three KPI cards (retrieval count, extracted entities, long-term candidates).
|
||||
* - Left column: deep-retrieval entries – each shows the user message, expandable
|
||||
* sub-queries with retrieved passages, and the generated answer.
|
||||
* - Right column: long-term candidate pool – aggregated entries waiting to be
|
||||
* promoted into persistent memory, with expand/collapse for long content.
|
||||
*
|
||||
* Route param `id` is the end-user ID.
|
||||
*/
|
||||
const ShortTermDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<ShortData>({} as ShortData)
|
||||
/** Tracks expand/collapse state for each long-term candidate card by index. */
|
||||
const [longTermExpandedMap, setLongTermExpandedMap] = useState<Record<number, boolean>>({})
|
||||
/** Tracks expand/collapse state for short-term sub-queries and answers by composite key. */
|
||||
const [shortTermExpandedMap, setShortTermExpandedMap] = useState<Record<string, boolean>>({})
|
||||
|
||||
/* Fetch data whenever the route user ID changes. */
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
/** Load short-term memory data (deep retrieval + long-term candidates) for the current user. */
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
@@ -49,71 +87,154 @@ const ShortTermDetail: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:text-[#FFFFFF] rb:leading-5 rb:h-30 rb:p-5 rb:bg-[url('@/assets/images/userMemory/shortTerm.png')] rb:bg-cover">
|
||||
<div className="rb:max-w-135">{t('shortTermDetail.title')}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-3 rb:mb-3">
|
||||
{(['retrieval_number', 'entity', 'long_term_number'] as const).map(key => (
|
||||
<div key={key} className="rb:bg-[rgba(255,255,255,0.2)] rb:rounded-lg rb:p-3.5 rb:text-[12px] rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-8 rb:mb-1">{(data as any)[key] ?? 0}</div>
|
||||
{t(`shortTermDetail.${key}`)}
|
||||
</div>
|
||||
<Flex key={key} align="center" justify="space-between" className="rb:bg-white rb:rounded-xl rb:py-3! rb:pl-5! rb:pr-4!">
|
||||
<div>
|
||||
<div className="rb:text-[24px] rb:leading-8 rb:mb-1">{(data as any)[key] ?? 0}</div>
|
||||
{t(`shortTermDetail.${key}`)}
|
||||
</div>
|
||||
{key === 'retrieval_number'
|
||||
? <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/retrieval_number.svg)]"></div>
|
||||
: key === 'entity'
|
||||
? <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/entity.svg')]"></div>
|
||||
: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/long_term_number.svg')]"></div>
|
||||
}
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RbCard
|
||||
title={() => (<Space size={4}>
|
||||
{t('shortTermDetail.shortTermTitle')}
|
||||
<Tooltip title={t('shortTermDetail.shortTermSubTitle')}>
|
||||
<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-183px)]!"
|
||||
>
|
||||
<Flex gap={12} vertical>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.short_term || data.short_term.length === 0
|
||||
? <Empty />
|
||||
: data.short_term?.map((vo, voIdx) => (
|
||||
<Flex key={voIdx} gap={12} vertical className="rb:leading-5 rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3!">
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:leading-5">{vo.message}</div>
|
||||
{vo.retrieval.map((item, index) => {
|
||||
const key = `${voIdx}-${index}`
|
||||
const expanded = shortTermExpandedMap[key]
|
||||
return (
|
||||
<div key={index} className="rb:bg-white rb:rounded-md rb:px-3 rb:py-2.5 rb:leading-5">
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
className={clsx("rb:font-medium rb:cursor-pointer", {
|
||||
'rb:mb-2!': expanded,
|
||||
})}
|
||||
onClick={() => setShortTermExpandedMap(prev => ({ ...prev, [key]: !prev[key] }))}
|
||||
>
|
||||
<span>{t('shortTermDetail.query')}{index + 1}</span>
|
||||
<div className={clsx("rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/down.svg')]", {
|
||||
'rb:rotate-180': expanded
|
||||
})}></div>
|
||||
</Flex>
|
||||
{expanded && (
|
||||
<>
|
||||
<div className="rb:text-[#5B6167] rb:mb-4">
|
||||
<Markdown content={item.query} />
|
||||
</div>
|
||||
<div className="rb:font-medium rb:mb-2">{t('shortTermDetail.answer')}</div>
|
||||
{item.retrieval.length > 0
|
||||
? <ul className="rb:list-disc rb:ml-4 rb:text-[#5B6167]">
|
||||
{item.retrieval.map((retrieval, retrievalIdx) => (
|
||||
<li key={retrievalIdx} className="rb:text-[#5B6167]">{retrieval}</li>
|
||||
))}
|
||||
</ul>
|
||||
: <div className="rb:text-[#5B6167]">{t('shortTermDetail.noAnswer')}</div>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="rb:leading-5 rb:bg-white rb:rounded-xl rb:p-3!">
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
className={clsx("rb:font-medium rb:cursor-pointer", {
|
||||
'rb:mb-2!': shortTermExpandedMap[`ans-${voIdx}`],
|
||||
})}
|
||||
onClick={() => setShortTermExpandedMap(prev => ({ ...prev, [`ans-${voIdx}`]: !prev[`ans-${voIdx}`] }))}
|
||||
>
|
||||
<span>{t('shortTermDetail.answer')}</span>
|
||||
<div className={clsx("rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/down.svg')]", {
|
||||
'rb:rotate-180': shortTermExpandedMap[`ans-${voIdx}`]
|
||||
})}></div>
|
||||
</Flex>
|
||||
{shortTermExpandedMap[`ans-${voIdx}`] && (
|
||||
<div className="rb:text-[#5B6167]">
|
||||
<Markdown content={vo.answer} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
))
|
||||
}
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={() => (<Space size={4}>
|
||||
{t('shortTermDetail.longTermTitle')}
|
||||
<Tooltip title={t('shortTermDetail.longTermTitleSubTitle')}>
|
||||
<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)]!"
|
||||
>
|
||||
<Flex vertical gap={12}>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.long_term || data.long_term.length === 0
|
||||
? <Empty />
|
||||
: data.long_term?.map((vo, voIdx) => {
|
||||
const lines = vo.retrieval.split('\n')
|
||||
const expanded = longTermExpandedMap[voIdx]
|
||||
return (
|
||||
<div key={voIdx} className="rb:leading-5 rb:bg-[#F6F6F6] rb:rounded-xl rb:p-3">
|
||||
<div className="rb:mb-3 rb:text-[#212332] rb:font-medium rb:px-1">{vo.query}</div>
|
||||
<div className="rb:bg-white rb:rounded-xl rb:px-3 rb:py-2.5">
|
||||
<div className={expanded ? undefined : "rb:wrap-break-word rb:line-clamp-3"}>
|
||||
<Markdown content={vo.retrieval} />
|
||||
</div>
|
||||
|
||||
<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('shortTermDetail.shortTermTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('shortTermDetail.shortTermSubTitle')}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.short_term || data.short_term.length === 0
|
||||
? <Empty />
|
||||
:data.short_term?.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-6 rb:py-3">
|
||||
<div className="rb:font-medium rb:text-[16px] rb:leading-5.5 rb:mb-3">{vo.message}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{vo.retrieval.map((item, index) => (
|
||||
<div key={index} className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-3 rb:py-2.5 rb:leading-5">
|
||||
<div className="rb:font-medium rb:mb-3">{t('shortTermDetail.query')}: {item.query}</div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('shortTermDetail.answer')}:</div>
|
||||
{item.retrieval.length > 0 ? item.retrieval.map((retrieval, retrievalIdx) => (
|
||||
<div key={retrievalIdx} className="rb:text-[#5B6167] rb:text-[12px]">- {retrieval}</div>
|
||||
)) : <div className="rb:text-[#5B6167] rb:text-[12px]">{t('shortTermDetail.noAnswer')}</div>}
|
||||
{lines.length > 4 && (
|
||||
<div
|
||||
className="rb:text-[#155EEF] rb:cursor-pointer rb:mt-1"
|
||||
onClick={() => setLongTermExpandedMap(prev => ({ ...prev, [voIdx]: !prev[voIdx] }))}
|
||||
>
|
||||
{expanded ? t('common.foldUp') : t('common.expanded')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('shortTermDetail.answer')}</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-3 rb:py-2.5 rb:leading-5">
|
||||
<Markdown content={vo.answer} />
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Space>
|
||||
|
||||
<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('shortTermDetail.longTermTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('shortTermDetail.shortTermSubTitle')}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.long_term || data.long_term.length === 0
|
||||
? <Empty />
|
||||
: data.long_term?.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-6 rb:py-3">
|
||||
<div className="rb:mb-1 rb:font-medium rb:leading-5.5">{vo.query}</div>
|
||||
<div className="rb:mt-1 rb:leading-5 rb:text-[#5B6167] rb:text-[12px]">
|
||||
<Markdown content={vo.retrieval} />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
export default ShortTermDetail
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-19 16:54:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:28:00
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:06:29
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { Row, Col, Space } from 'antd';
|
||||
import { Row, Col } from 'antd';
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import WordCloud from '../components/WordCloud'
|
||||
@@ -46,13 +46,19 @@ const StatementDetail = forwardRef<{ handleRefresh: () => void },{ refresh: () =
|
||||
handleRefresh
|
||||
}));
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={12}>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<WordCloud />
|
||||
<EmotionTags />
|
||||
<Health />
|
||||
</Space>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={24}>
|
||||
<WordCloud />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<EmotionTags />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Health />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions ref={suggestionsRef} refresh={refresh} />
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-12 14:42:02
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:10:17
|
||||
*/
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Skeleton, Button, Divider, Tooltip } from 'antd'
|
||||
import { Row, Col, Skeleton, Button, Divider, Tooltip, Flex } from 'antd'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getConversations,
|
||||
@@ -10,25 +17,45 @@ import {
|
||||
getConversationDetail,
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import Empty from '@/components/Empty'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import PageLoading from '@/components/Empty/PageLoading'
|
||||
|
||||
/** A conversation session entry in the sidebar list. */
|
||||
interface Conversation {
|
||||
title: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI-generated insight for a conversation, including key takeaways,
|
||||
* open questions, and an overall summary.
|
||||
*/
|
||||
interface Detail {
|
||||
theme: string;
|
||||
theme_intro: string;
|
||||
/** Core insight summary of the conversation. */
|
||||
summary: string;
|
||||
/** Open questions or pitfalls identified during the conversation. */
|
||||
question: string[];
|
||||
/** Successful experiences / key takeaways extracted from the conversation. */
|
||||
takeaways: string[];
|
||||
/** Information quality score. */
|
||||
info_score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* WorkingDetail – Three-column working-memory view for a user's conversations.
|
||||
*
|
||||
* Left column (360px): scrollable list of conversation sessions.
|
||||
* Centre column (fluid): real-time chat message stream for the selected conversation,
|
||||
* with a refresh button and time-range indicator.
|
||||
* Right column (360px): AI-generated conversation insights – successful experiences
|
||||
* (takeaways), open questions / pitfalls, and a core summary.
|
||||
*
|
||||
* Route param `id` is the end-user ID.
|
||||
*/
|
||||
const WorkingDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
@@ -40,11 +67,13 @@ const WorkingDetail: FC = () => {
|
||||
const [detail, setDetail] = useState<Detail | null>(null)
|
||||
const [selected, setSelected] = useState<Conversation | null>(null)
|
||||
|
||||
/* Fetch conversation list whenever the route user ID changes. */
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
/** Load all conversations for the current user and auto-select the first one. */
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
@@ -61,11 +90,16 @@ const WorkingDetail: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/* Load messages and AI insight whenever the selected conversation changes. */
|
||||
useEffect(() => {
|
||||
if (!id || !selected || !selected.id) return
|
||||
getDetail(selected.id)
|
||||
}, [id, selected])
|
||||
|
||||
/**
|
||||
* Fetch both the chat messages and the AI-generated insight for a conversation.
|
||||
* Both requests run in parallel.
|
||||
*/
|
||||
const getDetail = (conversationId: string) => {
|
||||
if (!id || !conversationId) return
|
||||
|
||||
@@ -88,6 +122,7 @@ const WorkingDetail: FC = () => {
|
||||
setDetailLoading(false)
|
||||
})
|
||||
}
|
||||
/** Derive a human-readable date range (e.g. "2024.01 - 2024.03") from message timestamps. */
|
||||
const timeRange = useMemo(() => {
|
||||
const times = messages.filter(m => m.created_at).map(m => Number(m.created_at))
|
||||
if (times.length === 0) return ''
|
||||
@@ -97,113 +132,126 @@ const WorkingDetail: FC = () => {
|
||||
}, [messages])
|
||||
|
||||
return (
|
||||
<div className="rb:h-[calc(100vh-64px)]! rb:w-full rb:-mx-4 rb:-my-3">
|
||||
<>
|
||||
{loading
|
||||
? <PageLoading />
|
||||
: data.length === 0
|
||||
? <Empty />
|
||||
:(
|
||||
<Row gutter={16} className="rb:h-full">
|
||||
<Col span={5}>
|
||||
<div className="rb:h-full! rb:border-r rb:border-[#EAECEE] rb:py-3 rb:px-4">
|
||||
{data.map(item => (
|
||||
<div key={item.id} className="rb:mb-3">
|
||||
<Tooltip title={item.title}>
|
||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap", {
|
||||
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === selected?.id,
|
||||
<Col flex='360px' className="rb:h-full">
|
||||
<RbCard
|
||||
title={t('workingDetail.conversation')}
|
||||
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)]'
|
||||
className="rb:h-full!"
|
||||
>
|
||||
<Flex gap={8} vertical>
|
||||
{data.map(item => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
gap={12}
|
||||
align="center"
|
||||
className={clsx("rb:cursor-pointer rb:rounded-xl rb:h-12 rb:py-1! rb:px-3! rb:hover:bg-[#F6F6F6]", {
|
||||
'rb:bg-[#171719] rb:hover:bg-[#171719]! rb:text-white': item.id === selected?.id,
|
||||
})}
|
||||
onClick={() => setSelected(item)}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
onClick={() => setSelected(item)}
|
||||
>
|
||||
<div className="rb:size-6 rb:bg-cover rb:bg-[url('@/assets/images/userMemory/chat.svg')]"></div>
|
||||
<Tooltip title={item.title}>
|
||||
<div className="rb:leading-5 rb:wrap-break-word rb:line-clamp-2 rb:flex-1">
|
||||
{item.title}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</Col>
|
||||
{selected && <>
|
||||
<Col span={19}>
|
||||
<div className="rb:text-[18px] rb:font-medium rb:leading-6 rb:mt-4">{selected.title}</div>
|
||||
<div className="rb:mt-1 rb:text-[#5B6167] rb:leading-5">{timeRange}</div>
|
||||
<Col flex="auto" className="rb:h-full">
|
||||
<RbCard
|
||||
title={selected.title}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[42px]! rb:pt-4! rb:font-[MiSans-Bold] rb:font-bold"
|
||||
bodyClassName='rb:p-4! rb:pt-0! rb:h-[calc(100%-42px)]'
|
||||
className="rb:h-full!"
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:leading-4.5 rb:text-[12px]">{timeRange}</div>
|
||||
<Flex justify="space-between" align="center" className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2.5! rb:pr-2.5! rb:pl-3.25! rb:mt-3!">
|
||||
{t('workingDetail.conversationStream')}
|
||||
<Button className="rb:h-6!" onClick={() => getDetail(selected.id)}>{t('workingDetail.refresh')}</Button>
|
||||
</Flex>
|
||||
{messagesLoading
|
||||
? <Skeleton active />
|
||||
: messages.length === 0
|
||||
? <Empty />
|
||||
: (
|
||||
<ChatContent
|
||||
classNames="rb:h-[calc(100%-77px)] rb:pt-5"
|
||||
contentClassNames="rb:max-w-110!"
|
||||
data={messages}
|
||||
streamLoading={false}
|
||||
labelFormat={(item) => formatDateTime(item.created_at)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col flex='360px' className="rb:h-full">
|
||||
<RbCard
|
||||
title={t('workingDetail.successfulTitle')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[50px]! rb:font-[MiSans-Bold] rb:font-bold rb:leading-5.5"
|
||||
bodyClassName='rb:p-4! rb:pt-0! rb:h-[calc(100%-50px)] rb:overflow-y-auto!'
|
||||
className="rb:h-full!"
|
||||
>
|
||||
{detailLoading
|
||||
? <Skeleton active />
|
||||
: detail
|
||||
? <>
|
||||
{detail.takeaways.length > 0
|
||||
? (
|
||||
<ul className="rb:leading-5 rb:list-disc rb:ml-4">
|
||||
{detail.takeaways.map(vo => <li>{vo}</li>)}
|
||||
</ul>
|
||||
)
|
||||
: <Empty size={88} />
|
||||
}
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={t('workingDetail.conversationStream')}
|
||||
extra={<Button className="rb:h-6!" onClick={() => getDetail(selected.id)}>{t('workingDetail.refresh')}</Button>}
|
||||
className="rb:mt-4!"
|
||||
headerClassName='rb:bg-[#F6F8FC]! rb:border-b! rb:border-b-[#DFE4ED]! rb:min-h-11!'
|
||||
headerType="borderless"
|
||||
bodyClassName="rb:h-[calc(100vh-210px)]"
|
||||
>
|
||||
{messagesLoading
|
||||
? <Skeleton active />
|
||||
: messages.length === 0
|
||||
? <Empty />
|
||||
: (
|
||||
<ChatContent
|
||||
classNames="rb:h-[calc(100vh-244px)]"
|
||||
data={messages}
|
||||
streamLoading={false}
|
||||
labelFormat={(item) => formatDateTime(item.created_at)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard className="rb:mt-4!" bodyClassName="rb:h-[calc(100vh-166px)] rb:overflow-y-auto">
|
||||
{detailLoading
|
||||
? <Skeleton active />
|
||||
: detail
|
||||
? <>
|
||||
<>
|
||||
<div className="rb:text-[#369F21] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.successfulTitle')}</div>
|
||||
<>
|
||||
<Divider className="rb:my-4!" />
|
||||
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[16px] rb:leading-5.5 rb:mb-3">{t('workingDetail.question')}</div>
|
||||
|
||||
{detail.takeaways.length > 0
|
||||
? (
|
||||
<ul className="rb:text-[#5B6167] rb:leading-5.5 rb:list-disc rb:ml-4">
|
||||
{detail.takeaways.map(vo => <li>{vo}</li>)}
|
||||
</ul>
|
||||
)
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
|
||||
<>
|
||||
<Divider />
|
||||
<div className="rb:text-[#FF5D34] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.question')}</div>
|
||||
|
||||
{detail.question.length > 0
|
||||
? (
|
||||
<ul className="rb:text-[#5B6167] rb:leading-5.5 rb:list-disc rb:ml-4">
|
||||
{detail.question.map(vo => <li>{vo}</li>)}
|
||||
</ul>
|
||||
)
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
|
||||
<>
|
||||
<Divider />
|
||||
<div className="rb:text-[#369F21] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.summary')}</div>
|
||||
{detail.summary
|
||||
? <RbAlert className="rb:text-[#212332]! rb:text-[14px]! rb:leading-5.5! rb:p-3!">{detail.summary}</RbAlert>
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
{detail.question.length > 0
|
||||
? (
|
||||
<ul className="rb:leading-5 rb:list-disc rb:ml-4">
|
||||
{detail.question.map(vo => <li>{vo}</li>)}
|
||||
</ul>
|
||||
)
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
: <Empty />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<>
|
||||
<Divider className="rb:my-4!" />
|
||||
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[16px] rb:leading-5.5 rb:mb-3">{t('workingDetail.summary')}</div>
|
||||
{detail.summary
|
||||
? <div className="rb:leading-5.5">{detail.summary}</div>
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
</>
|
||||
: <Empty />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</>}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default WorkingDetail
|
||||
10
web/src/views/UserMemoryDetail/pages/index.module.css
Normal file
10
web/src/views/UserMemoryDetail/pages/index.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.progressCustom :global(.ant-progress-inner) {
|
||||
width: 64px !important;
|
||||
height: 64px !important;
|
||||
}
|
||||
.progressCustom :global(.ant-progress-text) {
|
||||
color: #155EEF !important;
|
||||
font-weight: bold !important;
|
||||
font-family: 'MiSans-Bold' !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-07 20:37:34
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:27:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 11:22:20
|
||||
*/
|
||||
import { type FC, useEffect, useState, useMemo, useRef } from 'react'
|
||||
import { type FC, useState, useMemo, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dropdown, Button } from 'antd'
|
||||
import { Dropdown, Button, Flex, Space } from 'antd'
|
||||
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import PageHeader from '@/components/Layout/PageHeader'
|
||||
import StatementDetail from './StatementDetail'
|
||||
import ForgetDetail from './ForgetDetail'
|
||||
import ImplicitDetail from './ImplicitDetail'
|
||||
@@ -18,10 +18,6 @@ import PerceptualDetail from './PerceptualDetail'
|
||||
import EpisodicDetail from './EpisodicDetail'
|
||||
import ExplicitDetail from './ExplicitDetail'
|
||||
import WorkingDetail from './WorkingDetail'
|
||||
import {
|
||||
getEndUserProfile,
|
||||
} from '@/api/memory'
|
||||
import refreshIcon from '@/assets/images/refresh_hover.svg'
|
||||
import GraphDetail from './GraphDetail'
|
||||
|
||||
/**
|
||||
@@ -32,26 +28,11 @@ const Detail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id, type } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState<string>('')
|
||||
// Refs for child components that support imperative refresh
|
||||
const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
const statementDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
const implicitDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// Fetch end user profile to display the user's name in the header
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
getEndUserProfile(id).then((res) => {
|
||||
const response = res as { other_name: string; id: string; }
|
||||
setName(response.other_name || response.id)
|
||||
})
|
||||
}
|
||||
|
||||
// Build dropdown menu items for switching between memory types
|
||||
const items = useMemo(() => {
|
||||
return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGET_MEMORY']
|
||||
@@ -94,29 +75,46 @@ const Detail: FC = () => {
|
||||
if (type === 'GRAPH') {
|
||||
return <GraphDetail />
|
||||
}
|
||||
const handleGoBack = () => {
|
||||
navigate(`/user-memory/neo4j/${id}`, { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<PageHeader
|
||||
name={name}
|
||||
source="node"
|
||||
operation={
|
||||
<PageHeader
|
||||
title={
|
||||
<Dropdown menu={{ items, onClick, selectedKeys: type ? [type] : [] }}>
|
||||
<div className="rb:cursor-pointer rb:group rb:flex rb:items-center rb:gap-1">
|
||||
- {type ? t(`userMemory.${type}`) : ''}
|
||||
<Flex align="center" gap={8} className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[16px] rb:leading-6 rb:cursor-pointer rb:group">
|
||||
{type ? t(`userMemory.${type}`) : ''}
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/up_border.svg')] rb:transform-[rotate(180deg)] rb:group-hover:transform-[rotate(0deg)]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/up_border.svg')] rb:group-hover:transform-[rotate(180deg)]"
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
</Dropdown>
|
||||
}
|
||||
extra={['FORGET_MEMORY', 'EMOTIONAL_MEMORY', 'IMPLICIT_MEMORY'].includes(type as string) &&
|
||||
<Button type="primary" ghost size="small" className="rb:h-6! rb:px-2! rb:leading-5.5!" loading={loading} onClick={handleRefresh}>
|
||||
{!loading && <img src={refreshIcon} className="rb:w-4 rb:h-4" /> }
|
||||
{t('common.refresh')}
|
||||
</Button>}
|
||||
extra={
|
||||
<Space size={12}>
|
||||
{['FORGET_MEMORY', 'EMOTIONAL_MEMORY', 'IMPLICIT_MEMORY'].includes(type as string) &&
|
||||
<Button
|
||||
className="rb:px-2! rb:gap-0.5!"
|
||||
loading={loading}
|
||||
icon={<div className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh_dark.svg')]"></div>}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
className="rb:px-2! rb:gap-0.5!"
|
||||
icon={<div className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/return.svg')]"></div>}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
{t('common.return')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:p-3">
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail ref={statementDetailRef} refresh={handleRefresh} />}
|
||||
{type === 'FORGET_MEMORY' && <ForgetDetail ref={forgetDetailRef} />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail ref={implicitDetailRef} refresh={handleRefresh} />}
|
||||
|
||||
Reference in New Issue
Block a user