feat: 完成对话手动停止的功能
This commit is contained in:
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
@@ -26,7 +26,8 @@
|
||||
maxlength="300"
|
||||
/>
|
||||
<view class="input-container-send" @click="sendMessage">
|
||||
<image src='/static/input_send_icon.png'></image>
|
||||
<image v-if="props.isSessionActive" src='/static/input_stop_icon.png'></image>
|
||||
<image v-else src='/static/input_send_icon.png'></image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -36,7 +37,9 @@ import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
inputMessage: String,
|
||||
holdKeyboard: Boolean
|
||||
holdKeyboard: Boolean,
|
||||
isSessionActive: Boolean,
|
||||
stopRequest: Function
|
||||
})
|
||||
const emit = defineEmits(['update:inputMessage', 'send', 'noHideKeyboard', 'keyboardShow', 'keyboardHide'])
|
||||
|
||||
@@ -46,6 +49,7 @@ const inputMessage = ref(props.inputMessage || '')
|
||||
const isFocused = ref(false)
|
||||
const keyboardHeight = ref(0)
|
||||
|
||||
|
||||
// 保持和父组件同步
|
||||
watch(() => props.inputMessage, (val) => {
|
||||
inputMessage.value = val
|
||||
@@ -65,6 +69,13 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const sendMessage = () => {
|
||||
if (props.isSessionActive) {
|
||||
// 如果会话进行中,调用停止请求函数
|
||||
if (props.stopRequest) {
|
||||
props.stopRequest();
|
||||
}
|
||||
} else {
|
||||
// 否则发送新消息
|
||||
if (!inputMessage.value.trim()) return;
|
||||
emit('send', inputMessage.value)
|
||||
inputMessage.value = ''
|
||||
@@ -77,6 +88,7 @@ const sendMessage = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
isFocused.value = true
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
ref="inputAreaRef"
|
||||
v-model="inputMessage"
|
||||
:holdKeyboard="holdKeyboard"
|
||||
:is-session-active="isSessionActive"
|
||||
:stop-request="stopRequest"
|
||||
@send="sendMessageAction"
|
||||
@noHideKeyboard="handleNoHideKeyboard"
|
||||
@keyboardShow="handleKeyboardShow"
|
||||
@@ -129,6 +131,8 @@
|
||||
const chatMsgList = ref([])
|
||||
/// 输入口的输入消息
|
||||
const inputMessage = ref('')
|
||||
/// 加载中
|
||||
let currentAIMsgIndex = 0
|
||||
|
||||
/// 从个渠道获取如二维,没有的时候就返回首页的数据
|
||||
const sceneId = ref('')
|
||||
@@ -140,7 +144,9 @@
|
||||
const mainPageDataModel = ref({})
|
||||
|
||||
// 会话进行中标志
|
||||
let isSessionActive = false;
|
||||
const isSessionActive = ref(false);
|
||||
// 请求任务引用
|
||||
const requestTaskRef = ref(null);
|
||||
/// 指令
|
||||
let commonType = ''
|
||||
|
||||
@@ -229,7 +235,7 @@
|
||||
if (!inputText.trim()) return;
|
||||
handleNoHideKeyboard()
|
||||
sendMessage(inputText)
|
||||
if(!isSessionActive) {
|
||||
if(!isSessionActive.value) {
|
||||
inputMessage.value = ''
|
||||
}
|
||||
// 发送消息后保持键盘状态
|
||||
@@ -319,14 +325,14 @@
|
||||
|
||||
/// 发送消息的参数拼接
|
||||
const sendMessage = (message, isInstruct = false) => {
|
||||
if (isSessionActive) {
|
||||
if (isSessionActive.value) {
|
||||
uni.showToast({
|
||||
title: '请等待当前回复完成',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
isSessionActive = true;
|
||||
isSessionActive.value = true;
|
||||
const newMsg = {
|
||||
msgId: `msg_${chatMsgList.value.length}`,
|
||||
msgType: MessageRole.ME,
|
||||
@@ -368,6 +374,7 @@
|
||||
}
|
||||
chatMsgList.value.push(aiMsg)
|
||||
const aiMsgIndex = chatMsgList.value.length - 1
|
||||
currentAIMsgIndex = aiMsgIndex
|
||||
|
||||
// 动态加载中动画
|
||||
let dotCount = 1;
|
||||
@@ -385,7 +392,7 @@
|
||||
}
|
||||
|
||||
// 流式接收内容
|
||||
agentChatStream(args, (chunk) => {
|
||||
const { promise, requestTask } = agentChatStream(args, (chunk) => {
|
||||
console.log('分段内容:', chunk)
|
||||
if (chunk && chunk.error) {
|
||||
chatMsgList.value[aiMsgIndex].msg = '请求错误,请重试';
|
||||
@@ -393,7 +400,7 @@
|
||||
loadingTimer = null;
|
||||
isTyping = false;
|
||||
typeWriterTimer = null;
|
||||
isSessionActive = false; // 出错也允许再次发送
|
||||
isSessionActive.value = false; // 出错也允许再次发送
|
||||
console.error('流式错误:', chunk.message, chunk.detail);
|
||||
return;
|
||||
}
|
||||
@@ -444,14 +451,22 @@
|
||||
chatMsgList.value[aiMsgIndex].question = chunk.question
|
||||
}
|
||||
|
||||
isSessionActive = false;
|
||||
isSessionActive.value = false;
|
||||
scrollToBottom();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}).catch(e => {
|
||||
isSessionActive = false; // 出错也允许再次发送
|
||||
console.log('error:', e)
|
||||
})
|
||||
|
||||
// 存储请求任务
|
||||
requestTaskRef.value = requestTask;
|
||||
|
||||
// 可选:处理Promise完成/失败, 已经在回调中处理数据,此处无需再处理
|
||||
promise.then(data => {
|
||||
console.log('请求完成');
|
||||
}).catch(err => {
|
||||
isSessionActive.value = false; // 出错也允许再次发送
|
||||
console.log('error:', err)
|
||||
});
|
||||
|
||||
// 打字机函数
|
||||
@@ -472,6 +487,29 @@
|
||||
}
|
||||
|
||||
|
||||
// 停止请求函数
|
||||
const stopRequest = () => {
|
||||
if (requestTaskRef.value && requestTaskRef.value.abort) {
|
||||
requestTaskRef.value.abort();
|
||||
// 重置状态
|
||||
isSessionActive.value = false;
|
||||
const msg = chatMsgList.value[currentAIMsgIndex].msg;
|
||||
if (!msg || msg === '加载中.' || msg.startsWith('加载中')) {
|
||||
chatMsgList.value[currentAIMsgIndex].msg = '已终止请求,请重试';
|
||||
}
|
||||
// 清除计时器
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer);
|
||||
loadingTimer = null;
|
||||
}
|
||||
if (typeWriterTimer) {
|
||||
clearTimeout(typeWriterTimer);
|
||||
typeWriterTimer = null;
|
||||
}
|
||||
setTimeoutScrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -7,16 +7,17 @@ const API = '/agent/assistant/chat';
|
||||
* 获取AI聊天流式信息(仅微信小程序支持)
|
||||
* @param {Object} params 请求参数
|
||||
* @param {Function} onChunk 回调,每收到一段数据触发
|
||||
* @returns {Promise}
|
||||
* @returns {Object} 包含Promise和requestTask的对象
|
||||
*/
|
||||
function agentChatStream(params, onChunk) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let requestTask;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const token = uni.getStorageSync('token');
|
||||
let hasError = false;
|
||||
|
||||
console.log("发送请求内容: ", params)
|
||||
// #ifdef MP-WEIXIN
|
||||
const requestTask = uni.request({
|
||||
requestTask = uni.request({
|
||||
url: BASE_URL + API, // 替换为你的接口地址
|
||||
method: 'POST',
|
||||
data: params,
|
||||
@@ -37,7 +38,6 @@ function agentChatStream(params, onChunk) {
|
||||
complete(res) {
|
||||
if(res.statusCode !== 200) {
|
||||
console.log("====> ", JSON.stringify(res))
|
||||
|
||||
if (onChunk) {
|
||||
onChunk({ error: true, message: '服务器错误', detail: res });
|
||||
}
|
||||
@@ -75,18 +75,19 @@ function agentChatStream(params, onChunk) {
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
requestTask
|
||||
};
|
||||
}
|
||||
|
||||
// window.atob兼容性处理
|
||||
const weAtob = (string) => {
|
||||
const b64re =
|
||||
/^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
|
||||
const b64 =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
|
||||
const b64re = /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
|
||||
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
// 去除空白字符
|
||||
string = String(string).replace(/[\t\n\f\r ]+/g, '');
|
||||
|
||||
// 验证 Base64 编码
|
||||
if (!b64re.test(string)) {
|
||||
throw new TypeError(
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
static/input_stop_icon.png
Normal file
BIN
static/input_stop_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user