Vue3实现类ChatGPT聊天式流式输出(vue-sse实现)

1. 效果展示

流式输出

直接输出

2. 核心代码

找了一些示例与AI生成的代码,或多或少有些问题,搞了好久,郁闷~,在此记录下

2.1 依赖安装

sh 复制代码
npm install vue-sse

2.2 改写main.ts

sh 复制代码
import VueSSE from 'vue-sse'

const app = Vue.createApp(App)

// Use VueSSE, including a polyfill for older browsers
// @ts-ignore
app.use($).use(ElementPlus).use(store).use(router).use(VueSSE, {
    polyfill: true
})

2.3 Chat.vue完整代码

代码尚不完善,最新代码可参考Github, 见文末

js 复制代码
<template>
  <div class="chat">
    <el-form>
      <el-row>
        <div class="chat-container" style="margin-bottom: 40px">
<!--          <div v-for="message in messages" :key="message.id" class="message">-->
<!--            <el-avatar v-if="!message.isUser" shape="square" size="50" :src="botAvatar"></el-avatar>-->
<!--            <div :class="{'user-message': message.isUser, 'bot-message': !message.isUser}">-->
<!--              <div className="show-html" v-html=message.text></div>-->
<!--            </div>-->
<!--          </div>-->
          <div class="messages" v-for="msg in messages" :key="msg.id">
            <div :class="msg.from === 'user' ? 'user-message' : 'ai-message'">
              <div v-if="msg.type === 'code'" class="code-block">
                <pre>
                  <code class="language-javascript">{{ msg.text }}</code>
                </pre>
                <button @click="copyToClipboard(msg.text)">复制</button>
              </div>
              <div v-else v-html="renderMessageContent(msg.text)"></div>
            </div>
          </div>
        </div>
      </el-row>
      <el-row style="position: fixed; bottom: 45px; left: 5%; right: 5%; width: 90%;">
        <el-col :span="21">
          <el-input v-model="inputMessage" placeholder="请输入问题..." @keyup.enter="sendMessage" style="width: 100%;"></el-input>
        </el-col>
        <el-col :span="3">
          <el-button type="primary" @click="sendMessage" style="width: 100%;">发送</el-button>
        </el-col>
      </el-row>
    </el-form>
  </div>
</template>

<script>

