feat(web): chat's audio add status
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:17
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-19 20:45:39
|
||||
* @Last Modified time: 2026-03-23 18:24:33
|
||||
*/
|
||||
import { type FC, useRef, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -38,7 +38,8 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const [playingIndex, setPlayingIndex] = useState<number | null>(null)
|
||||
|
||||
const handlePlay = (index: number, audio_url: string) => {
|
||||
const handlePlay = (index: number, audio_url: string, audio_status?: string) => {
|
||||
if (audio_status !== 'completed' && !audio_status) return
|
||||
if (playingIndex === index) {
|
||||
audioRef.current?.pause()
|
||||
setPlayingIndex(null)
|
||||
@@ -180,11 +181,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
|
||||
? <SoundOutlined className="rb:cursor-pointer rb:hover:text-[#155EEF]! rb:size-5.5" onClick={() => handlePlay(index, item.meta_data?.audio_url!)} />
|
||||
{playingIndex !== index && item.meta_data?.audio_status === 'pending'
|
||||
? <Spin />
|
||||
: playingIndex !== index
|
||||
? <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)} />
|
||||
: <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!)}
|
||||
onClick={() => handlePlay(index, item.meta_data?.audio_url!, item.meta_data?.audio_status)}
|
||||
/>
|
||||
}
|
||||
</Space>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @Last Modified time: 2026-03-23 17:46:25
|
||||
*/
|
||||
import { type FC, useEffect, useMemo, useState } from 'react'
|
||||
import { Flex, Input, Form, Spin } from 'antd'
|
||||
import { Flex, Input, Spin } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type { ChatInputProps } from './types'
|
||||
@@ -24,30 +24,19 @@ const ChatInput: FC<ChatInputProps> = ({
|
||||
className = '',
|
||||
onChange
|
||||
}) => {
|
||||
const [form] = Form.useForm()
|
||||
const values = Form.useWatch([], form)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
// Monitor form value changes to control send button state
|
||||
|
||||
// Clear form when external message is empty
|
||||
// Clear input when external message is cleared
|
||||
useEffect(() => {
|
||||
if (!message) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [form, message])
|
||||
if (!message) setInputValue('')
|
||||
}, [message])
|
||||
|
||||
// Clear input when loading
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
if (loading) setInputValue('')
|
||||
}, [loading])
|
||||
|
||||
|
||||
const handleDelete = (file: any) => {
|
||||
fileChange?.(fileList?.filter(item => {
|
||||
return item.thumbUrl && file.thumbUrl ? item.thumbUrl !== file.thumbUrl
|
||||
@@ -55,7 +44,7 @@ const ChatInput: FC<ChatInputProps> = ({
|
||||
: item.uid !== file.uid
|
||||
}) || [])
|
||||
}
|
||||
// Convert file object to preview URL
|
||||
|
||||
const previewFileList = useMemo(() => {
|
||||
return fileList?.map(file => ({
|
||||
...file,
|
||||
@@ -64,16 +53,11 @@ const ChatInput: FC<ChatInputProps> = ({
|
||||
}, [fileList])
|
||||
|
||||
const handleSend = () => {
|
||||
if (loading || !values || !values?.message || values?.message?.trim() === '') return
|
||||
onSend(values.message)
|
||||
if (loading || !inputValue || inputValue.trim() === '') return
|
||||
onSend(inputValue)
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocus(true)
|
||||
}
|
||||
const handleBlur = () => {
|
||||
setIsFocus(false)
|
||||
}
|
||||
const canSend = !loading && inputValue.trim() !== ''
|
||||
|
||||
return (
|
||||
<div className={`rb:absolute rb:bottom-3 rb:left-0 rb:right-0 rb:w-full ${className}`}>
|
||||
@@ -174,41 +158,40 @@ const ChatInput: FC<ChatInputProps> = ({
|
||||
})}
|
||||
</Flex>
|
||||
</div>}
|
||||
{/* Message input form */}
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="message" noStyle>
|
||||
<Input.TextArea
|
||||
className="rb:m-[10px_12px_10px_12px]! rb:p-0! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto] rb:h-15! rb:resize-none! rb:rounded-none!"
|
||||
variant="borderless"
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
// Enter to send, Shift+Enter for new line
|
||||
if (e.key === 'Enter' && !e.shiftKey && (e.target as HTMLTextAreaElement).value?.trim() !== '' && !loading) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{/* Message input area */}
|
||||
<Input.TextArea
|
||||
value={inputValue}
|
||||
className="rb:m-[10px_12px_10px_12px]! rb:p-0! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto] rb:h-15! rb:resize-none! rb:rounded-none!"
|
||||
variant="borderless"
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value)
|
||||
onChange?.(e.target.value)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Enter to send, Shift+Enter for new line
|
||||
if (e.key === 'Enter' && !e.shiftKey && (e.target as HTMLTextAreaElement).value?.trim() !== '' && !loading) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
onFocus={() => setIsFocus(true)}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
/>
|
||||
|
||||
{/* Bottom action area */}
|
||||
<Flex align="center" justify="space-between" gap={8} className="rb:mx-2.5! rb:mb-2.5!">
|
||||
{/* Child component content (such as buttons) */}
|
||||
<div className="rb:flex-1">{children}</div>
|
||||
<Flex align="center" justify="center"
|
||||
className={clsx('rb:size-7 rb:rounded-full rb:shadow-[0px 2px 12px 0px rgba(23,23,25,0.1)]', {
|
||||
'rb:cursor-not-allowed rb:bg-[#F6F6F6]': loading || !values || !values?.message || values?.message?.trim() === '',
|
||||
'rb:cursor-pointer rb:bg-[#171719]': !loading && !(!values || !values?.message || values?.message?.trim() === '')
|
||||
'rb:cursor-not-allowed rb:bg-[#F6F6F6]': !canSend,
|
||||
'rb:cursor-pointer rb:bg-[#171719]': canSend
|
||||
})}
|
||||
onClick={handleSend}
|
||||
>
|
||||
<div className={clsx("rb:size-4 rb:bg-cover", {
|
||||
"rb:bg-[url('@/assets/images/conversation/loading.svg')]": loading,
|
||||
"rb:bg-[url('@/assets/images/conversation/sendDisabled.svg')]": !loading && (!values || !values?.message || values?.message?.trim() === ''),
|
||||
"rb:bg-[url('@/assets/images/conversation/send.svg')]": !loading && !(!values || !values?.message || values?.message?.trim() === '')
|
||||
"rb:bg-[url('@/assets/images/conversation/sendDisabled.svg')]": !loading && !canSend,
|
||||
"rb:bg-[url('@/assets/images/conversation/send.svg')]": canSend
|
||||
})}></div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:45:54
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-19 20:47:12
|
||||
* @Last Modified time: 2026-03-23 18:15:05
|
||||
*/
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface ChatItem {
|
||||
error?: string;
|
||||
meta_data?: {
|
||||
audio_url?: string;
|
||||
audio_status?: string;
|
||||
files?: any[];
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user