Vue2+marked.js实现AI流式输出的项目实践

 更新时间:2025年04月02日 11:59:50   作者:夜阑卧听风吹雨,铁马冰河入梦来  
本文主要介绍了Vue2+marked.js实现AI流式输出的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

1、实现效果

AI流式输出

2、实现流程

1、页面内容

<template>
  <div class="app-container">
    <!-- 聊天界面 -->
    <div class="chat-container">
      <!-- 消息展示区域 -->
      <div class="chat-box" ref="chatBox">
        <div
          v-for="message in messages"
          :key="message.id"
          class="message"
          :class="message.from === 'user' ? 'user-message' : 'ai-message'"
        >
          <div v-if="!message.content" class="chat-message waiting">

            <!-- 加载动画,例如一个旋转的图标 -->
            <div class="loading-spinner"></div>
            容我思考片刻 !
          </div>
          <p v-else v-html="markMessage(message.content)"></p>
        </div>
      </div>
      <!-- 输入框与发送按钮 -->
      <div class="input-container">
        <el-input
          v-model="userInput"
          placeholder="请输入消息..."
          clearable
          @keyup.enter.native="sendMessage"
          class="chat-input"
        />
        <el-button type="primary" icon="el-icon-send" @click="sendMessage" class="send-button">发送</el-button>
      </div>
    </div>
  </div>
</template>

循环展示消息内容,根据是用户发送消息和AI返回消息展示不同的样式

添加了一个等待动画

2、js部分与样式

<script>
// api部分大家根据自己的前端框架自己封装即可,分别调用后端的两个controller
import {sendMessage, sendMessage1} from "@/api/ai";
import { Marked } from 'marked'
import { markedHighlight } from "marked-highlight";
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
import "highlight.js/styles/paraiso-light.css";

export default {
  data() {
    return {
      messages: [], // 消息记录
      userInput: "你好", // 用户输入,默认一开始发一个“你好”
      pollingActive: false, // 是否正在长轮询
      isEnd: false, // 标记是否结束轮询
      currentAiMessageId: null, // 当前正在回复的 AI 消息的 ID
      userMsgData: {}, // 用户消息数据
    };
  },
  async mounted() {
    this.sendMessage();
  },
  // computed: {
  //   newMessages() {
  //     this.messages.forEach(message=>{
  //       message.content=this.markMessage(message.content)
  //       console.log(message.content)
  //     })
  //     return this.messages
  //   }
  // },
  methods: {
    markMessage(message) {
      message=message.replaceAll('\\n','\n')
      console.log('调用前'+message)
      const marked = new Marked(
        markedHighlight({
          pedantic: false,
          gfm: true, // 开启gfm
          breaks: true,
          smartLists: true,
          xhtml: true,
          async: false, // 如果是异步的,可以设置为 true
          langPrefix: 'hljs language-', // 可选:用于给代码块添加前缀
          emptyLangClass: 'no-lang', // 没有指定语言的代码块会添加这个类名
          highlight: (code) => {
                 return hljs.highlightAuto(code).value
          }
        })
      );
     let  markedMessage = marked.parse(message)
      console.log('调用了'+markedMessage)
      return markedMessage
    },
    sendMessage() {
      if (!this.userInput.trim()) return;

      // 添加用户消息
      this.messages.push({
        id: Date.now(),
        content: this.userInput,
        from: "user",
      });

      this.userMsgData.content = this.markMessage(this.userInput);
      // send(this.userMsgData);

      // 清空输入框
      this.userInput = "";

      // 添加 AI 回复占位
      let newAiMessage = {
        id: Date.now() + 1,
        content: "",
        from: "ai",
      };
      this.messages.push(newAiMessage);

      this.currentAiMessageId = newAiMessage.id;

      // 启动轮询
      // if (this.isEnd || !this.pollingActive) {
      //   this.isEnd = false;
      //   this.pollingActive = true;
      //   this.polling();
      // }
      this.polling()
    },
    async polling() {
      try {
        // 给定的字符串
        const response = await sendMessage1(this.userMsgData.content);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const reader = response.body.getReader();
        const decoder = new TextDecoder('utf-8');
        let buffer = ''; // 用于累积部分消息
        while (true) {
          const {done, value} = await reader.read();
          if (done) {
            this.isEnd = true;
            this.pollingActive = false;
            break
          }
          buffer = decoder.decode(value, {stream: true});
          this.processServerSentEvent(buffer);

        }
        // 流结束时处理可能剩余的部分消息
        // this.processServerSentEvent(buffer);
      } catch (e) {
        console.log(e.toString())
      }
    },
    processServerSentEvent(eventData, isFinal = false) {
      const lines = eventData.split('\n');
      let currentMessage = ''
      lines.forEach(line => {
        if (line.startsWith('data:')) {
          // 提取data字段的值(去掉前面的'data: ')
          let a = line.split(':')
          currentMessage += a[1];
        } else {
          currentMessage+=line.trim()
        }
      })
      this.addNewMessage(currentMessage)
    },
    addNewMessage(data) {
      if (data) {
        try {
          let newMessageContent = data;
          // 通过消息id获取目前的AI输入位置
          const aiMessage = this.messages.find(
            (msg) => msg.id === this.currentAiMessageId
          );
          // newMessageContent = this.markMessage(newMessageContent)
          if (aiMessage) {
            aiMessage.content = `${aiMessage.content}${newMessageContent}`;
          }
          this.scrollToBottom()
        } catch (error) {
          console.error('Failed to parse JSON:', error);
        }
      }
    },
    scrollToBottom() {
      const chatBox = this.$refs.chatBox;
      chatBox.scrollTop = chatBox.scrollHeight;
    }
  }
};
</script>

