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

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 35</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="多模态对话-3" transform="translate(-1228, -338)" stroke="#171719" stroke-width="1.2">
<g id="编组-34备份" transform="translate(540, 320)">
<g id="编组-33" transform="translate(683, 13)">
<g id="编组-35" transform="translate(5, 5)">
<g id="编组-63" transform="translate(3, 3)">
<polyline id="路径" points="10 4 6 4 6 0"></polyline>
<polyline id="路径备份-3" transform="translate(2, 8) scale(-1, -1) translate(-2, -8)" points="4 10 0 10 0 6"></polyline>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 36</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="多模态对话-3" transform="translate(-763, -266)" stroke="#171719" stroke-width="1.2">
<g id="编组-34" transform="translate(540, 248)">
<g id="编组-33" transform="translate(218, 13)">
<g id="编组-36" transform="translate(5, 5)">
<path d="M9.02271937,3 L13,3 L13,7.07419434 M6.96571644,13 L3,13 L3,9.04391117" id="形状"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 877 B

View File

@@ -24,10 +24,11 @@ interface BtnTabsProps {
onChange: (key: string) => void; onChange: (key: string) => void;
/** Optional extra class name for the container */ /** Optional extra class name for the container */
className?: string; className?: string;
variant?: 'outline' | 'borderless'
} }
/** Button-style tab switcher — renders tabs as pill-shaped buttons with active highlight */ /** Button-style tab switcher — renders tabs as pill-shaped buttons with active highlight */
const BtnTabs: FC<BtnTabsProps> = ({ items, activeKey, onChange, className }) => { const BtnTabs: FC<BtnTabsProps> = ({ items, activeKey, onChange, className, variant = 'borderless' }) => {
return ( return (
<Flex align="center" gap={8} className={className || ''}> <Flex align="center" gap={8} className={className || ''}>
{items.map((tab) => ( {items.map((tab) => (
@@ -35,8 +36,9 @@ const BtnTabs: FC<BtnTabsProps> = ({ items, activeKey, onChange, className }) =>
key={tab.key} key={tab.key}
onClick={() => onChange(tab.key)} onClick={() => onChange(tab.key)}
className={clsx('rb:px-2 rb:py-1 rb:rounded-[13px] rb:text-[12px] rb:leading-4.5 rb:cursor-pointer', { className={clsx('rb:px-2 rb:py-1 rb:rounded-[13px] rb:text-[12px] rb:leading-4.5 rb:cursor-pointer', {
'rb:bg-[#F6F6F6]': activeKey !== tab.key, 'rb:bg-[#F6F6F6]': activeKey !== tab.key && variant === 'borderless',
'rb:bg-[#171719] rb:text-white': activeKey === tab.key, 'rb-border rb:bg-white': activeKey !== tab.key && variant === 'outline',
'rb:bg-[#171719] rb:text-white rb:border-[#171719]': activeKey === tab.key,
})} })}
> >
{tab.label} {tab.label}

View File

@@ -0,0 +1,152 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-16 15:00:07
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-27 15:23:14
*/
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-border rb:rounded-xl rb:py-2 rb:px-2.5 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:size-5 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]" />
<div className="rb:flex-1">
<div className="rb:font-medium rb:leading-5 rb:text-[14px] rb:wrap-break-word rb:line-clamp-1">{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-5 rb:bg-cover", {
"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-5 rb:bg-[url('@/assets/images/common/more.svg')] rb:hover:bg-[url('@/assets/images/common/more_hover.svg')]"></div>
</Dropdown>
</Flex>
</Flex>
<Flex align="center" gap={8} className="rb:mt-3!">
<Slider
min={0}
max={duration || 0}
step={0.1}
value={current}
onChange={handleSeek}
tooltip={{ formatter: null }}
className="rb:flex-1 rb:m-0!"
styles={{ track: { background: '#171719' }, rail: { background: '#E4E4E4' }, handle: { display: 'none' } }}
/>
<span className="rb:text-[12px] rb:leading-4.5 rb:text-[#5B6167] rb:whitespace-nowrap">{fmt(current)} / {fmt(duration)}</span>
</Flex>
</div>
)
}
export default AudioPlayer

View File

@@ -8,9 +8,12 @@ import { type FC, useRef, useEffect, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import Markdown from '@/components/Markdown' import Markdown from '@/components/Markdown'
import type { ChatContentProps } from './types' import type { ChatContentProps } from './types'
import { Spin, Divider, Space, Image, Flex, Button } from 'antd' import { Spin, Image, Flex, Button } from 'antd'
import { SoundOutlined } from '@ant-design/icons' import { SoundOutlined } from '@ant-design/icons'
import { t } from 'i18next' import { useTranslation } from 'react-i18next'
import AudioPlayer from './AudioPlayer'
import VideoPlayer from './VideoPlayer'
const getFileUrl = (file: any) => { const getFileUrl = (file: any) => {
return file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined) return file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined)
@@ -32,6 +35,7 @@ const ChatContent: FC<ChatContentProps> = ({
renderRuntime, renderRuntime,
onSend onSend
}) => { }) => {
const { t } = useTranslation()
// Scroll container reference for controlling auto-scroll to bottom // Scroll container reference for controlling auto-scroll to bottom
const scrollContainerRef = useRef<(HTMLDivElement | null)>(null) const scrollContainerRef = useRef<(HTMLDivElement | null)>(null)
const prevDataLengthRef = useRef(data.length); const prevDataLengthRef = useRef(data.length);
@@ -151,65 +155,101 @@ const ChatContent: FC<ChatContentProps> = ({
} }
if (file.type.includes('video')) { if (file.type.includes('video')) {
return ( return (
<div key={file.url || file.uid} className="rb:inline-block rb:group rb:relative rb:rounded-lg"> <div key={file.url || file.uid} className="rb:w-50">
<video src={getFileUrl(file)} controls className="rb:max-w-80 rb:rounded-lg rb:object-cover rb:cursor-pointer" /> {/* <video src={getFileUrl(file)} controls className="rb:max-w-80 rb:rounded-lg rb:object-cover rb:cursor-pointer" /> */}
<VideoPlayer key={file.url || file.uid} src={getFileUrl(file)} />
</div> </div>
) )
} }
if (file.type.includes('audio')) { if (file.type.includes('audio')) {
return ( return (
<div key={file.url || file.uid} className="rb:inline-flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5 rb:gap-2"> <div key={file.url || file.uid} className="rb:w-50">
<audio src={getFileUrl(file)} controls className="rb:max-w-80" /> <AudioPlayer key={file.url || file.uid} src={getFileUrl(file)} />
</div> </div>
) )
} }
return ( return (
<div key={file.url || file.uid} className="rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:p-1! rb:cursor-pointer" onClick={() => handleDownload(file)}> <Flex
{(file.type.includes('excel') || file.type.includes('spreadsheetml.sheet') || file.type.includes('csv')) key={file.url || file.uid}
? <div align="center"
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/excel.svg')]" gap={10}
></div> className="rb:text-left rb:w-45 rb:text-[12px] rb:group rb:relative rb:rounded-lg rb-border rb:py-2! rb:px-2.5! rb:border rb:border-[#F6F6F6]"
:(file.type.includes('pdf')) onClick={() => handleDownload(file)}
? <div >
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf.svg')]" <div
></div> className={clsx(
: (file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document')) "rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf_disabled.svg')]",
? <div file.type?.includes('pdf')
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/word.svg')]" ? "rb:bg-[url('@/assets/images/file/pdf.svg')]"
></div> : (file.type?.includes('excel') || file.type?.includes('spreadsheetml.sheet'))
: null ? "rb:bg-[url('@/assets/images/file/excel.svg')]"
} : file.type?.includes('csv')
</div> ? "rb:bg-[url('@/assets/images/file/csv.svg')]"
: file.type?.includes('html')
? "rb:bg-[url('@/assets/images/file/html.svg')]"
: file.type?.includes('json')
? "rb:bg-[url('@/assets/images/file/json.svg')]"
: file.type?.includes('ppt')
? "rb:bg-[url('@/assets/images/file/ppt.svg')]"
: file.type?.includes('text')
? "rb:bg-[url('@/assets/images/file/txt.svg')]"
: file.type?.includes('markdown')
? "rb:bg-[url('@/assets/images/file/md.svg')]"
: (file.type?.includes('doc') || file.type?.includes('docx') || file.type?.includes('word') || file.type?.includes('wordprocessingml.document'))
? "rb:bg-[url('@/assets/images/file/word.svg')]"
: null
)}
></div>
<div className="rb:flex-1 rb:w-32.5">
<div className="rb:leading-4 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{file.name}</div>
<div className="rb:leading-3.5 rb:mt-0.5 rb:text-[#5B6167] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{file.type?.split('/')[file.type?.split('/').length - 1]} · {file.size}</div>
</div>
</Flex>
) )
})} })}
</Flex>} </Flex>}
{/* Message bubble */} {/* Message bubble */}
<div className={clsx('rb:text-left rb:rounded-lg rb:leading-5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-130 rb:wrap-break-word rb:relative', contentClassNames, { <div className={clsx('rb:text-left rb:leading-5 rb:inline-block rb:wrap-break-word rb:relative', item.role === 'user' ? contentClassNames : '', {
// Error message style (content is null and not assistant message) // Error message style (content is null and not assistant message)
'rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': (item.status && item.status !== 'completed') || (errorDesc && item.role === 'assistant' && item.content === null && !renderRuntime), 'rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': (item.status && item.status !== 'completed') || (errorDesc && item.role === 'assistant' && item.content === null && !renderRuntime),
// Assistant message style // Assistant message style
'rb:bg-[#E3EBFD]': item.role === 'user', 'rb:bg-[#E3EBFD] rb:p-[10px_12px_2px_12px] rb:rounded-lg rb:max-w-130': item.role === 'user',
'rb:max-w-full': item.role === 'assistant',
// User message style // User message style
'rb:bg-[#F6F6F6] rb:text-[#212332]': item.role === 'assistant' && (item.content || item.content === '' || typeof renderRuntime === 'function'), 'rb:text-[#212332]': item.role === 'assistant' && (item.content || item.content === '' || typeof renderRuntime === 'function'),
'rb:mt-1.5': labelPosition === 'top', 'rb:mt-1': labelPosition === 'top',
'rb:mb-1.5': labelPosition === 'bottom', 'rb:mb-1': labelPosition === 'bottom',
})}> })}>
{item.meta_data?.reasoning_content && <div className="rb:mb-2 rb:border rb:rounded-md rb:px-3 rb:pt-2 rb:bg-white rb:text-[12px]"> {item.meta_data?.reasoning_content &&
<Flex <div className={clsx("rb:mb-4 rb-border rb:rounded-xl rb:px-4 rb:pt-4 rb:bg-white", {
align="center" 'rb:hover:bg-[#F6F6F6] rb:w-64': !isReasoningExpanded(index)
justify="space-between" })}>
className="rb:text-[#5B6167] rb:font-medium rb:cursor-pointer rb:pb-2!" <Flex
onClick={() => toggleReasoning(index)} align="center"
> justify="space-between"
<span>{t('memoryConversation.reasoning_content')}</span> className="rb:font-medium rb:pb-4!"
<div >
className={clsx("rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')]", { <span>{t('memoryConversation.reasoning_content')}</span>
'rb:rotate-180': !isReasoningExpanded(index), <Flex
})} align="center"
></div> justify="center"
</Flex> className={clsx("rb:size-6.5 rb:cursor-pointer rb-border rb:rounded-lg", {
{isReasoningExpanded(index) && <Markdown content={item.meta_data.reasoning_content} />} 'rb:hover:bg-[#F6F6F6]!': isReasoningExpanded(index)
</div>} })}
onClick={() => toggleReasoning(index)}
>
<div
className={clsx("rb:size-4 rb:bg-cover", {
'rb:bg-[url("@/assets/images/conversation/compress.svg")]': isReasoningExpanded(index),
'rb:bg-[url("@/assets/images/conversation/expand.svg")]': !isReasoningExpanded(index)
})}
></div>
</Flex>
</Flex>
{isReasoningExpanded(index) && <Markdown content={item.meta_data.reasoning_content} className="rb:text-[#5B6167] rb:text-[12px]" />}
</div>
}
{item.status && <div className="rb:size-5 rb:bg-cover rb:bg-[url('@/assets/images/conversation/exclamation_circle.svg')] rb:absolute rb:-left-7"></div>} {item.status && <div className="rb:size-5 rb:bg-cover rb:bg-[url('@/assets/images/conversation/exclamation_circle.svg')] rb:absolute rb:-left-7"></div>}
{item.subContent && renderRuntime && renderRuntime(item, index)} {item.subContent && renderRuntime && renderRuntime(item, index)}
{/* Render message content using Markdown component */} {/* Render message content using Markdown component */}
@@ -222,44 +262,42 @@ const ChatContent: FC<ChatContentProps> = ({
>{question}</Button> >{question}</Button>
))} ))}
</Flex>} </Flex>}
{item.meta_data?.citations && item.meta_data?.citations.length > 0 && <div className="rb:mt-2 rb:pt-2 rb:border-t rb:border-[#E3EBFD]"> {item.meta_data?.citations && item.meta_data?.citations.length > 0 &&
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-medium">{t('memoryConversation.citations')}</div> <Flex vertical gap={4} className="rb:mt-1! rb:pt-3! rb-border-t rb:mb-2!">
{item.meta_data?.citations?.map((citation, idx) => ( <div className="rb:font-medium">{t('memoryConversation.citations')}</div>
<Button {item.meta_data?.citations?.map((citation, idx) => (
type="link" <div
key={idx} key={idx}
size="small" className="rb:text-[#155EEF] rb:leading-5 rb:underline rb:cursor-pointer"
className="rb:text-[12px]!" onClick={() => {
onClick={() => { const params = new URLSearchParams({ documentId: citation.document_id, parentId: citation.knowledge_id });
const params = new URLSearchParams({ documentId: citation.document_id, parentId: citation.knowledge_id }); window.open(`/#/knowledge-base/${citation.knowledge_id}/DocumentDetails?${params}`, '_blank');
window.open(`/#/knowledge-base/${citation.knowledge_id}/DocumentDetails?${params}`, '_blank'); }}
}} >{citation.file_name}</div>
>{citation.file_name}</Button> ))}
))} </Flex>
</div>} }
</div>
{/* Bottom label (such as timestamp, username, etc.) */}
{labelPosition === 'bottom' && <Flex gap={16} align="center" justify={item.role === 'user' ? 'end' : 'start'}>
{item.meta_data?.audio_url && <> {item.meta_data?.audio_url && <>
<Divider className="rb:my-3!" /> {playingIndex !== item.meta_data?.audio_url && item.meta_data?.audio_status === 'pending'
<Space size={12} className="rb:pb-2 rb:pl-1"> ? <Spin />
{playingIndex !== item.meta_data?.audio_url && item.meta_data?.audio_status === 'pending' : playingIndex !== item.meta_data?.audio_url
? <Spin />
: playingIndex !== item.meta_data?.audio_url
? <SoundOutlined className={clsx("rb:cursor-pointer rb:size-5.5", { ? <SoundOutlined className={clsx("rb:cursor-pointer rb:size-5.5", {
'rb:text-[#FF5D34]': item.meta_data?.audio_status === 'error', 'rb:text-[#FF5D34]': item.meta_data?.audio_status === 'error',
'rb:hover:text-[#155EEF]!': !item.meta_data?.audio_status || !['pending', 'error'].includes(item.meta_data?.audio_status) 'rb:hover:text-[#155EEF]!': !item.meta_data?.audio_status || !['pending', 'error'].includes(item.meta_data?.audio_status)
})} onClick={() => handlePlay(item.meta_data?.audio_url!, item.meta_data?.audio_status)} /> })} onClick={() => handlePlay(item.meta_data?.audio_url!, item.meta_data?.audio_status)} />
: <div : <div
className="rb:size-5.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]" className="rb:size-5.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]"
onClick={() => handlePlay(item.meta_data?.audio_url!, item.meta_data?.audio_status)} onClick={() => handlePlay(item.meta_data?.audio_url!, item.meta_data?.audio_status)}
/> />
} }
</Space>
</>} </>}
</div>
{/* Bottom label (such as timestamp, username, etc.) */}
{labelPosition === 'bottom' &&
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular"> <div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular">
{labelFormat(item)} {labelFormat(item)}
</div> </div>
</Flex>
} }
</> </>
} }

