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

本文介绍了使用VueCLI创建uni-app项目的经历,遇到的npm权限问题解决方案,以及如何在uni-app中实现聊天功能,包括处理流式接口数据和模拟打字机效果的代码示例。
摘要由CSDN通过智能技术生成

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>

效果图

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

 

 

 

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值