页面根据sse返回的流,逐句展示内容,达到gpt效果

之前的文章里,我写到了关于怎么获取sse中的流,但是缺少逐句展示的效果,这次来补齐。

image.png

比如这种,实现难点在于,当返回的markdown语法,不是完整的语句时,展示的代码块会错乱。

实现代码

app.vue

<template><div><input type="text" v-model="prompt"><button @click="fetchStream">第三方请求流数据</button><hr><p v-if="!streamContent && !loading">请开始您的提问</p><div v-for="(block, index) in contentBlocks" :key="index"><div v-if="block.type === 'html'" v-html="block.html"></div><CodeBlockv-else-if="block.type === 'code'":code="block.code":language="block.language"/></div><p v-if="loading">加载中...</p></div>
</template>
<script setup lang="ts">
import {ref, computed} from "vue";
import {marked} from 'marked';
import CodeBlock from "./components/CodeBlock.vue";const loading = ref(false);
const prompt = ref('写一段js快排')
const streamContent = ref('');// 渲染markdown内容
const contentBlocks = computed(() => {const renderer = new marked.Renderer();let blockIndex = 0; // 用于跟踪代码块的索引const blocks = []; // 存储所有块的数组renderer.code = (code, lang) => {const index = blockIndex++; // 获取当前代码块的索引// 将代码块信息存储到 blocks 数组中blocks.push({type: 'code',code: code,language: lang,index: index, // 存储索引,用于后续替换});// 返回一个特殊的占位符,包含当前代码块的索引return `<!--codeblock-${index}-->`;};// 使用自定义的 renderer 解析 Markdownconst html = marked(streamContent.value, {renderer});let codeBlockRegex = /<!--codeblock-(\d+)-->/;// 将解析后的 HTML 分割成块,并存储到 blocks 数组中const list = html.split(/(<!--codeblock-\d+-->)/).map((part, index) => {if (codeBlockRegex.test(part)) {let match = part.match(codeBlockRegex);return blocks[match?.[1]];} else {return {type: 'html',html: part,index: index, // 存储索引,用于后续替换};}});return list;
});document.cookie = `token=Bearer%20eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMTExNCIsImV4cCI6MTcxNjAxNzg1NSwiYml6VHlwZSI6Im1vZGIiLCJyb2xlTmFtZSI6IlJPTEVfbXZwIiwicGVybWlzc2lvbnMiOlsidmlkZW8iLCJjb21wYW55Il19.bQJ9WaT0BuczcW_8HRJoEUpyy_fM42wMoUd8amqOpmgo_PQ5sQoolGtvZIhwBe_W_BbGge5SmHhB677Wf0oH7w; userID=111xxx`async function fetchStream() {if (loading.value) return;loading.value = true;streamContent.value = '';const url = "http://rexxxx";const data = {select_param: "",chat_mode: "chat_normal",model_name: "qwen_proxyllm",user_input: prompt.value || '你好',conv_uid: "xxxx",};try {const response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(data)});const reader = response.body.getReader();const decoder = new TextDecoder();while (true) {const {done, value} = await reader.read();if (done) break;const textChunk = decoder.decode(value, {stream: true});// 找到最后一个"data:"的位置 并从那里开始截取到字符串结束const lastIndex = textChunk.lastIndexOf('data:');if (lastIndex !== -1) {// 避免重复数据const text = textChunk.substring(lastIndex + 'data:'.length).replace(/\\n/g, '\n');// 判断text的长度是否小于streamContent.value的长度 如果小于则不更新 避免数据错乱导致页面闪动if (text.length >= streamContent.value.length) {console.log(text, "------------------text")streamContent.value = text;}}}} catch (error) {console.error('请求失败', error);} finally {loading.value = false;}
}
</script>

CodeBlock.vue

