index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <template>
  2. <view class="w100 h100 flex">
  3. <!-- 侧边栏 -->
  4. <navbar :chatAsset="chatAsset" @click="getInputList" @history="getHistory" @delete="msgList = []" />
  5. <!-- 主体 -->
  6. <view class="w100 relative flex direction">
  7. <!-- 标题栏 -->
  8. <view class="title w100 flex item-center content-center size-large">
  9. {{ `AirSmartChat(${this.model})` }}
  10. </view>
  11. <!-- <view v-else class="flex item-center content-center p-10">
  12. <view class="background-color-gray p-5 gap radius flex">
  13. <button style="width: 400rpx;" v-for="item in chatgptVersion" :key="item"
  14. :class="['change-button radius button-none', item === model ? 'background-color-white' : 'background-color-gray']"
  15. @click="handleChangeVersion(item)">{{ item }}</button>
  16. </view>
  17. </view> -->
  18. <!-- 聊天区 -->
  19. <view class="chat size-large" style="flex:1">
  20. <block v-for="(x, i) in msgList" :key="i">
  21. <!-- 用户消息 -->
  22. <view v-if="x.my && x.type === 'msg'" class="chat-item " style="background-color: rgba(247,247,248,0.7);">
  23. <view class="chat-content">
  24. <view class="flex gap">
  25. <img class="flex direction item-end" src="../../static/logo-100.png" width="24" height="24" />
  26. <text>{{ x.msg }}</text>
  27. </view>
  28. </view>
  29. </view>
  30. <!-- AI消息 -->
  31. <view v-if="!x.my && x.type === 'msg'" class="chat-item">
  32. <view class="chat-content">
  33. <view class="flex gap">
  34. <img class="flex direction item-end" src="/static/chatgpt.png" width="24" height="24" />
  35. <img v-if="x.pic" :src="x.msg" />
  36. <text v-else>{{x.msg}}</text>
  37. <uni-icons class="ml-auto cursor-pointer" type="download" color="#acacbe"
  38. @click="x.msg && $squni.copy(x.msg)" />
  39. </view>
  40. </view>
  41. </view>
  42. <!-- 报错信息 -->
  43. <view v-if="x.type === 'error'" class="chat-item">
  44. <view class="chat-content flex gap item-center">
  45. <text class="cuIcon-roundclosefill text-red"></text> {{ x.msg }}
  46. </view>
  47. </view>
  48. </block>
  49. </view>
  50. <!-- 底部 -->
  51. <view class="w100 p-20 flex direction item-center background-color-white">
  52. <view class="w100">
  53. <view class="list flex wrap content-around">
  54. <uni-transition v-for="item in inputTipList" :key="item.type" :duration="500" show
  55. :mode-class="['fade', 'zoom-in']" @click="handleSendMsg(item)" :title="item.name"
  56. custom-class="item box-shadow p-15 border-gray radius cursor-pointer space-nowrap overflow-ellipsis overflow-hidden hover">
  57. {{ item.name }}
  58. </uni-transition>
  59. </view>
  60. </view>
  61. <!-- 输入框 -->
  62. <view class="w100">
  63. <view class="send-message relative box-shadow mt-10 radius border-gray">
  64. <textarea v-model="msg" auto-height maxlength="5000" cursor-spacing="10" :adjust-position="false"
  65. :placeholder="loading ? 'AirSmart is loading...' : 'Send a message'" @confirm="sendMsg"
  66. @input="handleInput" @keydown.tab.native.prevent="handleTab" />
  67. <uni-icons class="send-btn absolute cursor-pointer" type="paperplane-filled" size="24"
  68. :color="msg ? '#19c37d' : '#4559a480'" @click="sendMsg" />
  69. </view>
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. </template>
  75. <script>
  76. import {
  77. dateFormat,
  78. interval
  79. } from '@/util/squ.js'
  80. import squni, {
  81. scrollToBottom
  82. } from '@/util/squni.js'
  83. import websocket from '@/util/websocket'
  84. import {
  85. sendMsgApi,
  86. getUserChatAssetApi,
  87. startNewChatApi,
  88. findPromptListApi,
  89. sendMsgPic,
  90. msgList
  91. } from '@/api/chat.js'
  92. export default {
  93. data() {
  94. return {
  95. loading: false,
  96. userId: null,
  97. msgList: [],
  98. msgContent: "",
  99. msg: "",
  100. msgType: 0,
  101. chatAsset: {},
  102. assetType: 'n',
  103. msgLoading: false,
  104. promptId: null,
  105. curPrompt: {},
  106. inputTipList: [],
  107. model: 'gpt-4',
  108. // chatgpt版本
  109. chatgptVersion: ['gpt-3.5', 'gpt-4']
  110. }
  111. },
  112. watch: {
  113. loading(n, o) {
  114. if (n !== o && !n) {
  115. let last = this.msgList[this.msgList.length - 1]
  116. if (!last.my) {
  117. this.addHistory(last)
  118. }
  119. }
  120. },
  121. },
  122. async onShow() {
  123. await this.$ready
  124. this.userId = this.$store.getters.userId
  125. if (!this.userId) {
  126. this.$squni.toast('请先进行登录哦')
  127. return
  128. }
  129. this.promptId = this.$squni.getCurQuery('promptId')
  130. if (this.promptId) {
  131. findPromptListApi({
  132. id: this.promptId
  133. }).then(res => {
  134. if (res.status === 'success') {
  135. this.curPrompt = res.data
  136. if (this.ready && HELLO_MSG.msg.indexOf('提示词') < 0) {
  137. let prompt = ''
  138. if (this.curPrompt && this.curPrompt.title) {
  139. prompt = `(提示词:${this.curPrompt.title})`
  140. }
  141. HELLO_MSG.msg += prompt
  142. }
  143. }
  144. })
  145. }
  146. getUserChatAssetApi().then(res => {
  147. if (res.status === 'success') {
  148. this.chatAsset = res.data
  149. }
  150. })
  151. try {
  152. //建立socket连接
  153. websocket.connectSocket(this.$config.wssUrl + '/tools/chat/user/' + this.userId, msg => {
  154. this.recvMsg(msg)
  155. }, () => {
  156. //如果连接成功则发送心跳检测
  157. this.heartBeatTest()
  158. })
  159. } catch (error) {
  160. console.log('websocket connectSocket error:' + error)
  161. }
  162. },
  163. onLoad() {
  164. msgList().then(res => {
  165. if (res.code === 20000) {
  166. this.inputTipList = res.data
  167. }
  168. })
  169. },
  170. onHide() {
  171. this.closeSocket()
  172. },
  173. methods: {
  174. logout() {
  175. this.loginLoading = true
  176. doLoginApi('H5', {
  177. username: this.email || this.phone || this.username,
  178. password: this.password
  179. }).then(res => {
  180. this.loginLoading = false
  181. if (res === LoginSuccess) {
  182. uni.showToast({
  183. title: '登录成功'
  184. })
  185. // 跳转到主页发现已经登录,会自动重新获取用户信息,此处无需获取
  186. window.location.href = getUrlQuery('_originHref')
  187. } else {
  188. uni.showToast({
  189. title: (res && res.data && res.data.message) || '登录失败',
  190. icon: 'none'
  191. })
  192. this.createCode(4)
  193. }
  194. })
  195. },
  196. sendMsg() {
  197. if (this.msg == "") {
  198. this.$squni.toast('请先输入您的问题哦')
  199. return
  200. }
  201. let msg = this.msg
  202. this.putMsg(this.msg, true)
  203. this.msgLoading = true
  204. this.loading = true
  205. // ======== 开发环境模拟回复 ========
  206. // return this.mockReply()
  207. // ======== 开发环境模拟回复 ========
  208. if (this.calcAsset() === false) {
  209. this.loading = false
  210. return
  211. }
  212. if (this.msgType === 2) {
  213. sendMsgPic({
  214. userId: this.$squni.getStorageSync('userId'),
  215. question: msg,
  216. type: this.msgType
  217. }).then(res => {
  218. if (res.code === 20000) {
  219. JSON.parse(res.data.ack).map(i => {
  220. this.putMsg(i.url, false, 'msg', true)
  221. this.loading = false
  222. this.msgLoading = false
  223. this.msgType = 0
  224. })
  225. } else {
  226. this.putMsgError('机器人被拔网线了,请稍后再试~')
  227. }
  228. })
  229. } else {
  230. // 发送消息: M1/M2
  231. websocket.sendMessage(JSON.stringify({
  232. model: this.model,
  233. msg: msg,
  234. platform: this.$squni.getStorageSync('platform'),
  235. openid: this.$squni.getStorageSync('openid'),
  236. promptId: this.promptId
  237. }), null, () => {
  238. this.putMsgError('机器人被拔网线了,请稍后再试~')
  239. })
  240. }
  241. },
  242. recvMsg(msg) {
  243. this.msgLoading = false
  244. if (!msg) {
  245. this.putMsgError('机器人开小差了,请稍后再试~')
  246. return
  247. }
  248. // 发送消息
  249. // 1+1
  250. // 收到消息
  251. // {"role":"assistant","content":null}
  252. // {"role":null,"content":"2"}
  253. // {"role":null,"content":null}
  254. // [DONE]
  255. if (msg === '[DONE]') {
  256. this.loading = false
  257. } else {
  258. try {
  259. let msgJson = JSON.parse(msg)
  260. if (msgJson.role === 'sqchat') {
  261. let content = msgJson.content
  262. if (msgJson.codeKey) {
  263. content += `[${msgJson.codeKey}]`
  264. if (msgJson.codeKey === 'chat.asset_short') {
  265. } else if (msgJson.codeKey.indexOf('chat.asset_') >= 0) {
  266. this.chatAsset[this.assetType]++
  267. }
  268. }
  269. this.putMsgError(content)
  270. }
  271. if (msgJson.role === 'assistant') {
  272. this.putMsg('', false)
  273. } else if (msgJson.role == null && msgJson.content) {
  274. this.msgList[this.msgList.length - 1].msg += msgJson.content
  275. scrollToBottom()
  276. }
  277. } catch (error) {
  278. this.putMsgError(msg)
  279. }
  280. }
  281. },
  282. startNewChat() {
  283. HELLO_MSG.date = dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
  284. this.msgList = [HELLO_MSG]
  285. startNewChatApi()
  286. },
  287. calcAsset() {
  288. if (this.chatAsset.dfn > 0) {
  289. this.chatAsset.dfn--
  290. this.assetType = 'dfn'
  291. } else if (this.chatAsset.n > 0) {
  292. this.chatAsset.n--
  293. this.assetType = 'n'
  294. } else {
  295. this.$squni.toast('剩余次数不足')
  296. return false
  297. }
  298. },
  299. putMsg(msg, my = false, type = 'msg', pic = false) {
  300. let item = {
  301. type: type,
  302. msg: msg,
  303. my: my,
  304. pic: pic,
  305. date: dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
  306. }
  307. this.msgList.push(item)
  308. scrollToBottom()
  309. if (my) {
  310. this.addHistory(item)
  311. // 清除消息
  312. this.msg = ''
  313. this.msgReply = ''
  314. }
  315. },
  316. putMsgError(msg) {
  317. this.putMsg(msg, false, 'error')
  318. this.msgLoading = false
  319. this.loading = false
  320. },
  321. addHistory(item) {
  322. if (item.type === 'msg') {
  323. let chatHistory = this.$squni.getStorageSync('chatHistory')
  324. if (!chatHistory) {
  325. chatHistory = []
  326. }
  327. if (chatHistory.length >= 50) {
  328. chatHistory.splice(0, 1)
  329. }
  330. chatHistory.push(item)
  331. this.$squni.setStorageSync('chatHistory', chatHistory)
  332. }
  333. },
  334. //心跳检测
  335. heartBeatTest() {
  336. let globalTimer = null
  337. //清除定时器
  338. clearInterval(globalTimer)
  339. //开启定时器定时检测心跳
  340. globalTimer = setInterval(() => {
  341. //发送消息给服务端
  342. websocket.sendMessage('PING', null, () => {
  343. //如果失败则清除定时器
  344. clearInterval(globalTimer)
  345. })
  346. }, 10000)
  347. },
  348. closeSocket() {
  349. websocket.closeSocket()
  350. },
  351. // 对话输入框输入'/'显示提示信息
  352. handleInput(e) {
  353. if (e.detail.value === '') {
  354. this.msgType = 0
  355. }
  356. },
  357. // 支持tab输入空格
  358. insertInputTxt(className, insertTxt) {
  359. var elInput = document.getElementsByClassName(className)
  360. var startPos = elInput[0].selectionStart
  361. var endPos = elInput[0].selectionEnd
  362. if (startPos === undefined || endPos === undefined) return
  363. var txt = elInput[0].value
  364. var result = txt.substring(0, startPos) + insertTxt + txt.substring(endPos)
  365. this.msg = elInput[0].value = result
  366. // elInput[0].focus()
  367. // elInput[0].selectionStart = startPos + insertTxt.length
  368. // elInput[0].selectionEnd = startPos + insertTxt.length
  369. },
  370. handleTab() {
  371. this.insertInputTxt('uni-textarea-textarea', '\t')
  372. },
  373. handleSendMsg(e) {
  374. this.msgType = e.type
  375. this.msg = e.name
  376. },
  377. mockReply() {
  378. // 开发环境模拟回复
  379. if (process.env.NODE_ENV === 'development') {
  380. setTimeout(() => {
  381. this.putMsg('这是模拟返回消息: ' + new Date(), false)
  382. this.loading = false
  383. }, 1000)
  384. return
  385. }
  386. },
  387. getInputList(e) {
  388. this.inputTipList = e
  389. this.msgList = []
  390. },
  391. // 切換chatgpt版本
  392. handleChangeVersion(e) {
  393. this.model = e
  394. },
  395. getHistory(e) {
  396. if (e) {
  397. this.msgList = e.children
  398. }
  399. }
  400. }
  401. }
  402. </script>
  403. <style lang="scss" scoped>
  404. uni-page-body {
  405. height: 100%;
  406. background-color: #FFF;
  407. }
  408. .title {
  409. height: 128rpx;
  410. border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  411. }
  412. .chat {
  413. overflow-y: auto;
  414. .chat-item {
  415. width: 100%;
  416. border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  417. .chat-content {
  418. max-width: 1600rpx;
  419. margin: 0 auto;
  420. padding: 62rpx 40rpx;
  421. cursor: text;
  422. user-select: text;
  423. }
  424. .date {
  425. margin-top: 10px;
  426. }
  427. }
  428. }
  429. .item {
  430. width: 24%;
  431. }
  432. .list,
  433. .send-message {
  434. max-width: 1560rpx;
  435. margin: 10px auto 0;
  436. }
  437. uni-textarea {
  438. width: calc(100% - 30px);
  439. max-height: 400rpx;
  440. overflow-y: auto;
  441. padding: 15px 20px;
  442. line-height: 1.6;
  443. }
  444. .send-btn {
  445. right: 30rpx;
  446. top: 50%;
  447. transform: translate(0, -50%) rotate(45deg);
  448. }
  449. </style>