当前位置: 首页 > article >正文

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

1. 效果展示

流式输出
在这里插入图片描述
直接输出
在这里插入图片描述

2. 核心代码

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

2.1 依赖安装

npm install vue-sse

2.2 改写main.ts

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, 见文末

<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. 后端改造

// 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. 代码高亮可复制

http://www.kler.cn/a/316068.html

相关文章:

  • 从swagger直接转 vue的api
  • 动态规划问题-删除并获得点数(Java实现)
  • 4-6-2.C# 数据容器 - ArrayList 扩展(ArrayList 注意事项、ArrayList 存储对象的特性、ArrayList 与数组的转换)
  • AUTOSAR_EXP_ARAComAPI的7章笔记(3)
  • Gartner发布安全平台创新洞察:安全平台需具备的11项常见服务
  • 比ChatGPT更酷的AI工具
  • torch.embedding 报错 IndexError: index out of range in self
  • 数据结构之二叉树遍历
  • 【Linux系统编程】第二十一弹---进程的地址空间
  • 《概率论与数理统计》学渣笔记
  • uni-app功能 1. 实现点击置顶,滚动吸顶2.swiper一个轮播显示一个半内容且实现无缝滚动3.穿透修改uni-ui的样式
  • 美团测开OC!
  • 【论文串烧】多媒体推荐中的模态平衡学习 | 音视频语音识别中丢失导致的模态偏差对丢失视频帧鲁棒性的影响
  • erlang学习:Linux常用命令2
  • Github 2024-09-23 开源项目周报 Top15
  • Kubernetes集群架构、安装和配置全面指南
  • 目标检测-数据集
  • 【MySQL】获取最近7天和最近14天的订单数量,使用MySQL详细写出,使用不同的方法
  • 想学习下Python和深度学习,Python需要学习到什么程度呢?
  • C++入门——(类的默认成员函数)析构函数
  • 数据库基础知识---------------------------(3)
  • 早期病毒和反病毒技术(网络安全小知识)
  • MATLAB系列08:输入/输入函数
  • SSCMS 插件示例 一插件创建及插件菜单
  • 大厂面试真题:SpringBoot的核心注解
  • FastAPI 的隐藏宝石:自动生成 TypeScript 客户端