vue2调用大模型api流式输出,打字机效果展示

一个简单的demo,实现了1、调用服务端大模型接口流式输出;2、添加marked效果展示;3、添加打字机效果展示

<!-- Vue2组件 -->
<template>
  <div class="chat-container">
    <div class="output-box" ref="outputBox" v-html="showDisplayText"></div>

    <div class="input-group">
      <input
        v-model="inputQuestion"
        placeholder="输入你的问题"
        @keyup.enter="startStream"
      />
      <button @click="startStream" :disabled="loading">
        {{ loading ? '生成中...' : '开始生成' }}
      </button>
      <button @click="stopStream" v-show="loading">停止</button>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import { marked } from '@/static/js/marked.esm.min.js'

export default {
  data() {
    return {
      inputQuestion: '地球上有多少个国家?',
      loading: false,
      cancelToken: null,
      receivedContent: '',  // 已经接收的全部字符

      responseText: '',       // 新接收的字符
      displayText: '',        // 显示的字符
      currentIndex: 0,        // 当前打印位置
      animationFrameId: null, // 动画帧ID
      lastUpdateTime: 0,      // 上次更新时间
      displayTextRate: 30, // 打印机效果,多少毫秒一个字
    };
  },
  computed: {
    showDisplayText () {
      // return this.displayText;
      let tmp = this.displayText.trim();
      tmp = this.doMarked(tmp);
      return tmp;
    }
  },
  created() {
    this.rendererMD = new marked.Renderer();
    marked.setOptions({
      renderer: this.rendererMD,
      gfm: true,
      tables: true,
      breaks: true, // false
      pedantic: false,
      sanitize: false,
      smartLists: true,
      smartypants: false
    });//marked基本设置
  },
  methods: {
    async startStream() {
      this.resetState();
      this.loading = true;
      this.displayText = '';

      try {
        // 创建取消令牌
        const CancelToken = axios.CancelToken
        this.cancelToken = CancelToken.source()

        const response = await axios({
          method: 'post',
          url: '', // 用你的服务端流式输出的地址
          responseType: 'stream', // 重要:声明响应类型为流
          params: {msg: this.inputQuestion},
          cancelToken: this.cancelToken.token,
          onDownloadProgress: (progressEvent) => {
            const chunk = progressEvent.currentTarget.responseText;
            // console.log(chunk);
            // 处理增量内容
            if (chunk.length > this.receivedContent.length) {
              const newContent = chunk.slice(this.receivedContent.length); // 只取新的
              this.receivedContent += newContent;
              this.handleStreamChunk(newContent); // 从新数据中解析添加
              this.scrollToBottom();
            }
          }
        });

        this.loading = false;
      } catch (error) {
        if (!this.loading) return;
        this.displayText += '\n\n[请求已取消]';
        this.loading = false;
      }
    },
    handleStreamChunk(chunk) {
      // 这里需要根据你的API响应格式进行解析
      // 示例处理(假设API返回JSON格式的流数据):
      try {
        const lines = chunk.split('\n').filter(line => line.trim())

        lines.forEach(line => {
          const data = line.replace('data: ', '')
          const parsed = JSON.parse(data)
            // 这里根据自己的消息内容替换,比如这里我的每一行数据是
            // data: {"event": "message", "conversation_id": "会话ID", "task_id": "任务ID", "answer": "一个token内容"}
          if (parsed.event == 'message') {
            this.responseText += parsed.answer || ''

            this.startTypingEffect();
          }
        })
      } catch (e) {
        console.error('解析错误:', e)
      }
    },
    stopStream() {
      if (this.cancelToken) {
        this.cancelToken.cancel('用户取消请求')
      }
      // 这里还要请求你的大模型api的停止会话接口,通常是要传任务ID参数
      this.loading = false;
    },
    doMarked(str) {
      return marked(str);
    },
    scrollToBottom() {
      this.$nextTick(() => {
        const box = this.$refs.outputBox;
        box.scrollTop = box.scrollHeight;
      });
    },
    startTypingEffect() { //  打字机效果展示
      if (this.animationFrameId) return

      const animate = (timestamp) => {
        if (!this.loading && this.currentIndex >= this.responseText.length) {
          this.cancelAnimation()
          return
        }

        // 控制打印速度(30ms/字符)
        if (timestamp - this.lastUpdateTime > this.displayTextRate) {
          if (this.currentIndex < this.responseText.length) {
            this.displayText += this.responseText[this.currentIndex]
            this.currentIndex++
            this.lastUpdateTime = timestamp
            // 自动滚动到最新内容
            this.scrollToBottom();
          }
        }
        this.animationFrameId = requestAnimationFrame(animate)
      }
      this.animationFrameId = requestAnimationFrame(animate)
    },

    cancelAnimation() {
      if (this.animationFrameId) {
        cancelAnimationFrame(this.animationFrameId)
        this.animationFrameId = null
      }
    },
    resetState() {
      this.receivedContent = '';
      this.responseText = ''
      this.displayText = ''
      this.currentIndex = 0
      this.lastUpdateTime = 0
      this.cancelAnimation()
    }
  },
  beforeDestroy() {
    this.cancelAnimation()
  }
};
</script>

