前端接入chatgpt,实现流式文字的显示

前端接入chatgpt,实现流式文字的显示

业务需求:

项目需要接入chatgpt提供的api,后端返回流式的字符,前端接收并实时显示。

相关技术原理:

1. JS中的Stream流:

在JavaScript中,使用Stream流通常指的是处理数据流的一种方式,特别是在Node.js环境下。Stream可以是可读的、可写的、或者既可读又可写的。它们允许数据被处理成块,而不是一次性处理整个数据集,这对于处理大量数据或者来自网络请求的数据非常有用。

但曾经这些对于 JavaScript 是不可用的。以前,如果我们想要处理某种资源(如视频、文本文件等),我们必须下载完整的文件,等待它反序列化成适当的格式,然后在完整地接收到所有的内容后再进行处理。

随着流在 JavaScript 中的使用,一切发生了改变——只要原始数据在客户端可用,你就可以使用 JavaScript 按位处理它,而不再需要缓冲区、字符串或 blob。

img

2. Stream API

以下是封装的用来调用的Stream API的核心代码,为了方便调用封装成了Hook组件。有以下组成部分:

  1. useStream Hook: 接受一个URL和一个参数对象。这个对象可以包含几个回调函数(onFirst, onNext, onError, onDone)和一个fetchParams对象,用于自定义fetch请求。
  2. startStream 函数: 被useStream内部调用,用于实际发起fetch请求,并使用ReadableStream的reader来逐块读取数据。它处理流数据的读取,并根据提供的回调函数处理数据块、错误和流结束。
import React, { useCallback, useState, useRef, useEffect } from 'react';
import 'abortcontroller-polyfill';
import { getLoginToken } from '../../utils/localStorage.js';
import {getRoleFromLocation} from '../commonUtils.js';

/**
 * React hook for the [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
 * Use this hook to stream data from a URL.
 * @param {string} url
 * @param {object} [params]
 * @param {function(Response)} [params.onNext]
 * @param {function(Error)} [params.onError]
 * @param {function()} [params.onDone]
 * @param {RequestInit} [params.fetchParams]
 *
 * @returns {StreamHook}
 */

function useStream(url, params) {
  if (typeof params !== 'object' || params === null) {
    params = {};
  }

  const streamRef = useRef();
  const onFirst = useRef(params.onFirst);
  const onNext = useRef(params.onNext);
  const onError = useRef(params.onError);
  const onDone = useRef(params.onDone);
  const close = useCallback(() => {
    if (streamRef.current) {
      streamRef.current.abort();
    }
  }, []);
  useEffect(() => {
    if (streamRef.current) {
      streamRef.current.abort();
    }

    streamRef.current = new AbortController();
    if (params.fetchParams) {
      startStream(url, {
        onFirst: onFirst,
        onNext: onNext,
        onError: onError,
        onDone: onDone,
        fetchParams: {
          ...params.fetchParams,
          signal: streamRef.current.signal
        }
      });
    }
  }, [url, params.fetchParams]);

  useEffect(() => {
    onFirst.current = params.onFirst;
  }, [params.onFirst]);
  useEffect(() => {
    onNext.current = params.onNext;
  }, [params.onNext]);
  useEffect(() => {
    onError.current = params.onError;
  }, [params.onError]);
  useEffect(() => {
    onDone.current = params.onDone;
  }, [params.onDone]);
  return {
    close
  };
}
/**
 * Use this function to start streaming data from an URL
 * @param {string} url
 * @param {object} params
 * @param {React.MutableRefObject<function(Response)>} params.onNext
 * @param {React.MutableRefObject<function(Error)>} params.onError
 * @param {React.MutableRefObject<function()>} params.onDone
 * @param {RequestInit} params.fetchParams
 */

async function startStream(url, {
  onFirst,
  onNext,
  onError,
  onDone,
  fetchParams
}) {
  const errCb = err => {
    if (typeof onError.current === 'function') {
      onError.current(err);
    }
  };

  try {
    // 获取role
    const locationType = getRoleFromLocation();
    // add header
    const reqHeaders = { Authorization: getLoginToken(locationType), 'Content-Type': "application/json"}
    const res = await fetch(url, { method: 'GET', ...fetchParams, headers: reqHeaders });
    const reader = res.body.getReader();
    const headers = res.headers;
    if (typeof onFirst.current === 'function') {
      onFirst.current(headers);
    }

    if (fetchParams.signal instanceof AbortSignal) {
      fetchParams.signal.addEventListener('abort', evt => reader.cancel(evt), {
        once: true,
        passive: true
      });
    } // eslint-disable-next-line no-constant-condition

    while (true) {
      try {
        const {
          done,
          value
        } = await reader.read();
        if (done) {
          if (typeof onDone.current === 'function') {
            onDone.current();
          }
          return;
        }
        if (typeof onNext.current === 'function') {
          const data = new TextDecoder('utf-8').decode(value);
          onNext.current(data);
        }
      } catch (e) {
        errCb(e);
        return;
      }
    }
  } catch (e) {
    errCb(e);
  }
}

