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

1.0.21

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