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"
|
maxlength="300"
|
||||||
/>
|
/>
|
||||||
<view class="input-container-send" @click="sendMessage">
|
<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>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -36,7 +37,9 @@ import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
inputMessage: String,
|
inputMessage: String,
|
||||||
holdKeyboard: Boolean
|
holdKeyboard: Boolean,
|
||||||
|
isSessionActive: Boolean,
|
||||||
|
stopRequest: Function
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:inputMessage', 'send', 'noHideKeyboard', 'keyboardShow', 'keyboardHide'])
|
const emit = defineEmits(['update:inputMessage', 'send', 'noHideKeyboard', 'keyboardShow', 'keyboardHide'])
|
||||||
|
|
||||||
@@ -46,6 +49,7 @@ const inputMessage = ref(props.inputMessage || '')
|
|||||||
const isFocused = ref(false)
|
const isFocused = ref(false)
|
||||||
const keyboardHeight = ref(0)
|
const keyboardHeight = ref(0)
|
||||||
|
|
||||||
|
|
||||||
// 保持和父组件同步
|
// 保持和父组件同步
|
||||||
watch(() => props.inputMessage, (val) => {
|
watch(() => props.inputMessage, (val) => {
|
||||||
inputMessage.value = val
|
inputMessage.value = val
|
||||||
@@ -65,16 +69,24 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (!inputMessage.value.trim()) return;
|
if (props.isSessionActive) {
|
||||||
emit('send', inputMessage.value)
|
// 如果会话进行中,调用停止请求函数
|
||||||
inputMessage.value = ''
|
if (props.stopRequest) {
|
||||||
emit('update:inputMessage', inputMessage.value)
|
props.stopRequest();
|
||||||
|
}
|
||||||
// 发送后保持焦点(可选)
|
} else {
|
||||||
if (props.holdKeyboard && textareaRef.value) {
|
// 否则发送新消息
|
||||||
nextTick(() => {
|
if (!inputMessage.value.trim()) return;
|
||||||
textareaRef.value.focus()
|
emit('send', inputMessage.value)
|
||||||
})
|
inputMessage.value = ''
|
||||||
|
emit('update:inputMessage', inputMessage.value)
|
||||||
|
|
||||||
|
// 发送后保持焦点(可选)
|
||||||
|
if (props.holdKeyboard && textareaRef.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
textareaRef.value.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +140,7 @@ defineExpose({
|
|||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
/* 确保输入框在安全区域内 */
|
/* 确保输入框在安全区域内 */
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.input-container-voice {
|
.input-container-voice {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -137,13 +149,13 @@ defineExpose({
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|
||||||
image {
|
image {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container-send {
|
.input-container-send {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -152,13 +164,13 @@ defineExpose({
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|
||||||
image {
|
image {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 92px;
|
max-height: 92px;
|
||||||
@@ -169,4 +181,4 @@ defineExpose({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<view class="chat-container" @touchend="handleTouchEnd">
|
<view class="chat-container" @touchend="handleTouchEnd">
|
||||||
<!-- 顶部的背景 -->
|
<!-- 顶部的背景 -->
|
||||||
<ChatTopBgImg class="chat-container-bg"></ChatTopBgImg>
|
<ChatTopBgImg class="chat-container-bg"></ChatTopBgImg>
|
||||||
|
|
||||||
<view class="chat-content">
|
<view class="chat-content">
|
||||||
<!-- 顶部自定义导航栏 -->
|
<!-- 顶部自定义导航栏 -->
|
||||||
<view class="nav-bar-container" :style="{
|
<view class="nav-bar-container" :style="{
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
}">
|
}">
|
||||||
<ChatTopNavBar @openDrawer="openDrawer"></ChatTopNavBar>
|
<ChatTopNavBar @openDrawer="openDrawer"></ChatTopNavBar>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 消息列表(可滚动区域) -->
|
<!-- 消息列表(可滚动区域) -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
:scroll-top="scrollTop"
|
:scroll-top="scrollTop"
|
||||||
@@ -19,15 +19,15 @@
|
|||||||
class="area-msg-list"
|
class="area-msg-list"
|
||||||
>
|
>
|
||||||
<!-- welcome栏 -->
|
<!-- welcome栏 -->
|
||||||
<ChatTopWelcome class="chat-container-top-bannar"
|
<ChatTopWelcome class="chat-container-top-bannar"
|
||||||
:initPageImages="mainPageDataModel.initPageImages"
|
:initPageImages="mainPageDataModel.initPageImages"
|
||||||
:welcomeContent="mainPageDataModel.welcomeContent">
|
:welcomeContent="mainPageDataModel.welcomeContent">
|
||||||
</ChatTopWelcome>
|
</ChatTopWelcome>
|
||||||
|
|
||||||
<view class="area-msg-list-content" v-for="item in chatMsgList" :key="item.msgId" :id="item.msgId">
|
<view class="area-msg-list-content" v-for="item in chatMsgList" :key="item.msgId" :id="item.msgId">
|
||||||
<template v-if="item.msgType === MessageRole.AI">
|
<template v-if="item.msgType === MessageRole.AI">
|
||||||
<ChatCardAI class="message-item-ai" :text="item.msg">
|
<ChatCardAI class="message-item-ai" :text="item.msg">
|
||||||
<template #content v-if="item.toolCall">
|
<template #content v-if="item.toolCall">
|
||||||
<QuickBookingComponent v-if="item.toolCall.componentName === CompName.quickBookingCard"/>
|
<QuickBookingComponent v-if="item.toolCall.componentName === CompName.quickBookingCard"/>
|
||||||
<DiscoveryCardComponent v-else-if="item.toolCall.componentName === CompName.discoveryCard"/>
|
<DiscoveryCardComponent v-else-if="item.toolCall.componentName === CompName.discoveryCard"/>
|
||||||
<CreateServiceOrder v-else-if="item.toolCall.componentName === CompName.createWorkOrderCard"/>
|
<CreateServiceOrder v-else-if="item.toolCall.componentName === CompName.createWorkOrderCard"/>
|
||||||
@@ -36,39 +36,41 @@
|
|||||||
<!-- 这个是底部 -->
|
<!-- 这个是底部 -->
|
||||||
<AttachListComponent v-if="item.question" :question="item.question" @replySent="handleReply"/>
|
<AttachListComponent v-if="item.question" :question="item.question" @replySent="handleReply"/>
|
||||||
</template>
|
</template>
|
||||||
</ChatCardAI>
|
</ChatCardAI>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="item.msgType === MessageRole.ME">
|
<template v-else-if="item.msgType === MessageRole.ME">
|
||||||
<ChatCardMine class="message-item-mine" :text="item.msg">
|
<ChatCardMine class="message-item-mine" :text="item.msg">
|
||||||
</ChatCardMine>
|
</ChatCardMine>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ChatCardOther class="message-item-other" :text="item.msg">
|
<ChatCardOther class="message-item-other" :text="item.msg">
|
||||||
<ChatMoreTips @replySent="handleReply" :itemList="mainPageDataModel.guideWords"/>
|
<ChatMoreTips @replySent="handleReply" :itemList="mainPageDataModel.guideWords"/>
|
||||||
|
|
||||||
<ActivityListComponent v-if="mainPageDataModel.activityList.length > 0" :activityList="mainPageDataModel.activityList"/>
|
<ActivityListComponent v-if="mainPageDataModel.activityList.length > 0" :activityList="mainPageDataModel.activityList"/>
|
||||||
|
|
||||||
<RecommendPostsComponent v-if="mainPageDataModel.recommendTheme.length > 0" :recommendThemeList="mainPageDataModel.recommendTheme" />
|
<RecommendPostsComponent v-if="mainPageDataModel.recommendTheme.length > 0" :recommendThemeList="mainPageDataModel.recommendTheme" />
|
||||||
</ChatCardOther>
|
</ChatCardOther>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- 输入框区域 -->
|
<!-- 输入框区域 -->
|
||||||
<view class="footer-area">
|
<view class="footer-area">
|
||||||
<ChatQuickAccess @replySent="handleReplyInstruct"/>
|
<ChatQuickAccess @replySent="handleReplyInstruct"/>
|
||||||
<ChatInputArea
|
<ChatInputArea
|
||||||
ref="inputAreaRef"
|
ref="inputAreaRef"
|
||||||
v-model="inputMessage"
|
v-model="inputMessage"
|
||||||
:holdKeyboard="holdKeyboard"
|
:holdKeyboard="holdKeyboard"
|
||||||
@send="sendMessageAction"
|
:is-session-active="isSessionActive"
|
||||||
@noHideKeyboard="handleNoHideKeyboard"
|
:stop-request="stopRequest"
|
||||||
@keyboardShow="handleKeyboardShow"
|
@send="sendMessageAction"
|
||||||
@keyboardHide="handleKeyboardHide"
|
@noHideKeyboard="handleNoHideKeyboard"
|
||||||
/>
|
@keyboardShow="handleKeyboardShow"
|
||||||
|
@keyboardHide="handleKeyboardHide"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -79,7 +81,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { defineEmits } from 'vue'
|
import { defineEmits } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import { SCROLL_TO_BOTTOM, RECOMMEND_POSTS_TITLE, SEND_COMMAND_TEXT } from '@/constant/constant'
|
import { SCROLL_TO_BOTTOM, RECOMMEND_POSTS_TITLE, SEND_COMMAND_TEXT } from '@/constant/constant'
|
||||||
import { MessageRole, MessageType, CompName } from '../../model/ChatModel';
|
import { MessageRole, MessageType, CompName } from '../../model/ChatModel';
|
||||||
|
|
||||||
import ChatTopWelcome from './ChatTopWelcome.vue';
|
import ChatTopWelcome from './ChatTopWelcome.vue';
|
||||||
@@ -92,44 +94,46 @@
|
|||||||
import ChatMoreTips from './ChatMoreTips.vue';
|
import ChatMoreTips from './ChatMoreTips.vue';
|
||||||
import ChatInputArea from './ChatInputArea.vue'
|
import ChatInputArea from './ChatInputArea.vue'
|
||||||
import CommandWrapper from '@/components/CommandWrapper/index.vue'
|
import CommandWrapper from '@/components/CommandWrapper/index.vue'
|
||||||
|
|
||||||
import QuickBookingComponent from '../module/booking/QuickBookingComponent.vue'
|
import QuickBookingComponent from '../module/booking/QuickBookingComponent.vue'
|
||||||
import DiscoveryCardComponent from '../module/discovery/DiscoveryCardComponent.vue';
|
import DiscoveryCardComponent from '../module/discovery/DiscoveryCardComponent.vue';
|
||||||
import ActivityListComponent from '../module/banner/ActivityListComponent.vue';
|
import ActivityListComponent from '../module/banner/ActivityListComponent.vue';
|
||||||
import RecommendPostsComponent from '../module/recommend/RecommendPostsComponent.vue';
|
import RecommendPostsComponent from '../module/recommend/RecommendPostsComponent.vue';
|
||||||
import AttachListComponent from '../module/attach/AttachListComponent.vue';
|
import AttachListComponent from '../module/attach/AttachListComponent.vue';
|
||||||
|
|
||||||
import CreateServiceOrder from '@/components/CreateServiceOrder/index.vue'
|
import CreateServiceOrder from '@/components/CreateServiceOrder/index.vue'
|
||||||
|
|
||||||
import { agentChatStream } from '@/request/api/AgentChatStream';
|
import { agentChatStream } from '@/request/api/AgentChatStream';
|
||||||
import { mainPageData } from '@/request/api/MainPageDataApi';
|
import { mainPageData } from '@/request/api/MainPageDataApi';
|
||||||
import { conversationMsgList, recentConversation } from '@/request/api/ConversationApi';
|
import { conversationMsgList, recentConversation } from '@/request/api/ConversationApi';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// 导航栏相关
|
/// 导航栏相关
|
||||||
const statusBarHeight = ref(20);
|
const statusBarHeight = ref(20);
|
||||||
/// 输入框组件引用
|
/// 输入框组件引用
|
||||||
const inputAreaRef = ref(null);
|
const inputAreaRef = ref(null);
|
||||||
|
|
||||||
const timer = ref(null)
|
const timer = ref(null)
|
||||||
/// focus时,点击页面的时候不收起键盘
|
/// focus时,点击页面的时候不收起键盘
|
||||||
const holdKeyboard = ref(false)
|
const holdKeyboard = ref(false)
|
||||||
/// 是否在键盘弹出,点击界面时关闭键盘
|
/// 是否在键盘弹出,点击界面时关闭键盘
|
||||||
const holdKeyboardFlag = ref(true)
|
const holdKeyboardFlag = ref(true)
|
||||||
/// 键盘高度
|
/// 键盘高度
|
||||||
const keyboardHeight = ref(0)
|
const keyboardHeight = ref(0)
|
||||||
/// 是否显示键盘
|
/// 是否显示键盘
|
||||||
const isKeyboardShow = ref(false)
|
const isKeyboardShow = ref(false)
|
||||||
|
|
||||||
///(控制滚动位置)
|
///(控制滚动位置)
|
||||||
const scrollTop = ref(99999);
|
const scrollTop = ref(99999);
|
||||||
|
|
||||||
/// 会话列表
|
/// 会话列表
|
||||||
const chatMsgList = ref([])
|
const chatMsgList = ref([])
|
||||||
/// 输入口的输入消息
|
/// 输入口的输入消息
|
||||||
const inputMessage = ref('')
|
const inputMessage = ref('')
|
||||||
|
/// 加载中
|
||||||
|
let currentAIMsgIndex = 0
|
||||||
|
|
||||||
/// 从个渠道获取如二维,没有的时候就返回首页的数据
|
/// 从个渠道获取如二维,没有的时候就返回首页的数据
|
||||||
const sceneId = ref('')
|
const sceneId = ref('')
|
||||||
/// agentId 首页接口中获取
|
/// agentId 首页接口中获取
|
||||||
@@ -140,13 +144,15 @@
|
|||||||
const mainPageDataModel = ref({})
|
const mainPageDataModel = ref({})
|
||||||
|
|
||||||
// 会话进行中标志
|
// 会话进行中标志
|
||||||
let isSessionActive = false;
|
const isSessionActive = ref(false);
|
||||||
|
// 请求任务引用
|
||||||
|
const requestTaskRef = ref(null);
|
||||||
/// 指令
|
/// 指令
|
||||||
let commonType = ''
|
let commonType = ''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 打开抽屉
|
// 打开抽屉
|
||||||
const emits = defineEmits(['openDrawer'])
|
const emits = defineEmits(['openDrawer'])
|
||||||
const openDrawer = () => {
|
const openDrawer = () => {
|
||||||
@@ -164,12 +170,12 @@
|
|||||||
holdKeyboardFlag.value = true
|
holdKeyboardFlag.value = true
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击输入框、发送按钮时,不收键盘
|
// 点击输入框、发送按钮时,不收键盘
|
||||||
const handleNoHideKeyboard = () => {
|
const handleNoHideKeyboard = () => {
|
||||||
holdKeyboardFlag.value = false
|
holdKeyboardFlag.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 键盘弹起事件
|
// 键盘弹起事件
|
||||||
const handleKeyboardShow = (height) => {
|
const handleKeyboardShow = (height) => {
|
||||||
keyboardHeight.value = height
|
keyboardHeight.value = height
|
||||||
@@ -180,7 +186,7 @@
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, 150)
|
}, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 键盘收起事件
|
// 键盘收起事件
|
||||||
const handleKeyboardHide = () => {
|
const handleKeyboardHide = () => {
|
||||||
keyboardHeight.value = 0
|
keyboardHeight.value = 0
|
||||||
@@ -202,7 +208,7 @@
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送普通消息
|
/// 发送普通消息
|
||||||
const handleReply = (text) => {
|
const handleReply = (text) => {
|
||||||
sendMessage(text)
|
sendMessage(text)
|
||||||
@@ -229,7 +235,7 @@
|
|||||||
if (!inputText.trim()) return;
|
if (!inputText.trim()) return;
|
||||||
handleNoHideKeyboard()
|
handleNoHideKeyboard()
|
||||||
sendMessage(inputText)
|
sendMessage(inputText)
|
||||||
if(!isSessionActive) {
|
if(!isSessionActive.value) {
|
||||||
inputMessage.value = ''
|
inputMessage.value = ''
|
||||||
}
|
}
|
||||||
// 发送消息后保持键盘状态
|
// 发送消息后保持键盘状态
|
||||||
@@ -248,14 +254,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted( async() => {
|
onMounted( async() => {
|
||||||
getMainPageData()
|
getMainPageData()
|
||||||
await loadRecentConversation()
|
await loadRecentConversation()
|
||||||
loadConversationMsgList()
|
loadConversationMsgList()
|
||||||
addNoticeListener()
|
addNoticeListener()
|
||||||
})
|
})
|
||||||
|
|
||||||
const addNoticeListener = () => {
|
const addNoticeListener = () => {
|
||||||
uni.$on(SCROLL_TO_BOTTOM, (value) => {
|
uni.$on(SCROLL_TO_BOTTOM, (value) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -279,7 +285,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取最近一次的会话id
|
/// 获取最近一次的会话id
|
||||||
const loadRecentConversation = async() => {
|
const loadRecentConversation = async() => {
|
||||||
const res = await recentConversation()
|
const res = await recentConversation()
|
||||||
@@ -287,7 +293,7 @@
|
|||||||
conversationId.value = res.data.conversationId
|
conversationId.value = res.data.conversationId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加载历史消息的数据
|
/// 加载历史消息的数据
|
||||||
let historyCurrentPageNum = 1
|
let historyCurrentPageNum = 1
|
||||||
const loadConversationMsgList = async() => {
|
const loadConversationMsgList = async() => {
|
||||||
@@ -305,7 +311,7 @@
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化数据 首次数据加载的时候
|
/// 初始化数据 首次数据加载的时候
|
||||||
const initData = () => {
|
const initData = () => {
|
||||||
const msg = {
|
const msg = {
|
||||||
@@ -315,18 +321,18 @@
|
|||||||
}
|
}
|
||||||
chatMsgList.value.push(msg)
|
chatMsgList.value.push(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 发送消息的参数拼接
|
/// 发送消息的参数拼接
|
||||||
const sendMessage = (message, isInstruct = false) => {
|
const sendMessage = (message, isInstruct = false) => {
|
||||||
if (isSessionActive) {
|
if (isSessionActive.value) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请等待当前回复完成',
|
title: '请等待当前回复完成',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSessionActive = true;
|
isSessionActive.value = true;
|
||||||
const newMsg = {
|
const newMsg = {
|
||||||
msgId: `msg_${chatMsgList.value.length}`,
|
msgId: `msg_${chatMsgList.value.length}`,
|
||||||
msgType: MessageRole.ME,
|
msgType: MessageRole.ME,
|
||||||
@@ -337,10 +343,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatMsgList.value.push(newMsg)
|
chatMsgList.value.push(newMsg)
|
||||||
sendChat(message, isInstruct)
|
sendChat(message, isInstruct)
|
||||||
console.log("发送的新消息:",JSON.stringify(newMsg))
|
console.log("发送的新消息:",JSON.stringify(newMsg))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打字机效果实现的变量
|
/// 打字机效果实现的变量
|
||||||
let loadingTimer = null;
|
let loadingTimer = null;
|
||||||
let typeWriterTimer = null;
|
let typeWriterTimer = null;
|
||||||
@@ -355,7 +361,7 @@
|
|||||||
messageType: isInstruct ? 1 : 0,
|
messageType: isInstruct ? 1 : 0,
|
||||||
messageContent: isInstruct ? commonType : message
|
messageContent: isInstruct ? commonType : message
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插入AI消息
|
// 插入AI消息
|
||||||
const aiMsg = {
|
const aiMsg = {
|
||||||
msgId: `msg_${chatMsgList.value.length}`,
|
msgId: `msg_${chatMsgList.value.length}`,
|
||||||
@@ -368,7 +374,8 @@
|
|||||||
}
|
}
|
||||||
chatMsgList.value.push(aiMsg)
|
chatMsgList.value.push(aiMsg)
|
||||||
const aiMsgIndex = chatMsgList.value.length - 1
|
const aiMsgIndex = chatMsgList.value.length - 1
|
||||||
|
currentAIMsgIndex = aiMsgIndex
|
||||||
|
|
||||||
// 动态加载中动画
|
// 动态加载中动画
|
||||||
let dotCount = 1;
|
let dotCount = 1;
|
||||||
loadingTimer && clearInterval(loadingTimer);
|
loadingTimer && clearInterval(loadingTimer);
|
||||||
@@ -376,16 +383,16 @@
|
|||||||
dotCount = dotCount % 3 + 1;
|
dotCount = dotCount % 3 + 1;
|
||||||
chatMsgList.value[aiMsgIndex].msg = '加载中' + '.'.repeat(dotCount);
|
chatMsgList.value[aiMsgIndex].msg = '加载中' + '.'.repeat(dotCount);
|
||||||
}, 400);
|
}, 400);
|
||||||
|
|
||||||
aiMsgBuffer = '';
|
aiMsgBuffer = '';
|
||||||
isTyping = false;
|
isTyping = false;
|
||||||
if (typeWriterTimer) {
|
if (typeWriterTimer) {
|
||||||
clearTimeout(typeWriterTimer);
|
clearTimeout(typeWriterTimer);
|
||||||
typeWriterTimer = null;
|
typeWriterTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流式接收内容
|
// 流式接收内容
|
||||||
agentChatStream(args, (chunk) => {
|
const { promise, requestTask } = agentChatStream(args, (chunk) => {
|
||||||
console.log('分段内容:', chunk)
|
console.log('分段内容:', chunk)
|
||||||
if (chunk && chunk.error) {
|
if (chunk && chunk.error) {
|
||||||
chatMsgList.value[aiMsgIndex].msg = '请求错误,请重试';
|
chatMsgList.value[aiMsgIndex].msg = '请求错误,请重试';
|
||||||
@@ -393,7 +400,7 @@
|
|||||||
loadingTimer = null;
|
loadingTimer = null;
|
||||||
isTyping = false;
|
isTyping = false;
|
||||||
typeWriterTimer = null;
|
typeWriterTimer = null;
|
||||||
isSessionActive = false; // 出错也允许再次发送
|
isSessionActive.value = false; // 出错也允许再次发送
|
||||||
console.error('流式错误:', chunk.message, chunk.detail);
|
console.error('流式错误:', chunk.message, chunk.detail);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -406,7 +413,7 @@
|
|||||||
}
|
}
|
||||||
// 把新内容追加到缓冲区
|
// 把新内容追加到缓冲区
|
||||||
aiMsgBuffer += chunk.content;
|
aiMsgBuffer += chunk.content;
|
||||||
|
|
||||||
// 启动打字机(只启动一次)
|
// 启动打字机(只启动一次)
|
||||||
if (!isTyping) {
|
if (!isTyping) {
|
||||||
isTyping = true;
|
isTyping = true;
|
||||||
@@ -423,7 +430,7 @@
|
|||||||
loadingTimer = null;
|
loadingTimer = null;
|
||||||
isTyping = false;
|
isTyping = false;
|
||||||
typeWriterTimer = null;
|
typeWriterTimer = null;
|
||||||
|
|
||||||
// 补全:如果消息内容还停留在'加载中.'或为空,则给出友好提示
|
// 补全:如果消息内容还停留在'加载中.'或为空,则给出友好提示
|
||||||
const msg = chatMsgList.value[aiMsgIndex].msg;
|
const msg = chatMsgList.value[aiMsgIndex].msg;
|
||||||
console.log('msg:', msg)
|
console.log('msg:', msg)
|
||||||
@@ -433,33 +440,41 @@
|
|||||||
chatMsgList.value[aiMsgIndex].msg = '';
|
chatMsgList.value[aiMsgIndex].msg = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果有组件
|
// 如果有组件
|
||||||
if(chunk.toolCall) {
|
if(chunk.toolCall) {
|
||||||
console.log('chunk.toolCall:', chunk.toolCall)
|
console.log('chunk.toolCall:', chunk.toolCall)
|
||||||
chatMsgList.value[aiMsgIndex].toolCall = chunk.toolCall
|
chatMsgList.value[aiMsgIndex].toolCall = chunk.toolCall
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有问题,则设置问题
|
// 如果有问题,则设置问题
|
||||||
if(chunk.question && chunk.question.length > 0) {
|
if(chunk.question && chunk.question.length > 0) {
|
||||||
chatMsgList.value[aiMsgIndex].question = chunk.question
|
chatMsgList.value[aiMsgIndex].question = chunk.question
|
||||||
}
|
}
|
||||||
|
|
||||||
isSessionActive = false;
|
isSessionActive.value = false;
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 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)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 打字机函数
|
// 打字机函数
|
||||||
function typeWriter() {
|
function typeWriter() {
|
||||||
if (aiMsgBuffer.length > 0) {
|
if (aiMsgBuffer.length > 0) {
|
||||||
chatMsgList.value[aiMsgIndex].msg += aiMsgBuffer[0];
|
chatMsgList.value[aiMsgIndex].msg += aiMsgBuffer[0];
|
||||||
aiMsgBuffer = aiMsgBuffer.slice(1);
|
aiMsgBuffer = aiMsgBuffer.slice(1);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
@@ -468,12 +483,35 @@
|
|||||||
// 等待新内容到来,不结束
|
// 等待新内容到来,不结束
|
||||||
typeWriterTimer = setTimeout(typeWriter, 30);
|
typeWriterTimer = setTimeout(typeWriter, 30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 停止请求函数
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "styles/ChatMainList.scss";
|
@import "styles/ChatMainList.scss";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ const API = '/agent/assistant/chat';
|
|||||||
* 获取AI聊天流式信息(仅微信小程序支持)
|
* 获取AI聊天流式信息(仅微信小程序支持)
|
||||||
* @param {Object} params 请求参数
|
* @param {Object} params 请求参数
|
||||||
* @param {Function} onChunk 回调,每收到一段数据触发
|
* @param {Function} onChunk 回调,每收到一段数据触发
|
||||||
* @returns {Promise}
|
* @returns {Object} 包含Promise和requestTask的对象
|
||||||
*/
|
*/
|
||||||
function agentChatStream(params, onChunk) {
|
function agentChatStream(params, onChunk) {
|
||||||
return new Promise((resolve, reject) => {
|
let requestTask;
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
const token = uni.getStorageSync('token');
|
const token = uni.getStorageSync('token');
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
||||||
console.log("发送请求内容: ", params)
|
console.log("发送请求内容: ", params)
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
const requestTask = uni.request({
|
requestTask = uni.request({
|
||||||
url: BASE_URL + API, // 替换为你的接口地址
|
url: BASE_URL + API, // 替换为你的接口地址
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: params,
|
data: params,
|
||||||
@@ -28,22 +29,21 @@ function agentChatStream(params, onChunk) {
|
|||||||
},
|
},
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
success(res) {
|
success(res) {
|
||||||
resolve(res.data);
|
resolve(res.data);
|
||||||
},
|
},
|
||||||
fail(err) {
|
fail(err) {
|
||||||
console.log("====> ", JSON.stringify(err))
|
console.log("====> ", JSON.stringify(err))
|
||||||
reject(err);
|
reject(err);
|
||||||
},
|
},
|
||||||
complete(res) {
|
complete(res) {
|
||||||
if(res.statusCode !== 200) {
|
if(res.statusCode !== 200) {
|
||||||
console.log("====> ", JSON.stringify(res))
|
console.log("====> ", JSON.stringify(res))
|
||||||
|
if (onChunk) {
|
||||||
if (onChunk) {
|
onChunk({ error: true, message: '服务器错误', detail: res });
|
||||||
onChunk({ error: true, message: '服务器错误', detail: res });
|
}
|
||||||
}
|
reject(res);
|
||||||
reject(res);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
requestTask.onHeadersReceived(res => {
|
requestTask.onHeadersReceived(res => {
|
||||||
@@ -75,18 +75,19 @@ function agentChatStream(params, onChunk) {
|
|||||||
});
|
});
|
||||||
// #endif
|
// #endif
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise,
|
||||||
|
requestTask
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// window.atob兼容性处理
|
// window.atob兼容性处理
|
||||||
const weAtob = (string) => {
|
const weAtob = (string) => {
|
||||||
const b64re =
|
const b64re = /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
|
||||||
/^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
|
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
const b64 =
|
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
||||||
|
|
||||||
// 去除空白字符
|
// 去除空白字符
|
||||||
string = String(string).replace(/[\t\n\f\r ]+/g, '');
|
string = String(string).replace(/[\t\n\f\r ]+/g, '');
|
||||||
|
|
||||||
// 验证 Base64 编码
|
// 验证 Base64 编码
|
||||||
if (!b64re.test(string)) {
|
if (!b64re.test(string)) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
@@ -150,4 +151,4 @@ function parseSSEChunk(raw) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { agentChatStream }
|
export { agentChatStream }
|
||||||
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