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会优化这一点。
总结步骤:
- 在React项目中安装react-markdown、remark-gfm、rehype-highlight、highlight.js等依赖。
- 修改现有的状态更新逻辑,将content作为Markdown字符串保存。
- 使用react-markdown组件渲染消息的text字段,配置插件支持GFM(表格、删除线等)和代码高亮。
- 自定义图片渲染,确保图片正确显示,并处理可能的样式问题。
- 添加代码块的复制按钮,通过自定义渲染器实现。
- 调整CSS样式,允许文本选择和复制,处理空白字符。
- 测试流式响应中的Markdown解析,确保换行符、图片、代码块等正确显示。
- 处理可能的转义字符,如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>`);
关键功能说明:
- 流式处理优化:
- 新增缓冲区(buffer)处理不完整的JSON数据块
- 使用
replace()
处理特殊字符转义(\n, \", \u0026等) - 支持多个JSON对象的分割处理
- Markdown增强:
- 集成remarkGfm支持表格、删除线等扩展语法
- 使用rehypeHighlight实现代码语法高亮
- 自定义图片组件添加圆角和自适应宽度
- 代码复制功能:
- 为代码块添加浮动复制按钮
- 使用navigator.clipboard实现一键复制
- 代码块保持原始格式(pre-wrap)
- 样式优化:
- 用户可选择文本(user-select: text)
- 流式响应指示动画
- 响应式图片显示
- 暗色代码主题(github-dark.css)
使用注意事项:
- 安装依赖:
npm install react-markdown remark-gfm rehype-highlight highlight.js
- 安全性处理:
- 自动过滤危险HTML标签(默认行为)
- 推荐添加XSS过滤库(如dompurify)
- 限制图片域名白名单
- 性能优化:
- 对长内容使用虚拟滚动
- 图片懒加载
- 防抖处理高频状态更新
- 错误增强:
// 在processChunk中添加:
const processChunk = (chunk) => {
try {
// ...原有逻辑
} catch (e) {
setHistoryContent(prev =>
prev.map(msg =>
msg.id === botMessageId
? { ...msg, text: msg.text + '❗渲染错误' }
: msg
)
);
}
};
该方案完整实现了流式Markdown内容的实时渲染、代码高亮与复制、图片自适应展示等功能,同时保持响应速度和用户体验。