在 UniApp 中实现流式 AI 聊天功能
介绍
在现代 Web 开发中,流式 API 响应能够显著提升用户体验,尤其是在与 AI 聊天接口进行交互时。本文将介绍如何在 UniApp 中使用 Fetch API 实现一个流式响应的 AI 聊天功能,包括实时更新聊天内容和滚动到底部的功能。
实现
用 Markdown 格式展示 AI 输出的内容
<!-- index.vue -->
<view v-else class="bot-message" :key="'bot-msg-' + index">
<view class="avatar-container">
<image
class="message-avatar"
src="/static/images/icon_robot.png"
/>
</view>
<view class="message-content bot-bubble">
<u-loading-icon
v-if="
isSendLoading &&
index == chatMessages.length - 1 &&
!getText(message.content)
"
></u-loading-icon>
<text v-else>
// 用Markdown格式展示
<AiMarkdownViewer
class="message-text"
:content="message.content"
/>
<!-- {{ message.content || '服务异常,请重试' }} -->
</text>
</view>
</view>
<!-- AiMarkdownViewer.vue -->
// 使用showdown插件
<template>
<view
class="markdown-container"
v-html="parsedContent"
ref="markdownContainer"
></view>
</template>
<script>
import showdown from 'showdown'
export default {
name: 'AiMarkdownViewer',
props: {
content: {
type: String,
required: true,
},
},
data() {
return {
converter: new showdown.Converter({
tables: true,
tasklists: true,
simplifiedAutoLink: true,
strikethrough: true,
extensions: [this.tableEnhancement()],
}),
}
},
computed: {
parsedContent() {
if (this.content) {
const processed = this.content.replace(
/^```markdown\n([\s\S]*?)\n```$/gm, // 添加m标志处理多行
(match, content) => {
return content
}
)
return this.converter.makeHtml(processed)
} else {
return '微警灌云还在学习中,请您咨询当地派出所'
}
},
},
methods: {
tableEnhancement() {
return {
type: 'output',
filter: (text) => {
// 为表格添加容器和样式类
return text
.replace(
/<table>/g,
'<div class="table-wrapper"><table class="data-table">'
)
.replace(/<\/table>/g, '</table></div>')
.replace(/<td>/g, '<td class="data-cell">')
.replace(/<th>/g, '<th class="header-cell">')
},
}
},
},
}
</script>
<style scoped lang="scss">
.markdown-container {
margin: 0 auto;
// padding: 20px;
width: 100%;
font-size: 26rpx;
line-height: 1.7;
color: #374151;
white-space: normal;
}
/deep/ {
.table-wrapper {
overflow-x: auto;
margin: 1em 0;
border: 1px solid #ebeef5;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.data-table {
width: 100%;
min-width: 600px;
border-collapse: collapse;
font-size: 28rpx;
line-height: 1.5;
.header-cell {
background-color: #f8f9fa;
color: #606266;
font-weight: 600;
padding: 12px 16px;
border-bottom: 2px solid #ebeef5;
white-space: nowrap;
}
.data-cell {
padding: 12px 16px;
border-bottom: 1px solid #ebeef5;
color: #606266;
min-width: 80px;
&:empty::before {
content: ' ';
display: inline-block;
width: 1px;
}
}
tr:hover {
background-color: #f5f7fa;
}
tr:nth-child(even) {
background-color: #fafafa;
}
td:first-child,
th:first-child {
border-left: 1px solid #ebeef5;
}
td:last-child,
th:last-child {
border-right: 1px solid #ebeef5;
}
}
}
}
/deep/ {
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1em 0;
font-weight: 600;
color: #1f2937;
&:not(h1) {
border-bottom: 0.5px solid #e5e7eb;
padding-bottom: 0.4em;
}
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.4em;
}
h3 {
font-size: 1.3em;
}
h4 {
font-size: 1.2em;
}
h5 {
font-size: 1.1em;
}
h6 {
font-size: 1em;
}
// 列表样式
ul,
ol {
margin: 0.8em 0;
padding-left: 1.5em;
li {
margin: 0.4em 0;
&::marker {
color: #6b7280;
}
}
}
// 分割线
hr {
border: 0;
height: 0.5px;
background: #e5e7eb;
margin: 1.5em 0;
}
// 代码块
pre {
background-color: #f9fafb;
border-radius: 8px;
padding: 1em;
margin: 1.2em 0;
border: 0.5px solid #e5e7eb;
code {
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 13px;
color: #374151;
line-height: 1.6;
}
}
// 行内代码
code:not(pre code) {
background-color: #f3f4f6;
padding: 0.2em 0.4em;
border-radius: 4px;
font-size: 0.9em;
color: #dc2626;
}
// 引用块
blockquote {
border-left: 3px solid #e5e7eb;
margin: 1em 0;
padding: 0.5em 1em;
color: #4b5563;
background-color: #f8fafc;
border-radius: 0 4px 4px 0;
}
// 链接
a {
color: #3b82f6;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
/deep/ {
pre {
code {
display: block;
width: 100%;
overflow-x: auto;
}
}
}
</style>
我们需要使用 Fetch API 向 AI 聊天服务发送请求,并读取其流式响应。以下是实现的关键代码段。
async getAIChat(params, aiMessage) {
// 使用 Fetch API 进行流式请求
const response = await fetch(`${config.aiBaseUrl}/api/v1/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${config.apikey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
// 检测响应状态
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '请求失败');
}
this.isSendLoading = false; // 更新加载状态
// 使用 response.body.getReader() 开始逐块读取流式响应,使用 UTF-8 编码解码数据。
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let done = false;
// 用于流式读取数据
while (!done) {
const { done: readerDone, value } = await reader.read();
done = readerDone;
if (value) {
// 逐块解码并拼接
const chunkText = decoder.decode(value, { stream: !done });
// 解析接收到的 JSON 数据
const lines = chunkText.split('\n'); // 按行分割
lines.forEach((line) => {
if (line.startsWith('data:')) {
const jsonString = line.substring(5).trim();
try {
const jsonData = JSON.parse(jsonString);
// 提取内容部分
const content = jsonData.choices?.[0]?.delta?.content || '';
if (content) {
aiMessage.content += content; // 更新聊天内容
// 清除开头的换行符
if (aiMessage.content.startsWith('\n')) {
aiMessage.content = aiMessage.content.slice(1);
}
// 刷新页面渲染
this.$forceUpdate(); // 确保视图更新
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom(); // 调用滚动到底部的方法
});
}
} catch (error) {
console.error('Error parsing JSON:', error);
}
}
});
}
}
}