View File

@@ -0,0 +1,62 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-24 12:21:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-24 12:21:56
*/
import { type FC, useRef, useState } from 'react'
import { CloseOutlined } from '@ant-design/icons'
interface VideoPlayerProps {
src: string
}
const VideoPlayer: FC<VideoPlayerProps> = ({ src }) => {
const [open, setOpen] = useState(false)
const videoRef = useRef<HTMLVideoElement>(null)
const handleOpen = () => setOpen(true)
const handleClose = () => {
videoRef.current?.pause()
setOpen(false)
}
return (
<>
{/* Thumbnail with play overlay */}
<div
className="rb:relative rb:w-full rb:h-full rb:rounded-xl rb:overflow-hidden rb:cursor-pointer rb:group"
onClick={handleOpen}
>
<video src={src} className="rb:w-full rb:h-full rb:object-cover" preload="metadata" />
<div className="rb:absolute rb:inset-0 rb:bg-black/20 rb:flex rb:items-center rb:justify-center rb:transition-colors group-hover:rb:bg-black/30">
<div className="rb:size-10 rb:rounded-full rb:bg-white/80 rb:flex rb:items-center rb:justify-center">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M5 3.5L14.5 9L5 14.5V3.5Z" fill="#171719" />
</svg>
</div>
</div>
</div>
{/* Fullscreen modal */}
{open && (
<div
className="rb:fixed rb:inset-0 rb:z-1000 rb:bg-black/80 rb:flex rb:items-center rb:justify-center"
onClick={handleClose}
>
<button className="ant-image-preview-close"><CloseOutlined /></button>
<video
ref={videoRef}
src={src}
controls
autoPlay
className="rb:max-w-[90vw] rb:max-h-[90vh] rb:rounded-xl"
onClick={e => e.stopPropagation()}
/>
</div>
)}
</>
)
}
export default VideoPlayer

