Files
poc_system/frontend/app.js
phuongtc 78372d18ee Phase 8 Complete: Sync Audit Log + Frontend Integration
- Thêm audit/sync_audit.py: Ghi lịch sử sync vào audit/sync_log.json
- Thêm API endpoints: /sync/history, /sync/history/{run_id}
- Frontend: Nút 'Lịch sử đồng bộ' + panel expand chi tiết từng file
- Sửa frontend served từ backend (http://localhost:8000)
- Cập nhật DEPLOYMENT_GUIDE với hướng dẫn chạy localhost
- Cập nhật ARCHITECTURE_MAP: Phase 8 hoàn thành
2026-05-11 08:49:10 +00:00

362 lines
13 KiB
JavaScript

// API Base URL (relative khi served từ backend)
const API_BASE = window.location.origin;
// DOM Elements
const loginScreen = document.getElementById('login-screen');
const appContainer = document.getElementById('app-container');
const loginForm = document.getElementById('login-form');
const loginEmail = document.getElementById('login-email');
const chatWindow = document.getElementById('chat-window');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const sourcePanel = document.getElementById('source-panel');
const sourceList = document.getElementById('source-list');
const closePanel = document.getElementById('close-panel');
const clearChatBtn = document.getElementById('clear-chat');
const userName = document.getElementById('user-name');
const userRole = document.getElementById('user-role');
const logoutBtn = document.getElementById('logout-btn');
const syncBtn = document.getElementById('sync-btn');
const syncStatus = document.getElementById('sync-status');
const ssoBtn = document.getElementById('sso-btn');
const historyBtn = document.getElementById('history-btn');
const historyPanel = document.getElementById('history-panel');
const historyList = document.getElementById('history-list');
const closeHistory = document.getElementById('close-history');
let chatHistory = [];
let currentUser = null;
// ====== AUTH ======
function checkLogin() {
// Kiểm tra SSO callback (user data trong URL)
const params = new URLSearchParams(window.location.search);
const userData = params.get('user');
if (userData) {
try {
currentUser = JSON.parse(decodeURIComponent(userData));
localStorage.setItem('vibecode_user', JSON.stringify(currentUser));
window.history.replaceState({}, '', '/'); // Xóa query param
showApp();
return;
} catch (e) {
console.error('Parse SSO user data failed:', e);
}
}
// Kiểm tra localStorage
const saved = localStorage.getItem('vibecode_user');
if (saved) {
currentUser = JSON.parse(saved);
showApp();
}
}
function showApp() {
loginScreen.style.display = 'none';
appContainer.style.display = 'flex';
userName.textContent = currentUser.display_name;
userRole.textContent = currentUser.role;
}
function showLogin() {
loginScreen.style.display = 'flex';
appContainer.style.display = 'none';
currentUser = null;
localStorage.removeItem('vibecode_user');
}
loginForm.onsubmit = async (e) => {
e.preventDefault();
const email = loginEmail.value.trim();
if (!email) return;
try {
const response = await fetch(`${API_BASE}/auth/login-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
if (!response.ok) {
const err = await response.json();
alert(err.detail || 'Đăng nhập thất bại');
return;
}
currentUser = await response.json();
localStorage.setItem('vibecode_user', JSON.stringify(currentUser));
showApp();
} catch (error) {
console.error('Login error:', error);
alert('Không thể kết nối tới server. Vui lòng đảm bảo Backend đang chạy.');
}
};
logoutBtn.onclick = () => {
chatHistory = [];
chatWindow.innerHTML = '';
showLogin();
};
// SSO Login
ssoBtn.onclick = () => {
window.location.href = `${API_BASE}/auth/login`;
};
// ====== CHAT ======
// Tự động giãn nở ô nhập liệu
userInput.addEventListener('input', () => {
userInput.style.height = 'auto';
userInput.style.height = (userInput.scrollHeight) + 'px';
});
async function sendMessage() {
const text = userInput.value.trim();
if (!text) return;
appendMessage('user', text);
userInput.value = '';
userInput.style.height = 'auto';
const loadingId = appendMessage('ai', '<i class="fas fa-spinner fa-spin"></i> AI đang phân tích dữ liệu...');
try {
const headers = {
'Content-Type': 'application/json'
};
if (currentUser) {
headers['X-User-Email'] = currentUser.email;
headers['X-User-Role'] = currentUser.role;
}
const response = await fetch(`${API_BASE}/chat`, {
method: 'POST',
headers: headers,
body: JSON.stringify({
query: text,
history: chatHistory
})
});
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const data = await response.json();
updateMessage(loadingId, data.answer, data.sources);
chatHistory.push({ role: 'user', content: text });
chatHistory.push({ role: 'assistant', content: data.answer });
if (chatHistory.length > 6) chatHistory = chatHistory.slice(-6);
} catch (error) {
console.error("Chat error:", error);
updateMessage(loadingId, "⚠️ Lỗi kết nối tới máy chủ AI. Vui lòng đảm bảo Backend đang chạy tại port 8000.");
}
}
function appendMessage(role, text) {
const id = Date.now();
const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`;
msgDiv.id = `msg-${id}`;
const icon = role === 'ai' ? 'robot' : 'user';
msgDiv.innerHTML = `
<div class="avatar"><i class="fas fa-${icon}"></i></div>
<div class="content">${text}</div>
`;
chatWindow.appendChild(msgDiv);
chatWindow.scrollTop = chatWindow.scrollHeight;
return id;
}
function updateMessage(id, text, sources = []) {
const msgDiv = document.getElementById(`msg-${id}`);
if (!msgDiv) return;
const contentDiv = msgDiv.querySelector('.content');
contentDiv.innerHTML = text.replace(/\n/g, '<br>');
if (sources && sources.length > 0) {
const tagDiv = document.createElement('div');
tagDiv.style.marginTop = '15px';
tagDiv.style.display = 'flex';
tagDiv.style.gap = '8px';
tagDiv.style.flexWrap = 'wrap';
sources.forEach((src, idx) => {
const tag = document.createElement('span');
tag.className = 'citation-tag';
tag.innerHTML = `<i class="fas fa-file-pdf"></i> Nguồn ${idx + 1}`;
tag.onclick = (e) => {
e.stopPropagation();
showSources(sources);
};
tagDiv.appendChild(tag);
});
contentDiv.appendChild(tagDiv);
}
chatWindow.scrollTop = chatWindow.scrollHeight;
}
function showSources(sources) {
sourceList.innerHTML = '';
sources.forEach(src => {
const item = document.createElement('div');
item.className = 'source-item';
item.innerHTML = `
<h4><i class="fas fa-file-alt"></i> ${src.file_name}</h4>
<p><strong>Vị trí:</strong> Trang ${src.page}</p>
${src.url ? `<a href="${src.url}" target="_blank" style="color: #06b6d4; font-size: 11px; text-decoration: none; display: block; margin-top: 5px;">
<i class="fas fa-external-link-alt"></i> Xem trên SharePoint
</a>` : ''}
${src.download_url ? `<a href="${src.download_url}" target="_blank" style="color: #10b981; font-size: 11px; text-decoration: none; display: block; margin-top: 5px;">
<i class="fas fa-download"></i> Tải xuống
</a>` : ''}
`;
sourceList.appendChild(item);
});
sourcePanel.classList.add('active');
}
// Event Listeners
closePanel.onclick = () => sourcePanel.classList.remove('active');
sendBtn.onclick = sendMessage;
userInput.onkeydown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
clearChatBtn.onclick = () => {
chatWindow.innerHTML = '';
chatHistory = [];
appendMessage('ai', 'Lịch sử chat đã được làm sạch. Tôi có thể giúp gì tiếp cho bạn?');
};
// ====== SYNC ======
async function triggerSync() {
syncBtn.disabled = true;
syncStatus.style.display = 'flex';
syncStatus.className = 'sync-status';
syncStatus.querySelector('.sync-text').textContent = 'Đang đồng bộ...';
try {
const response = await fetch(`${API_BASE}/sync`, { method: 'POST' });
const data = await response.json();
if (data.status === 'already_running') {
syncStatus.querySelector('.sync-text').textContent = 'Đồng bộ đang chạy...';
} else {
syncStatus.querySelector('.sync-text').textContent = 'Đang xử lý...';
pollSyncStatus();
}
} catch (error) {
syncStatus.className = 'sync-status error';
syncStatus.querySelector('.sync-text').textContent = 'Lỗi kết nối server';
syncBtn.disabled = false;
}
}
async function pollSyncStatus() {
try {
const response = await fetch(`${API_BASE}/sync/status`);
const data = await response.json();
if (data.running) {
const count = data.processed + data.skipped;
syncStatus.querySelector('.sync-text').textContent = `Đang xử lý... (${count} file)`;
setTimeout(pollSyncStatus, 2000);
} else {
syncStatus.className = 'sync-status done';
syncStatus.querySelector('.sync-text').textContent =
`Xong! ${data.processed} file đã nạp, ${data.skipped} bỏ qua`;
syncBtn.disabled = false;
setTimeout(() => { syncStatus.style.display = 'none'; }, 5000);
}
} catch (error) {
syncStatus.className = 'sync-status error';
syncStatus.querySelector('.sync-text').textContent = 'Lỗi kiểm tra trạng thái';
syncBtn.disabled = false;
}
}
syncBtn.onclick = triggerSync;
// ====== SYNC HISTORY ======
async function loadSyncHistory() {
try {
const response = await fetch(`${API_BASE}/sync/history?limit=20`);
const data = await response.json();
historyList.innerHTML = '';
if (!data.runs || data.runs.length === 0) {
historyList.innerHTML = '<p style="color: var(--text-muted); font-size: 13px; text-align: center; padding: 20px;">Chưa có lần đồng bộ nào.</p>';
return;
}
data.runs.forEach(run => {
const item = document.createElement('div');
item.className = 'history-item';
const time = new Date(run.started_at).toLocaleString('vi-VN');
const statusClass = run.status;
const statusText = run.status === 'completed' ? 'Hoàn thành' :
run.status === 'failed' ? 'Thất bại' : 'Đang chạy';
item.innerHTML = `
<div class="history-header">
<span class="history-time">${time}</span>
<span class="history-status ${statusClass}">${statusText}</span>
</div>
<div class="history-summary">
<span><i class="fas fa-check-circle" style="color: #10b981"></i> ${run.summary.processed} nạp</span>
<span><i class="fas fa-forward" style="color: #eab308"></i> ${run.summary.skipped} bỏ</span>
<span><i class="fas fa-exclamation-circle" style="color: #ef4444"></i> ${run.summary.errors} lỗi</span>
</div>
<div class="history-files">
${run.files.map(f => `
<div class="history-file">
<span class="history-file-name" title="${f.name}">${f.name}</span>
<span class="history-file-status ${f.status}">${f.chunks > 0 ? f.chunks + ' chunks' : f.status}</span>
</div>
`).join('')}
${run.errors.length > 0 ? `
<div style="margin-top: 8px; font-size: 11px; color: #ef4444;">
${run.errors.map(e => `<div>⚠ ${e}</div>`).join('')}
</div>
` : ''}
</div>
`;
item.onclick = () => item.classList.toggle('expanded');
historyList.appendChild(item);
});
} catch (error) {
console.error('Load history error:', error);
historyList.innerHTML = '<p style="color: #ef4444; font-size: 13px; text-align: center;">Lỗi tải lịch sử.</p>';
}
}
historyBtn.onclick = () => {
historyPanel.classList.toggle('active');
if (historyPanel.classList.contains('active')) {
loadSyncHistory();
}
};
closeHistory.onclick = () => historyPanel.classList.remove('active');
// Init
checkLogin();