Xu ly SSO
This commit is contained in:
291
frontend/app.js
Normal file
291
frontend/app.js
Normal file
@@ -0,0 +1,291 @@
|
||||
// API Base URL
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
|
||||
// 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');
|
||||
|
||||
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;
|
||||
|
||||
// Init
|
||||
checkLogin();
|
||||
Reference in New Issue
Block a user