Merge branch 'develop' into feature/ui_upgrade_zy

This commit is contained in:
yingzhao
2026-04-01 11:29:03 +08:00
committed by GitHub
180 changed files with 7455 additions and 3660 deletions

View File

@@ -154,6 +154,8 @@ export const analyticsRefresh = (end_user_id: string) => {
export const getForgetStats = (end_user_id: string) => {
return request.get(`/memory/forget-memory/stats`, { end_user_id })
}
// 获取带遗忘节点列表
export const getForgetPendingNodesUrl = '/memory/forget-memory/pending-nodes'
// Implicit Memory - Preferences
export const getImplicitPreferences = (end_user_id: string) => {
return request.get(`/memory/implicit-memory/preferences/${end_user_id}`)

View File

@@ -37,7 +37,6 @@ const ChatContent: FC<ChatContentProps> = ({
const prevDataLengthRef = useRef(data.length);
const isScrolledToBottomRef = useRef(true);
const audioRef = useRef<HTMLAudioElement | null>(null)
const [playingIndex, setPlayingIndex] = useState<number | null>(null)
const [expandedReasoning, setExpandedReasoning] = useState<Set<number>>(new Set())
const [manualToggledReasoning, setManualToggledReasoning] = useState<Set<number>>(new Set())
@@ -54,10 +53,11 @@ const ChatContent: FC<ChatContentProps> = ({
if (manualToggledReasoning.has(index)) return expandedReasoning.has(index)
return !data[index]?.content
}
const [playingIndex, setPlayingIndex] = useState<string | null>(null)
const handlePlay = (index: number, audio_url: string, audio_status?: string) => {
if (audio_status !== 'completed' && !audio_status) return
if (playingIndex === index) {
const handlePlay = (audio_url: string, audio_status?: string) => {
if (audio_status !== 'completed' && typeof audio_status === 'string') return
if (playingIndex === audio_url) {
audioRef.current?.pause()
setPlayingIndex(null)
return
@@ -68,7 +68,7 @@ const ChatContent: FC<ChatContentProps> = ({
const audio = new Audio(audio_url)
audioRef.current = audio
audio.play()
setPlayingIndex(index)
setPlayingIndex(audio_url)
audio.onended = () => setPlayingIndex(null)
}
@@ -95,12 +95,16 @@ const ChatContent: FC<ChatContentProps> = ({
}
};
}, []);
// Auto-scroll to bottom when data changes to show latest messages
// When data array length remains unchanged, if data is updated and user manually scrolled up, don't auto-scroll to bottom
// When data array length changes, auto-scroll to bottom
// If already scrolled to bottom, will auto-scroll to bottom
useEffect(() => {
if (playingIndex && !data.some(item => item.meta_data?.audio_url === playingIndex)) {
audioRef.current?.pause()
setPlayingIndex(null)
}
setTimeout(() => {
if (scrollContainerRef.current) {
// Auto-scroll if data length changed OR user is currently at bottom
@@ -236,16 +240,16 @@ const ChatContent: FC<ChatContentProps> = ({
{item.meta_data?.audio_url && <>
<Divider className="rb:my-3!" />
<Space size={12} className="rb:pb-2 rb:pl-1">
{playingIndex !== index && item.meta_data?.audio_status === 'pending'
{playingIndex !== item.meta_data?.audio_url && item.meta_data?.audio_status === 'pending'
? <Spin />
: playingIndex !== index
: playingIndex !== item.meta_data?.audio_url
? <SoundOutlined className={clsx("rb:cursor-pointer rb:size-5.5", {
'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)
})} onClick={() => handlePlay(index, item.meta_data?.audio_url!, item.meta_data?.audio_status)} />
})} onClick={() => handlePlay(item.meta_data?.audio_url!, item.meta_data?.audio_status)} />
: <div
className="rb:size-5.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]"
onClick={() => handlePlay(index, item.meta_data?.audio_url!, item.meta_data?.audio_status)}
onClick={() => handlePlay(item.meta_data?.audio_url!, item.meta_data?.audio_status)}
/>
}
</Space>

View File

@@ -24,16 +24,17 @@ interface BodyWrapperProps {
/** Whether to show loading state */
loading?: boolean
/** Whether the content is empty */
empty: boolean
empty: boolean;
className?: string;
}
const BodyWrapper: FC<BodyWrapperProps> = ({ children, loading = false, empty }) => {
const BodyWrapper: FC<BodyWrapperProps> = ({ children, loading = false, empty, className = 'rb:max-h-[calc(100%-48px)]!' }) => {
// Show loading spinner while data is being fetched
if (loading) {
return <PageLoading />
return <PageLoading className={className} />
}
// Show empty state when no data is available
if (!loading && empty) {
return <PageEmpty />
return <PageEmpty className={className} />
}
// Render actual content when data is loaded and available
return children

View File

@@ -128,6 +128,7 @@ const Menu: FC<{
/** Filter menus based on user role and source */
useEffect(() => {
if (!user) return
let menuList: MenuItem[] = []
if (user.role === 'member' && source === 'space') {
@@ -136,7 +137,7 @@ const Menu: FC<{
menuList = allMenus[source] || []
}
const noAuthList = ['user', 'pricing'].filter(vo => !user.permissions?.includes(vo) && !user.permissions?.includes('all'))
const noAuthList = ['user', 'pricing'].filter(vo => (Array.isArray(user.permissions) && !user.permissions?.includes(vo) && !user.permissions?.includes('all')) || !Array.isArray(user.permissions))
if (noAuthList && !noAuthList?.includes('all')) {
const filterMenus = (list: MenuItem[]): MenuItem[] =>{

View File

@@ -1512,7 +1512,7 @@ export const en = {
EPISODIC_MEMORY: 'Episodic Memory',
FORGET_MEMORY: 'Forget Memory',
endUserProfile: 'Profile',
endUserProfile: 'Permanent Memory',
editEndUserProfile: 'Edit',
other_name: 'Name',
position: 'Position',

View File

@@ -1510,7 +1510,7 @@ export const zh = {
EPISODIC_MEMORY: '情景记忆',
FORGET_MEMORY: '遗忘记忆',
endUserProfile: '核心档案',
endUserProfile: '永久记忆',
editEndUserProfile: '编辑',
other_name: '名称',
position: '职位',

View File

@@ -377,9 +377,10 @@ body {
.ant-input-filled,
.ant-select-filled:not(.ant-select-customize-input) .ant-select-selector {
background-color: #FFFFFF;
border-color: #FFFFFF;
}
.ant-input-filled:hover,
.ant-select-filled:not(.ant-select-customize-input) .ant-select-selector {
.ant-select-filled:not(.ant-select-disabled):not(.ant-select-customize-input):not(.ant-pagination-size-changer):hover .ant-select-selector {
background-color: #FFFFFF;
border-color: #171719;
}
@@ -402,7 +403,7 @@ body {
color: #FF5D34;
}
.spin.ant-spin-nested-loading .ant-spin-container::after {
.spin .ant-spin-nested-loading .ant-spin-container::after {
background: transparent;
}
.upload-block,

View File

@@ -34,16 +34,16 @@ const Statistics: FC = () => {
className: 'rb:text-[#212332]'
},
{
title: t('user.createTime'),
title: t('application.created_at'),
dataIndex: 'created_at',
key: 'created_at',
render: (createdAt: string) => formatDateTime(createdAt, 'YYYY-MM-DD HH:mm:ss'),
},
{
title: t('user.lastLoginTime'),
dataIndex: 'last_login_at',
key: 'last_login_at',
render: (lastLoginAt: string) => lastLoginAt ? formatDateTime(lastLoginAt, 'YYYY-MM-DD HH:mm:ss') : '-',
title: t('common.updated_at'),
dataIndex: 'updated_at',
key: 'updated_at',
render: (updatedAt: string) => updatedAt ? formatDateTime(updatedAt, 'YYYY-MM-DD HH:mm:ss') : '-',
},
{
title: t('common.operation'),

View File

@@ -220,31 +220,31 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
/>
<Popover content={t('workflow.clear')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('src/assets/images/workflow/clear.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/clear.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
onClick={clear}
></div>
</Popover>
<Popover content={t('workflow.addvariable')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('src/assets/images/workflow/variable.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/variable.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
onClick={addvariable}
></div>
</Popover>
<Popover content={t('workflow.run')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('src/assets/images/workflow/run.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/run.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
onClick={run}
></div>
</Popover>
<Popover content={t('workflow.save')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('src/assets/images/workflow/save.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/save.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
onClick={save}
></div>
</Popover>
<Popover content={t('common.return')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('src/assets/images/workflow/return.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/return.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
onClick={goToApplication}
></div>
</Popover>

View File

@@ -49,7 +49,7 @@ const FeaturesConfig: FC<FeaturesConfigProps> = ({
?
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('src/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
onClick={handleFeaturesConfig}
></div>
</Popover>

View File

@@ -216,7 +216,7 @@ const ApplicationManagement: React.FC = () => {
'rb:text-[#155EEF]': key === 'type',
})}>
{key === 'source' && item.is_shared
? t('application.shared')
? item.source_workspace_name
: key === 'source' && !item.is_shared
? t('application.configuration')
: key === 'created_at'

View File

@@ -65,6 +65,13 @@ const Conversation: FC = () => {
const [isDeepThinking, setIsDeepThinking] = useState<Record<string, any>>({})
const [thinking, setThinking] = useState(false)
useEffect(() => {
return () => {
audioPollingRef.current.forEach((timer) => clearInterval(timer))
audioPollingRef.current.clear()
}
}, [])
useEffect(() => {
const shareToken = localStorage.getItem(`shareToken_${token}`)
setShareToken(shareToken)
@@ -146,13 +153,29 @@ const Conversation: FC = () => {
}
useEffect(() => {
audioPollingRef.current.forEach((timer) => clearInterval(timer))
audioPollingRef.current.clear()
if (conversation_id) {
getConversationDetail(token as string, conversation_id)
.then(res => {
const response = res as { messages: ChatItem[] }
setChatList(response?.messages || [])
const messages = response?.messages || []
const historyAudioUrls = new Set(messages.map(m => m.meta_data?.audio_url).filter(Boolean))
audioPollingRef.current.forEach((timer, key) => {
if (!historyAudioUrls.has(key)) {
clearInterval(timer)
audioPollingRef.current.delete(key)
}
})
messages.forEach(msg => {
if (msg.role === 'assistant' && msg.meta_data?.audio_url && msg.meta_data?.audio_status === 'pending') {
startAudioPolling(msg.meta_data.audio_url, msg.meta_data.audio_url)
}
})
setChatList(messages.map(msg => {
if (msg.role === 'assistant' && msg.meta_data?.audio_url && audioPollingRef.current.has(msg.meta_data.audio_url)) {
return { ...msg, meta_data: { ...msg.meta_data, audio_status: 'pending' } }
}
return msg
}))
})
} else {
if (features?.opening_statement?.statement) {
@@ -253,6 +276,28 @@ const Conversation: FC = () => {
}))
}, [audioStatusMap, chatList.length])
const startAudioPolling = (audioUrl: string, idToPoll: string) => {
if (audioPollingRef.current.has(idToPoll)) return
const fileId = audioUrl.split('/').pop()
if (!fileId) return
const timer = setInterval(() => {
getFileStatusById(fileId)
.then(res => {
const { status } = res as { status: string }
if (status && status !== 'pending') {
setAudioStatusMap(prev => ({ ...prev, [idToPoll]: status }))
clearInterval(audioPollingRef.current.get(idToPoll))
audioPollingRef.current.delete(idToPoll)
}
})
.catch(() => {
clearInterval(audioPollingRef.current.get(idToPoll))
audioPollingRef.current.delete(idToPoll)
})
}, 2000)
audioPollingRef.current.set(idToPoll, timer)
}
/** Send message and handle streaming response */
const handleSend = (msg?: string) => {
if (!token || !shareToken) return
@@ -316,35 +361,8 @@ const Conversation: FC = () => {
const { file_id } = item.data as { file_id?: string }
const idToPoll = file_id || audio_url || ''
const fileId = audio_url.split('/').pop()
if (fileId && idToPoll && !audioPollingRef.current.has(idToPoll)) {
const timer = setInterval(() => {
getFileStatusById(fileId)
.then(res => {
const { status } = res as { status: string }
if (status && status !== 'pending') {
setAudioStatusMap(prev => ({
...prev,
[idToPoll]: status
}))
clearInterval(audioPollingRef.current.get(idToPoll))
audioPollingRef.current.delete(idToPoll)
getHistory(true)
if (currentConversationId && currentConversationId !== conversation_id) {
setConversationId(currentConversationId)
}
}
})
.catch(() => {
clearInterval(audioPollingRef.current.get(idToPoll))
audioPollingRef.current.delete(idToPoll)
getHistory(true)
if (currentConversationId && currentConversationId !== conversation_id) {
setConversationId(currentConversationId)
}
})
}, 2000)
audioPollingRef.current.set(idToPoll, timer)
if (fileId && idToPoll) {
startAudioPolling(audio_url, idToPoll)
}
} else {
getHistory(true)
@@ -356,6 +374,10 @@ const Conversation: FC = () => {
updateAssistantMessage(content, audio_url, undefined, citations)
}
setLoading(false)
getHistory(true)
if (currentConversationId && currentConversationId !== conversation_id) {
setConversationId(currentConversationId)
}
break
}
})

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:37:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 10:05:39
* @Last Modified time: 2026-03-27 22:22:18
*/
/**
* Invite Register Page
@@ -144,7 +144,7 @@ const InviteRegister: React.FC = () => {
}).then((res) => {
const response = res as LoginInfo;
updateLoginInfo(response);
navigate('/');
navigate('/', { replace: true });
}).finally(() => {
setLoading(false);
});

View File

@@ -243,6 +243,7 @@ const Prompt: FC = () => {
<ModelSelect
params={{ type: 'llm,chat' }}
className={`rb:w-75! ${styles.select}`}
variant="filled"
/>
</Form.Item>
<Button className="rb:border-none!" onClick={handleJump}>{t('prompt.history')}</Button>

View File

@@ -116,13 +116,13 @@ const History: React.FC = () => {
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4.5">{formatDateTime(item.created_at, 'YYYY/MM/DD HH:mm')}</div>
<Space size={8}>
<div className="rb:size-4.5 rb:bg-cover rb:bg-[url('src/assets/images/prompt/eye.svg')] rb:hover:bg-[url('src/assets/images/prompt/eye_bg.svg')]"
<div className="rb:size-4.5 rb:bg-cover rb:bg-[url('@/assets/images/prompt/eye.svg')] rb:hover:bg-[url('@/assets/images/prompt/eye_bg.svg')]"
onClick={() => handleClick('detail', item)}
></div>
<div className="rb:size-4.5 rb:bg-cover rb:bg-[url('src/assets/images/prompt/edit.svg')] rb:hover:bg-[url('src/assets/images/prompt/edit_bg.svg')]"
<div className="rb:size-4.5 rb:bg-cover rb:bg-[url('@/assets/images/prompt/edit.svg')] rb:hover:bg-[url('@/assets/images/prompt/edit_bg.svg')]"
onClick={() => handleClick('edit', item)}
></div>
<div className="rb:size-4.5 rb:bg-cover rb:bg-[url('src/assets/images/prompt/delete.svg')] rb:hover:bg-[url('src/assets/images/prompt/delete_hover.svg')]"
<div className="rb:size-4.5 rb:bg-cover rb:bg-[url('@/assets/images/prompt/delete.svg')] rb:hover:bg-[url('@/assets/images/prompt/delete_hover.svg')]"
onClick={() => handleClick('delete', item)}
></div>
</Space>

View File

@@ -65,8 +65,8 @@ const CommunityNetwork: FC<{ onSelectCommunity?: (node: RawCommunityNode) => voi
}, [id])
if (loading) {
return <Flex align="center" justify="center" className="rb:w-full rb:h-full">
<Spin tip={t('userMemory.communityLoadingTip')} size="large" className="rb:text-[#5B6167]! spin">
return <Flex align="center" justify="center" className="rb:w-full rb:h-full spin">
<Spin tip={t('userMemory.communityLoadingTip')} size="large" className="rb:text-[#5B6167]!">
<div className="rb:w-64 rb:h-64" />
</Spin>
</Flex>

View File

@@ -12,6 +12,7 @@ import { Row, Col, Progress, App, Table } from 'antd'
import RbCard from '@/components/RbCard/Card'
import {
getForgetStats,
getForgetPendingNodesUrl,
} from '@/api/memory'
import type { ForgetData } from '../types'
import ActivationMetricsPieCard from '../components/ActivationMetricsPieCard'
@@ -19,6 +20,7 @@ import RecentTrendsLineCard from '../components/RecentTrendsLineCard'
import { formatDateTime } from '@/utils/format'
import StatusTag from '@/components/StatusTag'
import ForgetRefreshModal from '../components/ForgetRefreshModal';
import RbTable from '@/components/Table'
/** Maps node type keys to StatusTag colour presets for the pending-nodes table. */
const statusTagColors: Record<string, 'success' | 'purple' | 'default' | 'warning' | 'error' | 'lightBlue'> = {
@@ -191,7 +193,9 @@ const ForgetDetail = forwardRef((_props, ref) => {
bodyClassName="rb:p-3! rb:py-0! rb:h-[calc(100%-54px)]"
className="rb:h-full!"
>
<Table
<RbTable
apiUrl={getForgetPendingNodesUrl}
apiParams={{ end_user_id: id }}
rowKey='node_id'
dataSource={data.pending_nodes ?? []}
columns={[
@@ -225,11 +229,6 @@ const ForgetDetail = forwardRef((_props, ref) => {
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>