knowledgebase_influencer/src/pages/Chat/ChatWindow.jsx

526 lines
24 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 { resetMessageOperation, 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 ChatSidePanel from './ChatSidePanel';
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来跟踪已加载的会话
// Gmail无邮件警告状态
const [noEmailsWarning, setNoEmailsWarning] = useState(null);
const [troubleshooting, setTroubleshooting] = useState(null);
// 从 Redux store 获取聊天数据
const chats = useSelector((state) => state.chat.chats.items);
const currentChat = chats.find((chat) => chat.conversation_id === chatId);
const messages = currentChat?.messages || [];
// 获取消息操作状态
const messageStatus = useSelector((state) => state.chat.messageOperation.status);
const messageError = useSelector((state) => state.chat.messageOperation.error);
// 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);
// 使用新的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');
// 获取当前活跃聊天ID
const activeConversationId = useSelector((state) => state.chat.activeConversationId);
// 聊天操作状态
const chatOperationStatus = useSelector((state) => state.chat.chatOperation.status);
// 提取达人邮箱信息(用于侧边栏功能)
const [talentEmail, setTalentEmail] = useState('');
// 监听知识库ID变更确保保存在组件状态中
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]);
// 检查是否存在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]);
// 当currentChat或knowledgeBaseId更新时更新selectedKnowledgeBaseIds
useEffect(() => {
// 优先使用currentChat中的知识库列表
if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
const datasetIds = currentChat.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);
}
}, [currentChat, knowledgeBaseId]);
// 获取聊天详情
useEffect(() => {
if (!chatId) return;
// 如果已经加载过这个chatId的详情不再重复加载
if (hasLoadedDetailRef.current[chatId]) {
console.log('跳过已加载过的会话详情:', chatId);
return;
}
// 检查是否是已存在的聊天
const existingChat = chats.find((chat) => chat.conversation_id === chatId);
// 如果已经有这个聊天的消息,则不需要获取详情
if (existingChat && existingChat.messages && existingChat.messages.length > 0) {
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);
});
}, [chatId, dispatch, chats]);
// 组件销毁时完全清空ref缓存
useEffect(() => {
return () => {
hasLoadedDetailRef.current = {};
};
}, []);
// 新会话自动添加欢迎消息
useEffect(() => {
// 如果是新聊天且没有任何消息,添加一条系统欢迎消息
if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') {
// 检查是否有无邮件警告作为首条消息
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({
conversationId: chatId,
message: {
id: 'gmail-warning-' + Date.now(),
role: 'assistant',
content: `⚠️ ${noEmailsMessage}\n\n您仍然可以在此聊天中提问,但可能无法获得与邮件内容相关的回答。`,
created_at: new Date().toISOString(),
},
})
);
// 移除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({
conversationId: chatId,
message: {
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 (messageStatus === 'failed' && messageError) {
dispatch(
showNotification({
message: `发送失败: ${messageError}`,
type: 'danger',
})
);
dispatch(resetMessageOperation());
}
}, [messageStatus, messageError, dispatch]);
// 滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 获取当前会话的知识库信息
useEffect(() => {
// 如果currentChat有数据集信息优先使用它
if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
return;
}
// 如果没有会话数据集信息但有knowledgeBaseId尝试从知识库列表中查找
if (knowledgeBaseId && knowledgeBases.length === 0 && !availableDatasets.length) {
dispatch(fetchAvailableDatasets());
}
}, [dispatch, knowledgeBaseId, knowledgeBases, currentChat, availableDatasets]);
useEffect(() => {
// 尝试从聊天数据或知识库中提取达人邮箱
if (currentChat?.talent_email) {
setTalentEmail(currentChat.talent_email);
} else if (currentChat?.datasets?.[0]?.talent_email) {
setTalentEmail(currentChat.datasets[0].talent_email);
} else if (currentChat?.datasets?.[0]?.name && currentChat?.datasets[0]?.name.includes('@')) {
// 如果知识库名称中包含邮箱格式,提取出来
const emailMatch = currentChat.datasets[0].name.match(/[\w.-]+@[\w.-]+\.\w+/);
if (emailMatch) {
setTalentEmail(emailMatch[0]);
}
} else if (messages.length > 0) {
// 从消息中查找可能包含的达人邮箱
for (const message of messages) {
const emailMatch = message.content?.match(/[\w.-]+@[\w.-]+\.\w+/);
if (emailMatch) {
setTalentEmail(emailMatch[0]);
break;
}
}
}
}, [currentChat, messages]);
const handleSendMessage = (e) => {
e.preventDefault();
if (!inputMessage.trim() || messageStatus === '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 (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
// 如果已有会话,使用会话中的知识库
dataset_id_list = currentChat.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='d-flex h-100'>
{/* Main Chat Area */}
<div className='chat-window d-flex flex-column h-100 flex-grow-1'>
{/* Chat header */}
<div className='p-3 border-bottom'>
{currentChat && currentChat.datasets ? (
<>
<h5 className='mb-0'>{currentChat.datasets.map((dataset) => dataset.name).join(', ')}</h5>
{currentChat.datasets.length > 0 && currentChat.datasets[0].type && (
<small className='text-muted'>类型: {currentChat.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' && !messages.length
? renderLoading()
: messageStatus === 'failed' && !messages.length
? 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={messageStatus === 'loading'}
/>
<button
type='submit'
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
disabled={messageStatus === 'loading' || !inputMessage.trim()}
>
<SvgIcon className='send' color='#ffffff' />
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
发送
</span>
</button>
</form>
</div>
</div>
{/* Right Side Panel */}
<div
className='chat-side-panel-container border-start'
style={{ width: '350px', height: '100%', overflow: 'hidden' }}
>
<ChatSidePanel chatId={chatId} talentEmail={talentEmail} />
</div>
</div>
);
}