index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. <template>
  2. <view>
  3. <cu-custom bgColor="bg-cyan" :isBack="$squni.getPrePage() != null">
  4. <block slot="backText">返回</block>
  5. <block slot="content">AirSmartChat</block>
  6. </cu-custom>
  7. <view class="cu-chat">
  8. <block v-for="(x,i) in msgList" :key="i">
  9. <!-- 用户消息 -->
  10. <view v-if="x.my && x.type === 'msg'" class="cu-item self"
  11. :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
  12. <view class="main">
  13. <view class="content bg-cyan shadow"><!-- @click="x.msg && $squni.copy(x.msg)" -->
  14. <text>{{ x.msg }}</text>
  15. </view>
  16. </view>
  17. <image class="cu-avatar round" src="/static/logo-100.png">
  18. <view v-if="i === 0" class="date">{{ x.date }}</view>
  19. </view>
  20. <!-- AI消息 -->
  21. <view v-if="!x.my && x.type === 'msg'" class="cu-item"
  22. :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
  23. <view class="flex flex-direction align-center">
  24. <image class="cu-avatar round chat-avatar" src="/static/robot.png">
  25. <text v-if="i === 0" class="cuIcon-title" :class="[statusColor]"></text>
  26. </view>
  27. <view class="main">
  28. <view class="content shadow"><!-- @click="x.msg && $squni.copy(x.msg)" -->
  29. <img v-if="x.pic" :src="x.msg" />
  30. <text v-else>{{ x.msg }}</text>
  31. </view>
  32. </view>
  33. <view v-if="i === 0" class="date">{{ x.date }}</view>
  34. </view>
  35. <view v-if="x.type === 'error'" class="cu-info">
  36. <text class="cuIcon-roundclosefill text-red "></text> {{ x.msg }}
  37. </view>
  38. </block>
  39. <view v-if="msgLoading" class="cu-item">
  40. <view class="flex flex-direction align-center">
  41. <image class="cu-avatar round chat-avatar" src="/static/robot.png">
  42. </view>
  43. <view class="main">
  44. <text class="cuIcon-loading2 cuIconfont-spin text-cyan"></text>
  45. </view>
  46. </view>
  47. </view>
  48. <view class="cu-bar foot input" :style="[{bottom:inputBottom+'px'}]">
  49. <view class="tip-box">
  50. <button class="cu-btn round shadow sm cuIcon line-cyan" @click="startNewChat">
  51. <text class="cuIcon-paint"></text>
  52. </button>
  53. <button class="cu-btn round shadow sm line-cyan"
  54. @click="$squni.navigateTo('/pages/main/history')">历史消息</button>
  55. <button class="cu-btn round shadow sm bg-orange"
  56. @click="$squni.navigateTo('/pages/main/prompt/prompt')">提示词</button>
  57. <button class="cu-btn round shadow sm line-cyan" @click="openBottomFunc">我的信息</button>
  58. </view>
  59. <!-- <view class="action func" @click="openBottomFunc">
  60. <text class="cuIcon-list text-cyan" style="font-size: 60upx;"></text>
  61. </view> -->
  62. <view class="input-msg">
  63. <ol class="tip-list">
  64. <li class="tip-item" v-for="item in inputTipList" :key="item.type" @click="handleSendMsg(item)">
  65. {{ item.name }}
  66. </li>
  67. </ol>
  68. <input v-model="msg" class="solid padding-lr" :adjust-position="false" :focus="false" maxlength="5000"
  69. cursor-spacing="10" :placeholder="loading ? '小AirSmart正在思考中,请稍后~' : '你可以问我任何问题或输入 “/” 获取模板'" @focus="inputFocus"
  70. @blur="inputBlur" @confirm="sendMsg" @input="handleInput"></input>
  71. </view>
  72. <!-- <view class="action">
  73. <text class="cuIcon-emojifill text-grey"></text>
  74. </view> -->
  75. <button class="cu-btn bg-cyan shadow" :disabled="loading" @click="sendMsg">
  76. <text class="cuIcon-loading2 cuIconfont-spin"
  77. v-if="loading || !ready"></text>{{ !ready ? '连接中' : '发送' }}
  78. </button>
  79. </view>
  80. <bottom-func v-if="bottomFuncShow" ref="bottomFunc" :chatAsset="chatAsset"
  81. @rewarded-video-ad="() => chatAsset.dfn += 2"></bottom-func>
  82. </view>
  83. </template>
  84. <script>
  85. import {
  86. dateFormat,
  87. interval
  88. } from '@/util/squ.js'
  89. import squni, {
  90. scrollToBottom
  91. } from '@/util/squni.js'
  92. import websocket from '@/util/websocket'
  93. import {
  94. sendMsgApi,
  95. getUserChatAssetApi,
  96. startNewChatApi,
  97. findPromptListApi,
  98. msgList,
  99. sendMsgPic
  100. } from '@/api/chat.js'
  101. import BottomFunc from './bottom-func'
  102. const HELLO_MSG = {
  103. type: 'msg',
  104. my: false,
  105. msg: '连接中,请稍后~',
  106. date: dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
  107. }
  108. export default {
  109. components: {
  110. BottomFunc
  111. },
  112. data() {
  113. return {
  114. loading: false,
  115. userId: null,
  116. msgList: [HELLO_MSG],
  117. msgContent: "",
  118. msg: "",
  119. msgType: 0,
  120. inputBottom: 0,
  121. bottomFuncShow: false,
  122. chatAsset: {},
  123. assetType: 'n',
  124. statusColor: 'text-red',
  125. statusTimer: null,
  126. msgLoading: false,
  127. promptId: null,
  128. curPrompt: {},
  129. inputTipList: []
  130. }
  131. },
  132. computed: {
  133. ready() {
  134. return this.statusColor === 'text-green'
  135. }
  136. },
  137. watch: {
  138. loading(n, o) {
  139. if (n !== o && !n) {
  140. let last = this.msgList[this.msgList.length - 1]
  141. if (!last.my) {
  142. this.addHistory(last)
  143. }
  144. }
  145. },
  146. statusColor(n, o) {
  147. if (n === 'text-green') {
  148. let prompt = ''
  149. if (this.curPrompt && this.curPrompt.title) {
  150. prompt = `(提示词:${this.curPrompt.title})`
  151. }
  152. HELLO_MSG.msg = '你好,我是AirSmartChat机器人,可以帮你解答疑惑或提供灵感' + prompt
  153. } else {
  154. HELLO_MSG.msg = '连接中,请稍后~'
  155. }
  156. }
  157. },
  158. async onShow() {
  159. await this.$ready
  160. this.userId = this.$store.getters.userId
  161. if (!this.userId) {
  162. this.$squni.toast('请先进行登录哦')
  163. return
  164. }
  165. this.promptId = this.$squni.getCurQuery('promptId')
  166. if (this.promptId) {
  167. findPromptListApi({
  168. id: this.promptId
  169. }).then(res => {
  170. if (res.status === 'success') {
  171. this.curPrompt = res.data
  172. if (this.ready && HELLO_MSG.msg.indexOf('提示词') < 0) {
  173. let prompt = ''
  174. if (this.curPrompt && this.curPrompt.title) {
  175. prompt = `(提示词:${this.curPrompt.title})`
  176. }
  177. HELLO_MSG.msg += prompt
  178. }
  179. }
  180. })
  181. }
  182. getUserChatAssetApi().then(res => {
  183. if (res.status === 'success') {
  184. this.chatAsset = res.data
  185. }
  186. })
  187. this.heartStatus()
  188. try {
  189. //建立socket连接
  190. websocket.connectSocket(this.$config.wssUrl + '/tools/chat/user/' + this.userId, msg => {
  191. this.recvMsg(msg)
  192. }, () => {
  193. //如果连接成功则发送心跳检测
  194. this.heartBeatTest()
  195. })
  196. } catch (error) {
  197. console.log('websocket connectSocket error:' + error)
  198. }
  199. },
  200. onHide() {
  201. this.closeSocket()
  202. },
  203. methods: {
  204. logout() {
  205. this.loginLoading = true
  206. doLoginApi('H5', {
  207. username: this.email || this.phone || this.username,
  208. password: this.password
  209. }).then(res => {
  210. this.loginLoading = false
  211. if (res === LoginSuccess) {
  212. uni.showToast({
  213. title: '登录成功'
  214. })
  215. // 跳转到主页发现已经登录,会自动重新获取用户信息,此处无需获取
  216. window.location.href = getUrlQuery('_originHref')
  217. } else {
  218. uni.showToast({
  219. title: (res && res.data && res.data.message) || '登录失败',
  220. icon: 'none'
  221. })
  222. this.createCode(4)
  223. }
  224. })
  225. },
  226. sendMsg() {
  227. if (this.msg == "") {
  228. this.$squni.toast('请先输入您的问题哦')
  229. return
  230. }
  231. let msg = this.msg
  232. this.putMsg(this.msg, true)
  233. this.msgLoading = true
  234. this.loading = true
  235. // ======== 开发环境模拟回复 ========
  236. // return this.mockReply()
  237. // ======== 开发环境模拟回复 ========
  238. if (this.calcAsset() === false) {
  239. this.loading = false
  240. return
  241. }
  242. if(this.msgType === 2){
  243. sendMsgPic({
  244. userId: this.$squni.getStorageSync('userId'),
  245. question: msg,
  246. type: this.msgType
  247. }).then(res => {
  248. if(res.code === 20000) {
  249. JSON.parse(res.data.ack).map(i => {
  250. this.putMsg(i.url, false, 'msg', true)
  251. this.loading = false
  252. this.msgLoading = false
  253. this.msgType = 0
  254. })
  255. }else{
  256. this.putMsgError('机器人被拔网线了,请稍后再试~')
  257. }
  258. })
  259. }else{
  260. // 发送消息: M1/M2
  261. websocket.sendMessage(JSON.stringify({
  262. model: 'M1',
  263. msg: msg,
  264. platform: this.$squni.getStorageSync('platform'),
  265. openid: this.$squni.getStorageSync('openid'),
  266. promptId: this.promptId
  267. }), null, () => {
  268. this.putMsgError('机器人被拔网线了,请稍后再试~')
  269. })
  270. }
  271. },
  272. recvMsg(msg) {
  273. this.msgLoading = false
  274. if (!msg) {
  275. this.putMsgError('机器人开小差了,请稍后再试~')
  276. return
  277. }
  278. // 发送消息
  279. // 1+1
  280. // 收到消息
  281. // {"role":"assistant","content":null}
  282. // {"role":null,"content":"2"}
  283. // {"role":null,"content":null}
  284. // [DONE]
  285. if (msg === '[DONE]') {
  286. this.loading = false
  287. } else {
  288. try {
  289. let msgJson = JSON.parse(msg)
  290. if (msgJson.role === 'sqchat') {
  291. let content = msgJson.content
  292. if (msgJson.codeKey) {
  293. content += `[${msgJson.codeKey}]`
  294. if (msgJson.codeKey === 'chat.asset_short') {
  295. this.openBottomFunc()
  296. } else if (msgJson.codeKey.indexOf('chat.asset_') >= 0) {
  297. this.chatAsset[this.assetType]++
  298. }
  299. }
  300. this.putMsgError(content)
  301. }
  302. if (msgJson.role === 'assistant') {
  303. this.putMsg('', false)
  304. } else if (msgJson.role == null && msgJson.content) {
  305. this.msgList[this.msgList.length - 1].msg += msgJson.content
  306. scrollToBottom()
  307. }
  308. } catch (error) {
  309. this.putMsgError(msg)
  310. }
  311. }
  312. },
  313. startNewChat() {
  314. HELLO_MSG.date = dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
  315. this.msgList = [HELLO_MSG]
  316. startNewChatApi()
  317. },
  318. // sendMsgBak() {
  319. // let that = this
  320. // if (this.msg == "") {
  321. // return
  322. // }
  323. // this.msgContent += (this.userId + ":" + this.msg + "\n")
  324. // this.putMsg(this.msg, true)
  325. // this.loading = true
  326. // // ======== 开发环境模拟回复 ========
  327. // return this.mockReply()
  328. // // ======== 开发环境模拟回复 ========
  329. // sendMsgApi({
  330. // userId: this.userId + '',
  331. // question: this.msgContent
  332. // }).then(({
  333. // status,
  334. // data
  335. // }) => {
  336. // if (status === 'success') {
  337. // this.putMsg(data.ack, false)
  338. // this.msgContent += ("openai:" + this.msg + "\n")
  339. // } else {
  340. // this.putMsg(res.message || '机器人开小差了,请稍后再试~', false, 'error')
  341. // }
  342. // that.loading = false
  343. // })
  344. // },
  345. calcAsset() {
  346. if (this.chatAsset.dfn > 0) {
  347. this.chatAsset.dfn--
  348. this.assetType = 'dfn'
  349. } else if (this.chatAsset.n > 0) {
  350. this.chatAsset.n--
  351. this.assetType = 'n'
  352. } else {
  353. this.$squni.toast('剩余次数不足')
  354. this.openBottomFunc()
  355. return false
  356. }
  357. },
  358. putMsg(msg, my = false, type = 'msg', pic = false) {
  359. let item = {
  360. type: type,
  361. msg: msg,
  362. my: my,
  363. pic: pic,
  364. date: dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
  365. }
  366. this.msgList.push(item)
  367. scrollToBottom()
  368. if (my) {
  369. this.addHistory(item)
  370. // 清除消息
  371. this.msg = ''
  372. this.msgReply = ''
  373. }
  374. },
  375. putMsgError(msg) {
  376. this.putMsg(msg, false, 'error')
  377. this.msgLoading = false
  378. this.loading = false
  379. },
  380. addHistory(item) {
  381. if (item.type === 'msg') {
  382. let chatHistory = this.$squni.getStorageSync('chatHistory')
  383. if (!chatHistory) {
  384. chatHistory = []
  385. }
  386. if (chatHistory.length >= 50) {
  387. chatHistory.splice(0, 1)
  388. }
  389. chatHistory.push(item)
  390. this.$squni.setStorageSync('chatHistory', chatHistory)
  391. }
  392. },
  393. //心跳检测
  394. heartBeatTest() {
  395. let globalTimer = null
  396. //清除定时器
  397. clearInterval(globalTimer)
  398. //开启定时器定时检测心跳
  399. globalTimer = setInterval(() => {
  400. //发送消息给服务端
  401. websocket.sendMessage('PING', null, () => {
  402. //如果失败则清除定时器
  403. clearInterval(globalTimer)
  404. })
  405. }, 10000)
  406. },
  407. heartStatus() {
  408. this.statusTimer = interval(() => {
  409. if (websocket.isOpen) {
  410. this.statusColor = 'text-green'
  411. } else if (this.statusColor === 'text-red') {
  412. this.statusColor = 'text-yellow'
  413. } else {
  414. this.statusColor = 'text-red'
  415. }
  416. }, this.statusTimer, 200)
  417. },
  418. closeSocket() {
  419. websocket.closeSocket()
  420. clearInterval(this.statusTimer)
  421. this.statusColor = 'text-red'
  422. },
  423. inputFocus(e) {
  424. this.inputBottom = e.detail.height
  425. },
  426. inputBlur(e) {
  427. this.inputBottom = 0
  428. },
  429. // 对话输入框输入'/'显示提示信息
  430. handleInput(e) {
  431. if (e.detail.value.startsWith('/')) {
  432. msgList().then(res => {
  433. this.inputTipList = res.data
  434. })
  435. }
  436. },
  437. handleSendMsg(e){
  438. this.msgType = e.type
  439. this.msg = e.name
  440. this.inputTipList = []
  441. },
  442. openBottomFunc() {
  443. this.bottomFuncShow = true
  444. this.$nextTick(() => {
  445. this.$refs.bottomFunc.open()
  446. })
  447. },
  448. mockReply() {
  449. // 开发环境模拟回复
  450. if (process.env.NODE_ENV === 'development') {
  451. setTimeout(() => {
  452. this.putMsg('这是模拟返回消息: ' + new Date(), false)
  453. this.loading = false
  454. }, 1000)
  455. return
  456. }
  457. }
  458. }
  459. }
  460. </script>
  461. <style>
  462. page {
  463. padding-bottom: 220upx;
  464. }
  465. .cu-chat .chat-avatar.cu-avatar {
  466. width: 82upx;
  467. height: 82upx;
  468. }
  469. .cu-item:not(.first) {
  470. padding-bottom: 0upx;
  471. }
  472. .cu-item.sec {
  473. padding-top: 0upx;
  474. }
  475. .cu-chat .cu-item>.main {
  476. max-width: calc(100% - 160upx);
  477. }
  478. .main .content {
  479. word-wrap: break-word;
  480. cursor: text;
  481. user-select: text;
  482. }
  483. .cu-bar.foot {
  484. box-shadow: 0 -0.5px 1px rgba(0, 0, 0, 0.1);
  485. align-items: flex-end;
  486. }
  487. .foot {
  488. padding-top: 20upx;
  489. padding-bottom: 60upx;
  490. }
  491. .foot .cu-btn {
  492. margin-right: 20upx;
  493. width: 200upx;
  494. }
  495. .foot .action.func {
  496. margin-left: 30upx;
  497. }
  498. .foot .tip-box {
  499. position: absolute;
  500. top: -60upx;
  501. margin: 0 30upx;
  502. }
  503. .input-msg{
  504. width: 100%;
  505. .tip-list{
  506. margin: 0 40upx;
  507. padding: 0 40upx;
  508. .tip-item{
  509. cursor: pointer;
  510. color: #909399;
  511. line-height: 64upx;
  512. }
  513. .tip-item:hover{
  514. background: #d9ecff;
  515. color: #000;
  516. }
  517. }
  518. }
  519. </style>