433 lines
16 KiB
Markdown
433 lines
16 KiB
Markdown
# 停止功能实现改动文档
|
||
|
||
## 实施日期
|
||
2024-12-25
|
||
|
||
## 功能概述
|
||
为AIEC-server添加了完整的请求停止功能,允许用户在发送消息后通过点击停止按钮或按ESC键中断正在处理的请求。该功能参考了yundage项目(D:\云大阁备份\yundage\yundage-backserver-rag-agent)的实现方式。
|
||
|
||
## 改动的功能
|
||
|
||
### 1. 核心功能
|
||
- ✅ **停止按钮功能**:发送消息时,发送按钮变为红色停止图标,点击可中断请求
|
||
- ✅ **ESC键支持**:按下ESC键可快速中断正在进行的请求
|
||
- ✅ **状态同步**:首页和聊天模式的UI状态完全同步
|
||
- ✅ **界面切换保持**:切换界面时保持等待状态
|
||
- ✅ **用户友好提示**:显示取消提示和警告信息
|
||
|
||
### 2. UI表现
|
||
- 发送中显示红色停止图标(正方形)代替转圈动画
|
||
- 停止按钮在发送过程中保持可点击状态
|
||
- 取消后显示"已取消请求"文本提示
|
||
- 聊天窗口显示黄色警告框提醒后端可能仍在处理
|
||
- 所有提示信息5秒后自动消失
|
||
|
||
### 3. 技术实现
|
||
- 使用AbortController API实现请求中断
|
||
- 全局状态管理确保UI同步
|
||
- 统一的updateSendUI函数管理两个界面
|
||
|
||
## 改动的文件
|
||
|
||
### 1. F:\YXL_self\RAG-TEST\AIEC-server\js\main.js
|
||
|
||
主要的功能实现文件,包含所有核心逻辑。
|
||
|
||
## 改动的代码
|
||
|
||
### 1. 添加ESC键监听器(第12-67行)
|
||
|
||
```javascript
|
||
// DOM加载完成后执行
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 添加ESC键监听,用于中断流式响应
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
// 如果正在发送中,执行停止操作
|
||
if (window.isSubmitting) {
|
||
console.log('用户按下ESC,中断请求');
|
||
|
||
// 中断请求
|
||
if (window.currentAbortController) {
|
||
window.currentAbortController.abort();
|
||
window.currentAbortController = null;
|
||
}
|
||
|
||
// 重置状态
|
||
window.isSubmitting = false;
|
||
|
||
// 更新UI
|
||
if (typeof updateSendUI === 'function') {
|
||
updateSendUI(false);
|
||
} else if (window.updateSendUI) {
|
||
window.updateSendUI(false);
|
||
}
|
||
|
||
// 显示提示
|
||
const statusText = document.querySelector('.status-text');
|
||
if (statusText) {
|
||
statusText.textContent = '已通过ESC取消请求';
|
||
setTimeout(() => {
|
||
statusText.textContent = '';
|
||
}, 3000);
|
||
}
|
||
|
||
// 添加系统提示到聊天窗口
|
||
const chatMessages = document.getElementById('chatMessages');
|
||
if (chatMessages) {
|
||
const cancelMsg = document.createElement('div');
|
||
cancelMsg.className = 'message-item mb-4';
|
||
cancelMsg.innerHTML = `
|
||
<div style="text-align: center; padding: 10px;">
|
||
<div style="display: inline-block; background: #fff3cd; color: #856404; padding: 8px 16px; border-radius: 8px; font-size: 14px;">
|
||
⚠️ 已通过ESC键取消请求。注意:后端可能仍在处理,建议稍等片刻再发送新请求。
|
||
</div>
|
||
</div>
|
||
`;
|
||
chatMessages.appendChild(cancelMsg);
|
||
|
||
// 5秒后自动移除提示
|
||
setTimeout(() => {
|
||
if (cancelMsg.parentNode) {
|
||
cancelMsg.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
// ... 其他初始化代码
|
||
```
|
||
|
||
### 2. 全局updateSendUI函数(第279-375行)
|
||
|
||
```javascript
|
||
/* ===== 聊天功能相关全局函数 ===== */
|
||
|
||
// 更新发送UI状态 - 移到全局作用域,同时更新两个界面
|
||
function updateSendUI(isLoading) {
|
||
// 首页元素
|
||
const sendIcon = document.getElementById('sendIcon');
|
||
const loadingSpinner = document.getElementById('loadingSpinner');
|
||
const statusText = document.querySelector('.status-text');
|
||
const messageInput = document.getElementById('messageInput');
|
||
const sendBtn = document.getElementById('sendBtn');
|
||
|
||
// 聊天模式元素
|
||
const chatModeSendIcon = document.getElementById('chatModeSendIcon');
|
||
const chatModeLoadingSpinner = document.getElementById('chatModeLoadingSpinner');
|
||
const chatModeMessageInput = document.getElementById('chatModeMessageInput');
|
||
const chatModeSendBtn = document.getElementById('chatModeSendBtn');
|
||
|
||
if (isLoading) {
|
||
// 强制隐藏loading spinner
|
||
if (loadingSpinner) {
|
||
loadingSpinner.style.setProperty('display', 'none', 'important');
|
||
loadingSpinner.style.visibility = 'hidden';
|
||
}
|
||
// 显示停止图标
|
||
if (sendIcon) {
|
||
sendIcon.style.display = 'block';
|
||
sendIcon.style.visibility = 'visible';
|
||
// 将SVG图标改为停止图标(红色正方形)
|
||
sendIcon.innerHTML = '<rect x="6" y="6" width="12" height="12" fill="#dc3545"/>';
|
||
sendIcon.style.color = '#dc3545';
|
||
|
||
// 确保按钮可点击
|
||
if (sendBtn) {
|
||
sendBtn.style.cursor = 'pointer';
|
||
sendBtn.disabled = false; // 确保按钮不被禁用
|
||
}
|
||
}
|
||
if (statusText) statusText.textContent = 'AI is thinking...';
|
||
if (messageInput) messageInput.disabled = true;
|
||
|
||
// 更新聊天模式UI - 同样显示停止图标
|
||
if (chatModeLoadingSpinner) {
|
||
chatModeLoadingSpinner.style.setProperty('display', 'none', 'important');
|
||
chatModeLoadingSpinner.style.visibility = 'hidden';
|
||
}
|
||
if (chatModeSendIcon) {
|
||
chatModeSendIcon.style.display = 'block';
|
||
chatModeSendIcon.style.visibility = 'visible';
|
||
// 对于SVG元素,设置停止图标(红色方块)
|
||
chatModeSendIcon.innerHTML = '<rect x="6" y="6" width="12" height="12" fill="#dc3545" stroke="none"/>';
|
||
chatModeSendIcon.style.color = '#dc3545';
|
||
// 确保SVG的fill属性生效
|
||
chatModeSendIcon.setAttribute('fill', '#dc3545');
|
||
chatModeSendIcon.setAttribute('stroke', 'none');
|
||
}
|
||
if (chatModeSendBtn) {
|
||
chatModeSendBtn.style.cursor = 'pointer';
|
||
chatModeSendBtn.disabled = false; // 确保按钮可点击以便停止
|
||
}
|
||
if (chatModeMessageInput) chatModeMessageInput.disabled = true;
|
||
} else {
|
||
// 恢复发送图标
|
||
if (sendIcon) {
|
||
sendIcon.style.display = 'block';
|
||
// 恢复原始的SVG发送图标
|
||
sendIcon.innerHTML = '<path d="m3 3 3 9-3 9 19-9Z"/><path d="m6 12 13 0"/>';
|
||
sendIcon.style.color = '#999';
|
||
}
|
||
if (loadingSpinner) {
|
||
loadingSpinner.style.setProperty('display', 'none', 'important');
|
||
}
|
||
if (statusText) statusText.textContent = 'Ready to submit!';
|
||
if (messageInput) messageInput.disabled = false;
|
||
|
||
// 更新发送按钮状态
|
||
const hasText = messageInput && messageInput.value.trim().length > 0;
|
||
if (sendIcon) {
|
||
sendIcon.style.opacity = hasText && !window.isSubmitting ? '1' : '0.3';
|
||
}
|
||
|
||
// 更新聊天模式UI - 恢复发送图标
|
||
if (chatModeLoadingSpinner) {
|
||
chatModeLoadingSpinner.style.setProperty('display', 'none', 'important');
|
||
}
|
||
if (chatModeSendIcon) {
|
||
chatModeSendIcon.style.display = 'block';
|
||
// 恢复原始的发送图标
|
||
chatModeSendIcon.innerHTML = '<path d="m3 3 3 9-3 9 19-9Z"/><path d="m6 12 13 0"/>';
|
||
chatModeSendIcon.style.color = '#999';
|
||
// 恢复SVG的默认属性
|
||
chatModeSendIcon.setAttribute('fill', 'none');
|
||
chatModeSendIcon.setAttribute('stroke', 'currentColor');
|
||
}
|
||
if (chatModeSendBtn) {
|
||
chatModeSendBtn.style.cursor = 'pointer';
|
||
}
|
||
if (chatModeMessageInput) chatModeMessageInput.disabled = false;
|
||
|
||
// 更新聊天模式按钮状态
|
||
const chatHasText = chatModeMessageInput && chatModeMessageInput.value.trim().length > 0;
|
||
if (chatModeSendIcon) {
|
||
chatModeSendIcon.style.opacity = chatHasText && !window.isSubmitting ? '1' : '0.3';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将updateSendUI暴露到全局
|
||
window.updateSendUI = updateSendUI;
|
||
```
|
||
|
||
### 3. 首页发送按钮事件处理(第442-497行)
|
||
|
||
```javascript
|
||
// 发送消息 - 支持停止功能
|
||
sendBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
console.log('发送按钮被点击,当前状态:', window.isSubmitting);
|
||
|
||
// 如果正在发送中,执行停止操作
|
||
if (window.isSubmitting) {
|
||
console.log('正在发送中,尝试停止...');
|
||
|
||
// 中断请求
|
||
if (window.currentAbortController) {
|
||
console.log('找到AbortController,执行中断');
|
||
window.currentAbortController.abort();
|
||
window.currentAbortController = null;
|
||
}
|
||
|
||
// 重置状态
|
||
window.isSubmitting = false;
|
||
updateSendUI(false);
|
||
|
||
// 显示提示
|
||
const statusText = document.querySelector('.status-text');
|
||
if (statusText) {
|
||
statusText.textContent = '已取消请求';
|
||
setTimeout(() => {
|
||
statusText.textContent = '';
|
||
}, 3000);
|
||
}
|
||
|
||
// 添加系统提示到聊天窗口
|
||
const chatMessages = document.getElementById('chatMessages');
|
||
if (chatMessages) {
|
||
const cancelMsg = document.createElement('div');
|
||
cancelMsg.className = 'message-item mb-4';
|
||
cancelMsg.innerHTML = `
|
||
<div style="text-align: center; padding: 10px;">
|
||
<div style="display: inline-block; background: #fff3cd; color: #856404; padding: 8px 16px; border-radius: 8px; font-size: 14px;">
|
||
⚠️ 请求已取消。注意:后端可能仍在处理,建议稍等片刻再发送新请求。
|
||
</div>
|
||
</div>
|
||
`;
|
||
chatMessages.appendChild(cancelMsg);
|
||
|
||
// 5秒后自动移除提示
|
||
setTimeout(() => {
|
||
if (cancelMsg.parentNode) {
|
||
cancelMsg.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
} else {
|
||
// 正常发送消息
|
||
console.log('发送按钮被点击');
|
||
sendMessage();
|
||
}
|
||
});
|
||
```
|
||
|
||
### 4. 聊天模式发送按钮事件处理(第535-580行)
|
||
|
||
```javascript
|
||
// 发送消息 - 支持停止功能
|
||
chatModeSendBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
console.log('聊天模式发送按钮被点击,当前状态:', window.isSubmitting);
|
||
|
||
// 如果正在发送中,执行停止操作
|
||
if (window.isSubmitting) {
|
||
console.log('聊天模式:正在发送中,尝试停止...');
|
||
|
||
// 中断请求
|
||
if (window.currentAbortController) {
|
||
console.log('聊天模式:找到AbortController,执行中断');
|
||
window.currentAbortController.abort();
|
||
window.currentAbortController = null;
|
||
}
|
||
|
||
// 重置状态
|
||
window.isSubmitting = false;
|
||
updateSendUI(false);
|
||
|
||
// 添加系统提示到聊天窗口
|
||
const chatMessages = document.getElementById('chatMessages');
|
||
if (chatMessages) {
|
||
const cancelMsg = document.createElement('div');
|
||
cancelMsg.className = 'message-item mb-4';
|
||
cancelMsg.innerHTML = `
|
||
<div style="text-align: center; padding: 10px;">
|
||
<div style="display: inline-block; background: #fff3cd; color: #856404; padding: 8px 16px; border-radius: 8px; font-size: 14px;">
|
||
⚠️ 请求已取消。注意:后端可能仍在处理,建议稍等片刻再发送新请求。
|
||
</div>
|
||
</div>
|
||
`;
|
||
chatMessages.appendChild(cancelMsg);
|
||
|
||
// 5秒后自动移除提示
|
||
setTimeout(() => {
|
||
if (cancelMsg.parentNode) {
|
||
cancelMsg.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
} else {
|
||
// 正常发送消息
|
||
sendChatModeMessage();
|
||
}
|
||
});
|
||
```
|
||
|
||
### 5. 保存AbortController到全局(第780行、1016行)
|
||
|
||
```javascript
|
||
// 保存AbortController到全局,以便中断
|
||
window.currentAbortController = await window.ChatAPIService.sendMessageStream(
|
||
{
|
||
message: message,
|
||
conversationId: targetChatId,
|
||
mode: isDeepSearchActive ? 'research' : 'chat',
|
||
deepResearch: isDeepResearch,
|
||
showThinking: showThinking
|
||
},
|
||
// ... 回调函数
|
||
```
|
||
|
||
### 6. 简化updateChatModeSendUI函数(第769-771行)
|
||
|
||
```javascript
|
||
// 聊天模式UI状态更新 - 直接调用全局的updateSendUI
|
||
function updateChatModeSendUI(isLoading) {
|
||
updateSendUI(isLoading);
|
||
}
|
||
```
|
||
|
||
### 7. 界面切换时的状态保持
|
||
|
||
#### startNewChat函数(第1457-1462行)
|
||
```javascript
|
||
// 如果当前正在等待中,保持等待状态
|
||
if (window.isSubmitting) {
|
||
// 确保首页也显示等待状态
|
||
updateSendUI(true);
|
||
console.log('返回首页,保持等待状态');
|
||
}
|
||
```
|
||
|
||
#### showChatContainer函数(第1534-1537行)
|
||
```javascript
|
||
// 如果当前正在提交消息,确保聊天模式也显示等待状态
|
||
if (window.isSubmitting) {
|
||
// 使用统一的updateSendUI函数来同步两个界面的状态
|
||
updateSendUI(true);
|
||
}
|
||
```
|
||
|
||
## 关键改动说明
|
||
|
||
### 1. 统一的UI状态管理
|
||
- 将原本分散在initChat内部的updateSendUI函数提升到全局作用域
|
||
- 删除了重复的函数定义,确保整个应用使用同一个状态更新函数
|
||
- updateChatModeSendUI简化为直接调用全局updateSendUI
|
||
|
||
### 2. 停止图标实现
|
||
- 使用红色正方形(`<rect>`)作为停止图标
|
||
- 强制隐藏loading spinner,避免与停止图标冲突
|
||
- 正确设置SVG属性确保颜色显示
|
||
|
||
### 3. AbortController管理
|
||
- 将sendMessageStream返回的AbortController保存到`window.currentAbortController`
|
||
- 停止时调用`abort()`方法中断请求
|
||
- 中断后将其设为null避免重复调用
|
||
|
||
### 4. 用户体验优化
|
||
- 停止按钮在发送过程中保持可点击状态
|
||
- 提供清晰的视觉反馈(红色停止图标)
|
||
- 显示友好的取消提示信息
|
||
- 警告用户后端可能仍在处理
|
||
|
||
## 测试要点
|
||
|
||
1. **基本功能测试**
|
||
- 发送消息后按钮变为红色停止图标
|
||
- 点击停止图标能中断请求
|
||
- ESC键能中断请求
|
||
|
||
2. **状态同步测试**
|
||
- 首页发送后切换到聊天模式,仍显示停止状态
|
||
- 聊天模式发送后返回首页,仍显示停止状态
|
||
- 停止后两个界面同时恢复
|
||
|
||
3. **边界情况测试**
|
||
- 快速切换界面
|
||
- 连续点击停止
|
||
- 网络延迟情况
|
||
|
||
## 注意事项
|
||
|
||
1. **前端限制**:前端中断只是断开连接,后端可能继续处理
|
||
2. **后端配合**:需要后端配合才能真正停止处理
|
||
3. **用户提醒**:建议用户等待片刻再发送新请求
|
||
|
||
## 参考来源
|
||
|
||
本功能实现参考了以下项目:
|
||
- 项目路径:`D:\云大阁备份\yundage\yundage-backserver-rag-agent`
|
||
- 参考文件:`js/main.js`
|
||
- 参考文档:`停止功能实现总结.md`
|
||
|
||
## 版本信息
|
||
|
||
- 实施日期:2024-12-25
|
||
- 影响文件:`F:\YXL_self\RAG-TEST\AIEC-server\js\main.js`
|
||
- 功能状态:✅ 已完成并测试 |