You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1047 lines
24 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="content">
<u-picker
:show="show"
:columns="gptModeColumns"
@confirm="confirm"
@cancel="show = false"
></u-picker>
<view class="top-bar">
<image
@click="changeMode"
class="version"
:src="
gptMode == 'gpt-3.5-turbo'
? '../../static/image/3.5.png'
: gptMode == 'gpt-4-1106-preview'
? '../../static/image/4.0.png'
: '../../static/image/V.png'
"
/>
<view class="title">FONCHAT</view>
<view @click="reLoad">
<u-icon
size="30"
style="margin-bottom: 25rpx; margin-right: 40rpx"
name="reload"
></u-icon>
</view>
</view>
<view class="bottom">
<div v-show="!acqStatus">
<div class="line"></div>
</div>
<view class="chat-content" id="chat-content" ref="chatContent">
<view
class="chat-wrapper"
v-for="(item, index) in chatList"
:key="index"
>
<view class="chat-friend" v-if="item.uid !== 'admin'">
<span style="color: #175abd"
>智能助手<span
style="font-size: 20rpx"
v-show="!acqStatus && index === chatList.length - 1"
>(正在思索……)</span
></span
>
<image
style="
height: 40px;
width: 40px;
position: absolute;
left: -48px;
top: 5px;
"
src="../../static/image/avator.png"
/>
<view class="chat-text" v-if="item.chatType == 0">
<view v-if="item.msg" class="chat-word">{{ item.msg }}</view>
</view>
<view class="chat-image" v-if="item.chatType == 1">
<image
:src="item.msg"
alt="表情"
v-if="item.extend.imageType == 1"
style="width: 100px; height: 100px"
/>
<u-album style="border-radius: 10px" :urls="[item.msg]" v-else>
</u-album>
</view>
</view>
<view class="chat-me" v-else>
<image
:src="item.headimage"
alt=""
style="
height: 40px;
width: 40px;
position: absolute;
right: -48px;
top: 5px;
"
/>
<view class="chat-text" v-if="item.chatType == 0">
<span style="font-size: 16px">
<!-- <div v-for="url in item.fileList">
<image style="height: 100rpx; width: 100rpx" :src="url.url" />
</div> -->
<u-album
v-if="item.fileList.length > 0"
style="height: 100rpx; width: 100rpx"
:urls="[item.fileList[0].url]"
>
</u-album>
{{ item.msg }}</span
>
</view>
<view class="chat-image" v-if="item.chatType == 1">
<image
:src="item.msg"
alt="表情"
v-if="item.extend.imageType == 1"
style="width: 100px; height: 100px"
/>
<u-album
style="max-width: 300px; border-radius: 10px"
:urls="[item.msg]"
v-else
>
</u-album>
</view>
</view>
</view>
</view>
</view>
<!-- 下面是两个是占位元素 -->
<view style="height: 12vh"></view>
<!-- <view class="text-area"> </view> -->
<view class="text-area" style="position: fixed">
<!-- <u-icon
v-show="isText"
@click="changeMic"
size="30"
style="margin-bottom: 30rpx; margin-right: 30rpx"
name="mic"
></u-icon>
<u-icon
v-show="!isText"
@click="changeMic"
size="30"
style="margin-bottom: 30rpx; margin-right: 30rpx"
name="chat-fill"
></u-icon>
<view class="toolBox" v-if="!isText">
<view
class="recorder"
:class="{ active: isUseRecorder }"
@touchstart.prevent="startRecorder"
@touchend.prevent="endRecorder"
>
{{ isUseRecorder ? "松开 结束" : "按住 说话" }}
</view>
</view> -->
<!-- <view class="toolBg"></view> -->
<u--input
v-if="isText"
style="
width: 500rpx;
height: 60rpx;
border-radius: 40rpx;
background-color: #d8d8d8;
margin-right: 20rpx;
"
:disabled="!acqStatus"
@confirm="sendText"
v-model="inputMsg"
border="none"
placeholder="请输入内容"
></u--input>
<u-button
v-if="isText"
style="
border-radius: 40rpx;
width: 140rpx;
height: 64rpx;
background-color: #175abd;
"
@click="sendText"
type="primary"
text="发送"
></u-button>
<u-upload
v-show="gptMode === 'gpt-4-vision-preview'"
:fileList="fileList"
name="6"
@delete="deleteFile"
accept="image"
multiple
@clickPreview="clickPreview"
@afterRead="upLoaded"
:maxCount="1"
>
<u-icon
size="30"
style="margin-bottom: 30rpx; margin-left: 30rpx"
name="plus-circle-fill"
></u-icon>
</u-upload>
<view v-if="!isText" style="width: 50rpx"></view>
</view>
<!-- <mumu-recorder
ref="recorderRef"
@success="handlerSuccess"
@error="handlerError"
></mumu-recorder> -->
</view>
</template>
<script>
import api from "@/http/";
import moment from "moment";
import {
OPENAI_API_KEY,
GPT_MODEL,
FREQUENCY_PENALTY,
MAX_TOKENS,
PRESENCE_PENALTY,
TEMPERATURE,
TOP_P,
} from "utils/openAiConfig";
import MumuRecorder from "@/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue";
export default {
components: { MumuRecorder },
data() {
return {
chatList: [],
inputMsg: "",
acqStatus: true,
gptMode: "gpt-3.5-turbo",
fileList: [],
show: false,
gptModeColumns: [["GPT-3", "GPT-4", "GPT-V"]],
audio: null,
isUseRecorder: false,
playItemIndex: -1,
isText: true,
currentAudio: "",
};
},
mounted() {
this.audio = document.createElement("audio");
this.audio.addEventListener("ended", () => {
this.playItemIndex = -1;
this.currentAudio = "";
});
},
onLoad(item) {},
methods: {
// 开始录音
startRecorder() {
// this.$refs.recorderRef.start();
this.isUseRecorder = true;
},
// 结束录音
endRecorder() {
// this.$refs.recorderRef.stop();
this.isUseRecorder = false;
},
// 录音成功
handlerSuccess(res) {
if (res.duration < 1)
return uni.showToast({
title: "语言时间小于1秒",
icon: "error",
});
console.log(res, 666);
},
// 播放报错
handlerError(code) {
switch (code) {
case "101":
uni.showModal({
content: "当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。",
});
break;
case "201":
uni.showModal({
content: "麦克风权限被拒绝,请刷新页面后授权麦克风权限。",
});
break;
default:
uni.showModal({
content: "未知错误,请刷新页面重试",
});
break;
}
},
//发送文字信息
sendText() {
this.$nextTick(() => {
this.acqStatus = false;
});
const dateNow = moment().format("YYYY/M/DD/ HH:mm:ss");
let params = {};
console.log(this.inputMsg);
if (this.inputMsg) {
let chatMsg = {
headimage: "../../static/image/head.jpg",
name: GPT_MODEL,
time: dateNow,
msg: this.inputMsg,
fileList:
this.fileList.length > 0
? JSON.parse(JSON.stringify(this.fileList))
: [],
chatType: 0, //信息类型0文字1图片
uid: "admin", //uid
};
console.log(chatMsg, 888);
this.sendMsg(chatMsg);
//如果是文字模式则进入
(params.model = this.gptMode),
(params.max_tokens = MAX_TOKENS),
(params.temperature = TEMPERATURE),
(params.top_p = TOP_P),
(params.presence_penalty = PRESENCE_PENALTY),
(params.frequency_penalty = FREQUENCY_PENALTY);
let chatBeforResMsg = {
headimage: "",
name: "",
time: moment().format("YYYY/M/DD/ HH:mm:ss"),
msg: "",
chatType: 0, //信息类型0文字1图片
uid: "ai", //uid
};
this.chatCompletion(params, chatBeforResMsg);
this.inputMsg = "";
this.fileList = [];
} else {
this.$nextTick(() => {
this.acqStatus = true;
});
}
},
// 自动滚动到底部
scrollToBottom() {
this.$nextTick(() => {
let chatBox = document.getElementById("chat-content");
chatBox.scrollTop = chatBox.scrollHeight;
});
},
// 切换模式
changeMode(e) {
this.show = true;
},
// 确认模式
confirm(e) {
console.log(e, 666);
this.show = false;
this.gptMode =
e.value[0] == "GPT-3"
? "gpt-3.5-turbo"
: e.value[0] == "GPT-4"
? "gpt-4-1106-preview"
: "gpt-4-vision-preview";
this.acqStatus = true;
this.fileList = [];
// 清空对话
this.chatList = [];
},
// 从接口获取对话结果
async chatCompletion(params, chatBeforResMsg) {
let conversation = this.contextualAssemblyData();
params.messages = conversation.map((item) => {
return {
role: item.speaker === "user" ? "user" : "assistant",
content: item.text,
};
});
params.stream = true;
//新增一个空的消息
this.sendMsg(chatBeforResMsg);
const self = this;
const currentResLocation = this.chatList.length - 1;
// 获取当前环境地址
const baseUrl = "https://erpapi.fontree.cn/";
const token = "";
try {
await fetch(baseUrl + "chat/app-completion", {
method: "POST",
timeout: 10000,
body: JSON.stringify({
...params,
}),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: token,
},
}).then((response) => {
const reader = response.body.getReader();
function readStream(reader) {
console.log(reader, 666);
return reader.read().then(({ done, value }) => {
if (done) {
return;
}
console.log(self.chatList, currentResLocation, 777);
if (!self.chatList[currentResLocation].reminder) {
self.chatList[currentResLocation].reminder = "";
}
let decoded = new TextDecoder().decode(value);
decoded = self.chatList[currentResLocation].reminder + decoded;
let decodedArray = decoded.split("data: ");
decodedArray.forEach((decoded) => {
if (decoded !== "") {
if (decoded.trim() === "[DONE]") {
return;
} else {
const response = JSON.parse(decoded).choices[0].delta
.content
? JSON.parse(decoded).choices[0].delta.content
: "";
self.chatList[currentResLocation].msg =
self.chatList[currentResLocation].msg + response;
self.scrollToBottom();
}
}
});
return readStream(reader);
});
}
this.chatList[currentResLocation].msg =
this.chatList[currentResLocation].msg;
readStream(reader);
this.$nextTick(() => {
this.acqStatus = true;
});
});
} catch (error) {
console.error(error);
}
},
//组装上下文数据
contextualAssemblyData() {
const conversation = [];
for (var chat of this.chatList) {
let role = [];
if (chat.uid == "admin") {
let my;
if (this.gptMode === "gpt-4-vision-preview") {
if (chat.fileList.length > 0) {
chat.fileList.forEach((item) => {
if (item) {
role.push({
type: "image_url",
image_url: item.url,
});
}
});
}
role.unshift({
type: "text",
text: chat.msg,
});
my = { speaker: "user", text: role };
} else {
my = { speaker: "user", text: chat.msg };
}
conversation.push(my);
} else if (chat.uid == "ai") {
let ai = { speaker: "agent", text: chat.msg };
conversation.push(ai);
}
}
return conversation;
},
// 发送信息
sendMsg(msgList) {
this.chatList.push(msgList);
this.scrollToBottom();
},
// 当监听到向上滚动的时候
scrollToUpper() {
console.log("滚动到顶部");
},
// 上传接口
uploadFilePromise(url) {
console.log(url, 123);
return new Promise((resolve, reject) => {
const Authorization = "";
let a = uni.uploadFile({
url: "https://erpapi.fontree.cn/upload/img",
filePath: url,
name: "file",
formData: {
source: "artwork",
type: "image",
},
header: {
Authorization,
},
success: (res) => {
resolve(JSON.parse(res.data).data.ori_url);
},
});
});
},
// 上传图片
async upLoaded(file, lists, name) {
const item = file.file[0];
this.fileList.push({
...item,
//提示上传中
status: "uploading",
message: "上传中",
});
let result = await this.uploadFilePromise(file.file[0].url);
this.fileList.splice(
0,
1,
Object.assign(item, {
status: "success",
message: "上传成功",
url: result,
})
);
},
// 删除图片
deleteFile(file, detail) {
this.fileList = [];
},
// 刷新
reLoad() {
location.reload();
},
// 切换语音和文字
changeMic() {
this.isText = !this.isText;
},
},
};
</script>
<style lang="scss" scoped>
.uni-body {
height: 100vh;
background-color: #fff;
background-size: auto 100%;
background-attachment: fixed;
}
page {
height: 100vh;
background-color: #fff;
background-size: auto 100%;
background-attachment: fixed;
}
/deep/ .u-input__content {
color: #fff;
padding-left: 40rpx;
}
/deep/ .u-upload {
flex: none !important;
}
/deep/ .u-upload__wrap__preview__image {
width: 90rpx !important;
height: 90rpx !important;
top: -8px;
margin-left: 10px;
}
/deep/ .u-album {
uni-image {
width: 100rpx !important;
height: 100rpx !important;
}
}
.text-area {
// position: fixed;
width: 95%;
bottom: 0;
height: 6vh;
background-size: 100% 100%;
background-color: #f2f2f2;
display: flex;
justify-content: space-between;
padding: 30rpx 40rpx 0rpx 20rpx;
}
.content {
width: 97%;
background-color: #fff;
background-size: auto 100%;
background-attachment: fixed;
height: 100%;
// overflow: hidden;
.top-bar {
width: 100%;
z-index: 100;
position: fixed;
height: 180rpx;
// 背景色渐变
background: linear-gradient(to bottom, #175abd 0%, #25a7f2 100%);
display: flex;
align-items: flex-end;
justify-content: space-between;
.version {
margin-bottom: 25rpx;
width: 100rpx;
height: 56rpx;
margin-left: 30rpx;
}
.title {
margin-bottom: 30rpx;
font-size: 36rpx;
color: #fff;
font-weight: bold;
}
}
.bottom {
width: 100%;
height: 90vh;
background-size: 100% 100%;
// background-color: rgb(50, 54, 68);
border-radius: 20px;
padding: 99px 20px 20px 20px;
box-sizing: border-box;
position: relative;
.chat-content {
width: 100%;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
padding: 20px;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 3px;
/* 设置滚动条宽度 */
}
&::-webkit-scrollbar-thumb {
background-color: #8b67ef;
/* 设置滚动条滑块的背景色 */
}
.chat-friend {
width: 100%;
float: left;
margin-bottom: 20px;
position: relative;
display: flex;
margin-left: 30px;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
.chat-text {
float: left;
max-width: 80%;
padding: 15px;
color: #535353;
border-radius: 20px 20px 20px 5px;
border: 1px solid #d8d8d8;
background-color: #fff;
}
.chat-image {
image {
max-width: 300px;
max-height: 200px;
border-radius: 10px;
}
}
.info-time {
margin: 10px 0;
font-size: 14px;
display: flex;
align-items: center;
justify-content: flex-start;
color: #8b67ef;
image {
width: 30px;
height: 30px;
border-radius: 50%;
vertical-align: middle;
margin-right: 10px;
}
span {
line-height: 30px;
}
span:last-child {
margin-left: 10px;
vertical-align: middle;
}
}
}
.chat-me {
width: 100%;
float: right;
margin-bottom: 20px;
position: relative;
margin-right: 28px;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
.chat-text {
float: right;
max-width: 90%;
padding: 15px;
border-radius: 20px 20px 5px 20px;
background-color: #175abd;
color: #fff;
word-break: break-all;
// 让文字一个一个显示
}
.chat-image {
image {
max-width: 300px;
max-height: 200px;
border-radius: 10px;
}
}
.info-time {
margin: 10px 0;
color: #8b67ef;
font-size: 14px;
align-items: center;
display: flex;
justify-content: flex-end;
image {
width: 30px;
height: 30px;
border-radius: 50%;
vertical-align: middle;
margin-left: 10px;
}
span {
line-height: 30px;
}
span:first-child {
margin-right: 10px;
vertical-align: middle;
}
}
}
}
.chatInputs {
width: 90%;
position: absolute;
bottom: 0;
margin: 3%;
display: flex;
.boxinput {
width: 50px;
height: 50px;
background-color: rgb(50, 54, 68);
border-radius: 15px;
border: 1px solid rgb(80, 85, 103);
box-shadow: 0px 0px 5px 0px rgb(0, 136, 255);
position: relative;
cursor: pointer;
image {
width: 30px;
height: 30px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.emoji {
transition: 0.3s;
width: 50px;
min-width: 50px;
}
.luyin {
color: #fff;
margin-left: 1.5%;
font-size: 30px;
text-align: center;
transition: 0.3s;
width: 50px;
min-width: 50px;
}
.inputs {
width: 95%;
height: 50px;
background-color: rgb(66, 70, 86);
border-radius: 15px;
border: 2px solid rgb(34, 135, 225);
padding: 10px;
box-sizing: border-box;
transition: 0.2s;
font-size: 20px;
color: #fff;
font-weight: 100;
margin: 0 20px;
&:focus {
outline: none;
}
}
}
}
.line {
position: relative;
width: 100%;
margin-left: 2%;
height: 2px;
background: linear-gradient(to right, #175abd 0%, #25a7f2 100%);
animation: shrink-and-expand 2s ease-in-out infinite;
}
.line::before,
.line::after {
content: "";
position: absolute;
top: 0;
width: 50%;
height: 100%;
background: inherit;
}
.line::before {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
left: 0;
transform-origin: left;
animation: shrink-left 2s ease-in-out infinite;
}
.line::after {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
right: 0;
transform-origin: right;
animation: shrink-right 2s ease-in-out infinite;
}
@keyframes shrink-and-expand {
0%,
100% {
transform: scaleX(1);
}
50% {
transform: scaleX(0);
}
}
@keyframes shrink-left {
0%,
50% {
transform: scaleX(1);
}
50.1%,
100% {
transform: scaleX(0);
}
}
@keyframes shrink-right {
0%,
50% {
transform: scaleX(1);
}
50.1%,
100% {
transform: scaleX(0);
}
}
.chat-word {
// 文字缓慢出现
animation: word 0.5s linear;
}
@keyframes word {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
.record {
padding: 20rpx;
font-size: 28rpx;
.item {
display: flex;
justify-content: flex-end;
padding: 10rpx;
margin-bottom: 15rpx;
.content {
margin-right: 20rpx;
position: relative;
background-color: rgba(107, 197, 107, 0.85);
padding: 20rpx 30rpx;
border-radius: 10rpx;
&::before {
position: absolute;
right: -8px;
top: 8px;
content: "";
display: inline-block;
width: 0;
height: 0;
border: 4px solid transparent;
border-left-color: rgba(107, 197, 107, 0.85);
}
image {
max-width: 400rpx;
}
.recorder {
display: flex;
align-items: center;
.time {
margin-right: 40rpx;
}
.icon {
font-weight: bold;
color: #fff;
font-size: 32rpx;
display: flex;
align-items: center;
text {
display: block;
}
& text:nth-of-type(1) {
transform: scale(1.2);
}
& text:nth-of-type(2) {
transform: scale(0.8);
}
& text:nth-of-type(3) {
transform: scale(0.6);
}
}
}
&.active {
.recorder {
.icon {
animation: play 1.5s ease-in-out infinite backwards;
}
}
}
}
.hander {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
}
}
.toolBg {
height: 140rpx;
}
.toolBox {
margin-right: 20rpx;
.recorder {
width: 380rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
padding: 17rpx;
border-radius: 10rpx;
font-size: 28rpx;
box-shadow: 2rpx 3rpx 10rpx rgba(0, 0, 0, 0.2);
position: relative;
&.active {
background-color: #95a5a6;
&::before {
position: absolute;
left: 50%;
transform: translate(-50%);
top: -3px;
content: "";
width: 0;
height: 3px;
background-color: #7bed9f;
animation: loading-data 1.25s ease-in-out infinite backwards;
}
}
}
@keyframes loading-data {
0% {
width: 0;
}
100% {
width: 100%;
}
}
@keyframes play {
0% {
color: #fff;
}
50% {
color: #c3c3c3;
}
100% {
columns: #fff;
}
}
.camrea {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>