当前位置:首页 » 《随便一记》 » 正文

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

1 人参与  2024年04月23日 19:08  分类 : 《随便一记》  评论

点击全文阅读


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用)

<template>    <view class="main-dislogue">        <view class="header-suspension">            <view class="record-btn">悬浮</view>        </view>        <view class="content" ref="QAContent">          <scroll-view id="scrollpage" :scroll-top="scrollTop" :scroll-y="true">                 <view v-for="item in dest" :key="item.id" id="msglistview">                  <view class="ask" v-if="item.flag != 1">                      <view class="ask-text">                          <view class="ask-desc" style="word-break: break-all;">                            {{ item.content }}                          </view>                      </view>                      <text class="ask-bulge"></text>                      <view class="ask-avatar">                          <image class="ask-sex" v-if="sex == 1" src="/static/boy.png" fit="contain"></image>                          <image class="ask-sex" v-if="sex == 2" src="/static/girl.png" fit="contain"></image>                      </view>                  </view>                  <view class="answer">                      <view class="answer-avatar">                          <image class="answer-ai" src="/static/ai.png" fit="contain"></image>                      </view>                      <text class="answer-bulge"></text>                      <view class="answer-text">                          <view class="answer-desc" ref="copyAiContent">{{item.ai_content}}</view>                      </view>                  </view>              </view>            </scroll-view>        </view>        <view class="bottom">            <input :cursorSpacing="20" class="bottom-input" name="name" placeholder="请输入" v-model="value"/>            <button class="bottom-button" type="primary" :disabled="isSend" @click="handleSend">发送</button>        </view>    </view></template>
<style>.main-dislogue {  height: calc(100vh - 70px);  background: #f5f5f5;  display: flex;  flex-direction: column;}/* 头部悬浮 */.header-suspension {  width: 100rpx;  height: 300rpx;  /* pointer-events: none; */  z-index: 100;  position: fixed;  right: 10rpx;  bottom: 300rpx;}.head-image {  width: 74rpx;  height: 74rpx;  z-index: 99;  background: #d4d4d4;  border-radius: 50%;  padding: 6rpx;  box-shadow: 0px 2rpx 20rpx rgba(0, 0, 0, 0.5);}.record-btn {  width: 74rpx;  height: 74rpx;  background: #FFFFFF;  border-radius: 50%;  font-size: 26rpx;  text-align: center;  padding: 6rpx;  box-shadow: 0px 2rpx 20rpx rgba(0, 0, 0, 0.5);  color: #4A90E2;  margin-top: 29rpx;}/* 内容 */.content {    padding: 12rpx;    padding-bottom: 100px;    background: #f5f5f5;}/* #scrollpage {} *//* 问 */.ask {    display: flex;    justify-content: flex-end;    width: 100%;    margin-top: 6rpx;}.ask-avatar {    width: 120rpx;    margin-top: 20rpx;}.ask-sex {    width: 100rpx;    height: 100rpx;}.ask-bulge {    position: relative;    top: 41rpx;    right: 23rpx;    display: block;    width: 0;    height: 0;    border: 15rpx solid #38a579;    transform: rotate(45deg);}.ask-text {    z-index: 1;}.ask-desc {    background: #38a579;    border-radius: 13rpx;    padding: 15rpx;    line-height: 58rpx;    margin-top: 27rpx;    white-space: pre-line;    word-break: break-all;    color: #fff;    margin-left: 124rpx;}/* 答 */.answer {    display: flex;    justify-content: flex-start;    margin-top: 6rpx;}.answer-avatar {    width: 120rpx;    margin-top: 20rpx;}.answer-ai {  width: 100rpx;  height: 100rpx;}.answer-bulge {    position: relative;    top: 41rpx;    left: 23rpx;    display: block;    width: 0;    height: 0;    border: 15rpx solid #ffffff;    transform: rotate(45deg);}.answer-text {    z-index: 1;}.answer-desc {    margin-right: 88rpx;    border-radius: 13rpx;    line-height: 58rpx;    background: #fff;    margin-top: 27rpx;    tab-size: 12rpx;    padding: 15rpx;    white-space: pre-wrap;    box-shadow: 0rpx 5rpx 47rpx 0rpx #97979773;}/* 尾部 */.bottom {    border-top: 2rpx solid #CCCCCC;    background: #f5f5f5;    display: flex;    padding: 10rpx;    padding-bottom: 50rpx;    position: fixed;    bottom: 0;    z-index: 99;    width: 100%;}.bottom-input {    flex: 1;    font-size: 35rpx;    border-radius: 10rpx;    background: #FFFFFF;    padding: 17rpx;}.bottom-button {  width: 190rpx;  height: 80rpx;  font-size: 14px;  line-height: 80rpx;  margin-left: 20rpx;  background: #4A90E2 !important;}</style>
<script>import Api from "@/utils/api.js";import base from '@/utils/base.js';const BASE_URL = base.baseUrl;const recorderManager = uni.getRecorderManager()export default {    data() {        return {            sex: "",            birthDate: "",            generateRecordsFlag: false,            dest: [],            dialogue_code: "",            value: "",            isSend: false,            scrollTop: 0,            currentText: "",            isSpeaking: false        }    },    onLoad(option) {        this.sex = option.sex;        this.birthDate = option.birthDate;        this.dialogue_code = option.dialogue_code;    },    onReady() {        let _this = this;        uni.getStorage({            key: 'gpt_h5_dialogue',            success: function (res) {                let list = res.data || "";                if (list.length) {                    this.dest = JSON.parse(list);                    if (this.dest.length >= 2) {                        this.generateRecordsFlag = true;                    }                } else {                    setTimeout(() => {                        _this.handleSend();                    }, 500)                }            }        });    },    methods: {        // 年龄转换        ageCalculation(date) {            var today = new Date();            // 获取出生日期            var birthDate = new Date(date); // 假设出生日期为1990年1月1日            // 计算年龄            var age = today.getFullYear() - birthDate.getFullYear();            var m = today.getMonth(), d = today.getDate();            if (m < birthDate.getMonth()) {                age--;            } else if (m === birthDate.getMonth() && d < birthDate.getDate()) {                age--;            }            return age;        },        // 发送聊天        async handleSend() {            this.preEventSource && this.preEventSource?.close();            if (this.dest.length != 0 && !this.value) {                return;            }            let _this = this;            let { prompt, model } = await Api.getPromptList({ type: 1 });            let sex = this.sex == 1 ? "男" : "女";            let age = this.ageCalculation(this.birthDate);            prompt = prompt.replace('{age}', `${age}岁`).replace('{sex}', `${sex}性`);            let obj = {                ai_content: "...",                chat_model: model,                content: prompt,                create_time: "2024-01-05T06:55:29.000Z",                dialogue_code: this.dialogue_code,                id: 450,                req_time: "2024-01-05T06:55:30.000Z",                res_time: null,                tags: null,                user_code: "00468",                flag: 1            };            const diaObj = {                content: this.value,                ai_content: "...",                chat_model: model,                create_time: new Date(),                dialogue_code: this.dialogue_code,                id: Date.now(),                tags: null,                user_code: "00468",                loading: false,                flag: 2            };            if (this.dest.length == 0) {                // 第一次                this.dest.push(obj);            } else {                this.dest.push(diaObj);            }            let params = {                "dialogue_code": this.dialogue_code,                "content": this.value || this.dest[0].content,                "chat_model": model            }            this.isSend = true;            _this.scrollToBottom();            let ai_content = "", startFlag = false;            this.value = ""; // 置空输入框            // 从这往上可以忽略,这是我业务逻辑,不必关注。重点是uni.request success回调内容            uni.request({                url: `${BASE_URL}/hmgpt/dialogue`,                data: params,                method: "POST",                headers: {                    "Content-Type": 'application/json',                },                success: (res) => {                    let str = JSON.stringify(res.data);                    // 将字符串按"data: ["分割,然后取最后一个部分                      const lastDataSection = str.split("data: [").pop();                    // 截取最后一个JSON对象的部分                      const lastJsonString = lastDataSection.split("]")[0].replace(/\\/g, '');                    // 解析JSON字符串                      const lastJsonObject = JSON.parse(lastJsonString);                    // 获取ai_content的值                      const lastAiContent = lastJsonObject.ai_content;                    console.log(lastAiContent, 'lastAiContent');                    ai_content = lastAiContent;                    if (lastAiContent == "") {                        // 返回空,则默认提示                        ai_content = "目前公司GPU服务器有限,会因为调试需要临时中断出现服务不可用,请稍后重试。";                    }                    _this.dest[_this.dest.length - 1].ai_content = "";                    if (!startFlag) {                        startFlag = true                        startTyping();                    }                }            });            function startTyping() {                let currentIndex = 0;                const typingSpeed = 100; // 打字速度,单位:毫秒                const timer = setInterval(() => {                    _this.dest[_this.dest.length - 1].ai_content += ai_content[currentIndex];                    currentIndex++;                    _this.scrollToBottom();                    if (currentIndex >= ai_content.length) {                        clearInterval(timer);                        _this.isSend = false;                    }                }, typingSpeed);                uni.setStorage({                    key: 'gpt_h5_dialogue',                    data: JSON.stringify(_this.dest),                    success: function () { }                });            }        },        // 滚动至聊天底部        scrollToBottom() {            this.$nextTick(() => {                const query = uni.createSelectorQuery();                query.select('#scrollpage').boundingClientRect();                query.exec(res => {                    this.scrollTop = res[0].height;                    uni.pageScrollTo({                        scrollTop: res[0].height + 170, // 将滚动位置设置为顶部                        duration: 300 // 滚动到顶部的动画时长,单位为毫秒                    });                })            })        }    },}</script>

效果图

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

 

 

 


点击全文阅读


本文链接:http://zhangshiyu.com/post/99308.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1