mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 05:09:44 +08:00
[PR]chats & upload files
This commit is contained in:
commit
3526fb2d60
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 { showNotification } from '../../store/notification.slice';
|
||||||
import ChatSidebar from './ChatSidebar';
|
import ChatSidebar from './ChatSidebar';
|
||||||
import NewChat from './NewChat';
|
import NewChat from './NewChat';
|
||||||
@ -51,18 +51,28 @@ export default function Chat() {
|
|||||||
if (knowledgeBaseId && !chatId && status === 'succeeded' && !status.includes('loading')) {
|
if (knowledgeBaseId && !chatId && status === 'succeeded' && !status.includes('loading')) {
|
||||||
console.log('Chat.jsx: 检查是否需要创建聊天...');
|
console.log('Chat.jsx: 检查是否需要创建聊天...');
|
||||||
|
|
||||||
// 检查是否存在包含此知识库的聊天记录
|
// 处理可能的多个知识库ID (以逗号分隔)
|
||||||
|
const knowledgeBaseIds = knowledgeBaseId.split(',').map((id) => id.trim());
|
||||||
|
console.log('Chat.jsx: 处理知识库ID列表:', knowledgeBaseIds);
|
||||||
|
|
||||||
|
// 检查是否存在包含所有选中知识库的聊天记录
|
||||||
const existingChat = chatHistory.find((chat) => {
|
const existingChat = chatHistory.find((chat) => {
|
||||||
// 检查知识库ID是否匹配
|
// 没有datasets属性或不是数组,跳过
|
||||||
if (chat.datasets && Array.isArray(chat.datasets)) {
|
if (!chat.datasets || !Array.isArray(chat.datasets)) {
|
||||||
return chat.datasets.some((ds) => ds.id === knowledgeBaseId);
|
return false;
|
||||||
}
|
}
|
||||||
// 兼容旧格式
|
|
||||||
if (chat.dataset_id_list && Array.isArray(chat.dataset_id_list)) {
|
// 获取当前聊天记录中的知识库ID列表
|
||||||
return chat.dataset_id_list.includes(knowledgeBaseId.replace(/-/g, ''));
|
const chatDatasetIds = chat.datasets.map((ds) => ds.id);
|
||||||
}
|
|
||||||
return false;
|
// 检查所有选中的知识库是否都包含在这个聊天中
|
||||||
|
// 并且聊天中的知识库数量要和选中的相同(完全匹配)
|
||||||
|
return (
|
||||||
|
knowledgeBaseIds.length === chatDatasetIds.length &&
|
||||||
|
knowledgeBaseIds.every((id) => chatDatasetIds.includes(id))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Chat.jsx: existingChat', existingChat);
|
console.log('Chat.jsx: existingChat', existingChat);
|
||||||
|
|
||||||
if (existingChat) {
|
if (existingChat) {
|
||||||
@ -73,11 +83,10 @@ export default function Chat() {
|
|||||||
navigate(`/chat/${knowledgeBaseId}/${existingChat.conversation_id}`);
|
navigate(`/chat/${knowledgeBaseId}/${existingChat.conversation_id}`);
|
||||||
} else {
|
} else {
|
||||||
console.log('Chat.jsx: 创建新聊天...');
|
console.log('Chat.jsx: 创建新聊天...');
|
||||||
// 创建新聊天
|
// 创建新聊天 - 使用新的API创建会话
|
||||||
dispatch(
|
dispatch(
|
||||||
createChatRecord({
|
createConversation({
|
||||||
dataset_id_list: [knowledgeBaseId.replace(/-/g, '')],
|
dataset_id_list: knowledgeBaseIds,
|
||||||
question: '选择当前知识库,创建聊天',
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -11,12 +11,10 @@ export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading
|
|||||||
navigate('/chat');
|
navigate('/chat');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseEnter = (id) => {
|
const handleToggleDropdown = (e, id) => {
|
||||||
setActiveDropdown(id);
|
e.preventDefault();
|
||||||
};
|
e.stopPropagation();
|
||||||
|
setActiveDropdown(activeDropdown === id ? null : id);
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setActiveDropdown(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteChat = (e, id) => {
|
const handleDeleteChat = (e, id) => {
|
||||||
@ -88,7 +86,9 @@ export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to={`/chat/${chat.datasets?.[0]?.id || knowledgeBaseId}/${chat.conversation_id}`}
|
to={`/chat/${chat.datasets?.map((ds) => ds.id).join(',') || knowledgeBaseId}/${
|
||||||
|
chat.conversation_id
|
||||||
|
}`}
|
||||||
className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${
|
className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${
|
||||||
chatId === chat.conversation_id ? 'fw-bold' : ''
|
chatId === chat.conversation_id ? 'fw-bold' : ''
|
||||||
}`}
|
}`}
|
||||||
@ -100,12 +100,13 @@ export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div
|
<div
|
||||||
className='dropdown-hover-area position-absolute end-0 top-0 bottom-0 d-none'
|
className='dropdown-area position-absolute end-0 top-0 bottom-0'
|
||||||
style={{ width: '40px' }}
|
style={{ width: '40px' }}
|
||||||
onMouseEnter={() => handleMouseEnter(chat.conversation_id)}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
>
|
||||||
<button className='btn btn-sm position-absolute end-0 top-50 translate-middle-y me-2'>
|
<button
|
||||||
|
className='btn btn-sm position-absolute end-0 top-50 translate-middle-y me-2'
|
||||||
|
onClick={(e) => handleToggleDropdown(e, chat.conversation_id)}
|
||||||
|
>
|
||||||
<SvgIcon className='more-dot' width='5' height='16' />
|
<SvgIcon className='more-dot' width='5' height='16' />
|
||||||
</button>
|
</button>
|
||||||
{activeDropdown === chat.conversation_id && (
|
{activeDropdown === chat.conversation_id && (
|
||||||
|
@ -14,6 +14,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
const [inputMessage, setInputMessage] = useState('');
|
const [inputMessage, setInputMessage] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话
|
||||||
|
|
||||||
// 从 Redux store 获取消息
|
// 从 Redux store 获取消息
|
||||||
const messages = useSelector((state) => state.chat.messages.items);
|
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 conversationStatus = useSelector((state) => state.chat.currentChat.status);
|
||||||
const conversationError = useSelector((state) => state.chat.currentChat.error);
|
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(() => {
|
useEffect(() => {
|
||||||
if (chatId) {
|
if (!chatId) return;
|
||||||
setLoading(true);
|
|
||||||
dispatch(fetchConversationDetail(chatId))
|
// 如果已经加载过这个chatId的详情,不再重复加载
|
||||||
.unwrap()
|
if (hasLoadedDetailRef.current[chatId]) {
|
||||||
.catch((error) => {
|
console.log('跳过已加载过的会话详情:', chatId);
|
||||||
// 如果是新聊天,API会返回404,此时不显示错误
|
return;
|
||||||
if (error && error !== 'Error: Request failed with status code 404') {
|
|
||||||
dispatch(
|
|
||||||
showNotification({
|
|
||||||
message: `获取聊天详情失败: ${error || '未知错误'}`,
|
|
||||||
type: 'danger',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否是新创建的会话
|
||||||
|
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 () => {
|
return () => {
|
||||||
dispatch(resetMessages());
|
dispatch(resetMessages());
|
||||||
|
// 不要清空hasLoadedDetailRef,否则会导致重复加载
|
||||||
|
// hasLoadedDetailRef.current = {}; // 清理ref缓存
|
||||||
};
|
};
|
||||||
}, [chatId, dispatch]);
|
}, [chatId, dispatch, createSessionStatus, createSessionId]);
|
||||||
|
|
||||||
|
// 组件销毁时完全清空ref缓存
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
hasLoadedDetailRef.current = {};
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 新会话自动添加欢迎消息
|
// 新会话自动添加欢迎消息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -117,18 +172,31 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
|
|
||||||
if (!inputMessage.trim() || sendStatus === 'loading') return;
|
if (!inputMessage.trim() || sendStatus === 'loading') return;
|
||||||
|
|
||||||
|
console.log('准备发送消息:', inputMessage);
|
||||||
|
console.log('当前会话ID:', chatId);
|
||||||
|
|
||||||
// 获取知识库ID列表
|
// 获取知识库ID列表
|
||||||
let dataset_id_list = [];
|
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, ''));
|
dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, ''));
|
||||||
|
console.log('使用会话中的知识库列表:', dataset_id_list);
|
||||||
} else if (knowledgeBaseId) {
|
} 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) {
|
} else if (availableDatasets.length > 0) {
|
||||||
// 如果都没有,尝试使用可用知识库列表中的第一个
|
// 如果都没有,尝试使用可用知识库列表中的第一个
|
||||||
dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')];
|
dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')];
|
||||||
|
console.log('使用可用知识库列表中的第一个:', dataset_id_list);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataset_id_list.length === 0) {
|
if (dataset_id_list.length === 0) {
|
||||||
@ -141,6 +209,12 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('发送消息参数:', {
|
||||||
|
dataset_id_list,
|
||||||
|
question: inputMessage,
|
||||||
|
conversation_id: chatId,
|
||||||
|
});
|
||||||
|
|
||||||
// 发送消息到服务器
|
// 发送消息到服务器
|
||||||
dispatch(
|
dispatch(
|
||||||
createChatRecord({
|
createChatRecord({
|
||||||
@ -150,12 +224,13 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then(() => {
|
.then((response) => {
|
||||||
// 成功发送后,可以执行任何需要的操作
|
// 成功发送后,可以执行任何需要的操作
|
||||||
// 例如:在用户发送第一条消息后更新URL中的会话ID
|
console.log('消息发送成功:', response);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
// 发送失败,显示错误信息
|
// 发送失败,显示错误信息
|
||||||
|
console.error('消息发送失败:', error);
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: `发送失败: ${error}`,
|
message: `发送失败: ${error}`,
|
||||||
@ -264,7 +339,33 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='message-time small text-muted mt-1'>
|
<div className='message-time small text-muted mt-1'>
|
||||||
{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 && ' · 正在生成...'}
|
{message.is_streaming && ' · 正在生成...'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { showNotification } from '../../store/notification.slice';
|
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';
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
|
|
||||||
export default function NewChat() {
|
export default function NewChat() {
|
||||||
@ -103,34 +103,31 @@ export default function NewChat() {
|
|||||||
|
|
||||||
if (existingChat) {
|
if (existingChat) {
|
||||||
// 找到现有聊天记录,导航到该聊天页面
|
// 找到现有聊天记录,导航到该聊天页面
|
||||||
// 使用第一个知识库ID作为URL参数
|
// 使用所有知识库ID作为URL参数,以逗号分隔
|
||||||
const primaryDatasetId = selectedDatasetIds[0];
|
const knowledgeBaseIdsParam = selectedDatasetIds.join(',');
|
||||||
console.log(`找到现有聊天记录,直接导航到 /chat/${primaryDatasetId}/${existingChat.conversation_id}`);
|
console.log(
|
||||||
navigate(`/chat/${primaryDatasetId}/${existingChat.conversation_id}`);
|
`找到现有聊天记录,直接导航到 /chat/${knowledgeBaseIdsParam}/${existingChat.conversation_id}`
|
||||||
|
);
|
||||||
|
navigate(`/chat/${knowledgeBaseIdsParam}/${existingChat.conversation_id}`);
|
||||||
} else {
|
} else {
|
||||||
// 没有找到现有聊天记录,创建新的聊天
|
// 没有找到现有聊天记录,创建新的聊天
|
||||||
console.log(`未找到现有聊天记录,直接创建新的聊天,选中的知识库ID: ${selectedDatasetIds.join(', ')}`);
|
console.log(`未找到现有聊天记录,创建新会话,选中的知识库ID: ${selectedDatasetIds.join(', ')}`);
|
||||||
|
|
||||||
// 创建新的聊天记录
|
|
||||||
const formattedIds = selectedDatasetIds.map((id) => id.replace(/-/g, ''));
|
|
||||||
console.log('格式化后的知识库ID:', formattedIds);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试创建聊天记录
|
// 调用createConversation创建新会话(不发送消息)
|
||||||
const response = await dispatch(
|
const response = await dispatch(
|
||||||
createChatRecord({
|
createConversation({
|
||||||
dataset_id_list: formattedIds,
|
dataset_id_list: selectedDatasetIds,
|
||||||
question: '选择当前知识库,创建聊天',
|
|
||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
console.log('创建聊天响应:', response);
|
console.log('创建会话响应:', response);
|
||||||
|
|
||||||
if (response && response.conversation_id) {
|
if (response && response.conversation_id) {
|
||||||
// 使用第一个知识库ID作为URL参数
|
// 使用所有知识库ID作为URL参数,以逗号分隔
|
||||||
const primaryDatasetId = selectedDatasetIds[0];
|
const knowledgeBaseIdsParam = selectedDatasetIds.join(',');
|
||||||
console.log(`创建成功,导航到 /chat/${primaryDatasetId}/${response.conversation_id}`);
|
console.log(`创建会话成功,导航到 /chat/${knowledgeBaseIdsParam}/${response.conversation_id}`);
|
||||||
navigate(`/chat/${primaryDatasetId}/${response.conversation_id}`);
|
navigate(`/chat/${knowledgeBaseIdsParam}/${response.conversation_id}`);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('未能获取会话ID:' + JSON.stringify(response));
|
throw new Error('未能获取会话ID:' + JSON.stringify(response));
|
||||||
}
|
}
|
||||||
|
@ -316,7 +316,7 @@ export default function DatasetTab({ knowledgeBase }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='w-25'>
|
<div className='w-25 d-none'>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='form-control'
|
className='form-control'
|
||||||
@ -338,7 +338,7 @@ export default function DatasetTab({ knowledgeBase }) {
|
|||||||
onDeleteDocument={handleDeleteDocument}
|
onDeleteDocument={handleDeleteDocument}
|
||||||
/>
|
/>
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className='d-flex justify-content-between align-items-center mt-3'>
|
{/* <div className='d-flex justify-content-between align-items-center mt-3'>
|
||||||
<div>
|
<div>
|
||||||
每页行数:
|
每页行数:
|
||||||
<select className='form-select form-select d-inline-block ms-2' style={{ width: '70px' }}>
|
<select className='form-select form-select d-inline-block ms-2' style={{ width: '70px' }}>
|
||||||
@ -364,7 +364,7 @@ export default function DatasetTab({ knowledgeBase }) {
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* File upload modal */}
|
{/* File upload modal */}
|
||||||
<FileUploadModal
|
<FileUploadModal
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { formatDate } from '../../../../utils/dateUtils';
|
import { formatDate } from '../../../../utils/dateUtils';
|
||||||
import { deleteKnowledgeBaseDocument } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
|
import { deleteKnowledgeBaseDocument } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||||
@ -9,10 +9,67 @@ import DocumentPreviewModal from './DocumentPreviewModal';
|
|||||||
*/
|
*/
|
||||||
const DocumentList = ({ knowledgeBaseId }) => {
|
const DocumentList = ({ knowledgeBaseId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { items, loading, pagination } = useSelector((state) => state.knowledgeBase.documents);
|
const { items, loading, error } = useSelector((state) => state.knowledgeBase.documents);
|
||||||
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
||||||
const [selectedDocumentId, setSelectedDocumentId] = useState(null);
|
const [selectedDocumentId, setSelectedDocumentId] = useState(null);
|
||||||
|
|
||||||
|
// 前端分页状态
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [displayedItems, setDisplayedItems] = useState([]);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
|
// 处理分页
|
||||||
|
useEffect(() => {
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
setDisplayedItems([]);
|
||||||
|
setTotalPages(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
const total = Math.ceil(items.length / pageSize);
|
||||||
|
setTotalPages(total);
|
||||||
|
|
||||||
|
// 确保当前页有效
|
||||||
|
let page = currentPage;
|
||||||
|
if (page > total) {
|
||||||
|
page = total;
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前页显示的项目
|
||||||
|
const startIndex = (page - 1) * pageSize;
|
||||||
|
const endIndex = Math.min(startIndex + pageSize, items.length);
|
||||||
|
setDisplayedItems(items.slice(startIndex, endIndex));
|
||||||
|
|
||||||
|
console.log(`前端分页: 总项目 ${items.length}, 当前页 ${page}/${total}, 显示 ${startIndex + 1}-${endIndex}`);
|
||||||
|
}, [items, currentPage, pageSize]);
|
||||||
|
|
||||||
|
// 分页控制
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevPage = () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
setCurrentPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextPage = () => {
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
setCurrentPage(currentPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调试输出
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('DocumentList 渲染 - 知识库ID:', knowledgeBaseId);
|
||||||
|
console.log('文档列表状态:', { items, loading, error });
|
||||||
|
console.log('文档列表项数:', items ? items.length : 0);
|
||||||
|
}, [knowledgeBaseId, items, loading, error]);
|
||||||
|
|
||||||
const handleDeleteDocument = (documentId) => {
|
const handleDeleteDocument = (documentId) => {
|
||||||
if (window.confirm('确定要删除此文档吗?')) {
|
if (window.confirm('确定要删除此文档吗?')) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -35,6 +92,7 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
console.log('DocumentList - 加载中...');
|
||||||
return (
|
return (
|
||||||
<div className='text-center py-4'>
|
<div className='text-center py-4'>
|
||||||
<div className='spinner-border text-primary' role='status'>
|
<div className='spinner-border text-primary' role='status'>
|
||||||
@ -44,7 +102,8 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
|
console.log('DocumentList - 暂无文档');
|
||||||
return (
|
return (
|
||||||
<div className='text-center py-4 text-muted'>
|
<div className='text-center py-4 text-muted'>
|
||||||
<p>暂无文档,请上传文档</p>
|
<p>暂无文档,请上传文档</p>
|
||||||
@ -52,6 +111,7 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('DocumentList - 渲染文档列表', displayedItems);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='table-responsive'>
|
<div className='table-responsive'>
|
||||||
@ -65,22 +125,30 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((doc) => (
|
{displayedItems.map((doc) => (
|
||||||
<tr key={doc.id}>
|
<tr key={doc.id || doc.document_id}>
|
||||||
<td>{doc.document_name}</td>
|
<td>
|
||||||
<td>{formatDateTime(doc.create_time)}</td>
|
<div
|
||||||
<td>{formatDateTime(doc.update_time)}</td>
|
className='text-truncate'
|
||||||
|
style={{ maxWidth: '250px' }}
|
||||||
|
title={doc.document_name || doc.name}
|
||||||
|
>
|
||||||
|
{doc.document_name || doc.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className='text-nowrap'>{formatDateTime(doc.create_time || doc.created_at)}</td>
|
||||||
|
<td className='text-nowrap'>{formatDateTime(doc.update_time || doc.updated_at)}</td>
|
||||||
<td>
|
<td>
|
||||||
<div className='btn-group' role='group'>
|
<div className='btn-group' role='group'>
|
||||||
<button
|
<button
|
||||||
className='btn btn-sm btn-outline-dark me-2'
|
className='btn btn-sm btn-outline-dark me-2'
|
||||||
onClick={() => handlePreviewDocument(doc.document_id)}
|
onClick={() => handlePreviewDocument(doc.document_id || doc.id)}
|
||||||
>
|
>
|
||||||
预览
|
预览
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className='btn btn-sm btn-outline-danger'
|
className='btn btn-sm btn-outline-danger'
|
||||||
onClick={() => handleDeleteDocument(doc.document_id)}
|
onClick={() => handleDeleteDocument(doc.document_id || doc.id)}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</button>
|
</button>
|
||||||
@ -91,9 +159,33 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{pagination.total > 0 && (
|
{/* 分页控件 */}
|
||||||
|
{items.length > 0 && (
|
||||||
<div className='d-flex justify-content-between align-items-center mt-3'>
|
<div className='d-flex justify-content-between align-items-center mt-3'>
|
||||||
<p className='text-muted mb-0'>共 {pagination.total} 条记录</p>
|
<div className='text-muted mb-0'>
|
||||||
|
共 {items.length} 条记录,第 {currentPage}/{totalPages} 页
|
||||||
|
</div>
|
||||||
|
<nav aria-label='文档列表分页'>
|
||||||
|
<ul className='pagination dark-pagination pagination-sm mb-0'>
|
||||||
|
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||||
|
<button className='page-link' onClick={handlePrevPage}>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
||||||
|
<li key={page} className={`page-item ${page === currentPage ? 'active' : ''}`}>
|
||||||
|
<button className='page-link' onClick={() => handlePageChange(page)}>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||||
|
<button className='page-link' onClick={handleNextPage}>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,6 +34,8 @@ const DocumentPreviewModal = ({ show, documentId, knowledgeBaseId, onClose }) =>
|
|||||||
// 兜底情况
|
// 兜底情况
|
||||||
setDocumentContent(response);
|
setDocumentContent(response);
|
||||||
}
|
}
|
||||||
|
console.log(documentContent);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取文档内容失败:', error);
|
console.error('获取文档内容失败:', error);
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -88,8 +90,8 @@ const DocumentPreviewModal = ({ show, documentId, knowledgeBaseId, onClose }) =>
|
|||||||
</div>
|
</div>
|
||||||
) : documentContent ? (
|
) : documentContent ? (
|
||||||
<div className='document-content'>
|
<div className='document-content'>
|
||||||
{documentContent.content &&
|
{documentContent?.paragraphs.length > 0 &&
|
||||||
documentContent.content.map((section, index) => {
|
documentContent.paragraphs.map((section, index) => {
|
||||||
let contentDisplay;
|
let contentDisplay;
|
||||||
try {
|
try {
|
||||||
// 尝试解析JSON内容
|
// 尝试解析JSON内容
|
||||||
|
@ -9,9 +9,10 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const modalRef = useRef(null);
|
const modalRef = useRef(null);
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [fileError, setFileError] = useState('');
|
const [fileError, setFileError] = useState('');
|
||||||
|
const [uploadResults, setUploadResults] = useState(null);
|
||||||
|
|
||||||
// 处理上传区域点击事件
|
// 处理上传区域点击事件
|
||||||
const handleUploadAreaClick = () => {
|
const handleUploadAreaClick = () => {
|
||||||
@ -29,59 +30,68 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
handleFileSelected(e.dataTransfer.files[0]);
|
handleFilesSelected(e.dataTransfer.files);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
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('');
|
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 = () => {
|
const resetFileInput = () => {
|
||||||
setSelectedFile(null);
|
setSelectedFiles([]);
|
||||||
setFileError('');
|
setFileError('');
|
||||||
|
setUploadResults(null);
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!selectedFile) {
|
if (selectedFiles.length === 0) {
|
||||||
setFileError('请选择要上传的文件');
|
setFileError('请至少选择一个要上传的文件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
setUploadResults(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatch(
|
const result = await dispatch(
|
||||||
uploadDocument({
|
uploadDocument({
|
||||||
knowledge_base_id: knowledgeBaseId,
|
knowledge_base_id: knowledgeBaseId,
|
||||||
file: selectedFile,
|
files: selectedFiles,
|
||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
// 成功上传后刷新文档列表
|
// 成功上传后刷新文档列表
|
||||||
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
|
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
|
||||||
|
|
||||||
// Reset the file input
|
// 显示上传结果
|
||||||
resetFileInput();
|
setUploadResults(result);
|
||||||
|
|
||||||
// 上传成功后关闭模态框
|
// 如果没有失败的文件,就在3秒后自动关闭模态窗
|
||||||
onClose();
|
if (result.failed_count === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClose();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload failed:', error);
|
console.error('Upload failed:', error);
|
||||||
setFileError('文件上传失败: ' + (error?.message || '未知错误'));
|
setFileError('文件上传失败: ' + (error?.message || '未知错误'));
|
||||||
|
|
||||||
// 清空选中的文件
|
|
||||||
resetFileInput();
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
}
|
}
|
||||||
@ -130,9 +140,11 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
|||||||
<div
|
<div
|
||||||
className='modal-content bg-white rounded shadow'
|
className='modal-content bg-white rounded shadow'
|
||||||
style={{
|
style={{
|
||||||
width: '500px',
|
width: '600px',
|
||||||
maxWidth: '90%',
|
maxWidth: '90%',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
|
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
|
||||||
@ -162,22 +174,92 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept='.pdf,.doc,.docx,.txt,.md,.csv,.xlsx,.xls'
|
accept='.pdf,.doc,.docx,.txt,.md,.csv,.xlsx,.xls'
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
|
multiple
|
||||||
/>
|
/>
|
||||||
{selectedFile ? (
|
<div>
|
||||||
<div>
|
<p className='mb-1'>点击或拖拽文件到此处上传</p>
|
||||||
<p className='mb-1'>已选择文件:</p>
|
<p className='text-muted small mb-0'>支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式</p>
|
||||||
<p className='fw-bold mb-0'>{selectedFile.name}</p>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<p className='mb-1'>点击或拖拽文件到此处上传</p>
|
|
||||||
<p className='text-muted small mb-0'>
|
|
||||||
支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{fileError && <div className='text-danger mt-2'>{fileError}</div>}
|
{fileError && <div className='text-danger mt-2'>{fileError}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 选择的文件列表 */}
|
||||||
|
{selectedFiles.length > 0 && (
|
||||||
|
<div className='mb-3'>
|
||||||
|
<p className='fw-bold mb-2'>已选择 {selectedFiles.length} 个文件:</p>
|
||||||
|
<ul className='list-group'>
|
||||||
|
{selectedFiles.map((file, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className='list-group-item d-flex justify-content-between align-items-center'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='d-flex align-items-center'
|
||||||
|
style={{ maxWidth: 'calc(100% - 70px)' }}
|
||||||
|
>
|
||||||
|
<div className='text-truncate'>{file.name}</div>
|
||||||
|
<div className='text-nowrap ms-2 text-muted' style={{ flexShrink: 0 }}>
|
||||||
|
({(file.size / 1024).toFixed(0)} KB)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isUploading && (
|
||||||
|
<button
|
||||||
|
className='btn btn-sm btn-outline-danger'
|
||||||
|
onClick={() => removeFile(index)}
|
||||||
|
style={{ minWidth: '60px' }}
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 上传结果显示 */}
|
||||||
|
{uploadResults && (
|
||||||
|
<div className='mt-3 border rounded p-3'>
|
||||||
|
<h6 className='mb-2'>上传结果</h6>
|
||||||
|
<p className='mb-2'>
|
||||||
|
总文件: {uploadResults.total_files}, 成功: {uploadResults.uploaded_count}, 失败:{' '}
|
||||||
|
{uploadResults.failed_count}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{uploadResults.documents && uploadResults.documents.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className='mb-1 fw-bold text-success'>上传成功:</p>
|
||||||
|
<ul className='list-group mb-2'>
|
||||||
|
{uploadResults.documents.map((doc) => (
|
||||||
|
<li key={doc.id} className='list-group-item py-2 d-flex align-items-center'>
|
||||||
|
<span className='badge bg-success me-2'>✓</span>
|
||||||
|
<span className='text-truncate'>{doc.name}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploadResults.failed_documents && uploadResults.failed_documents.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className='mb-1 fw-bold text-danger'>上传失败:</p>
|
||||||
|
<ul className='list-group'>
|
||||||
|
{uploadResults.failed_documents.map((doc, index) => (
|
||||||
|
<li key={index} className='list-group-item py-2 d-flex align-items-center'>
|
||||||
|
<span className='badge bg-danger me-2'>✗</span>
|
||||||
|
<div className='text-truncate'>
|
||||||
|
{doc.name}
|
||||||
|
{doc.reason && (
|
||||||
|
<small className='ms-2 text-danger'>({doc.reason})</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-footer gap-2'>
|
<div className='modal-footer gap-2'>
|
||||||
<button type='button' className='btn btn-secondary' onClick={handleClose} disabled={isUploading}>
|
<button type='button' className='btn btn-secondary' onClick={handleClose} disabled={isUploading}>
|
||||||
@ -187,7 +269,7 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
|||||||
type='button'
|
type='button'
|
||||||
className='btn btn-dark'
|
className='btn btn-dark'
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!selectedFile || isUploading}
|
disabled={selectedFiles.length === 0 || isUploading}
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<>
|
<>
|
||||||
@ -199,7 +281,7 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
|||||||
上传中...
|
上传中...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'上传文档'
|
`上传文档${selectedFiles.length > 0 ? ` (${selectedFiles.length})` : ''}`
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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/') {
|
if (url === '/permissions/reject/') {
|
||||||
const { id, responseMessage } = data;
|
const { id, responseMessage } = data;
|
||||||
@ -1026,7 +1045,47 @@ export const mockDelete = async (url) => {
|
|||||||
return { success: true };
|
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\/[^/]+\/$/)) {
|
if (url.match(/^\/chat-history\/[^/]+\/$/)) {
|
||||||
const id = url.split('/')[2];
|
const id = url.split('/')[2];
|
||||||
return { data: mockDeleteChat(id) };
|
return { data: mockDeleteChat(id) };
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
deleteChat,
|
deleteChat,
|
||||||
createChatRecord,
|
createChatRecord,
|
||||||
fetchConversationDetail,
|
fetchConversationDetail,
|
||||||
|
createConversation,
|
||||||
} from './chat.thunks';
|
} from './chat.thunks';
|
||||||
import { fetchMessages, sendMessage } from './chat.messages.thunks';
|
import { fetchMessages, sendMessage } from './chat.messages.thunks';
|
||||||
|
|
||||||
@ -276,6 +277,32 @@ const chatSlice = createSlice({
|
|||||||
state.sendMessage.error = action.error.message;
|
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) => {
|
.addCase(fetchAvailableDatasets.pending, (state) => {
|
||||||
state.availableDatasets.status = 'loading';
|
state.availableDatasets.status = 'loading';
|
||||||
|
@ -78,7 +78,7 @@ export const updateChat = createAsyncThunk('chat/updateChat', async ({ id, data
|
|||||||
*/
|
*/
|
||||||
export const deleteChat = createAsyncThunk('chat/deleteChat', async (conversationId, { rejectWithValue }) => {
|
export const deleteChat = createAsyncThunk('chat/deleteChat', async (conversationId, { rejectWithValue }) => {
|
||||||
try {
|
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) {
|
if (response && response.code === 200) {
|
||||||
@ -128,13 +128,8 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
const requestBody = {
|
const requestBody = {
|
||||||
question,
|
question,
|
||||||
dataset_id_list,
|
dataset_id_list,
|
||||||
|
conversation_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果存在对话 ID,添加到请求中
|
|
||||||
if (conversation_id) {
|
|
||||||
requestBody.conversation_id = conversation_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('准备发送聊天请求:', requestBody);
|
console.log('准备发送聊天请求:', requestBody);
|
||||||
|
|
||||||
// 先添加用户消息到聊天窗口
|
// 先添加用户消息到聊天窗口
|
||||||
@ -366,8 +361,23 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
*/
|
*/
|
||||||
export const fetchConversationDetail = createAsyncThunk(
|
export const fetchConversationDetail = createAsyncThunk(
|
||||||
'chat/fetchConversationDetail',
|
'chat/fetchConversationDetail',
|
||||||
async (conversationId, { rejectWithValue, dispatch }) => {
|
async (conversationId, { rejectWithValue, dispatch, getState }) => {
|
||||||
try {
|
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', {
|
const response = await get('/chat-history/conversation_detail', {
|
||||||
params: { conversation_id: conversationId },
|
params: { conversation_id: conversationId },
|
||||||
});
|
});
|
||||||
@ -386,8 +396,11 @@ export const fetchConversationDetail = createAsyncThunk(
|
|||||||
|
|
||||||
return rejectWithValue('获取会话详情失败');
|
return rejectWithValue('获取会话详情失败');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果是新聊天,API会返回404,此时不返回错误
|
// 明确检查是否是404错误
|
||||||
if (error.response && error.response.status === 404) {
|
const is404Error = error.response && error.response.status === 404;
|
||||||
|
|
||||||
|
if (is404Error) {
|
||||||
|
console.log('会话未找到,可能是新创建的会话:', conversationId);
|
||||||
return null;
|
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 || '创建会话失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -218,9 +218,13 @@ const knowledgeBaseSlice = createSlice({
|
|||||||
state.documents.items = action.payload.items || [];
|
state.documents.items = action.payload.items || [];
|
||||||
state.documents.pagination = {
|
state.documents.pagination = {
|
||||||
total: action.payload.total || 0,
|
total: action.payload.total || 0,
|
||||||
page: action.payload.page || 1,
|
page: 1,
|
||||||
page_size: action.payload.page_size || 10,
|
page_size: 10,
|
||||||
};
|
};
|
||||||
|
console.log('文档数据已更新到store:', {
|
||||||
|
itemsCount: state.documents.items.length,
|
||||||
|
items: state.documents.items,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.addCase(getKnowledgeBaseDocuments.rejected, (state, action) => {
|
.addCase(getKnowledgeBaseDocuments.rejected, (state, action) => {
|
||||||
state.documents.loading = false;
|
state.documents.loading = false;
|
||||||
|
@ -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 {Object} params - Upload parameters
|
||||||
* @param {string} params.knowledge_base_id - Knowledge base ID
|
* @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(
|
export const uploadDocument = createAsyncThunk(
|
||||||
'knowledgeBase/uploadDocument',
|
'knowledgeBase/uploadDocument',
|
||||||
async ({ knowledge_base_id, file }, { rejectWithValue, dispatch }) => {
|
async ({ knowledge_base_id, files }, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
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);
|
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(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'success',
|
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;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.message || error.message || '文档上传失败';
|
const errorMessage = error.response?.data?.message || error.message || '文档上传失败';
|
||||||
@ -244,24 +266,33 @@ export const uploadDocument = createAsyncThunk(
|
|||||||
* Get documents list for a knowledge base
|
* Get documents list for a knowledge base
|
||||||
* @param {Object} params - Parameters
|
* @param {Object} params - Parameters
|
||||||
* @param {string} params.knowledge_base_id - Knowledge base ID
|
* @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(
|
export const getKnowledgeBaseDocuments = createAsyncThunk(
|
||||||
'knowledgeBase/getDocuments',
|
'knowledgeBase/getDocuments',
|
||||||
async ({ knowledge_base_id, page = 1, page_size = 10 }, { rejectWithValue }) => {
|
async ({ knowledge_base_id }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await get(`/knowledge-bases/${knowledge_base_id}/documents/`, {
|
console.log('获取知识库文档列表:', knowledge_base_id);
|
||||||
params: { page, page_size },
|
const { data, code } = await get(`/knowledge-bases/${knowledge_base_id}/documents`);
|
||||||
});
|
console.log('文档列表API响应:', { data, code });
|
||||||
|
|
||||||
// 处理返回格式
|
// 处理返回格式
|
||||||
if (response.data && response.data.code === 200) {
|
if (code === 200) {
|
||||||
return response.data.data;
|
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) {
|
} catch (error) {
|
||||||
|
console.error('获取知识库文档失败:', error);
|
||||||
return rejectWithValue(error.response?.data?.message || '获取文档列表失败');
|
return rejectWithValue(error.response?.data?.message || '获取文档列表失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user