fix(web): chat history audio add status

This commit is contained in:
zhaoying
2026-03-30 15:49:58 +08:00
parent e59a215078
commit 64a73c41d6
3 changed files with 69 additions and 43 deletions

View File

@@ -37,11 +37,11 @@ const ChatContent: FC<ChatContentProps> = ({
const prevDataLengthRef = useRef(data.length); const prevDataLengthRef = useRef(data.length);
const isScrolledToBottomRef = useRef(true); const isScrolledToBottomRef = useRef(true);
const audioRef = useRef<HTMLAudioElement | null>(null) const audioRef = useRef<HTMLAudioElement | null>(null)
const [playingIndex, setPlayingIndex] = useState<number | null>(null) const [playingIndex, setPlayingIndex] = useState<string | null>(null)
const handlePlay = (index: number, audio_url: string, audio_status?: string) => { const handlePlay = (audio_url: string, audio_status?: string) => {
if (audio_status !== 'completed' && !audio_status) return if (audio_status !== 'completed' && typeof audio_status === 'string') return
if (playingIndex === index) { if (playingIndex === audio_url) {
audioRef.current?.pause() audioRef.current?.pause()
setPlayingIndex(null) setPlayingIndex(null)
return return
@@ -52,7 +52,7 @@ const ChatContent: FC<ChatContentProps> = ({
const audio = new Audio(audio_url) const audio = new Audio(audio_url)
audioRef.current = audio audioRef.current = audio
audio.play() audio.play()
setPlayingIndex(index) setPlayingIndex(audio_url)
audio.onended = () => setPlayingIndex(null) audio.onended = () => setPlayingIndex(null)
} }
@@ -79,12 +79,16 @@ const ChatContent: FC<ChatContentProps> = ({
} }
}; };
}, []); }, []);
// Auto-scroll to bottom when data changes to show latest messages // 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 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 // When data array length changes, auto-scroll to bottom
// If already scrolled to bottom, will auto-scroll to bottom // If already scrolled to bottom, will auto-scroll to bottom
useEffect(() => { useEffect(() => {
if (playingIndex && !data.some(item => item.meta_data?.audio_url === playingIndex)) {
audioRef.current?.pause()
setPlayingIndex(null)
}
setTimeout(() => { setTimeout(() => {
if (scrollContainerRef.current) { if (scrollContainerRef.current) {
// Auto-scroll if data length changed OR user is currently at bottom // Auto-scroll if data length changed OR user is currently at bottom
@@ -204,16 +208,16 @@ const ChatContent: FC<ChatContentProps> = ({
{item.meta_data?.audio_url && <> {item.meta_data?.audio_url && <>
<Divider className="rb:my-3!" /> <Divider className="rb:my-3!" />
<Space size={12} className="rb:pb-2 rb:pl-1"> <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 /> ? <Spin />
: playingIndex !== index : 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(index, 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(index, item.meta_data?.audio_url!, item.meta_data?.audio_status)} onClick={() => handlePlay(item.meta_data?.audio_url!, item.meta_data?.audio_status)}
/> />
} }
</Space> </Space>

View File

@@ -34,7 +34,7 @@ const Statistics: FC = () => {
className: 'rb:text-[#212332]' className: 'rb:text-[#212332]'
}, },
{ {
title: t('application.createTime'), title: t('application.created_at'),
dataIndex: 'created_at', dataIndex: 'created_at',
key: 'created_at', key: 'created_at',
render: (createdAt: string) => formatDateTime(createdAt, 'YYYY-MM-DD HH:mm:ss'), render: (createdAt: string) => formatDateTime(createdAt, 'YYYY-MM-DD HH:mm:ss'),

View File

@@ -64,6 +64,13 @@ const Conversation: FC = () => {
const [config, setConfig] = useState<Record<string, any>>({}) const [config, setConfig] = useState<Record<string, any>>({})
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({}) const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
useEffect(() => {
return () => {
audioPollingRef.current.forEach((timer) => clearInterval(timer))
audioPollingRef.current.clear()
}
}, [])
useEffect(() => { useEffect(() => {
const shareToken = localStorage.getItem(`shareToken_${token}`) const shareToken = localStorage.getItem(`shareToken_${token}`)
setShareToken(shareToken) setShareToken(shareToken)
@@ -144,13 +151,29 @@ const Conversation: FC = () => {
} }
useEffect(() => { useEffect(() => {
audioPollingRef.current.forEach((timer) => clearInterval(timer))
audioPollingRef.current.clear()
if (conversation_id) { if (conversation_id) {
getConversationDetail(token as string, conversation_id) getConversationDetail(token as string, conversation_id)
.then(res => { .then(res => {
const response = res as { messages: ChatItem[] } 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 { } else {
if (features?.opening_statement?.statement) { if (features?.opening_statement?.statement) {
@@ -228,6 +251,28 @@ const Conversation: FC = () => {
})) }))
}, [audioStatusMap, chatList.length]) }, [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 */ /** Send message and handle streaming response */
const handleSend = (msg?: string) => { const handleSend = (msg?: string) => {
if (!token || !shareToken) return if (!token || !shareToken) return
@@ -287,35 +332,8 @@ const Conversation: FC = () => {
const { file_id } = item.data as { file_id?: string } const { file_id } = item.data as { file_id?: string }
const idToPoll = file_id || audio_url || '' const idToPoll = file_id || audio_url || ''
const fileId = audio_url.split('/').pop() const fileId = audio_url.split('/').pop()
if (fileId && idToPoll && !audioPollingRef.current.has(idToPoll)) { if (fileId && idToPoll) {
startAudioPolling(audio_url, 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)
} }
} else { } else {
getHistory(true) getHistory(true)
@@ -327,6 +345,10 @@ const Conversation: FC = () => {
updateAssistantMessage(content, audio_url, undefined, citations) updateAssistantMessage(content, audio_url, undefined, citations)
} }
setLoading(false) setLoading(false)
getHistory(true)
if (currentConversationId && currentConversationId !== conversation_id) {
setConversationId(currentConversationId)
}
break break
} }
}) })