import {marked} from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
import store from "@/store";
export default {
  name: "sseChat",
  data () {
    return {
      messages: [
        {id: 1, text: '我是您的私人智能助理,请问现在能帮您做什么?', isUser: false}
      ],
      inputMessage: '',
      botAvatar: require('../../assets/images/robot.png'),
      handlers: [
        {
          event: 'message',
          color: '#60778e'
        },
        {
          event: 'time',
          color: '#778e60'
        }
      ],
      client: null,
      // 无需跨域,否则无法接收消息, 这个原因浪费我好多时间
      url: 'http://127.0.0.1:8080/sse/subscribe?token=' + store.getters.token,
    }
  },
  mounted() {
    this.connect()
  },
  methods: {
    copyToClipboard(text) {
      navigator.clipboard.writeText(text).then(() => {
        alert('代码已复制到剪贴板!');
      });
    },
    connect () {
      // create the client with the user's config
      const self = this
      let client = this.$sse.create({
        url: this.url,
        includeCredentials: false
      })
      // add the user's handlers
      this.handlers.forEach((h) => {
        client.on(h.event, (data) => { //
          if (data === '<SSE_START>') {
            self.messages.push( {
              text: '',
              from: 'ai',
              type: 'text',
            })
            console.log(data)
          } else if (data === '<SSE_END>') {
            console.log(data)
          } else {
            const isCode = data.startsWith('```');
            console.log(data)
            const msg = {
              text: data,
              from: 'ai',
              type: isCode ? 'code' : 'text',
            };
            self.messages[self.messages.length - 1].text += data;
            self.highlightCode();
          }
        })
      })

      client.on('error', () => { // eslint-disable-line
        console.log('[error] disconnected, automatically re-attempting connection', 'system')
      })

      // and finally -- try to connect!
      client.connect() // eslint-disable-line
          .then(() => {
            console.log('[info] connected', 'system')
          })
          .catch(() => {
            console.log('[error] failed to connect', 'system')
          })
    },
    highlightCode() {
      this.$nextTick(() => {
        this.$el.querySelectorAll('pre code').forEach((block) => {
          hljs.highlightBlock(block);
        });
      });
    },
    // markdown
    renderMessageContent(msg) {
      if (msg === '') {
        return '';
      }
      marked.setOptions({
        renderer: new marked.Renderer(),
        highlight: function(code, lang) {
          // If lang is provided, use it; otherwise, let hljs guess
          return hljs.highlight(code, { language: lang || '' }).value;
        },
        langPrefix: 'hljs language-',
        pedantic: false,
        gfm: true,  // GitHub Flavored Markdown for better code block support among other things
        breaks: false,
        sanitize: true,  // For security, sanitize the HTML output unless you trust the source
        smartypants: false,
        xhtml: false
      });
      let html = marked(msg)
      return html
    },
    sendMessage() {
      const self = this
      if (self.inputMessage) {
        self.messages.push({id: self.messages.length + 1, text: self.inputMessage, isUser: true});
        // 一次性输出
        // self.$http.post('/chat/chat', {'content': self.inputMessage}, 'apiUrl').then(res => {
        //   self.messages.push({id: self.messages.length + 1, text: self.renderMessageContent(res), isUser: false});
        //   self.inputMessage = '';
        // })
        // 流式输出
        self.$http.post('/chat/sseChat', {'content': self.inputMessage}, 'apiUrl').then(res => {
          self.inputMessage = '';
        })
      }
    },

  }
}
</script>

<style scoped>
.chat{
  height: calc(100vh - 120px); /* Adjust based on your header/footer size */
  overflow-y: auto;
}

.message {
  display: flex;
  align-items: flex-start;
  margin: 10px;
}

.user-message {
  justify-content: flex-end;
  text-align: right;
}

.bot-message {
  text-align: left;
}
chat-container {
  display: flex;
  flex-direction: column;
  max-width: 600px;
  margin: auto;
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
}

.user-message {
  text-align: right;
  background-color: #d1e7dd;
  padding: 8px;
  border-radius: 5px;
  margin: 5px 0;
}

.ai-message {
  text-align: left;
  background-color: #f6f8f8;
  padding: 8px;
  border-radius: 5px;
  margin: 5px 0;
}

input {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
}

button {
  margin-left: 10px;
  padding: 5px 10px;
  cursor: pointer;
}
</style>

3. 后端改造

java 复制代码
// 1.配置允许跨域与流式响应
@GetMapping(value = "/subscribe", produces = "text/event-stream")
@CrossOrigin
@Operation(summary = "SSE订阅", tags = "AI大模型")
public SseEmitter subscribe(String token, HttpServletResponse response) {
    SseEmitter sseEmitter = SseServer.subscribe(token);
    response.setHeader("Cache-Control", "no-cache");
    response.setHeader("Connection", "keep-alive");
    return sseEmitter;
}

// 2.SecurityConfiguration.java中权限控制放开
.requestMatchers("/sse/**").permitAll()

// 3.在订阅式发送了开始<SSE_START>标识,消息结束发送了<SSE_END>标识,其他内容直接返回大模型字符串

4. 开源地址

https://github.com/SJshenjian/cloud-web
https://github.com/SJshenjian/cloud

TODO

  1. 流式输出Markdown支持
  2. 代码高亮可复制
相关推荐
m0_748255261 分钟前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
web1478621072334 分钟前
C# .Net Web 路由相关配置
前端·c#·.net
m0_7482478035 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖39 分钟前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案11 小时前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_748254881 小时前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.1 小时前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营1 小时前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood2 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端2 小时前
0基础学前端-----CSS DAY9
前端·css