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

1.0.21

- 修复 用户输入的内容,会被markdown解析,导致显示错误的问题
- 修复 web-pc端 发送消息,会失去焦点的问题
- 修复 当ai回答的问题未完成时,点击换答案会进入异常的问题
- 修复 部分情况下 ai回复问题时,光标不停地换行和撤销换行 导致界面上下抖动的问题
- 修复 短时间内多次操作 `停止响应`和`换答案`功能,偶发响应的数据顺序错乱的问题
- 新增 客户端支持`model`选择功能(仅预置了`openai`的6个model,可以自行在组件路径:`/components/llm-config/llm-config.vue`中,增改)
上级 e683dc17
## 1.0.21(2023-06-06)
- 修复 用户输入的内容,会被markdown解析,导致显示错误的问题
- 修复 web-pc端 发送消息,会失去焦点的问题
- 修复 当ai回答的问题未完成时,点击换答案会进入异常的问题
- 修复 部分情况下 ai回复问题时,光标不停地换行和撤销换行 导致界面上下抖动的问题
- 修复 短时间内多次操作 `停止响应``换答案`功能,偶发响应的数据顺序错乱的问题
- 新增 客户端支持`model`选择功能(仅预置了`openai`的6个model,可以自行在组件路径:`/components/llm-config/llm-config.vue`中,增改)
## 1.0.20(2023-06-05) ## 1.0.20(2023-06-05)
- 修复 移动端 部分情况下 代码块的复制按钮,挡住内容的问题 - 修复 移动端 部分情况下 代码块的复制按钮,挡住内容的问题
- 修复 微信小程序端 点击“复制按钮”无效的问题 - 修复 微信小程序端 点击“复制按钮”无效的问题
......
class Task {
constructor({
success,
fail,
complete
} = {}) {
this.status = 0
this.callback = {
success,
fail,
complete
}
}
invoke(callbackName, ...args) {
if (this.status !== 0) {
// console.log('此任务已被终止');
return
}
const callback = this.callback[callbackName]
callback && callback(...args)
}
abort() {
this.status = 1
}
}
export default function main({
coName,
funName,
param,
success,
fail,
complete,
config
} = {}) {
if(!Array.isArray(param)){
throw new Error('param的值必须为数组')
}
const task = new Task({
success,
fail,
complete
})
const uniCloudCo = uniCloud.importObject(coName, config||{})
uniCloudCo[funName](...param)
.then(res => {
task.invoke('success', res)
})
.catch(err => {
task.invoke('fail', err)
})
.finally(res => {
task.invoke('complete', res)
})
return task
}
\ No newline at end of file
<template>
<uni-popup ref="popup" type="top">
<view class="box">
<text class="title">请选择llm的model</text>
<radio-group @change="radioChange" class="radio-group">
<label class="item" v-for="(model, index) in models" :key="model">
<radio :value="model" :checked="currentModel === model" class="radio" />
<view class="item-title">{{model}}</view>
</label>
</radio-group>
<view class="btn-box">
<view @click="cancel" class="btn cancel">取消</view>
<view @click="confirm" class="btn confirm">确认</view>
</view>
</view>
</uni-popup>
</template>
<script>
let confirmCallback = ()=>{}
export default {
name: "llm-config",
data() {
return {
models: [
"gpt-4",
"gpt-4-0314",
"gpt-4-32k",
"gpt-4-32k-0314",
"gpt-3.5-turbo",
"gpt-3.5-turbo-0301"
],
currentModel:''
};
},
mounted() {
this.currentModel = uni.getStorageSync('uni-ai-chat-llmModel')
},
methods: {
open(callback){
confirmCallback = callback
this.$refs.popup.open('center')
},
radioChange(event) {
console.log('event',event.detail.value)
this.currentModel = event.detail.value
},
cancel(){
this.$refs.popup.close()
},
confirm(){
// console.log(this.models[this.current]);
confirmCallback(this.currentModel)
this.$refs.popup.close()
},
}
}
</script>
<style>
/* #ifndef APP-NVUE */
.box,
/* #ifdef H5 */
.box *,
/* #endif */
radio-group,
label
{
display: flex;
box-sizing: border-box;
}
/* #endif */
.box,.title,.btn-box {
width: 250px;
}
.box {
background-color: #fff;
display: flex;
flex-direction: column;
align-items: flex-start;
padding-bottom: 0;
border-radius: 5px;
}
.title {
font-size: 16px;
padding: 10px 0;
padding-bottom: 5px;
font-weight: 400;
flex: 1;
text-align: center;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
}
.radio-group {
flex-direction: column;
padding: 0 15px;
}
.radio {
transform: scale(0.7);
}
.item {
flex-direction: row;
margin-bottom: 5px;
position: relative;
}
.item-title {
font-size: 14px;
color: #555;
}
.btn-box{
/* #ifdef APP-NVUE */
border-top:solid 1px #ccc;
/* #endif */
height: 48px;
position: relative;
}
/* #ifndef APP-NVUE */
.btn-box:after {
content: " ";
position: absolute;
left: 0;
top: 0;
right: 0;
height: 1px;
border-top: 1px solid #d5d5d6;
color: #d5d5d6;
transform-origin: 0 0;
transform: scaleY(.5);
}
/* #endif */
.btn{
justify-content: center;
align-items: center;
width: 150px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.confirm {
color: #007aff;
position: relative;
/* #ifdef APP-NVUE */
border-left:solid 1px #ccc;
/* #endif */
}
/* #ifndef APP-NVUE */
.confirm::before {
content: "";
position: absolute;
left: 0;
top: 0;
right: 0;
background-color: #d5d5d6;
height: 48px;
width: 1px;
/* border-top: 1px solid #d5d5d6; */
/* color: #d5d5d6; */
/* transform-origin: 0 0; */
transform: scaleX(.5);
}
/* #endif */
</style>
\ No newline at end of file
...@@ -8,9 +8,13 @@ ...@@ -8,9 +8,13 @@
<image class="avatar" :src="msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'" mode="widthFix"></image> <image class="avatar" :src="msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'" mode="widthFix"></image>
</view> </view>
<view class="content"> <view class="content">
<view class="rich-text-box" :class="{'show-cursor':showCursor}" ref="rich-text-box"> <view v-if="msg.isAi" class="rich-text-box" :class="{'show-cursor':showCursor}" ref="rich-text-box">
<rich-text v-if="nodes&&nodes.length" space="nbsp" :nodes="nodes" @itemclick="trOnclick"></rich-text> <rich-text v-if="nodes&&nodes.length" space="nbsp" :nodes="nodes" @itemclick="trOnclick"></rich-text>
</view> </view>
<view v-else>
{{msgContent}}
</view>
<view v-if="isLastMsg && adpid && msg.insufficientScore"> <view v-if="isLastMsg && adpid && msg.insufficientScore">
<text style="color: red;"> <text style="color: red;">
默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效) 默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效)
...@@ -154,17 +158,17 @@ ...@@ -154,17 +158,17 @@
} }
}, },
computed: { computed: {
md() { msgContent() {
return this.msg.content return this.msg.content
}, },
nodes() { nodes() {
let htmlString = '' let htmlString = ''
// 修改转换结果的htmlString值 用于正确给界面增加鼠标闪烁的效果 // 修改转换结果的htmlString值 用于正确给界面增加鼠标闪烁的效果
// 判断markdown中代码块标识符的数量是否为偶数 // 判断markdown中代码块标识符的数量是否为偶数
if (this.md.split("```").length % 2) { if (this.msgContent.split("```").length % 2) {
htmlString = markdownIt.render(this.md + ' \n <span class="cursor">|</span>'); htmlString = markdownIt.render(this.msgContent + ' <span class="cursor">|</span>');
} else { } else {
htmlString = markdownIt.render(this.md) + ' \n <span class="cursor">|</span>'; htmlString = markdownIt.render(this.msgContent) + ' \n <span class="cursor">|</span>';
} }
// #ifndef APP-NVUE // #ifndef APP-NVUE
...@@ -229,7 +233,7 @@ ...@@ -229,7 +233,7 @@
// 复制文本内容到系统剪切板 // 复制文本内容到系统剪切板
copy() { copy() {
uni.setClipboardData({ uni.setClipboardData({
data: this.md, data: this.msgContent,
showToast: false, showToast: false,
success() { success() {
uni.showToast({ uni.showToast({
......
{ {
"id": "uni-ai-chat", "id": "uni-ai-chat",
"name": "uni-ai-chat", "name": "uni-ai-chat",
"version": "1.0.20", "version": "1.0.21",
"description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体", "description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
......
...@@ -18,15 +18,19 @@ ...@@ -18,15 +18,19 @@
</scroll-view> </scroll-view>
<view class="foot-box"> <view class="foot-box">
<view class="menu" v-if="isWidescreen"> <view class="pc-menu" v-if="isWidescreen">
<view class="trash menu-item"> <view class="pc-trash pc-menu-item" @click="clear" title="删除">
<image @click="clear" src="@/static/remove.png" mode="heightFix"></image> <image src="@/static/remove.png" mode="heightFix"></image>
</view>
<view class="settings pc-menu-item" @click="setLLMmodel" title="设置">
<uni-icons color="#555" size="20px" type="settings"></uni-icons>
</view> </view>
</view> </view>
<view class="foot-box-content"> <view class="foot-box-content">
<view v-if="!isWidescreen" class="trash"> <view v-if="!isWidescreen" class="menu">
<uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons> <uni-icons class="menu-item" @click="clear" type="trash" size="24" color="#888"></uni-icons>
<uni-icons class="menu-item" @click="setLLMmodel" color="#555" size="20px" type="settings"></uni-icons>
</view> </view>
<view class="textarea-box" @click="focus = true"> <view class="textarea-box" @click="focus = true">
<textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen" <textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen"
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
</view> </view>
</view> </view>
</view> </view>
<llm-config ref="llm-config"></llm-config>
</view> </view>
</template> </template>
...@@ -52,6 +57,16 @@ ...@@ -52,6 +57,16 @@
msgList msgList
} from '@/pages/chat/msgList.js'; } from '@/pages/chat/msgList.js';
// 导入uniCloud云对象task模块
import uniCoTask from '@/common/unicloud-co-task.js';
// 收集所有执行云对象的任务列表
let uniCoTaskList = []
// 定义终止并清空 云对象的任务列表中所有 任务的方法
uniCoTaskList.clear = function(){
uniCoTaskList.forEach(task=>task.abort())
uniCoTaskList.slice(0,0)
}
// 获取广告id // 获取广告id
const { const {
adpid adpid
...@@ -59,9 +74,6 @@ ...@@ -59,9 +74,6 @@
// 初始化sse通道 // 初始化sse通道
let sseChannel = false; let sseChannel = false;
// 是否通过回调,当用户点击清空后应当跳过前一次请求的回调
let skip_callback = false;
// 键盘的shift键是否被按下 // 键盘的shift键是否被按下
let shiftKeyPressed = false let shiftKeyPressed = false
export default { export default {
...@@ -83,7 +95,8 @@ ...@@ -83,7 +95,8 @@
isWidescreen: false, isWidescreen: false,
// 广告位id // 广告位id
adpid, adpid,
focus: false focus: false,
llmModel:false
} }
}, },
computed: { computed: {
...@@ -111,14 +124,6 @@ ...@@ -111,14 +124,6 @@
}, },
// 监听msgList变化,将其存储到本地缓存中 // 监听msgList变化,将其存储到本地缓存中
watch: { watch: {
// #ifdef H5
inputBoxDisabled(val) {
this.$nextTick(() => {
this.focus = !val
// console.log('this.focus', this.focus);
})
},
// #endif
msgList: { msgList: {
handler(msgList) { handler(msgList) {
let msgLength = msgList.length let msgLength = msgList.length
...@@ -136,6 +141,22 @@ ...@@ -136,6 +141,22 @@
}, },
// 深度监听msgList变化 // 深度监听msgList变化
deep: true deep: true
},
llmModel(llmModel){
let title = 'uni-ai-chat'
if(llmModel){
title += ` (${llmModel})`
}
uni.setNavigationBarTitle({title})
// #ifdef H5
if(this.isWidescreen){
document.querySelector('.header').innerText = title
}
// #endif
uni.setStorage({
key:'uni-ai-chat-llmModel',
data:llmModel
})
} }
}, },
beforeMount() { beforeMount() {
...@@ -177,14 +198,16 @@ ...@@ -177,14 +198,16 @@
// }) // })
// } // }
// 获得历史对话记录
let _msgList = uni.getStorageSync('uni-ai-msg') || []; let _msgList = uni.getStorageSync('uni-ai-msg') || [];
if (_msgList.length) { if (_msgList.length) {
msgList.push(..._msgList) msgList.push(..._msgList)
} }
this.msgList = msgList this.msgList = msgList
// 获得之前设置的llmModel
this.llmModel = uni.getStorageSync('uni-ai-chat-llmModel')
// 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。 // 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。
let length = this.msgList.length let length = this.msgList.length
if (length) { if (length) {
...@@ -233,6 +256,12 @@ ...@@ -233,6 +256,12 @@
// #endif // #endif
}, },
methods: { methods: {
setLLMmodel(){
this.$refs['llm-config'].open(model=>{
console.log('model',model);
this.llmModel = model
})
},
// 此(惰性)函数,检查是否开通uni-push;决定是否启用enableStream // 此(惰性)函数,检查是否开通uni-push;决定是否启用enableStream
async checkIsOpenPush() { async checkIsOpenPush() {
try { try {
...@@ -324,7 +353,14 @@ ...@@ -324,7 +353,14 @@
// 发送消息 // 发送消息
this.send() this.send()
}, },
// 换一个答案
async changeAnswer(){ async changeAnswer(){
// 如果问题还在回答中需要先关闭
if(this.sseIndex){
this.closeSseChannel()
}
//删除旧的回答
this.msgList.pop() this.msgList.pop()
// 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问 // 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问
...@@ -389,9 +425,6 @@ ...@@ -389,9 +425,6 @@
} }
} }
// 检查是否开通uni-push;决定是否启用enableStream
await this.checkIsOpenPush()
// 如果内容为空 // 如果内容为空
if (!this.content) { if (!this.content) {
// 弹出提示框 // 弹出提示框
...@@ -424,6 +457,7 @@ ...@@ -424,6 +457,7 @@
this.send() // 发送消息 this.send() // 发送消息
}, },
async send() { async send() {
let messages = [] let messages = []
// 复制一份,消息列表数据 // 复制一份,消息列表数据
let msgs = JSON.parse(JSON.stringify(this.msgList)) let msgs = JSON.parse(JSON.stringify(this.msgList))
...@@ -462,6 +496,10 @@ ...@@ -462,6 +496,10 @@
// 在控制台输出 向ai机器人发送的完整消息内容 // 在控制台输出 向ai机器人发送的完整消息内容
console.log('send to ai messages:', messages); console.log('send to ai messages:', messages);
// 检查是否开通uni-push;决定是否启用enableStream
await this.checkIsOpenPush()
// console.log('this.enableStream',this.enableStream);
// 判断是否开启了流式响应模式 // 判断是否开启了流式响应模式
if (this.enableStream) { if (this.enableStream) {
// 创建消息通道 // 创建消息通道
...@@ -527,19 +565,19 @@ ...@@ -527,19 +565,19 @@
await sseChannel.open() // 等待通道开启 await sseChannel.open() // 等待通道开启
} }
// 重置skip_callback为false,以便下一次请求可以正常回调
skip_callback = false
// 导入uni-ai-chat模块,并设置customUI为true // 导入uni-ai-chat模块,并设置customUI为true
const uniAiChat = uniCloud.importObject("uni-ai-chat", { let task = uniCoTask({
customUI: true coName:"uni-ai-chat",
}) funName:"send",
param:[{
// 发送消息给ai机器人
uniAiChat.send({
messages, // 消息列表 messages, // 消息列表
sseChannel // 消息通道 sseChannel, // 消息通道
}) llmModel:this.llmModel
.then(res => { }],
config:{
customUI: true
},
success:res => {
// console.log(111,res); // console.log(111,res);
if (!sseChannel) { if (!sseChannel) {
if (!res.data) { if (!res.data) {
...@@ -550,8 +588,6 @@ ...@@ -550,8 +588,6 @@
state: 100 state: 100
}) })
// console.log(res, res.reply); // console.log(res, res.reply);
// 判断是否要跳过本次回调,防止请求未返回时,历史对话已被清空。引起对话顺序错误 导致 对话输入框卡住
if (!skip_callback) {
let { let {
"reply": content, "reply": content,
summarize, summarize,
...@@ -583,16 +619,13 @@ ...@@ -583,16 +619,13 @@
this.$nextTick(() => { this.$nextTick(() => {
this.showLastMsg() this.showLastMsg()
}) })
} else {
console.log('用户点击了清空按钮,跳过前一次请求的回调。内容:', res.data.reply);
}
} else { } else {
// 处理 sseChannel没结束 云函数提前结束的情况 // 处理 sseChannel没结束 云函数提前结束的情况
sseChannel.close() sseChannel.close()
this.sseIndex = 0 this.sseIndex = 0
} }
}) },
.catch(e => { fail:e => {
console.log(e); console.log(e);
// 获取消息列表长度 // 获取消息列表长度
let l = this.msgList.length let l = this.msgList.length
...@@ -613,15 +646,18 @@ ...@@ -613,15 +646,18 @@
content: JSON.stringify(e.message), content: JSON.stringify(e.message),
showCancel: false showCancel: false
}); });
}
}) })
uniCoTaskList.push(task)
}, },
closeSseChannel(){ closeSseChannel(){
// 如果存在消息通道,就关闭消息通道
if(sseChannel){ if(sseChannel){
sseChannel.close() sseChannel.close()
sseChannel = false sseChannel = false
} }
// 将skip_callback设置为true,以便下一次请求可以正常回调 // 清空历史网络请求(调用云对象)任务
skip_callback = true uniCoTaskList.clear()
// 将流式响应计数值归零 // 将流式响应计数值归零
this.sseIndex = 0 this.sseIndex = 0
}, },
...@@ -647,14 +683,8 @@ ...@@ -647,14 +683,8 @@
complete: (e) => { complete: (e) => {
// 如果用户确认清空聊天记录 // 如果用户确认清空聊天记录
if (e.confirm) { if (e.confirm) {
// 如果存在消息通道,就关闭消息通道 // 关闭ssh请求
if (sseChannel) { this.closeSseChannel()
sseChannel.close()
}
// 将skip_callback设置为true,以便下一次请求可以正常回调
skip_callback = true
// 将流式响应计数值归零
this.sseIndex = 0
// 将消息列表清空 // 将消息列表清空
this.msgList.splice(0, this.msgLength); this.msgList.splice(0, this.msgLength);
} }
...@@ -685,7 +715,7 @@ ...@@ -685,7 +715,7 @@
font-size: 14px; font-size: 14px;
border-radius: 3px; border-radius: 3px;
margin-bottom:15px; margin-bottom:15px;
background-color: #f0a020; background-color: #f0b00a;
color: #FFF; color: #FFF;
width: 90px; width: 90px;
height: 30px; height: 30px;
...@@ -788,6 +818,16 @@ ...@@ -788,6 +818,16 @@
margin-left: 10rpx; margin-left: 10rpx;
} }
.menu {
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.menu-item{
width: 30rpx;
margin:0 10rpx;
}
.send { .send {
color: #FFF; color: #FFF;
border-radius: 4px; border-radius: 4px;
...@@ -915,11 +955,11 @@ ...@@ -915,11 +955,11 @@
padding-bottom: 0; padding-bottom: 0;
} }
.menu { .pc-menu {
padding: 0 10px; padding: 0 10px;
} }
.menu-item { .pc-menu-item {
height: 20px; height: 20px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
...@@ -929,11 +969,11 @@ ...@@ -929,11 +969,11 @@
cursor: pointer; cursor: pointer;
} }
.trash { .pc-trash {
opacity: 0.8; opacity: 0.8;
} }
.trash image { .pc-trash image {
height: 15px; height: 15px;
} }
......
...@@ -198,7 +198,9 @@ module.exports = { ...@@ -198,7 +198,9 @@ module.exports = {
// 消息内容 // 消息内容
messages, messages,
// sse渠道对象 // sse渠道对象
sseChannel sseChannel,
// 语言模型
llmModel
}) { }) {
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 // 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
...@@ -217,6 +219,10 @@ module.exports = { ...@@ -217,6 +219,10 @@ module.exports = {
// 向uni-ai发送消息 // 向uni-ai发送消息
// 调用chatCompletion函数,传入messages、sseChannel、llm参数 // 调用chatCompletion函数,传入messages、sseChannel、llm参数
let {llm,chatCompletionOptions} = config let {llm,chatCompletionOptions} = config
// 如果客户端传了llmModel 就覆盖配置的model
if(llmModel){
chatCompletionOptions.model = llmModel
}
return await chatCompletion({ return await chatCompletion({
messages, //消息内容 messages, //消息内容
sseChannel, //sse渠道对象 sseChannel, //sse渠道对象
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册