[dev]edited chat

This commit is contained in:
susie-laptop 2025-04-02 21:26:54 -04:00
parent a4239cac87
commit cdcd3374ad
8 changed files with 435 additions and 98 deletions

View File

@ -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">
<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>`,
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>`
};

View File

@ -254,24 +254,22 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
) : (
<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 className='message-time small text-muted mt-1'>
{message.created_at && new Date(message.created_at).toLocaleTimeString()}
{message.is_streaming && ' · 正在生成...'}
</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>
</div>

View File

@ -74,6 +74,9 @@ export default function NewChat() {
try {
setIsNavigating(true);
//
console.log('选中的知识库ID:', selectedDatasetIds);
//
//
const existingChat = chatHistory.find((chat) => {
@ -110,30 +113,49 @@ export default function NewChat() {
//
const formattedIds = selectedDatasetIds.map((id) => id.replace(/-/g, ''));
const response = await dispatch(
createChatRecord({
dataset_id_list: formattedIds,
question: '选择当前知识库,创建聊天',
})
).unwrap();
console.log('格式化后的知识库ID:', formattedIds);
if (response && response.conversation_id) {
// 使IDURL
const primaryDatasetId = selectedDatasetIds[0];
console.log(`创建成功,导航到 /chat/${primaryDatasetId}/${response.conversation_id}`);
navigate(`/chat/${primaryDatasetId}/${response.conversation_id}`);
} else {
throw new Error('未能获取会话ID');
try {
//
const response = await dispatch(
createChatRecord({
dataset_id_list: formattedIds,
question: '选择当前知识库,创建聊天',
})
).unwrap();
console.log('创建聊天响应:', response);
if (response && response.conversation_id) {
// 使IDURL
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) {
console.error('导航或创建聊天失败:', error);
//
if (error.stack) {
console.error('错误堆栈:', error.stack);
}
//
dispatch(
showNotification({
message: `创建聊天失败: ${error.message || '请重试'}`,
type: 'danger',
})
);
} finally {
setIsNavigating(false);
}
};

View File

@ -58,6 +58,9 @@ export default function KnowledgeCard({
</ul>
</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}>
{description}
</p>
@ -95,6 +98,7 @@ export default function KnowledgeCard({
)}
{access === 'full' || access === 'read' ? (
<></>
) : (
// <button
// className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'
// onClick={handleNewChat}
@ -102,7 +106,6 @@ export default function KnowledgeCard({
// <SvgIcon className={'chat-dot'} />
//
// </button>
) : (
<button
className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'
onClick={handleRequestAccess}

View File

@ -214,6 +214,93 @@ export const switchToRealApi = async () => {
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
export const applyPermission = (data) => {
return post('/permissions/', data);
@ -231,4 +318,4 @@ export const rejectPermission = (permissionId) => {
return post(`/permissions/reject_permission/${permissionId}`);
};
export { get, post, put, del, upload };
export { get, post, put, del, upload, streamRequest };

View File

@ -104,6 +104,25 @@ const chatSlice = createSlice({
addMessage: (state, action) => {
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) => {
// 获取聊天列表
@ -114,14 +133,24 @@ const chatSlice = createSlice({
})
.addCase(fetchChats.fulfilled, (state, action) => {
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.items = action.payload.results;
state.history.error = null;
})
.addCase(fetchChats.rejected, (state, action) => {
@ -232,62 +261,19 @@ const chatSlice = createSlice({
state.sendMessage.error = null;
})
.addCase(createChatRecord.fulfilled, (state, action) => {
state.sendMessage.status = 'succeeded';
// 添加新的消息
state.messages.items.push({
id: action.payload.id,
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 = {
// 更新状态以反映聊天已创建
if (action.payload.conversation_id && !state.currentChat.data) {
// 设置当前聊天的会话ID
state.currentChat.data = {
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) => {
state.sendMessage.status = 'failed';
state.sendMessage.error = action.payload || '创建聊天记录失败';
state.sendMessage.error = action.error.message;
})
// 处理获取可用知识库
@ -334,6 +320,7 @@ export const {
resetMessages,
resetSendMessageStatus,
addMessage,
updateMessage,
} = chatSlice.actions;
// 导出 reducer

View File

@ -1,6 +1,7 @@
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 { addMessage, updateMessage, setCurrentChat } from './chat.slice';
/**
* 获取聊天列表
@ -119,25 +120,223 @@ export const fetchAvailableDatasets = createAsyncThunk(
* @param {string} params.question - 用户问题
* @param {string} params.conversation_id - 会话ID可选
*/
export const createChatRecord = createAsyncThunk('chat/createChatRecord', async (params, { rejectWithValue }) => {
try {
const response = await post('/chat-history/', {
dataset_id_list: params.dataset_id_list,
question: params.question,
conversation_id: params.conversation_id,
});
export const createChatRecord = createAsyncThunk(
'chat/createChatRecord',
async ({ question, conversation_id, dataset_id_list }, { dispatch, getState, rejectWithValue }) => {
try {
// 构建请求数据
const requestBody = {
question,
dataset_id_list,
};
// 处理返回格式
if (response && response.code === 200) {
return response.data;
// 如果存在对话 ID添加到请求中
if (conversation_id) {
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 || '创建聊天记录失败');
}
});
);
/**
* 获取会话详情

View File

@ -346,4 +346,43 @@
border-radius: 0.375rem;
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;
}
}
}