KnowledgeBase_frontend/src/pages/Chat/ChatWindow.jsx

298 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchMessages } from '../../store/chat/chat.messages.thunks';
import { resetMessages, resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice';
import { showNotification } from '../../store/notification.slice';
import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks';
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
import SvgIcon from '../../components/SvgIcon';
import { get } from '../../services/api';
export default function ChatWindow({ chatId, knowledgeBaseId }) {
const dispatch = useDispatch();
const [inputMessage, setInputMessage] = useState('');
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef(null);
// 从 Redux store 获取消息
const messages = useSelector((state) => state.chat.messages.items);
const messageStatus = useSelector((state) => state.chat.messages.status);
const messageError = useSelector((state) => state.chat.messages.error);
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
// 使用新的Redux状态结构
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId);
const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.loading);
// 获取可用数据集列表
const availableDatasets = useSelector((state) => state.chat.availableDatasets.items || []);
const availableDatasetsLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
// 获取会话详情
const conversation = useSelector((state) => state.chat.currentChat.data);
const conversationStatus = useSelector((state) => state.chat.currentChat.status);
const conversationError = useSelector((state) => state.chat.currentChat.error);
// 获取聊天详情
useEffect(() => {
if (chatId) {
setLoading(true);
dispatch(fetchConversationDetail(chatId))
.unwrap()
.catch((error) => {
// 如果是新聊天API会返回404此时不显示错误
if (error && error !== 'Error: Request failed with status code 404') {
dispatch(
showNotification({
message: `获取聊天详情失败: ${error || '未知错误'}`,
type: 'danger',
})
);
}
})
.finally(() => {
setLoading(false);
});
}
// 组件卸载时清空消息
return () => {
dispatch(resetMessages());
};
}, [chatId, dispatch]);
// 新会话自动添加欢迎消息
useEffect(() => {
// 如果是新聊天且没有任何消息,添加一条系统欢迎消息
if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') {
const selectedKb = knowledgeBase ||
availableDatasets.find((ds) => ds.id === knowledgeBaseId) || { name: '知识库' };
dispatch(
addMessage({
id: 'welcome-' + Date.now(),
role: 'assistant',
content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`,
created_at: new Date().toISOString(),
})
);
}
}, [chatId, messages.length, loading, messageStatus, knowledgeBase, knowledgeBaseId, availableDatasets, dispatch]);
// 监听发送消息状态
useEffect(() => {
if (sendStatus === 'failed' && sendError) {
dispatch(
showNotification({
message: `发送失败: ${sendError}`,
type: 'danger',
})
);
dispatch(resetSendMessageStatus());
}
}, [sendStatus, sendError, dispatch]);
// 滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 获取当前会话的知识库信息
useEffect(() => {
// 如果conversation有数据集信息优先使用它
if (conversation && conversation.datasets && conversation.datasets.length > 0) {
return;
}
// 如果没有会话数据集信息但有knowledgeBaseId尝试从知识库列表中查找
if (knowledgeBaseId && knowledgeBases.length === 0 && !availableDatasets.length) {
dispatch(fetchAvailableDatasets());
}
}, [dispatch, knowledgeBaseId, knowledgeBases, conversation, availableDatasets]);
const handleSendMessage = (e) => {
e.preventDefault();
if (!inputMessage.trim() || sendStatus === 'loading') return;
// 获取知识库ID列表
let dataset_id_list = [];
if (conversation && conversation.datasets) {
// 如果已有会话,使用会话中的知识库
dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, ''));
} else if (knowledgeBaseId) {
// 如果是新会话,使用当前选择的知识库
dataset_id_list = [knowledgeBaseId.replace(/-/g, '')];
} else if (availableDatasets.length > 0) {
// 如果都没有,尝试使用可用知识库列表中的第一个
dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')];
}
if (dataset_id_list.length === 0) {
dispatch(
showNotification({
message: '发送失败:未选择知识库',
type: 'danger',
})
);
return;
}
// 发送消息到服务器
dispatch(
createChatRecord({
dataset_id_list: dataset_id_list,
question: inputMessage,
conversation_id: chatId,
})
)
.unwrap()
.then(() => {
// 成功发送后,可以执行任何需要的操作
// 例如在用户发送第一条消息后更新URL中的会话ID
})
.catch((error) => {
// 发送失败,显示错误信息
dispatch(
showNotification({
message: `发送失败: ${error}`,
type: 'danger',
})
);
});
// 清空输入框
setInputMessage('');
};
// 渲染加载状态
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='alert alert-danger'>
<p className='mb-0'>
<strong>加载消息失败</strong>
</p>
<p className='mb-0 small'>{messageError}</p>
<button className='btn btn-outline-secondary mt-3' onClick={() => dispatch(fetchMessages(chatId))}>
重试
</button>
</div>
);
// 渲染空消息状态
const renderEmpty = () => {
if (loading) return null;
return (
<div className='text-center my-5'>
<p className='text-muted'>暂无消息开始发送第一条消息吧</p>
</div>
);
};
return (
<div className='chat-window d-flex flex-column h-100'>
{/* Chat header */}
<div className='p-3 border-bottom'>
{conversation && conversation.datasets ? (
<>
<h5 className='mb-0'>{conversation.datasets.map((dataset) => dataset.name).join(', ')}</h5>
{conversation.datasets.length > 0 && conversation.datasets[0].type && (
<small className='text-muted'>类型: {conversation.datasets[0].type}</small>
)}
</>
) : knowledgeBase ? (
<>
<h5 className='mb-0'>{knowledgeBase.name}</h5>
<small className='text-muted'>{knowledgeBase.description}</small>
</>
) : (
<h5 className='mb-0'>{loading || availableDatasetsLoading ? '加载中...' : '聊天'}</h5>
)}
</div>
{/* Chat messages */}
<div className='flex-grow-1 p-3 overflow-auto'>
<div className='container'>
{messageStatus === 'loading'
? renderLoading()
: messageStatus === 'failed'
? renderError()
: messages.length === 0
? renderEmpty()
: messages.map((message) => (
<div
key={message.id}
className={`d-flex ${
message.role === 'user' ? 'align-items-end' : 'align-items-start'
} mb-3 flex-column`}
>
<div
className={`chat-message p-3 rounded-3 ${
message.role === 'user' ? 'bg-dark text-white' : 'bg-light'
}`}
style={{
maxWidth: '75%',
position: 'relative',
}}
>
<div className='message-content'>{message.content}</div>
</div>
<div className='message-time small text-muted mt-1'>
{message.created_at && new Date(message.created_at).toLocaleTimeString()}
</div>
</div>
))}
{sendStatus === 'loading' && (
<div className='d-flex justify-content-start mb-3'>
<div className='chat-message p-3 rounded-3 bg-light'>
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
<span className='visually-hidden'>加载中...</span>
</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)}
disabled={sendStatus === 'loading'}
/>
<button
type='submit'
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
disabled={sendStatus === 'loading' || !inputMessage.trim()}
>
<SvgIcon className='send' color='#ffffff' />
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
发送
</span>
</button>
</form>
</div>
</div>
);
}