<style scoped>
.app-container {
  display: flex;
  height: 90vh;
  background-color: #f3f4f6;
  font-family: "Arial", sans-serif;
}

/* 聊天容器 */
.chat-container {
  flex: 1; /* 右侧占比 */
  display: flex;
  flex-direction: column;
  border-left: 1px solid #ddd;
  background-color: #fff;
  overflow: hidden;
}

.chat-box {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  background-color: #fafafa;
  display: flex;
  flex-direction: column;
}

/* 通用消息样式 */
.message {
  margin: 10px 0;
  padding: 10px;
  max-width: 70%;
  word-wrap: break-word;
  border-radius: 8px;
}

/* 用户消息:右对齐 */
.user-message {
  align-self: flex-end;
  background-color: #e0f7fa;
  text-align: left;
}

/* AI 消息:左对齐 */
.ai-message {
  align-self: flex-start;
  background-color: #f1f1f1;
  text-align: left;
}

/* 输入框和发送按钮 */
.input-container {
  display: flex;
  padding: 10px;
  border-top: 1px solid #e0e0e0;
  background-color: #f9f9f9;
}

.chat-input {
  flex: 1;
  margin-right: 10px;
}

.send-button {
  flex-shrink: 0;
}
/* 加载指示器的样式 */
.loading-spinner {
  border: 4px solid rgba(0, 0, 0, 0.1);
  border-left-color: #4caf50; /* 可以根据需要调整颜色 */
  border-radius: 50%;
  width: 20px;
  height: 20px;
  animation: spin 1s linear infinite;
  margin-right: 10px; /* 与文本之间留出一些空间 */
}

/* 定义旋转动画 */
@keyframes spin {
  to { transform: rotate(360deg); }
}
/* 聊天消息的基本样式 */
.chat-message {
  padding: 10px;
  border-radius: 8px;
  margin: 5px 0;
  position: relative;
  display: flex;
  align-items: center;
}

/* 正在等待的消息样式 */
.waiting {
  color: #777; /* 设置文本颜色 */
  background-color: #f0f0f0; /* 设置背景颜色 */
}
</style>

发送消息后,生成用户消息和AI回复占位

