[React]流式响应代码数据,处理转换为支持Markdown渲染和可复制性功能的实现

React代码从流式API响应处理转换为支持Markdown渲染和可复制性的实现。用户之前提供的代码是使用fetch进行流式请求,并通过正则表达式提取内容更新状态。现在他们希望展示Markdown格式,包括图片,并且内容可以复制。

首先,我需要理解用户现有的代码结构。他们使用fetch POST请求Coze的API,设置stream为true,这意味着数据会分块返回。然后使用reader读取流,解码每个chunk,用正则表达式提取content字段,并更新historyContent状态来实时显示内容。

接下来,用户的需求是让这些内容以Markdown格式渲染,特别是处理图片,并且确保文本可复制。之前的回答是基于Vue的,现在需要转换为React,并且结合用户现有的流处理逻辑。

首先,我需要考虑如何在React中渲染Markdown。常用的库有react-markdown,它可以将Markdown字符串转换为React组件。同时,需要处理图片的渲染,可能需要自定义组件来确保图片正确显示。另外,代码块的高亮可以使用remark-gfm和rehype-highlight等插件。

然后,可复制性方面,需要确保用户可以选择文本,并且代码块有复制按钮。这可能需要额外的CSS样式和事件处理,比如在代码块旁边添加一个复制按钮,点击时复制代码内容。

接下来,需要将现有的流处理逻辑与Markdown渲染结合起来。用户当前的代码使用正则表达式提取content,这可能不够健壮,特别是如果API返回的JSON结构更复杂的话。可能需要改用JSON.parse来解析每个chunk,但需要注意流式响应中可能出现的部分JSON问题。不过,用户提供的正则表达式方法可能已经可以提取content,暂时可以保留,但可能需要处理转义字符,比如将\n转换为换行,或者处理Unicode字符如\u0026变成&等。

然后,在React组件中,当更新historyContent时,每个消息的text字段应该存储原始的Markdown内容。然后在渲染时,使用react-markdown来转换该内容为HTML元素。

关于图片渲染,需要确保Markdown中的图片链接是正确的,并且图片可以加载。可能需要检查CORS设置,但通常Coze返回的图片链接应该是可访问的。

代码高亮部分,需要安装highlight.js,并在react-markdown中配置插件,使其在渲染代码块时自动应用高亮样式。同时,为代码块添加复制按钮,这可以通过自定义渲染器来实现,比如覆盖code元素的渲染方式,添加一个按钮,当点击时复制代码内容。

可复制性还需要确保整个文本内容可以被用户选择和复制,因此需要检查CSS样式,避免user-select被设置为none,可能需要设置user-select: text,并且处理空白字符,比如white-space: pre-wrap。

此外,用户当前的代码在处理流式响应时,可能没有考虑多个chunk拼接成一个完整的JSON对象的情况。如果每个chunk是一个独立的数据块,可能需要确保正确分割和处理。但根据用户的正则表达式方法,可能每个chunk都包含一个content字段,所以直接提取即可。不过需要注意,如果content中有换行符或特殊字符,正则表达式可能无法正确捕获,需要测试这种情况。

最后,在错误处理和状态更新方面,用户已经在try/catch中处理了错误情况,并在finally中更新状态,停止加载状态。需要确保在渲染Markdown时,这些状态更新不会导致问题,比如在流式传输过程中,每次更新text时,react-markdown会重新渲染内容,可能会有性能问题,但通常React的虚拟DOM会优化这一点。

总结步骤:

  1. 在React项目中安装react-markdown、remark-gfm、rehype-highlight、highlight.js等依赖。
  2. 修改现有的状态更新逻辑,将content作为Markdown字符串保存。
  3. 使用react-markdown组件渲染消息的text字段,配置插件支持GFM(表格、删除线等)和代码高亮。
  4. 自定义图片渲染,确保图片正确显示,并处理可能的样式问题。
  5. 添加代码块的复制按钮,通过自定义渲染器实现。
  6. 调整CSS样式,允许文本选择和复制,处理空白字符。
  7. 测试流式响应中的Markdown解析,确保换行符、图片、代码块等正确显示。
  8. 处理可能的转义字符,如Unicode转义序列,确保内容正确解码。

可能遇到的问题:

  • 流式响应中的chunk可能不完整,导致JSON解析失败,但用户使用正则表达式可能规避了这个问题。
  • 图片无法加载,需要检查URL是否正确,以及服务器CORS配置。
  • 代码高亮样式未正确应用,需要导入highlight.js的CSS文件。
  • 复制按钮的功能实现,需要处理点击事件和剪贴板API。

需要确保所有依赖正确安装,组件正确导入,样式文件引入,以及处理可能的异步更新问题。

以下是基于React实现流式Markdown内容渲染的完整方案,包含图片展示和可复制功能:

import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github-dark.css';

