xunfeiAI/pages/index/index.vue

618 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="wrapper">
<view class="tips color_fff size_12 align_c" :class="{ 'show':ajax.loading }" @tap="getHistoryMsg">{{ajax.loadText}}
</view>
<view class="placeholder"></view>
<view class="box-1" id="list-box" ref="box">
<view class="talk-list">
<view v-for="(item,index) in talkList" :key="index" :id="`msg-${item.id}`">
<view class="item flex_col" :class=" item.type == 1 ? 'push':'pull' ">
<image :src="item.pic" mode="aspectFill" class="pic"></image>
<view v-if="talkList.length-1==index" class="content multiline-text">
<!-- <rich-text :nodes="item.content"></rich-text> -->
<bing-math :key="`math-${item.id}`" class="bing-math" :latex="c_content"></bing-math>
</view>
<view v-else class="content multiline-text">
<!-- <rich-text :nodes="item.content"></rich-text> -->
<bing-math :key="`math-${item.id}`" class="bing-math" :latex="item.content"></bing-math>
</view>
</view>
</view>
</view>
</view>
<!-- <view v-show="showplc" :style="{'min-height': (keyboardHeight+200)+'px'}" class="placeholder">显示</view> -->
<view class="box-2">
<view class="flex_col">
<view class="flex_grow">
<input type="text" class="content" v-model="content" placeholder="请输入聊天内容" @focus="focus" @confirm="send"
placeholder-style="color:#DDD;" :cursor-spacing="6">
</view>
<button class="send" @tap="send">发送</button>
</view>
</view>
</view>
</template>
<script>
import * as base64 from "base-64"
import CryptoJS from '../../static/crypto-js/crypto-js.js'
import parser from '../../static/fast-xml-parser/src/parser'
import * as utf8 from "utf8"
import fetch from 'miniprogram-fetch';
import axios from 'axios';
import BingMath from "@/components/bing-math/bing-math.vue"
export default {
components: {
'bing-math': BingMath
},
data() {
return {
talkList: [],
ajax: {
rows: 20, //每页数量
page: 1, //页码
flag: false, // 请求开关
loading: false, // 加载中
loadText: '正在获取消息'
},
keyboardHeight: 0,
showplc: true,
content: '',
c_content: '',
n_content: '',
timer: '',
TEXT: '你好,我的名字叫大',
APPID: '2eda6c2e', // 控制台获取填写
APIKey: '12ec1f9d113932575fc4b114a2f60ffd',
APISecret: 'MDEyMzE5YTc5YmQ5NjMwOTU1MWY4N2Y2',
sparkResult: '',
historyTextList: [], // 历史会话信息由于最大token12000,可以结合实际使用,进行移出
tempRes: '', // 临时答复保存
}
},
mounted() {
this.$nextTick(() => {
this.getHistoryMsg();
});
uni.onKeyboardHeightChange(e => {
let h = this.keyboardHeight;
this.keyboardHeight = e.height;
if(e.height==0)h *= -1;
else h = e.height;
uni.createSelectorQuery().selectViewport().scrollOffset(function(res) {
const scrollTop = res.scrollTop; // 页面滚动距离
uni.pageScrollTo({
scrollTop: scrollTop + h, // 当前位置向下滚动
duration: 0 // 滚动过渡时间为300ms默认值为300ms
});
}).exec();
})
},
beforeDestroy() {
// #ifdef APP-PLUS
uni.offKeyboardHeightChange();
// #endif
},
onPageScroll(e) {
if (e.scrollTop < 5) {
this.getHistoryMsg();
}
},
watch: {
n_content(n, o) {
// this.c_content = n;
if (this.timer) clearInterval(this.timer);
let cl = this.c_content.length;
let nc = this.n_content.split('')
this.timer = setInterval(() => {
if (cl < nc.length) {
this.c_content += nc[cl];
cl++;
if (cl % 6 == 0) this.$nextTick(()=>{
uni.pageScrollTo({
scrollTop: 999999,
})
// uni.createSelectorQuery()
// .in(this)
// .select('.box-1')
// .boundingClientRect((rect) => {
// const height = rect?.height;
// console.log(height, rect);
// })
// .exec();
})
} else {
clearInterval(this.timer);
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 9999999,
})
})
}
}, 60)
}
},
methods: {
copyText(str) {
uni.setClipboardData({
data: str,
success: function() {
uni.showToast({
icon: 'none',
title: '复制成功'
});
}
});
},
// 获取历史消息
getHistoryMsg() {
if (!this.ajax.flag) {
return; //
}
// 此处用到 ES7 的 async/await 知识,为使代码更加优美。不懂的请自行学习。
let get = async () => {
this.hideLoadTips();
this.ajax.flag = false;
let data = await this.joinHistoryMsg();
console.log('----- 模拟数据格式,供参考 -----');
console.log(data); // 查看请求返回的数据结构
// 获取待滚动元素选择器,解决插入数据后,滚动条定位时使用
let selector = '';
if (this.ajax.page > 1) {
// 非第一页,则取历史消息数据的第一条信息元素
selector = `#msg-${this.talkList[0].id}`;
} else {
// 第一页,则取当前消息数据的最后一条信息元素
selector = `#msg-${data[data.length-1].id}`;
}
// 将获取到的消息数据合并到消息数组中
this.talkList = [...data, ...this.talkList];
// 数据挂载后执行,不懂的请自行阅读 Vue.js 文档对 Vue.nextTick 函数说明。
this.$nextTick(() => {
// 设置当前滚动的位置
this.setPageScrollTo(selector);
this.hideLoadTips(true);
if (data.length < this.ajax.rows) {
// 当前消息数据条数小于请求要求条数时,则无更多消息,不再允许请求。
// 可在此处编写无更多消息数据时的逻辑
} else {
this.ajax.page++;
// 延迟 200ms ,以保证设置窗口滚动已完成
setTimeout(() => {
this.ajax.flag = true;
}, 200)
}
})
}
get();
},
// 拼接历史记录消息,正式项目可替换为请求历史记录接口
joinHistoryMsg() {
let join = () => {
let arr = [];
//通过当前页码及页数,模拟数据内容
let startIndex = (this.ajax.page - 1) * this.ajax.rows;
let endIndex = startIndex + this.ajax.rows;
for (let i = startIndex; i < endIndex; i++) {
arr.push({
"id": i, // 消息的ID
"content": `这是历史记录的第${i+1}条消息`, // 消息内容
"type": Math.random() > 0.5 ? 1 : 0, // 此为消息类别,设 1 为发出去的消息0 为收到对方的消息,
"pic": "/static/avatar.png" // 头像
})
}
/*
颠倒数组中元素的顺序。将最新的数据排在本次接口返回数据的最后面。
后端接口按 消息的时间降序查找出当前页的数据后,再将本页数据按消息时间降序排序返回。
这是数据的重点,因为页面滚动条和上拉加载历史的问题。
*/
arr.reverse();
return arr;
}
// 此处用到 ES6 的 Promise 知识,不懂的请自行学习。
return new Promise((done, fail) => {
// 无数据请求接口,由 setTimeout 模拟,正式项目替换为 ajax 即可。
setTimeout(() => {
let data = join();
done(data);
}, 1500);
})
},
focus(){
uni.pageScrollTo({
top: this.keyboardHeight
})
},
// 设置页面滚动位置
setPageScrollTo(selector) {
let view = uni.createSelectorQuery().in(this).select(selector);
view.boundingClientRect((res) => {
uni.pageScrollTo({
scrollTop: res.top - 30, // -30 为多显示出大半个消息的高度,示意上面还有信息。
duration: 0
});
}).exec();
},
// 隐藏加载提示
hideLoadTips(flag) {
if (flag) {
this.ajax.loadText = '消息获取成功';
setTimeout(() => {
this.ajax.loading = false;
}, 300);
} else {
this.ajax.loading = true;
this.ajax.loadText = '正在获取消息';
}
},
// 发送信息
send() {
if (!this.content) {
uni.showToast({
title: '请输入有效的内容',
icon: 'none'
})
return;
}
// 将当前发送信息 添加到消息列表。
let data = {
"id": new Date().getTime(),
"content": this.content,
"type": 1,
"pic": "/static/avatar.png"
}
this.TEXT = this.content;
this.n_content = '';
this.c_content = '';
this.talkList.push(data);
this.talkList.push({
"id": new Date().getTime(),
"content": '',
"type": 2,
"pic": "/static/avatar.png"
});
this.$nextTick(() => {
// 清空内容框中的内容
this.content = '';
uni.pageScrollTo({
scrollTop: 999999, // 设置一个超大值,以保证滚动条滚动到底部
duration: 0
});
})
this.sendToSpark();
},
async sendToSpark() {
let myUrl = await this.getWebSocketUrl();
this.tempRes = "";
// this.sparkResult = "";
let realThis = this;
this.socketTask = uni.connectSocket({
//url: encodeURI(encodeURI(myUrl).replace(/\+/g, '%2B')),
url: myUrl,
method: 'GET',
success: res => {
console.log(res, "ws成功连接...", myUrl)
realThis.wsLiveFlag = true;
}
})
realThis.socketTask.onError((res) => {
console.log("连接发生错误请检查appid是否填写", res)
})
realThis.socketTask.onOpen((res) => {
this.historyTextList.push({
"role": "user",
"content": this.TEXT
})
console.info("wss的onOpen成功执行...", res)
// 第一帧..........................................
console.log('open成功...')
let params = {
"header": {
"app_id": this.APPID,
"uid": "aef9f963-7"
},
"parameter": {
"chat": {
"domain": "generalv2",
"temperature": 0.5,
"max_tokens": 1024
}
},
"payload": {
"message": {
"text": this.historyTextList
}
}
};
console.log("请求的params" + JSON.stringify(params))
this.sparkResult = this.sparkResult + "\r\n我" + this.TEXT + "\r\n"
this.sparkResult = this.sparkResult + "大模型:"
console.log("发送第一帧...", params)
realThis.socketTask.send({ // 发送消息都用uni的官方版本
data: JSON.stringify(params),
success() {
console.log('第一帧发送成功')
}
});
});
// 接受到消息时
realThis.socketTask.onMessage((res) => {
console.log('收到API返回的内容', res.data);
let obj = JSON.parse(res.data)
// console.log("我打印的"+obj.payload);
let dataArray = obj.payload.choices.text;
for (let i = 0; i < dataArray.length; i++) {
realThis.sparkResult = realThis.sparkResult + dataArray[i].content;
this.talkList[this.talkList.length - 1].content += dataArray[i].content;
this.n_content = this.talkList[this.talkList.length - 1].content;
realThis.tempRes = realThis.tempRes + dataArray[i].content
}
// realThis.sparkResult =realThis.sparkResult+
let temp = JSON.parse(res.data)
// console.log("0726",temp.header.code)
if (temp.header.code !== 0) {
console.log(`${temp.header.code}:${temp.message}`);
realThis.socketTask.close({
success(res) {
console.log('关闭成功', res)
realThis.wsLiveFlag = false;
},
fail(err) {
console.log('关闭失败', err)
}
})
}
if (temp.header.code === 0) {
if (res.data && temp.header.status === 2) {
realThis.sparkResult = realThis.sparkResult +
"\r\n**********************************************"
this.historyTextList.push({
"role": "assistant",
"content": this.tempRes
})
/* let dataArray= obj.payload.choices.text;
for(let i=0;i<dataArray.length;i++){
realThis.sparkResult =realThis.sparkResult+ dataArray[i].content
} */
setTimeout(() => {
realThis.socketTask.close({
success(res) {
console.log('关闭成功', res)
},
fail(err) {
// console.log('关闭失败', err)
}
})
}, 1000)
}
}
})
},
// 鉴权
getWebSocketUrl() {
return new Promise((resolve, reject) => {
// https://spark-api.xf-yun.com/v1.1/chat V1.5 domain general
// https://spark-api.xf-yun.com/v2.1/chat V2.0 domain generalv2
var url = "wss://spark-api.xf-yun.com/v2.1/chat";
var host = "spark-api.xf-yun.com";
var apiKeyName = "api_key";
var date = new Date().toGMTString();
var algorithm = "hmac-sha256";
var headers = "host date request-line";
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2.1/chat HTTP/1.1`;
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, this.APISecret);
var signature = CryptoJS.enc.Base64.stringify(signatureSha);
var authorizationOrigin =
`${apiKeyName}="${this.APIKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
var authorization = base64.encode(authorizationOrigin);
url = `${url}?authorization=${authorization}&date=${encodeURI(date)}&host=${host}`;
// console.log(url)
resolve(url);
});
},
}
}
</script>
<style lang="scss">
@import "../../lib/global.scss";
page {
background-color: #f5f5f5;
font-size: 28rpx;
}
.wrapper {
height: auto !important;
}
/* 加载数据提示 */
.tips {
position: fixed;
left: 0;
top: var(--window-top);
width: 100%;
z-index: 9;
background-color: rgba(0, 0, 0, 0.15);
height: 72rpx;
line-height: 72rpx;
transform: translateY(-80rpx);
transition: transform 0.3s ease-in-out 0s;
&.show {
transform: translateY(0);
}
}
.box-1 {
width: 100%;
height: auto;
padding-bottom: 100rpx;
box-sizing: content-box;
/* 兼容iPhoneX */
margin-bottom: 0;
margin-bottom: constant(safe-area-inset-bottom);
margin-bottom: env(safe-area-inset-bottom);
}
.multiline-text {
white-space: pre-line;
/* white-space: pre-wrap; */
}
.box-2 {
position: fixed;
left: 0;
width: 100%;
bottom: 0;
height: auto;
z-index: 2;
border-top: #e5e5e5 solid 1px;
box-sizing: content-box;
background-color: #f5f5f5;
transform: translateY(0); /* 初始化 transform 属性 */
transition: transform 0.3s ease; /* 添加过渡效果 */
/* 兼容iPhoneX */
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
>view {
padding: 0 20rpx;
height: 100rpx;
}
.content {
background-color: #fff;
height: 64rpx;
padding: 0 20rpx;
border-radius: 6rpx;
font-size: 28rpx;
}
.send {
background-color: #2573fb;
color: #fff;
height: 64rpx;
margin-left: 20rpx;
border-radius: 6rpx;
padding: 0;
width: 120rpx;
line-height: 62rpx;
&:active {
background-color: #1573fb;
}
}
}
.talk-list {
padding-bottom: 20rpx;
/* 消息项,基础类 */
.item {
padding: 20rpx 20rpx 0 20rpx;
align-items: flex-start;
align-content: flex-start;
color: #333;
.pic {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
border: #fff solid 1px;
}
.content {
padding: 20rpx;
border-radius: 4px;
max-width: 500rpx;
word-break: break-all;
line-height: 52rpx;
position: relative;
}
/* 收到的消息 */
&.pull {
.content {
min-width: 20rpx;
min-height: 52rpx;
margin-left: 32rpx;
background-color: #fff;
&::after {
content: '';
display: block;
width: 0;
height: 0;
border-top: 16rpx solid transparent;
border-bottom: 16rpx solid transparent;
border-right: 20rpx solid #fff;
position: absolute;
top: 30rpx;
left: -18rpx;
}
}
}
/* 发出的消息 */
&.push {
/* 主轴为水平方向起点在右端。使不修改DOM结构也能改变元素排列顺序 */
flex-direction: row-reverse;
.content {
min-width: 20rpx;
min-height: 52rpx;
margin-right: 32rpx;
background-color: #2573fb;
color: #fff;
&::after {
content: '';
display: block;
width: 0;
height: 0;
border-top: 16rpx solid transparent;
border-bottom: 16rpx solid transparent;
border-left: 20rpx solid #2573fb;
position: absolute;
top: 30rpx;
right: -18rpx;
}
}
}
}
}
.bing-math {
margin: 0 !important;
padding: 0 !important;
}
.placeholder {
width: 100vw;
background-color: #1573fb;
// background-color: transparent;
// transform: translateY(0); /* 初始化 transform 属性 */
// transition: transform 0.3s ease; /* 添加过渡效果 */
}
</style>