export default useStream;

3. React中的dangerouslySetInnerHTML

dangerouslySetInnerHTML是React中的一个属性,允许你直接在组件内部插入HTML代码字符串。由于直接使用HTML字符串可能会导致跨站脚本(XSS)攻击,因此React将其命名为dangerouslySetInnerHTML,以此提醒开发者注意使用时的潜在风险。

使用dangerouslySetInnerHTML时,需要传递一个对象,该对象有一个__html键,对应的值就是你想要插入的HTML字符串。

例如:

<div dangerouslySetInnerHTML={{ __html: "<span>这是HTML内容</span>" }}></div>

在上述代码中,

标签内将显示 这是HTML内容,而不是将其作为字符串显示出来。

使用dangerouslySetInnerHTML时应该非常小心,确保传入的HTML内容是安全的,避免XSS攻击。在可能的情况下,尽量使用React的组件和属性来动态生成内容,而不是直接使用dangerouslySetInnerHTML。

业务实现

当理清上述的技术点后,剩下的业务逻辑实现就不算困难了。但是本人项目里面夹杂了太多了的业务性质的代码,所以这里只展示主要逻辑了。因为流式传来的是一个个字符,所以前期需要收集并拼接传来的字符,等待如[DONE]这类明确状态的字符传来后,再通过setState更新DOM.

  1. 导入依赖:引入了React库的useCallback、useState、useRef钩子,antd-mobile库的Avatar组件,样式文件,一个图片资源,以及自定义的useStream钩子。
  2. 组件定义:ChatGptStream是一个函数式组件,接收props作为参数。
  3. 状态和引用
  • 使用useState钩子定义了chatgptAnswer状态,用于存储聊天回答的内容。
  • 使用useRef钩子创建了answerDataRef引用,用于累积接收到的流数据。
  1. 处理流数据
  • getChatGptStream函数处理从流中接收到的每一条消息。如果消息包含特定的结束标记(如[DONE]、[FAILED]、[OVER]),则调用handleCommend函数处理并结束处理流程。如果消息包含
    ,则将其替换为换行符,并累积到answerDataRef中。
  • 更新chatgptAnswer状态以显示累积的聊天内容,并调用scrollMessageListToEnd函数滚动到消息列表的底部。
  1. 使用自定义钩子:通过useStream钩子与后端建立流连接,传入requestUrl、onFirst、getChatGptStream函数和chatgptParams参数。
  2. 渲染UI:组件返回的JSX中,如果chatgptAnswer.title_zh有内容,则显示聊天记录。使用Avatar组件显示机器人头像,dangerouslySetInnerHTML属性将聊天内容作为HTML插入到页面中,以保留格式(如换行)。
  3. 样式和布局:通过内联样式和className引用外部.less文件中定义的样式,设置聊天记录的布局和外观。
import React, { useCallback, useState, useRef } from 'react';
import { Avatar } from 'antd-mobile';

import './index.less';
import siuvoRobot from '@/assets/images/avatar_robot.png';
import useStream from '@/utils/hooks/useStreamV2';

const ChatGptStream = (props) => {
  const {
    chatgptParamsObj,
    scrollMessageListToEnd,
  } = props;
  const [chatgptAnswer, setChatgptAnswer] = useState({
    title_zh: '',
  });
  const answerDataRef = useRef('');
// 由外部传来的请求地址和入参
  const { requestUrl, chatgptParams } = chatgptParamsObj;

  const handleCommend = data => {
    // 处理data逻辑
  }

  const getChatGptStream = async res => {
    let data = res;
    // 根据后端返回字符,做相应的处理
    if (data.includes('[DONE]') || data.includes('[FAILED]') || data.includes('[OVER]')) {
      handleCommend(data);
      return;
    }
    // 换行
    if (data.includes('<br/>')) {
      data = data.replace(/<br\/>/g, '\r\n');
    }
    answerDataRef.current += data;
    // 显示聊天内容
    setChatgptAnswer({ title_zh: answerDataRef.current, });
    scrollMessageListToEnd();
  };

  const onFirst = useCallback(async res => {
    // 处理首次返回的数据
  }, []);

  useStream(requestUrl, { onFirst, onNext: getChatGptStream, fetchParams: chatgptParams });

  return (
    <>
      {
        chatgptAnswer?.title_zh && (
          <div className="chatting-records-content"
            style={{
              padding: '0 0.5rem',
              marginTop: '-1rem',
            }}
          >
            <div className="dialogue-block flex-start">
              <div className="head">
                <Avatar src={siuvoRobot} style={{ '--size': '32px' }} />
              </div>
              <div className="dialogue left-message-text" style={{ background: 'lavender' }}>
                <div dangerouslySetInnerHTML={{ __html: chatgptAnswer?.title_zh }}>
                </div>
              </div>
            </div>
          </div>
        )
      }
    </>
  )
}

