Tiến độ cho tác vụ dài và truyền trực tiếp câu trả lời AI
Xuất, nhập, mã hóa và tác vụ AI: thanh tiến độ đi theo công việc thật, kết quả hiện ngay khi xong, và câu trả lời hiện ra ngay khi đang được viết.
Xuất dữ liệu, mã hóa video, tạo báo cáo, nhập dữ liệu và tác vụ AI có thể mất từ mười giây đến mười phút. Một biểu tượng xoay không có chút phản hồi nào khiến người dùng tải lại, bấm lần nữa hoặc bỏ đi. Hiển thị tiến độ thật, và kết quả ngay khi sẵn sàng, giữ họ ở lại trang. Cách này cũng dùng cho tính năng AI: truyền câu trả lời của trợ lý ngay khi đang được viết, kể cả khi công việc chạy trong một job nền.
Người dùng nhận được gì
- Thanh tiến độ đi theo công việc thật: “Đang xử lý 3.200 trên 10.000 dòng”.
- Liên kết tải xuống hoặc kết quả hiện ra ngay khi job xong, kể cả ở tab khác.
- Câu trả lời AI hiện ra dần dần thay vì sau một lúc chờ lâu.
- Có thể rời trang: họ sẽ được báo khi xong.
Dùng những gì
- Xác thực người dùng và
sendToUser, để cập nhật của job chỉ tới người đã khởi chạy nó, trên mọi tab. - Kênh riêng tư (
private-job-<id>) khi nhiều người cùng theo dõi một job. - Worker bằng bất kỳ ngôn ngữ nào: thư viện máy chủ Pusher chính thức cho Node.js, PHP, Python, Go, Ruby, Java và .NET đều phát được từ worker của hàng đợi.
- Thông báo đẩy để báo cho người dùng đã đóng ứng dụng rằng một job dài đã xong.
Bước 1: khởi chạy job, nhận ID
app.post('/exports', requireLogin, async (req, res) => {
const job = await queue.add('export', { userId: req.user.id, filters: req.body });
res.status(202).json({ jobId: job.id });
});Bước 2: báo tiến độ từ worker
Báo tiến độ ở những mốc có ý nghĩa, không phải ở từng dòng:
worker.process('export', async (job) => {
const user = String(job.data.userId);
const rows = await db.orders.query(job.data.filters);
for (let i = 0; i < rows.length; i += 1000) {
await writeChunk(rows.slice(i, i + 1000));
await pusher.sendToUser(user, 'job-progress', { jobId: job.id, done: Math.min(i + 1000, rows.length), total: rows.length });
}
const url = await uploadAndSign();
await pusher.sendToUser(user, 'job-done', { jobId: job.id, url });
});Tương tự trong worker Python dùng gói pusher:
pusher_client.send_to_user(str(user_id), "job-progress", {"jobId": job_id, "done": done, "total": total})Bước 3: hiển thị trong trình duyệt
Khi người dùng đã đăng nhập trên socket (xem thông báo trong ứng dụng):
const { jobId } = await fetch('/exports', { method: 'POST', body: form }).then((r) => r.json());
pusher.user.bind('job-progress', (p) => {
if (p.jobId === jobId) progress.set(p.done / p.total);
});
pusher.user.bind('job-done', (p) => {
if (p.jobId === jobId) showDownload(p.url);
});Truyền câu trả lời AI
Khi mô hình AI viết câu trả lời trong một job nền, hãy chuyển kết quả cho người dùng theo từng nhóm nhỏ, để một câu trả lời dài chỉ là vài chục tin nhắn chứ không phải hàng nghìn:
let buffer = '';
let last = Date.now();
for await (const chunk of llm.stream(prompt)) {
buffer += chunk.text;
if (Date.now() - last > 150 || buffer.length > 2000) {
await pusher.sendToUser(user, 'ai-delta', { id: answerId, text: buffer });
buffer = '';
last = Date.now();
}
}
await pusher.sendToUser(user, 'ai-done', { id: answerId, text: buffer });pusher.user.bind('ai-delta', ({ id, text }) => answers.get(id).append(text));
pusher.user.bind('ai-done', ({ id, text }) => answers.get(id).append(text).finish());Lưu cả câu trả lời đầy đủ trên máy chủ để nó vẫn còn sau khi tải lại.
Mẹo
- Mỗi cập nhật là một tin nhắn. Cập nhật mỗi 1–5% hoặc mỗi 150 mili giây trông đã mượt và giữ mỗi job ở mức vài chục tin nhắn.
- Đăng ký trước khi khởi chạy job, hoặc lấy trạng thái job một lần khi mở trang, để không lỡ cập nhật được gửi trước khi trang kết nối.
- Nếu người dùng đã đóng tab, gửi thông báo đẩy theo hướng dẫn thông báo đẩy khi job xong.