View File

@@ -62,7 +62,7 @@ const components = {
ul: ({ children, ...props }: any) => <ul className="rb:list-disc rb:ml-6 rb:mb-2" {...props}>{children}</ul>, ul: ({ children, ...props }: any) => <ul className="rb:list-disc rb:ml-6 rb:mb-2" {...props}>{children}</ul>,
ol: ({ children, ...props }: any) => <ol className="rb:list-decimal rb:ml-6 rb:mb-2" {...props}>{children}</ol>, ol: ({ children, ...props }: any) => <ol className="rb:list-decimal rb:ml-6 rb:mb-2" {...props}>{children}</ol>,
li: ({ children, ...props }: any) => <li className="rb:mb-1" {...props}>{children}</li>, li: ({ children, ...props }: any) => <li className="rb:mb-1" {...props}>{children}</li>,
blockquote: ({ children, ...props }: any) => <blockquote className="rb:border-l-4 rb:border-[#D9D9D9] rb:pl-4 rb:mb-2" {...props}>{children}</blockquote>, blockquote: ({ children, ...props }: any) => <blockquote className="rb:bg-[#F6F6F6] rb:rounded-lg rb:pt-2.5 rb:pb-0.5 rb:px-3 rb:mb-3 rb:mt-1" {...props}>{children}</blockquote>,
p: ({ children, ...props }: any) => <p className="rb:mb-2" {...props}>{children}</p>, p: ({ children, ...props }: any) => <p className="rb:mb-2" {...props}>{children}</p>,
strong: ({ children, ...props }: any) => <strong className="rb:font-bold" {...props}>{children}</strong>, strong: ({ children, ...props }: any) => <strong className="rb:font-bold" {...props}>{children}</strong>,
em: ({ children, ...props }: any) => <em className="rb:italic" {...props}>{children}</em>, em: ({ children, ...props }: any) => <em className="rb:italic" {...props}>{children}</em>,
@@ -81,10 +81,10 @@ const components = {
audio: ({ src, ...props }: any) => <AudioBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />, audio: ({ src, ...props }: any) => <AudioBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
a: ({ href, children, ...props }: any) => <Link href={href || '#'} {...props}>{children}</Link>, a: ({ href, children, ...props }: any) => <Link href={href || '#'} {...props}>{children}</Link>,
button: ({ children }: any) => <RbButton node={{ children }}>{[children]}</RbButton>, button: ({ children }: any) => <RbButton node={{ children }}>{[children]}</RbButton>,
table: ({ children, ...props }: any) => <div className="rb:overflow-x-auto rb:max-w-full"><table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table></div>, table: ({ children, ...props }: any) => <div className="rb:overflow-x-auto rb:max-w-full"><table className="rb:border rb:border-[#EBEBEB] rb:mb-2" {...props}>{children}</table></div>,
tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#D9D9D9]" {...props}>{children}</tr>, tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#EBEBEB]" {...props}>{children}</tr>,
th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>, th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#EBEBEB] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>,
td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>, td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#EBEBEB] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>,
input: ({ children, ...props }: any) => { input: ({ children, ...props }: any) => {
switch (props.type) { switch (props.type) {
case 'color': case 'color':
@@ -122,6 +122,7 @@ const components = {
select: ({ children, ...props }: any) => <Select style={{width: '100%'}} {...props}>{children}</Select>, select: ({ children, ...props }: any) => <Select style={{width: '100%'}} {...props}>{children}</Select>,
textarea: ({ children, ...props }: any) => <Input.TextArea {...props}>{children}</Input.TextArea>, textarea: ({ children, ...props }: any) => <Input.TextArea {...props}>{children}</Input.TextArea>,
form: ({ children, ...props }: any) => <Form {...props}>{children}</Form>, form: ({ children, ...props }: any) => <Form {...props}>{children}</Form>,
hr: (props: any) => <hr className="rb:border-t rb:border-[#EBEBEB] rb:my-3" {...props} />
} }
const RbMarkdown: FC<RbMarkdownProps> = ({ const RbMarkdown: FC<RbMarkdownProps> = ({

View File

@@ -149,6 +149,9 @@ export const lightTheme: ThemeConfig = {
}, },
Segmented: { Segmented: {
trackBg: '#E1E2E7', trackBg: '#E1E2E7',
},
Pagination: {
itemSizeSM: 28,
} }
} }
}; };

View File

@@ -271,6 +271,13 @@ body {
.ant-pagination .ant-pagination-item.ant-pagination-item-active { .ant-pagination .ant-pagination-item.ant-pagination-item-active {
font-weight: 400; font-weight: 400;
} }
.ant-pagination.ant-pagination-mini .ant-pagination-item {
margin-right: 8px;
}
.ant-pagination.ant-pagination-mini .ant-pagination-options-quick-jumper input,
.ant-pagination.ant-pagination-mini .ant-select-single.ant-select-sm {
height: 28px;
}
.ant-modal .ant-modal-content { .ant-modal .ant-modal-content {
padding: 24px; padding: 24px;
@@ -409,4 +416,8 @@ body {
.upload-block, .upload-block,
.upload-block.ant-upload-wrapper .ant-upload-select { .upload-block.ant-upload-wrapper .ant-upload-select {
display: block; display: block;
}
.ant-picker-outlined:focus,
.ant-picker-outlined:focus-within {
box-shadow: none;
} }

View File

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

View File

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

View File

@@ -7,7 +7,8 @@
import { type FC, useEffect, useState, useRef } from 'react' import { type FC, useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' 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 * as echarts from 'echarts'
import 'echarts-wordcloud' import 'echarts-wordcloud'
@@ -41,6 +42,7 @@ export interface SemanticMemory {
/** Combined API response containing both memory categories. */ /** Combined API response containing both memory categories. */
interface Data { interface Data {
total: number;
episodic_memories: EpisodicMemory[]; episodic_memories: EpisodicMemory[];
semantic_memories: SemanticMemory[] semantic_memories: SemanticMemory[]
} }
@@ -71,7 +73,19 @@ const ExplicitDetail: FC = () => {
/** Keeps a stable reference to the ECharts instance for cleanup. */ /** Keeps a stable reference to the ECharts instance for cleanup. */
const chartInstance = useRef<echarts.ECharts | null>(null) const chartInstance = useRef<echarts.ECharts | null>(null)
const [loading, setLoading] = useState<boolean>(false) 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. */ /* Fetch data whenever the route user ID changes. */
useEffect(() => { useEffect(() => {
@@ -131,35 +145,77 @@ const ExplicitDetail: FC = () => {
}) })
return () => { chartInstance.current?.dispose(); chartInstance.current = null } return () => { chartInstance.current?.dispose(); chartInstance.current = null }
}, [data.semantic_memories]) }, [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 ( return (
<Row gutter={12} className="rb:h-full!"> <Row gutter={12} className="rb:h-full!">
<Col span={12} className="rb:h-full!"> <Col span={12} className="rb:h-full!">
<RbCard <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" headerType="borderless"
headerClassName="rb:min-h-[50px]! rb:font-[MiSans-Bold] rb:font-bold" headerClassName="rb:min-h-[50px]!"
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-50px)] rb:overflow-y-auto!" bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-50px)]"
className="rb:h-full!" className="rb:h-full!"
> >
{loading ? {loading ?
<Skeleton active /> <Skeleton active />
: data.episodic_memories?.length > 0 ? ( : (
<Flex gap={12} vertical> <Flex gap={12} vertical className="rb:h-full!">
{data.episodic_memories.map(item => ( <Row gutter={12}>
<div <Col span={12}>
key={item.id} <DatePicker.RangePicker
className="rb:cursor-pointer rb:bg-[#F6F6F6] rb:rounded-xl rb:pt-2.5 rb:px-3 rb:pb-3" value={dateRange}
onClick={() => handleView(item)} onChange={(val) => { setDateRange(val); setPage(1) }}
> allowClear
<Flex align="center" justify="space-between"> />
<span className="rb:font-medium rb:pl-1">{item.title}</span> </Col>
<div className="rb:textt-[#5B6167] rb:leading-4.25 rb:text-[12px]">{formatDateTime(item.created_at)}</div> </Row>
</Flex> <div className="rb:max-h-[calc(100%-92px)] rb:overflow-y-auto">
<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> {pagedEpisodic.length > 0 ? pagedEpisodic.map(item => (
</div> <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> </Flex>
) : <Empty /> )
} }
</RbCard> </RbCard>
</Col> </Col>

View File

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