const ChatComponent = () => {
  const [historyContent, setHistoryContent] = useState([]);

  // 自定义代码块渲染(添加复制按钮)
  const CodeBlock = ({ node, inline, className, children, ...props }) => {
    const copyToClipboard = (code) => {
      navigator.clipboard.writeText(code);
    };

    return inline ? (
      <code className={className} {...props}>
        {children}
      </code>
    ) : (
      <div className="code-block-wrapper">
        <pre className={className}>
          <code {...props}>{children}</code>
        </pre>
        <button 
          onClick={() => copyToClipboard(String(children))}
          className="copy-button"
        >
          Copy
        </button>
      </div>
    );
  };

  // 处理流式响应
  const handleStreamRequest = async (curContent) => {
    const botMessageId = Date.now();
    
    try {
      const response = await fetch('https://api.coze.cn/v3/chat', {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer YOUR_TOKEN',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          bot_id: "74768042021",
          user_id: "123456",
          stream: true,
          auto_save_history: true,
          additional_messages: [{
            role: "user",
            content: curContent,
            content_type: "text"
          }]
        })
      });

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      let buffer = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        
        // 处理可能的多个JSON对象
        while (buffer.includes('}{')) {
          const splitIndex = buffer.indexOf('}{') + 1;
          const chunk = buffer.slice(0, splitIndex);
          buffer = buffer.slice(splitIndex);
          processChunk(chunk);
        }
      }
      processChunk(buffer); // 处理剩余数据
    } catch (error) {
      // 错误处理...
    }
  };

  // 处理数据块
  const processChunk = (chunk) => {
    try {
      const jsonData = JSON.parse(chunk);
      const content = jsonData.content
        .replace(/\\n/g, '\n')
        .replace(/\\"/g, '"')
        .replace(/\\u0026/g, '&');

      setHistoryContent(prev => 
        prev.map(msg => 
          msg.id === botMessageId 
            ? { ...msg, text: msg.text + content }
            : msg
        )
      );
    } catch (e) {
      console.error('JSON解析错误:', e);
    }
  };

  return (
    <div className="chat-container">
      {historyContent.map(msg => (
        <div key={msg.id} className="message">
          <ReactMarkdown
            remarkPlugins={[remarkGfm]}
            rehypePlugins={[rehypeHighlight]}
            components={{
              img: ({ node, ...props }) => (
                <img 
                  {...props} 
                  style={{ maxWidth: '100%', borderRadius: '8px' }}
                  alt={props.alt || '图片'}
                />
              ),
              code: CodeBlock
            }}
          >
            {msg.text}
          </ReactMarkdown>
          {msg.isStreaming && <div className="streaming-indicator" />}
        </div>
      ))}
    </div>
  );
};

// CSS样式
const styles = `
  .chat-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }

  .message {
    background: #f5f5f5;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 16px;
    user-select: text;
  }

  .code-block-wrapper {
    position: relative;
    margin: 16px 0;
  }

  .copy-button {
    position: absolute;
    top: 8px;
    right: 8px;
    background: #007bff;
    color: white;
    border: none;
    padding: 4px 8px;
    border-radius: 4px;
    cursor: pointer;
    transition: opacity 0.3s;
  }

  .copy-button:hover {
    opacity: 0.8;
  }

  .streaming-indicator {
    display: inline-block;
    width: 8px;
    height: 8px;
    background: #007bff;
    border-radius: 50%;
    margin-left: 8px;
    animation: pulse 1.2s infinite;
  }

  @keyframes pulse {
    0% { opacity: 1; }
    50% { opacity: 0.4; }
    100% { opacity: 1; }
  }
`;

// 在组件中注入样式
document.head.insertAdjacentHTML('beforeend', `<style>${styles}</style>`);

关键功能说明:

  1. 流式处理优化
  • 新增缓冲区(buffer)处理不完整的JSON数据块
  • 使用replace()处理特殊字符转义(\n, \", \u0026等)
  • 支持多个JSON对象的分割处理
  1. Markdown增强
  • 集成remarkGfm支持表格、删除线等扩展语法
  • 使用rehypeHighlight实现代码语法高亮
  • 自定义图片组件添加圆角和自适应宽度
  1. 代码复制功能
  • 为代码块添加浮动复制按钮
  • 使用navigator.clipboard实现一键复制
  • 代码块保持原始格式(pre-wrap)
  1. 样式优化
  • 用户可选择文本(user-select: text)
  • 流式响应指示动画
  • 响应式图片显示
  • 暗色代码主题(github-dark.css)

使用注意事项:

  1. 安装依赖:
npm install react-markdown remark-gfm rehype-highlight highlight.js
  1. 安全性处理:
  • 自动过滤危险HTML标签(默认行为)
  • 推荐添加XSS过滤库(如dompurify)
  • 限制图片域名白名单
  1. 性能优化:
  • 对长内容使用虚拟滚动
  • 图片懒加载
  • 防抖处理高频状态更新
  1. 错误增强:
// 在processChunk中添加:
const processChunk = (chunk) => {
  try {
    // ...原有逻辑
  } catch (e) {
    setHistoryContent(prev => 
      prev.map(msg => 
        msg.id === botMessageId 
          ? { ...msg, text: msg.text + '❗渲染错误' }
          : msg
      )
    );
  }
};

该方案完整实现了流式Markdown内容的实时渲染、代码高亮与复制、图片自适应展示等功能,同时保持响应速度和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GISer_Jinger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值