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ì:

Giá tham khảo 2026 cho các mô hình phổ biến:

⚠️ Ả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