当前位置:   article > 正文

uniapp开发小程序实现AI聊天打字机功能_uniapp实现ai对话

uniapp实现ai对话

uni-app官网

 一、创建uni-app

我用的是vue-cli命令行创建uniapp项目。

踩坑1:执行命令报错了

npm ERR! Darwin 20.6.0
npm ERR! argv "/Users/zhuzhu/.nvm/versions/node/v6.2.0/bin/node" "/Users/zhuzhu/.nvm/versions/node/v6.2.0/bin/npm" "install"
npm ERR! node v6.2.0
npm ERR! npm  v3.8.9

npm ERR! This request requires auth credentials. Run `npm login` and repeat the request.
npm ERR! 
npm ERR! If you need help, you may report this error at:
npm ERR!     <https://github.com/npm/npm/issues>

npm ERR! Please include the following file with any support request:
npm ERR!     /Users/zhuzhu/Downloads/uni-preset-vue-vite/npm-debug.log

解决:直接访问官网的gitee,下载模板,然后npm install,之后在npm run XX运行你想要的程序就好啦。

二、开发聊天功能

实现思路

之前开发的是网页版的,现在要改成小程序,接口是算法已经写好的,直接拿来了。前端这块实现最重要的是success回调里的代码,接口返回的是流式(如图一),然后前端通过截取最后一次对话内容,通过startTyping方法实现打字机效果

图一

