在 UniApp 中实现stream流式输出 AI 聊天功能,AI输出内容用Markdown格式展示

在 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(
							//g,'
').replace(/<\/table>/g,'
') .replace(//g, '') .replace(//g, '') }, } }, }, } </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);  
                    }  
                }  
            });  
        }  
    }  
}  

你可能感兴趣的:(uniapp,uni-app,前端,AI,stream,流式输出)