0


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

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

效果图


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


本文转载自: https://blog.csdn.net/zty_989443/article/details/136190842
版权归原作者 猪奇迹 所有, 如有侵权,请联系我们删除。

“uniapp开发小程序实现AI聊天打字机功能”的评论:

还没有评论