feat(web): ui upgrade

This commit is contained in:
zhaoying
2026-04-01 16:43:45 +08:00
parent e77a1a92fd
commit ad4ddea977
13 changed files with 590 additions and 219 deletions

View File

@@ -205,35 +205,33 @@ const OrderHistory: React.FC = () => {
];
return (
<div className="rb:h-[calc(100vh-80px)] rb:overflow-hidden">
<Flex justify="space-between" className="rb:mb-4!">
<Space size={10}>
<Select
defaultValue={query.status}
placeholder={t('common.select')}
options={statusOptions}
className="rb:w-30"
onChange={handleChangeStatus}
/>
<Select
defaultValue={query.product_type}
placeholder={t('common.select')}
options={productTypeOptions}
className="rb:w-30"
onChange={handleChangeType}
/>
<Select
defaultValue={timeType}
placeholder={t('common.select')}
options={timeOptions}
className="rb:w-30"
onChange={handleChangeTime}
/>
</Space>
<div className="rb:h-full rb:overflow-hidden rb:bg-white rb:rounded-lg rb:pt-3 rb:px-3">
<Flex className="rb:mb-3!" gap={10}>
<Select
defaultValue={query.status}
placeholder={t('common.select')}
options={statusOptions}
className="rb:w-30"
onChange={handleChangeStatus}
/>
<Select
defaultValue={query.product_type}
placeholder={t('common.select')}
options={productTypeOptions}
className="rb:w-30"
onChange={handleChangeType}
/>
<Select
defaultValue={timeType}
placeholder={t('common.select')}
options={timeOptions}
className="rb:w-30"
onChange={handleChangeTime}
/>
<SearchInput
placeholder={t('pricing.searchPlaceholder')}
onSearch={(value) => setQuery(prev => ({ ...prev, search: value }))}
className="rb:w-70"
variant="outlined"
/>
</Flex>
<Table

View File

@@ -11,19 +11,20 @@
*/
import React, { useState, useEffect } from 'react';
import { Row, Col, Form, App, Button, Space, Select, Flex } from 'antd';
import { Row, Col, Form, App, Button, Space, Select, Flex, Divider } from 'antd';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import RbCard from '@/components/RbCard/Card';
import { getMemoryReflectionConfig, updateMemoryReflectionConfig, pilotRunMemoryReflectionConfig } from '@/api/memory'
import type { ConfigForm, Result, ReflexionData, MemoryVerify, QualityAssessment } from './types'
import Tag from '@/components/Tag'
import type { ConfigForm, Result, ReflexionData } from './types'
import { useI18n } from '@/store/locale';
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
import LabelWrapper from '@/components/FormItem/LabelWrapper'
import DescWrapper from '@/components/FormItem/DescWrapper'
import ModelSelect from '@/components/ModelSelect';
import BtnTabs from '@/components/BtnTabs'
/** Configuration list */
const configList = [
@@ -91,6 +92,8 @@ const SelfReflectionEngine: React.FC = () => {
const { message } = App.useApp();
const [loading, setLoading] = useState(false)
const [runLoading, setRunLoading] = useState(false)
const [activeTabMap, setActiveTabMap] = useState<Record<number, string>>({});
const [expanded, setExpanded] = useState({ conflict: true, quality: true, privacy: true });
const [result, setResult] = useState<Result | null>(null)
const { language } = useI18n()
@@ -158,6 +161,8 @@ const SelfReflectionEngine: React.FC = () => {
})
.then((res) => {
setResult(res as Result)
setExpanded({ conflict: true, quality: true, privacy: true })
setActiveTabMap({})
})
.finally(() => {
setRunLoading(false)
@@ -174,8 +179,8 @@ const SelfReflectionEngine: React.FC = () => {
<RbCard
title={t('reflectionEngine.reflectionEngineConfig')}
extra={<Space>
<Button block onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" loading={loading} block onClick={handleSave}>{t('common.save')}</Button>
<Button onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" loading={loading} onClick={handleSave}>{t('common.save')}</Button>
</Space>}
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
@@ -253,100 +258,110 @@ const SelfReflectionEngine: React.FC = () => {
</RbCard>
</Col>
<Col span={12} className="rb:h-full!">
<Flex gap={16} vertical className="rb:h-full!">
<RbCard
title={t('memoryExtractionEngine.example')}
>
<div className="rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mb-6">
<RbCard
title={t('memoryExtractionEngine.example')}
extra={<Space>
<Button type="primary" loading={runLoading} disabled={!values?.reflection_enabled} onClick={handleRun}>{t('reflectionEngine.run')}</Button>
</Space>}
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
className="rb:h-full!"
bodyClassName="rb:h-[calc(100%-54px)] rb:overflow-y-auto! rb:p-4! rb:pt-0!"
>
<Flex vertical gap={12}>
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:py-2.5 rb:px-3 rb:leading-5.5">
{t('reflectionEngine.exampleText')}
</div>
<Button type="primary" block loading={runLoading} disabled={!values?.reflection_enabled} onClick={handleRun}>{t('reflectionEngine.run')}</Button>
</RbCard>
{result && <>
<RbCard
title={t('reflectionEngine.runTitle')}
>
<div
className="rb:flex rb:gap-4 rb:justify-start rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.baseline`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{result.baseline}
</div>
</div>
</RbCard>
{result.reflexion_data.length > 0 && (
<RbCard
title={t('reflectionEngine.conflictDetection')}
>
<Space size={12} direction="vertical" className="rb:w-full">
{result.reflexion_data.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['reason', 'solution'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{item[key as keyof ReflexionData]}
</div>
</div>
))}
{result && <>
<Flex justify="space-between" className="rb:bg-[#F6F6F6] rb:rounded-xl rb:py-2.5! rb:px-3! rb:leading-5">
<span className="rb:font-medium rb:text-[#212332]">{t('reflectionEngine.runTitle')}</span>
<span className="rb:text-[#5B6167]">{t(`reflectionEngine.baseline`)}: {t(`reflectionEngine.${result.baseline}`)}</span>
</Flex>
{result.reflexion_data.length > 0 &&
<Flex vertical gap={12} className="rb:bg-[#F6F6F6] rb:rounded-xl rb:py-2.5! rb:px-3! rb:leading-5.5">
<Flex justify="space-between" className="rb:font-medium rb:text-[#212332] rb:cursor-pointer" onClick={() => setExpanded(p => ({ ...p, conflict: !p.conflict }))}>
{t('reflectionEngine.conflictDetection')}
<div className={clsx("rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')] rb:transition-transform", {
'rb:rotate-180': !expanded.conflict,
})}></div>
</Flex>
{expanded.conflict && result.reflexion_data.map((item, index) => (
<div key={index} className="rb:bg-white rb:rounded-xl rb:py-2.5! rb:px-3!">
<BtnTabs
className="rb:mb-3!"
variant="outline"
activeKey={activeTabMap[index] ?? 'reason'}
items={['reason', 'solution'].map(key => ({
label: t(`reflectionEngine.${key}`),
key
}))}
onChange={(key) => setActiveTabMap(prev => ({ ...prev, [index]: key }))}
/>
<div className="rb:leading-5.5">{item[(activeTabMap[index] ?? 'reason') as keyof ReflexionData]}</div>
</div>
))}
</Space>
</RbCard>
)}
{result.quality_assessments.length > 0 && (
<RbCard
title={t('reflectionEngine.qualityAssessment')}
>
{result.quality_assessments.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['score', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.qualityAssessmentObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{item[key as keyof QualityAssessment]}
</div>
</Flex>
}
{result.quality_assessments.length > 0 &&
<Flex vertical gap={12} className="rb:bg-[#F6F6F6] rb:rounded-xl rb:py-2.5! rb:px-3! rb:leading-5.5">
<Flex justify="space-between" className="rb:font-medium rb:text-[#212332] rb:cursor-pointer" onClick={() => setExpanded(p => ({ ...p, quality: !p.quality }))}>
{t('reflectionEngine.qualityAssessment')}
<div className={clsx("rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')] rb:transition-transform", {
'rb:rotate-180': !expanded.quality,
})}></div>
</Flex>
{expanded.quality && result.quality_assessments.map((item, index) => (
<div key={index} className="rb:bg-white rb:rounded-xl rb:py-2.5! rb:px-3!">
<div>
<span className="rb:font-medium rb:text-[#212332] rb:leading-5 rb:mr-4.5">{t(`reflectionEngine.qualityAssessmentObj.score`)}</span>
<span className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[#155EEF] rb:leading-5">{item.score}</span>
</div>
))}
</div>
))}
</RbCard>
)}
{result.memory_verifies.length > 0 && (
<RbCard
title={t('reflectionEngine.privacyAudit')}
>
{result.memory_verifies.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['has_privacy', 'privacy_types', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.privacyAuditObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{key === 'has_privacy'
? <Tag color={item[key as keyof MemoryVerify] ? 'success' : 'error'}>{t(`reflectionEngine.privacyAuditObj.${item[key as keyof MemoryVerify]}`)}</Tag>
: key === 'privacy_types' ? (item[key as keyof MemoryVerify] as string[]).join('、')
: item[key as keyof MemoryVerify]
}
</div>
<Divider className="rb:my-3!" />
<div className="rb:font-medium rb:text-[#212332] rb:leading-5 rb:mb-2">{t(`reflectionEngine.qualityAssessmentObj.summary`)}</div>
<div className="rb:mt-1 rb:leading-5.5">{item.summary}</div>
</div>
))}
</Flex>
}
{result.memory_verifies.length > 0 &&
<Flex vertical gap={12} className="rb:bg-[#F6F6F6] rb:rounded-xl rb:py-2.5! rb:px-3! rb:leading-5.5">
<Flex justify="space-between" className="rb:font-medium rb:text-[#212332] rb:cursor-pointer" onClick={() => setExpanded(p => ({ ...p, privacy: !p.privacy }))}>
{t('reflectionEngine.privacyAudit')}
<div className={clsx("rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')] rb:transition-transform", {
'rb:rotate-180': !expanded.privacy,
})}></div>
</Flex>
{expanded.privacy && result.memory_verifies.map((item, index) => (
<div key={index} className="rb:bg-white rb:rounded-xl rb:py-2.5! rb:px-3!">
<div>
<span className="rb:font-medium rb:text-[#212332] rb:leading-5 rb:mr-4.5">{t(`reflectionEngine.privacyAuditObj.has_privacy`)}</span>
<span className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[#155EEF] rb:leading-5">{item.has_privacy}</span>
</div>
))}
</div>
))}
</RbCard>
)}
</>}
</Flex>
<Divider className="rb:my-3!" />
<div className="rb:font-medium rb:text-[#212332] rb:leading-5 rb:mb-2">{t(`reflectionEngine.privacyAuditObj.privacy_types`)}</div>
<div className="rb:mt-1 rb:leading-5.5">{item.privacy_types.join(', ')}</div>
<Divider className="rb:my-3!" />
<div className="rb:font-medium rb:text-[#212332] rb:leading-5 rb:mb-2">{t(`reflectionEngine.privacyAuditObj.summary`)}</div>
<div className="rb:mt-1 rb:leading-5.5">{item.summary}</div>
</div>
))}
</Flex>
}
</>}
</Flex>
</RbCard>
</Col>
</Row>
);

