-
+
+ {chat.title &&
{chat.title}
}
+
{chat.datasets?.map((ds) => ds.name).join(', ') || '未命名知识库'}
diff --git a/src/pages/Chat/ChatWindow.jsx b/src/pages/Chat/ChatWindow.jsx
index 82ecf2a..7168f26 100644
--- a/src/pages/Chat/ChatWindow.jsx
+++ b/src/pages/Chat/ChatWindow.jsx
@@ -1,12 +1,12 @@
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 { 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 ResourceList from '../../components/ResourceList';
import { get } from '../../services/api';
export default function ChatWindow({ chatId, knowledgeBaseId }) {
@@ -17,11 +17,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话
// 从 Redux store 获取消息
- const messages = useSelector((state) => state.chat.messages.items);
- const messageStatus = useSelector((state) => state.chat.messages.status);
- const messageError = useSelector((state) => state.chat.messages.error);
+ const chatList = useSelector((state) => state.chat.list.items);
+ const currentChatId = useSelector((state) => state.chat.currentChat.conversationId || chatId);
+ const currentChat = chatList.find((chat) => chat.conversation_id === currentChatId);
+ const messages = currentChat?.messages || [];
+ const messageStatus = useSelector((state) => state.chat.list.messageStatus);
+ const messageError = useSelector((state) => state.chat.list.messageError);
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
+ // 获取消息资源
+ const resources = useSelector((state) => state.chat.resources);
+
// 使用新的Redux状态结构
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId);
@@ -43,6 +49,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
// 监听知识库ID变更,确保保存在组件状态中
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]);
+ // 当chatId改变时设置当前会话ID
+ useEffect(() => {
+ if (chatId) {
+ // 通过设置currentChat.conversationId确保消息显示在正确的会话下
+ dispatch({
+ type: 'chat/setCurrentChat',
+ payload: { conversation_id: chatId },
+ });
+ }
+ }, [chatId, dispatch]);
+
// 当conversation或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds
useEffect(() => {
// 优先使用conversation中的知识库列表
@@ -105,7 +122,8 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
// 组件卸载时清空消息
return () => {
- dispatch(resetMessages());
+ // Don't reset messages when switching chats
+ // dispatch(resetMessages());
// 不要清空hasLoadedDetailRef,否则会导致重复加载
// hasLoadedDetailRef.current = {}; // 清理ref缓存
};
@@ -253,19 +271,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
);
- // 渲染错误状态
- const renderError = () => (
-
-
- 加载消息失败
-
-
{messageError}
-
-
- );
-
// 渲染空消息状态
const renderEmpty = () => {
if (loading) return null;
@@ -283,16 +288,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
{conversation && conversation.datasets ? (
<>
-
{conversation.datasets.map((dataset) => dataset.name).join(', ')}
+
{conversation.title}
{conversation.datasets.length > 0 && conversation.datasets[0].type && (
类型: {conversation.datasets[0].type}
)}
>
- ) : knowledgeBase ? (
- <>
-
{knowledgeBase.name}
-
{knowledgeBase.description}
- >
) : (
{loading || availableDatasetsLoading ? '加载中...' : '聊天'}
)}
@@ -303,8 +303,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
{messageStatus === 'loading'
? renderLoading()
- : messageStatus === 'failed'
- ? renderError()
: messages.length === 0
? renderEmpty()
: messages.map((message) => (
@@ -327,7 +325,10 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
{message.role === 'user' ? (
message.content
) : (
-
+
)}
{message.is_streaming && (
@@ -336,6 +337,14 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
)}
+
+ {/* 只在AI回复消息下方显示资源列表 */}
+ {message.role === 'assistant' &&
+ !message.is_streaming &&
+ resources.messageId === message.id &&
+ resources.items.length > 0 && (
+
+ )}
diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx
index 809d1de..e4f8d92 100644
--- a/src/pages/Chat/NewChat.jsx
+++ b/src/pages/Chat/NewChat.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { showNotification } from '../../store/notification.slice';
@@ -17,14 +17,14 @@ 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 chatHistory = useSelector((state) => state.chat.list.items || []);
+ const chatHistoryLoading = useSelector((state) => state.chat.list.status === 'loading');
const chatCreationStatus = useSelector((state) => state.chat.sendMessage?.status);
// 获取可用知识库列表和聊天历史
useEffect(() => {
dispatch(fetchAvailableDatasets());
- dispatch(fetchChats({ page: 1, page_size: 50 }));
+ dispatch(fetchChats());
}, [dispatch]);
// 监听错误状态
@@ -77,65 +77,31 @@ export default function NewChat() {
// 打印调试信息
console.log('选中的知识库ID:', selectedDatasetIds);
- // 检查是否已存在包含所有选中知识库的聊天记录
- // 注意:这里的逻辑简化了,实际可能需要更复杂的匹配算法
- const existingChat = chatHistory.find((chat) => {
- // 检查聊天记录中的知识库是否完全匹配当前选择
- if (chat.datasets && Array.isArray(chat.datasets)) {
- const chatDatasetIds = chat.datasets.map((ds) => ds.id);
- return (
- chatDatasetIds.length === selectedDatasetIds.length &&
- selectedDatasetIds.every((id) => chatDatasetIds.includes(id))
- );
- }
-
- // 兼容旧格式
- if (chat.dataset_id_list && Array.isArray(chat.dataset_id_list)) {
- const formattedSelectedIds = selectedDatasetIds.map((id) => id.replace(/-/g, ''));
- return (
- chat.dataset_id_list.length === formattedSelectedIds.length &&
- formattedSelectedIds.every((id) => chat.dataset_id_list.includes(id))
- );
- }
-
- return false;
- });
-
- if (existingChat) {
- // 找到现有聊天记录,导航到该聊天页面
- // 使用所有知识库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(', ')}`);
-
- try {
- // 调用createConversation创建新会话(不发送消息)
- const response = await dispatch(
- createConversation({
- dataset_id_list: selectedDatasetIds,
- })
- ).unwrap();
-
- console.log('创建会话响应:', response);
-
- if (response && 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));
- }
- } catch (apiError) {
- // 专门处理API调用错误
- console.error('API调用失败:', apiError);
- throw new Error(`API调用失败: ${apiError.message || '未知错误'}`);
+ // 创建新的聊天会话
+ console.log(`创建新会话,选中的知识库ID: ${selectedDatasetIds.join(', ')}`);
+
+ try {
+ // 调用createConversation创建新会话(不发送消息)
+ const response = await dispatch(
+ createConversation({
+ dataset_id_list: selectedDatasetIds,
+ })
+ ).unwrap();
+
+ console.log('创建会话响应:', response);
+
+ if (response && 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));
}
+ } catch (apiError) {
+ // 专门处理API调用错误
+ console.error('API调用失败:', apiError);
+ throw new Error(`API调用失败: ${apiError.message || '未知错误'}`);
}
} catch (error) {
console.error('导航或创建聊天失败:', error);
@@ -157,6 +123,11 @@ export default function NewChat() {
}
};
+ // Refresh chat history when a new chat is created
+ const refreshChatHistory = useCallback(() => {
+ dispatch(fetchChats());
+ }, [dispatch]);
+
// 渲染加载状态
if (isLoading || chatHistoryLoading) {
return (
diff --git a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx
index 4dc4fab..d9643ed 100644
--- a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx
+++ b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx
@@ -16,6 +16,7 @@ export default function DatasetTab({ knowledgeBase }) {
const [selectAll, setSelectAll] = useState(false);
const [showBatchDropdown, setShowBatchDropdown] = useState(false);
const [showAddFileModal, setShowAddFileModal] = useState(false);
+ const [isUploadModalMinimized, setIsUploadModalMinimized] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [newFile, setNewFile] = useState({
name: '',
@@ -34,14 +35,13 @@ export default function DatasetTab({ knowledgeBase }) {
setDocuments(knowledgeBase.documents || []);
}, [knowledgeBase]);
- // 获取文档列表
- useEffect(() => {
+ // 获取文档列表
+ useEffect(() => {
if (knowledgeBase?.id) {
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBase.id }));
}
}, [dispatch, knowledgeBase?.id]);
-
// Handle click outside dropdown
useEffect(() => {
function handleClickOutside(event) {
@@ -244,6 +244,8 @@ export default function DatasetTab({ knowledgeBase }) {
});
setFileErrors({});
setShowAddFileModal(false);
+ // 同时重置最小化状态
+ setIsUploadModalMinimized(false);
};
// Handle delete document
@@ -270,6 +272,28 @@ export default function DatasetTab({ knowledgeBase }) {
(doc.description && doc.description.toLowerCase().includes(searchQuery.toLowerCase()))
);
+ // 处理上传按钮点击
+ const handleAddFileClick = () => {
+ // 如果弹窗已经最小化,则恢复显示
+ if (isUploadModalMinimized) {
+ setIsUploadModalMinimized(false);
+ setShowAddFileModal(true);
+ } else {
+ // 否则正常显示上传弹窗
+ setShowAddFileModal(true);
+ }
+ };
+
+ // 处理上传弹窗最小化状态变化
+ const handleUploadModalMinimizeChange = (isMinimized) => {
+ console.log('Upload modal minimize state changed:', isMinimized);
+ setIsUploadModalMinimized(isMinimized);
+ // 如果是最小化,则隐藏主弹窗但保持最小化状态显示
+ if (isMinimized) {
+ setShowAddFileModal(false);
+ }
+ };
+
return (
<>
{/* Breadcrumb navigation */}
@@ -281,7 +305,7 @@ export default function DatasetTab({ knowledgeBase }) {
{formatDateTime(doc.create_time || doc.created_at)} |
-
{formatDateTime(doc.update_time || doc.updated_at)} |
+
{doc.uploader_name || ''} |
|
@@ -256,7 +256,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
>
-
+
diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx
index 603135b..3452332 100644
--- a/src/pages/auth/Signup.jsx
+++ b/src/pages/auth/Signup.jsx
@@ -244,9 +244,9 @@ export default function Signup() {
name='role'
value={formData.role}
onChange={handleInputChange}
- disabled={loading}
+ disabled
>
-
+
diff --git a/src/services/api.js b/src/services/api.js
index d92e095..1d55c25 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -220,10 +220,30 @@ const streamRequest = async (url, data, onChunk, onError) => {
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);
+ 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' };
}
@@ -239,7 +259,7 @@ const streamRequest = async (url, data, onChunk, onError) => {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'Authorization': token ? `Token ${token}` : '',
+ Authorization: token ? `Token ${token}` : '',
},
body: JSON.stringify(data),
});
@@ -248,50 +268,104 @@ const streamRequest = async (url, data, onChunk, onError) => {
throw new Error(`HTTP error! Status: ${response.status}`);
}
- // 获取响应体的reader
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- let conversationId = null;
+ // 检查是否为SSE (Server-Sent Events)格式
+ const contentType = response.headers.get('Content-Type');
+ const isSSE = contentType && contentType.includes('text/event-stream');
+ console.log('响应内容类型:', contentType, '是否SSE:', isSSE);
- // 处理流式数据
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
+ // 处理SSE格式
+ if (isSSE) {
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+ let conversationId = null;
- // 解码并处理数据
- const chunk = decoder.decode(value, { stream: true });
- buffer += chunk;
+ // 处理流式数据
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
- // 按行分割并处理JSON
- const lines = buffer.split('\n');
- buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
+ // 解码并处理数据 - 不使用stream选项以确保完整解码
+ const chunk = decoder.decode(value, { stream: false });
+ buffer += chunk;
- for (const line of lines) {
- if (!line.trim()) continue;
-
- try {
- // 检查是否为SSE格式(data: {...})
- let jsonStr = line;
- if (line.startsWith('data:')) {
+ // 按行分割并处理JSON
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
+
+ for (const line of lines) {
+ if (!line.trim()) continue;
+
+ try {
// 提取data:后面的JSON部分
- jsonStr = line.substring(5).trim();
- console.log('检测到SSE格式数据,提取JSON:', jsonStr);
+ let jsonStr = line;
+ if (line.startsWith('data:')) {
+ jsonStr = line.substring(5).trim();
+ console.log('SSE数据块:', jsonStr);
+ }
+
+ // 尝试解析JSON
+ const parsedData = JSON.parse(jsonStr);
+ if (parsedData.code === 200 && parsedData.data && parsedData.data.conversation_id) {
+ conversationId = parsedData.data.conversation_id;
+ }
+
+ // 立即调用处理函数
+ onChunk(jsonStr);
+ } catch (e) {
+ console.warn('解析JSON失败:', line, e);
}
-
- // 尝试解析JSON
- const data = JSON.parse(jsonStr);
- if (data.code === 200 && data.data && data.data.conversation_id) {
- conversationId = data.data.conversation_id;
- }
- onChunk(jsonStr);
- } catch (e) {
- console.warn('Failed to parse JSON:', line, e);
}
}
- }
- return { success: true, conversation_id: conversationId };
+ return { success: true, conversation_id: conversationId };
+ }
+ // 处理常规JSON响应
+ else {
+ // 原始响应可能是单个JSON对象而不是流
+ const responseData = await response.json();
+ console.log('接收到非流式响应:', responseData);
+
+ if (responseData.code === 200) {
+ // 模拟分段处理
+ const content = responseData.data?.content || '';
+ const conversationId = responseData.data?.conversation_id;
+
+ // 每100个字符分段处理
+ let offset = 0;
+ const chunkSize = 100;
+
+ while (offset < content.length) {
+ const isLast = offset + chunkSize >= content.length;
+ const chunk = content.substring(offset, offset + chunkSize);
+
+ // 构造类似流式传输的JSON
+ const chunkData = {
+ code: 200,
+ message: 'partial',
+ data: {
+ content: chunk,
+ conversation_id: conversationId,
+ is_end: isLast,
+ },
+ };
+
+ // 调用处理函数
+ onChunk(JSON.stringify(chunkData));
+
+ // 暂停一下让UI有时间更新
+ await new Promise((resolve) => setTimeout(resolve, 50));
+
+ offset += chunkSize;
+ }
+
+ return { success: true, conversation_id: conversationId };
+ }
+
+ // 如果不是成功响应,直接传递原始数据
+ onChunk(JSON.stringify(responseData));
+ return { success: responseData.code === 200, conversation_id: responseData.data?.conversation_id };
+ }
} catch (error) {
console.error('Streaming request failed:', error);
if (onError) {
@@ -318,4 +392,21 @@ export const rejectPermission = (permissionId) => {
return post(`/permissions/reject_permission/${permissionId}`);
};
+/**
+ * 获取聊天回复相关资源列表
+ * @param {Object} data - 请求参数
+ * @param {Array} data.dataset_id_list - 知识库ID列表
+ * @param {string} data.question - 用户问题
+ * @returns {Promise