Giới thiệu — Tại sao cần hiển thị tiến trình?
Khi bạn gửi một yêu cầu xử lý AI (ví dụ: phân tích tài liệu, tạo báo cáo dài, hoặc dịch thuật hàng nghìn trang), server có thể mất từ 5 giây đến 5 phút để hoàn thành. Nếu người dùng chỉ nhìn thấy một vòng xoay xoay không có thông tin gì, họ sẽ nghĩ ứng dụng bị treo và tắt đi.
Trong bài viết này, mình sẽ hướng dẫn bạn từng bước cách tạo một thanh tiến trình sống động sử dụng Server-Sent Events (SSE) — một công nghệ đơn giản mà hiệu quả, hoàn toàn miễn phí.
⚠️ Ảnh minh họa đề xuất: Chụp màn hình một ứng dụng web có thanh progress bar màu xanh chạy từ 0% đến 100%
Server-Sent Events (SSE) là gì?
Hãy tưởng tượng bạn đặt một cốc cà phê tại quán. Thay vì ngồi đợi 10 phút mà không biết còn bao lâu, nhân viên sẽ thông báo từng bước: "Đã xay cà phê xong", "Đang pha", "Còn 1 phút nữa". SSE hoạt động theo cách tương tự — server gửi thông tin cập nhật đến trình duyệt theo dòng chảy.
So với WebSocket, SSE chỉ có một chiều (server → client), nhưng đổi lại cài đặt cực kỳ đơn giản và hoạt động tốt với hầu hết các proxy.
Chuẩn bị dự án
Trước tiên, bạn cần có một tài khoản API. Mình khuyên dùng HolySheep AI vì:
- Tỷ giá chỉ ¥1 = $1 (tiết kiệm 85%+ so với các nhà cung cấp khác)
- Hỗ trợ WeChat/Alipay — thuận tiện cho người Việt
- Độ trễ trung bình <50ms
- Nhận tín dụng miễn phí ngay khi đăng ký
Giá tham khảo 2026 cho các mô hình phổ biến:
- DeepSeek V3.2: $0.42/MTok — rẻ nhất, phù hợp cho tác vụ dài
- Gemini 2.5 Flash: $2.50/MTok — cân bằng giữa tốc độ và chi phí
- GPT-4.1: $8/MTok — chất lượng cao
- Claude Sonnet 4.5: $15/MTok — premium option
⚠️ Ảnh minh họa đề xuất: Bảng so sánh giá các mô hình AI năm 2026
Phần 1 — Backend: Tạo API với SSE
1.1. Cài đặt thư viện cần thiết
npm install express cors
npm install --save-dev nodemon
1.2. Tạo server Express với endpoint SSE
Tạo file server.js với nội dung sau:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// Endpoint SSE để gửi tiến trình
app.get('/api/analyze-progress', (req, res) => {
// Thiết lập header cho SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
// Gửi sự kiện ban đầu
res.write('event: connected\n');
res.write('data: {"status": "connected"}\n\n');
let progress = 0;
// Hàm gửi cập nhật tiến trình
const sendProgress = (percent, message) => {
const data = JSON.stringify({ percent, message });
res.write(event: progress\ndata: ${data}\n\n);
};
// Mô phỏng quá trình xử lý AI dài
const processTask = async () => {
// Bước 1: Tiếp nhận dữ liệu (0-20%)
sendProgress(10, 'Đang tiếp nhận dữ liệu...');
await sleep(800);
sendProgress(20, 'Đã nhận 2,500 từ');
await sleep(600);
// Bước 2: Gọi API AI (20-70%)
sendProgress(30, 'Đang phân tích nội dung với AI...');
await sleep(1000);
sendProgress(50, 'AI đang xử lý batch 1/3');
await sleep(1200);
sendProgress(70, 'Hoàn thành phân tích ngữ nghĩa');
await sleep(800);
// Bước 3: Tổng hợp kết quả (70-100%)
sendProgress(85, 'Đang tạo báo cáo tổng hợp...');
await sleep(1000);
sendProgress(95, 'Định dạng kết quả cuối cùng...');
await sleep(500);
sendProgress(100, 'Hoàn thành!');
// Kết thúc kết nối sau 1 giây
setTimeout(() => {
res.end();
}, 1000);
};
processTask();
// Giữ kết nối sống
const keepAlive = setInterval(() => {
res.write(': keepalive\n\n');
}, 15000);
// Dọn dẹp khi client ngắt kết nối
req.on('close', () => {
clearInterval(keepAlive);
res.end();
});
});
// Endpoint gọi thực sự đến HolySheep AI
app.post('/api/analyze-with-ai', async (req, res) => {
const { text } = req.body;
try {
// Gọi HolySheep AI API
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${process.env.HOLYSHEEP_API_KEY}
},
body: JSON.stringify({
model: 'deepseek-v3.2',
messages: [{
role: 'user',
content: Phân tích văn bản sau và trả về tóm tắt 3 điểm chính:\n\n${text}
}]
})
});
const data = await response.json();
res.json({ success: true, result: data });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(Server chạy tại http://localhost:${PORT});
});
Phần 2 — Frontend: Hiển thị thanh tiến trình
Tạo file index.html với giao diện người dùng:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo SSE Progress Indicator</title>
<style>
* {
box-sizing: border-box;
font-family: 'Segoe UI', sans-serif;
}
body {
max-width: 700px;
margin: 40px auto;
padding: 20px;
background: #f5f7fa;
}
.container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
h1 {
color: #1a1a2e;
margin-bottom: 20px;
}
textarea {
width: 100%;
height: 150px;
padding: 15px;
border: 2px solid #e1e5eb;
border-radius: 8px;
font-size: 15px;
resize: vertical;
margin-bottom: 20px;
}
textarea:focus {
outline: none;
border-color: #4361ee;
}
button {
background: linear-gradient(135deg, #4361ee, #3a0ca3);
color: white;
border: none;
padding: 14px 32px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
/* Thanh tiến trình */
.progress-container {
margin-top: 25px;
display: none;
}
.progress-container.active {
display: block;
}
.progress-bar-bg {
background: #e9ecef;
border-radius: 10px;
height: 24px;
overflow: hidden;
}
.progress-bar-fill {
background: linear-gradient(90deg, #4361ee, #7209b7);
height: 100%;
width: 0%;
border-radius: 10px;
transition: width 0.3s ease;
}
.progress-text {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 14px;
color: #666;
}
.progress-message {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
color: #495057;
}
.result-box {
margin-top: 20px;
padding: 20px;
background: #e8f5e9;
border-left: 4px solid #4caf50;
border-radius: 4px;
display: none;
}
.result-box.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 Demo SSE Progress Indicator</h1>
<textarea id="inputText" placeholder="Nhập văn bản cần phân tích (tối thiểu 100 từ)...">Trí tuệ nhân tạo (AI) đang thay đổi cách chúng ta làm việc và sống. Từ chatbot đến xe tự lái, AI được ứng dụng trong mọi lĩnh vực. Công nghệ học sâu (deep learning) cho phép máy tính học từ dữ liệu lớn và đưa ra dự đoán chính xác. Các công ty lớn đang đầu tư mạnh vào AI để cải thiện sản phẩm và dịch vụ của họ.</textarea>
<button id="startBtn" onclick="startAnalysis()">Bắt đầu phân tích</button>
<div class="progress-container" id="progressContainer">
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressBar"></div>
</div>
<div class="progress-text">
<span id="percentText">0%</span>
<span id="timeText">Đang xử lý...</span>
</div>
<div class="progress-message" id="progressMessage">
Đang kết nối đến server...
</div>
</div>
<div class="result-box" id="resultBox">
<strong>✅ Kết quả phân tích:</strong>
<div id="resultContent" style="margin-top: 10px;"></div>
</div>
</div>
<script>
let eventSource = null;
let startTime = null;
async function startAnalysis() {
const text = document.getElementById('inputText').value;
const btn = document.getElementById('startBtn');
if (text.length < 100) {
alert('Vui lòng nhập ít nhất 100 từ để phân tích');
return;
}
// Reset UI
btn.disabled = true;
btn.textContent = 'Đang xử lý...';
document.getElementById('progressContainer').classList.add('active');
document.getElementById('resultBox').classList.remove('active');
document.getElementById('progressBar').style.width = '0%';
document.getElementById('percentText').textContent = '0%';
startTime = Date.now();
// Kết nối SSE
eventSource = new EventSource('/api/analyze-progress');
eventSource.addEventListener('progress', (event) => {
const data = JSON.parse(event.data);
updateProgress(data.percent, data.message);
});
eventSource.addEventListener('connected', (event) => {
document.getElementById('progressMessage').textContent = 'Đã kết nối thành công!';
});
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
document.getElementById('progressMessage').textContent = '❌ Lỗi kết nối!';
eventSource.close();
btn.disabled = false;
btn.textContent = 'Thử lại';
};
eventSource.onclose = () => {
// Gọi API thực sự sau khi hoàn tất
fetch('/api/analyze-with-ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
})
.then(res => res.json())
.then(data => {
document.getElementById('resultContent').textContent =
data.result?.choices?.[0]?.message?.content || 'Hoàn thành!';
document.getElementById('resultBox').classList.add('active');
btn.disabled = false;
btn.textContent = 'Phân tích lại';
})
.catch(err => {
console.error('API Error:', err);
btn.disabled = false;
btn.textContent = 'Thử lại';
});
};
}
function updateProgress(percent, message) {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
document.getElementById('progressBar').style.width = percent + '%';
document.getElementById('percentText').textContent = percent + '%';
document.getElementById('timeText').textContent = Đã xử lý: ${elapsed}s;
document.getElementById('progressMessage').textContent = message;
}
</script>
</body>
</html>
Phần 3 — Chạy và kiểm tra
3.1. Khởi động server
# Terminal 1: Chạy server
npx nodemon server.js
Terminal 2: Mở trình duyệt
Truy cập: http://localhost:3000
⚠️ Ảnh minh họa đề xuất: Chụp màn hình terminal hiển thị "Server chạy tại http://localhost:3000"
3.2. Thiết lập biến môi trường
# Tạo file .env
echo "HOLYSHEEP_API_KEY=YOUR_HOLYSHEEP_API_KEY" > .env
echo "PORT=3000" >> .env
Để lấy API key, bạn cần đăng ký tài khoản HolySheep và vào Dashboard → API Keys → Tạo key mới.
⚠️ Ảnh minh họa đề xuất: Chụp màn hình trang Dashboard của HolySheep AI với nút tạo API key được highlight