<style scoped>
.chat-container {
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
}

.output-box {
  height: 400px;
  border: 1px solid #ddd;
  padding: 15px;
  margin-bottom: 20px;
  overflow-y: auto;
  white-space: pre-wrap;
  line-height: 1.6;
}

.input-group {
  display: flex;
  gap: 10px;
}

input {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
}

button {
  padding: 8px 15px;
  background: #42b983;
  color: white;
  border: none;
  cursor: pointer;
}
button:disabled {
  background: #ccc;
}
/* 一些marked替换的html样式调整 */
.output-box >>> p {
  margin-block-end: 0.4rem;
}
.output-box >>> ul {
  display: inline-block;
  list-style: disc;
  padding-left: 2rem;
  margin-block-start:0;
  margin-block-end:0;
  padding-inline-start: 0;
}
.output-box >>> li {
  margin-block-end: 0.4rem;
}
</style>

注意:1. 下载或者通过npm安装marked,因为各种chatbox之类的大模型调用都自带了marked展示效果,所以必须带上。

2. 大模型流式输出的api地址,自行调整,可以直接调用服务端,如我另一篇文章的php调用大模型为服务端,也可以是直接大模型api。

3. 大模型流式内容解析那里根据自己的api消息内容调整。

### 实现 Vue2 中的文字流式输出渲染 在 Vue2 项目中实现文字流式输出渲染可以通过多种方法完成,其中一种有效的方式是通过模拟服务器端事件(SSE),即 Event Stream 来达到这一目的。这种方式允许客户端持续接收来自服务端的数据更新并即时反映到页面上。 对于纯前端实现类似 ChatGPT 的文字流式输出效果,在不依赖于真实的 SSE 或 WebSocket 连接的情况下,可以采用定时器或者 Promise 配合 `v-html` 指令来逐步追加文本内容[^2]。 下面给出一个简单的例子说明如何在 Vue 组件内部创建这样的功能: ```javascript new Vue({ el: '#app', data() { return { zdxmycText: '' } }, methods: { async simulateStreamOutput(text) { let index = 0; while (index < text.length) { await new Promise(resolve => setTimeout(() => { this.zdxmycText += text.charAt(index++); resolve(); }, Math.random() * 50)); // 控制字符间延迟时间 } } }, mounted() { const sampleText = '这是一个用于演示的样本文本'; this.simulateStreamOutput(sampleText); } }) ``` 此代码片段展示了如何定义一个名为 `simulateStreamOutput` 的异步函数,该函数接受一段字符串作为参数,并按照一定的时间间隔逐个字符地将其添加至组件的状态变量 `zdxmycText` 中。每次调用都会触发视图重新渲染,从而实现了类似于打字机效果。 为了更好地控制样式和用户体验,可以在 HTML 结构中加入适当的 CSS 和容器设置,如下所示: ```html <div id="app"> <div class="fx-content" style="height: 300px; overflow-y: auto;" v-html="zdxmycText"></div> </div> ``` 这里使用了 `v-html` 指令绑定到了 `zdxmycText` 属性,使得每当其值发生变化时都能立即体现在界面上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值