292 lines
9.5 KiB
JavaScript
292 lines
9.5 KiB
JavaScript
// 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();
|