123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- <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)" -->
- <img v-if="x.pic" :src="x.msg" />
- <text v-else>{{ 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> -->
- <view class="input-msg">
- <ol class="tip-list">
- <li class="tip-item" v-for="item in inputTipList" :key="item.type" @click="handleSendMsg(item)">
- {{ item.name }}
- </li>
- </ol>
- <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="handleInput"></input>
- </view>
- <!-- <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 squni, {
- scrollToBottom
- } from '@/util/squni.js'
- import websocket from '@/util/websocket'
- import {
- sendMsgApi,
- getUserChatAssetApi,
- startNewChatApi,
- findPromptListApi,
- msgList,
- sendMsgPic
- } 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: "",
- msgType: 0,
- inputBottom: 0,
- bottomFuncShow: false,
- chatAsset: {},
- assetType: 'n',
- statusColor: 'text-red',
- statusTimer: null,
- msgLoading: false,
- promptId: null,
- curPrompt: {},
- inputTipList: []
- }
- },
- 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
- }
-
- if(this.msgType === 2){
- sendMsgPic({
- userId: this.$squni.getStorageSync('userId'),
- question: msg,
- type: this.msgType
- }).then(res => {
- if(res.code === 20000) {
- JSON.parse(res.data.ack).map(i => {
- this.putMsg(i.url, false, 'msg', true)
- this.loading = false
- this.msgLoading = false
- this.msgType = 0
- })
- }else{
- this.putMsgError('机器人被拔网线了,请稍后再试~')
- }
- })
- }else{
- // 发送消息: 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', pic = false) {
- let item = {
- type: type,
- msg: msg,
- my: my,
- pic: pic,
- 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
- },
- // 对话输入框输入'/'显示提示信息
- handleInput(e) {
- if (e.detail.value.startsWith('/')) {
- msgList().then(res => {
- this.inputTipList = res.data
- })
- }
- },
- handleSendMsg(e){
- this.msgType = e.type
- this.msg = e.name
- this.inputTipList = []
- },
- 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);
- align-items: flex-end;
- }
- .foot {
- padding-top: 20upx;
- padding-bottom: 60upx;
- }
- .foot .cu-btn {
- margin-right: 20upx;
- width: 200upx;
- }
- .foot .action.func {
- margin-left: 30upx;
- }
- .foot .tip-box {
- position: absolute;
- top: -60upx;
- margin: 0 30upx;
- }
-
- .input-msg{
- width: 100%;
- .tip-list{
- margin: 0 40upx;
- padding: 0 40upx;
-
- .tip-item{
- cursor: pointer;
- color: #909399;
- line-height: 64upx;
- }
-
- .tip-item:hover{
- background: #d9ecff;
- color: #000;
- }
- }
- }
- </style>
|