提交 df9bb5d7 编写于 作者: DCloud_JSON's avatar DCloud_JSON

1.1.0

- 【重要】优化`uni-ai-chat`云对象的`send`方法性能(并发:“普通消息对话”和“获得内容总结”),大幅提高会话响应速度。
- 修复 手机端 当消息列表数据不满一屏幕时,键盘弹出后看不到历史消息
- 修复 单独删除最后一次对话后,新的最后一条对话消息没有`换答案`按钮
- 修复 当消息发送失败 重新发送消息按钮图标与 复制和删除消息按钮 样式重叠的问题
上级 4948c67c
## 1.1.0(2023-06-09)
- 【重要】优化`uni-ai-chat`云对象的`send`方法性能(并发:“普通消息对话”和“获得内容总结”),大幅提高会话响应速度。
- 修复 手机端 当消息列表数据不满一屏幕时,键盘弹出后看不到历史消息
- 修复 单独删除最后一次对话后,新的最后一条对话消息没有`换答案`按钮
- 修复 当消息发送失败 重新发送消息按钮图标与 复制和删除消息按钮 样式重叠的问题
## 1.0.23(2023-06-08)
- 新增 支持单独删除某一次对话(注意:一次对话包含提问和回答2条消息)
- 更新 客户端网络请求超时时间(`manifest.json`->`networkTimeout`->`request`)设置为`600000`毫秒 [详情参考](https://uniapp.dcloud.net.cn/collocation/manifest.html#networktimeout)
......
......@@ -39,10 +39,10 @@
</template>
</view>
</view>
<uni-icons v-if="isLastMsg && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
<!-- <uni-icons v-if="isLastMsg && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
@click="msg.state == -100 ? retriesSendMsg() : ''" :color="msg.state===0?'#999':'#d22'"
:type="msgStateIcon(msg)" class="msgStateIcon">
</uni-icons>
</uni-icons> -->
</view>
</view>
</template>
......@@ -370,11 +370,19 @@
.more-icon {
color: #d4d4d4;
transform: rotate(90deg);
transform: rotate(270deg);
position: relative;
left: 4px;
left: -8px;
font-size: 16px;
z-index: 999;
padding-bottom: 5px;
height: 10px;
}
.menu-box-ai .more-icon{
left: 7px;
top: -2px;
transform: rotate(90deg);
}
.more-menu {
......
{
"id": "uni-ai-chat",
"name": "uni-ai-chat",
"version": "1.0.23",
"version": "1.1.0",
"description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js",
"scripts": {
......
......@@ -5,6 +5,9 @@
"style": {
"navigationBarTitleText": "uni-ai-chat",
"enablePullDownRefresh": false
// #ifdef H5
,"navigationStyle": "custom"
//#endif
}
}, {
"path": "uni_modules/uni-id-pages/pages/userinfo/deactivate/deactivate",
......
<template>
<view class="page">
<view class="container">
<!-- #ifdef H5 -->
<view v-if="isWidescreen" class="header">uni-ai-chat</view>
<!-- #endif -->
<text class="noData" v-if="msgLength === 0">没有对话记录</text>
<scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true">
<uni-ai-msg ref="msg" v-for="(msgIndex,index) in msgLength" :key="index" :msgIndex="index" @retriesSendMsg="retriesSendMsg" @changeAnswer="changeAnswer"
:show-cursor="index == msgLength - 1 && msgLength%2 === 0 && sseIndex" :isLastMsg="index == msgLength - 1" @removeMsg="removeMsg"></uni-ai-msg>
<view class="tip-ai-ing" v-if="msgLength && msgLength%2 !== 0 && lastMsgState != -100">
<uni-ai-msg ref="msg" v-for="(msgIndex,index) in msgLength" :key="index" :msgIndex="index"
@retriesSendMsg="retriesSendMsg" @changeAnswer="changeAnswer"
:show-cursor="index == msgLength - 1 && msgLength%2 === 0 && sseIndex"
:isLastMsg="index == visibleMsgLength - 1" @removeMsg="removeMsg"></uni-ai-msg>
<view v-if="lastMsgState == -100" class="retries-box">
<text>消息发送失败</text>
<uni-icons @click="retriesSendMsg" color="#d22" type="refresh-filled" class="retries-icon"></uni-icons>
</view>
<view class="tip-ai-ing" v-else-if="msgLength && msgLength%2 !== 0">
<text>uni-ai正在思考中...</text>
<view v-if="NODE_ENV == 'development' && !enableStream">
如需提速,请开通<uni-link class="uni-link" href="https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html"
......@@ -19,7 +24,7 @@
<view id="last-msg-item" style="height: 1px;"></view>
</scroll-view>
<view class="foot-box">
<view class="foot-box" :style="{'padding-bottom':footBoxPaddingBottom}">
<!-- #ifdef H5 -->
<view class="pc-menu" v-if="isWidescreen">
<view class="pc-trash pc-menu-item" @click="clearAllMsg" title="删除">
......@@ -33,12 +38,13 @@
<view class="foot-box-content">
<view v-if="!isWidescreen" class="menu">
<uni-icons class="menu-item" @click="clearAllMsg" type="trash" size="24" color="#888"></uni-icons>
<uni-icons class="menu-item" @click="setLLMmodel" color="#555" size="20px" type="settings"></uni-icons>
<uni-icons class="menu-item" @click="setLLMmodel" color="#555" size="20px"
type="settings"></uni-icons>
</view>
<view class="textarea-box">
<textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen"
:placeholder="placeholderText" :maxlength="-1"
placeholder-class="input-placeholder"></textarea>
:placeholder="placeholderText" :maxlength="-1" :adjust-position="false"
:disable-default-padding="false" placeholder-class="input-placeholder"></textarea>
</view>
<view class="send-btn-box" :title="(msgLength && msgLength%2 !== 0) ? 'ai正在回复中不能发送':''">
<!-- #ifdef H5 -->
......@@ -49,7 +55,6 @@
</view>
</view>
</view>
</view>
<llm-config ref="llm-config"></llm-config>
</view>
</template>
......@@ -67,11 +72,11 @@
// 收集所有执行云对象的任务列表
let uniCoTaskList = []
// 定义终止并清空 云对象的任务列表中所有 任务的方法
uniCoTaskList.clear = function(){
uniCoTaskList.clear = function() {
// 执行数组内的所有任务
uniCoTaskList.forEach(task=>task.abort())
uniCoTaskList.forEach(task => task.abort())
// 清空数组
uniCoTaskList.slice(0,uniCoTaskList.length)
uniCoTaskList.slice(0, uniCoTaskList.length)
}
// 获取广告id
......@@ -102,7 +107,9 @@
isWidescreen: false,
// 广告位id
adpid,
llmModel:false
llmModel: false,
keyboardHeight: 0,
visibleMsgLength:0
}
},
computed: {
......@@ -128,13 +135,16 @@
return process.env.NODE_ENV
},
//最后一条消息的状态
lastMsgState(){
lastMsgState() {
let mLength = this.msgList.length
if(mLength){
if (mLength) {
return this.msgList[mLength - 1].state
}else{
} else {
return false
}
},
footBoxPaddingBottom() {
return (this.keyboardHeight || 10) + 'px'
}
},
// 监听msgList变化,将其存储到本地缓存中
......@@ -148,7 +158,8 @@
this.updateLastMsg(msgList[msgLength - 1])
})
}
msgList = msgList.filter(i=>i.isDelete !== true)
msgList = msgList.filter(i => i.isDelete !== true)
this.visibleMsgLength = msgList.length
// 将msgList存储到本地缓存中
uni.setStorage({
"key": "uni-ai-msg",
......@@ -158,20 +169,20 @@
// 深度监听msgList变化
deep: true
},
llmModel(llmModel){
llmModel(llmModel) {
let title = 'uni-ai-chat'
if(llmModel){
if (llmModel) {
title += ` (${llmModel})`
}
uni.setNavigationBarTitle({title})
// uni.setNavigationBarTitle({title})
// #ifdef H5
if(this.isWidescreen){
if (this.isWidescreen) {
document.querySelector('.header').innerText = title
}
// #endif
uni.setStorage({
key:'uni-ai-chat-llmModel',
data:llmModel
key: 'uni-ai-chat-llmModel',
data: llmModel
})
}
},
......@@ -186,7 +197,6 @@
// #endif
},
async mounted() {
// 如果存在广告位id且用户token未过期
if (this.adpid && uniCloud.getCurrentUserInfo().tokenExpired > Date.now()) {
// 查询当前用户的积分
......@@ -233,7 +243,6 @@
}
}
// this.msgList.pop()
// console.log('this.msgList', this.msgList);
......@@ -257,9 +266,9 @@
if (e.keyCode == 13 && !adjunctKeydown) {
e.preventDefault()
// 执行发送
setTimeout(()=>{
setTimeout(() => {
this.beforeSendMsg();
},300)
}, 300)
}
};
textareaDom.onkeyup = e => {
......@@ -268,13 +277,47 @@
adjunctKeydown = false;
}
};
// 可视窗口高
let initialInnerHeight = window.innerHeight;
if (uni.getSystemInfoSync().platform == "ios") {
textareaDom.addEventListener('focus', () => {
let interval = setInterval(function() {
if (window.innerHeight !== initialInnerHeight) {
clearInterval(interval)
// 触发相应的回调函数
document.querySelector('.container').style.height = window.innerHeight + 'px'
window.scrollTo(0, 0);
this.showLastMsg()
}
}, 1);
})
textareaDom.addEventListener('blur', () => {
document.querySelector('.container').style.height = initialInnerHeight + 'px'
})
}else{
window.addEventListener('resize',(e)=>{
this.showLastMsg()
})
}
}
// #endif
// #ifndef H5
uni.onKeyboardHeightChange(e => {
this.keyboardHeight = e.height
// 在dom渲染完毕后 使聊天窗口滚动到最后一条消息
this.$nextTick(() => {
this.showLastMsg()
})
})
// #endif
},
methods: {
setLLMmodel(){
this.$refs['llm-config'].open(model=>{
console.log('model',model);
setLLMmodel() {
this.$refs['llm-config'].open(model => {
console.log('model', model);
this.llmModel = model
})
},
......@@ -313,6 +356,20 @@
}
this.msgList.splice(length - 1, 1, lastMsg)
},
setSummarize(summarizeData) {
// console.log('setSummarize');
let length = this.msgList.length;
let index = length - 2
// 从 v1.1.0起,总结可能是总结倒数第二条之前的内容
if (index < 0) {
index = 0
}
let msg = this.msgList[index]
msg.summarize = summarizeData
this.msgList.splice(index, 1, msg)
// console.log('setSummarize this.msgList',this.msgList);
},
// 广告关闭事件
onAdClose(e) {
console.log('onAdClose e.detail.isEnded', e.detail.isEnded);
......@@ -370,9 +427,9 @@
this.send()
},
// 换一个答案
async changeAnswer(){
async changeAnswer() {
// 如果问题还在回答中需要先关闭
if(this.sseIndex){
if (this.sseIndex) {
this.closeSseChannel()
}
......@@ -381,32 +438,31 @@
// 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问
this.updateLastMsg({
illegal:false
illegal: false
})
this.send()
},
removeMsg(index){
removeMsg(index) {
// #ifdef VUE3
this.msgList[index].isDelete = true
if(this.msgList[index].isAi && this.msgList[index - 1]){
if (this.msgList[index].isAi && this.msgList[index - 1]) {
this.msgList[index - 1].isDelete = true
}else if(this.msgList[index + 1]){
} else if (this.msgList[index + 1]) {
this.msgList[index + 1].isDelete = true
}
// #endif
// #ifdef VUE2
this.$set(msgList[index],"isDelete",true)
msgList.slice(index,1,msgList[index])
if(msgList[index].isAi && msgList[index - 1]){
this.$set(msgList[index - 1],"isDelete",true)
}else if(msgList[index + 1]){
this.$set(msgList[index + 1],"isDelete",true)
this.$set(msgList[index], "isDelete", true)
if (msgList[index].isAi && msgList[index - 1]) {
this.$set(msgList[index - 1], "isDelete", true)
} else if (msgList[index + 1]) {
this.$set(msgList[index + 1], "isDelete", true)
}
// #endif
},
async beforeSendMsg() {
if(this.inputBoxDisabled){
if (this.inputBoxDisabled) {
return uni.showToast({
title: 'ai正在回复中不能发送',
icon: 'none'
......@@ -496,7 +552,7 @@
let messages = []
// 复制一份,消息列表数据
let msgs = msgList.filter(i=>i.isDelete !== true)
let msgs = msgList.filter(i => i.isDelete !== true)
// 带总结的消息 index
let findIndex = [...msgs].reverse().findIndex(item => item.summarize)
// console.log('findIndex', findIndex)
......@@ -570,6 +626,11 @@
// console.log('on end', e);
// 如果e存在且包含summarize或insufficientScore属性
if (e) {
// 如果e包含summarize属性
if (e.summarize) {
// 设置总结
this.setSummarize(e.summarize)
}else{
// 更新最后一条消息
this.updateLastMsg(lastMsg => {
// 如果e包含illegal属性
......@@ -580,11 +641,6 @@
// 倒数第二条(用户发问内容)也需要设置illegal的值
this.msgList[this.msgList.length - 2].illegal = e.illegal
}
// 如果e包含summarize属性
else if (e.summarize) {
// 将最后一条消息的summarize属性更新为e的summarize属性
lastMsg.summarize = e.summarize
}
// 如果e包含insufficientScore属性
else if (e.insufficientScore) {
// 将最后一条消息的insufficientScore属性更新为e的insufficientScore属性
......@@ -592,6 +648,7 @@
}
})
}
}
// 结束流式响应 将流式响应计数值 设置为 0
this.sseIndex = 0
......@@ -603,17 +660,17 @@
// 导入uni-ai-chat模块,并设置customUI为true
let task = uniCoTask({
coName:"uni-ai-chat",
funName:"send",
param:[{
coName: "uni-ai-chat",
funName: "send",
param: [{
messages, // 消息列表
sseChannel, // 消息通道
llmModel:this.llmModel
llmModel: this.llmModel
}],
config:{
config: {
customUI: true
},
success:res => {
success: res => {
// console.log(111,res);
if (!sseChannel) {
if (!res.data) {
......@@ -644,13 +701,15 @@
isAi: true,
// 添加消息内容
content,
// 添加消息总结
summarize,
// 添加消息分数不足标记
insufficientScore,
// 添加消息涉敏标记
illegal
})
// 如果回调包含总结的内容,就设置总结
if(summarize){
this.setSummarize(summarize)
}
// 滚动窗口以显示最新的一条消息
this.$nextTick(() => {
this.showLastMsg()
......@@ -661,7 +720,7 @@
this.sseIndex = 0
}
},
fail:e => {
fail: e => {
console.log(e);
// 获取消息列表长度
let l = this.msgList.length
......@@ -686,9 +745,9 @@
})
uniCoTaskList.push(task)
},
closeSseChannel(){
closeSseChannel() {
// 如果存在消息通道,就关闭消息通道
if(sseChannel){
if (sseChannel) {
sseChannel.close()
sseChannel = false
}
......@@ -737,26 +796,31 @@
/* #endif */
/* #ifndef APP-NVUE */
.container *,
view,
textarea,
button,
.page {
button {
display: flex;
box-sizing: border-box;
}
page {
height: 100%;
width: 100%;
}
/* #endif */
.stop-responding {
font-size: 14px;
border-radius: 3px;
margin-bottom:15px;
margin-bottom: 15px;
background-color: #f0b00a;
color: #FFF;
width: 90px;
height: 30px;
line-height: 30px;
margin:0 auto;
margin: 0 auto;
justify-content: center;
margin-bottom: 15px;
/* #ifdef H5 */
......@@ -764,41 +828,19 @@
/* #endif */
}
.stop-responding:hover{
.stop-responding:hover {
box-shadow: 0 0 10px #aaa;
}
/* #ifndef APP-NVUE */
page,
/* #endif */
.page,
.container {
background-color: #efefef;
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
height: 100vh;
/* #endif */
/* #ifdef H5 */
height: calc(100vh - 44px);
/* #endif */
height: 100%;
background-color: #FAFAFA;
flex-direction: column;
align-items: center;
justify-content: center;
// border: 1px solid blue;
}
/* #ifndef APP-NVUE */
.container {
background-color: #FAFAFA;
}
/* #endif */
.foot-box {
width: 750rpx;
display: flex;
......@@ -859,9 +901,10 @@
align-items: center;
flex-shrink: 0;
}
.menu-item{
.menu-item {
width: 30rpx;
margin:0 10rpx;
margin: 0 10rpx;
}
.send {
......@@ -883,9 +926,10 @@
.msg-list {
height: 0; //不可省略,先设置为0 再由flex: 1;撑开才是一个滚动容器
flex: 1;
height: 1px;
width: 750rpx;
// border: 1px solid red;
}
.noData {
......@@ -916,17 +960,11 @@
border-top: solid 1px #dde0e2;
}
.page {
width: 100vw;
flex-direction: row;
}
.page * {
.container * {
max-width: 950px;
}
.container
{
.container {
box-shadow: 0 0 5px #e0e1e7;
margin-top: 44px;
border-radius: 10px;
......@@ -1026,6 +1064,15 @@
line-height: 28px;
}
}
/* #endif */
.retries-box{
justify-content: center;
align-items: center;
font-size: 12px;
color: #d2071b;
}
.retries-icon{
margin-top: 1px;
margin-left: 5px;
}
</style>
\ No newline at end of file
......@@ -191,8 +191,6 @@ module.exports = {
// 返回处理后的结果
return result
},
// 发送消息
async send({
// 消息内容
......@@ -226,6 +224,31 @@ module.exports = {
}
chatCompletionOptions.model = llmModel
}
// 判断是否需要总结,如果需要就开始总结
// 计算消息总长度,判断是否需要总结
let needSummarize = messages.map(i => i.content).join('').length > 50,
// 总结的内容默认为 false 表示没有内容或者暂未拿到
summarizeData = false,
//成功拿到总结内容的回调函数列表
getSummarizeCallbackList = []
console.log('needSummarize',needSummarize);
if (needSummarize) {
// 获取总结
let replySummarize = getSummarize(messages)
.then((replySummarize)=>{
// console.log('replySummarize1',replySummarize);
summarizeData = replySummarize
getSummarizeCallbackList.forEach(fun=>fun())
})
.catch((error)=>{
// 抛出错误
throw error
})
}
return await chatCompletion({
messages, //消息内容
sseChannel, //sse渠道对象
......@@ -247,6 +270,7 @@ module.exports = {
// 获取语言模型管理器
const llmManager = uniCloud.ai.getLLMManager(llm)
// 调用chatCompletion方法,传入参数
// console.log('______messages',messages);
let res = await llmManager.chatCompletion({
...chatCompletionOptions,
messages,
......@@ -290,19 +314,27 @@ module.exports = {
// 计算消息总长度
let totalTokens = messages.map(i => i.content).join('').length;
// console.log('totalTokens',totalTokens);
// 如果不需要总结且消息总长度超过500
if (!summarize && totalTokens > 500) {
// 获取总结
let replySummarize = await getSummarize(messages)
// console.log('replySummarize',replySummarize)
// 判断:是否有‘总结’需要带上
if(needSummarize){
if(!summarizeData){
// 如果需要等待
await new Promise((reject,resolve)=>{
getSummarizeCallbackList.push(reject)
})
// console.log('等到了总结',summarizeData);
} else{
// console.log('直接拿到总结',summarizeData);
}
// 结束sseChannel并返回总结
await channel.end({
summarize: replySummarize
"summarize": summarizeData
})
} else {
}else{
// 结束sseChannel
await channel.end()
}
// 返回处理结果
resolve({
errCode: 0
......@@ -326,21 +358,26 @@ module.exports = {
})
})
} else {
// 如果不需要总结
// 如果 不是正在总结
if (summarize == false) {
// 将回复内容添加到消息列表中
messages.push({
"content": res.reply,
"role": "assistant"
})
// 计算消息总长度
let totalTokens = messages.map(i => i.content).join('').length;
// 如果消息总长度超过500
if (totalTokens > 500) {
// 获取总结
let replySummarize = await getSummarize(messages)
// 将总结添加到返回结果中
res.summarize = replySummarize
// 判断:是否有‘总结’需要带上
if(needSummarize){
if(!summarizeData){
// 如果需要等待
await new Promise((reject,resolve)=>{
getSummarizeCallbackList.push(reject)
})
// console.log('等到了总结',summarizeData);
}else{
// console.log('直接拿到总结',summarizeData);
}
res.summarize = summarizeData
}
}
// 如果存在错误
......@@ -359,23 +396,32 @@ module.exports = {
//获总结
async function getSummarize(messages) {
messages.push({
let _messages = [...messages]
_messages.push({
"content": "请简要总结上述全部对话",
"role": "user"
})
// 调用chatCompletion函数,传入messages、summarize、stream、sseChannel参数
let res = await chatCompletion({
// 消息内容
messages,
messages:_messages,
// 是否需要总结
summarize: true,
// 是否需要流式返回
stream: false,
// sse渠道对象
sseChannel: false
sseChannel: false ,
// 大语言模型配置
llm
})
//故意延迟看看,总结比答案晚,是否成功进入等的逻辑
//function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); }
// await sleep(10000)
// console.log('getSummarize',res);
// 返回总结的文字内容
return res.reply
return res.data.reply
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册