123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- <template>
- <view class="w100 h100 flex">
- <!-- 侧边栏 -->
- <navbar :chatAsset="chatAsset" @click="getInputList" @history="getHistory" @delete="msgList = []" />
- <!-- 主体 -->
- <view class="w100 relative flex direction">
- <!-- 标题栏 -->
- <view class="title w100 flex item-center content-center size-large">
- {{ `AirSmartChat(${this.model})` }}
- </view>
- <!-- <view v-else class="flex item-center content-center p-10">
- <view class="background-color-gray p-5 gap radius flex">
- <button style="width: 400rpx;" v-for="item in chatgptVersion" :key="item"
- :class="['change-button radius button-none', item === model ? 'background-color-white' : 'background-color-gray']"
- @click="handleChangeVersion(item)">{{ item }}</button>
- </view>
- </view> -->
- <!-- 聊天区 -->
- <view class="chat size-large" style="flex:1">
- <block v-for="(x, i) in msgList" :key="i">
- <!-- 用户消息 -->
- <view v-if="x.my && x.type === 'msg'" class="chat-item " style="background-color: rgba(247,247,248,0.7);">
- <view class="chat-content">
- <view class="flex gap">
- <img class="flex direction item-end" src="../../static/logo-100.png" width="24" height="24" />
- <text>{{ x.msg }}</text>
- </view>
- </view>
- </view>
- <!-- AI消息 -->
- <view v-if="!x.my && x.type === 'msg'" class="chat-item">
- <view class="chat-content">
- <view class="flex gap">
- <img class="flex direction item-end" src="/static/chatgpt.png" width="24" height="24" />
- <img v-if="x.pic" :src="x.msg" />
- <text v-else>{{x.msg}}</text>
- <uni-icons class="ml-auto cursor-pointer" type="download" color="#acacbe"
- @click="x.msg && $squni.copy(x.msg)" />
- </view>
- </view>
- </view>
- <!-- 报错信息 -->
- <view v-if="x.type === 'error'" class="chat-item">
- <view class="chat-content flex gap item-center">
- <text class="cuIcon-roundclosefill text-red"></text> {{ x.msg }}
- </view>
- </view>
- </block>
- </view>
- <!-- 底部 -->
- <view class="w100 p-20 flex direction item-center background-color-white">
- <view class="w100">
- <view class="list flex wrap content-around">
- <uni-transition v-for="item in inputTipList" :key="item.type" :duration="500" show
- :mode-class="['fade', 'zoom-in']" @click="handleSendMsg(item)" :title="item.name"
- custom-class="item box-shadow p-15 border-gray radius cursor-pointer space-nowrap overflow-ellipsis overflow-hidden hover">
- {{ item.name }}
- </uni-transition>
- </view>
- </view>
- <!-- 输入框 -->
- <view class="w100">
- <view class="send-message relative box-shadow mt-10 radius border-gray">
- <textarea v-model="msg" auto-height maxlength="5000" cursor-spacing="10" :adjust-position="false"
- :placeholder="loading ? 'AirSmart is loading...' : 'Send a message'" @confirm="sendMsg"
- @input="handleInput" @keydown.tab.native.prevent="handleTab" />
- <uni-icons class="send-btn absolute cursor-pointer" type="paperplane-filled" size="24"
- :color="msg ? '#19c37d' : '#4559a480'" @click="sendMsg" />
- </view>
- </view>
- </view>
- </view>
- </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,
- sendMsgPic,
- msgList
- } from '@/api/chat.js'
- export default {
- data() {
- return {
- loading: false,
- userId: null,
- msgList: [],
- msgContent: "",
- msg: "",
- msgType: 0,
- chatAsset: {},
- assetType: 'n',
- msgLoading: false,
- promptId: null,
- curPrompt: {},
- inputTipList: [],
- model: 'gpt-4',
- // chatgpt版本
- chatgptVersion: ['gpt-3.5', 'gpt-4']
- }
- },
- watch: {
- loading(n, o) {
- if (n !== o && !n) {
- let last = this.msgList[this.msgList.length - 1]
- if (!last.my) {
- this.addHistory(last)
- }
- }
- },
- },
- 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
- }
- })
- 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)
- }
- },
- onLoad() {
- msgList().then(res => {
- if (res.code === 20000) {
- this.inputTipList = res.data
- }
- })
- },
- 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: this.model,
- 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') {
- } 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()
- },
- 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('剩余次数不足')
- 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)
- },
- closeSocket() {
- websocket.closeSocket()
- },
- // 对话输入框输入'/'显示提示信息
- handleInput(e) {
- if (e.detail.value === '') {
- this.msgType = 0
- }
- },
- // 支持tab输入空格
- insertInputTxt(className, insertTxt) {
- var elInput = document.getElementsByClassName(className)
- var startPos = elInput[0].selectionStart
- var endPos = elInput[0].selectionEnd
- if (startPos === undefined || endPos === undefined) return
- var txt = elInput[0].value
- var result = txt.substring(0, startPos) + insertTxt + txt.substring(endPos)
- this.msg = elInput[0].value = result
- // elInput[0].focus()
- // elInput[0].selectionStart = startPos + insertTxt.length
- // elInput[0].selectionEnd = startPos + insertTxt.length
- },
- handleTab() {
- this.insertInputTxt('uni-textarea-textarea', '\t')
- },
- handleSendMsg(e) {
- this.msgType = e.type
- this.msg = e.name
- },
- mockReply() {
- // 开发环境模拟回复
- if (process.env.NODE_ENV === 'development') {
- setTimeout(() => {
- this.putMsg('这是模拟返回消息: ' + new Date(), false)
- this.loading = false
- }, 1000)
- return
- }
- },
- getInputList(e) {
- this.inputTipList = e
- this.msgList = []
- },
- // 切換chatgpt版本
- handleChangeVersion(e) {
- this.model = e
- },
- getHistory(e) {
- if (e) {
- this.msgList = e.children
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- uni-page-body {
- height: 100%;
- background-color: #FFF;
- }
- .title {
- height: 128rpx;
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
- }
- .chat {
- overflow-y: auto;
- .chat-item {
- width: 100%;
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
- .chat-content {
- max-width: 1600rpx;
- margin: 0 auto;
- padding: 62rpx 40rpx;
- cursor: text;
- user-select: text;
- }
- .date {
- margin-top: 10px;
- }
- }
- }
- .item {
- width: 24%;
- }
- .list,
- .send-message {
- max-width: 1560rpx;
- margin: 10px auto 0;
- }
- uni-textarea {
- width: calc(100% - 30px);
- max-height: 400rpx;
- overflow-y: auto;
- padding: 15px 20px;
- line-height: 1.6;
- }
- .send-btn {
- right: 30rpx;
- top: 50%;
- transform: translate(0, -50%) rotate(45deg);
- }
- </style>
|