给AI接口发送消息,发送消息后,获取到响应,然后使用reader.read方法读取内容。

给AI接口发送消息

export async function sendMessage1(message) {
  console.log(message+"----")
  try {
    const response = await fetch( '你的接口路径', {
      method: 'POST',
      body:message
    })
    return response
  } catch (error) {
    console.error('请求失败:', error);
  }
}

因为是流式响应,所以连接不是一次响应就直接断开的,使用while(true)循环不断从中获取到响应内容,并将响应的内容解码。

AI接口响应 content-type: text/event-stream;charset=utf-8是这样的,不是平常用的application/json,这表明响应体是一个服务器发送事件(Server-Sent Events,简称SSE)流。SSE 允许服务器向客户端(通常是浏览器)推送实时更新,而无需客户端轮询服务器。

async polling() {
      try {
        // 给定的字符串
        const response = await sendMessage1(this.userMsgData.content);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const reader = response.body.getReader();
        const decoder = new TextDecoder('utf-8');
        let buffer = ''; // 用于累积部分消息
        while (true) {
          const {done, value} = await reader.read();
          if (done) {
            this.isEnd = true;
            this.pollingActive = false;
            break
          }
          buffer = decoder.decode(value, {stream: true});
          this.processServerSentEvent(buffer);

        }
        // 流结束时处理可能剩余的部分消息
        // this.processServerSentEvent(buffer);
      } catch (e) {
        console.log(e.toString())
      }

将解码后内容经过处理后添加到messages数组中。

 processServerSentEvent(eventData, isFinal = false) {
      console.log('收到数据:  '+eventData)
      const lines = eventData.split('\n');
      let currentMessage = ''
      lines.forEach(line => {
        if (line.startsWith('data:')) {
          // 提取data字段的值(去掉前面的'data: ')
          let a = line.split(':')
          currentMessage += a[1];
        } else {
          currentMessage+=line.trim()
        }
      })
      this.addNewMessage(currentMessage)
    },

3、marked.js和highlight.js

marked.js 是一个用于将 Markdown 文本转换为 HTML 的 JavaScript 库,而 highlight.js 是一个用于语法高亮的库,它可以与 marked.js 一起使用来高亮 Markdown 中的代码块

安装marked.js和hightlight.js然后导入

npm install marked
npm install highlight.js
import { Marked } from 'marked'
import { markedHighlight } from "marked-highlight";
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
import "highlight.js/styles/paraiso-light.css";
markMessage(message) {
      message=message.replaceAll('\\n','\n')
      // console.log('调用前'+message)
      const marked = new Marked(
        markedHighlight({
          pedantic: false,
          gfm: true, // 开启gfm
          breaks: true,
          smartLists: true,
          xhtml: true,
          async: false, // 如果是异步的,可以设置为 true
          langPrefix: 'hljs language-', // 可选:用于给代码块添加前缀
          emptyLangClass: 'no-lang', // 没有指定语言的代码块会添加这个类名
          highlight: (code) => {
                 return hljs.highlightAuto(code).value
          }
        })
      );
     let  markedMessage = marked.parse(message)
      // console.log('调用了'+markedMessage)
      return markedMessage
    },

message就是markdown格式的文本内容

4、添加等待效果

当消息内容为空时,显示等待动画,不为空显示消息内容

<div v-if="!message.content" class="chat-message waiting">

            <!-- 加载动画,例如一个旋转的图标 -->
            <div class="loading-spinner"></div>
            容我思考片刻 !
          </div>
          <p v-else v-html="markMessage(message.content)"></p>

等待样式

/* 加载指示器的样式 */
.loading-spinner {
  border: 4px solid rgba(0, 0, 0, 0.1);
  border-left-color: #4caf50; /* 可以根据需要调整颜色 */
  border-radius: 50%;
  width: 20px;
  height: 20px;
  animation: spin 1s linear infinite;
  margin-right: 10px; /* 与文本之间留出一些空间 */
}

/* 定义旋转动画 */
@keyframes spin {
  to { transform: rotate(360deg); }
}
/* 聊天消息的基本样式 */
.chat-message {
  padding: 10px;
  border-radius: 8px;
  margin: 5px 0;
  position: relative;
  display: flex;
  align-items: center;
}

/* 正在等待的消息样式 */
.waiting {
  color: #777; /* 设置文本颜色 */
  background-color: #f0f0f0; /* 设置背景颜色 */
}

5、引入marked.js报错解决

引入marked.js后,打包工程后,代码报错如下

You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
|             cells.shift();
|         }
>         if (cells.length > 0 && !cells.at(-1)?.trim()) {
|             cells.pop();
|         }

