123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- <template>
- <view>
- <cu-custom bgColor="bg-cyan" :isBack="$squni.getPrePage() != null">
- <block slot="backText">返回</block>
- <block slot="content">AirSmartChat</block>
- </cu-custom>
- <view class="cu-chat">
- <block v-for="(x,i) in msgList" :key="i">
- <!-- 用户消息 -->
- <view v-if="x.my && x.type === 'msg'" class="cu-item self" :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
- <view class="main">
- <view class="content bg-cyan shadow"><!-- @click="x.msg && $squni.copy(x.msg)" -->
- <text>{{ x.msg }}</text>
- </view>
- </view>
- <image class="cu-avatar round" src="/static/logo-100.png">
- <view v-if="i === 0" class="date">{{ x.date }}</view>
- </view>
- <!-- AI消息 -->
- <view v-if="!x.my && x.type === 'msg'" class="cu-item" :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
- <view class="flex flex-direction align-center">
- <image class="cu-avatar round chat-avatar" src="/static/robot.png">
- <text v-if="i === 0" class="cuIcon-title" :class="[statusColor]"></text>
- </view>
- <view class="main">
- <view class="content shadow"><!-- @click="x.msg && $squni.copy(x.msg)" -->
- <text>{{ x.msg }}</text>
- </view>
- </view>
- <view v-if="i === 0" class="date">{{ x.date }}</view>
- </view>
- <view v-if="x.type === 'error'" class="cu-info">
- <text class="cuIcon-roundclosefill text-red "></text> {{ x.msg }}
- </view>
- </block>
-
- <view v-if="msgLoading" class="cu-item">
- <view class="flex flex-direction align-center">
- <image class="cu-avatar round chat-avatar" src="/static/robot.png">
- </view>
- <view class="main">
- <text class="cuIcon-loading2 cuIconfont-spin text-cyan"></text>
- </view>
- </view>
- </view>
- <view class="cu-bar foot input" :style="[{bottom:inputBottom+'px'}]">
- <view class="tip-box">
- <button class="cu-btn round shadow sm cuIcon line-cyan" @click="startNewChat">
- <text class="cuIcon-paint"></text>
- </button>
- <button class="cu-btn round shadow sm line-cyan" @click="$squni.navigateTo('/pages/main/history')">历史消息</button>
- <button class="cu-btn round shadow sm bg-orange" @click="$squni.navigateTo('/pages/main/prompt/prompt')">提示词</button>
- <button class="cu-btn round shadow sm line-cyan" @click="openBottomFunc">我的信息</button>
- </view>
- <!-- <view class="action func" @click="openBottomFunc">
- <text class="cuIcon-list text-cyan" style="font-size: 60upx;"></text>
- </view> -->
- <input v-model="msg" class="solid padding-lr" :adjust-position="false" :focus="false" maxlength="5000"
- cursor-spacing="10" :placeholder="loading ? '小AirSmart正在思考中,请稍后~' : '你可以问我任何问题'"
- @focus="inputFocus" @blur="inputBlur" @confirm="sendMsg"></input>
- <!-- <view class="action">
- <text class="cuIcon-emojifill text-grey"></text>
- </view> -->
- <button class="cu-btn bg-cyan shadow" :disabled="loading" @click="sendMsg">
- <text class="cuIcon-loading2 cuIconfont-spin" v-if="loading || !ready"></text>{{ !ready ? '连接中' : '发送' }}
- </button>
- </view>
-
- <bottom-func v-if="bottomFuncShow" ref="bottomFunc" :chatAsset="chatAsset" @rewarded-video-ad="() => chatAsset.dfn += 2"></bottom-func>
- </view>
- </template>
- <script>
- import {
- dateFormat, interval
- } from '@/util/squ.js'
- import {
- scrollToBottom
- } from '@/util/squni.js'
- import websocket from '@/util/websocket'
- import {
- sendMsgApi, getUserChatAssetApi, startNewChatApi, findPromptListApi
- } from '@/api/chat.js'
- import BottomFunc from './bottom-func'
- const HELLO_MSG = {
- type: 'msg',
- my: false,
- msg: '连接中,请稍后~',
- date: dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
- }
- export default {
- components: { BottomFunc },
- data() {
- return {
- loading: false,
- userId: null,
- msgList: [HELLO_MSG],
- msgContent: "",
- msg: "",
- inputBottom: 0,
- bottomFuncShow: false,
- chatAsset: {},
- assetType: 'n',
- statusColor: 'text-red',
- statusTimer: null,
- msgLoading: false,
- promptId: null,
- curPrompt: {}
- }
- },
- computed: {
- ready () {
- return this.statusColor === 'text-green'
- }
- },
- watch: {
- loading(n, o) {
- if (n !== o && !n) {
- let last = this.msgList[this.msgList.length - 1]
- if (!last.my) {
- this.addHistory(last)
- }
- }
- },
- statusColor(n, o) {
- if (n === 'text-green') {
- let prompt = ''
- if (this.curPrompt && this.curPrompt.title) {
- prompt = `(提示词:${this.curPrompt.title})`
- }
- HELLO_MSG.msg = '你好,我是AirSmartChat机器人,可以帮你解答疑惑或提供灵感' + prompt
- } else {
- HELLO_MSG.msg = '连接中,请稍后~'
- }
- }
- },
- async onShow() {
- await this.$ready
-
- this.userId = this.$store.getters.userId
- if(!this.userId) {
- this.$squni.toast('请先进行登录哦')
- return
- }
-
- this.promptId = this.$squni.getCurQuery('promptId')
- if(this.promptId) {
- findPromptListApi({ id: this.promptId }).then(res => {
- if (res.status === 'success') {
- this.curPrompt = res.data
-
- if(this.ready && HELLO_MSG.msg.indexOf('提示词') < 0) {
- let prompt = ''
- if (this.curPrompt && this.curPrompt.title) {
- prompt = `(提示词:${this.curPrompt.title})`
- }
- HELLO_MSG.msg += prompt
- }
- }
- })
- }
-
- getUserChatAssetApi().then(res => {
- if (res.status === 'success') {
- this.chatAsset = res.data
- }
- })
-
- this.heartStatus()
- try {
- //建立socket连接
- websocket.connectSocket(this.$config.wssUrl + '/tools/chat/user/' + this.userId, msg => {
- this.recvMsg(msg)
- }, () => {
- //如果连接成功则发送心跳检测
- this.heartBeatTest()
- })
- } catch (error) {
- console.log('websocket connectSocket error:' + error)
- }
- },
- onHide() {
- this.closeSocket()
- },
- methods: {
- logout() {
- this.loginLoading = true
- doLoginApi('H5', {
- username: this.email || this.phone|| this.username,
- password: this.password
- }).then(res => {
- this.loginLoading = false
- if (res === LoginSuccess) {
- uni.showToast({ title: '登录成功' })
- // 跳转到主页发现已经登录,会自动重新获取用户信息,此处无需获取
- window.location.href = getUrlQuery('_originHref')
- } else {
- uni.showToast({ title: (res && res.data && res.data.message) || '登录失败', icon: 'none' })
- this.createCode(4)
- }
- })
- },
- sendMsg() {
- if (this.msg == "") {
- this.$squni.toast('请先输入您的问题哦')
- return
- }
- let msg = this.msg
- this.putMsg(this.msg, true)
- this.msgLoading = true
- this.loading = true
-
- // ======== 开发环境模拟回复 ========
- //return this.mockReply()
- // ======== 开发环境模拟回复 ========
-
- if(this.calcAsset() === false) {
- this.loading = false
- return
- }
-
- // 发送消息: M1/M2
- websocket.sendMessage(JSON.stringify({
- model: 'M1',
- msg: msg,
- platform: this.$squni.getStorageSync('platform'),
- openid: this.$squni.getStorageSync('openid'),
- promptId: this.promptId
- }), null, () => {
- this.putMsgError('机器人被拔网线了,请稍后再试~')
- })
- },
- recvMsg(msg) {
- this.msgLoading = false
- if(!msg) {
- this.putMsgError('机器人开小差了,请稍后再试~')
- return
- }
- // 发送消息
- // 1+1
- // 收到消息
- // {"role":"assistant","content":null}
- // {"role":null,"content":"2"}
- // {"role":null,"content":null}
- // [DONE]
- if (msg === '[DONE]') {
- this.loading = false
- } else {
- try {
- let msgJson = JSON.parse(msg)
- if (msgJson.role === 'sqchat') {
- let content = msgJson.content
- if (msgJson.codeKey) {
- content += `[${msgJson.codeKey}]`
- if(msgJson.codeKey === 'chat.asset_short') {
- this.openBottomFunc()
- } else if(msgJson.codeKey.indexOf('chat.asset_') >= 0) {
- this.chatAsset[this.assetType]++
- }
- }
- this.putMsgError(content)
- } if (msgJson.role === 'assistant') {
- this.putMsg('', false)
- } else if (msgJson.role == null && msgJson.content) {
- this.msgList[this.msgList.length - 1].msg += msgJson.content
- scrollToBottom()
- }
- } catch(error) {
- this.putMsgError(msg)
- }
- }
- },
- startNewChat() {
- HELLO_MSG.date = dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
- this.msgList = [HELLO_MSG]
- startNewChatApi()
- },
- sendMsgBak() {
- let that = this
- if (this.msg == "") {
- return
- }
- this.msgContent += (this.userId + ":" + this.msg + "\n")
- this.putMsg(this.msg, true)
- this.loading = true
- // ======== 开发环境模拟回复 ========
- return this.mockReply()
- // ======== 开发环境模拟回复 ========
- sendMsgApi({
- userId: this.userId + '',
- question: this.msgContent
- }).then(({
- status, data
- }) => {
- if (status === 'success') {
- this.putMsg(data.ack, false)
- this.msgContent += ("openai:" + this.msg + "\n")
- } else {
- this.putMsg(res.message || '机器人开小差了,请稍后再试~', false, 'error')
- }
- that.loading = false
- })
- },
- calcAsset() {
- if (this.chatAsset.dfn > 0) {
- this.chatAsset.dfn--
- this.assetType = 'dfn'
- } else if (this.chatAsset.n > 0) {
- this.chatAsset.n--
- this.assetType = 'n'
- } else {
- this.$squni.toast('剩余次数不足')
- this.openBottomFunc()
- return false
- }
- },
- putMsg (msg, my = false, type = 'msg') {
- let item = {
- type: type,
- msg: msg,
- my: my,
- date: dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
- }
- this.msgList.push(item)
- scrollToBottom()
- if (my) {
- this.addHistory(item)
- // 清除消息
- this.msg = ''
- this.msgReply = ''
- }
- },
- putMsgError(msg) {
- this.putMsg(msg, false, 'error')
- this.msgLoading = false
- this.loading = false
- },
- addHistory(item) {
- if (item.type === 'msg') {
- let chatHistory = this.$squni.getStorageSync('chatHistory')
- if(!chatHistory) {
- chatHistory = []
- }
- if(chatHistory.length >= 50) {
- chatHistory.splice(0, 1)
- }
- chatHistory.push(item)
- this.$squni.setStorageSync('chatHistory', chatHistory)
- }
- },
- //心跳检测
- heartBeatTest() {
- let globalTimer = null
- //清除定时器
- clearInterval(globalTimer)
- //开启定时器定时检测心跳
- globalTimer = setInterval(() => {
- //发送消息给服务端
- websocket.sendMessage('PING', null, () => {
- //如果失败则清除定时器
- clearInterval(globalTimer)
- })
- }, 10000)
- },
- heartStatus() {
- this.statusTimer = interval(() => {
- if (websocket.isOpen) {
- this.statusColor = 'text-green'
- } else if(this.statusColor === 'text-red') {
- this.statusColor = 'text-yellow'
- } else {
- this.statusColor = 'text-red'
- }
- }, this.statusTimer, 200)
- },
- closeSocket() {
- websocket.closeSocket()
- clearInterval(this.statusTimer)
- this.statusColor = 'text-red'
- },
- inputFocus(e) {
- this.inputBottom = e.detail.height
- },
- inputBlur(e) {
- this.inputBottom = 0
- },
- openBottomFunc() {
- this.bottomFuncShow = true
- this.$nextTick(() => {
- this.$refs.bottomFunc.open()
- })
- },
- mockReply() {
- // 开发环境模拟回复
- if (process.env.NODE_ENV === 'development') {
- setTimeout(() => {
- this.putMsg('这是模拟返回消息: ' + new Date(), false)
- this.loading = false
- }, 1000)
- return
- }
- }
- }
- }
- </script>
- <style>
- page {
- padding-bottom: 220upx;
- }
- .cu-chat .chat-avatar.cu-avatar {
- width: 82upx;
- height: 82upx;
- }
- .cu-item:not(.first) {
- padding-bottom: 0upx;
- }
- .cu-item.sec {
- padding-top: 0upx;
- }
- .cu-chat .cu-item>.main {
- max-width: calc(100% - 160upx);
- }
- .main .content {
- word-wrap: break-word;
- cursor: text;
- user-select: text;
- }
- .cu-bar.foot {
- box-shadow: 0 -0.5px 1px rgba(0, 0, 0, 0.1);
- }
- .foot {
- padding-top: 20upx;
- padding-bottom: 60upx;
- }
- .foot .cu-btn {
- margin-right: 20upx;
- }
- .foot .action.func {
- margin-left: 30upx;
- }
- .foot .tip-box {
- position: absolute;
- top: -60upx;
- margin: 0 30upx;
- }
- </style>
|