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

1.0.12

上级 fa34d5a7
## 1.0.12(2023-05-22)
- 优化 提升性能
- 更新 更改`uni-ai-chat`云对象的`runtime`版本为`Nodejs12`
## 1.0.11(2023-05-17)
- 修复 流式响应模式,微信小程序端,部分情况下:消息内容突然变空白的问题
- 优化 uni-ai问题回复完成后,消息输入框自动获取焦点
......
@import "@/lib/highlight/github-dark.min.css";
@import "@/lib/highlight/github-dark.min.css";
/* #ifndef APP-NVUE */
.rich-text-box ::v-deep pre.hljs {
......
<template>
<view class="rich-text-box" :class="{'show-cursor':showCursor}" ref="rich-text-box">
<rich-text v-if="nodes&&nodes.length" space="nbsp" :nodes="nodes"></rich-text>
<!-- #ifdef H5 -->
<view class="copy-box" :style="{left,top}">
<text class="copy" @click="copy">复制</text>
<!-- <view v-if="left != '-100px'" class="copy-mask" @click="left = '-100px'"></view> -->
</view>
<!-- #endif -->
</view>
<template>
<view class="msg-item">
<view class="create_time-box">
<uni-dateformat class="create_time" :date="msg.create_time" format="MM/dd hh:mm:ss"></uni-dateformat>
</view>
<view :class="{reverse:!msg.isAi}">
<view class="userInfo">
<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">
<rich-text v-if="nodes&&nodes.length" space="nbsp" :nodes="nodes"></rich-text>
<!-- #ifdef H5 -->
<view class="copy-box" :style="{left,top}">
<text class="copy" @click="copy">复制</text>
</view>
<!-- #endif -->
</view>
<view v-if="msgIndex == msgIndexList.length-1 && adpid && msg.insufficientScore">
<uni-ad-rewarded-video :adpid="adpid" @onAdClose="onAdClose"></uni-ad-rewarded-video>
</view>
</view>
<uni-icons v-if="msgIndex == msgIndexList.length-1 && !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>
</view>
</view>
</template>
<script>
<script>
import {msgList} from '@/pages/chat/msgList.js';
// 引入markdown-it库
import MarkdownIt from '@/lib/markdown-it.min.js';
......@@ -53,13 +76,17 @@
export default {
name: "msg",
data() {
return {
return {
// 悬浮的复制按钮的左边距
left: "-100px",
// 悬浮的复制按钮的上边距
top: "-100px"
top: "-100px",
msg:{
content:""
},
msgIndexList:0
};
},
},
mounted() {
// #ifdef H5
// web端限制不选中文字时出现系统右键菜单
......@@ -86,24 +113,36 @@
this.left = "-100px"
})
// #endif
},
created() {
this.msg = msgList[this.msgIndex]
},
props: {
// 传入的markdown内容
md: {
type: String,
default () {
return ''
}
},
// // 传入的markdown内容
// md: {
// type: String,
// default () {
// return ''
// }
// },
// 是否显示鼠标闪烁的效果
showCursor: {
type: [Boolean, Number],
default () {
return false
}
},
msgIndex:{
type: Number,
default () {
return false
}
}
},
computed: {
computed: {
md(){
return this.msg.content
},
nodes() {
let htmlString = ''
// 修改转换结果的htmlString值 用于正确给界面增加鼠标闪烁的效果
......@@ -142,8 +181,105 @@
}
</script>
<style lang="scss">
<style lang="scss">
/* #ifndef APP-NVUE */
view,
textarea,
button,
.page {
display: flex;
box-sizing: border-box;
}
/* #endif */
.userInfo {
flex-direction: column;
}
.msg-item {
position: relative;
width: 750rpx;
flex-direction: column;
padding: 0 15px;
padding-bottom: 15px;
}
.msgStateIcon {
position: relative;
top: 5px;
right: 5px;
align-self: center;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 2px;
}
.create_time {
font-size: 12px;
padding: 5px 0;
padding-top: 0;
color: #aaa;
text-align: center;
width: 750rpx;
/* #ifdef MP */
display: flex;
/* #endif */
justify-content: center;
}
.content {
/* #ifndef APP-NVUE */
max-width: 550rpx;
/* #endif */
background-color: #FFF;
border-radius: 5px;
padding: 12px 10px;
margin-left: 10px;
/* #ifndef APP-NVUE */
word-break: break-all;
user-select: text;
cursor: text;
/* #endif */
}
/* #ifndef APP-NVUE */
.content {
display: inline;
}
.content ::v-deep rich-text {
max-width: 550rpx;
overflow: auto;
}
/* #endif */
/* #ifdef H5 */
.content * {
display: inline;
}
/* #endif */
.reverse {
flex-direction: row-reverse;
}
.reverse .content {
margin-left: 0;
margin-right: 10px;
}
.reverse-align {
align-items: flex-end;
}
/* #ifndef VUE3 && APP-PLUS */
@import "@/components/uni-ai-msg/uni-ai-msg.scss";
/* #endif */
/* #endif */
</style>
\ No newline at end of file
{
"id": "uni-ai-chat",
"name": "uni-ai-chat",
"version": "1.0.11",
"version": "1.0.12",
"description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js",
"scripts": {
......
......@@ -2,30 +2,12 @@
<view class="page">
<view class="container">
<view v-if="isWidescreen" class="header">uni-ai-chat</view>
<text class="noData" v-if="msgList.length === 0">没有对话记录</text>
<text class="noData" v-if="msgLength === 0">没有对话记录</text>
<scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true">
<view v-for="(msg,index) in msgList" class="msg-item" :key="index">
<view class="create_time-box">
<uni-dateformat class="create_time" :date="msg.create_time" format="MM/dd hh:mm:ss"></uni-dateformat>
</view>
<view :class="{reverse:!msg.isAi}">
<view class="userInfo">
<image class="avatar" :src="msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'" mode="widthFix"></image>
</view>
<view class="content">
<!-- <text class="copy" @click="copy">复制</text> -->
<uni-ai-msg :md="msg.content" :show-cursor="index == msgList.length-1 && msg.isAi && sseIndex"></uni-ai-msg>
<view v-if="index == msgList.length-1 && adpid && msg.insufficientScore">
<uni-ad-rewarded-video :adpid="adpid" @onAdClose="onAdClose"></uni-ad-rewarded-video>
</view>
</view>
<uni-icons v-if="index == msgList.length-1 && !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>
</view>
</view>
<view class="tip-ai-ing" v-if="msgList.length && msgList.length%2 !== 0">
<uni-ai-msg ref="msg" v-for="(msgIndex,index) in msgLength" :key="index" :msgIndex="index"
:show-cursor="index == msgLength - 1 && msgLength%2 === 0 && sseIndex"
></uni-ai-msg>
<view class="tip-ai-ing" v-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" text="[流式响应]"></uni-link>
......@@ -46,24 +28,29 @@
<uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons>
</view>
<view class="textarea-box">
<textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen"
@keyup.shift="onKeyup('shift')" @keydown.shift="onKeydown('shift')" @keydown.enter="onKeydown('enter')"
:disabled="inputBoxDisabled" :placeholder="placeholderText" :maxlength="-1" :focus="focus"
<textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen"
@keyup.shift="onKeyup('shift')" @keydown.shift="onKeydown('shift')"
@keydown.enter="onKeydown('enter')" :disabled="inputBoxDisabled"
:placeholder="placeholderText" :maxlength="-1" :focus="focus"
placeholder-class="input-placeholder"></textarea>
</view>
<view class="send-btn-box">
<text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
<button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send" type="primary">发送</button>
<button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send"
type="primary">发送</button>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
// 引入配置文件
import config from '@/config.js';
import config from '@/config.js';
import {msgList} from '@/pages/chat/msgList.js';
// 获取广告id
const {
adpid
......@@ -72,15 +59,17 @@
let sseChannel = false;
// 是否通过回调,当用户点击清空后应当跳过前一次请求的回调
let skip_callback = false;
// 键盘的shift键是否被按下
let skip_callback = false;
// 键盘的shift键是否被按下
let shiftKeyPressed = false
export default {
export default {
data() {
return {
// 使聊天窗口滚动到指定元素id的值
scrollIntoView: "",
// 消息长度(个数)
msgLength:0,
// 消息列表数据
msgList: [],
// 输入框的消息内容
......@@ -92,10 +81,10 @@
// 当前屏幕是否为宽屏
isWidescreen: false,
// 广告位id
adpid,
focus:false
adpid,
focus: false
}
},
},
computed: {
// 输入框是否禁用
inputBoxDisabled() {
......@@ -104,7 +93,7 @@
return true
}
// 如果消息列表长度为奇数,则禁用输入框
return !!(this.msgList.length && this.msgList.length % 2 !== 0)
return !!(this.msgLength && this.msgLength % 2 !== 0)
},
// 输入框占位符文本
placeholderText() {
......@@ -125,17 +114,26 @@
}
},
// 监听msgList变化,将其存储到本地缓存中
watch: {
// #ifdef H5
inputBoxDisabled(val){
this.$nextTick(()=>{
this.focus = !val
console.log('this.focus',this.focus);
})
},
// #endif
watch: {
// #ifdef H5
inputBoxDisabled(val) {
this.$nextTick(() => {
this.focus = !val
// console.log('this.focus', this.focus);
})
},
// #endif
msgList: {
handler(msgList) {
handler(msgList) {
let msgLength = msgList.length
if(msgLength != this.msgLength){
this.msgLength = msgLength
this.$nextTick(()=>{
this.updateLastMsg(msgList[msgLength - 1])
})
}
// 将msgList存储到本地缓存中
uni.setStorage({
"key":"uni-ai-msg",
......@@ -146,7 +144,8 @@
deep: true
}
},
async mounted() {
async mounted() {
// 如果存在广告位id且用户token未过期
if (this.adpid && uniCloud.getCurrentUserInfo().tokenExpired > Date.now()) {
// 查询当前用户的积分
......@@ -165,7 +164,7 @@
.get()
// 输出当前用户积分
console.log('当前用户有多少积分:', res.data[0] && res.data[0].score);
}
}
// for (let i = 0; i < 15; i++) {
// this.msgList.push({
......@@ -173,9 +172,14 @@
// content: "1-" + i
// })
// }
this.msgList = uni.getStorageSync('uni-ai-msg') || []
let _msgList = uni.getStorageSync('uni-ai-msg') || [];
if(_msgList.length){
msgList.push(..._msgList)
}
this.msgList = msgList
// 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。
let length = this.msgList.length
......@@ -202,59 +206,59 @@
}, matches => {
this.isWidescreen = matches;
})
// #endif
// 兼容 Vue3下textarea不支持@keydown
// #ifdef H5 && VUE3
//获得消息输入框对象
let adjunctKeydown = false
const textareaDom = document.querySelector('.textarea-box textarea');
if (textareaDom) {
//键盘按下时
textareaDom.onkeydown = e => {
// console.log('onkeydown', e.keyCode)
if ([16, 17, 18, 93].includes(e.keyCode)) {
//按下了shift ctrl alt windows键
adjunctKeydown = true;
}
if (e.keyCode == 13 && !adjunctKeydown) {
// 延迟兼容 v-model的时机小于onkeydown的问题
this.content = textareaDom.value
// 执行发送
this.beforeSendMsg();
}
};
textareaDom.onkeyup = e => {
//松开adjunct键
if ([16, 17, 18, 93].includes(e.keyCode)) {
adjunctKeydown = false;
}
};
}
// #endif
// #endif
// 兼容 Vue3下textarea不支持@keydown
// #ifdef H5 && VUE3
//获得消息输入框对象
let adjunctKeydown = false
const textareaDom = document.querySelector('.textarea-box textarea');
if (textareaDom) {
//键盘按下时
textareaDom.onkeydown = e => {
// console.log('onkeydown', e.keyCode)
if ([16, 17, 18, 93].includes(e.keyCode)) {
//按下了shift ctrl alt windows键
adjunctKeydown = true;
}
if (e.keyCode == 13 && !adjunctKeydown) {
// 延迟兼容 v-model的时机小于onkeydown的问题
this.content = textareaDom.value
// 执行发送
this.beforeSendMsg();
}
};
textareaDom.onkeyup = e => {
//松开adjunct键
if ([16, 17, 18, 93].includes(e.keyCode)) {
adjunctKeydown = false;
}
};
}
// #endif
},
methods: {
// #ifdef H5 && VUE2
onKeydown(keyname){
if(keyname == 'shift'){
//按下了shift键
shiftKeyPressed = true;
}
// 按下了回车 且 之前没按下 shift
if (keyname == 'enter' && !shiftKeyPressed) {
this.$nextTick(()=>{
this.beforeSendMsg();
})
}
},
onKeyup(keyname){
if(keyname == 'shift'){
//按下了shift键
shiftKeyPressed = false;
}
},
// #endif
methods: {
// #ifdef H5 && VUE2
onKeydown(keyname) {
if (keyname == 'shift') {
//按下了shift键
shiftKeyPressed = true;
}
// 按下了回车 且 之前没按下 shift
if (keyname == 'enter' && !shiftKeyPressed) {
this.$nextTick(() => {
this.beforeSendMsg();
})
}
},
onKeyup(keyname) {
if (keyname == 'shift') {
//按下了shift键
shiftKeyPressed = false;
}
},
// #endif
// 此(惰性)函数,检查是否开通uni-push;决定是否启用enableStream
async checkIsOpenPush() {
try {
......@@ -269,7 +273,7 @@
},
// 更新最后一条消息
updateLastMsg(param) {
let length = this.msgList.length
let length = this.msgLength
if (length === 0) {
return
}
......@@ -288,7 +292,7 @@
lastMsg = Object.assign(lastMsg, data)
}
}
this.msgList.splice(length - 1, 1, lastMsg)
this.msgList.splice(length - 1, 1, lastMsg)
},
// 广告关闭事件
onAdClose(e) {
......@@ -420,13 +424,13 @@
state: 0,
// 消息创建时间
create_time: Date.now()
})
})
// 展示最后一条消息
this.showLastMsg()
// dom加载完成后 清空文本内容
// dom加载完成后 清空文本内容
this.$nextTick(() => {
this.content = ''
this.content = ''
})
this.send() // 发送消息
},
......@@ -476,7 +480,7 @@
// console.log('sseChannel',sseChannel);
// 监听message事件
sseChannel.on('message',(message) => {
sseChannel.on('message', (message) => {
// console.log('on message', message);
// 将从云端接收到的消息添加到消息列表中
......@@ -505,20 +509,20 @@
if (e) {
// 更新最后一条消息
this.updateLastMsg(lastMsg => {
// 如果e包含illegal属性
if (e.illegal) {
// 将最后一条消息的illegal属性更新为e的illegal属性
lastMsg.illegal = e.illegal
lastMsg.content = "内容涉及敏感"
// 倒数第二条(用户发问内容)也需要设置illegal的值
this.msgList[this.msgList.length - 2].illegal = e.illegal
}
// 如果e包含illegal属性
if (e.illegal) {
// 将最后一条消息的illegal属性更新为e的illegal属性
lastMsg.illegal = e.illegal
lastMsg.content = "内容涉及敏感"
// 倒数第二条(用户发问内容)也需要设置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属性
}
// 如果e包含insufficientScore属性
else if (e.insufficientScore) {
// 将最后一条消息的insufficientScore属性更新为e的insufficientScore属性
lastMsg.insufficientScore
......@@ -548,51 +552,63 @@
})
.then(res => {
// console.log(111,res);
if (!sseChannel && res.data) {
// 更新最后一条消息的状态为100(发送成功)
this.updateLastMsg({
state: 100
})
if (!sseChannel && res.data) {
// 更新最后一条消息的状态为100(发送成功)
this.updateLastMsg({
state: 100
})
// console.log(res, res.reply);
// 判断是否要跳过本次回调,防止请求未返回时,历史对话已被清空。引起对话顺序错误 导致 对话输入框卡住
if (!skip_callback) {
let {"reply": content,summarize,insufficientScore,illegal} = res.data
let {
"reply": content,
summarize,
insufficientScore,
illegal
} = res.data
if (illegal) {
// 如果返回的数据包含illegal属性,就更新最后一条消息的illegal属性为true
this.updateLastMsg({illegal: true})
this.updateLastMsg({
illegal: true
})
}
// 将从云端接收到的消息添加到消息列表中
this.msgList.push({
// 添加消息创建时间
create_time: Date.now(),
// 标记消息为来自AI机器人
isAi: true,
// 添加消息内容
content,
// 添加消息总结
summarize,
// 添加消息分数不足标记
insufficientScore,
// 添加消息涉敏标记
illegal
// 添加消息创建时间
create_time: Date.now(),
// 标记消息为来自AI机器人
isAi: true,
// 添加消息内容
content,
// 添加消息总结
summarize,
// 添加消息分数不足标记
insufficientScore,
// 添加消息涉敏标记
illegal
})
// 滚动窗口以显示最新的一条消息
this.$nextTick(()=>{
this.showLastMsg()
this.$nextTick(() => {
this.showLastMsg()
})
} else {
console.log('用户点击了清空按钮,跳过前一次请求的回调。内容:', res.data.reply);
}
}else{
// 处理 sseChannel没结束 云函数提前结束的情况
sseChannel.close()
this.sseIndex = 0
}
})
.catch(e => {
console.log(e);
console.log(e);
// 获取消息列表长度
let l = this.msgList.length
let l = this.msgList.length
// console.log(l,this.msgList[l-1]);
// 如果最后一条消息的来源是人工智能机器人 就将流式响应计数值设置为0
if (l && sseChannel && this.msgList[l - 1].isAi) {
sseChannel.close()
this.sseIndex = 0
}
......@@ -609,14 +625,14 @@
},
// 滚动窗口以显示最新的一条消息
showLastMsg() {
// 等待DOM更新
this.$nextTick(() => {
// 将scrollIntoView属性设置为"last-msg-item",以便滚动窗口到最后一条消息
this.scrollIntoView = "last-msg-item"
// 等待DOM更新,即:滚动完成
this.$nextTick(() => {
// 将scrollIntoView属性设置为空,以便下次设置滚动条位置可被监听
this.scrollIntoView = ""
// 等待DOM更新
this.$nextTick(() => {
// 将scrollIntoView属性设置为"last-msg-item",以便滚动窗口到最后一条消息
this.scrollIntoView = "last-msg-item"
// 等待DOM更新,即:滚动完成
this.$nextTick(() => {
// 将scrollIntoView属性设置为空,以便下次设置滚动条位置可被监听
this.scrollIntoView = ""
})
})
},
......@@ -659,7 +675,7 @@
// 将流式响应计数值归零
this.sseIndex = 0
// 将消息列表清空
this.msgList = []
this.msgList.splice(0,this.msgLength);
}
}
});
......@@ -739,7 +755,7 @@
/* #ifndef APP-NVUE */
overflow: auto;
/* #endif */
width: 450rpx;
width: 450rpx;
font-size: 14px;
}
......@@ -748,10 +764,11 @@
.textarea-box .textarea::-webkit-scrollbar {
width: 0;
}
/* #endif */
.input-placeholder {
color: #bbb;
color: #bbb;
line-height: 18px;
}
......@@ -783,7 +800,6 @@
.send::after {
display: none;
}
/* #endif */
......@@ -792,92 +808,6 @@
height: 1px;
width: 750rpx;
}
.userInfo {
flex-direction: column;
}
.msg-item {
position: relative;
width: 750rpx;
flex-direction: column;
padding: 0 15px;
padding-bottom: 15px;
}
.msgStateIcon {
position: relative;
top: 5px;
right: 5px;
align-self: center;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 2px;
}
.create_time {
font-size: 12px;
padding: 5px 0;
padding-top: 0;
color: #aaa;
text-align: center;
width: 750rpx;
/* #ifdef MP */
display: flex;
/* #endif */
justify-content: center;
}
.content {
/* #ifndef APP-NVUE */
max-width: 550rpx;
/* #endif */
background-color: #FFF;
border-radius: 5px;
padding: 12px 10px;
margin-left: 10px;
/* #ifndef APP-NVUE */
word-break: break-all;
user-select: text;
cursor: text;
/* #endif */
}
/* #ifndef APP-NVUE */
.content {
display: inline;
}
.content ::v-deep rich-text {
max-width: 550rpx;
overflow: auto;
}
/* #endif */
/* #ifdef H5 */
.content * {
display: inline;
}
/* #endif */
.reverse {
flex-direction: row-reverse;
}
.reverse .content {
margin-left: 0;
margin-right: 10px;
}
.reverse-align {
align-items: flex-end;
}
.noData {
margin-top: 15px;
text-align: center;
......@@ -886,7 +816,6 @@
font-size: 12px;
justify-content: center;
}
.tip-ai-ing {
align-items: center;
flex-direction: column;
......
export const msgList = []
......@@ -11,6 +11,7 @@
"uni-cloud-ai": {}
},
"cloudfunction-config": {
"timeout": 60
"timeout": 60,
"runtime":"Nodejs12"
}
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册