2025-03-05 03:46:45 +08:00
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
2025-03-13 09:14:25 +08:00
|
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
|
|
|
import { fetchMessages, sendMessage } from '../../store/chat/chat.messages.thunks';
|
|
|
|
|
import { resetMessages, resetSendMessageStatus } from '../../store/chat/chat.slice';
|
|
|
|
|
import { showNotification } from '../../store/notification.slice';
|
2025-03-05 03:46:45 +08:00
|
|
|
|
import SvgIcon from '../../components/SvgIcon';
|
|
|
|
|
|
|
|
|
|
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
2025-03-13 09:14:25 +08:00
|
|
|
|
const dispatch = useDispatch();
|
2025-03-05 03:46:45 +08:00
|
|
|
|
const [inputMessage, setInputMessage] = useState('');
|
|
|
|
|
const messagesEndRef = useRef(null);
|
|
|
|
|
|
2025-03-13 09:14:25 +08:00
|
|
|
|
// 从 Redux store 获取数据
|
|
|
|
|
const {
|
|
|
|
|
items: messages,
|
|
|
|
|
status: messagesStatus,
|
|
|
|
|
error: messagesError,
|
|
|
|
|
} = useSelector((state) => state.chat.messages);
|
|
|
|
|
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
|
|
|
|
|
const knowledgeBase = useSelector((state) =>
|
|
|
|
|
state.knowledgeBase.list.items.find((kb) => kb.id === knowledgeBaseId)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 获取聊天消息
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (chatId) {
|
|
|
|
|
dispatch(fetchMessages(chatId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 组件卸载时重置消息状态
|
|
|
|
|
return () => {
|
|
|
|
|
dispatch(resetMessages());
|
|
|
|
|
};
|
|
|
|
|
}, [chatId, dispatch]);
|
|
|
|
|
|
|
|
|
|
// 监听发送消息状态
|
2025-03-05 03:46:45 +08:00
|
|
|
|
useEffect(() => {
|
2025-03-13 09:14:25 +08:00
|
|
|
|
if (sendStatus === 'failed' && sendError) {
|
|
|
|
|
dispatch(
|
|
|
|
|
showNotification({
|
|
|
|
|
message: `发送失败: ${sendError}`,
|
|
|
|
|
type: 'danger',
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
dispatch(resetSendMessageStatus());
|
2025-03-05 03:46:45 +08:00
|
|
|
|
}
|
2025-03-13 09:14:25 +08:00
|
|
|
|
}, [sendStatus, sendError, dispatch]);
|
2025-03-05 03:46:45 +08:00
|
|
|
|
|
2025-03-13 09:14:25 +08:00
|
|
|
|
// 滚动到底部
|
2025-03-05 03:46:45 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
}, [messages]);
|
|
|
|
|
|
|
|
|
|
const handleSendMessage = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
2025-03-13 09:14:25 +08:00
|
|
|
|
if (!inputMessage.trim() || sendStatus === 'loading') return;
|
2025-03-05 03:46:45 +08:00
|
|
|
|
|
2025-03-13 09:14:25 +08:00
|
|
|
|
// 发送消息
|
|
|
|
|
dispatch(sendMessage({ chatId, content: inputMessage }));
|
2025-03-05 03:46:45 +08:00
|
|
|
|
setInputMessage('');
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-13 09:14:25 +08:00
|
|
|
|
// 渲染加载状态
|
|
|
|
|
const renderLoading = () => (
|
|
|
|
|
<div className='p-5 text-center'>
|
|
|
|
|
<div className='spinner-border text-secondary' role='status'>
|
|
|
|
|
<span className='visually-hidden'>加载中...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className='mt-3 text-muted'>加载聊天记录...</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 渲染错误状态
|
|
|
|
|
const renderError = () => (
|
|
|
|
|
<div className='p-5 text-center'>
|
|
|
|
|
<div className='text-danger mb-3'>
|
|
|
|
|
<SvgIcon className='error' width='48' height='48' />
|
|
|
|
|
</div>
|
|
|
|
|
<div className='text-muted'>加载聊天记录失败,请重试</div>
|
|
|
|
|
<button className='btn btn-outline-secondary mt-3' onClick={() => dispatch(fetchMessages(chatId))}>
|
|
|
|
|
重新加载
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 渲染空状态
|
|
|
|
|
const renderEmpty = () => (
|
|
|
|
|
<div className='p-5 text-center'>
|
|
|
|
|
<div className='text-muted'>暂无聊天记录,发送一条消息开始聊天吧</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-05 03:46:45 +08:00
|
|
|
|
return (
|
|
|
|
|
<div className='chat-window d-flex flex-column h-100'>
|
|
|
|
|
{/* Chat header */}
|
|
|
|
|
<div className='p-3 border-bottom'>
|
2025-03-13 09:14:25 +08:00
|
|
|
|
<h5 className='mb-0'>{knowledgeBase?.name || '加载中...'}</h5>
|
2025-03-05 03:46:45 +08:00
|
|
|
|
<small className='text-muted'>{knowledgeBase?.description}</small>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Chat messages */}
|
2025-03-13 09:14:25 +08:00
|
|
|
|
<div className='flex-grow-1 p-3 overflow-auto'>
|
2025-03-05 03:46:45 +08:00
|
|
|
|
<div className='container'>
|
2025-03-13 09:14:25 +08:00
|
|
|
|
{messagesStatus === 'loading'
|
|
|
|
|
? renderLoading()
|
|
|
|
|
: messagesStatus === 'failed'
|
|
|
|
|
? renderError()
|
|
|
|
|
: messages.length === 0
|
|
|
|
|
? renderEmpty()
|
|
|
|
|
: messages.map((message) => (
|
|
|
|
|
<div
|
|
|
|
|
key={message.id}
|
|
|
|
|
className={`d-flex ${
|
|
|
|
|
message.sender === 'user' ? 'justify-content-end' : 'justify-content-start'
|
|
|
|
|
} mb-3`}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`p-3 rounded-3 ${
|
|
|
|
|
message.sender === 'user' ? 'bg-primary text-white' : 'bg-white border'
|
|
|
|
|
}`}
|
|
|
|
|
style={{ maxWidth: '75%' }}
|
|
|
|
|
>
|
|
|
|
|
{message.content}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
{sendStatus === 'loading' && (
|
2025-03-05 03:46:45 +08:00
|
|
|
|
<div className='d-flex justify-content-start mb-3'>
|
|
|
|
|
<div className='p-3 rounded-3 bg-white border'>
|
|
|
|
|
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
|
2025-03-13 09:14:25 +08:00
|
|
|
|
<span className='visually-hidden'>加载中...</span>
|
2025-03-05 03:46:45 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div ref={messagesEndRef} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Chat input */}
|
|
|
|
|
<div className='p-3 border-top'>
|
|
|
|
|
<form onSubmit={handleSendMessage} className='d-flex gap-2'>
|
|
|
|
|
<input
|
|
|
|
|
type='text'
|
|
|
|
|
className='form-control'
|
|
|
|
|
placeholder='输入你的问题...'
|
|
|
|
|
value={inputMessage}
|
|
|
|
|
onChange={(e) => setInputMessage(e.target.value)}
|
2025-03-13 09:14:25 +08:00
|
|
|
|
disabled={sendStatus === 'loading'}
|
2025-03-05 03:46:45 +08:00
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type='submit'
|
|
|
|
|
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
|
2025-03-13 09:14:25 +08:00
|
|
|
|
disabled={sendStatus === 'loading' || !inputMessage.trim()}
|
2025-03-05 03:46:45 +08:00
|
|
|
|
>
|
|
|
|
|
<SvgIcon className='send' color='#ffffff' />
|
2025-03-13 09:14:25 +08:00
|
|
|
|
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
|
|
|
|
|
发送
|
|
|
|
|
</span>
|
2025-03-05 03:46:45 +08:00
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|