export default ChatGptStream;

这里展示ChatGptStream在外部的引用:

...
  // 如果消息超出了屏幕,自动滚动到最底部
  const scrollMessageListToEnd = useCallback(() => {
    // ...根据实际样式,获取元素
    // 元素当前的滚动位置 = 这是元素内容的总高度 - 元素可见部分的高度
    messagesShowContent.scrollTop = messagesShowContent.scrollHeight - messagesShowContent.clientHeight;
    // ...
  }, [])

  // chatgptParamsObj对象值发生更变,触发更新
  setChatgptParamsObj({
    ...chatgptParamsObj,
    chatgptParams: {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    },
    requestUrl: `${BASE_URL}ai/suggest/v2?sessionId=${sessionIdRef.current}`
  });

...
return (
  ...
    {
      chatgptParamsObj.chatgptParams &&
      <ChatGptStream
        chatgptParamsObj={chatgptParamsObj}
        scrollMessageListToEnd={scrollMessageListToEnd}
      />
    }
...
)

以上,便是实现业务需求的总体逻辑了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RK3568驱动指南|第十五篇 I2C-第172章 I2C 驱动框架编写

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

吃瓜Llama3-V之余,看多模态大模型架构演变!

今天最大的瓜莫过于&#xff1a;斯坦福 Llama3-V PK 清华 MiniCPM-Llama3-V-2.5&#xff0c;详细证据&#xff1a; https://github.com/OpenBMB/MiniCPM-V/issues/196吃瓜之余&#xff0c;来看一下多模态大模型架构演变&#xff01; 一篇优秀的论文综述了多模态AI架构——包含…

无线领夹麦克风哪个牌子好,口碑最好的麦克风品牌推荐!

自媒体的兴起极大地推动了音频设备技术的发展&#xff0c;尤其是麦克风&#xff0c;它已成为自媒体创作中不可或缺的工具。从早期的新闻采访到当下流行的网络直播和Vlog&#xff0c;麦克风的应用场景不断扩展。一个视频的音频质量直接影响观众的观看体验&#xff0c;因此&#…

使用Netty框架实现WebSocket服务端与客户端通信(附ssl)

仓库地址&#xff1a; https://gitee.com/lfw1024/netty-websocket 导入后可直接运行 预览页面 自签证书&#xff1a; #换成自己的本地ip keytool -genkey -alias server -keyalg RSA -validity 3650 -keystore D:\mystore.jks -ext sanip:192.168.3.7,ip:127.0.0.1,dns:lo…

【存储】相关内容

【存储】相关内容 1. 存储类型1. 块存储2. 文件存储3. 对象存储4. 三种存储类型对比 2. 常见的存储分类1. DAS2. SAN3. NAS4. 存储分类分析比较 3. 一些存储的概念1. LUN2. volume3. HBA4. iSCSI 1. 存储类型 块存储和文件存储是我们比较熟悉的两种主流的存储类型&#xff0c;…

《昇思25天学习打卡营第21天 | 昇思MindSporePix2Pix实现图像转换》

21天 本节学习了通过Pix2Pix实现图像转换。 Pix2Pix是基于条件生成对抗网络&#xff08;cGAN&#xff09;实现的一种深度学习图像转换模型。可以实现语义/标签到真实图片、灰度图到彩色图、航空图到地图、白天到黑夜、线稿图到实物图的转换。Pix2Pix是将cGAN应用于有监督的图…

JavaSE--基础语法--类和对象(第二期)

&#xff08;一&#xff09;.面向对象的初步认知 1.1什么是面向对象&#xff1f; Java是一门纯面向对象的语言(Object Oriented Program&#xff0c;简称OOP)&#xff0c;在面向对象的世界里&#xff0c;一切皆为对象。面向对象是解决问题的一种思想&#xff0c;主要依靠对象…

