mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 04:52:26 +08:00
[dev]edited chat
This commit is contained in:
parent
a4239cac87
commit
cdcd3374ad
@ -120,4 +120,6 @@ export const icons = {
|
|||||||
plus: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
plus: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||||
</svg>`,
|
</svg>`,
|
||||||
|
building: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="16" height="16" fill="currentColor"><path d="M48 0C21.5 0 0 21.5 0 48L0 464c0 26.5 21.5 48 48 48l96 0 0-80c0-26.5 21.5-48 48-48s48 21.5 48 48l0 80 96 0c26.5 0 48-21.5 48-48l0-416c0-26.5-21.5-48-48-48L48 0zM64 240c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zm112-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zm80 16c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zM80 96l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zm80 16c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zM272 96l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16z"/></svg>`,
|
||||||
|
group:`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="16" height="16" fill="currentColor"><path d="M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304l91.4 0C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7L29.7 512C13.3 512 0 498.7 0 482.3zM609.3 512l-137.8 0c5.4-9.4 8.6-20.3 8.6-32l0-8c0-60.7-27.1-115.2-69.8-151.8c2.4-.1 4.7-.2 7.1-.2l61.4 0C567.8 320 640 392.2 640 481.3c0 17-13.8 30.7-30.7 30.7zM432 256c-31 0-59-12.6-79.3-32.9C372.4 196.5 384 163.6 384 128c0-26.8-6.6-52.1-18.3-74.3C384.3 40.1 407.2 32 432 32c61.9 0 112 50.1 112 112s-50.1 112-112 112z"/></svg>`
|
||||||
};
|
};
|
||||||
|
@ -254,24 +254,22 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
) : (
|
) : (
|
||||||
<SafeMarkdown content={message.content} />
|
<SafeMarkdown content={message.content} />
|
||||||
)}
|
)}
|
||||||
|
{message.is_streaming && (
|
||||||
|
<span className='streaming-indicator'>
|
||||||
|
<span className='dot dot1'></span>
|
||||||
|
<span className='dot dot2'></span>
|
||||||
|
<span className='dot dot3'></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</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 && new Date(message.created_at).toLocaleTimeString()}
|
||||||
|
{message.is_streaming && ' · 正在生成...'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{sendStatus === 'loading' && (
|
|
||||||
<div className='d-flex justify-content-start mb-3'>
|
|
||||||
<div className='chat-message p-3 rounded-3 bg-light'>
|
|
||||||
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
|
|
||||||
<span className='visually-hidden'>加载中...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,6 +74,9 @@ export default function NewChat() {
|
|||||||
try {
|
try {
|
||||||
setIsNavigating(true);
|
setIsNavigating(true);
|
||||||
|
|
||||||
|
// 打印调试信息
|
||||||
|
console.log('选中的知识库ID:', selectedDatasetIds);
|
||||||
|
|
||||||
// 检查是否已存在包含所有选中知识库的聊天记录
|
// 检查是否已存在包含所有选中知识库的聊天记录
|
||||||
// 注意:这里的逻辑简化了,实际可能需要更复杂的匹配算法
|
// 注意:这里的逻辑简化了,实际可能需要更复杂的匹配算法
|
||||||
const existingChat = chatHistory.find((chat) => {
|
const existingChat = chatHistory.find((chat) => {
|
||||||
@ -110,30 +113,49 @@ export default function NewChat() {
|
|||||||
|
|
||||||
// 创建新的聊天记录
|
// 创建新的聊天记录
|
||||||
const formattedIds = selectedDatasetIds.map((id) => id.replace(/-/g, ''));
|
const formattedIds = selectedDatasetIds.map((id) => id.replace(/-/g, ''));
|
||||||
const response = await dispatch(
|
console.log('格式化后的知识库ID:', formattedIds);
|
||||||
createChatRecord({
|
|
||||||
dataset_id_list: formattedIds,
|
|
||||||
question: '选择当前知识库,创建聊天',
|
|
||||||
})
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
if (response && response.conversation_id) {
|
try {
|
||||||
// 使用第一个知识库ID作为URL参数
|
// 尝试创建聊天记录
|
||||||
const primaryDatasetId = selectedDatasetIds[0];
|
const response = await dispatch(
|
||||||
console.log(`创建成功,导航到 /chat/${primaryDatasetId}/${response.conversation_id}`);
|
createChatRecord({
|
||||||
navigate(`/chat/${primaryDatasetId}/${response.conversation_id}`);
|
dataset_id_list: formattedIds,
|
||||||
} else {
|
question: '选择当前知识库,创建聊天',
|
||||||
throw new Error('未能获取会话ID');
|
})
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('未能获取会话ID:' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
} catch (apiError) {
|
||||||
|
// 专门处理API调用错误
|
||||||
|
console.error('API调用失败:', apiError);
|
||||||
|
throw new Error(`API调用失败: ${apiError.message || '未知错误'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导航或创建聊天失败:', error);
|
console.error('导航或创建聊天失败:', error);
|
||||||
|
|
||||||
|
// 添加更详细的错误日志
|
||||||
|
if (error.stack) {
|
||||||
|
console.error('错误堆栈:', error.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示更友好的错误消息
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: `创建聊天失败: ${error.message || '请重试'}`,
|
message: `创建聊天失败: ${error.message || '请重试'}`,
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
setIsNavigating(false);
|
setIsNavigating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -58,6 +58,9 @@ export default function KnowledgeCard({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className='text-muted d-flex align-items-center gap-1'>
|
||||||
|
<SvgIcon className={'group'} />{department || ''} {group || 'N/A'}
|
||||||
|
</div>
|
||||||
<p className='card-text text-muted mb-3' style={descriptionStyle} title={description}>
|
<p className='card-text text-muted mb-3' style={descriptionStyle} title={description}>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
@ -95,6 +98,7 @@ export default function KnowledgeCard({
|
|||||||
)}
|
)}
|
||||||
{access === 'full' || access === 'read' ? (
|
{access === 'full' || access === 'read' ? (
|
||||||
<></>
|
<></>
|
||||||
|
) : (
|
||||||
// <button
|
// <button
|
||||||
// className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'
|
// className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'
|
||||||
// onClick={handleNewChat}
|
// onClick={handleNewChat}
|
||||||
@ -102,7 +106,6 @@ export default function KnowledgeCard({
|
|||||||
// <SvgIcon className={'chat-dot'} />
|
// <SvgIcon className={'chat-dot'} />
|
||||||
// 新聊天
|
// 新聊天
|
||||||
// </button>
|
// </button>
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'
|
className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'
|
||||||
onClick={handleRequestAccess}
|
onClick={handleRequestAccess}
|
||||||
|
@ -214,6 +214,93 @@ export const switchToRealApi = async () => {
|
|||||||
return isServerUp;
|
return isServerUp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle streaming requests
|
||||||
|
const streamRequest = async (url, data, onChunk, onError) => {
|
||||||
|
try {
|
||||||
|
if (isServerDown) {
|
||||||
|
console.log(`[MOCK MODE] STREAM ${url}`);
|
||||||
|
// 模拟流式响应
|
||||||
|
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"这是模拟的","conversation_id":"mock-1234"}}'), 300);
|
||||||
|
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"流式","conversation_id":"mock-1234"}}'), 600);
|
||||||
|
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"响应","conversation_id":"mock-1234"}}'), 900);
|
||||||
|
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"数据","conversation_id":"mock-1234","is_end":true}}'), 1200);
|
||||||
|
return { success: true, conversation_id: 'mock-1234' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取认证Token
|
||||||
|
const encryptedToken = sessionStorage.getItem('token') || '';
|
||||||
|
let token = '';
|
||||||
|
if (encryptedToken) {
|
||||||
|
token = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用fetch API进行流式请求
|
||||||
|
const response = await fetch(`/api${url}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Token ${token}` : '',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取响应体的reader
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
let conversationId = null;
|
||||||
|
|
||||||
|
// 处理流式数据
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
// 解码并处理数据
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
// 按行分割并处理JSON
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查是否为SSE格式(data: {...})
|
||||||
|
let jsonStr = line;
|
||||||
|
if (line.startsWith('data:')) {
|
||||||
|
// 提取data:后面的JSON部分
|
||||||
|
jsonStr = line.substring(5).trim();
|
||||||
|
console.log('检测到SSE格式数据,提取JSON:', jsonStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析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 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Streaming request failed:', error);
|
||||||
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 权限相关API
|
// 权限相关API
|
||||||
export const applyPermission = (data) => {
|
export const applyPermission = (data) => {
|
||||||
return post('/permissions/', data);
|
return post('/permissions/', data);
|
||||||
@ -231,4 +318,4 @@ export const rejectPermission = (permissionId) => {
|
|||||||
return post(`/permissions/reject_permission/${permissionId}`);
|
return post(`/permissions/reject_permission/${permissionId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { get, post, put, del, upload };
|
export { get, post, put, del, upload, streamRequest };
|
||||||
|
@ -104,6 +104,25 @@ const chatSlice = createSlice({
|
|||||||
addMessage: (state, action) => {
|
addMessage: (state, action) => {
|
||||||
state.messages.items.push(action.payload);
|
state.messages.items.push(action.payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 更新消息(用于流式传输)
|
||||||
|
updateMessage: (state, action) => {
|
||||||
|
const { id, ...updates } = action.payload;
|
||||||
|
const messageIndex = state.messages.items.findIndex((msg) => msg.id === id);
|
||||||
|
|
||||||
|
if (messageIndex !== -1) {
|
||||||
|
// 更新现有消息
|
||||||
|
state.messages.items[messageIndex] = {
|
||||||
|
...state.messages.items[messageIndex],
|
||||||
|
...updates,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果流式传输结束,更新发送消息状态
|
||||||
|
if (updates.is_streaming === false) {
|
||||||
|
state.sendMessage.status = 'succeeded';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// 获取聊天列表
|
// 获取聊天列表
|
||||||
@ -114,14 +133,24 @@ const chatSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(fetchChats.fulfilled, (state, action) => {
|
.addCase(fetchChats.fulfilled, (state, action) => {
|
||||||
state.list.status = 'succeeded';
|
state.list.status = 'succeeded';
|
||||||
state.list.items = action.payload.results;
|
|
||||||
state.list.total = action.payload.total;
|
|
||||||
state.list.page = action.payload.page;
|
|
||||||
state.list.page_size = action.payload.page_size;
|
|
||||||
|
|
||||||
// 同时更新新的状态结构
|
// 检查是否是追加模式
|
||||||
|
if (action.payload.append) {
|
||||||
|
// 追加模式:将新结果添加到现有列表的前面
|
||||||
|
state.list.items = [...action.payload.results, ...state.list.items];
|
||||||
|
state.history.items = [...action.payload.results, ...state.history.items];
|
||||||
|
} else {
|
||||||
|
// 替换模式:使用新结果替换整个列表
|
||||||
|
state.list.items = action.payload.results;
|
||||||
|
state.list.total = action.payload.total;
|
||||||
|
state.list.page = action.payload.page;
|
||||||
|
state.list.page_size = action.payload.page_size;
|
||||||
|
|
||||||
|
// 同时更新新的状态结构
|
||||||
|
state.history.items = action.payload.results;
|
||||||
|
}
|
||||||
|
|
||||||
state.history.status = 'succeeded';
|
state.history.status = 'succeeded';
|
||||||
state.history.items = action.payload.results;
|
|
||||||
state.history.error = null;
|
state.history.error = null;
|
||||||
})
|
})
|
||||||
.addCase(fetchChats.rejected, (state, action) => {
|
.addCase(fetchChats.rejected, (state, action) => {
|
||||||
@ -232,62 +261,19 @@ const chatSlice = createSlice({
|
|||||||
state.sendMessage.error = null;
|
state.sendMessage.error = null;
|
||||||
})
|
})
|
||||||
.addCase(createChatRecord.fulfilled, (state, action) => {
|
.addCase(createChatRecord.fulfilled, (state, action) => {
|
||||||
state.sendMessage.status = 'succeeded';
|
// 更新状态以反映聊天已创建
|
||||||
// 添加新的消息
|
if (action.payload.conversation_id && !state.currentChat.data) {
|
||||||
state.messages.items.push({
|
// 设置当前聊天的会话ID
|
||||||
id: action.payload.id,
|
state.currentChat.data = {
|
||||||
role: 'user',
|
|
||||||
content: action.meta.arg.question,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加助手回复
|
|
||||||
if (action.payload.role === 'assistant' && action.payload.content) {
|
|
||||||
state.messages.items.push({
|
|
||||||
id: action.payload.id,
|
|
||||||
role: 'assistant',
|
|
||||||
content: action.payload.content,
|
|
||||||
created_at: action.payload.created_at,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新聊天记录列表
|
|
||||||
const chatExists = state.history.items.some(
|
|
||||||
(chat) => chat.conversation_id === action.payload.conversation_id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!chatExists) {
|
|
||||||
const newChat = {
|
|
||||||
conversation_id: action.payload.conversation_id,
|
conversation_id: action.payload.conversation_id,
|
||||||
last_message: action.payload.content,
|
// 其他信息将由流式更新填充
|
||||||
last_time: action.payload.created_at,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
id: action.payload.dataset_id,
|
|
||||||
name: action.payload.dataset_name,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dataset_id_list: action.payload.dataset_id_list,
|
|
||||||
message_count: 2, // 用户问题和助手回复
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state.history.items.unshift(newChat);
|
|
||||||
} else {
|
|
||||||
// 更新已存在聊天的最后消息和时间
|
|
||||||
const chatIndex = state.history.items.findIndex(
|
|
||||||
(chat) => chat.conversation_id === action.payload.conversation_id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (chatIndex !== -1) {
|
|
||||||
state.history.items[chatIndex].last_message = action.payload.content;
|
|
||||||
state.history.items[chatIndex].last_time = action.payload.created_at;
|
|
||||||
state.history.items[chatIndex].message_count += 2; // 新增用户问题和助手回复
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 不再在这里添加消息,因为消息已经在thunk函数中添加
|
||||||
})
|
})
|
||||||
.addCase(createChatRecord.rejected, (state, action) => {
|
.addCase(createChatRecord.rejected, (state, action) => {
|
||||||
state.sendMessage.status = 'failed';
|
state.sendMessage.status = 'failed';
|
||||||
state.sendMessage.error = action.payload || '创建聊天记录失败';
|
state.sendMessage.error = action.error.message;
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理获取可用知识库
|
// 处理获取可用知识库
|
||||||
@ -334,6 +320,7 @@ export const {
|
|||||||
resetMessages,
|
resetMessages,
|
||||||
resetSendMessageStatus,
|
resetSendMessageStatus,
|
||||||
addMessage,
|
addMessage,
|
||||||
|
updateMessage,
|
||||||
} = chatSlice.actions;
|
} = chatSlice.actions;
|
||||||
|
|
||||||
// 导出 reducer
|
// 导出 reducer
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { get, post, put, del } from '../../services/api';
|
import { get, post, put, del, streamRequest } from '../../services/api';
|
||||||
import { showNotification } from '../notification.slice';
|
import { showNotification } from '../notification.slice';
|
||||||
|
import { addMessage, updateMessage, setCurrentChat } from './chat.slice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取聊天列表
|
* 获取聊天列表
|
||||||
@ -119,25 +120,223 @@ export const fetchAvailableDatasets = createAsyncThunk(
|
|||||||
* @param {string} params.question - 用户问题
|
* @param {string} params.question - 用户问题
|
||||||
* @param {string} params.conversation_id - 会话ID,可选
|
* @param {string} params.conversation_id - 会话ID,可选
|
||||||
*/
|
*/
|
||||||
export const createChatRecord = createAsyncThunk('chat/createChatRecord', async (params, { rejectWithValue }) => {
|
export const createChatRecord = createAsyncThunk(
|
||||||
try {
|
'chat/createChatRecord',
|
||||||
const response = await post('/chat-history/', {
|
async ({ question, conversation_id, dataset_id_list }, { dispatch, getState, rejectWithValue }) => {
|
||||||
dataset_id_list: params.dataset_id_list,
|
try {
|
||||||
question: params.question,
|
// 构建请求数据
|
||||||
conversation_id: params.conversation_id,
|
const requestBody = {
|
||||||
});
|
question,
|
||||||
|
dataset_id_list,
|
||||||
|
};
|
||||||
|
|
||||||
// 处理返回格式
|
// 如果存在对话 ID,添加到请求中
|
||||||
if (response && response.code === 200) {
|
if (conversation_id) {
|
||||||
return response.data;
|
requestBody.conversation_id = conversation_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('准备发送聊天请求:', requestBody);
|
||||||
|
|
||||||
|
// 先添加用户消息到聊天窗口
|
||||||
|
const userMessageId = Date.now().toString();
|
||||||
|
dispatch(
|
||||||
|
addMessage({
|
||||||
|
id: userMessageId,
|
||||||
|
role: 'user',
|
||||||
|
content: question,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加临时的助手消息(流式传输期间显示)
|
||||||
|
const assistantMessageId = (Date.now() + 1).toString();
|
||||||
|
dispatch(
|
||||||
|
addMessage({
|
||||||
|
id: assistantMessageId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
is_streaming: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let finalMessage = '';
|
||||||
|
let conversationId = conversation_id;
|
||||||
|
|
||||||
|
// 使用流式请求函数处理
|
||||||
|
const result = await streamRequest(
|
||||||
|
'/chat-history/',
|
||||||
|
requestBody,
|
||||||
|
// 处理每个数据块
|
||||||
|
(chunkText) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(chunkText);
|
||||||
|
console.log('收到聊天数据块:', data);
|
||||||
|
|
||||||
|
if (data.code === 200) {
|
||||||
|
// 保存会话ID (无论消息类型,只要找到会话ID就保存)
|
||||||
|
if (data.data && data.data.conversation_id && !conversationId) {
|
||||||
|
conversationId = data.data.conversation_id;
|
||||||
|
console.log('获取到会话ID:', conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理各种可能的消息类型
|
||||||
|
const messageType = data.message;
|
||||||
|
|
||||||
|
// 处理部分内容更新
|
||||||
|
if ((messageType === 'partial' || messageType === '部分') && data.data) {
|
||||||
|
// 累加内容
|
||||||
|
if (data.data.content !== undefined) {
|
||||||
|
finalMessage += data.data.content;
|
||||||
|
console.log('累加内容:', finalMessage);
|
||||||
|
|
||||||
|
// 更新消息内容
|
||||||
|
dispatch(
|
||||||
|
updateMessage({
|
||||||
|
id: assistantMessageId,
|
||||||
|
content: finalMessage,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理结束标志
|
||||||
|
if (data.data.is_end) {
|
||||||
|
console.log('检测到消息结束标志');
|
||||||
|
dispatch(
|
||||||
|
updateMessage({
|
||||||
|
id: assistantMessageId,
|
||||||
|
is_streaming: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理开始流式传输的消息
|
||||||
|
else if (messageType === '开始流式传输' || messageType === 'start_streaming') {
|
||||||
|
console.log('开始流式传输,会话ID:', data.data?.conversation_id);
|
||||||
|
}
|
||||||
|
// 处理完成消息
|
||||||
|
else if (
|
||||||
|
messageType === 'completed' ||
|
||||||
|
messageType === '完成' ||
|
||||||
|
messageType === 'end_streaming' ||
|
||||||
|
messageType === '结束流式传输'
|
||||||
|
) {
|
||||||
|
console.log('收到完成消息');
|
||||||
|
dispatch(
|
||||||
|
updateMessage({
|
||||||
|
id: assistantMessageId,
|
||||||
|
is_streaming: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 其他类型的消息
|
||||||
|
else {
|
||||||
|
console.log('收到其他类型消息:', messageType);
|
||||||
|
// 如果有content字段,也尝试更新
|
||||||
|
if (data.data && data.data.content !== undefined) {
|
||||||
|
finalMessage += data.data.content;
|
||||||
|
dispatch(
|
||||||
|
updateMessage({
|
||||||
|
id: assistantMessageId,
|
||||||
|
content: finalMessage,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('收到非成功状态码:', data.code, data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析或处理JSON失败:', error, '原始数据:', chunkText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 处理错误
|
||||||
|
(error) => {
|
||||||
|
console.error('流式请求错误:', error);
|
||||||
|
dispatch(
|
||||||
|
updateMessage({
|
||||||
|
id: assistantMessageId,
|
||||||
|
content: `错误: ${error.message || '请求失败'}`,
|
||||||
|
is_streaming: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确保流式传输结束后标记消息已完成
|
||||||
|
dispatch(
|
||||||
|
updateMessage({
|
||||||
|
id: assistantMessageId,
|
||||||
|
is_streaming: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 返回会话信息
|
||||||
|
const chatInfo = {
|
||||||
|
conversation_id: conversationId || result.conversation_id,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果聊天创建成功,添加到历史列表,并设置为当前激活聊天
|
||||||
|
if (chatInfo.conversation_id) {
|
||||||
|
// 获取知识库信息
|
||||||
|
const state = getState();
|
||||||
|
const availableDatasets = state.chat.availableDatasets.items || [];
|
||||||
|
|
||||||
|
// 创建一个新的聊天记录对象添加到历史列表
|
||||||
|
const newChatEntry = {
|
||||||
|
conversation_id: chatInfo.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: question,
|
||||||
|
message_count: 2, // 用户问题和助手回复
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新当前聊天
|
||||||
|
dispatch({
|
||||||
|
type: 'chat/fetchChats/fulfilled',
|
||||||
|
payload: {
|
||||||
|
results: [newChatEntry],
|
||||||
|
total: 1,
|
||||||
|
append: true, // 标记为追加,而不是替换
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置为当前聊天
|
||||||
|
dispatch(
|
||||||
|
setCurrentChat({
|
||||||
|
conversation_id: chatInfo.conversation_id,
|
||||||
|
datasets: newChatEntry.datasets,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatInfo;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建聊天记录失败:', error);
|
||||||
|
|
||||||
|
// 显示错误通知
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `发送失败: ${error.message || '未知错误'}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return rejectWithValue(error.message || '创建聊天记录失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
return rejectWithValue(response.message || '创建聊天记录失败');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating chat record:', error);
|
|
||||||
return rejectWithValue(error.response?.data?.message || '创建聊天记录失败');
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会话详情
|
* 获取会话详情
|
||||||
|
@ -346,4 +346,43 @@
|
|||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Streaming message indicator */
|
||||||
|
.streaming-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #6c757d;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 2px;
|
||||||
|
animation: pulse 1.5s infinite ease-in-out;
|
||||||
|
|
||||||
|
&.dot1 {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dot2 {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dot3 {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user