更新日志:(我最开始是使用非流式接口实现的,后续业务改造使用流式接口,重点的代码讲解都在下边哈~~)
  1. 关于打印机效果后端同样可以实现的逻辑说明:(2024/4/11)

      打印机效果使用前端的框架实现时,其实颇有局限,而且逻辑有点复杂,可以百度的也不是很多,最好是交给后端通同学来做,方案是后端控制接口请求的请求头content-type为text/event-stream;这是SSE热更新方案,相当于建立了一个长连接,前端再对该event事件流返回的数据进行判断和监听处理;简单理解就是可以实现后端一直给前端同学返回数据,我们再进行拼接即可,这样可以直接实现打印机效果啦,

   2. 补丁上述使用流式接口实现打印机效果讲解:(2024/5/10)

   3. 补充实退出时对会话进行保存(下次进入可以看到之前的对话):(2024/6/7)
 
   4. 
      1)更新更为规范且简便的流式数据获取方法;
      2)大模型接入deepseek,处理渲染流式数据返回的think标签
      3)更新会话dom滚动方法
      4)删除非流式打印机实现步骤,仅保留插件地址参考
      5)上述内容于2025/2/18更新


1.与AI问答机器人对话模型效果展示

vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。

2-流式重点代码解析(流式解析,deepSeek的think标签处理,实时dom滚动与取消)

      
//发送用户提问的内容时
async sendMsg() {
        //可以根据自己业务情况做一些请求前的的中止提示,比如非空等
        //data中的数据不再赘述
        this.chunkRef = null //重置临时数据
        //重置处理deepSeek返回think标签的变量
        this.haveThinkStart = false
        this.haveThinkEnd = false
        this.controller = new AbortController() // 创建AbortController实例,以便中止请求
        //使用fetch 实现流式请求
        const response = await fetch('url地址', {
          method: 'POST',
          body: JSON.stringify('请求体对象数据'),
          timeout: 0,
          dataType: 'text/event-stream',
          headers: {
            'Content-Type': 'application/json',
          },
          signal: this.controller.signal,
        })
        if (response.ok) {
          const reader = response.body.getReader()
          const decoder = new TextDecoder() //解码器
          while (1) {
            const { done, value } = await reader.read()
            if (done) {
              //this.controller?.abort() //这个可以在停止生成功能中使用,意为中断请求,这里不要加,若你有停止生成功能,可在停止生成回调中如此操作。
              //do something
                  //对于有think开始标签而无结束的我们要做处理
                  if (!this.haveThinkEnd && this.haveThinkStart) {
                    let length = this.chatList.length - 1
                    let newContent = this.chatList[length].think
                    this.chatList[length].think = ''
                    this.chatList[length].content = newContent
                  }
              break
            }
            //解码器解析后的单条数据
            const text = decoder.decode(value, { stream: true })
            //防止后端返回数据不全,创建临时数据进行比对。非常重要!
            const chunk = this.handleChunkData(text)
           if (!this.chunkRef) {
              for (const item of chunk.split('\n')) {
                // 移除前缀 'data: ' 或 'data: [DONE]',这里正则有点问题,只能去除data:
                let _item = item.replace(/^(data: [DONE])|^(data:)/, '')
                try {
                  if (_item && _item != '[DONE]') {
                   let index = this.chatList.length - 1
                      content = JSON.parse(_item)?.choices[0]?.delta.content
                      //处理deepSeek的think标签数据,markDown-it解析有问题,手动变量保存
                      if (content == '<think>') {
                        this.haveThinkStart = true
                      } else if (content == '</think>') {
                        this.haveThinkEnd = true
                      }
                      if (this.haveThinkEnd) {
                        this.haveThinkStart = false
                      }
                      if (this.haveThinkStart) {
                        this.chatList[index].think += content
                      } else {
                        this.chatList[index].content += content
                      }
                   
                  }
                } catch (error) {
                  // 处理 JSON 解析错误
                  console.error('解析数据时出错:', error)
                }
              }
            }
          }
        }
      },
     
      //暂存处理不完整的json数据
      handleChunkData(chunk) {
        chunk = chunk.trim()
        if (this.chunkRef) {
          chunk = this.chunkRef + chunk
          this.chunkRef = null
        }
        if (chunk.includes('[DONE]')) {
          return chunk
        }
        if (chunk[chunk.length - 1] !== '}') {
          this.chunkRef = chunk
        }
        return chunk
      },


/**----分割线,下面是保存历史会话方法,用户下次进入可以实现查看历史对话----**/


mounted(){

      //监听浏览器刷新事件 &&注意!亲测不包含浏览器回退
      window.addEventListener('beforeunload', this.onBeforeUnload)
},
beforeDestroy() {

      // 移除事件监听器
      window.removeEventListener('beforeunload', this.onBeforeUnload)
     
    },
methods:{
     onBeforeUnload(){

     //此方法中调用后端给的保存会话接口即可
},
 async beforeRouteLeave(to, from, next) {

   //await同步调用接口保存会话

   next()
}

/**----分割线,如何Dom实现实时跟随会话滚动,但在上滑后停止滚动----**/

//不再使用定时器实现,使用感官很差
//给dom绑定滚动事件,设置阙值,设置滚动开关(控制这个开关就行了,很重要),然后大模型再流式输出和输出完毕后都调用滚动到底部方法

      chatBoxScroll(e) {
        const scrollTop = e.target.scrollTop //滚动条距离滚动内容顶部的垂直距离
        const scrollHeight = e.target.scrollHeight //元素内容的总高度,包括那些在视口中不可见的部分
        const clientHeight = e.target.clientHeight //元素节点的可见高度,包括内边距(padding),但不包括水平滚动条、边框和外边距(margin)的高度。
        const threshold = 10 // 阈值,可以根据需要调整,以避免在非常接近底部时触发加载
        if (!scrollTop || !scrollHeight || !clientHeight) return
        //触底
        if (scrollTop + clientHeight >= scrollHeight - threshold) {
          //  alert('我执行设置了true')
          this.setScrollStatus(true)
        } else {
          this.$nextTick(() => {
            //alert('我执行设置了false')
            this.setScrollStatus(false)
          })
        }
      },

      //设置dom滚动开关
      setScrollStatus(value) {
        this.isScroll = value
      },

      //dom渲染更新完成后页面滚动到底,大模型输出时和结束输出调用
      scrollTop() {
        let domScrollHeight = this.$refs.list.scrollHeight
        if (domScrollHeight && this.isScroll) {
          this.$nextTick(() => {
            this.$refs.list.scrollTop = domScrollHeight
          })
        }
      },

3-涉及文档:

markdown-it | markdown-it 中文文档    // 流式markdown数据解析插件

Typed.js - Type your heart out    //非流式想实现打印机效果的同学用这个插件 

最后补充一下:
      对于非流式的i机器人的回答,是由接口提供的模板数据,那么就涉及到dom渲染;如果要使用打印机效果,就不可以使用v-html!原因是v-htm会将模板中的“<,/>”打印出来!

所以我们就要换一种方式实现了~ Typed.js插件,给每一个dom绑定唯一id,具体参考typed.js文档。这里不再赘述,非流式用的人少~

推荐内容
阅读全文
AI总结
GitHub 加速计划 / vu / vue
97
16
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:8 个月前 )
9e887079 [skip ci] 6 个月前
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 10 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