大概意思就是没有loader来处理新语法.?,

解决方案vue.config.js中configurewebpack中增加如下代码

module: {

      rules: [
        {
          test: /\.js$/,
          use: {
            loader: 'babel-loader',
          },
        },
        // 其他rules...
      ],

    },
configureWebpack: {
    // provide the app's title in webpack's name field, so that
    // it can be accessed in index.html to inject the correct title.
    name: name,
    output: {
      chunkFilename: 'static/js/[name].js'
      // chunkFilename: 'static/js/[name][contenthash].js'
    },
    resolve: {
      alias: {
        '@': resolve('src')
      }
    },
    module: {

      rules: [
        {
          test: /\.js$/,
          use: {
            loader: 'babel-loader',
          },
        },
        // 其他rules...
      ],

    },
  }

到此这篇关于Vue2+marked.js实现AI流式输出的项目实践的文章就介绍到这了,更多相关Vue2+marked.js AI流式输出内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

您可能感兴趣的文章:

相关文章

  • vue3 hook重构DataV的全屏容器组件详解

    vue3 hook重构DataV的全屏容器组件详解

    这篇文章主要为大家介绍了vue3 hook重构DataV的全屏容器组件详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • el-radio-group中的area-hidden报错的问题解决

    el-radio-group中的area-hidden报错的问题解决

    本文主要介绍了el-radio-group中的area-hidden报错的问题解决,下面就来介绍几种解决方法,具有一定的参考价值,感兴趣的可以了解一下
    2025-04-04
  • 如何使用Gitee Pages服务 搭建Vue项目

    如何使用Gitee Pages服务 搭建Vue项目

    这篇文章主要介绍了如何使用Gitee Pages服务 搭建Vue项目,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • VUE3页面div如何点击改变样式

    VUE3页面div如何点击改变样式

    这篇文章主要介绍了VUE3页面div如何点击改变样式问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • vue开发移动端h5环境搭建的全过程

    vue开发移动端h5环境搭建的全过程

    在正式使用Vue进行移动端页面开发前,需要做一些前置工作,下面这篇文章主要给大家介绍了关于vue开发移动端h5环境搭建的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-08-08
  • 利用WebStorm创建一个Vue项目的完整步骤

    利用WebStorm创建一个Vue项目的完整步骤

    WebStorm是一个非常适合学习和开发Vue项目的集成开发环境,下面这篇文章主要给大家介绍了关于利用WebStorm创建一个Vue项目的完整步骤,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2024-06-06
  • vue中的循环遍历对象、数组和字符串

    vue中的循环遍历对象、数组和字符串

    这篇文章主要介绍了vue中的循环遍历对象、数组和字符串,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • vue购物车插件编写代码

    vue购物车插件编写代码

    这篇文章主要为大家详细介绍了vue购物车插件的编写代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-11-11
  • antd vue 如何调整checkbox默认样式

    antd vue 如何调整checkbox默认样式

    这篇文章主要介绍了antd vue 如何调整checkbox默认样式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • vue项目中销毁window.addEventListener事件监听解析

    vue项目中销毁window.addEventListener事件监听解析

    这篇文章主要介绍了vue项目中销毁window.addEventListener事件监听,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07

最新评论