上代码(样式和方法可直接copy用)

  1. <template>
  2. <view class="main-dislogue">
  3. <view class="header-suspension">
  4. <view class="record-btn">悬浮</view>
  5. </view>
  6. <view class="content" ref="QAContent">
  7. <scroll-view id="scrollpage" :scroll-top="scrollTop" :scroll-y="true">
  8. <view v-for="item in dest" :key="item.id" id="msglistview">
  9. <view class="ask" v-if="item.flag != 1">
  10. <view class="ask-text">
  11. <view class="ask-desc" style="word-break: break-all;">
  12. {{ item.content }}
  13. </view>
  14. </view>
  15. <text class="ask-bulge"></text>
  16. <view class="ask-avatar">
  17. <image class="ask-sex" v-if="sex == 1" src="/static/boy.png" fit="contain"></image>
  18. <image class="ask-sex" v-if="sex == 2" src="/static/girl.png" fit="contain"></image>
  19. </view>
  20. </view>
  21. <view class="answer">
  22. <view class="answer-avatar">
  23. <image class="answer-ai" src="/static/ai.png" fit="contain"></image>
  24. </view>
  25. <text class="answer-bulge"></text>
  26. <view class="answer-text">
  27. <view class="answer-desc" ref="copyAiContent">{{item.ai_content}}</view>
  28. </view>
  29. </view>
  30. </view>
  31. </scroll-view>
  32. </view>
  33. <view class="bottom">
  34. <input :cursorSpacing="20" class="bottom-input" name="name" placeholder="请输入" v-model="value"/>
  35. <button class="bottom-button" type="primary" :disabled="isSend" @click="handleSend">发送</button>
  36. </view>
  37. </view>
  38. </template>
  1. <style>
  2. .main-dislogue {
  3. height: calc(100vh - 70px);
  4. background: #f5f5f5;
  5. display: flex;
  6. flex-direction: column;
  7. }
  8. /* 头部悬浮 */
  9. .header-suspension {
  10. width: 100rpx;
  11. height: 300rpx;
  12. /* pointer-events: none; */
  13. z-index: 100;
  14. position: fixed;
  15. right: 10rpx;
  16. bottom: 300rpx;
  17. }
  18. .head-image {
  19. width: 74rpx;
  20. height: 74rpx;
  21. z-index: 99;
  22. background: #d4d4d4;
  23. border-radius: 50%;
  24. padding: 6rpx;
  25. box-shadow: 0px 2rpx 20rpx rgba(0, 0, 0, 0.5);
  26. }
  27. .record-btn {
  28. width: 74rpx;
  29. height: 74rpx;
  30. background: #FFFFFF;
  31. border-radius: 50%;
  32. font-size: 26rpx;
  33. text-align: center;
  34. padding: 6rpx;
  35. box-shadow: 0px 2rpx 20rpx rgba(0, 0, 0, 0.5);
  36. color: #4A90E2;
  37. margin-top: 29rpx;
  38. }
  39. /* 内容 */
  40. .content {
  41. padding: 12rpx;
  42. padding-bottom: 100px;
  43. background: #f5f5f5;
  44. }
  45. /* #scrollpage {
  46. } */
  47. /* 问 */
  48. .ask {
  49. display: flex;
  50. justify-content: flex-end;
  51. width: 100%;
  52. margin-top: 6rpx;
  53. }
  54. .ask-avatar {
  55. width: 120rpx;
  56. margin-top: 20rpx;
  57. }
  58. .ask-sex {
  59. width: 100rpx;
  60. height: 100rpx;
  61. }
  62. .ask-bulge {
  63. position: relative;
  64. top: 41rpx;
  65. right: 23rpx;
  66. display: block;
  67. width: 0;
  68. height: 0;
  69. border: 15rpx solid #38a579;
  70. transform: rotate(45deg);
  71. }
  72. .ask-text {
  73. z-index: 1;
  74. }
  75. .ask-desc {
  76. background: #38a579;
  77. border-radius: 13rpx;
  78. padding: 15rpx;
  79. line-height: 58rpx;
  80. margin-top: 27rpx;
  81. white-space: pre-line;
  82. word-break: break-all;
  83. color: #fff;
  84. margin-left: 124rpx;
  85. }
  86. /* 答 */
  87. .answer {
  88. display: flex;
  89. justify-content: flex-start;
  90. margin-top: 6rpx;
  91. }
  92. .answer-avatar {
  93. width: 120rpx;
  94. margin-top: 20rpx;
  95. }
  96. .answer-ai {
  97. width: 100rpx;
  98. height: 100rpx;
  99. }
  100. .answer-bulge {
  101. position: relative;
  102. top: 41rpx;
  103. left: 23rpx;
  104. display: block;
  105. width: 0;
  106. height: 0;
  107. border: 15rpx solid #ffffff;
  108. transform: rotate(45deg);
  109. }
  110. .answer-text {
  111. z-index: 1;
  112. }
  113. .answer-desc {
  114. margin-right: 88rpx;
  115. border-radius: 13rpx;
  116. line-height: 58rpx;
  117. background: #fff;
  118. margin-top: 27rpx;
  119. tab-size: 12rpx;
  120. padding: 15rpx;
  121. white-space: pre-wrap;
  122. box-shadow: 0rpx 5rpx 47rpx 0rpx #97979773;
  123. }
  124. /* 尾部 */
  125. .bottom {
  126. border-top: 2rpx solid #CCCCCC;
  127. background: #f5f5f5;
  128. display: flex;
  129. padding: 10rpx;
  130. padding-bottom: 50rpx;
  131. position: fixed;
  132. bottom: 0;
  133. z-index: 99;
  134. width: 100%;
  135. }
  136. .bottom-input {
  137. flex: 1;
  138. font-size: 35rpx;
  139. border-radius: 10rpx;
  140. background: #FFFFFF;
  141. padding: 17rpx;
  142. }
  143. .bottom-button {
  144. width: 190rpx;
  145. height: 80rpx;
  146. font-size: 14px;
  147. line-height: 80rpx;
  148. margin-left: 20rpx;
  149. background: #4A90E2 !important;
  150. }
  151. </style>
  1. <script>
  2. import Api from "@/utils/api.js";
  3. import base from '@/utils/base.js';
  4. const BASE_URL = base.baseUrl;
  5. const recorderManager = uni.getRecorderManager()
  6. export default {
  7. data() {
  8. return {
  9. sex: "",
  10. birthDate: "",
  11. generateRecordsFlag: false,
  12. dest: [],
  13. dialogue_code: "",
  14. value: "",
  15. isSend: false,
  16. scrollTop: 0,
  17. currentText: "",
  18. isSpeaking: false
  19. }
  20. },
  21. onLoad(option) {
  22. this.sex = option.sex;
  23. this.birthDate = option.birthDate;
  24. this.dialogue_code = option.dialogue_code;
  25. },
  26. onReady() {
  27. let _this = this;
  28. uni.getStorage({
  29. key: 'gpt_h5_dialogue',
  30. success: function (res) {
  31. let list = res.data || "";
  32. if (list.length) {
  33. this.dest = JSON.parse(list);
  34. if (this.dest.length >= 2) {
  35. this.generateRecordsFlag = true;
  36. }
  37. } else {
  38. setTimeout(() => {
  39. _this.handleSend();
  40. }, 500)
  41. }
  42. }
  43. });
  44. },
  45. methods: {
  46. // 年龄转换
  47. ageCalculation(date) {
  48. var today = new Date();
  49. // 获取出生日期
  50. var birthDate = new Date(date); // 假设出生日期为199011
  51. // 计算年龄
  52. var age = today.getFullYear() - birthDate.getFullYear();
  53. var m = today.getMonth(), d = today.getDate();
  54. if (m < birthDate.getMonth()) {
  55. age--;
  56. } else if (m === birthDate.getMonth() && d < birthDate.getDate()) {
  57. age--;
  58. }
  59. return age;
  60. },
  61. // 发送聊天
  62. async handleSend() {
  63. this.preEventSource && this.preEventSource?.close();
  64. if (this.dest.length != 0 && !this.value) {
  65. return;
  66. }
  67. let _this = this;
  68. let { prompt, model } = await Api.getPromptList({ type: 1 });
  69. let sex = this.sex == 1 ? "男" : "女";
  70. let age = this.ageCalculation(this.birthDate);
  71. prompt = prompt.replace('{age}', `${age}岁`).replace('{sex}', `${sex}性`);
  72. let obj = {
  73. ai_content: "...",
  74. chat_model: model,
  75. content: prompt,
  76. create_time: "2024-01-05T06:55:29.000Z",
  77. dialogue_code: this.dialogue_code,
  78. id: 450,
  79. req_time: "2024-01-05T06:55:30.000Z",
  80. res_time: null,
  81. tags: null,
  82. user_code: "00468",
  83. flag: 1
  84. };
  85. const diaObj = {
  86. content: this.value,
  87. ai_content: "...",
  88. chat_model: model,
  89. create_time: new Date(),
  90. dialogue_code: this.dialogue_code,
  91. id: Date.now(),
  92. tags: null,
  93. user_code: "00468",
  94. loading: false,
  95. flag: 2
  96. };
  97. if (this.dest.length == 0) {
  98. // 第一次
  99. this.dest.push(obj);
  100. } else {
  101. this.dest.push(diaObj);
  102. }
  103. let params = {
  104. "dialogue_code": this.dialogue_code,
  105. "content": this.value || this.dest[0].content,
  106. "chat_model": model
  107. }
  108. this.isSend = true;
  109. _this.scrollToBottom();
  110. let ai_content = "", startFlag = false;
  111. this.value = ""; // 置空输入框
  112. // 从这往上可以忽略,这是我业务逻辑,不必关注。重点是uni.request success回调内容
  113. uni.request({
  114. url: `${BASE_URL}/hmgpt/dialogue`,
  115. data: params,
  116. method: "POST",
  117. headers: {
  118. "Content-Type": 'application/json',
  119. },
  120. success: (res) => {
  121. let str = JSON.stringify(res.data);
  122. // 将字符串按"data: ["分割,然后取最后一个部分
  123. const lastDataSection = str.split("data: [").pop();
  124. // 截取最后一个JSON对象的部分
  125. const lastJsonString = lastDataSection.split("]")[0].replace(/\\/g, '');
  126. // 解析JSON字符串
  127. const lastJsonObject = JSON.parse(lastJsonString);
  128. // 获取ai_content的值
  129. const lastAiContent = lastJsonObject.ai_content;
  130. console.log(lastAiContent, 'lastAiContent');
  131. ai_content = lastAiContent;
  132. if (lastAiContent == "") {
  133. // 返回空,则默认提示
  134. ai_content = "目前公司GPU服务器有限,会因为调试需要临时中断出现服务不可用,请稍后重试。";
  135. }
  136. _this.dest[_this.dest.length - 1].ai_content = "";
  137. if (!startFlag) {
  138. startFlag = true
  139. startTyping();
  140. }
  141. }
  142. });
  143. function startTyping() {
  144. let currentIndex = 0;
  145. const typingSpeed = 100; // 打字速度,单位:毫秒
  146. const timer = setInterval(() => {
  147. _this.dest[_this.dest.length - 1].ai_content += ai_content[currentIndex];
  148. currentIndex++;
  149. _this.scrollToBottom();
  150. if (currentIndex >= ai_content.length) {
  151. clearInterval(timer);
  152. _this.isSend = false;
  153. }
  154. }, typingSpeed);
  155. uni.setStorage({
  156. key: 'gpt_h5_dialogue',
  157. data: JSON.stringify(_this.dest),
  158. success: function () { }
  159. });
  160. }
  161. },
  162. // 滚动至聊天底部
  163. scrollToBottom() {
  164. this.$nextTick(() => {
  165. const query = uni.createSelectorQuery();
  166. query.select('#scrollpage').boundingClientRect();
  167. query.exec(res => {
  168. this.scrollTop = res[0].height;
  169. uni.pageScrollTo({
  170. scrollTop: res[0].height + 170, // 将滚动位置设置为顶部
  171. duration: 300 // 滚动到顶部的动画时长,单位为毫秒
  172. });
  173. })
  174. })
  175. }
  176. },
  177. }
  178. </script>

效果图

打字机效果可以自行试试哈,整体页面大概是这个样子

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/698540
推荐阅读
相关标签
  

闽ICP备14008679号