Vue2+marked.js實現(xiàn)AI流式輸出的項目實踐
1、實現(xiàn)效果
AI流式輸出
2、實現(xiàn)流程
1、頁面內(nèi)容
<template> <div class="app-container"> <!-- 聊天界面 --> <div class="chat-container"> <!-- 消息展示區(qū)域 --> <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"> <!-- 加載動畫,例如一個旋轉(zhuǎn)的圖標(biāo) --> <div class="loading-spinner"></div> 容我思考片刻 ! </div> <p v-else v-html="markMessage(message.content)"></p> </div> </div> <!-- 輸入框與發(fā)送按鈕 --> <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">發(fā)送</el-button> </div> </div> </div> </template>
循環(huán)展示消息內(nèi)容,根據(jù)是用戶發(fā)送消息和AI返回消息展示不同的樣式
添加了一個等待動畫
2、js部分與樣式
<script> // api部分大家根據(jù)自己的前端框架自己封裝即可,分別調(diào)用后端的兩個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: "你好", // 用戶輸入,默認(rèn)一開始發(fā)一個“你好” pollingActive: false, // 是否正在長輪詢 isEnd: false, // 標(biāo)記是否結(jié)束輪詢 currentAiMessageId: null, // 當(dāng)前正在回復(fù)的 AI 消息的 ID userMsgData: {}, // 用戶消息數(shù)據(jù) }; }, 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('調(diào)用前'+message) const marked = new Marked( markedHighlight({ pedantic: false, gfm: true, // 開啟gfm breaks: true, smartLists: true, xhtml: true, async: false, // 如果是異步的,可以設(shè)置為 true langPrefix: 'hljs language-', // 可選:用于給代碼塊添加前綴 emptyLangClass: 'no-lang', // 沒有指定語言的代碼塊會添加這個類名 highlight: (code) => { return hljs.highlightAuto(code).value } }) ); let markedMessage = marked.parse(message) console.log('調(diào)用了'+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 回復(fù)占位 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); } // 流結(jié)束時處理可能剩余的部分消息 // 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; /* 右側(cè)占比 */ 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; } /* 輸入框和發(fā)送按鈕 */ .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; /* 可以根據(jù)需要調(diào)整顏色 */ border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin-right: 10px; /* 與文本之間留出一些空間 */ } /* 定義旋轉(zhuǎn)動畫 */ @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; /* 設(shè)置文本顏色 */ background-color: #f0f0f0; /* 設(shè)置背景顏色 */ } </style>
發(fā)送消息后,生成用戶消息和AI回復(fù)占位
給AI接口發(fā)送消息,發(fā)送消息后,獲取到響應(yīng),然后使用reader.read方法讀取內(nèi)容。
給AI接口發(fā)送消息
export async function sendMessage1(message) { console.log(message+"----") try { const response = await fetch( '你的接口路徑', { method: 'POST', body:message }) return response } catch (error) { console.error('請求失敗:', error); } }
因為是流式響應(yīng),所以連接不是一次響應(yīng)就直接斷開的,使用while(true)循環(huán)不斷從中獲取到響應(yīng)內(nèi)容,并將響應(yīng)的內(nèi)容解碼。
AI接口響應(yīng) content-type: text/event-stream;charset=utf-8是這樣的,不是平常用的application/json,這表明響應(yīng)體是一個服務(wù)器發(fā)送事件(Server-Sent Events,簡稱SSE)流。SSE 允許服務(wù)器向客戶端(通常是瀏覽器)推送實時更新,而無需客戶端輪詢服務(wù)器。
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); } // 流結(jié)束時處理可能剩余的部分消息 // this.processServerSentEvent(buffer); } catch (e) { console.log(e.toString()) }
將解碼后內(nèi)容經(jīng)過處理后添加到messages數(shù)組中。
processServerSentEvent(eventData, isFinal = false) { console.log('收到數(shù)據(jù): '+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 文本轉(zhuǎn)換為 HTML 的 JavaScript 庫,而 highlight.js
是一個用于語法高亮的庫,它可以與 marked.js
一起使用來高亮 Markdown 中的代碼塊
安裝marked.js和hightlight.js然后導(dǎo)入
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('調(diào)用前'+message) const marked = new Marked( markedHighlight({ pedantic: false, gfm: true, // 開啟gfm breaks: true, smartLists: true, xhtml: true, async: false, // 如果是異步的,可以設(shè)置為 true langPrefix: 'hljs language-', // 可選:用于給代碼塊添加前綴 emptyLangClass: 'no-lang', // 沒有指定語言的代碼塊會添加這個類名 highlight: (code) => { return hljs.highlightAuto(code).value } }) ); let markedMessage = marked.parse(message) // console.log('調(diào)用了'+markedMessage) return markedMessage },
message就是markdown格式的文本內(nèi)容
4、添加等待效果
當(dāng)消息內(nèi)容為空時,顯示等待動畫,不為空顯示消息內(nèi)容
<div v-if="!message.content" class="chat-message waiting"> <!-- 加載動畫,例如一個旋轉(zhuǎn)的圖標(biāo) --> <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; /* 可以根據(jù)需要調(diào)整顏色 */ border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin-right: 10px; /* 與文本之間留出一些空間 */ } /* 定義旋轉(zhuǎn)動畫 */ @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; /* 設(shè)置文本顏色 */ background-color: #f0f0f0; /* 設(shè)置背景顏色 */ }
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(); | }
大概意思就是沒有l(wèi)oader來處理新語法.?,
解決方案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... ], }, }
到此這篇關(guān)于Vue2+marked.js實現(xiàn)AI流式輸出的項目實踐的文章就介紹到這了,更多相關(guān)Vue2+marked.js AI流式輸出內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue3?hook重構(gòu)DataV的全屏容器組件詳解
這篇文章主要為大家介紹了vue3?hook重構(gòu)DataV的全屏容器組件詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04el-radio-group中的area-hidden報錯的問題解決
本文主要介紹了el-radio-group中的area-hidden報錯的問題解決,下面就來介紹幾種解決方法,具有一定的參考價值,感興趣的可以了解一下2025-04-04利用WebStorm創(chuàng)建一個Vue項目的完整步驟
WebStorm是一個非常適合學(xué)習(xí)和開發(fā)Vue項目的集成開發(fā)環(huán)境,下面這篇文章主要給大家介紹了關(guān)于利用WebStorm創(chuàng)建一個Vue項目的完整步驟,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2024-06-06antd vue 如何調(diào)整checkbox默認(rèn)樣式
這篇文章主要介紹了antd vue 如何調(diào)整checkbox默認(rèn)樣式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12vue項目中銷毀window.addEventListener事件監(jiān)聽解析
這篇文章主要介紹了vue項目中銷毀window.addEventListener事件監(jiān)聽,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07