Để thực hiện việc gửi yêu cầu lưu lượng và hiển thị văn bản từ AI một cách từng ký tự, bạn có thể làm theo các bước sau:
- Gửi yêu cầu lưu lượng: Sử dụng fetch để gọi đến API, trả về đối tượng Response chứa ReadableStream.
- Lấy bộ đọc lưu lượng: Sử dụng phương thức getReader() trên response.body để lấy một instance của ReadableStreamDefaultReader.
- Đọc dữ liệu từng phần: Sử dụng vòng lặp while hoặc for await để đọc dữ liệu từ bộ đọc, nhận được các khối dữ liệu dưới dạng Uint8Array.
- Giải mã và hiển thị: Sử dụng TextDecoder để chuyển đổi dữ liệu nhị phân thành chuỗi, sau đó cập nhật từng phần lên trang web mà không cần đợi toàn bộ dữ liệu hoàn thành.
Gửi yêu cầu lưu lượng
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: 'Xin chào, AI.' })
});
// response.body là một ReadableStream
Lấy và sử dụng bộ đọc
const reader = response.body.getReader(); // Khóa luồng, lấy bộ đọc
const decoder = new TextDecoder('utf-8'); // Giải mã từ Uint8Array sang chuỗi
let done = false;
while (!done) {
const { value, done: streamDone } = await reader.read();
done = streamDone;
if (value) {
const chunkText = decoder.decode(value, { stream: true });
// Đã nhận được một đoạn chuỗi chunkText
displayChunk(chunkText);
}
}
Hiển thị dữ liệu khi đọc
<div id="output"></div>
<script>
function displayChunk(text) {
const output = document.getElementById('output');
output.textContent += text; // Hoặc sử dụng output.innerHTML += để thêm và định dạng lại
}
</script>
Ví dụ với React
import React, { useState, useEffect } from 'react';
function StreamedText({ query }) {
const [content, setContent] = useState('');
useEffect(() => {
let aborted = false;
async function fetchData() {
setContent('');
const res = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ query }) });
const reader = res.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done && !aborted) {
const { value, done: streamDone } = await reader.read();
done = streamDone;
if (value) {
const chunk = decoder.decode(value, { stream: true });
// Thêm nội dung mới
setContent(prev => prev + chunk);
}
}
}
fetchData();
return () => { aborted = true; };
}, [query]);
return <div>{content}</div>;
}
export default StreamedText;
Lưu ý và tối ưu hóa
- Xử lý lỗi: Bắt lỗi từ reader.read() hoặc fetch để hiển thị tùy chọn thử lại.
- Tối ưu hiệu năng: Nếu lượng dữ liệu lớn, cân nhắc cập nhật giao diện mỗi khi tích lũy đủ độ dài nhất định để tránh quá nhiều lần render.
- Tương thích: Safari hỗ trợ không đầy đủ cho API lưu lượng, nếu cần tương thích hãy sử dụng polyfill hoặc quay lại fetch().then(res => res.text()).
- Dữ liệu lưu lượng JSON: Nếu backend trả về luồng đối tượng JSON được phân cách bằng dấu xuống dòng, sau khi giải mã, hãy cắt chuỗi theo \\n và xử lý từng đối tượng JSON riêng biệt.