From ad4ddea97759b87ee11fd67fa525cb8d504d43e2 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 1 Apr 2026 16:43:45 +0800 Subject: [PATCH] feat(web): ui upgrade --- .../assets/images/conversation/compress.svg | 18 ++ web/src/assets/images/conversation/expand.svg | 15 ++ web/src/components/BtnTabs/index.tsx | 8 +- web/src/components/Chat/AudioPlayer.tsx | 152 +++++++++++++ web/src/components/Chat/ChatContent.tsx | 180 +++++++++------- web/src/components/Chat/VideoPlayer.tsx | 62 ++++++ web/src/components/Markdown/index.tsx | 11 +- web/src/styles/antdThemeConfig.ts | 3 + web/src/styles/index.css | 11 + web/src/views/OrderHistory/index.tsx | 50 +++-- web/src/views/SelfReflectionEngine/index.tsx | 199 ++++++++++-------- .../UserMemoryDetail/pages/ExplicitDetail.tsx | 98 +++++++-- .../UserMemoryDetail/pages/WorkingDetail.tsx | 2 +- 13 files changed, 590 insertions(+), 219 deletions(-) create mode 100644 web/src/assets/images/conversation/compress.svg create mode 100644 web/src/assets/images/conversation/expand.svg create mode 100644 web/src/components/Chat/AudioPlayer.tsx create mode 100644 web/src/components/Chat/VideoPlayer.tsx diff --git a/web/src/assets/images/conversation/compress.svg b/web/src/assets/images/conversation/compress.svg new file mode 100644 index 00000000..640d80ba --- /dev/null +++ b/web/src/assets/images/conversation/compress.svg @@ -0,0 +1,18 @@ + + + 编组 35 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/conversation/expand.svg b/web/src/assets/images/conversation/expand.svg new file mode 100644 index 00000000..8cc87d99 --- /dev/null +++ b/web/src/assets/images/conversation/expand.svg @@ -0,0 +1,15 @@ + + + 编组 36 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/BtnTabs/index.tsx b/web/src/components/BtnTabs/index.tsx index 772a4c8d..8a6e670b 100644 --- a/web/src/components/BtnTabs/index.tsx +++ b/web/src/components/BtnTabs/index.tsx @@ -24,10 +24,11 @@ interface BtnTabsProps { onChange: (key: string) => void; /** Optional extra class name for the container */ className?: string; + variant?: 'outline' | 'borderless' } /** Button-style tab switcher — renders tabs as pill-shaped buttons with active highlight */ -const BtnTabs: FC = ({ items, activeKey, onChange, className }) => { +const BtnTabs: FC = ({ items, activeKey, onChange, className, variant = 'borderless' }) => { return ( {items.map((tab) => ( @@ -35,8 +36,9 @@ const BtnTabs: FC = ({ items, activeKey, onChange, className }) => key={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', { - 'rb:bg-[#F6F6F6]': activeKey !== tab.key, - 'rb:bg-[#171719] rb:text-white': activeKey === tab.key, + 'rb:bg-[#F6F6F6]': activeKey !== tab.key && variant === 'borderless', + 'rb-border rb:bg-white': activeKey !== tab.key && variant === 'outline', + 'rb:bg-[#171719] rb:text-white rb:border-[#171719]': activeKey === tab.key, })} > {tab.label} diff --git a/web/src/components/Chat/AudioPlayer.tsx b/web/src/components/Chat/AudioPlayer.tsx new file mode 100644 index 00000000..766c8deb --- /dev/null +++ b/web/src/components/Chat/AudioPlayer.tsx @@ -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 + * + */ +const AudioPlayer: FC = ({ src, fileName, fileSize }) => { + const { t } = useTranslation() + const audioRef = useRef(null) + const [playing, setPlaying] = useState(false) + const [current, setCurrent] = useState(0) + const [duration, setDuration] = useState(0) + const [speed, setSpeed] = useState(1) + + /* Bind native audio events to sync React state; re-binds when src changes. */ + useEffect(() => { + const audio = audioRef.current + if (!audio) return + const onTime = () => setCurrent(audio.currentTime) + const onMeta = () => setDuration(audio.duration) + const onEnd = () => setPlaying(false) + audio.addEventListener('timeupdate', onTime) + audio.addEventListener('loadedmetadata', onMeta) + audio.addEventListener('ended', onEnd) + return () => { + audio.removeEventListener('timeupdate', onTime) + audio.removeEventListener('loadedmetadata', onMeta) + audio.removeEventListener('ended', onEnd) + } + }, [src]) + + /** Toggle between play and pause. */ + const togglePlay = () => { + const audio = audioRef.current + if (!audio) return + if (playing) { audio.pause(); setPlaying(false) } + else { audio.play(); setPlaying(true) } + } + + /** Seek to a specific position (in seconds) on the audio timeline. */ + const handleSeek = (val: number) => { + if (audioRef.current) audioRef.current.currentTime = val + setCurrent(val) + } + + /** Update playback speed on both React state and the native audio element. */ + const setPlaybackSpeed = (s: number) => { + setSpeed(s) + if (audioRef.current) audioRef.current.playbackRate = s + } + + /** Open the audio source URL in a new tab to trigger download. */ + const handleDownload = () => window.open(src, '_blank') + + /** Dropdown menu items: download and playback speed sub-menu. */ + const mainMenu: MenuProps = { + items: [ + { + key: 'download', + icon:
, + label: t('common.download'), + onClick: handleDownload, + }, + { + key: 'speed', + icon:
, + label: t('perceptualDetail.playbackSpeed'), + children: SPEEDS.map(s => ({ + key: String(s), + label: {s === 1 ? 'normal' : s}, + onClick: () => setPlaybackSpeed(s), + })), + }, + ], + } + + return ( +
+