1、实现效果AI流式输出 2、实现流程1、页面内容[code]<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>[/code]循环展示消息内容,根据是用户发送消息和AI返回消息展示不同的样式 添加了一个等待动画 2、js部分与样式[code]<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>[/code]发送消息后,生成用户消息和AI回复占位 给AI接口发送消息,发送消息后,获取到响应,然后使用reader.read方法读取内容。 给AI接口发送消息 [code]export async function sendMessage1(message) { console.log(message+"----") try { const response = await fetch( '你的接口路径', { method: 'POST', body:message }) return response } catch (error) { console.error('请求失败:', error); } }[/code]因为是流式响应,所以连接不是一次响应就直接断开的,使用while(true)循环不断从中获取到响应内容,并将响应的内容解码。 AI接口响应 content-type: text/event-stream;charset=utf-8是这样的,不是平常用的application/json,这表明响应体是一个服务器发送事件(Server-Sent Events,简称SSE)流。SSE 允许服务器向客户端(通常是浏览器)推送实时更新,而无需客户端轮询服务器。 [code]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()) }[/code]将解码后内容经过处理后添加到messages数组中。 [code] 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) },[/code]3、marked.js和highlight.jsmarked.js 是一个用于将 Markdown 文本转换为 HTML 的 JavaScript 库,而 highlight.js 是一个用于语法高亮的库,它可以与 marked.js 一起使用来高亮 Markdown 中的代码块 安装marked.js和hightlight.js然后导入 [code]npm install marked npm install highlight.js[/code] [code]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";[/code] [code]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 },[/code] [code]message就是markdown格式的文本内容 [/code]4、添加等待效果当消息内容为空时,显示等待动画,不为空显示消息内容 [code]<div v-if="!message.content" class="chat-message waiting"> <!-- 加载动画,例如一个旋转的图标 --> <div class="loading-spinner"></div> 容我思考片刻 ! </div> <p v-else v-html="markMessage(message.content)"></p>[/code]等待样式 [code]/* 加载指示器的样式 */ .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; /* 设置背景颜色 */ }[/code]5、引入marked.js报错解决引入marked.js后,打包工程后,代码报错如下 [code]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(); | } [/code]大概意思就是没有loader来处理新语法.?, 解决方案vue.config.js中configurewebpack中增加如下代码 [code]module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', }, }, // 其他rules... ], },[/code] [code]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... ], }, }[/code]免责声明:本内容来源于网络,如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |