knowledgebase_influencer/src/pages/Chat/ChatWindow.jsx

488 lines
22 KiB
React
Raw Normal View History

2025-04-16 09:51:27 +08:00
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 SafeMarkdown from '../../components/SafeMarkdown';
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);
const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话
2025-04-18 07:19:57 +08:00
// Gmail无邮件警告状态
const [noEmailsWarning, setNoEmailsWarning] = useState(null);
const [troubleshooting, setTroubleshooting] = useState(null);
2025-04-16 09:51:27 +08:00
// 从 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);
2025-04-18 07:19:57 +08:00
// Gmail集成状态
const gmailSetupStatus = useSelector((state) => state.gmailChat?.setup?.status);
const gmailNoEmailsWarning = useSelector((state) => state.gmailChat?.setup?.noEmailsWarning);
const gmailTroubleshooting = useSelector((state) => state.gmailChat?.setup?.troubleshooting);
2025-04-16 09:51:27 +08:00
// 使用新的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);
// 获取会话创建状态
const createSessionStatus = useSelector((state) => state.chat.createSession?.status);
const createSessionId = useSelector((state) => state.chat.createSession?.sessionId);
// 监听知识库ID变更确保保存在组件状态中
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]);
2025-04-18 07:19:57 +08:00
// 检查是否存在Gmail无邮件警告
useEffect(() => {
// 优先使用Redux状态中的警告
if (gmailNoEmailsWarning) {
setNoEmailsWarning(gmailNoEmailsWarning);
setTroubleshooting(gmailTroubleshooting);
return;
}
// 从localStorage中获取警告信息
try {
const savedWarning = localStorage.getItem('gmailNoEmailsWarning');
if (savedWarning) {
const warningData = JSON.parse(savedWarning);
// 检查是否是当前会话的警告且未过期24小时内
const isCurrentChat = warningData.chatId === chatId;
const isStillValid = Date.now() - warningData.timestamp < 24 * 60 * 60 * 1000;
if (isCurrentChat && isStillValid) {
setNoEmailsWarning(warningData.message);
setTroubleshooting(warningData.troubleshooting);
} else if (!isStillValid) {
// 警告过期,清除
localStorage.removeItem('gmailNoEmailsWarning');
}
}
} catch (error) {
console.error('解析Gmail警告信息失败:', error);
localStorage.removeItem('gmailNoEmailsWarning');
}
}, [chatId, gmailNoEmailsWarning, gmailTroubleshooting]);
2025-04-16 09:51:27 +08:00
// 当conversation或knowledgeBaseId更新时更新selectedKnowledgeBaseIds
useEffect(() => {
// 优先使用conversation中的知识库列表
if (conversation && conversation.datasets && conversation.datasets.length > 0) {
const datasetIds = conversation.datasets.map((ds) => ds.id);
console.log('从会话中获取知识库列表:', datasetIds);
setSelectedKnowledgeBaseIds(datasetIds);
}
// 其次使用URL中传入的知识库ID
else if (knowledgeBaseId) {
// 可能是单个ID或以逗号分隔的多个ID
const ids = knowledgeBaseId.split(',').map((id) => id.trim());
console.log('从URL参数中获取知识库列表:', ids);
setSelectedKnowledgeBaseIds(ids);
}
}, [conversation, knowledgeBaseId]);
// 获取聊天详情
useEffect(() => {
if (!chatId) return;
// 如果已经加载过这个chatId的详情不再重复加载
if (hasLoadedDetailRef.current[chatId]) {
console.log('跳过已加载过的会话详情:', chatId);
return;
}
// 检查是否是新创建的会话
const isNewlyCreatedChat = createSessionStatus === 'succeeded' && createSessionId === chatId;
// 如果是新创建的会话且已经有会话数据,则跳过详情获取
if (isNewlyCreatedChat && conversation && conversation.conversation_id === chatId) {
console.log('跳过新创建会话的详情获取:', chatId);
hasLoadedDetailRef.current[chatId] = true;
return;
}
console.log('获取会话详情:', chatId);
setLoading(true);
dispatch(fetchConversationDetail(chatId))
.unwrap()
.then((response) => {
console.log('获取会话详情成功:', response);
// 标记为已加载
hasLoadedDetailRef.current[chatId] = true;
})
.catch((error) => {
console.error('获取会话详情失败:', error);
dispatch(
showNotification({
message: `获取聊天详情失败: ${error || '未知错误'}`,
type: 'danger',
})
);
})
.finally(() => {
setLoading(false);
});
// 组件卸载时清空消息
return () => {
dispatch(resetMessages());
// 不要清空hasLoadedDetailRef否则会导致重复加载
// hasLoadedDetailRef.current = {}; // 清理ref缓存
};
}, [chatId, dispatch, createSessionStatus, createSessionId]);
// 组件销毁时完全清空ref缓存
useEffect(() => {
return () => {
hasLoadedDetailRef.current = {};
};
}, []);
// 新会话自动添加欢迎消息
useEffect(() => {
// 如果是新聊天且没有任何消息,添加一条系统欢迎消息
if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') {
2025-04-18 07:19:57 +08:00
// 检查是否有无邮件警告作为首条消息
let isNoEmailsFirstMessage = false;
let noEmailsMessage = null;
try {
const savedWarning = localStorage.getItem('gmailNoEmailsWarning');
if (savedWarning) {
const warningData = JSON.parse(savedWarning);
// 检查是否是当前会话的警告、是否是首条消息
if (warningData.chatId === chatId && warningData.isFirstMessage) {
isNoEmailsFirstMessage = true;
noEmailsMessage = warningData.message;
}
}
} catch (error) {
console.error('解析Gmail警告信息失败:', error);
}
if (isNoEmailsFirstMessage && noEmailsMessage) {
// 使用警告消息作为首条消息
dispatch(
addMessage({
id: 'gmail-warning-' + Date.now(),
role: 'assistant',
content: `⚠️ ${noEmailsMessage}\n\n您仍然可以在此聊天中提问但可能无法获得与邮件内容相关的回答。`,
created_at: new Date().toISOString(),
})
);
2025-04-16 09:51:27 +08:00
2025-04-18 07:19:57 +08:00
// 移除isFirstMessage标记防止再次显示
try {
const savedWarning = localStorage.getItem('gmailNoEmailsWarning');
if (savedWarning) {
const warningData = JSON.parse(savedWarning);
warningData.isFirstMessage = false;
localStorage.setItem('gmailNoEmailsWarning', JSON.stringify(warningData));
}
} catch (error) {
console.error('更新Gmail警告信息失败:', error);
}
} else {
// 使用常规欢迎消息
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(),
})
);
}
2025-04-16 09:51:27 +08:00
}
}, [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;
console.log('准备发送消息:', inputMessage);
console.log('当前会话ID:', chatId);
// 获取知识库ID列表
let dataset_id_list = [];
// 优先使用组件状态中保存的知识库列表
if (selectedKnowledgeBaseIds.length > 0) {
// 使用已保存的知识库列表
dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, ''));
console.log('使用组件状态中的知识库列表:', dataset_id_list);
} else if (conversation && conversation.datasets && conversation.datasets.length > 0) {
// 如果已有会话,使用会话中的知识库
dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, ''));
console.log('使用会话中的知识库列表:', dataset_id_list);
} else if (knowledgeBaseId) {
// 如果是新会话,使用当前选择的知识库
// 可能是单个ID或以逗号分隔的多个ID
const ids = knowledgeBaseId.split(',').map((id) => id.trim().replace(/-/g, ''));
dataset_id_list = ids;
console.log('使用URL参数中的知识库:', dataset_id_list);
} else if (availableDatasets.length > 0) {
// 如果都没有,尝试使用可用知识库列表中的第一个
dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')];
console.log('使用可用知识库列表中的第一个:', dataset_id_list);
}
if (dataset_id_list.length === 0) {
dispatch(
showNotification({
message: '发送失败:未选择知识库',
type: 'danger',
})
);
return;
}
console.log('发送消息参数:', {
dataset_id_list,
question: inputMessage,
conversation_id: chatId,
});
// 发送消息到服务器
dispatch(
createChatRecord({
dataset_id_list: dataset_id_list,
question: inputMessage,
conversation_id: chatId,
})
)
.unwrap()
.then((response) => {
// 成功发送后,可以执行任何需要的操作
console.log('消息发送成功:', response);
})
.catch((error) => {
// 发送失败,显示错误信息
console.error('消息发送失败:', 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.role === 'user' ? (
message.content
) : (
<SafeMarkdown content={message.content} />
)}
{message.is_streaming && (
<span className='streaming-indicator'>
<span className='dot dot1'></span>
<span className='dot dot2'></span>
<span className='dot dot3'></span>
</span>
)}
</div>
</div>
<div className='message-time small text-muted mt-1'>
{message.created_at &&
(() => {
const messageDate = new Date(message.created_at);
const today = new Date();
// 检查是否是今天
const isToday =
messageDate.getDate() === today.getDate() &&
messageDate.getMonth() === today.getMonth() &&
messageDate.getFullYear() === today.getFullYear();
// 如果是今天,只显示时间;否则显示年月日和时间
if (isToday) {
return messageDate.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
});
} else {
return messageDate.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
})()}
{message.is_streaming && ' · 正在生成...'}
</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>
);
}