From 6a654950a503d5c16ed2b2ea89da0f4564a49581 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Thu, 17 Apr 2025 20:38:06 -0400 Subject: [PATCH] [dev]gmail side panel --- src/components/ApiModeSwitch.jsx | 83 -- src/pages/Chat/Chat.jsx | 7 +- src/pages/Chat/ChatSidePanel.jsx | 252 +++++ src/pages/Chat/ChatWindow.jsx | 354 +++--- src/pages/Chat/NewChat.jsx | 6 +- src/pages/KnowledgeBase/KnowledgeBase.jsx | 4 - src/services/api.js | 104 +- src/services/mockApi.js | 1240 --------------------- src/store/chat/chat.messages.thunks.js | 17 +- src/store/chat/chat.slice.js | 399 +++---- src/store/chat/chat.thunks.js | 162 ++- src/store/talentChat/talentChat.slice.js | 80 +- src/store/talentChat/talentChat.thunks.js | 4 +- 13 files changed, 781 insertions(+), 1931 deletions(-) delete mode 100644 src/components/ApiModeSwitch.jsx create mode 100644 src/pages/Chat/ChatSidePanel.jsx delete mode 100644 src/services/mockApi.js diff --git a/src/components/ApiModeSwitch.jsx b/src/components/ApiModeSwitch.jsx deleted file mode 100644 index 1dbc4a6..0000000 --- a/src/components/ApiModeSwitch.jsx +++ /dev/null @@ -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 ( -
-
-
- - -
- {isMockMode && 使用本地模拟数据} - {!isMockMode && 已连接到后端服务器} -
- - {showNotification && ( -
- {notification.message} -
- )} -
- ); -} diff --git a/src/pages/Chat/Chat.jsx b/src/pages/Chat/Chat.jsx index 9c62bdc..d4c08c3 100644 --- a/src/pages/Chat/Chat.jsx +++ b/src/pages/Chat/Chat.jsx @@ -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]); diff --git a/src/pages/Chat/ChatSidePanel.jsx b/src/pages/Chat/ChatSidePanel.jsx new file mode 100644 index 0000000..4ed0762 --- /dev/null +++ b/src/pages/Chat/ChatSidePanel.jsx @@ -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 ( +
+ {/* 聊天目标 */} +
+
+ 聊天目标 +
+
+ + +
+ {userGoal && ( +
+ 当前目标: +

+ {userGoal.content || + (typeof userGoal === 'string' ? userGoal : userGoal?.data?.content || '')} +

+
+ )} +
+ + {/* 历史聊天记录总结 */} +
+
+ 历史聊天记录总结 + +
+ {loading.summary ? ( +
+
+ 加载中... +
+

获取聊天总结...

+
+ ) : conversationSummary?.summary ? ( +
+ +
+ ) : ( +

暂无聊天总结

+ )} +
+ + {/* 推荐聊天话术 */} +
+
+ 推荐聊天话术 + +
+ {loading.reply ? ( +
+
+ 加载中... +
+

生成话术中...

+
+ ) : recommendedReply?.reply || recommendedReply?.status === 'success' ? ( +
+ +
+ +
+
+ ) : ( +

暂无推荐话术

+ )} +
+
+ ); +} diff --git a/src/pages/Chat/ChatWindow.jsx b/src/pages/Chat/ChatWindow.jsx index 11537b1..70914c6 100644 --- a/src/pages/Chat/ChatWindow.jsx +++ b/src/pages/Chat/ChatWindow.jsx @@ -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 }) { @@ -20,11 +21,14 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { const [noEmailsWarning, setNoEmailsWarning] = useState(null); const [troubleshooting, setTroubleshooting] = useState(null); - // 从 Redux store 获取消息 - const messages = useSelector((state) => state.chat.messages.items); - const messageStatus = useSelector((state) => state.chat.messages.status); - const messageError = useSelector((state) => state.chat.messages.error); - const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage); + // 从 Redux 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); @@ -40,14 +44,14 @@ 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([]); @@ -84,11 +88,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { } }, [chatId, gmailNoEmailsWarning, gmailTroubleshooting]); - // 当conversation或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds + // 当currentChat或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds useEffect(() => { - // 优先使用conversation中的知识库列表 - if (conversation && conversation.datasets && conversation.datasets.length > 0) { - const datasetIds = conversation.datasets.map((ds) => ds.id); + // 优先使用currentChat中的知识库列表 + if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) { + const datasetIds = currentChat.datasets.map((ds) => ds.id); console.log('从会话中获取知识库列表:', datasetIds); setSelectedKnowledgeBaseIds(datasetIds); } @@ -99,7 +103,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { console.log('从URL参数中获取知识库列表:', ids); setSelectedKnowledgeBaseIds(ids); } - }, [conversation, knowledgeBaseId]); + }, [currentChat, knowledgeBaseId]); // 获取聊天详情 useEffect(() => { @@ -111,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; } @@ -143,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(() => { @@ -185,10 +182,13 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { // 使用警告消息作为首条消息 dispatch( addMessage({ - id: 'gmail-warning-' + Date.now(), - role: 'assistant', - content: `⚠️ ${noEmailsMessage}\n\n您仍然可以在此聊天中提问,但可能无法获得与邮件内容相关的回答。`, - created_at: new Date().toISOString(), + conversationId: chatId, + message: { + id: 'gmail-warning-' + Date.now(), + role: 'assistant', + content: `⚠️ ${noEmailsMessage}\n\n您仍然可以在此聊天中提问,但可能无法获得与邮件内容相关的回答。`, + created_at: new Date().toISOString(), + }, }) ); @@ -210,10 +210,13 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { dispatch( addMessage({ - id: 'welcome-' + Date.now(), - role: 'assistant', - content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`, - created_at: new Date().toISOString(), + conversationId: chatId, + message: { + id: 'welcome-' + Date.now(), + role: 'assistant', + content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`, + created_at: new Date().toISOString(), + }, }) ); } @@ -222,16 +225,16 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { // 监听发送消息状态 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(() => { @@ -240,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; } @@ -249,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); @@ -267,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) { // 如果是新会话,使用当前选择的知识库 @@ -362,125 +389,136 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { }; return ( -
- {/* Chat header */} -
- {conversation && conversation.datasets ? ( - <> -
{conversation.datasets.map((dataset) => dataset.name).join(', ')}
- {conversation.datasets.length > 0 && conversation.datasets[0].type && ( - 类型: {conversation.datasets[0].type} - )} - - ) : knowledgeBase ? ( - <> -
{knowledgeBase.name}
- {knowledgeBase.description} - - ) : ( -
{loading || availableDatasetsLoading ? '加载中...' : '聊天'}
- )} -
+
+ {/* Main Chat Area */} +
+ {/* Chat header */} +
+ {currentChat && currentChat.datasets ? ( + <> +
{currentChat.datasets.map((dataset) => dataset.name).join(', ')}
+ {currentChat.datasets.length > 0 && currentChat.datasets[0].type && ( + 类型: {currentChat.datasets[0].type} + )} + + ) : knowledgeBase ? ( + <> +
{knowledgeBase.name}
+ {knowledgeBase.description} + + ) : ( +
{loading || availableDatasetsLoading ? '加载中...' : '聊天'}
+ )} +
- {/* Chat messages */} -
-
- {messageStatus === 'loading' - ? renderLoading() - : messageStatus === 'failed' - ? renderError() - : messages.length === 0 - ? renderEmpty() - : messages.map((message) => ( -
+ {/* Chat messages */} +
+
+ {messageStatus === 'loading' && !messages.length + ? renderLoading() + : messageStatus === 'failed' && !messages.length + ? renderError() + : messages.length === 0 + ? renderEmpty() + : messages.map((message) => (
-
- {message.role === 'user' ? ( - message.content - ) : ( - - )} - {message.is_streaming && ( - - - - - - )} +
+
+ {message.role === 'user' ? ( + message.content + ) : ( + + )} + {message.is_streaming && ( + + + + + + )} +
+
+
+ {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 && ' · 正在生成...'}
-
- {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 && ' · 正在生成...'} -
-
- ))} - -
+ {/* Chat input */} +
+
+ setInputMessage(e.target.value)} + disabled={messageStatus === 'loading'} + /> + +
- {/* Chat input */} -
-
- setInputMessage(e.target.value)} - disabled={sendStatus === 'loading'} - /> - -
+ {/* Right Side Panel */} +
+
); diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx index fd558c7..c725172 100644 --- a/src/pages/Chat/NewChat.jsx +++ b/src/pages/Chat/NewChat.jsx @@ -25,9 +25,9 @@ export default function NewChat() { 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); diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index 4f4a15b..42945c9 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -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 (
- {/*
- -
*/}
{ - // 处理服务器无法连接的情况 - 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') || ''; diff --git a/src/services/mockApi.js b/src/services/mockApi.js deleted file mode 100644 index 10426fc..0000000 --- a/src/services/mockApi.js +++ /dev/null @@ -1,1240 +0,0 @@ -// Mock API service for development without backend -import { v4 as uuidv4 } from 'uuid'; - -// Mock data for knowledge bases -const mockKnowledgeBases = [ - { - id: uuidv4(), - user_id: 'user-001', - name: 'Frontend Development', - desc: 'Resources and guides for frontend development including React, Vue, and Angular', - type: 'private', - department: '研发部', - group: '前端开发组', - documents: [], - char_length: 0, - document_count: 0, - external_id: uuidv4(), - create_time: '2024-02-26T08:30:00Z', - update_time: '2024-02-26T14:45:00Z', - permissions: { - can_read: true, - can_edit: true, - can_delete: false, - }, - }, - { - id: uuidv4(), - user_id: 'user-001', - name: 'Backend Technologies', - desc: 'Information about backend frameworks, databases, and server configurations', - type: 'private', - department: '研发部', - group: '后端开发组', - documents: [], - char_length: 0, - document_count: 0, - external_id: uuidv4(), - create_time: '2024-02-25T10:15:00Z', - update_time: '2024-02-26T09:20:00Z', - permissions: { - can_read: true, - can_edit: true, - can_delete: false, - }, - }, - { - id: 'kb-003', - name: 'DevOps Practices', - description: 'Best practices for CI/CD, containerization, and cloud deployment', - created_at: '2023-11-12T15:45:00Z', - updated_at: '2024-02-05T11:30:00Z', - create_time: '2023-11-12T15:45:00Z', - update_time: '2024-02-05T11:30:00Z', - type: 'public', - owner: { - id: 'user-002', - username: 'janedoe', - email: 'jane@example.com', - }, - document_count: 18, - tags: ['docker', 'kubernetes', 'aws'], - permissions: { - can_edit: false, - can_read: true, - }, - }, - { - id: 'kb-004', - name: 'Machine Learning Fundamentals', - description: 'Introduction to machine learning concepts, algorithms, and frameworks', - created_at: '2023-08-20T09:00:00Z', - updated_at: '2024-01-25T16:15:00Z', - create_time: '2023-08-20T09:00:00Z', - update_time: '2024-01-25T16:15:00Z', - type: 'public', - owner: { - id: 'user-003', - username: 'alexsmith', - email: 'alex@example.com', - }, - document_count: 30, - tags: ['ml', 'python', 'tensorflow'], - permissions: { - can_edit: false, - can_read: true, - }, - }, - { - id: 'kb-005', - name: 'UI/UX Design Principles', - description: 'Guidelines for creating effective and user-friendly interfaces', - created_at: '2023-12-01T13:20:00Z', - updated_at: '2024-02-15T10:45:00Z', - create_time: '2023-12-01T13:20:00Z', - update_time: '2024-02-15T10:45:00Z', - type: 'private', - owner: { - id: 'user-002', - username: 'janedoe', - email: 'jane@example.com', - }, - document_count: 12, - tags: ['design', 'ui', 'ux'], - permissions: { - can_edit: true, - can_read: true, - }, - }, - { - id: 'kb-006', - name: 'Mobile App Development', - description: 'Resources for iOS, Android, and cross-platform mobile development', - created_at: '2023-10-25T11:10:00Z', - updated_at: '2024-01-30T14:00:00Z', - create_time: '2023-10-25T11:10:00Z', - update_time: '2024-01-30T14:00:00Z', - type: 'private', - owner: { - id: 'user-001', - username: 'johndoe', - email: 'john@example.com', - }, - document_count: 20, - tags: ['mobile', 'react-native', 'flutter'], - permissions: { - can_edit: true, - can_read: true, - }, - }, - { - id: 'kb-007', - name: 'Cybersecurity Best Practices', - description: 'Guidelines for securing applications, networks, and data', - created_at: '2023-09-18T14:30:00Z', - updated_at: '2024-02-10T09:15:00Z', - create_time: '2023-09-18T14:30:00Z', - update_time: '2024-02-10T09:15:00Z', - type: 'private', - owner: { - id: 'user-004', - username: 'sarahwilson', - email: 'sarah@example.com', - }, - document_count: 25, - tags: ['security', 'encryption', 'authentication'], - permissions: { - can_edit: false, - can_read: false, - }, - }, - { - id: 'kb-008', - name: 'Data Science Toolkit', - description: 'Tools and techniques for data analysis, visualization, and modeling', - created_at: '2023-11-05T10:00:00Z', - updated_at: '2024-01-20T15:30:00Z', - create_time: '2023-11-05T10:00:00Z', - update_time: '2024-01-20T15:30:00Z', - type: 'public', - owner: { - id: 'user-003', - username: 'alexsmith', - email: 'alex@example.com', - }, - document_count: 28, - tags: ['data-science', 'python', 'visualization'], - permissions: { - can_edit: false, - can_read: true, - }, - }, -]; - -// In-memory store for CRUD operations -let knowledgeBases = [...mockKnowledgeBases]; - -// Mock user data for authentication -const mockUsers = [ - { - id: 'user-001', - username: 'leader2', - password: 'leader123', // 在实际应用中不应该存储明文密码 - email: 'admin@example.com', - name: '管理员', - department: '研发部', - group: '前端开发组', - role: 'admin', - avatar: null, - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }, - { - id: 'user-002', - username: 'user', - password: 'user123', // 在实际应用中不应该存储明文密码 - email: 'user@example.com', - name: '普通用户', - department: '市场部', - group: '市场组', - role: 'user', - avatar: null, - created_at: '2024-01-02T00:00:00Z', - updated_at: '2024-01-02T00:00:00Z', - }, -]; - -// Helper function for pagination -const paginate = (array, page_size, page) => { - const startIndex = (page - 1) * page_size; - const endIndex = startIndex + page_size; - const items = array.slice(startIndex, endIndex); - - return { - items, - total: array.length, - page, - page_size, - }; -}; - -// Mock chat history data -const mockChatHistory = [ - { - id: 'chat-001', - title: '关于React组件开发的问题', - knowledge_base_id: 'kb-001', - knowledge_base_name: 'Frontend Development', - message_count: 5, - created_at: '2024-03-15T10:30:00Z', - updated_at: '2024-03-15T11:45:00Z', - }, - { - id: 'chat-002', - title: 'Vue.js性能优化讨论', - knowledge_base_id: 'kb-001', - knowledge_base_name: 'Frontend Development', - message_count: 3, - created_at: '2024-03-14T15:20:00Z', - updated_at: '2024-03-14T16:10:00Z', - }, - { - id: 'chat-003', - title: '后端API集成问题', - knowledge_base_id: 'kb-002', - knowledge_base_name: 'Backend Technologies', - message_count: 4, - created_at: '2024-03-13T09:15:00Z', - updated_at: '2024-03-13T10:30:00Z', - }, -]; - -// Mock chat history functions -const mockGetChatHistory = (params) => { - const { page = 1, page_size = 10 } = params; - return paginate(mockChatHistory, page_size, page); -}; - -const mockCreateChat = (data) => { - const newChat = { - id: `chat-${uuidv4().slice(0, 8)}`, - title: data.title || '新对话', - knowledge_base_id: data.knowledge_base_id, - knowledge_base_name: data.knowledge_base_name, - message_count: 0, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }; - mockChatHistory.unshift(newChat); - return newChat; -}; - -const mockUpdateChat = (id, data) => { - const index = mockChatHistory.findIndex((chat) => chat.id === id); - if (index === -1) { - throw new Error('Chat not found'); - } - const updatedChat = { - ...mockChatHistory[index], - ...data, - updated_at: new Date().toISOString(), - }; - mockChatHistory[index] = updatedChat; - return updatedChat; -}; - -const mockDeleteChat = (id) => { - const index = mockChatHistory.findIndex((chat) => chat.id === id); - if (index === -1) { - throw new Error('Chat not found'); - } - mockChatHistory.splice(index, 1); - return { success: true }; -}; - -// 模拟聊天消息数据 -const chatMessages = {}; - -// 权限申请列表的 mock 数据 -const mockPendingRequests = [ - { - id: 1, - knowledge_base: 'f13c4bdb-eb03-4ce2-b83c-30917351fb72', - applicant: 'f2799611-7a3d-436d-b3fa-3789bdd877e2', - permissions: { - can_edit: false, - can_read: true, - can_delete: false, - }, - status: 'pending', - reason: '需要访问知识库进行学习', - response_message: null, - expires_at: '2025-03-19T00:17:43.781000Z', - created_at: '2025-03-12T00:17:44.044351Z', - updated_at: '2025-03-12T00:17:44.044369Z', - }, - { - id: 2, - knowledge_base: 'f13c4bdb-eb03-4ce2-b83c-30917351fb73', - applicant: 'f2799611-7a3d-436d-b3fa-3789bdd877e3', - permissions: { - can_edit: true, - can_read: true, - can_delete: false, - }, - status: 'pending', - reason: '需要编辑和更新文档', - response_message: null, - expires_at: '2025-03-20T00:17:43.781000Z', - created_at: '2025-03-12T00:17:44.044351Z', - updated_at: '2025-03-12T00:17:44.044369Z', - }, -]; - -// 用户权限列表的 mock 数据 -const mockUserPermissions = [ - { - id: 'perm-001', - user: { - id: 'user-001', - username: 'johndoe', - name: 'John Doe', - email: 'john@example.com', - department: '研发部', - group: '前端开发组', - }, - knowledge_base: { - id: 'kb-001', - name: 'Frontend Development Guide', - }, - permissions: { - can_read: true, - can_edit: true, - can_delete: true, - can_manage: true, - }, - granted_at: '2024-01-15T10:00:00Z', - granted_by: { - id: 'user-admin', - username: 'admin', - name: 'System Admin', - }, - }, - { - id: 'perm-002', - user: { - id: 'user-002', - username: 'janedoe', - name: 'Jane Doe', - email: 'jane@example.com', - department: '研发部', - group: '前端开发组', - }, - knowledge_base: { - id: 'kb-001', - name: 'Frontend Development Guide', - }, - permissions: { - can_read: true, - can_edit: true, - can_delete: false, - can_manage: false, - }, - granted_at: '2024-01-20T14:30:00Z', - granted_by: { - id: 'user-001', - username: 'johndoe', - name: 'John Doe', - }, - }, - { - id: 'perm-003', - user: { - id: 'user-003', - username: 'alexsmith', - name: 'Alex Smith', - email: 'alex@example.com', - department: '研发部', - group: '后端开发组', - }, - knowledge_base: { - id: 'kb-001', - name: 'Frontend Development Guide', - }, - permissions: { - can_read: true, - can_edit: false, - can_delete: false, - can_manage: false, - }, - granted_at: '2024-02-01T09:15:00Z', - granted_by: { - id: 'user-001', - username: 'johndoe', - name: 'John Doe', - }, - }, -]; - -// Mock API handlers for permissions -const mockPermissionApi = { - // 获取待处理的权限申请列表 - getPendingRequests: () => { - return { - code: 200, - message: 'success', - data: { - items: mockPendingRequests, - total: mockPendingRequests.length, - }, - }; - }, - - // 获取用户权限列表 - getUserPermissions: (knowledgeBaseId) => { - const permissions = mockUserPermissions.filter((perm) => perm.knowledge_base.id === knowledgeBaseId); - return { - code: 200, - message: 'success', - data: { - items: permissions, - total: permissions.length, - }, - }; - }, - - // 处理权限申请 - handlePermissionRequest: (requestId, action) => { - const request = mockPendingRequests.find((req) => req.id === requestId); - if (!request) { - return { - code: 404, - message: 'Permission request not found', - }; - } - - request.status = action === 'approve' ? 'approved' : 'rejected'; - - if (action === 'approve') { - // 如果批准,添加新的权限记录 - const newPermission = { - id: `perm-${Date.now()}`, - user: request.user, - knowledge_base: request.knowledge_base, - permissions: { - can_read: true, - can_edit: request.request_type === 'edit', - can_delete: false, - can_manage: false, - }, - granted_at: new Date().toISOString(), - granted_by: mockCurrentUser, - }; - mockUserPermissions.push(newPermission); - } - - return { - code: 200, - message: 'success', - data: request, - }; - }, - - // 更新用户权限 - updateUserPermission: (permissionId, permissions) => { - const permission = mockUserPermissions.find((perm) => perm.id === permissionId); - if (!permission) { - return { - code: 404, - message: 'Permission not found', - }; - } - - permission.permissions = { - ...permission.permissions, - ...permissions, - }; - - return { - code: 200, - message: 'success', - data: permission, - }; - }, - - // 删除用户权限 - deleteUserPermission: (permissionId) => { - const index = mockUserPermissions.findIndex((perm) => perm.id === permissionId); - if (index === -1) { - return { - code: 404, - message: 'Permission not found', - }; - } - - mockUserPermissions.splice(index, 1); - - return { - code: 200, - message: 'success', - }; - }, -}; - -// Mock notifications data -const mockNotifications = [ - { - id: "7e383fe6-6776-4609-bfd2-76446593b3b8", - type: "permission_approved", - icon: "bi-shield-check", - title: "权限申请已通过", - content: "您对知识库 '管理员个人' 的权限申请已通过", - sender: "a9fa3c33-ca28-4ff1-b0ce-49adf0ec66f3", - receiver: "33cc280f-7bc6-4eff-b789-8434bb8c1f78", - is_read: false, - related_resource: "1", - created_at: "2025-04-12T04:49:51.724411", - hasDetail: true, - time: "1小时前" - }, - { - id: "cad476f6-1b0c-49c3-b36f-5404debf9bc2", - type: "permission_updated", - icon: "bi-shield", - title: "知识库权限更新", - content: "管理员已更新您对知识库 '测试' 的权限", - sender: "a9fa3c33-ca28-4ff1-b0ce-49adf0ec66f3", - receiver: "33cc280f-7bc6-4eff-b789-8434bb8c1f78", - is_read: false, - related_resource: "29", - created_at: "2025-04-12T04:36:43.851494", - hasDetail: true, - time: "2小时前" - }, - { - id: uuidv4(), - type: "system", - icon: "bi-info-circle", - title: "系统更新通知", - content: "系统将在今晚22:00-23:00进行维护更新,请提前保存您的工作", - sender: "system", - receiver: "all", - is_read: true, - related_resource: null, - created_at: "2025-04-11T14:30:00.000000", - hasDetail: false, - time: "1天前" - } -]; - -let notifications = [...mockNotifications]; - -// Mock API functions -export const mockGet = async (url, params = {}) => { - console.log(`[MOCK API] GET ${url}`, params); - - // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 500)); - - // Get current user - if (url === '/users/me/') { - return { - user: mockUsers[0], // 默认返回第一个用户 - }; - } - - // Get knowledge bases - if (url === '/knowledge-bases/') { - const result = paginate(knowledgeBases, params.page_size, params.page); - - return { - data: { - code: 200, - message: '获取知识库列表成功', - data: { - total: result.total, - page: result.page, - page_size: result.page_size, - items: result.items, - }, - }, - }; - } - - // Get knowledge base details - if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) { - const id = url.split('/')[2]; - const knowledgeBase = knowledgeBases.find((kb) => kb.id === id); - - if (!knowledgeBase) { - throw { response: { status: 404, data: { message: 'Knowledge base not found' } } }; - } - - return { - data: { - code: 200, - message: 'success', - data: { - knowledge_base: knowledgeBase, - }, - }, - }; - } - - // Get chat history - if (url === '/chat-history/') { - const result = mockGetChatHistory(params); - return { - data: { - code: 200, - message: 'success', - data: result, - }, - }; - } - - // Get chat messages - if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) { - const chatId = url.split('/')[2]; - - // 如果没有该聊天的消息记录,创建一个空数组 - if (!chatMessages[chatId]) { - chatMessages[chatId] = []; - - // 添加一条欢迎消息 - const chat = mockChatHistory.find((chat) => chat.id === chatId); - if (chat) { - chatMessages[chatId].push({ - id: uuidv4(), - chat_id: chatId, - sender: 'bot', - content: `欢迎使用 ${chat.knowledge_base_name},有什么可以帮助您的?`, - type: 'text', - created_at: new Date().toISOString(), - }); - } - } - - return { - code: 200, - message: '获取成功', - data: { - messages: chatMessages[chatId] || [], - }, - }; - } - - // Knowledge base search - if (url === '/knowledge-bases/search/') { - const { keyword = '', page = 1, page_size = 10 } = params.params || {}; - const filtered = knowledgeBases.filter( - (kb) => - kb.name.toLowerCase().includes(keyword.toLowerCase()) || - kb.description.toLowerCase().includes(keyword.toLowerCase()) || - kb.tags.some((tag) => tag.toLowerCase().includes(keyword.toLowerCase())) - ); - const result = paginate(filtered, page_size, page); - return { - code: 200, - message: 'success', - data: result, - }; - } - - // 用户权限管理 - 获取用户列表 - if (url === '/users/permissions/') { - return { - code: 200, - message: 'success', - data: { - users: mockUsers, - }, - }; - } - - // 用户权限管理 - 获取待处理申请 - if (url === '/permissions/pending/') { - return { - code: 200, - message: 'success', - data: { - items: mockPendingRequests, - total: mockPendingRequests.length, - }, - }; - } - - // 用户权限管理 - 获取用户权限详情 - if (url.match(/\/users\/(.+)\/permissions\//)) { - const userId = url.match(/\/users\/(.+)\/permissions\//)[1]; - - return { - code: 200, - message: 'success', - data: { - permissions: mockUserPermissions[userId] || [], - }, - }; - } - - // 获取知识库文档列表 - if (url.match(/\/knowledge-bases\/([^/]+)\/documents\//)) { - const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/documents\//)[1]; - const page = params?.params?.page || 1; - const page_size = params?.params?.page_size || 10; - - // 模拟文档列表数据 - const mockDocuments = [ - { - id: 'df6d2c2b-895c-4c56-83c8-1644345e654d', - document_id: '772044ae-0ecf-11f0-8082-0242ac120002', - document_name: '产品说明书.pdf', - external_id: '772044ae-0ecf-11f0-8082-0242ac120002', - create_time: '2023-04-01 08:01:06', - update_time: '2023-04-01 08:01:06', - }, - { - id: 'eba8f519-debf-461c-b4fd-87177d94bece', - document_id: '429a2c08-0ea3-11f0-bdec-0242ac120002', - document_name: '用户手册.docx', - external_id: '429a2c08-0ea3-11f0-bdec-0242ac120002', - create_time: '2023-04-01 02:44:38', - update_time: '2023-04-01 02:44:38', - }, - { - id: '7a9e4c31-5b2d-437e-9a8f-2b5c7e8a9d1e', - document_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e', - document_name: '技术文档.txt', - external_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e', - create_time: '2023-03-15 10:23:45', - update_time: '2023-03-15 10:23:45', - }, - ]; - - return { - data: { - code: 200, - message: '获取文档列表成功', - data: { - total: mockDocuments.length, - page: page, - page_size: page_size, - items: mockDocuments, - }, - }, - }; - } - - // Notifications API - if (url === '/notifications/') { - await mockDelay(); - return [...notifications]; - } - - // Default case (if no other match) - console.warn(`Mock GET for ${url} not implemented`); - return { code: 404, message: 'Not found', data: null }; -}; - -export const mockPost = async (url, data, isMultipart = false) => { - console.log(`[MOCK API] POST ${url}`, data); - - // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 800)); - - // Login - if (url === '/auth/login/') { - const { username, password } = data; - const user = mockUsers.find((u) => u.username === username && u.password === password); - - if (!user) { - throw { - response: { - status: 401, - data: { - code: 401, - message: '用户名或密码错误', - }, - }, - }; - } - - // 在实际应用中,这里应该生成 JWT token - const token = `mock-jwt-token-${uuidv4()}`; - - return { - code: 200, - message: '登录成功', - data: { - token, - id: user.id, - username: user.username, - email: user.email, - name: user.name, - department: user.department, - group: user.group, - role: user.role, - avatar: user.avatar, - }, - }; - } - - // Create knowledge base - if (url === '/knowledge-bases/') { - const newKnowledgeBase = { - id: `kb-${uuidv4().slice(0, 8)}`, - name: data.name, - description: data.description || '', - desc: data.desc || data.description || '', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - create_time: new Date().toISOString(), - update_time: new Date().toISOString(), - type: data.type || 'private', - department: data.department || null, - group: data.group || null, - owner: { - id: 'user-001', - username: 'johndoe', - email: 'john@example.com', - }, - document_count: 0, - tags: data.tags || [], - permissions: { - can_edit: true, - can_read: true, - }, - documents: [], - }; - - knowledgeBases.push(newKnowledgeBase); - - return { - code: 200, - message: '知识库创建成功', - data: { - knowledge_base: newKnowledgeBase, - external_id: uuidv4(), - }, - }; - } - - // Create new chat - if (url === '/chat-history/') { - const newChat = mockCreateChat(data); - return { - code: 200, - message: 'success', - data: { - chat: newChat, - }, - }; - } - - // Send chat message - if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) { - const chatId = url.split('/')[2]; - - // 如果没有该聊天的消息记录,创建一个空数组 - if (!chatMessages[chatId]) { - chatMessages[chatId] = []; - } - - // 创建用户消息 - const userMessage = { - id: uuidv4(), - chat_id: chatId, - sender: 'user', - content: data.content, - type: data.type || 'text', - created_at: new Date().toISOString(), - }; - - // 添加用户消息 - chatMessages[chatId].push(userMessage); - - // 创建机器人回复 - const botMessage = { - id: uuidv4(), - chat_id: chatId, - sender: 'bot', - content: `这是对您问题的回复:${data.content}`, - type: 'text', - created_at: new Date(Date.now() + 1000).toISOString(), - }; - - // 添加机器人回复 - chatMessages[chatId].push(botMessage); - - // 更新聊天的最后一条消息和时间 - const chatIndex = mockChatHistory.findIndex((chat) => chat.id === chatId); - if (chatIndex !== -1) { - mockChatHistory[chatIndex].message_count = (mockChatHistory[chatIndex].message_count || 0) + 2; - mockChatHistory[chatIndex].updated_at = new Date().toISOString(); - } - - return { - code: 200, - message: '发送成功', - data: { - user_message: userMessage, - bot_message: botMessage, - }, - }; - } - - // 批准权限申请 - if (url === '/permissions/approve/') { - const { id, responseMessage } = data; - - // 从待处理列表中移除该申请 - mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id); - - return { - code: 200, - message: 'Permission approved successfully', - data: { - id: id, - status: 'approved', - response_message: responseMessage, - }, - }; - } - - // 创建会话 (不发送消息) - if (url === '/chat-history/create_conversation') { - const { dataset_id_list } = data; - - // 生成新的会话ID - const conversation_id = `conv-${uuidv4()}`; - - console.log(`[MOCK API] 创建新会话: ${conversation_id}, 知识库: ${dataset_id_list.join(', ')}`); - - return { - code: 200, - message: '会话创建成功', - data: { - conversation_id: conversation_id, - dataset_id_list: dataset_id_list, - }, - }; - } - - // 拒绝权限申请 - if (url === '/permissions/reject/') { - const { id, responseMessage } = data; - - // 从待处理列表中移除该申请 - mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id); - - return { - code: 200, - message: 'Permission rejected successfully', - data: { - id: id, - status: 'rejected', - response_message: responseMessage, - }, - }; - } - - // 上传知识库文档 - if (url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)) { - const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)[1]; - const file = isMultipart ? data.get('file') : null; - - return { - data: { - code: 200, - message: 'Document uploaded successfully', - data: { - id: `doc-${Date.now()}`, - knowledge_base_id: knowledge_base_id, - filename: file ? file.name : 'mock-document.pdf', - status: 'processing', - created_at: new Date().toISOString(), - }, - }, - }; - } - - // Mark all notifications as read - if (url === '/notifications/mark-all-as-read/') { - // Update all notifications to be read - notifications.forEach(notification => { - notification.is_read = true; - }); - - return { - code: 200, - message: 'All notifications marked as read successfully', - data: { success: true } - }; - } - - // Mark a notification as read - if (url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)) { - const notificationId = url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)[1]; - const notificationIndex = notifications.findIndex(n => n.id === notificationId); - - if (notificationIndex !== -1) { - notifications[notificationIndex] = { - ...notifications[notificationIndex], - is_read: true - }; - return { - code: 200, - message: 'Notification marked as read successfully', - data: { success: true, notification: notifications[notificationIndex] } - }; - } - - return { code: 404, message: 'Notification not found', data: null }; - } - - throw { response: { status: 404, data: { message: 'Not found' } } }; -}; - -export const mockPut = async (url, data) => { - console.log('Mock PUT:', url, data); - await mockDelay(); - - // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 600)); - - // Update knowledge base - if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) { - const id = url.split('/')[2]; - const index = knowledgeBases.findIndex((kb) => kb.id === id); - - if (index === -1) { - throw { response: { status: 404, data: { message: 'Knowledge base not found' } } }; - } - - const updatedKnowledgeBase = { - ...knowledgeBases[index], - ...data, - updated_at: new Date().toISOString(), - update_time: new Date().toISOString(), - }; - - knowledgeBases[index] = updatedKnowledgeBase; - - // 返回与 mockPost 类似的格式 - return { - code: 200, - message: '知识库更新成功', - data: { - knowledge_base: updatedKnowledgeBase, - external_id: knowledgeBases[index].id, - }, - }; - } - - // Update chat - if (url.match(/^\/chat-history\/[^/]+\/$/)) { - const id = url.split('/')[2]; - return { data: mockUpdateChat(id, data) }; - } - - // 更新用户权限 - if (url.match(/\/users\/(.+)\/permissions\//)) { - const userId = url.match(/\/users\/(.+)\/permissions\//)[1]; - const { permissions } = data; - - // 将权限更新应用到模拟数据 - if (mockUserPermissions[userId]) { - // 遍历permissions对象,更新对应知识库的权限 - Object.entries(permissions).forEach(([knowledgeBaseId, permissionType]) => { - // 查找该用户的该知识库权限 - const permissionIndex = mockUserPermissions[userId].findIndex( - (p) => p.knowledge_base.id === knowledgeBaseId - ); - - if (permissionIndex !== -1) { - // 根据权限类型设置具体权限 - const permission = { - can_read: permissionType === 'read' || permissionType === 'edit' || permissionType === 'admin', - can_edit: permissionType === 'edit' || permissionType === 'admin', - can_admin: permissionType === 'admin', - }; - - // 更新权限 - mockUserPermissions[userId][permissionIndex].permission = permission; - } - }); - } - - return { - code: 200, - message: 'Permissions updated successfully', - data: { - permissions: permissions, - }, - }; - } - - // Mark notification as read - if (url.match(/\/notifications\/([^\/]+)\/read/)) { - const notificationId = url.match(/\/notifications\/([^\/]+)\/read/)[1]; - const notificationIndex = notifications.findIndex(n => n.id === notificationId); - - if (notificationIndex !== -1) { - notifications[notificationIndex] = { - ...notifications[notificationIndex], - is_read: true - }; - return { success: true, notification: notifications[notificationIndex] }; - } - - return { code: 404, message: 'Notification not found', data: null }; - } - - // Default case - console.warn(`Mock PUT for ${url} not implemented`); - return { code: 404, message: 'Not found', data: null }; -}; - -export const mockDelete = async (url) => { - console.log(`[MOCK API] DELETE ${url}`); - - // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 800)); - - // Delete knowledge base - if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) { - const id = url.split('/')[2]; - const index = knowledgeBases.findIndex((kb) => kb.id === id); - - if (index === -1) { - throw { response: { status: 404, data: { message: 'Knowledge base not found' } } }; - } - - knowledgeBases.splice(index, 1); - return { success: true }; - } - - // Delete chat (new endpoint) - if (url.match(/^\/chat-history\/delete_conversation/)) { - const params = new URLSearchParams(url.split('?')[1]); - const conversationId = params.get('conversation_id'); - - if (!conversationId) { - throw { response: { status: 400, data: { message: 'Missing conversation_id parameter' } } }; - } - - console.log(`[MOCK API] Deleting conversation: ${conversationId}`); - - // 查找并删除会话 - const index = mockChatHistory.findIndex( - (chat) => chat.id === conversationId || chat.conversation_id === conversationId - ); - - if (index === -1) { - // 即使找不到也返回成功,保持与API一致的行为 - console.log(`[MOCK API] Conversation not found: ${conversationId}, but returning success`); - return { - code: 200, - message: '会话删除成功', - data: {}, - }; - } - - mockChatHistory.splice(index, 1); - - // 清除会话消息 - if (chatMessages[conversationId]) { - delete chatMessages[conversationId]; - } - - return { - code: 200, - message: '会话删除成功', - data: {}, - }; - } - - // Delete chat (old endpoint - keeping for backward compatibility) - if (url.match(/^\/chat-history\/[^/]+\/$/)) { - const id = url.split('/')[2]; - return { data: mockDeleteChat(id) }; - } - - // 删除知识库文档 - if (url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/)) { - const matches = url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/); - const knowledge_base_id = matches[1]; - const document_id = matches[2]; - - console.log(`[MOCK API] Deleting document ${document_id} from knowledge base ${knowledge_base_id}`); - - return { - data: { - code: 200, - message: '文档删除成功', - }, - }; - } - - throw { response: { status: 404, data: { message: 'Not found' } } }; -}; - -// Reset mock data to initial state (useful for testing) -export const resetMockData = () => { - knowledgeBases = [...mockKnowledgeBases]; -}; - -// 添加权限相关的 API 处理 -export const mockApi = { - // ... existing api handlers ... - - // 权限管理相关的 API - 'GET /api/permissions/pending': () => mockPermissionApi.getPendingRequests(), - 'GET /api/permissions/users/:knowledgeBaseId': (params) => - mockPermissionApi.getUserPermissions(params.knowledgeBaseId), - 'POST /api/permissions/handle/:requestId': (params, body) => - mockPermissionApi.handlePermissionRequest(params.requestId, body.action), - 'PUT /api/permissions/:permissionId': (params, body) => - mockPermissionApi.updateUserPermission(params.permissionId, body.permissions), - 'DELETE /api/permissions/:permissionId': (params) => mockPermissionApi.deleteUserPermission(params.permissionId), -}; diff --git a/src/store/chat/chat.messages.thunks.js b/src/store/chat/chat.messages.thunks.js index 7beea0c..e6ef1a3 100644 --- a/src/store/chat/chat.messages.thunks.js +++ b/src/store/chat/chat.messages.thunks.js @@ -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 || {}; diff --git a/src/store/chat/chat.slice.js b/src/store/chat/chat.slice.js index 890acda..3d5e8ce 100644 --- a/src/store/chat/chat.slice.js +++ b/src/store/chat/chat.slice.js @@ -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; diff --git a/src/store/chat/chat.thunks.js b/src/store/chat/chat.thunks.js index 607c120..fe42f0c 100644 --- a/src/store/chat/chat.thunks.js +++ b/src/store/chat/chat.thunks.js @@ -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; } diff --git a/src/store/talentChat/talentChat.slice.js b/src/store/talentChat/talentChat.slice.js index f1825ec..51ed15b 100644 --- a/src/store/talentChat/talentChat.slice.js +++ b/src/store/talentChat/talentChat.slice.js @@ -1,84 +1,84 @@ import { createSlice } from '@reduxjs/toolkit'; import { setUserGoal, getConversationSummary, getRecommendedReply } from './talentChat.thunks'; +// 初始状态 const initialState = { - goal: { + userGoal: { + data: null, status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' - error: null, - data: { - id: null, - content: null, - created_at: null, - updated_at: null, - }, + error: null }, - summary: { + conversationSummary: { + data: null, status: 'idle', - error: null, - data: { - id: null, - talent_email: null, - conversation_id: null, - summary: null, - created_at: null, - updated_at: null, - }, + error: null }, recommendedReply: { + data: null, status: 'idle', - error: null, - reply: null, - }, + error: null + } }; +// 创建 slice const talentChatSlice = createSlice({ name: 'talentChat', initialState, - reducers: {}, + 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 - // Handle setUserGoal .addCase(setUserGoal.pending, (state) => { - state.goal.status = 'loading'; + state.userGoal.status = 'loading'; + state.userGoal.error = null; }) .addCase(setUserGoal.fulfilled, (state, action) => { - state.goal.status = 'succeeded'; - state.goal.data = action.payload.goal; - state.goal.error = null; + state.userGoal.status = 'succeeded'; + state.userGoal.data = action.payload?.data || action.payload; + state.userGoal.error = null; }) .addCase(setUserGoal.rejected, (state, action) => { - state.goal.status = 'failed'; - state.goal.error = action.error.message; + state.userGoal.status = 'failed'; + state.userGoal.error = action.payload || action.error.message; }) - // Handle getConversationSummary + // 获取会话摘要 .addCase(getConversationSummary.pending, (state) => { - state.summary.status = 'loading'; + state.conversationSummary.status = 'loading'; + state.conversationSummary.error = null; }) .addCase(getConversationSummary.fulfilled, (state, action) => { - state.summary.status = 'succeeded'; - state.summary.data = action.payload.summary; - state.summary.error = null; + state.conversationSummary.status = 'succeeded'; + state.conversationSummary.data = action.payload || null; + state.conversationSummary.error = null; }) .addCase(getConversationSummary.rejected, (state, action) => { - state.summary.status = 'failed'; - state.summary.error = action.error.message; + state.conversationSummary.status = 'failed'; + state.conversationSummary.error = action.payload || action.error.message; }) - // Handle getRecommendedReply + // 获取推荐回复 .addCase(getRecommendedReply.pending, (state) => { state.recommendedReply.status = 'loading'; + state.recommendedReply.error = null; }) .addCase(getRecommendedReply.fulfilled, (state, action) => { state.recommendedReply.status = 'succeeded'; - state.recommendedReply.reply = action.payload.reply; + 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.error.message; + state.recommendedReply.error = action.payload || action.error.message; }); - }, + } }); +export const { resetTalentChat } = talentChatSlice.actions; export default talentChatSlice.reducer; diff --git a/src/store/talentChat/talentChat.thunks.js b/src/store/talentChat/talentChat.thunks.js index a3d4d90..3958465 100644 --- a/src/store/talentChat/talentChat.thunks.js +++ b/src/store/talentChat/talentChat.thunks.js @@ -23,8 +23,8 @@ export const getConversationSummary = createAsyncThunk( talent_email: talentEmail, }; - const response = await get('/conversation-summary/', { params }); - return response; + const { summary } = await get('/conversation-summary/', { params }); + return summary; } catch (error) { return rejectWithValue( error.response?.data?.message || error.message || 'Failed to get conversation summary'