微信小程序-接入sse数据流并实现打字机效果( ChatGPT )

从流中获取的数据格式如下

小程序调用SSE接口

javascript 复制代码
const requestTask = wx.request({
      url: `xxx`, // 需要请求的接口地址
      enableChunked: true, // enableChunked必须为true
      method: "GET",
      timeout: '120000',
      success(res) {
        console.log(res.data)
      },
      fail: function (error) {
        // 请求失败的操作
        console.error(error);
      },
      complete: function () {
        // 请求完成的操作,无论成功或失败都会执行
        console.log('请求完成', str);
      }
    })
    // 监听服务端返回的数据
    requestTask.onChunkReceived(res => {
      console.log( res, res.data);
    })

我这边接收到的数据类型为Uint8Array,需要处理成text文本(如上图)

javascript 复制代码
 // 监听服务端返回的数据
    requestTask.onChunkReceived(res => {
      console.log( res, res.data);
      // Uint8Array转为text格式
      let arrayBuffer = res.data;
      let decoder = new TextDecoder('utf-8');
      let text = decoder.decode(arrayBuffer);
      //正则匹配上所有event:data后面的文字
      const eventRegex = /event:data\ndata:"data:(.*?)"/g;
      const eventRegexErr = /event:600\ndata:"(.*?)"/g;
      let matches = [];
      let match;
      if (text.indexOf('600') != -1) {//如果获取响应失败
        while ((match = eventRegexErr.exec(text)) !== null) {
          wx.showToast({
            title: match[1],
          })
          matches.push(match[1]);
        }
        str = str + matches.join('')
      } else {//如果获取响应成功
        while ((match = eventRegex.exec(text)) !== null) {
          matches.push(match[1]);
        }
        //处理成字符串
        str = str + matches.join('')
        console.log(text, str);
      }
    })

使对话有打字机效果

参考自:小程序实现 ChatGPT 聊天打字兼自动滚动效果

javascript 复制代码
 handleRequestResolve(result) {
    this.setData({
      currentContent: ''
    })
    const contentCharArr = result.trim().split("")
    this.showText(0, contentCharArr);
  },
  showText(key = 0, value) {
    /* 所有内容展示完成 */
    if (key >= value.length) {
      // wx.vibrateShort()
      //判断字是否展示完
       this.setData({
        isShowFinish: true
      })
      return;
    }
    /* 渲染回话内容 */
    this.setData({
      currentContent: this.data.currentContent + value[key],
    })
    setTimeout(() => {
      /* 递归渲染内容 */
      this.showText(key + 1, value);
    }, 50);
  },

对话滚动到可视区域内

javascript 复制代码
 handleScollTop() {
    return new Promise((resolve) => {
      const query = wx.createSelectorQuery()
      query.select('.page-content').boundingClientRect()
      query.select('.scroll-view-content').boundingClientRect()
      query.exec((res) => {
        const scrollViewHeight = res[0].height
        const scrollContentHeight = res[1].height
        if (scrollContentHeight > (scrollViewHeight - 200)) {
          const scrollTop = scrollContentHeight - scrollViewHeight + 200
          this.setData({
            scrollTop
          }, () => {
            resolve()
          })
        } else {
          resolve()
        }
      })
    })
  },
   showText(key = 0, value) {
    /* 所有内容展示完成 */
    if (key >= value.length) {
      // wx.vibrateShort()
      this.setData({
        isShowFinish: true
      })
      return;
    }
    /* 渲染回话内容 */
    this.setData({
      currentContent: this.data.currentContent + value[key],
    }, () => {
      this.handleScollTop().then(() => {
        setTimeout(() => {
          this.showText(key + 1, value);
        }, 20);
      })
    })
  },

完整代码

.wxml
javascript 复制代码
<scroll-view scroll-y scroll-top="{{scrollTop}}" wx:else class="page-content {{isFirst ? '' : 'page-content-bg'}}">
    <view class="scroll-view-content">
      <view wx:for="{{talkArr}}" wx:key="index" class="talk-box1">
        <view class="talk-box-question" wx:if="{{item.isAnswer=='0'}}">
          <view class="left">
            <text class="left-content">{{item.content}}</text>
          </view>
          <image class="right" src="../images/user-icon.png" mode="aspectFill" />
        </view>
        <view class="talk-box-reply" wx:else>
          <image class="left" src="../images/ai-icon.png" mode="aspectFill" />
          <view class="right">
            <view class="right-content">
              <view wx:if="{{(index!=talkArr.length-1)}}">{{item.content}}</view>
              <view wx:else>
                <view wx:if="{{loading}}">
                  <image class="loading" src="../images/loading-1.png" mode="aspectFill" />
                </view>
                <view wx:else>
                  {{currentContent}}
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
  </scroll-view>
.wxss
css 复制代码
.page-content {
  width: 100%;
  margin-top: 48rpx;
  padding-top: 150rpx;
}

.page-content-bg {
  background: #F5F6F7;
  height: 75%;
  padding-bottom: 280rpx;
  overflow: scroll;
  padding-top: 0;
}

.scroll-view-content {
  padding-top: 50rpx;
}


.talk-box {
  display: flex;
}

.talk-box1 {
  width: 90%;
  margin: 0 auto;
}

.talk-box .left {
  width: 80rpx;
  height: 80rpx;
}

.talk-box .right {
  margin-left: 30rpx;
  flex: 1;

}

.talk-item {
  height: 92rpx;
  background: #F6FFF9;
  border-radius: 0rpx 20rpx 20rpx 20rpx;
  font-family: PingFang SC, PingFang SC;
  font-weight: 500;
  font-size: 28rpx;
  color: rgba(51, 51, 51, 0.9);
  text-align: left;
  display: flex;
  align-items: center;
  padding: 0 38rpx;
}