新手使用超市收银系统应该注意哪些问题?

大部分小型超市都没怎么使用过智能收银系统&#xff0c;都是采用的传统手工收银方式&#xff0c;盘点、进货、库存也都是靠手工记录&#xff0c;完全没有接触过智能收银系统带来的优势和便利。超市收银软件特别是小区里面的超市&#xff0c;就跟传统的夫妻便利店的营销模式差不…

教育心理学期末考试重点

人本主义学习理论 人本主义主张&#xff0c;心理学应当把人作为一个整体来研究&#xff0c;而不是将人的心理肢解为不完整的几个部分&#xff0c;应该研究正常的人&#xff0c;而且更应该关注人的高级心理活动&#xff0c;如热情、信念、生命、尊严等内容。人本主义的学习理论…

【C++】使用C++在线程中动态记录数据到外部文件

在现代软件开发中&#xff0c;多线程编程已成为处理并发任务、提高程序性能的重要手段。而在多线程环境下&#xff0c;如何有效地管理和记录数据&#xff0c;尤其是将动态生成的数据安全地写入外部文件&#xff0c;是许多应用程序必须面对的问题。本文将深入探讨如何在C中使用多…

【运维】Windows server 2022 开启 telnet 功能

控制面板》启动或关闭Windows 功能 仪表盘》添加角色和功能》功能》telnet客户端

python-糖果俱乐部(赛氪OJ)

[题目描述] 为了庆祝“华为杯”的举办&#xff0c;校园中开展了许多有趣的热身小活动。小理听到这个消息非常激动&#xff0c;他赶忙去参加了糖果俱乐部的活动。 该活动的规则是这样的&#xff1a;摊位上有 n 堆糖果&#xff0c;第 i 堆糖果有 ai​ 个&#xff0c;参与的同学可…

全平台7合一自定义小程序源码系统功能强大 前后端分离 带完整的安装代码包以及搭建教程

系统概述 这款全平台 7 合一自定义小程序源码系统是专为满足各种业务需求而设计的。它整合了多种功能&#xff0c;能够在不同平台上运行&#xff0c;为用户提供了全方位的体验。无论你是企业主、开发者还是创业者&#xff0c;这款系统都能为你提供强大的支持。 代码示例 系统…

MATLAB code 生成C代码样式

Matlab code 生成C代码需要以下产品&#xff1a; MATLABMATLAB CoderC 编译器 MATLAB Coder 将查找并使用支持的已安装编译器。 可以使用 mex -setup 更改默认编译器。 在本地工作文件夹中创建文件 创建一个本地工作文件夹&#xff0c;例如 c:\ecoder\work。创建包含以下代…

【Python】Python的安装与环境搭建

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️Python】 文章目录 前言Python下载环境配置测试环境变量是否配置成功配置环境变量 运行Python交互式解释器&#xff1a;命令行脚本集成开发环境&#xff08;IDE&#xff1a;Integrated Development E…

电脑IP地址自动获取:操作指南与优势分析

在数字化时代&#xff0c;网络连接已成为我们日常生活和工作中的重要组成部分。而在建立网络连接的过程中&#xff0c;IP地址的设置无疑是至关重要的一环。IP地址&#xff0c;作为网络设备的唯一标识&#xff0c;其设置方式直接影响到网络的稳定性和安全性。本文将详细介绍如何…

大数据、人工智能、云计算、物联网、区块链序言【大数据导论】

各位大佬好 &#xff0c;这里是阿川的博客&#xff0c;祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 本篇序言前 必看 【大数据导论】—大数据序言 这是…

山东益康,聚焦绿葆医院场景媒体,用爱服务人类健康

山东益康集团创建于1983年&#xff0c;发展成为集药品研发生产、销售、特医功能食品、精细化工、医疗防护产品等多产业经营为一体的省级企业集团。益康集团紧跟国家发展战略&#xff0c;满足民众日益增长的健康需求&#xff0c;将食品生产向特医保健功能食品转型升级&#xff0…

校园兼职小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;商家管理&#xff0c;管理员管理&#xff0c;用户管理&#xff0c;兼职管理&#xff0c;论坛管理&#xff0c;公告管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;公告&#xff0c;兼职&…

【TS】TypeScript 入门指南:强大的JavaScript超集

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 TypeScript 入门指南&#xff1a;强大的JavaScript超集一、TypeScript 简介1.1 …