View File

@@ -7,7 +7,8 @@
import { type FC, useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Skeleton, Row, Col, Flex } from 'antd'
import { Skeleton, Row, Col, Flex, DatePicker, Pagination } from 'antd'
import type { Dayjs } from 'dayjs'
import * as echarts from 'echarts'
import 'echarts-wordcloud'
@@ -41,6 +42,7 @@ export interface SemanticMemory {
/** Combined API response containing both memory categories. */
interface Data {
total: number;
episodic_memories: EpisodicMemory[];
semantic_memories: SemanticMemory[]
}
@@ -71,7 +73,19 @@ const ExplicitDetail: FC = () => {
/** 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: [] })
const [data, setData] = useState<Data>({ episodic_memories: [], semantic_memories: [], total: 0 })
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null] | null>(null)
const [page, setPage] = useState(1)
const PAGE_SIZE = 10
const filteredEpisodic = dateRange?.[0] && dateRange?.[1]
? data.episodic_memories.filter(item => {
const ts = item.created_at
return ts >= dateRange[0]!.startOf('day').valueOf() && ts <= dateRange[1]!.endOf('day').valueOf()
})
: data.episodic_memories
const pagedEpisodic = filteredEpisodic.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
/* Fetch data whenever the route user ID changes. */
useEffect(() => {
@@ -131,35 +145,77 @@ const ExplicitDetail: FC = () => {
})
return () => { chartInstance.current?.dispose(); chartInstance.current = null }
}, [data.semantic_memories])
/* Redraw the word cloud when the container dimensions change. */
useEffect(() => {
const target = wordCloudRef.current?.parentElement
if (!target) return
const observer = new ResizeObserver(() => {
if (!chartInstance.current) return
chartInstance.current.resize()
chartInstance.current.setOption({ series: [{ type: 'wordCloud' }] })
})
observer.observe(target)
return () => {
observer.disconnect()
chartInstance.current?.dispose();
chartInstance.current = null
}
}, [])
return (
<Row gutter={12} className="rb:h-full!">
<Col span={12} className="rb:h-full!">
<RbCard
title={t('explicitDetail.episodic_memories')}
title={() => <span className="rb:font-[MiSans-Bold] rb:font-bold">{t('explicitDetail.episodic_memories')}</span>}
extra={<span className="rb:text-[#5B6167]">{t('table.totalRecords', { total: data.total })}</span>}
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!"
headerClassName="rb:min-h-[50px]!"
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-50px)]"
className="rb:h-full!"
>
{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 gap={12} vertical className="rb:h-full!">
<Row gutter={12}>
<Col span={12}>
<DatePicker.RangePicker
value={dateRange}
onChange={(val) => { setDateRange(val); setPage(1) }}
allowClear
/>
</Col>
</Row>
<div className="rb:max-h-[calc(100%-92px)] rb:overflow-y-auto">
{pagedEpisodic.length > 0 ? pagedEpisodic.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:text-[#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>
)) : <Empty />}
</div>
{filteredEpisodic.length > PAGE_SIZE && (
<Pagination
current={page}
pageSize={PAGE_SIZE}
total={filteredEpisodic.length}
onChange={setPage}
size="small"
showSizeChanger={true}
showQuickJumper={true}
className="rb:mt-1!"
/>
)}
</Flex>
) : <Empty />
)
}
</RbCard>
</Col>

View File

@@ -197,7 +197,7 @@ const WorkingDetail: FC = () => {
</RbCard>
</Col>
{selected && <>
<Col flex="auto" className="rb:h-full!">
<Col flex="1" className="rb:h-full!">
<RbCard
title={selected.title}
headerType="borderless"