@@ -44,7 +102,8 @@ const DocumentList = ({ knowledgeBaseId }) => {
);
}
- if (items.length === 0) {
+ if (!items || items.length === 0) {
+ console.log('DocumentList - 暂无文档');
return (
- {documentContent.content &&
- documentContent.content.map((section, index) => {
+ {documentContent?.paragraphs.length > 0 &&
+ documentContent.paragraphs.map((section, index) => {
let contentDisplay;
try {
// 尝试解析JSON内容
diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
index 5078982..301526c 100644
--- a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
+++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
@@ -9,9 +9,10 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
const dispatch = useDispatch();
const fileInputRef = useRef(null);
const modalRef = useRef(null);
- const [selectedFile, setSelectedFile] = useState(null);
+ const [selectedFiles, setSelectedFiles] = useState([]);
const [isUploading, setIsUploading] = useState(false);
const [fileError, setFileError] = useState('');
+ const [uploadResults, setUploadResults] = useState(null);
// 处理上传区域点击事件
const handleUploadAreaClick = () => {
@@ -29,59 +30,68 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
e.stopPropagation();
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
- handleFileSelected(e.dataTransfer.files[0]);
+ handleFilesSelected(e.dataTransfer.files);
}
};
const handleFileChange = (e) => {
if (e.target.files && e.target.files.length > 0) {
- handleFileSelected(e.target.files[0]);
+ handleFilesSelected(e.target.files);
}
};
- const handleFileSelected = (file) => {
+ const handleFilesSelected = (files) => {
setFileError('');
- setSelectedFile(file);
+ // 将FileList转为数组
+ const filesArray = Array.from(files);
+ setSelectedFiles((prev) => [...prev, ...filesArray]);
+ };
+
+ const removeFile = (index) => {
+ setSelectedFiles((prev) => prev.filter((_, i) => i !== index));
};
const resetFileInput = () => {
- setSelectedFile(null);
+ setSelectedFiles([]);
setFileError('');
+ setUploadResults(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleUpload = async () => {
- if (!selectedFile) {
- setFileError('请选择要上传的文件');
+ if (selectedFiles.length === 0) {
+ setFileError('请至少选择一个要上传的文件');
return;
}
setIsUploading(true);
+ setUploadResults(null);
try {
- await dispatch(
+ const result = await dispatch(
uploadDocument({
knowledge_base_id: knowledgeBaseId,
- file: selectedFile,
+ files: selectedFiles,
})
).unwrap();
// 成功上传后刷新文档列表
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
- // Reset the file input
- resetFileInput();
+ // 显示上传结果
+ setUploadResults(result);
- // 上传成功后关闭模态框
- onClose();
+ // 如果没有失败的文件,就在3秒后自动关闭模态窗
+ if (result.failed_count === 0) {
+ setTimeout(() => {
+ handleClose();
+ }, 3000);
+ }
} catch (error) {
console.error('Upload failed:', error);
setFileError('文件上传失败: ' + (error?.message || '未知错误'));
-
- // 清空选中的文件
- resetFileInput();
} finally {
setIsUploading(false);
}
@@ -130,9 +140,11 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
@@ -162,22 +174,92 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
onChange={handleFileChange}
accept='.pdf,.doc,.docx,.txt,.md,.csv,.xlsx,.xls'
disabled={isUploading}
+ multiple
/>
- {selectedFile ? (
-
-
已选择文件:
-
{selectedFile.name}
-
- ) : (
-
-
点击或拖拽文件到此处上传
-
- 支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式
-
-
- )}
+
+
点击或拖拽文件到此处上传
+
支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式
+
{fileError &&
{fileError}
}
+
+ {/* 选择的文件列表 */}
+ {selectedFiles.length > 0 && (
+
+
已选择 {selectedFiles.length} 个文件:
+
+ {selectedFiles.map((file, index) => (
+ -
+
+
{file.name}
+
+ ({(file.size / 1024).toFixed(0)} KB)
+
+
+ {!isUploading && (
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* 上传结果显示 */}
+ {uploadResults && (
+
+
上传结果
+
+ 总文件: {uploadResults.total_files}, 成功: {uploadResults.uploaded_count}, 失败:{' '}
+ {uploadResults.failed_count}
+
+
+ {uploadResults.documents && uploadResults.documents.length > 0 && (
+ <>
+
上传成功:
+
+ {uploadResults.documents.map((doc) => (
+ -
+ ✓
+ {doc.name}
+
+ ))}
+
+ >
+ )}
+
+ {uploadResults.failed_documents && uploadResults.failed_documents.length > 0 && (
+ <>
+
上传失败:
+
+ >
+ )}
+
+ )}
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 || '创建会话失败');
+ }
+ }
+);
diff --git a/src/store/knowledgeBase/knowledgeBase.slice.js b/src/store/knowledgeBase/knowledgeBase.slice.js
index 95c45dd..e7c925c 100644
--- a/src/store/knowledgeBase/knowledgeBase.slice.js
+++ b/src/store/knowledgeBase/knowledgeBase.slice.js
@@ -218,9 +218,13 @@ const knowledgeBaseSlice = createSlice({
state.documents.items = action.payload.items || [];
state.documents.pagination = {
total: action.payload.total || 0,
- page: action.payload.page || 1,
- page_size: action.payload.page_size || 10,
+ page: 1,
+ page_size: 10,
};
+ console.log('文档数据已更新到store:', {
+ itemsCount: state.documents.items.length,
+ items: state.documents.items,
+ });
})
.addCase(getKnowledgeBaseDocuments.rejected, (state, action) => {
state.documents.loading = false;
diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js
index 959dc09..62bf221 100644
--- a/src/store/knowledgeBase/knowledgeBase.thunks.js
+++ b/src/store/knowledgeBase/knowledgeBase.thunks.js
@@ -200,32 +200,54 @@ export const requestKnowledgeBaseAccess = createAsyncThunk(
);
/**
- * Upload a document to a knowledge base
+ * Upload documents to a knowledge base
* @param {Object} params - Upload parameters
* @param {string} params.knowledge_base_id - Knowledge base ID
- * @param {File} params.file - File to upload
+ * @param {File[]} params.files - Files to upload
*/
export const uploadDocument = createAsyncThunk(
'knowledgeBase/uploadDocument',
- async ({ knowledge_base_id, file }, { rejectWithValue, dispatch }) => {
+ async ({ knowledge_base_id, files }, { rejectWithValue, dispatch }) => {
try {
const formData = new FormData();
- formData.append('file', file);
+
+ // 支持单文件和多文件上传
+ if (Array.isArray(files)) {
+ // 多文件上传
+ files.forEach(file => {
+ formData.append('files', file);
+ });
+ } else {
+ // 单文件上传(向后兼容)
+ formData.append('files', files);
+ }
const response = await post(`/knowledge-bases/${knowledge_base_id}/upload_document/`, formData, true);
+ // 处理新的返回格式
+ if (response.data && response.data.code === 200) {
+ const result = response.data.data;
+
+ // 使用API返回的消息作为通知
+ dispatch(
+ showNotification({
+ type: 'success',
+ message: response.data.message || `文档上传完成,成功: ${result.uploaded_count},失败: ${result.failed_count}`,
+ })
+ );
+
+ return result;
+ }
+
dispatch(
showNotification({
type: 'success',
- message: `文档 ${file.name} 上传成功`,
+ message: Array.isArray(files)
+ ? `${files.length} 个文档上传成功`
+ : `文档 ${files.name} 上传成功`,
})
);
- // 处理新的返回格式
- if (response.data && response.data.code === 200) {
- return response.data.data;
- }
-
return response.data;
} catch (error) {
const errorMessage = error.response?.data?.message || error.message || '文档上传失败';
@@ -244,24 +266,33 @@ export const uploadDocument = createAsyncThunk(
* Get documents list for a knowledge base
* @param {Object} params - Parameters
* @param {string} params.knowledge_base_id - Knowledge base ID
- * @param {number} params.page - Page number (default: 1)
- * @param {number} params.page_size - Page size (default: 10)
*/
export const getKnowledgeBaseDocuments = createAsyncThunk(
'knowledgeBase/getDocuments',
- async ({ knowledge_base_id, page = 1, page_size = 10 }, { rejectWithValue }) => {
+ async ({ knowledge_base_id }, { rejectWithValue }) => {
try {
- const response = await get(`/knowledge-bases/${knowledge_base_id}/documents/`, {
- params: { page, page_size },
- });
-
+ console.log('获取知识库文档列表:', knowledge_base_id);
+ const { data, code } = await get(`/knowledge-bases/${knowledge_base_id}/documents`);
+ console.log('文档列表API响应:', { data, code });
+
// 处理返回格式
- if (response.data && response.data.code === 200) {
- return response.data.data;
+ if (code === 200) {
+ console.log('API返回数据:', data);
+ return {
+ items: data || [],
+ total: (data || []).length
+ };
+ } else {
+ // 未知格式,尝试提取数据
+ const items = data?.items || data || [];
+ console.log('未识别格式,提取数据:', items);
+ return {
+ items: items,
+ total: items.length
+ };
}
-
- return response.data;
} catch (error) {
+ console.error('获取知识库文档失败:', error);
return rejectWithValue(error.response?.data?.message || '获取文档列表失败');
}
}