<template><div class="code-enhance light"><div class="code-enhance-header"><span class="code-enhance-title">{{ language }}</span><span class="code-enhance-copy" @click="copyCode"><span>复制</span></span></div><preclass="code-enhance-content"><code :class="['language-' + language]" v-html="highlightedCode"></code></pre></div>
</template>
<script setup lang="ts">
import {ref, watchEffect} from 'vue';
import hljs from 'highlight.js';// 定义props
const props = defineProps<{code: string;language?: string;
}>();const highlightedCode = ref('');// 使用watchEffect来处理代码高亮
watchEffect(() => {const validLanguage = hljs.getLanguage(props.language);if (validLanguage) {highlightedCode.value = hljs.highlight(props.code, {language: props.language}).value;} else {highlightedCode.value = hljs.highlightAuto(props.code).value;}
});// 定义方法
const copyCode = () => {console.log(props.code);
};
</script>
<style scoped lang="scss">
.code-enhance {width: 100%;display: flex;flex-direction: column;border-radius: 7px;overflow: hidden;.code-enhance-content {width: 100%;overflow: auto;padding: 10px;background-color: #ececee;border-radius: 0 0 6px 6px;code {display: block;overflow: auto;padding: 10px;}pre code.hljs {display: block;overflow-x: auto;white-space: pre; // 保持空白符的处理padding: 0;}}.code-enhance-header {height: 32px;box-sizing: border-box;padding: 0 16px;font-size: 12px;display: flex;justify-content: space-between;align-items: center;gap: 8px;.code-enhance-title {-webkit-user-select: none;user-select: none;}.code-enhance-copy {display: inline-flex;cursor: pointer;align-items: center;gap: 6px;font-size: 12px;word-spacing: -4px;}}&.light {.code-enhance-header {background: #e2e6ea;color: #333;}.hljs {color: #24292e;background: none;}pre code.hljs {display: block;overflow-x: auto;padding: 0;}}
}
</style>

package.json

{"name": "stream-vue-demo","private": true,"version": "0.0.0","type": "module","scripts": {"dev": "vite","build": "vite build","preview": "vite preview"},"dependencies": {"github-markdown-css": "^5.5.1","highlight.js": "^11.9.0","marked": "^4.0.0","sass": "^1.76.0","vue": "^3.4.21"},"devDependencies": {"@types/marked": "^4.0.0","@vitejs/plugin-vue": "^5.0.4","vite": "^5.2.0"}
}

实现思路

这段代码是一个使用Vue.js框架的单文件组件(.vue文件),它由三个主要部分组成:<template><script><style>。下面我将逐句解释这段代码的意思。

template部分:

<div><input type="text" v-model="prompt"><button @click="fetchStream">第三方请求流数据</button><hr><p v-if="!streamContent && !loading">请开始您的提问</p><div v-for="(block, index) in contentBlocks" :key="index"><div v-if="block.type === 'html'" v-html="block.html"></div><CodeBlockv-else-if="block.type === 'code'":code="block.code":language="block.language"/></div><p v-if="loading">加载中...</p>
</div>
  1. <input type="text" v-model="prompt">:一个文本输入框,其值与变量prompt双向绑定。
  2. <button @click="fetchStream">第三方请求流数据</button>:一个按钮,点击时会触发fetchStream方法。
  3. <hr>:水平分割线。
  4. <p v-if="!streamContent && !loading">请开始您的提问</p>:当没有流内容且不在加载状态时,显示提示信息“请开始您的提问”。
  5. <div v-for="(block, index) in contentBlocks" :key="index">:遍历contentBlocks数组,为每个块创建一个div元素,并使用index作为唯一键。
  6. <div v-if="block.type === 'html'" v-html="block.html"></div>:如果块的类型是html,则使用v-html指令将其内容渲染为HTML。
  7. <CodeBlock v-else-if="block.type === 'code'" :code="block.code" :language="block.language"/>:如果块的类型是code,则使用自定义组件CodeBlock来渲染代码块,传递codelanguage作为属性。
  8. <p v-if="loading">加载中...</p>:如果处于加载状态,显示“加载中…”。

script setup lang="ts"部分:

这部分使用了TypeScript语言。

  1. 引入Vue的refcomputed函数,以及marked库和CodeBlock组件。
  2. 定义了一些响应式变量:loadingpromptstreamContent
  3. 定义了contentBlocks计算属性,用于解析Markdown内容并创建一个内容块数组。
  4. 设置了一个cookie,其中包含了一个模拟的token和userID。
  5. 定义了fetchStream异步函数,用于发送POST请求到一个URL,并处理流式响应数据。

CodeBlock.vue部分:

这是一个子组件,用于渲染代码块并提供复制功能。

  1. <template>部分定义了组件的HTML结构,包括代码标题、复制按钮和代码内容。
  2. <script setup lang="ts">部分定义了组件的逻辑,包括接收codelanguage属性,使用highlight.js库对代码进行高亮处理,并定义了复制代码的方法。
  3. <style scoped lang="scss">部分定义了组件的样式,使用了SCSS语法,并且是作用域限定的,只影响当前组件。

整体来看,这个Vue组件是一个简单的Markdown编辑器,它可以接收用户输入的Markdown文本,发送到服务器获取解析后的流数据,并将Markdown解析为HTML和代码块来展示。同时,它还包含了一个子组件CodeBlock用于渲染和复制代码块。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/391013.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

P10477 题解

题目传送门 题目传送门&#xff08;洛谷&#xff09; Step1 理解题意 一共有 T T T 组数据有一个地铁&#xff0c;有一个中心车站&#xff08;即为根&#xff09;&#xff0c;有一个人从中心车站出发。对于每组数据&#xff0c;给定两个同样长度的01串 s 1 s_1 s1​ , s …

内网安全:多种横向移动方式

1.MMC20.Application远程执行命令 2.ShellWindows远程执行命令 3.ShellBrowserWindow远程执行命令 4.WinRM远程执行命令横向移动 5.使用系统漏洞ms17010横向移动 DCOM&#xff1a; DCOM&#xff08;分布式组件对象模型&#xff09;是微软的一系列概念和程序接口。它支持不同…

C语言--函数

1. 函数定义 语法&#xff1a; 类型标识符 函数名&#xff08;形式参数&#xff09; {函数体代码 } &#xff08;1&#xff09;类型标识符 --- 数据类型&#xff08;函数要带出的结果的类型&#xff09; 注&#xff1a;数组类型不能做函数返回结果的类型&#xff0c;如果函…

Tomcat 8.5 下载、安装、启动及各种问题

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 本期内容主要介绍 Tomcat 8 的安装&#xff0c;以及可能会遇到的问题 文章目录 1. Tomcat 安装2. 可能会遇到的问题2.…

【vluhub】skywalking

SkyWalking是一个开源监控平台&#xff0c;用于从服务和云原生基础设施收集、分析、聚合和可视化数据 低版本存在sql注入漏洞 访问地址 http://192.168.203.12:8080/graphql burpsuite抓数据包 替换 {"query":"query queryLogs($condition: LogQueryConditi…

机器学习 第9章-聚类

机器学习 第9章-聚类 9.1 聚类任务 在“无监督学习”(unsupervised learning)中&#xff0c;训练样本的标记信息是未知的&#xff0c;目标是通过对无标记训练样本的学习来揭示数据的内在性质及规律&#xff0c;为进一步的数据分析提供基础。此类学习任务中研究最多、应用最广…

计算机毕业设计选题推荐-学生作业管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

errno错误码列举

errno&#xff0c;int变量&#xff0c;表示系统最近一次错误码。 当系统调用和一些库函数发生错误时&#xff0c;会给errno赋值&#xff0c;以指示哪里出了问题。 目录 errno值列表 errno值获取示例 errno值列表 <errno.h>头文件定义了errno的一些值&#xff0c;部分…

【C++ STL】list

文章目录 list1. list的使用1.1 增删查改1.2 功能接口 2. list的模拟实现2.1 list的定义2.2 默认成员函数2.3 迭代器正向迭代器解引用箭头 反向迭代器迭代器接口 2.4 基本功能 3. list对比vector list 与 vector 相比&#xff0c;list 的好处就是每次插⼊或删除 ⼀个 元素 就 …

pydal,一个实用的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个实用的 Python 库 - pydal。 Github地址&#xff1a;https://github.com/web2py/pydal/ 在现代应用开发中&#xff0c;数据库操作是一个核心部分。为了简化与数据库的交互…

PMP–知识卡片--Scrum框架

定义 Scrum框架包含由产品负责人、开发团队、敏捷专家构成的Scrum团队&#xff0c;以及活动工件。框架中的每一个组件都服务于一个特定的目标&#xff0c;且是Scrum成功和运用的基本要素。 Scrum的规则将角色、活动和工件绑定在一起&#xff0c;管理它们之间的关系和交互。 …

【Vue3】组件通信之$parent

【Vue3】组件通信之$parent 背景简介开发环境开发步骤及源码总结 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努力学习奋斗的…

基于Java的网络考试系统的设计与实现

点击下载源码 基于Java的网络考试系统的设计与实现 摘 要 科技在进步&#xff0c;人们生活和工作的方式正发生着改变&#xff0c;不仅体现在人们的衣食住行&#xff0c;也体现在与时俱进的考试形式上。以前的考试需要组织者投入大量的时间和精力&#xff0c;需要对考试的试题…

人工智能与大数据的融合:驱动未来的力量

人工智能与大数据的融合&#xff1a;驱动未来的力量 一、人工智能与大数据的概述二、人工智能与大数据在数据库中的融合三、实际应用案例四、未来发展方向总结 【纪录片】中国数据库前世今生 在数字化潮流席卷全球的今天&#xff0c;数据库作为IT技术领域的“活化石”&#xff…

【Python实战】如何优雅地实现文字 二维码检测?

前几篇&#xff0c;和大家分享了如何通过 Python 和相关库&#xff0c;自动化处理 PDF 文档&#xff0c;提高办公效率。 【Python实战】自动化处理 PDF 文档&#xff0c;完美实现 WPS 会员功能【Python实战】如何优雅地实现 PDF 去水印&#xff1f;【Python实战】一键生成 PDF…

【Linux详解】基础IO:软硬连接 | 动静态库管理

目录 软硬链接 1. 介绍 2.理解 2.1 如何理解硬链接&#xff1f; 2.2 如何理解软连接&#xff1f; 动静态库 1.介绍 1.1 使用 1.2 什么是库&#xff1f; 2.生成 2.1 静态库 2.2 动态库&#xff1a; 软硬链接 1. 介绍 1.1 软连接 是一个独立文件&#xff0c;具有独…

【Python机器学习】支持向量机——利用完整platt SMO算法加速优化

在几百个数据点组成的小规模数据集上&#xff0c;简化版SMO算法的运行是没有什么问题&#xff0c;但是在更大的数据集上的运行速度就会变慢。完整版的platt SMO算法应用了一些能够提速的启动方法。 platt SMO算法时通过一个外循环来选择第一个alpha值的&#xff0c;并且其选择…

内网穿透--ICMP隧道转发实验

实验背景 通过公司带有防火墙功能的路由器接入互联网&#xff0c;然后由于私网IP的缘故&#xff0c;公网无法直接访问内部web服务器主机。通过内网其它主机做代理&#xff0c;穿透访问内网web服务器主机边界路由器或防火墙做静态NAT映射访问内网服务器inux主机&#xff0c;且策…

MySQL的数据类型

文章目录 数据类型分类整型bit类型浮点类型字符串类型charvarchar 日期和时间类型enum和set find_ in_ set 数据类型分类 整型 在MySQL中&#xff0c;整型可以指定是有符号的和无符号的&#xff0c;默认是有符号的。 可以通过UNSIGNED来说明某个字段是无符号的。 在MySQL中如…

Tree-of-Traversals:结合知识图谱与大模型,通过树遍历和回溯寻找高置信度推理路径

Tree-of-Traversals&#xff1a;结合知识图谱与大模型&#xff0c;通过树遍历和回溯寻找高置信度推理路径 Tree-of-Traversals算法解析对比 MindMap1. 与知识图谱&#xff08;KGs&#xff09;的整合2. 推理方法3. 灵活性与可扩展性4. 在医学诊断中的应用 速度和准确1. 速度2. 推…