.talk-box-question,
.talk-box-reply {
  width: 100%;
  display: flex;
  margin-bottom: 32rpx;
}

.talk-box-question .left {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.left-content {
  background: linear-gradient(273deg, #44BE35 0%, #6ECB63 100%);
  box-shadow: 0rpx 2rpx 8rpx 0rpx rgba(0, 0, 0, 0.05);
  border-radius: 24rpx 0rpx 24rpx 24rpx;
  padding: 24rpx;
  font-family: PingFang SC, PingFang SC;
  font-weight: 400;
  font-size: 28rpx;
  color: #FFFFFF;
  line-height: 44rpx;
  text-align: left;
}

.talk-box-question .right {
  margin-left: 30rpx;
  width: 80rpx;
  height: 80rpx;
}

.talk-box-reply .left {
  width: 80rpx;
  height: 80rpx;
}

.talk-box-reply .right {
  margin-left: 30rpx;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: flex-start;
}

.right-content {
  background: #FFFFFF;
  box-shadow: 0rpx 2rpx 8rpx 0rpx rgba(0, 0, 0, 0.05);
  border-radius: 0rpx 24rpx 24rpx 24rpx;
  border: 2rpx solid #6ECB63;
  padding: 24rpx;
  font-family: PingFang SC, PingFang SC;
  font-weight: 400;
  font-size: 28rpx;
  color: rgba(0, 0, 0, 0.9);
  line-height: 46rpx;
  text-align: left;
}
.js
javascript 复制代码
 data: {
    isShowFinish: false,
    scrollTop: '',
    currentContent: '',
    loading: false,
    talkArr: []
  },

 getDataStream(data) {
    let str = ''
    let that = this
    this.setData({
      loading: true,
    })
    // 基础库为2.33.0
    const requestTask = wx.request({
      enableChunked: true, // 开启分片模式
      url: `xxx`, // 需要请求的接口地址
      enableChunked: true, // enableChunked必须为true
      method: "GET",
      responseType: "arraybuffer",
      timeout: '120000',
      success(res) {},
      fail: function (error) {
        // 请求失败的操作
        console.error(error);
      },
      complete: function () {
        // 请求完成的操作,无论成功或失败都会执行
        that.handleRequestResolve(str)
        let index = that.data.talkArr.length - 1
        let answerContent = `talkArr[${index}].content`
        that.setData({
          [answerContent]: str,
          loading: false
        })
      }
    })
    // 监听服务端返回的数据
    requestTask.onChunkReceived(res => {
      // Uint8Array转为text格式
      let arrayBuffer = res.data;
      let decoder = new TextDecoder('utf-8');
      let text = decoder.decode(arrayBuffer);
      //正则匹配上所有event:data后面的文字
      const eventRegex = /event:data\ndata:"data:(.*?)"/g;
      const eventRegexErr = /event:600\ndata:"(.*?)"/g;
      let matches = [];
      let match;
      if (text.indexOf('600') != -1) { //如果获取响应失败
        while ((match = eventRegexErr.exec(text)) !== null) {
          wx.showToast({
            title: match[1],
            icon: 'none'
          })
          matches.push(match[1]);
        }
        str = str + matches.join('')
      } else { //如果获取响应成功
        while ((match = eventRegex.exec(text)) !== null) {
          matches.push(match[1]);
        }
        //处理成字符串
        str = str + matches.join('')
      }

    })
    requestTask.offChunkReceived(res => {})
  },
  handleScollTop() {
    return new Promise((resolve) => {
      const query = wx.createSelectorQuery()
      query.select('.page-content').boundingClientRect()
      query.select('.scroll-view-content').boundingClientRect()
      query.exec((res) => {
        const scrollViewHeight = res[0].height
        const scrollContentHeight = res[1].height
        if (scrollContentHeight > (scrollViewHeight - 200)) {
          const scrollTop = scrollContentHeight - scrollViewHeight + 200
          this.setData({
            scrollTop
          }, () => {
            resolve()
          })
        } else {
          resolve()
        }
      })
    })
  },
  
  handleRequestResolve(result) {
    this.setData({
      currentContent: ''
    })
    const contentCharArr = result.trim().split("")
    this.setData({
      isShowFinish: false
    })
    this.showText(0, contentCharArr);
  },
  showText(key = 0, value) {
    /* 所有内容展示完成 */
    if (key >= value.length) {
      // wx.vibrateShort()
      this.setData({
        isShowFinish: true
      })
      return;
    }
    /* 渲染回话内容 */
    this.setData({
      currentContent: this.data.currentContent + value[key],
    }, () => {
      this.handleScollTop().then(() => {
        setTimeout(() => {
          this.showText(key + 1, value);
        }, 20);
      })
    })
  },
相关推荐
郭wes代码10 小时前
Cmd命令大全(万字详细版)
python·算法·小程序
.生产的驴15 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
汤姆yu20 小时前
基于微信小程序的乡村旅游系统
微信小程序·旅游·乡村旅游
计算机徐师兄20 小时前
基于TP5框架的家具购物小程序的设计与实现【附源码、文档】
小程序·php·家具购物小程序·家具购物微信小程序·家具购物
曲辒净21 小时前
微信小程序实现二维码海报保存分享功能
微信小程序·小程序
朽木成才1 天前
小程序快速实现大模型聊天机器人
小程序·机器人
peachSoda71 天前
随手记:小程序使用uni.createVideoContext视频无法触发播放
小程序
何极光1 天前
uniapp小程序样式穿透
前端·小程序·uni-app
小墨&晓末1 天前
【PythonGui实战】自动摇号小程序
python·算法·小程序·系统安全
oil欧哟1 天前
🤔认真投入一个月做的小程序,能做成什么样子?有人用吗?
前端·vue.js·微信小程序