mirror of
https://github.com/Funkoala14/knowledgebase_influencer.git
synced 2025-06-08 03:08:14 +08:00
Compare commits
2 Commits
97203b4bcd
...
6a654950a5
Author | SHA1 | Date | |
---|---|---|---|
6a654950a5 | |||
2b89db7301 |
@ -1,83 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { switchToMockApi, switchToRealApi, checkServerStatus } from '../services/api';
|
||||
|
||||
export default function ApiModeSwitch() {
|
||||
const [isMockMode, setIsMockMode] = useState(false);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [showNotification, setShowNotification] = useState(false);
|
||||
const [notification, setNotification] = useState({ message: '', type: 'info' });
|
||||
|
||||
// 组件加载时检查服务器状态
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
setIsChecking(true);
|
||||
const isServerUp = await checkServerStatus();
|
||||
setIsMockMode(!isServerUp);
|
||||
setIsChecking(false);
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
}, []);
|
||||
|
||||
// 切换API模式
|
||||
const handleToggleMode = async () => {
|
||||
setIsChecking(true);
|
||||
|
||||
if (isMockMode) {
|
||||
// 尝试切换回真实API
|
||||
const isServerUp = await switchToRealApi();
|
||||
if (isServerUp) {
|
||||
setIsMockMode(false);
|
||||
showNotificationMessage('已切换到真实API模式', 'success');
|
||||
} else {
|
||||
showNotificationMessage('服务器连接失败,继续使用模拟数据', 'warning');
|
||||
}
|
||||
} else {
|
||||
// 切换到模拟API
|
||||
switchToMockApi();
|
||||
setIsMockMode(true);
|
||||
showNotificationMessage('已切换到模拟API模式', 'info');
|
||||
}
|
||||
|
||||
setIsChecking(false);
|
||||
};
|
||||
|
||||
// 显示通知消息
|
||||
const showNotificationMessage = (message, type) => {
|
||||
setNotification({ message, type });
|
||||
setShowNotification(true);
|
||||
|
||||
// 3秒后自动隐藏通知
|
||||
setTimeout(() => {
|
||||
setShowNotification(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='api-mode-switch'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<div className='form-check form-switch me-2'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
id='apiModeToggle'
|
||||
checked={isMockMode}
|
||||
onChange={handleToggleMode}
|
||||
disabled={isChecking}
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='apiModeToggle'>
|
||||
{isChecking ? '检查中...' : isMockMode ? '模拟API模式' : '真实API模式'}
|
||||
</label>
|
||||
</div>
|
||||
{isMockMode && <span className='badge bg-warning text-dark'>使用本地模拟数据</span>}
|
||||
{!isMockMode && <span className='badge bg-success'>已连接到后端服务器</span>}
|
||||
</div>
|
||||
|
||||
{showNotification && (
|
||||
<div className={`alert alert-${notification.type} mt-2 py-2 px-3`} style={{ fontSize: '0.85rem' }}>
|
||||
{notification.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchChats, deleteChat, createChatRecord, createConversation } from '../../store/chat/chat.thunks';
|
||||
import { fetchChats, deleteChat, createConversation } from '../../store/chat/chat.thunks';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import ChatSidebar from './ChatSidebar';
|
||||
import NewChat from './NewChat';
|
||||
@ -17,12 +17,13 @@ export default function Chat() {
|
||||
items: chatHistory,
|
||||
status,
|
||||
error,
|
||||
} = useSelector((state) => state.chat.history || { items: [], status: 'idle', error: null });
|
||||
const operationStatus = useSelector((state) => state.chat.createSession?.status);
|
||||
const operationError = useSelector((state) => state.chat.createSession?.error);
|
||||
} = useSelector((state) => state.chat.chats || { items: [], status: 'idle', error: null });
|
||||
const operationStatus = useSelector((state) => state.chat.chatOperation?.status);
|
||||
const operationError = useSelector((state) => state.chat.chatOperation?.error);
|
||||
|
||||
// 获取聊天记录列表
|
||||
useEffect(() => {
|
||||
console.log(chatHistory);
|
||||
dispatch(fetchChats({ page: 1, page_size: 20 }));
|
||||
}, [dispatch]);
|
||||
|
||||
|
252
src/pages/Chat/ChatSidePanel.jsx
Normal file
252
src/pages/Chat/ChatSidePanel.jsx
Normal file
@ -0,0 +1,252 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setUserGoal, getConversationSummary, getRecommendedReply } from '../../store/talentChat/talentChat.thunks';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import SafeMarkdown from '../../components/SafeMarkdown';
|
||||
|
||||
export default function ChatSidePanel({ chatId, talentEmail }) {
|
||||
const dispatch = useDispatch();
|
||||
const [goal, setGoal] = useState('');
|
||||
const [loading, setLoading] = useState({
|
||||
goal: false,
|
||||
summary: false,
|
||||
reply: false,
|
||||
});
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
|
||||
// Get data from Redux store
|
||||
const userGoal = useSelector((state) => state.talentChat?.userGoal?.data);
|
||||
const conversationSummary = useSelector((state) => state.talentChat?.conversationSummary?.data);
|
||||
const recommendedReply = useSelector((state) => state.talentChat?.recommendedReply?.data);
|
||||
|
||||
// Load conversation summary and recommended reply when component mounts
|
||||
useEffect(() => {
|
||||
if (talentEmail && chatId) {
|
||||
handleGetSummary();
|
||||
handleGetRecommendedReply();
|
||||
}
|
||||
}, [chatId, talentEmail]);
|
||||
|
||||
// Reset copy success state after 2 seconds
|
||||
useEffect(() => {
|
||||
if (copySuccess) {
|
||||
const timer = setTimeout(() => {
|
||||
setCopySuccess(false);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [copySuccess]);
|
||||
|
||||
const handleGoalSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!goal.trim()) return;
|
||||
|
||||
setLoading((prev) => ({ ...prev, goal: true }));
|
||||
try {
|
||||
await dispatch(setUserGoal(goal)).unwrap();
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '聊天目标设置成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `设置目标失败: ${error}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, goal: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetSummary = async () => {
|
||||
if (!talentEmail) return;
|
||||
|
||||
setLoading((prev) => ({ ...prev, summary: true }));
|
||||
try {
|
||||
await dispatch(getConversationSummary(talentEmail)).unwrap();
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `获取聊天总结失败: ${error}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, summary: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetRecommendedReply = async () => {
|
||||
if (!chatId || !talentEmail) return;
|
||||
|
||||
setLoading((prev) => ({ ...prev, reply: true }));
|
||||
try {
|
||||
await dispatch(
|
||||
getRecommendedReply({
|
||||
conversation_id: chatId,
|
||||
talent_email: talentEmail,
|
||||
})
|
||||
).unwrap();
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `获取推荐话术失败: ${error}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, reply: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyReply = () => {
|
||||
// Get reply text from the appropriate property path
|
||||
const replyText = recommendedReply?.reply || '';
|
||||
|
||||
if (replyText) {
|
||||
navigator.clipboard
|
||||
.writeText(replyText)
|
||||
.then(() => {
|
||||
setCopySuccess(true);
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '已复制到剪贴板',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('复制失败:', err);
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '复制失败,请手动复制',
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='chat-side-panel d-flex flex-column h-100'>
|
||||
{/* 聊天目标 */}
|
||||
<div className='panel-section p-3 border-bottom' style={{ flex: '0 0 auto' }}>
|
||||
<h5 className='section-title mb-3'>
|
||||
<span className='bg-warning bg-opacity-25 px-2 py-1 rounded'>聊天目标</span>
|
||||
</h5>
|
||||
<form onSubmit={handleGoalSubmit}>
|
||||
<textarea
|
||||
className='form-control mb-2'
|
||||
rows='3'
|
||||
placeholder='请输入聊天目标...'
|
||||
value={goal}
|
||||
onChange={(e) => setGoal(e.target.value)}
|
||||
></textarea>
|
||||
<button type='submit' className='btn btn-sm btn-primary' disabled={loading.goal || !goal.trim()}>
|
||||
{loading.goal ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
提交中...
|
||||
</>
|
||||
) : (
|
||||
'设置目标'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
{userGoal && (
|
||||
<div className='mt-2 p-2 bg-light rounded'>
|
||||
<small className='fw-bold'>当前目标:</small>
|
||||
<p className='mb-0 small'>
|
||||
{userGoal.content ||
|
||||
(typeof userGoal === 'string' ? userGoal : userGoal?.data?.content || '')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 历史聊天记录总结 */}
|
||||
<div className='panel-section p-3 border-bottom' style={{ flex: '1 0 50%', overflowY: 'auto' }}>
|
||||
<h5 className='section-title mb-3'>
|
||||
<span className='bg-warning bg-opacity-25 px-2 py-1 rounded'>历史聊天记录总结</span>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-secondary ms-2'
|
||||
onClick={handleGetSummary}
|
||||
disabled={loading.summary || !talentEmail}
|
||||
>
|
||||
{loading.summary ? (
|
||||
<span className='spinner-border spinner-border-sm' role='status' aria-hidden='true'></span>
|
||||
) : (
|
||||
'刷新'
|
||||
)}
|
||||
</button>
|
||||
</h5>
|
||||
{loading.summary ? (
|
||||
<div className='text-center py-3'>
|
||||
<div className='spinner-border text-secondary' role='status'>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
<p className='mt-2 text-muted'>获取聊天总结...</p>
|
||||
</div>
|
||||
) : conversationSummary?.summary ? (
|
||||
<div className='summary-content'>
|
||||
<SafeMarkdown content={conversationSummary.summary} />
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted'>暂无聊天总结</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 推荐聊天话术 */}
|
||||
<div className='panel-section p-3' style={{ flex: '1 0 50%', overflowY: 'auto' }}>
|
||||
<h5 className='section-title mb-3'>
|
||||
<span className='bg-warning bg-opacity-25 px-2 py-1 rounded'>推荐聊天话术</span>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-secondary ms-2'
|
||||
onClick={handleGetRecommendedReply}
|
||||
disabled={loading.reply || !chatId || !talentEmail}
|
||||
>
|
||||
{loading.reply ? (
|
||||
<span className='spinner-border spinner-border-sm' role='status' aria-hidden='true'></span>
|
||||
) : (
|
||||
'刷新'
|
||||
)}
|
||||
</button>
|
||||
</h5>
|
||||
{loading.reply ? (
|
||||
<div className='text-center py-3'>
|
||||
<div className='spinner-border text-secondary' role='status'>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
<p className='mt-2 text-muted'>生成话术中...</p>
|
||||
</div>
|
||||
) : recommendedReply?.reply || recommendedReply?.status === 'success' ? (
|
||||
<div
|
||||
className='recommended-reply bg-light p-3 rounded position-relative'
|
||||
style={{ whiteSpace: 'pre-line', fontFamily: 'Arial, sans-serif' }}
|
||||
>
|
||||
<SafeMarkdown content={recommendedReply.reply} />
|
||||
<div className='position-absolute top-0 end-0 m-2'>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-primary'
|
||||
onClick={handleCopyReply}
|
||||
title='复制话术内容'
|
||||
>
|
||||
{copySuccess ? '已复制 ✓' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted'>暂无推荐话术</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
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 { 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 }) {
|
||||
@ -16,11 +17,23 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
const messagesEndRef = useRef(null);
|
||||
const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话
|
||||
|
||||
// 从 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);
|
||||
// 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 || []);
|
||||
@ -31,23 +44,55 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
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);
|
||||
// 获取当前活跃聊天ID
|
||||
const activeConversationId = useSelector((state) => state.chat.activeConversationId);
|
||||
|
||||
// 获取会话创建状态
|
||||
const createSessionStatus = useSelector((state) => state.chat.createSession?.status);
|
||||
const createSessionId = useSelector((state) => state.chat.createSession?.sessionId);
|
||||
// 聊天操作状态
|
||||
const chatOperationStatus = useSelector((state) => state.chat.chatOperation.status);
|
||||
|
||||
// 提取达人邮箱信息(用于侧边栏功能)
|
||||
const [talentEmail, setTalentEmail] = useState('');
|
||||
|
||||
// 监听知识库ID变更,确保保存在组件状态中
|
||||
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]);
|
||||
|
||||
// 当conversation或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds
|
||||
// 检查是否存在Gmail无邮件警告
|
||||
useEffect(() => {
|
||||
// 优先使用conversation中的知识库列表
|
||||
if (conversation && conversation.datasets && conversation.datasets.length > 0) {
|
||||
const datasetIds = conversation.datasets.map((ds) => ds.id);
|
||||
// 优先使用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);
|
||||
}
|
||||
@ -58,7 +103,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
console.log('从URL参数中获取知识库列表:', ids);
|
||||
setSelectedKnowledgeBaseIds(ids);
|
||||
}
|
||||
}, [conversation, knowledgeBaseId]);
|
||||
}, [currentChat, knowledgeBaseId]);
|
||||
|
||||
// 获取聊天详情
|
||||
useEffect(() => {
|
||||
@ -70,12 +115,12 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是新创建的会话
|
||||
const isNewlyCreatedChat = createSessionStatus === 'succeeded' && createSessionId === chatId;
|
||||
// 检查是否是已存在的聊天
|
||||
const existingChat = chats.find((chat) => chat.conversation_id === chatId);
|
||||
|
||||
// 如果是新创建的会话且已经有会话数据,则跳过详情获取
|
||||
if (isNewlyCreatedChat && conversation && conversation.conversation_id === chatId) {
|
||||
console.log('跳过新创建会话的详情获取:', chatId);
|
||||
// 如果已经有这个聊天的消息,则不需要获取详情
|
||||
if (existingChat && existingChat.messages && existingChat.messages.length > 0) {
|
||||
console.log('聊天已存在且有消息,跳过详情获取:', chatId);
|
||||
hasLoadedDetailRef.current[chatId] = true;
|
||||
return;
|
||||
}
|
||||
@ -102,14 +147,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// 组件卸载时清空消息
|
||||
return () => {
|
||||
dispatch(resetMessages());
|
||||
// 不要清空hasLoadedDetailRef,否则会导致重复加载
|
||||
// hasLoadedDetailRef.current = {}; // 清理ref缓存
|
||||
};
|
||||
}, [chatId, dispatch, createSessionStatus, createSessionId]);
|
||||
}, [chatId, dispatch, chats]);
|
||||
|
||||
// 组件销毁时完全清空ref缓存
|
||||
useEffect(() => {
|
||||
@ -122,32 +160,81 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
useEffect(() => {
|
||||
// 如果是新聊天且没有任何消息,添加一条系统欢迎消息
|
||||
if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') {
|
||||
const selectedKb = knowledgeBase ||
|
||||
availableDatasets.find((ds) => ds.id === knowledgeBaseId) || { name: '知识库' };
|
||||
// 检查是否有无邮件警告作为首条消息
|
||||
let isNoEmailsFirstMessage = false;
|
||||
let noEmailsMessage = null;
|
||||
|
||||
dispatch(
|
||||
addMessage({
|
||||
id: 'welcome-' + Date.now(),
|
||||
role: 'assistant',
|
||||
content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
);
|
||||
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 (sendStatus === 'failed' && sendError) {
|
||||
if (messageStatus === 'failed' && messageError) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `发送失败: ${sendError}`,
|
||||
message: `发送失败: ${messageError}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
dispatch(resetSendMessageStatus());
|
||||
dispatch(resetMessageOperation());
|
||||
}
|
||||
}, [sendStatus, sendError, dispatch]);
|
||||
}, [messageStatus, messageError, dispatch]);
|
||||
|
||||
// 滚动到底部
|
||||
useEffect(() => {
|
||||
@ -156,8 +243,8 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
|
||||
// 获取当前会话的知识库信息
|
||||
useEffect(() => {
|
||||
// 如果conversation有数据集信息,优先使用它
|
||||
if (conversation && conversation.datasets && conversation.datasets.length > 0) {
|
||||
// 如果currentChat有数据集信息,优先使用它
|
||||
if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -165,12 +252,36 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
if (knowledgeBaseId && knowledgeBases.length === 0 && !availableDatasets.length) {
|
||||
dispatch(fetchAvailableDatasets());
|
||||
}
|
||||
}, [dispatch, knowledgeBaseId, knowledgeBases, conversation, availableDatasets]);
|
||||
}, [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() || sendStatus === 'loading') return;
|
||||
if (!inputMessage.trim() || messageStatus === 'loading') return;
|
||||
|
||||
console.log('准备发送消息:', inputMessage);
|
||||
console.log('当前会话ID:', chatId);
|
||||
@ -183,9 +294,9 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
// 使用已保存的知识库列表
|
||||
dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, ''));
|
||||
console.log('使用组件状态中的知识库列表:', dataset_id_list);
|
||||
} else if (conversation && conversation.datasets && conversation.datasets.length > 0) {
|
||||
} else if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
|
||||
// 如果已有会话,使用会话中的知识库
|
||||
dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, ''));
|
||||
dataset_id_list = currentChat.datasets.map((ds) => ds.id.replace(/-/g, ''));
|
||||
console.log('使用会话中的知识库列表:', dataset_id_list);
|
||||
} else if (knowledgeBaseId) {
|
||||
// 如果是新会话,使用当前选择的知识库
|
||||
@ -278,125 +389,136 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
};
|
||||
|
||||
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>
|
||||
<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'
|
||||
? 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`}
|
||||
>
|
||||
{/* 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
|
||||
className={`chat-message p-3 rounded-3 ${
|
||||
message.role === 'user' ? 'bg-dark text-white' : 'bg-light'
|
||||
}`}
|
||||
style={{
|
||||
maxWidth: '75%',
|
||||
position: 'relative',
|
||||
}}
|
||||
key={message.id}
|
||||
className={`d-flex ${
|
||||
message.role === 'user' ? 'align-items-end' : 'align-items-start'
|
||||
} mb-3 flex-column`}
|
||||
>
|
||||
<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
|
||||
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 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();
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// 如果是今天,只显示时间;否则显示年月日和时间
|
||||
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} />
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import { fetchAvailableDatasets, fetchChats, createConversation } from '../../store/chat/chat.thunks';
|
||||
import { setupGmailChat } from '../../store/gmailChat/gmailChat.thunks';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
|
||||
export default function NewChat() {
|
||||
@ -11,15 +12,26 @@ export default function NewChat() {
|
||||
const [selectedDatasetIds, setSelectedDatasetIds] = useState([]);
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
// Gmail集成相关状态
|
||||
const [talentEmail, setTalentEmail] = useState('');
|
||||
const [showAuthCodeInput, setShowAuthCodeInput] = useState(false);
|
||||
const [authCode, setAuthCode] = useState('');
|
||||
const [authUrl, setAuthUrl] = useState('');
|
||||
const [isGmailLoading, setIsGmailLoading] = useState(false);
|
||||
|
||||
// 从 Redux store 获取可用知识库数据
|
||||
const datasets = useSelector((state) => state.chat.availableDatasets.items || []);
|
||||
const isLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
|
||||
const error = useSelector((state) => state.chat.availableDatasets.error);
|
||||
|
||||
// 获取聊天历史记录
|
||||
const chatHistory = useSelector((state) => state.chat.history.items || []);
|
||||
const chatHistoryLoading = useSelector((state) => state.chat.history.status === 'loading');
|
||||
const chatCreationStatus = useSelector((state) => state.chat.sendMessage?.status);
|
||||
const chatHistory = useSelector((state) => state.chat.chats.items || []);
|
||||
const chatHistoryLoading = useSelector((state) => state.chat.chats.status === 'loading');
|
||||
const chatCreationStatus = useSelector((state) => state.chat.messageOperation?.status);
|
||||
|
||||
// Gmail集成状态
|
||||
const gmailSetupStatus = useSelector((state) => state.gmailChat?.setup?.status);
|
||||
const gmailSetupError = useSelector((state) => state.gmailChat?.setup?.error);
|
||||
|
||||
// 获取可用知识库列表和聊天历史
|
||||
useEffect(() => {
|
||||
@ -57,6 +69,157 @@ export default function NewChat() {
|
||||
});
|
||||
};
|
||||
|
||||
// 处理Gmail集成
|
||||
const handleGmailIntegration = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!talentEmail) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '请输入达人邮箱',
|
||||
type: 'warning',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGmailLoading(true);
|
||||
try {
|
||||
// 调用Gmail设置API
|
||||
const response = await dispatch(setupGmailChat(talentEmail)).unwrap();
|
||||
|
||||
// 如果是没有邮件往来的情况
|
||||
if (response.message && response.message.includes('没有找到') && response.knowledge_base_id) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: response.message,
|
||||
type: 'warning',
|
||||
})
|
||||
);
|
||||
|
||||
// 使用获取的知识库ID创建新会话
|
||||
try {
|
||||
const conversationResponse = await dispatch(
|
||||
createConversation({
|
||||
dataset_id_list: [response.knowledge_base_id],
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
console.log('使用Gmail知识库创建会话成功:', conversationResponse);
|
||||
|
||||
if (conversationResponse && conversationResponse.conversation_id) {
|
||||
// 保存警告信息到localStorage
|
||||
localStorage.setItem(
|
||||
'gmailNoEmailsWarning',
|
||||
JSON.stringify({
|
||||
message: response.message,
|
||||
troubleshooting: response.troubleshooting || null,
|
||||
chatId: conversationResponse.conversation_id,
|
||||
timestamp: Date.now(),
|
||||
isFirstMessage: true, // 标记这是首次消息
|
||||
})
|
||||
);
|
||||
|
||||
// 导航到新会话
|
||||
navigate(`/chat/${response.knowledge_base_id}/${conversationResponse.conversation_id}`);
|
||||
} else {
|
||||
throw new Error('未能获取会话ID');
|
||||
}
|
||||
} catch (convError) {
|
||||
console.error('创建会话失败:', convError);
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `创建会话失败: ${convError.message || '请重试'}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
// 如果成功返回知识库ID和会话ID
|
||||
else if (response && response.data && response.data.knowledge_base_id && response.data.conversation_id) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: response.message || 'Gmail集成成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
// 导航到新会话
|
||||
navigate(`/chat/${response.data.knowledge_base_id}/${response.data.conversation_id}`);
|
||||
} else if (response && response.status === 'authorization_required' && response.auth_url) {
|
||||
// 需要授权
|
||||
setAuthUrl(response.auth_url);
|
||||
setShowAuthCodeInput(true);
|
||||
// 打开授权URL
|
||||
window.open(response.auth_url, '_blank');
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '需要Gmail授权,请在新打开的窗口中完成授权并获取授权码',
|
||||
type: 'info',
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gmail集成失败:', error);
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `Gmail集成失败: ${error.message || '请重试'}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsGmailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理授权码提交
|
||||
const handleAuthCodeSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!authCode) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '请输入授权码',
|
||||
type: 'warning',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGmailLoading(true);
|
||||
try {
|
||||
// 使用授权码再次调用Gmail设置API
|
||||
const response = await dispatch(
|
||||
setupGmailChat({
|
||||
talent_gmail: talentEmail,
|
||||
auth_code: authCode,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
if (response && response.data && response.data.knowledge_base_id && response.data.conversation_id) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: response.message || 'Gmail集成成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
// 导航到新会话
|
||||
navigate(`/chat/${response.data.knowledge_base_id}/${response.data.conversation_id}`);
|
||||
} else {
|
||||
throw new Error('未能获取会话信息');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('授权码提交失败:', error);
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `授权码提交失败: ${error.message || '请重试'}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsGmailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始聊天
|
||||
const handleStartChat = async () => {
|
||||
if (selectedDatasetIds.length === 0) {
|
||||
@ -188,6 +351,121 @@ export default function NewChat() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gmail集成模块 */}
|
||||
<div className='card mb-5 shadow-sm'>
|
||||
<div className='card-body'>
|
||||
<h4 className='card-title mb-4'>Gmail邮件集成</h4>
|
||||
|
||||
{!showAuthCodeInput ? (
|
||||
<form onSubmit={handleGmailIntegration} className='mb-3'>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='talentEmail' className='form-label'>
|
||||
达人邮箱地址
|
||||
</label>
|
||||
<div className='input-group'>
|
||||
<input
|
||||
type='email'
|
||||
className='form-control'
|
||||
id='talentEmail'
|
||||
placeholder='请输入正在联络的达人邮箱'
|
||||
value={talentEmail}
|
||||
onChange={(e) => setTalentEmail(e.target.value)}
|
||||
disabled={isGmailLoading}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
className='btn btn-outline-dark'
|
||||
type='submit'
|
||||
disabled={isGmailLoading || !talentEmail}
|
||||
>
|
||||
{isGmailLoading ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
处理中...
|
||||
</>
|
||||
) : (
|
||||
'创建邮件会话'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className='form-text'>将创建一个包含您与该达人Gmail邮件沟通记录的知识库及会话</div>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={handleAuthCodeSubmit} className='mb-3'>
|
||||
<div className='alert alert-info mb-3'>
|
||||
<p className='mb-2'>
|
||||
<strong>需要Gmail授权</strong>
|
||||
</p>
|
||||
<p className='mb-2'>系统已在新窗口打开Gmail授权页面,请完成授权并获取授权码。</p>
|
||||
<p className='mb-0'>
|
||||
<a
|
||||
href={authUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='btn btn-sm btn-outline-primary'
|
||||
>
|
||||
重新打开授权页面
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='authCode' className='form-label'>
|
||||
授权码
|
||||
</label>
|
||||
<div className='input-group'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='authCode'
|
||||
placeholder='请输入Gmail授权页面提供的授权码'
|
||||
value={authCode}
|
||||
onChange={(e) => setAuthCode(e.target.value)}
|
||||
disabled={isGmailLoading}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
className='btn btn-outline-success'
|
||||
type='submit'
|
||||
disabled={isGmailLoading || !authCode}
|
||||
>
|
||||
{isGmailLoading ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
处理中...
|
||||
</>
|
||||
) : (
|
||||
'提交授权码'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-link ps-0'
|
||||
onClick={() => {
|
||||
setShowAuthCodeInput(false);
|
||||
setAuthCode('');
|
||||
setAuthUrl('');
|
||||
}}
|
||||
>
|
||||
« 返回输入达人邮箱
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='d-flex justify-content-between align-items-center mb-4'>
|
||||
<h4 className='m-0'>选择知识库开始聊天</h4>
|
||||
<button
|
||||
|
@ -15,7 +15,6 @@ import AccessRequestModal from '../../components/AccessRequestModal';
|
||||
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
|
||||
import Pagination from '../../components/Pagination';
|
||||
import SearchBar from '../../components/SearchBar';
|
||||
import ApiModeSwitch from '../../components/ApiModeSwitch';
|
||||
|
||||
// 导入拆分的组件
|
||||
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
||||
@ -461,9 +460,6 @@ export default function KnowledgeBase() {
|
||||
|
||||
return (
|
||||
<div className='knowledge-base container my-4'>
|
||||
{/* <div className='api-mode-control mb-3'>
|
||||
<ApiModeSwitch />
|
||||
</div> */}
|
||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||
<SearchBar
|
||||
searchKeyword={searchKeyword}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { mockGet, mockPost, mockPut, mockDelete } from './mockApi';
|
||||
|
||||
const secretKey = import.meta.env.VITE_SECRETKEY;
|
||||
|
||||
@ -42,13 +41,6 @@ api.interceptors.response.use(
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// 处理服务器无法连接的情况
|
||||
if (!error.response || error.code === 'ECONNABORTED' || error.message.includes('Network Error')) {
|
||||
console.error('Server appears to be down. Switching to mock data.');
|
||||
isServerDown = true;
|
||||
hasCheckedServer = true;
|
||||
}
|
||||
|
||||
// Handle errors in the response
|
||||
if (error.response) {
|
||||
// monitor /verify
|
||||
@ -74,52 +66,20 @@ api.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
// 检查服务器状态
|
||||
export const checkServerStatus = async () => {
|
||||
try {
|
||||
// await api.get('/health-check', { timeout: 3000 });
|
||||
isServerDown = false;
|
||||
hasCheckedServer = true;
|
||||
console.log('Server connection established');
|
||||
return true;
|
||||
} catch (error) {
|
||||
isServerDown = true;
|
||||
hasCheckedServer = true;
|
||||
console.error('Server connection failed, using mock data');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始检查服务器状态
|
||||
checkServerStatus();
|
||||
|
||||
// Define common HTTP methods with fallback to mock API
|
||||
// Define common HTTP methods
|
||||
const get = async (url, params = {}) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] GET ${url}`);
|
||||
return await mockGet(url, params);
|
||||
}
|
||||
|
||||
const res = await api.get(url, { ...params });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
||||
console.log(`Failed to connect to server. Falling back to mock API for GET ${url}`);
|
||||
return await mockGet(url, params);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle POST requests for JSON data with fallback to mock API
|
||||
// Handle POST requests for JSON data
|
||||
const post = async (url, data, isMultipart = false) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] POST ${url}`);
|
||||
return await mockPost(url, data);
|
||||
}
|
||||
|
||||
const headers = isMultipart
|
||||
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
||||
: { 'Content-Type': 'application/json' }; // For JSON data
|
||||
@ -127,61 +87,34 @@ const post = async (url, data, isMultipart = false) => {
|
||||
const res = await api.post(url, data, { headers });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
||||
console.log(`Failed to connect to server. Falling back to mock API for POST ${url}`);
|
||||
return await mockPost(url, data);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle PUT requests with fallback to mock API
|
||||
// Handle PUT requests
|
||||
const put = async (url, data) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] PUT ${url}`);
|
||||
return await mockPut(url, data);
|
||||
}
|
||||
|
||||
const res = await api.put(url, data, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
||||
console.log(`Failed to connect to server. Falling back to mock API for PUT ${url}`);
|
||||
return await mockPut(url, data);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle DELETE requests with fallback to mock API
|
||||
// Handle DELETE requests
|
||||
const del = async (url) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] DELETE ${url}`);
|
||||
return await mockDelete(url);
|
||||
}
|
||||
|
||||
const res = await api.delete(url);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
||||
console.log(`Failed to connect to server. Falling back to mock API for DELETE ${url}`);
|
||||
return await mockDelete(url);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const upload = async (url, data) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] Upload ${url}`);
|
||||
return await mockPost(url, data, true);
|
||||
}
|
||||
|
||||
const axiosInstance = await axios.create({
|
||||
baseURL: '/api',
|
||||
headers: {
|
||||
@ -191,41 +124,12 @@ const upload = async (url, data) => {
|
||||
const res = await axiosInstance.post(url, data);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
||||
console.log(`Failed to connect to server. Falling back to mock API for Upload ${url}`);
|
||||
return await mockPost(url, data, true);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 手动切换到模拟API(为调试目的)
|
||||
export const switchToMockApi = () => {
|
||||
isServerDown = true;
|
||||
hasCheckedServer = true;
|
||||
console.log('Manually switched to mock API');
|
||||
};
|
||||
|
||||
// 手动切换回真实API
|
||||
export const switchToRealApi = async () => {
|
||||
// 重新检查服务器状态
|
||||
const isServerUp = await checkServerStatus();
|
||||
console.log(isServerUp ? 'Switched back to real API' : 'Server still down, continuing with mock API');
|
||||
return isServerUp;
|
||||
};
|
||||
|
||||
// Handle streaming requests
|
||||
const streamRequest = async (url, data, onChunk, onError) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] STREAM ${url}`);
|
||||
// 模拟流式响应
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"这是模拟的","conversation_id":"mock-1234"}}'), 300);
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"流式","conversation_id":"mock-1234"}}'), 600);
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"响应","conversation_id":"mock-1234"}}'), 900);
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"数据","conversation_id":"mock-1234","is_end":true}}'), 1200);
|
||||
return { success: true, conversation_id: 'mock-1234' };
|
||||
}
|
||||
|
||||
// 获取认证Token
|
||||
const encryptedToken = sessionStorage.getItem('token') || '';
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,18 +3,20 @@ import { get, post } from '../../services/api';
|
||||
|
||||
/**
|
||||
* 获取聊天消息
|
||||
* @param {string} chatId - 聊天ID
|
||||
* @param {string} conversationId - 会话ID
|
||||
*/
|
||||
export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (chatId, { rejectWithValue }) => {
|
||||
export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (conversationId, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get(`/chat-history/${chatId}/messages/`);
|
||||
const response = await get(`/chat-history/conversation_detail`, {
|
||||
params: { conversation_id: conversationId },
|
||||
});
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data.messages;
|
||||
return response.data.messages || [];
|
||||
}
|
||||
|
||||
return response.data?.messages || [];
|
||||
return [];
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data?.message || 'Failed to fetch messages');
|
||||
}
|
||||
@ -35,7 +37,10 @@ export const sendMessage = createAsyncThunk('chat/sendMessage', async ({ chatId,
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data;
|
||||
return {
|
||||
...response.data,
|
||||
role: response.data.role || 'user', // 确保有角色字段
|
||||
};
|
||||
}
|
||||
|
||||
return response.data || {};
|
||||
|
@ -2,7 +2,6 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
fetchAvailableDatasets,
|
||||
fetchChats,
|
||||
createChat,
|
||||
updateChat,
|
||||
deleteChat,
|
||||
createChatRecord,
|
||||
@ -13,28 +12,24 @@ import { fetchMessages, sendMessage } from './chat.messages.thunks';
|
||||
|
||||
// 初始状态
|
||||
const initialState = {
|
||||
// Chat history state
|
||||
history: {
|
||||
items: [],
|
||||
// 聊天列表,包含所有聊天及其消息
|
||||
chats: {
|
||||
items: [], // 每个chat对象包含conversation_id, datasets, create_time, messages等
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
pagination: {
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
},
|
||||
// Chat session creation state
|
||||
createSession: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
sessionId: null,
|
||||
},
|
||||
// Chat messages state
|
||||
messages: {
|
||||
items: [],
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// Send message state
|
||||
sendMessage: {
|
||||
// 当前活跃聊天的ID
|
||||
activeConversationId: null,
|
||||
// 消息发送状态
|
||||
messageOperation: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
streamingMessageId: null, // 当前正在流式传输的消息ID(如果有)
|
||||
},
|
||||
// 可用于聊天的知识库列表
|
||||
availableDatasets: {
|
||||
@ -42,26 +37,16 @@ const initialState = {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
},
|
||||
// 操作状态(创建、更新、删除)
|
||||
operations: {
|
||||
// 聊天操作状态(创建、更新、删除)
|
||||
chatOperation: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// 兼容旧版本的state结构
|
||||
list: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
},
|
||||
// 当前聊天
|
||||
currentChat: {
|
||||
data: null,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
|
||||
// 辅助函数:查找聊天索引
|
||||
const findChatIndex = (state, conversationId) => {
|
||||
return state.chats.items.findIndex((chat) => chat.conversation_id === conversationId);
|
||||
};
|
||||
|
||||
// 创建 slice
|
||||
@ -70,57 +55,98 @@ const chatSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
// 重置操作状态
|
||||
resetOperationStatus: (state) => {
|
||||
state.operations.status = 'idle';
|
||||
state.operations.error = null;
|
||||
resetChatOperation: (state) => {
|
||||
state.chatOperation.status = 'idle';
|
||||
state.chatOperation.error = null;
|
||||
},
|
||||
|
||||
// 重置当前聊天
|
||||
resetCurrentChat: (state) => {
|
||||
state.currentChat.data = null;
|
||||
state.currentChat.status = 'idle';
|
||||
state.currentChat.error = null;
|
||||
// 设置当前活跃聊天
|
||||
setActiveChat: (state, action) => {
|
||||
state.activeConversationId = action.payload;
|
||||
},
|
||||
|
||||
// 设置当前聊天
|
||||
setCurrentChat: (state, action) => {
|
||||
state.currentChat.data = action.payload;
|
||||
state.currentChat.status = 'succeeded';
|
||||
// 重置消息操作状态
|
||||
resetMessageOperation: (state) => {
|
||||
state.messageOperation.status = 'idle';
|
||||
state.messageOperation.error = null;
|
||||
state.messageOperation.streamingMessageId = null;
|
||||
},
|
||||
|
||||
// 重置消息状态
|
||||
resetMessages: (state) => {
|
||||
state.messages.items = [];
|
||||
state.messages.status = 'idle';
|
||||
state.messages.error = null;
|
||||
},
|
||||
|
||||
// 重置发送消息状态
|
||||
resetSendMessageStatus: (state) => {
|
||||
state.sendMessage.status = 'idle';
|
||||
state.sendMessage.error = null;
|
||||
},
|
||||
|
||||
// 添加消息
|
||||
// 添加消息到特定聊天
|
||||
addMessage: (state, action) => {
|
||||
state.messages.items.push(action.payload);
|
||||
const { conversationId, message } = action.payload;
|
||||
const chatIndex = findChatIndex(state, conversationId);
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
// 确保chat有messages数组
|
||||
if (!state.chats.items[chatIndex].messages) {
|
||||
state.chats.items[chatIndex].messages = [];
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
state.chats.items[chatIndex].messages.push(message);
|
||||
|
||||
// 更新最后一条消息和消息计数
|
||||
state.chats.items[chatIndex].last_message = message.content;
|
||||
state.chats.items[chatIndex].message_count = (state.chats.items[chatIndex].message_count || 0) + 1;
|
||||
|
||||
// 如果是助手消息且正在流式传输,记录ID
|
||||
if (message.role === 'assistant' && message.is_streaming) {
|
||||
state.messageOperation.streamingMessageId = message.id;
|
||||
state.messageOperation.status = 'loading';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 更新消息(用于流式传输)
|
||||
updateMessage: (state, action) => {
|
||||
const { id, ...updates } = action.payload;
|
||||
const messageIndex = state.messages.items.findIndex((msg) => msg.id === id);
|
||||
const { conversationId, messageId, updates, serverMessageId } = action.payload;
|
||||
const chatIndex = findChatIndex(state, conversationId);
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
// 更新现有消息
|
||||
state.messages.items[messageIndex] = {
|
||||
...state.messages.items[messageIndex],
|
||||
...updates,
|
||||
};
|
||||
if (chatIndex !== -1 && state.chats.items[chatIndex].messages) {
|
||||
// 首先尝试使用服务器返回的ID找到消息
|
||||
let messageIndex = -1;
|
||||
|
||||
// 如果流式传输结束,更新发送消息状态
|
||||
if (updates.is_streaming === false) {
|
||||
state.sendMessage.status = 'succeeded';
|
||||
if (serverMessageId) {
|
||||
// 如果提供了服务器ID,优先使用它查找消息
|
||||
messageIndex = state.chats.items[chatIndex].messages.findIndex(
|
||||
(msg) => msg.id === serverMessageId || msg.server_id === serverMessageId
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没找到或没提供服务器ID,则使用客户端生成的ID
|
||||
if (messageIndex === -1) {
|
||||
messageIndex = state.chats.items[chatIndex].messages.findIndex((msg) => msg.id === messageId);
|
||||
}
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
// 更新现有消息
|
||||
const updatedMessage = {
|
||||
...state.chats.items[chatIndex].messages[messageIndex],
|
||||
...updates,
|
||||
};
|
||||
|
||||
// 如果收到了服务器ID且消息没有server_id字段,添加它
|
||||
if (serverMessageId && !updatedMessage.server_id) {
|
||||
updatedMessage.server_id = serverMessageId;
|
||||
}
|
||||
|
||||
state.chats.items[chatIndex].messages[messageIndex] = updatedMessage;
|
||||
|
||||
// 如果流式传输结束,更新状态
|
||||
if (
|
||||
updates.is_streaming === false &&
|
||||
(messageId === state.messageOperation.streamingMessageId ||
|
||||
serverMessageId === state.messageOperation.streamingMessageId)
|
||||
) {
|
||||
state.messageOperation.status = 'succeeded';
|
||||
state.messageOperation.streamingMessageId = null;
|
||||
}
|
||||
|
||||
// 如果更新了内容,更新最后一条消息
|
||||
if (updates.content) {
|
||||
state.chats.items[chatIndex].last_message = updates.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -129,189 +155,161 @@ const chatSlice = createSlice({
|
||||
// 获取聊天列表
|
||||
builder
|
||||
.addCase(fetchChats.pending, (state) => {
|
||||
state.list.status = 'loading';
|
||||
state.history.status = 'loading';
|
||||
state.chats.status = 'loading';
|
||||
})
|
||||
.addCase(fetchChats.fulfilled, (state, action) => {
|
||||
state.list.status = 'succeeded';
|
||||
state.chats.status = 'succeeded';
|
||||
|
||||
// 检查是否是追加模式
|
||||
if (action.payload.append) {
|
||||
// 追加模式:将新结果添加到现有列表的前面
|
||||
state.list.items = [...action.payload.results, ...state.list.items];
|
||||
state.history.items = [...action.payload.results, ...state.history.items];
|
||||
state.chats.items = [...action.payload.results, ...state.chats.items];
|
||||
} else {
|
||||
// 替换模式:使用新结果替换整个列表
|
||||
state.list.items = action.payload.results;
|
||||
state.list.total = action.payload.total;
|
||||
state.list.page = action.payload.page;
|
||||
state.list.page_size = action.payload.page_size;
|
||||
|
||||
// 同时更新新的状态结构
|
||||
state.history.items = action.payload.results;
|
||||
state.chats.items = action.payload.results;
|
||||
state.chats.pagination.total = action.payload.total;
|
||||
state.chats.pagination.page = action.payload.page;
|
||||
state.chats.pagination.page_size = action.payload.page_size;
|
||||
}
|
||||
|
||||
state.history.status = 'succeeded';
|
||||
state.history.error = null;
|
||||
})
|
||||
.addCase(fetchChats.rejected, (state, action) => {
|
||||
state.list.status = 'failed';
|
||||
state.list.error = action.payload || action.error.message;
|
||||
|
||||
// 同时更新新的状态结构
|
||||
state.history.status = 'failed';
|
||||
state.history.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 创建聊天
|
||||
.addCase(createChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
})
|
||||
.addCase(createChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
state.list.items.unshift(action.payload);
|
||||
state.list.total += 1;
|
||||
state.currentChat.data = action.payload;
|
||||
state.currentChat.status = 'succeeded';
|
||||
})
|
||||
.addCase(createChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
state.chats.status = 'failed';
|
||||
state.chats.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 删除聊天
|
||||
.addCase(deleteChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
state.chatOperation.status = 'loading';
|
||||
})
|
||||
.addCase(deleteChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
// 更新旧的状态结构
|
||||
state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
|
||||
// 更新新的状态结构
|
||||
state.history.items = state.history.items.filter((chat) => chat.conversation_id !== action.payload);
|
||||
state.chatOperation.status = 'succeeded';
|
||||
// 删除聊天
|
||||
state.chats.items = state.chats.items.filter((chat) => chat.conversation_id !== action.payload);
|
||||
|
||||
if (state.list.total > 0) {
|
||||
state.list.total -= 1;
|
||||
if (state.chats.pagination.total > 0) {
|
||||
state.chats.pagination.total -= 1;
|
||||
}
|
||||
|
||||
if (state.currentChat.data && state.currentChat.data.id === action.payload) {
|
||||
state.currentChat.data = null;
|
||||
// 如果删除的是当前活跃聊天,重置activeConversationId
|
||||
if (state.activeConversationId === action.payload) {
|
||||
state.activeConversationId = null;
|
||||
}
|
||||
})
|
||||
.addCase(deleteChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
state.chatOperation.status = 'failed';
|
||||
state.chatOperation.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 更新聊天
|
||||
.addCase(updateChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
state.chatOperation.status = 'loading';
|
||||
})
|
||||
.addCase(updateChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
const index = state.list.items.findIndex((chat) => chat.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.list.items[index] = action.payload;
|
||||
}
|
||||
if (state.currentChat.data && state.currentChat.data.id === action.payload.id) {
|
||||
state.currentChat.data = action.payload;
|
||||
state.chatOperation.status = 'succeeded';
|
||||
const chatIndex = findChatIndex(state, action.payload.conversation_id);
|
||||
if (chatIndex !== -1) {
|
||||
// 保留messages字段,避免覆盖
|
||||
const existingMessages = state.chats.items[chatIndex].messages;
|
||||
state.chats.items[chatIndex] = {
|
||||
...action.payload,
|
||||
messages: existingMessages || [],
|
||||
};
|
||||
}
|
||||
})
|
||||
.addCase(updateChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
state.chatOperation.status = 'failed';
|
||||
state.chatOperation.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 获取聊天消息
|
||||
.addCase(fetchMessages.pending, (state) => {
|
||||
state.messages.status = 'loading';
|
||||
state.messages.error = null;
|
||||
state.messageOperation.status = 'loading';
|
||||
})
|
||||
.addCase(fetchMessages.fulfilled, (state, action) => {
|
||||
state.messages.status = 'succeeded';
|
||||
state.messages.items = action.payload;
|
||||
state.messageOperation.status = 'succeeded';
|
||||
// 假设action.meta.arg是conversationId
|
||||
const conversationId = action.meta.arg;
|
||||
const chatIndex = findChatIndex(state, conversationId);
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
state.chats.items[chatIndex].messages = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(fetchMessages.rejected, (state, action) => {
|
||||
state.messages.status = 'failed';
|
||||
state.messages.error = action.error.message;
|
||||
state.messageOperation.status = 'failed';
|
||||
state.messageOperation.error = action.error.message;
|
||||
})
|
||||
|
||||
// 发送聊天消息
|
||||
.addCase(sendMessage.pending, (state) => {
|
||||
state.sendMessage.status = 'loading';
|
||||
state.sendMessage.error = null;
|
||||
state.messageOperation.status = 'loading';
|
||||
})
|
||||
.addCase(sendMessage.fulfilled, (state, action) => {
|
||||
state.sendMessage.status = 'succeeded';
|
||||
// 更新消息列表
|
||||
const index = state.messages.items.findIndex(
|
||||
(msg) => msg.content === action.payload.content && msg.sender === action.payload.sender
|
||||
);
|
||||
if (index === -1) {
|
||||
state.messages.items.push(action.payload);
|
||||
state.messageOperation.status = 'succeeded';
|
||||
// 假设action.meta.arg包含chatId
|
||||
const { chatId } = action.meta.arg;
|
||||
const chatIndex = findChatIndex(state, chatId);
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
// 确保chat有messages数组
|
||||
if (!state.chats.items[chatIndex].messages) {
|
||||
state.chats.items[chatIndex].messages = [];
|
||||
}
|
||||
|
||||
// 检查消息是否已存在
|
||||
const messageExists = state.chats.items[chatIndex].messages.some(
|
||||
(msg) => msg.content === action.payload.content && msg.role === action.payload.role
|
||||
);
|
||||
|
||||
if (!messageExists) {
|
||||
state.chats.items[chatIndex].messages.push(action.payload);
|
||||
state.chats.items[chatIndex].last_message = action.payload.content;
|
||||
state.chats.items[chatIndex].message_count =
|
||||
(state.chats.items[chatIndex].message_count || 0) + 1;
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(sendMessage.rejected, (state, action) => {
|
||||
state.sendMessage.status = 'failed';
|
||||
state.sendMessage.error = action.error.message;
|
||||
state.messageOperation.status = 'failed';
|
||||
state.messageOperation.error = action.error.message;
|
||||
})
|
||||
|
||||
// 处理创建聊天记录
|
||||
.addCase(createChatRecord.pending, (state) => {
|
||||
state.sendMessage.status = 'loading';
|
||||
state.sendMessage.error = null;
|
||||
state.messageOperation.status = 'loading';
|
||||
})
|
||||
.addCase(createChatRecord.fulfilled, (state, action) => {
|
||||
// 更新状态以反映聊天已创建
|
||||
if (action.payload.conversation_id && !state.currentChat.data) {
|
||||
// 设置当前聊天的会话ID
|
||||
state.currentChat.data = {
|
||||
conversation_id: action.payload.conversation_id,
|
||||
// 其他信息将由流式更新填充
|
||||
};
|
||||
}
|
||||
// 不再在这里添加消息,因为消息已经在thunk函数中添加
|
||||
// 聊天创建成功,但消息状态由addMessage和updateMessage处理
|
||||
state.activeConversationId = action.payload.conversation_id;
|
||||
})
|
||||
.addCase(createChatRecord.rejected, (state, action) => {
|
||||
state.sendMessage.status = 'failed';
|
||||
state.sendMessage.error = action.error.message;
|
||||
state.messageOperation.status = 'failed';
|
||||
state.messageOperation.error = action.error.message;
|
||||
})
|
||||
|
||||
// 处理创建会话
|
||||
.addCase(createConversation.pending, (state) => {
|
||||
state.createSession.status = 'loading';
|
||||
state.createSession.error = null;
|
||||
state.chatOperation.status = 'loading';
|
||||
})
|
||||
.addCase(createConversation.fulfilled, (state, action) => {
|
||||
state.createSession.status = 'succeeded';
|
||||
state.createSession.sessionId = action.payload.conversation_id;
|
||||
state.chatOperation.status = 'succeeded';
|
||||
state.activeConversationId = action.payload.conversation_id;
|
||||
|
||||
// 当前聊天设置 - 使用与fetchConversationDetail相同的数据结构
|
||||
state.currentChat.data = {
|
||||
conversation_id: action.payload.conversation_id,
|
||||
datasets: action.payload.datasets || [],
|
||||
// 添加其他必要的字段,确保与fetchConversationDetail返回的数据结构兼容
|
||||
messages: [],
|
||||
create_time: new Date().toISOString(),
|
||||
update_time: new Date().toISOString(),
|
||||
};
|
||||
state.currentChat.status = 'succeeded';
|
||||
state.currentChat.error = null;
|
||||
// 在执行createConversation时,已经通过dispatch添加了新聊天到列表
|
||||
// 所以这里只需确保当前激活的聊天ID已设置
|
||||
})
|
||||
.addCase(createConversation.rejected, (state, action) => {
|
||||
state.createSession.status = 'failed';
|
||||
state.createSession.error = action.payload || action.error.message;
|
||||
state.chatOperation.status = 'failed';
|
||||
state.chatOperation.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 处理获取可用知识库
|
||||
.addCase(fetchAvailableDatasets.pending, (state) => {
|
||||
state.availableDatasets.status = 'loading';
|
||||
state.availableDatasets.error = null;
|
||||
})
|
||||
.addCase(fetchAvailableDatasets.fulfilled, (state, action) => {
|
||||
state.availableDatasets.status = 'succeeded';
|
||||
state.availableDatasets.items = action.payload || [];
|
||||
state.availableDatasets.error = null;
|
||||
})
|
||||
.addCase(fetchAvailableDatasets.rejected, (state, action) => {
|
||||
state.availableDatasets.status = 'failed';
|
||||
@ -320,35 +318,38 @@ const chatSlice = createSlice({
|
||||
|
||||
// 获取会话详情
|
||||
.addCase(fetchConversationDetail.pending, (state) => {
|
||||
state.currentChat.status = 'loading';
|
||||
state.currentChat.error = null;
|
||||
// 设置加载状态
|
||||
})
|
||||
.addCase(fetchConversationDetail.fulfilled, (state, action) => {
|
||||
// 如果有返回数据
|
||||
if (action.payload) {
|
||||
state.currentChat.status = 'succeeded';
|
||||
state.currentChat.data = action.payload;
|
||||
} else {
|
||||
state.currentChat.status = 'idle';
|
||||
state.currentChat.data = null;
|
||||
const conversationId = action.payload.conversation_id;
|
||||
const chatIndex = findChatIndex(state, conversationId);
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
// 更新现有聊天
|
||||
state.chats.items[chatIndex] = {
|
||||
...state.chats.items[chatIndex],
|
||||
...action.payload,
|
||||
};
|
||||
} else {
|
||||
// 添加新聊天
|
||||
state.chats.items.push(action.payload);
|
||||
}
|
||||
|
||||
// 设置为当前活跃聊天
|
||||
state.activeConversationId = conversationId;
|
||||
}
|
||||
})
|
||||
.addCase(fetchConversationDetail.rejected, (state, action) => {
|
||||
state.currentChat.status = 'failed';
|
||||
state.currentChat.error = action.payload || action.error.message;
|
||||
// 仅在操作失败时设置错误状态
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 actions
|
||||
export const {
|
||||
resetOperationStatus,
|
||||
resetCurrentChat,
|
||||
setCurrentChat,
|
||||
resetMessages,
|
||||
resetSendMessageStatus,
|
||||
addMessage,
|
||||
updateMessage,
|
||||
} = chatSlice.actions;
|
||||
export const { resetChatOperation, setActiveChat, resetMessageOperation, addMessage, updateMessage } =
|
||||
chatSlice.actions;
|
||||
|
||||
// 导出 reducer
|
||||
export default chatSlice.reducer;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post, put, del, streamRequest } from '../../services/api';
|
||||
import { showNotification } from '../notification.slice';
|
||||
import { addMessage, updateMessage, setCurrentChat } from './chat.slice';
|
||||
import { addMessage, updateMessage, setActiveChat } from './chat.slice';
|
||||
|
||||
/**
|
||||
* 获取聊天列表
|
||||
@ -30,29 +30,9 @@ export const fetchChats = createAsyncThunk('chat/fetchChats', async (params = {}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建新聊天
|
||||
* @param {Object} chatData - 聊天数据
|
||||
* @param {string} chatData.knowledge_base_id - 知识库ID
|
||||
* @param {string} chatData.title - 聊天标题
|
||||
*/
|
||||
export const createChat = createAsyncThunk('chat/createChat', async (chatData, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await post('/chat-history/', chatData);
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data.chat;
|
||||
}
|
||||
|
||||
return response.data?.chat || {};
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data?.message || 'Failed to create chat');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新聊天
|
||||
* 更新已经发送出去的聊天,获得新的回复,相当于编辑以往的聊天记录。暂时未使用
|
||||
* @param {Object} params - 更新参数
|
||||
* @param {string} params.id - 聊天ID
|
||||
* @param {Object} params.data - 更新数据
|
||||
@ -114,7 +94,8 @@ export const fetchAvailableDatasets = createAsyncThunk(
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建聊天记录
|
||||
* 创建/继续聊天
|
||||
* 创建新会话,或者继续一个已有的会话,根据conversation_id来更新
|
||||
* @param {Object} params - 聊天参数
|
||||
* @param {string[]} params.dataset_id_list - 知识库ID列表
|
||||
* @param {string} params.question - 用户问题
|
||||
@ -134,24 +115,36 @@ export const createChatRecord = createAsyncThunk(
|
||||
|
||||
// 先添加用户消息到聊天窗口
|
||||
const userMessageId = Date.now().toString();
|
||||
const userMessage = {
|
||||
id: userMessageId,
|
||||
role: 'user',
|
||||
content: question,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 添加用户消息
|
||||
dispatch(
|
||||
addMessage({
|
||||
id: userMessageId,
|
||||
role: 'user',
|
||||
content: question,
|
||||
created_at: new Date().toISOString(),
|
||||
conversationId: conversation_id,
|
||||
message: userMessage,
|
||||
})
|
||||
);
|
||||
|
||||
// 添加临时的助手消息(流式传输期间显示)
|
||||
const assistantMessageId = (Date.now() + 1).toString();
|
||||
const assistantMessage = {
|
||||
id: assistantMessageId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: new Date().toISOString(),
|
||||
is_streaming: true,
|
||||
};
|
||||
|
||||
// 添加助手消息
|
||||
dispatch(
|
||||
addMessage({
|
||||
id: assistantMessageId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: new Date().toISOString(),
|
||||
is_streaming: true,
|
||||
conversationId: conversation_id,
|
||||
message: assistantMessage,
|
||||
})
|
||||
);
|
||||
|
||||
@ -185,11 +178,16 @@ export const createChatRecord = createAsyncThunk(
|
||||
finalMessage += data.data.content;
|
||||
console.log('累加内容:', finalMessage);
|
||||
|
||||
// 获取服务器消息ID (如果存在)
|
||||
const serverMessageId = data.data.id;
|
||||
|
||||
// 更新消息内容
|
||||
dispatch(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
content: finalMessage,
|
||||
conversationId: conversationId,
|
||||
messageId: assistantMessageId,
|
||||
serverMessageId: serverMessageId,
|
||||
updates: { content: finalMessage },
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -197,10 +195,16 @@ export const createChatRecord = createAsyncThunk(
|
||||
// 处理结束标志
|
||||
if (data.data.is_end) {
|
||||
console.log('检测到消息结束标志');
|
||||
|
||||
// 获取服务器消息ID (如果存在)
|
||||
const serverMessageId = data.data.id;
|
||||
|
||||
dispatch(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
is_streaming: false,
|
||||
conversationId: conversationId,
|
||||
messageId: assistantMessageId,
|
||||
serverMessageId: serverMessageId,
|
||||
updates: { is_streaming: false },
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -217,10 +221,15 @@ export const createChatRecord = createAsyncThunk(
|
||||
messageType === '结束流式传输'
|
||||
) {
|
||||
console.log('收到完成消息');
|
||||
// 获取服务器消息ID (如果存在)
|
||||
const serverMessageId = data.data?.id;
|
||||
|
||||
dispatch(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
is_streaming: false,
|
||||
conversationId: conversationId,
|
||||
messageId: assistantMessageId,
|
||||
serverMessageId: serverMessageId,
|
||||
updates: { is_streaming: false },
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -230,10 +239,15 @@ export const createChatRecord = createAsyncThunk(
|
||||
// 如果有content字段,也尝试更新
|
||||
if (data.data && data.data.content !== undefined) {
|
||||
finalMessage += data.data.content;
|
||||
// 获取服务器消息ID (如果存在)
|
||||
const serverMessageId = data.data.id;
|
||||
|
||||
dispatch(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
content: finalMessage,
|
||||
conversationId: conversationId,
|
||||
messageId: assistantMessageId,
|
||||
serverMessageId: serverMessageId,
|
||||
updates: { content: finalMessage },
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -250,9 +264,12 @@ export const createChatRecord = createAsyncThunk(
|
||||
console.error('流式请求错误:', error);
|
||||
dispatch(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
content: `错误: ${error.message || '请求失败'}`,
|
||||
is_streaming: false,
|
||||
conversationId: conversationId,
|
||||
messageId: assistantMessageId,
|
||||
updates: {
|
||||
content: `错误: ${error.message || '请求失败'}`,
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -261,8 +278,9 @@ export const createChatRecord = createAsyncThunk(
|
||||
// 确保流式传输结束后标记消息已完成
|
||||
dispatch(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
is_streaming: false,
|
||||
conversationId: conversationId,
|
||||
messageId: assistantMessageId,
|
||||
updates: { is_streaming: false },
|
||||
})
|
||||
);
|
||||
|
||||
@ -277,7 +295,7 @@ export const createChatRecord = createAsyncThunk(
|
||||
// 获取知识库信息
|
||||
const state = getState();
|
||||
const availableDatasets = state.chat.availableDatasets.items || [];
|
||||
const existingChats = state.chat.history.items || [];
|
||||
const existingChats = state.chat.chats.items || [];
|
||||
|
||||
// 检查是否已存在此会话ID的记录
|
||||
const existingChat = existingChats.find((chat) => chat.conversation_id === chatInfo.conversation_id);
|
||||
@ -303,6 +321,7 @@ export const createChatRecord = createAsyncThunk(
|
||||
create_time: new Date().toISOString(),
|
||||
last_message: question,
|
||||
message_count: 2, // 用户问题和助手回复
|
||||
messages: [userMessage, assistantMessage], // 添加消息到聊天记录
|
||||
};
|
||||
|
||||
// 更新当前聊天
|
||||
@ -319,23 +338,7 @@ export const createChatRecord = createAsyncThunk(
|
||||
}
|
||||
|
||||
// 设置为当前聊天
|
||||
dispatch(
|
||||
setCurrentChat({
|
||||
conversation_id: chatInfo.conversation_id,
|
||||
datasets: existingChat
|
||||
? existingChat.datasets
|
||||
: dataset_id_list.map((id) => {
|
||||
const formattedId = id.includes('-')
|
||||
? id
|
||||
: id.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5');
|
||||
const dataset = availableDatasets.find((ds) => ds.id === formattedId);
|
||||
return {
|
||||
id: formattedId,
|
||||
name: dataset?.name || '新知识库对话',
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
dispatch(setActiveChat(chatInfo.conversation_id));
|
||||
}
|
||||
|
||||
return chatInfo;
|
||||
@ -363,34 +366,11 @@ export const fetchConversationDetail = createAsyncThunk(
|
||||
'chat/fetchConversationDetail',
|
||||
async (conversationId, { rejectWithValue, dispatch, getState }) => {
|
||||
try {
|
||||
// 先检查是否是刚创建的会话
|
||||
const state = getState();
|
||||
const createSession = state.chat.createSession || {};
|
||||
const currentChat = state.chat.currentChat.data;
|
||||
|
||||
// 如果是刚创建成功的会话,且会话ID匹配,则直接返回现有会话数据
|
||||
if (
|
||||
createSession.status === 'succeeded' &&
|
||||
createSession.sessionId === conversationId &&
|
||||
currentChat?.conversation_id === conversationId
|
||||
) {
|
||||
console.log('使用新创建的会话数据,跳过详情请求:', conversationId);
|
||||
return currentChat;
|
||||
}
|
||||
|
||||
const response = await get('/chat-history/conversation_detail', {
|
||||
params: { conversation_id: conversationId },
|
||||
});
|
||||
|
||||
if (response && response.code === 200) {
|
||||
// 如果存在消息,更新Redux状态
|
||||
if (response.data.messages) {
|
||||
dispatch({
|
||||
type: 'chat/fetchMessages/fulfilled',
|
||||
payload: response.data.messages,
|
||||
});
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@ -411,7 +391,7 @@ export const fetchConversationDetail = createAsyncThunk(
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建新会话(仅获取会话ID,不发送消息)
|
||||
* 创建新会话(仅获取会话ID,相当于一个会话凭证,不发送消息)
|
||||
* @param {Object} params - 参数
|
||||
* @param {string[]} params.dataset_id_list - 知识库ID列表
|
||||
*/
|
||||
@ -450,6 +430,7 @@ export const createConversation = createAsyncThunk(
|
||||
create_time: new Date().toISOString(),
|
||||
last_message: '',
|
||||
message_count: 0,
|
||||
messages: [], // 初始化空消息数组
|
||||
};
|
||||
|
||||
// 更新聊天历史列表
|
||||
@ -463,12 +444,7 @@ export const createConversation = createAsyncThunk(
|
||||
});
|
||||
|
||||
// 设置为当前聊天
|
||||
dispatch(
|
||||
setCurrentChat({
|
||||
conversation_id: conversationData.conversation_id,
|
||||
datasets: newChatEntry.datasets,
|
||||
})
|
||||
);
|
||||
dispatch(setActiveChat(conversationData.conversation_id));
|
||||
|
||||
return conversationData;
|
||||
}
|
||||
|
101
src/store/gmailChat/gmailChat.slice.js
Normal file
101
src/store/gmailChat/gmailChat.slice.js
Normal file
@ -0,0 +1,101 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { refreshGmailWatch, setupGmailChat, checkGmailAuth } from './gmailChat.thunks';
|
||||
|
||||
const initialState = {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
data: {
|
||||
historyId: null,
|
||||
expiration: null,
|
||||
wasExpired: null,
|
||||
gmailEmail: null,
|
||||
},
|
||||
setup: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
knowledgeBaseId: null,
|
||||
conversationId: null,
|
||||
message: null,
|
||||
noEmailsWarning: null,
|
||||
troubleshooting: null,
|
||||
},
|
||||
auth: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
isAuthenticated: false,
|
||||
needsSetup: false,
|
||||
watchExpired: false,
|
||||
lastHistoryId: null,
|
||||
watchExpiration: null,
|
||||
},
|
||||
};
|
||||
|
||||
const gmailChatSlice = createSlice({
|
||||
name: 'gmailChat',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(refreshGmailWatch.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
})
|
||||
.addCase(refreshGmailWatch.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded';
|
||||
state.data = {
|
||||
historyId: action.payload.data.history_id,
|
||||
expiration: action.payload.data.expiration,
|
||||
wasExpired: action.payload.data.was_expired,
|
||||
gmailEmail: action.payload.data.gmail_email,
|
||||
};
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(refreshGmailWatch.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.error.message;
|
||||
})
|
||||
.addCase(setupGmailChat.pending, (state) => {
|
||||
state.setup.status = 'loading';
|
||||
state.setup.noEmailsWarning = null;
|
||||
state.setup.troubleshooting = null;
|
||||
})
|
||||
.addCase(setupGmailChat.fulfilled, (state, action) => {
|
||||
state.setup.status = 'succeeded';
|
||||
|
||||
if (action.payload.message && action.payload.message.includes('没有找到')) {
|
||||
state.setup.noEmailsWarning = action.payload.message;
|
||||
state.setup.troubleshooting = action.payload.troubleshooting || null;
|
||||
} else {
|
||||
state.setup.noEmailsWarning = null;
|
||||
state.setup.troubleshooting = null;
|
||||
}
|
||||
|
||||
state.setup.knowledgeBaseId =
|
||||
action.payload.knowledge_base_id || action.payload.data?.knowledge_base_id;
|
||||
state.setup.conversationId = action.payload.conversation_id || action.payload.data?.conversation_id;
|
||||
state.setup.message = action.payload.message;
|
||||
state.setup.error = null;
|
||||
})
|
||||
.addCase(setupGmailChat.rejected, (state, action) => {
|
||||
state.setup.status = 'failed';
|
||||
state.setup.error = action.error.message;
|
||||
})
|
||||
.addCase(checkGmailAuth.pending, (state) => {
|
||||
state.auth.status = 'loading';
|
||||
})
|
||||
.addCase(checkGmailAuth.fulfilled, (state, action) => {
|
||||
state.auth.status = 'succeeded';
|
||||
state.auth.isAuthenticated = action.payload.data.authenticated;
|
||||
state.auth.needsSetup = action.payload.data.needs_setup;
|
||||
state.auth.watchExpired = action.payload.data.watch_expired;
|
||||
state.auth.lastHistoryId = action.payload.data.last_history_id;
|
||||
state.auth.watchExpiration = action.payload.data.watch_expiration;
|
||||
state.auth.error = null;
|
||||
})
|
||||
.addCase(checkGmailAuth.rejected, (state, action) => {
|
||||
state.auth.status = 'failed';
|
||||
state.auth.error = action.error.message;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default gmailChatSlice.reducer;
|
76
src/store/gmailChat/gmailChat.thunks.js
Normal file
76
src/store/gmailChat/gmailChat.thunks.js
Normal file
@ -0,0 +1,76 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
import { post } from '../../services/api';
|
||||
|
||||
export const refreshGmailWatch = createAsyncThunk('gmailChat/refreshGmailWatch', async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const requestData = {
|
||||
use_proxy: true,
|
||||
proxy_url: 'http://127.0.0.1:7890',
|
||||
};
|
||||
|
||||
const response = await post('/gmail/refresh-watch/', requestData);
|
||||
return response;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || 'Failed to refresh Gmail watch';
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
export const setupGmailChat = createAsyncThunk('gmailChat/setupGmailChat', async (payload, { rejectWithValue }) => {
|
||||
try {
|
||||
let requestData;
|
||||
|
||||
// 处理两种不同的调用情况:只传入邮箱字符串 或 传入包含auth_code的对象
|
||||
if (typeof payload === 'string') {
|
||||
// 第一次调用,只传入达人邮箱
|
||||
requestData = {
|
||||
client_secret_json: {
|
||||
installed: {
|
||||
client_id: '266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com',
|
||||
project_id: 'knowledge-454905',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_secret: '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',
|
||||
redirect_uris: ['http://localhost'],
|
||||
},
|
||||
},
|
||||
talent_gmail: payload,
|
||||
auth_code: "",
|
||||
};
|
||||
} else {
|
||||
// 第二次调用,包含授权码
|
||||
requestData = {
|
||||
client_secret_json: {
|
||||
installed: {
|
||||
client_id: '266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com',
|
||||
project_id: 'knowledge-454905',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_secret: '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',
|
||||
redirect_uris: ['http://localhost'],
|
||||
},
|
||||
},
|
||||
talent_gmail: payload.talent_gmail,
|
||||
auth_code: payload.auth_code,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await post('/gmail/setup/', requestData);
|
||||
return response;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data?.message || error.message || 'Failed to setup Gmail chat');
|
||||
}
|
||||
});
|
||||
|
||||
export const checkGmailAuth = createAsyncThunk('gmailChat/checkAuth', async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get('/gmail/check-auth/');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || 'Failed to check Gmail authentication';
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
@ -7,6 +7,8 @@ import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
||||
import chatReducer from './chat/chat.slice.js';
|
||||
import permissionsReducer from './permissions/permissions.slice.js';
|
||||
import notificationCenterReducer from './notificationCenter/notificationCenter.slice.js';
|
||||
import gmailChatReducer from './gmailChat/gmailChat.slice.js';
|
||||
import talentChatReducer from './talentChat/talentChat.slice.js';
|
||||
|
||||
const rootRducer = combineReducers({
|
||||
auth: authReducer,
|
||||
@ -15,6 +17,8 @@ const rootRducer = combineReducers({
|
||||
chat: chatReducer,
|
||||
permissions: permissionsReducer,
|
||||
notificationCenter: notificationCenterReducer,
|
||||
gmailChat: gmailChatReducer,
|
||||
talentChat: talentChatReducer,
|
||||
});
|
||||
|
||||
const persistConfig = {
|
||||
|
84
src/store/talentChat/talentChat.slice.js
Normal file
84
src/store/talentChat/talentChat.slice.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { setUserGoal, getConversationSummary, getRecommendedReply } from './talentChat.thunks';
|
||||
|
||||
// 初始状态
|
||||
const initialState = {
|
||||
userGoal: {
|
||||
data: null,
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null
|
||||
},
|
||||
conversationSummary: {
|
||||
data: null,
|
||||
status: 'idle',
|
||||
error: null
|
||||
},
|
||||
recommendedReply: {
|
||||
data: null,
|
||||
status: 'idle',
|
||||
error: null
|
||||
}
|
||||
};
|
||||
|
||||
// 创建 slice
|
||||
const talentChatSlice = createSlice({
|
||||
name: 'talentChat',
|
||||
initialState,
|
||||
reducers: {
|
||||
resetTalentChat: (state) => {
|
||||
state.userGoal = { data: null, status: 'idle', error: null };
|
||||
state.conversationSummary = { data: null, status: 'idle', error: null };
|
||||
state.recommendedReply = { data: null, status: 'idle', error: null };
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// 设置用户目标
|
||||
builder
|
||||
.addCase(setUserGoal.pending, (state) => {
|
||||
state.userGoal.status = 'loading';
|
||||
state.userGoal.error = null;
|
||||
})
|
||||
.addCase(setUserGoal.fulfilled, (state, action) => {
|
||||
state.userGoal.status = 'succeeded';
|
||||
state.userGoal.data = action.payload?.data || action.payload;
|
||||
state.userGoal.error = null;
|
||||
})
|
||||
.addCase(setUserGoal.rejected, (state, action) => {
|
||||
state.userGoal.status = 'failed';
|
||||
state.userGoal.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 获取会话摘要
|
||||
.addCase(getConversationSummary.pending, (state) => {
|
||||
state.conversationSummary.status = 'loading';
|
||||
state.conversationSummary.error = null;
|
||||
})
|
||||
.addCase(getConversationSummary.fulfilled, (state, action) => {
|
||||
state.conversationSummary.status = 'succeeded';
|
||||
state.conversationSummary.data = action.payload || null;
|
||||
state.conversationSummary.error = null;
|
||||
})
|
||||
.addCase(getConversationSummary.rejected, (state, action) => {
|
||||
state.conversationSummary.status = 'failed';
|
||||
state.conversationSummary.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 获取推荐回复
|
||||
.addCase(getRecommendedReply.pending, (state) => {
|
||||
state.recommendedReply.status = 'loading';
|
||||
state.recommendedReply.error = null;
|
||||
})
|
||||
.addCase(getRecommendedReply.fulfilled, (state, action) => {
|
||||
state.recommendedReply.status = 'succeeded';
|
||||
state.recommendedReply.data = action.payload?.data || action.payload;
|
||||
state.recommendedReply.error = null;
|
||||
})
|
||||
.addCase(getRecommendedReply.rejected, (state, action) => {
|
||||
state.recommendedReply.status = 'failed';
|
||||
state.recommendedReply.error = action.payload || action.error.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { resetTalentChat } = talentChatSlice.actions;
|
||||
export default talentChatSlice.reducer;
|
51
src/store/talentChat/talentChat.thunks.js
Normal file
51
src/store/talentChat/talentChat.thunks.js
Normal file
@ -0,0 +1,51 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
import { post, get } from '../../services/api';
|
||||
|
||||
export const setUserGoal = createAsyncThunk('talentChat/setUserGoal', async (content, { rejectWithValue }) => {
|
||||
try {
|
||||
const requestData = {
|
||||
content,
|
||||
};
|
||||
|
||||
const response = await post('/user-goal/', requestData);
|
||||
return response;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data?.message || error.message || 'Failed to set user goal');
|
||||
}
|
||||
});
|
||||
|
||||
export const getConversationSummary = createAsyncThunk(
|
||||
'talentChat/getConversationSummary',
|
||||
async (talentEmail, { rejectWithValue }) => {
|
||||
try {
|
||||
const params = {
|
||||
talent_email: talentEmail,
|
||||
};
|
||||
|
||||
const { summary } = await get('/conversation-summary/', { params });
|
||||
return summary;
|
||||
} catch (error) {
|
||||
return rejectWithValue(
|
||||
error.response?.data?.message || error.message || 'Failed to get conversation summary'
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getRecommendedReply = createAsyncThunk(
|
||||
'talentChat/getRecommendedReply',
|
||||
async ({ conversation_id, talent_email }, { rejectWithValue }) => {
|
||||
try {
|
||||
const requestData = {
|
||||
conversation_id,
|
||||
talent_email,
|
||||
};
|
||||
|
||||
const response = await post('/recommended-reply/', requestData);
|
||||
return response;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data?.message || error.message || 'Failed to get recommended reply');
|
||||
}
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user