From 518c71b859134db761ddbc19374bac2d0ad8534b Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Mon, 7 Apr 2025 19:57:23 -0400 Subject: [PATCH] [dev]update chat & delete chat --- src/pages/Chat/Chat.jsx | 37 ++++---- src/pages/Chat/ChatSidebar.jsx | 23 ++--- src/pages/Chat/ChatWindow.jsx | 149 +++++++++++++++++++++++++++------ src/pages/Chat/NewChat.jsx | 35 ++++---- src/services/mockApi.js | 61 +++++++++++++- src/store/chat/chat.slice.js | 27 ++++++ src/store/chat/chat.thunks.js | 113 ++++++++++++++++++++++--- 7 files changed, 366 insertions(+), 79 deletions(-) diff --git a/src/pages/Chat/Chat.jsx b/src/pages/Chat/Chat.jsx index bbd4723..ae16ed3 100644 --- a/src/pages/Chat/Chat.jsx +++ b/src/pages/Chat/Chat.jsx @@ -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 } from '../../store/chat/chat.thunks'; +import { fetchChats, deleteChat, createChatRecord, createConversation } from '../../store/chat/chat.thunks'; import { showNotification } from '../../store/notification.slice'; import ChatSidebar from './ChatSidebar'; import NewChat from './NewChat'; @@ -51,18 +51,28 @@ export default function Chat() { if (knowledgeBaseId && !chatId && status === 'succeeded' && !status.includes('loading')) { console.log('Chat.jsx: 检查是否需要创建聊天...'); - // 检查是否存在包含此知识库的聊天记录 + // 处理可能的多个知识库ID (以逗号分隔) + const knowledgeBaseIds = knowledgeBaseId.split(',').map((id) => id.trim()); + console.log('Chat.jsx: 处理知识库ID列表:', knowledgeBaseIds); + + // 检查是否存在包含所有选中知识库的聊天记录 const existingChat = chatHistory.find((chat) => { - // 检查知识库ID是否匹配 - if (chat.datasets && Array.isArray(chat.datasets)) { - return chat.datasets.some((ds) => ds.id === knowledgeBaseId); + // 没有datasets属性或不是数组,跳过 + if (!chat.datasets || !Array.isArray(chat.datasets)) { + return false; } - // 兼容旧格式 - if (chat.dataset_id_list && Array.isArray(chat.dataset_id_list)) { - return chat.dataset_id_list.includes(knowledgeBaseId.replace(/-/g, '')); - } - return false; + + // 获取当前聊天记录中的知识库ID列表 + const chatDatasetIds = chat.datasets.map((ds) => ds.id); + + // 检查所有选中的知识库是否都包含在这个聊天中 + // 并且聊天中的知识库数量要和选中的相同(完全匹配) + return ( + knowledgeBaseIds.length === chatDatasetIds.length && + knowledgeBaseIds.every((id) => chatDatasetIds.includes(id)) + ); }); + console.log('Chat.jsx: existingChat', existingChat); if (existingChat) { @@ -73,11 +83,10 @@ export default function Chat() { navigate(`/chat/${knowledgeBaseId}/${existingChat.conversation_id}`); } else { console.log('Chat.jsx: 创建新聊天...'); - // 创建新聊天 + // 创建新聊天 - 使用新的API创建会话 dispatch( - createChatRecord({ - dataset_id_list: [knowledgeBaseId.replace(/-/g, '')], - question: '选择当前知识库,创建聊天', + createConversation({ + dataset_id_list: knowledgeBaseIds, }) ) .unwrap() diff --git a/src/pages/Chat/ChatSidebar.jsx b/src/pages/Chat/ChatSidebar.jsx index f76baf7..766482d 100644 --- a/src/pages/Chat/ChatSidebar.jsx +++ b/src/pages/Chat/ChatSidebar.jsx @@ -11,12 +11,10 @@ export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading navigate('/chat'); }; - const handleMouseEnter = (id) => { - setActiveDropdown(id); - }; - - const handleMouseLeave = () => { - setActiveDropdown(null); + const handleToggleDropdown = (e, id) => { + e.preventDefault(); + e.stopPropagation(); + setActiveDropdown(activeDropdown === id ? null : id); }; const handleDeleteChat = (e, id) => { @@ -88,7 +86,9 @@ export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading }`} > ds.id).join(',') || knowledgeBaseId}/${ + chat.conversation_id + }`} className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${ chatId === chat.conversation_id ? 'fw-bold' : '' }`} @@ -100,12 +100,13 @@ export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading
handleMouseEnter(chat.conversation_id)} - onMouseLeave={handleMouseLeave} > - {activeDropdown === chat.conversation_id && ( diff --git a/src/pages/Chat/ChatWindow.jsx b/src/pages/Chat/ChatWindow.jsx index fe1782c..82ecf2a 100644 --- a/src/pages/Chat/ChatWindow.jsx +++ b/src/pages/Chat/ChatWindow.jsx @@ -14,6 +14,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { const [inputMessage, setInputMessage] = useState(''); const [loading, setLoading] = useState(false); const messagesEndRef = useRef(null); + const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话 // 从 Redux store 获取消息 const messages = useSelector((state) => state.chat.messages.items); @@ -35,33 +36,87 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { const conversationStatus = useSelector((state) => state.chat.currentChat.status); const conversationError = useSelector((state) => state.chat.currentChat.error); + // 获取会话创建状态 + const createSessionStatus = useSelector((state) => state.chat.createSession?.status); + const createSessionId = useSelector((state) => state.chat.createSession?.sessionId); + + // 监听知识库ID变更,确保保存在组件状态中 + const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]); + + // 当conversation或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds + useEffect(() => { + // 优先使用conversation中的知识库列表 + if (conversation && conversation.datasets && conversation.datasets.length > 0) { + const datasetIds = conversation.datasets.map((ds) => ds.id); + console.log('从会话中获取知识库列表:', datasetIds); + setSelectedKnowledgeBaseIds(datasetIds); + } + // 其次使用URL中传入的知识库ID + else if (knowledgeBaseId) { + // 可能是单个ID或以逗号分隔的多个ID + const ids = knowledgeBaseId.split(',').map((id) => id.trim()); + console.log('从URL参数中获取知识库列表:', ids); + setSelectedKnowledgeBaseIds(ids); + } + }, [conversation, knowledgeBaseId]); + // 获取聊天详情 useEffect(() => { - if (chatId) { - setLoading(true); - dispatch(fetchConversationDetail(chatId)) - .unwrap() - .catch((error) => { - // 如果是新聊天,API会返回404,此时不显示错误 - if (error && error !== 'Error: Request failed with status code 404') { - dispatch( - showNotification({ - message: `获取聊天详情失败: ${error || '未知错误'}`, - type: 'danger', - }) - ); - } - }) - .finally(() => { - setLoading(false); - }); + if (!chatId) return; + + // 如果已经加载过这个chatId的详情,不再重复加载 + if (hasLoadedDetailRef.current[chatId]) { + console.log('跳过已加载过的会话详情:', chatId); + return; } + // 检查是否是新创建的会话 + const isNewlyCreatedChat = createSessionStatus === 'succeeded' && createSessionId === chatId; + + // 如果是新创建的会话且已经有会话数据,则跳过详情获取 + if (isNewlyCreatedChat && conversation && conversation.conversation_id === chatId) { + console.log('跳过新创建会话的详情获取:', chatId); + hasLoadedDetailRef.current[chatId] = true; + return; + } + + console.log('获取会话详情:', chatId); + setLoading(true); + + dispatch(fetchConversationDetail(chatId)) + .unwrap() + .then((response) => { + console.log('获取会话详情成功:', response); + // 标记为已加载 + hasLoadedDetailRef.current[chatId] = true; + }) + .catch((error) => { + console.error('获取会话详情失败:', error); + dispatch( + showNotification({ + message: `获取聊天详情失败: ${error || '未知错误'}`, + type: 'danger', + }) + ); + }) + .finally(() => { + setLoading(false); + }); + // 组件卸载时清空消息 return () => { dispatch(resetMessages()); + // 不要清空hasLoadedDetailRef,否则会导致重复加载 + // hasLoadedDetailRef.current = {}; // 清理ref缓存 }; - }, [chatId, dispatch]); + }, [chatId, dispatch, createSessionStatus, createSessionId]); + + // 组件销毁时完全清空ref缓存 + useEffect(() => { + return () => { + hasLoadedDetailRef.current = {}; + }; + }, []); // 新会话自动添加欢迎消息 useEffect(() => { @@ -117,18 +172,31 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { if (!inputMessage.trim() || sendStatus === 'loading') return; + console.log('准备发送消息:', inputMessage); + console.log('当前会话ID:', chatId); + // 获取知识库ID列表 let dataset_id_list = []; - if (conversation && conversation.datasets) { + // 优先使用组件状态中保存的知识库列表 + if (selectedKnowledgeBaseIds.length > 0) { + // 使用已保存的知识库列表 + dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, '')); + console.log('使用组件状态中的知识库列表:', dataset_id_list); + } else if (conversation && conversation.datasets && conversation.datasets.length > 0) { // 如果已有会话,使用会话中的知识库 dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, '')); + console.log('使用会话中的知识库列表:', dataset_id_list); } else if (knowledgeBaseId) { // 如果是新会话,使用当前选择的知识库 - dataset_id_list = [knowledgeBaseId.replace(/-/g, '')]; + // 可能是单个ID或以逗号分隔的多个ID + const ids = knowledgeBaseId.split(',').map((id) => id.trim().replace(/-/g, '')); + dataset_id_list = ids; + console.log('使用URL参数中的知识库:', dataset_id_list); } else if (availableDatasets.length > 0) { // 如果都没有,尝试使用可用知识库列表中的第一个 dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')]; + console.log('使用可用知识库列表中的第一个:', dataset_id_list); } if (dataset_id_list.length === 0) { @@ -141,6 +209,12 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { return; } + console.log('发送消息参数:', { + dataset_id_list, + question: inputMessage, + conversation_id: chatId, + }); + // 发送消息到服务器 dispatch( createChatRecord({ @@ -150,12 +224,13 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { }) ) .unwrap() - .then(() => { + .then((response) => { // 成功发送后,可以执行任何需要的操作 - // 例如:在用户发送第一条消息后更新URL中的会话ID + console.log('消息发送成功:', response); }) .catch((error) => { // 发送失败,显示错误信息 + console.error('消息发送失败:', error); dispatch( showNotification({ message: `发送失败: ${error}`, @@ -264,7 +339,33 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
- {message.created_at && new Date(message.created_at).toLocaleTimeString()} + {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 && ' · 正在生成...'}
diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx index 69ede59..809d1de 100644 --- a/src/pages/Chat/NewChat.jsx +++ b/src/pages/Chat/NewChat.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { showNotification } from '../../store/notification.slice'; -import { fetchAvailableDatasets, fetchChats, createChatRecord } from '../../store/chat/chat.thunks'; +import { fetchAvailableDatasets, fetchChats, createConversation } from '../../store/chat/chat.thunks'; import SvgIcon from '../../components/SvgIcon'; export default function NewChat() { @@ -103,34 +103,31 @@ export default function NewChat() { if (existingChat) { // 找到现有聊天记录,导航到该聊天页面 - // 使用第一个知识库ID作为URL参数 - const primaryDatasetId = selectedDatasetIds[0]; - console.log(`找到现有聊天记录,直接导航到 /chat/${primaryDatasetId}/${existingChat.conversation_id}`); - navigate(`/chat/${primaryDatasetId}/${existingChat.conversation_id}`); + // 使用所有知识库ID作为URL参数,以逗号分隔 + const knowledgeBaseIdsParam = selectedDatasetIds.join(','); + console.log( + `找到现有聊天记录,直接导航到 /chat/${knowledgeBaseIdsParam}/${existingChat.conversation_id}` + ); + navigate(`/chat/${knowledgeBaseIdsParam}/${existingChat.conversation_id}`); } else { // 没有找到现有聊天记录,创建新的聊天 - console.log(`未找到现有聊天记录,直接创建新的聊天,选中的知识库ID: ${selectedDatasetIds.join(', ')}`); - - // 创建新的聊天记录 - const formattedIds = selectedDatasetIds.map((id) => id.replace(/-/g, '')); - console.log('格式化后的知识库ID:', formattedIds); + console.log(`未找到现有聊天记录,创建新会话,选中的知识库ID: ${selectedDatasetIds.join(', ')}`); try { - // 尝试创建聊天记录 + // 调用createConversation创建新会话(不发送消息) const response = await dispatch( - createChatRecord({ - dataset_id_list: formattedIds, - question: '选择当前知识库,创建聊天', + createConversation({ + dataset_id_list: selectedDatasetIds, }) ).unwrap(); - console.log('创建聊天响应:', response); + console.log('创建会话响应:', response); if (response && response.conversation_id) { - // 使用第一个知识库ID作为URL参数 - const primaryDatasetId = selectedDatasetIds[0]; - console.log(`创建成功,导航到 /chat/${primaryDatasetId}/${response.conversation_id}`); - navigate(`/chat/${primaryDatasetId}/${response.conversation_id}`); + // 使用所有知识库ID作为URL参数,以逗号分隔 + const knowledgeBaseIdsParam = selectedDatasetIds.join(','); + console.log(`创建会话成功,导航到 /chat/${knowledgeBaseIdsParam}/${response.conversation_id}`); + navigate(`/chat/${knowledgeBaseIdsParam}/${response.conversation_id}`); } else { throw new Error('未能获取会话ID:' + JSON.stringify(response)); } diff --git a/src/services/mockApi.js b/src/services/mockApi.js index 0eae4f9..dd31eb5 100644 --- a/src/services/mockApi.js +++ b/src/services/mockApi.js @@ -885,6 +885,25 @@ export const mockPost = async (url, data, isMultipart = false) => { }; } + // 创建会话 (不发送消息) + 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; @@ -1026,7 +1045,47 @@ export const mockDelete = async (url) => { return { success: true }; } - // Delete chat + // 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) }; diff --git a/src/store/chat/chat.slice.js b/src/store/chat/chat.slice.js index 328746b..890acda 100644 --- a/src/store/chat/chat.slice.js +++ b/src/store/chat/chat.slice.js @@ -7,6 +7,7 @@ import { deleteChat, createChatRecord, fetchConversationDetail, + createConversation, } from './chat.thunks'; import { fetchMessages, sendMessage } from './chat.messages.thunks'; @@ -276,6 +277,32 @@ const chatSlice = createSlice({ state.sendMessage.error = action.error.message; }) + // 处理创建会话 + .addCase(createConversation.pending, (state) => { + state.createSession.status = 'loading'; + state.createSession.error = null; + }) + .addCase(createConversation.fulfilled, (state, action) => { + state.createSession.status = 'succeeded'; + state.createSession.sessionId = 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; + }) + .addCase(createConversation.rejected, (state, action) => { + state.createSession.status = 'failed'; + state.createSession.error = action.payload || action.error.message; + }) + // 处理获取可用知识库 .addCase(fetchAvailableDatasets.pending, (state) => { state.availableDatasets.status = 'loading'; diff --git a/src/store/chat/chat.thunks.js b/src/store/chat/chat.thunks.js index a50ff05..607c120 100644 --- a/src/store/chat/chat.thunks.js +++ b/src/store/chat/chat.thunks.js @@ -78,7 +78,7 @@ export const updateChat = createAsyncThunk('chat/updateChat', async ({ id, data */ export const deleteChat = createAsyncThunk('chat/deleteChat', async (conversationId, { rejectWithValue }) => { try { - const response = await del(`/chat-history/conversation/${conversationId}/`); + const response = await del(`/chat-history/delete_conversation?conversation_id=${conversationId}`); // 处理返回格式 if (response && response.code === 200) { @@ -128,13 +128,8 @@ export const createChatRecord = createAsyncThunk( const requestBody = { question, dataset_id_list, + conversation_id, }; - - // 如果存在对话 ID,添加到请求中 - if (conversation_id) { - requestBody.conversation_id = conversation_id; - } - console.log('准备发送聊天请求:', requestBody); // 先添加用户消息到聊天窗口 @@ -366,8 +361,23 @@ export const createChatRecord = createAsyncThunk( */ export const fetchConversationDetail = createAsyncThunk( 'chat/fetchConversationDetail', - async (conversationId, { rejectWithValue, dispatch }) => { + 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 }, }); @@ -386,8 +396,11 @@ export const fetchConversationDetail = createAsyncThunk( return rejectWithValue('获取会话详情失败'); } catch (error) { - // 如果是新聊天,API会返回404,此时不返回错误 - if (error.response && error.response.status === 404) { + // 明确检查是否是404错误 + const is404Error = error.response && error.response.status === 404; + + if (is404Error) { + console.log('会话未找到,可能是新创建的会话:', conversationId); return null; } @@ -396,3 +409,83 @@ export const fetchConversationDetail = createAsyncThunk( } } ); + +/** + * 创建新会话(仅获取会话ID,不发送消息) + * @param {Object} params - 参数 + * @param {string[]} params.dataset_id_list - 知识库ID列表 + */ +export const createConversation = createAsyncThunk( + 'chat/createConversation', + async ({ dataset_id_list }, { dispatch, getState, rejectWithValue }) => { + try { + console.log('创建新会话,知识库ID列表:', dataset_id_list); + const params = { + dataset_id_list: dataset_id_list, + }; + const response = await post('/chat-history/create_conversation/', params); + + if (response && response.code === 200) { + const conversationData = response.data; + console.log('会话创建成功:', conversationData); + + // 获取知识库信息 + const state = getState(); + const availableDatasets = state.chat.availableDatasets.items || []; + + // 创建一个新的聊天记录对象添加到历史列表 + const newChatEntry = { + conversation_id: conversationData.conversation_id, + 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 || '新知识库对话', + }; + }), + create_time: new Date().toISOString(), + last_message: '', + message_count: 0, + }; + + // 更新聊天历史列表 + dispatch({ + type: 'chat/fetchChats/fulfilled', + payload: { + results: [newChatEntry], + total: 1, + append: true, // 标记为追加,而不是替换 + }, + }); + + // 设置为当前聊天 + dispatch( + setCurrentChat({ + conversation_id: conversationData.conversation_id, + datasets: newChatEntry.datasets, + }) + ); + + return conversationData; + } + + return rejectWithValue('创建会话失败'); + } catch (error) { + console.error('创建会话失败:', error); + + // 显示错误通知 + dispatch( + showNotification({ + message: `创建会话失败: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + + return rejectWithValue(error.message || '创建会话失败'); + } + } +);