一个简单的demo,实现了1、调用服务端大模型接口流式输出;2、添加marked效果展示;3、添加打字机效果展示
<!-- Vue2组件 -->
<template>
<div class="chat-container">
<div class="output-box" ref="outputBox" v-html="showDisplayText"></div>
<div class="input-group">
<input
v-model="inputQuestion"
placeholder="输入你的问题"
@keyup.enter="startStream"
/>
<button @click="startStream" :disabled="loading">
{{ loading ? '生成中...' : '开始生成' }}
</button>
<button @click="stopStream" v-show="loading">停止</button>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { marked } from '@/static/js/marked.esm.min.js'
export default {
data() {
return {
inputQuestion: '地球上有多少个国家?',
loading: false,
cancelToken: null,
receivedContent: '', // 已经接收的全部字符
responseText: '', // 新接收的字符
displayText: '', // 显示的字符
currentIndex: 0, // 当前打印位置
animationFrameId: null, // 动画帧ID
lastUpdateTime: 0, // 上次更新时间
displayTextRate: 30, // 打印机效果,多少毫秒一个字
};
},
computed: {
showDisplayText () {
// return this.displayText;
let tmp = this.displayText.trim();
tmp = this.doMarked(tmp);
return tmp;
}
},
created() {
this.rendererMD = new marked.Renderer();
marked.setOptions({
renderer: this.rendererMD,
gfm: true,
tables: true,
breaks: true, // false
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false
});//marked基本设置
},
methods: {
async startStream() {
this.resetState();
this.loading = true;
this.displayText = '';
try {
// 创建取消令牌
const CancelToken = axios.CancelToken
this.cancelToken = CancelToken.source()
const response = await axios({
method: 'post',
url: '', // 用你的服务端流式输出的地址
responseType: 'stream', // 重要:声明响应类型为流
params: {msg: this.inputQuestion},
cancelToken: this.cancelToken.token,
onDownloadProgress: (progressEvent) => {
const chunk = progressEvent.currentTarget.responseText;
// console.log(chunk);
// 处理增量内容
if (chunk.length > this.receivedContent.length) {
const newContent = chunk.slice(this.receivedContent.length); // 只取新的
this.receivedContent += newContent;
this.handleStreamChunk(newContent); // 从新数据中解析添加
this.scrollToBottom();
}
}
});
this.loading = false;
} catch (error) {
if (!this.loading) return;
this.displayText += '\n\n[请求已取消]';
this.loading = false;
}
},
handleStreamChunk(chunk) {
// 这里需要根据你的API响应格式进行解析
// 示例处理(假设API返回JSON格式的流数据):
try {
const lines = chunk.split('\n').filter(line => line.trim())
lines.forEach(line => {
const data = line.replace('data: ', '')
const parsed = JSON.parse(data)
// 这里根据自己的消息内容替换,比如这里我的每一行数据是
// data: {"event": "message", "conversation_id": "会话ID", "task_id": "任务ID", "answer": "一个token内容"}
if (parsed.event == 'message') {
this.responseText += parsed.answer || ''
this.startTypingEffect();
}
})
} catch (e) {
console.error('解析错误:', e)
}
},
stopStream() {
if (this.cancelToken) {
this.cancelToken.cancel('用户取消请求')
}
// 这里还要请求你的大模型api的停止会话接口,通常是要传任务ID参数
this.loading = false;
},
doMarked(str) {
return marked(str);
},
scrollToBottom() {
this.$nextTick(() => {
const box = this.$refs.outputBox;
box.scrollTop = box.scrollHeight;
});
},
startTypingEffect() { // 打字机效果展示
if (this.animationFrameId) return
const animate = (timestamp) => {
if (!this.loading && this.currentIndex >= this.responseText.length) {
this.cancelAnimation()
return
}
// 控制打印速度(30ms/字符)
if (timestamp - this.lastUpdateTime > this.displayTextRate) {
if (this.currentIndex < this.responseText.length) {
this.displayText += this.responseText[this.currentIndex]
this.currentIndex++
this.lastUpdateTime = timestamp
// 自动滚动到最新内容
this.scrollToBottom();
}
}
this.animationFrameId = requestAnimationFrame(animate)
}
this.animationFrameId = requestAnimationFrame(animate)
},
cancelAnimation() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
},
resetState() {
this.receivedContent = '';
this.responseText = ''
this.displayText = ''
this.currentIndex = 0
this.lastUpdateTime = 0
this.cancelAnimation()
}
},
beforeDestroy() {
this.cancelAnimation()
}
};
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.output-box {
height: 400px;
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
overflow-y: auto;
white-space: pre-wrap;
line-height: 1.6;
}
.input-group {
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
}
button {
padding: 8px 15px;
background: #42b983;
color: white;
border: none;
cursor: pointer;
}
button:disabled {
background: #ccc;
}
/* 一些marked替换的html样式调整 */
.output-box >>> p {
margin-block-end: 0.4rem;
}
.output-box >>> ul {
display: inline-block;
list-style: disc;
padding-left: 2rem;
margin-block-start:0;
margin-block-end:0;
padding-inline-start: 0;
}
.output-box >>> li {
margin-block-end: 0.4rem;
}
</style>
注意:1. 下载或者通过npm安装marked,因为各种chatbox之类的大模型调用都自带了marked展示效果,所以必须带上。
2. 大模型流式输出的api地址,自行调整,可以直接调用服务端,如我另一篇文章的php调用大模型为服务端,也可以是直接大模型api。
3. 大模型流式内容解析那里根据自己的api消息内容调整。