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();
|
||||
119
frontend/index.html
Normal file
119
frontend/index.html
Normal file
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Knowledge Hub | Enterprise RAG</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Login Screen -->
|
||||
<div class="login-screen" id="login-screen">
|
||||
<div class="login-card">
|
||||
<div class="login-logo">
|
||||
<div class="logo-icon"><i class="fas fa-brain"></i></div>
|
||||
<h1>VibeCode AI</h1>
|
||||
<p>Enterprise Knowledge Hub</p>
|
||||
</div>
|
||||
<form id="login-form">
|
||||
<div class="input-group">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<input type="email" id="login-email" placeholder="Nhập email của bạn" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">
|
||||
<i class="fas fa-sign-in-alt"></i> Đăng nhập
|
||||
</button>
|
||||
</form>
|
||||
<div class="login-divider"><span>hoặc</span></div>
|
||||
<button class="sso-btn" id="sso-btn">
|
||||
<i class="fab fa-microsoft"></i> Đăng nhập Microsoft SSO
|
||||
</button>
|
||||
<p class="login-hint">Dùng tài khoản Microsoft 365 của công ty.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main App (hidden until login) -->
|
||||
<div class="app-container" id="app-container" style="display: none;">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo-area">
|
||||
<div class="logo-icon"><i class="fas fa-brain"></i></div>
|
||||
<h1>VibeCode AI</h1>
|
||||
</div>
|
||||
|
||||
<nav class="side-nav">
|
||||
<div class="nav-item active"><i class="fas fa-comments"></i> <span>Hỏi đáp RAG</span></div>
|
||||
<button class="sync-btn" id="sync-btn"><i class="fas fa-sync-alt"></i> Đồng bộ SharePoint</button>
|
||||
</nav>
|
||||
|
||||
<div class="sync-status" id="sync-status" style="display: none;">
|
||||
<div class="sync-spinner"><i class="fas fa-spinner fa-spin"></i></div>
|
||||
<span class="sync-text">Đang đồng bộ...</span>
|
||||
</div>
|
||||
|
||||
<div class="system-status">
|
||||
<div class="status-dot online"></div>
|
||||
<div class="status-text">OpenSearch: Online</div>
|
||||
</div>
|
||||
|
||||
<div class="user-info" id="user-info">
|
||||
<div class="user-avatar"><i class="fas fa-user"></i></div>
|
||||
<div class="user-details">
|
||||
<span class="user-name" id="user-name">-</span>
|
||||
<span class="user-role" id="user-role">-</span>
|
||||
</div>
|
||||
<button id="logout-btn" title="Đăng xuất"><i class="fas fa-sign-out-alt"></i></button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Chat Area -->
|
||||
<main class="chat-main">
|
||||
<header class="chat-header">
|
||||
<div class="header-info">
|
||||
<h2>SharePoint Intelligence</h2>
|
||||
<p>Hỏi đáp dựa trên cơ sở dữ liệu nội bộ của bạn</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="clear-chat" title="Xoá lịch sử"><i class="fas fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="messages-container" id="chat-window">
|
||||
<!-- Chào mừng -->
|
||||
<div class="message ai greeting">
|
||||
<div class="avatar"><i class="fas fa-robot"></i></div>
|
||||
<div class="content">
|
||||
<p>Xin chào! Tôi là trợ lý tri thức của bạn. Tôi đã sẵn sàng trả lời các câu hỏi dựa trên tài liệu từ SharePoint. Bạn muốn tìm hiểu điều gì hôm nay?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<footer class="chat-footer">
|
||||
<div class="input-wrapper">
|
||||
<textarea id="user-input" placeholder="Nhập câu hỏi của bạn tại đây..." rows="1"></textarea>
|
||||
<button id="send-btn"><i class="fas fa-paper-plane"></i></button>
|
||||
</div>
|
||||
<p class="disclaimer">AI có thể đưa ra câu trả lời chưa chính xác. Vui lòng kiểm tra lại nguồn trích dẫn.</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- Source Sidebar (Hidden by default) -->
|
||||
<section class="source-panel" id="source-panel">
|
||||
<div class="panel-header">
|
||||
<h3><i class="fas fa-book-open"></i> Nguồn trích dẫn</h3>
|
||||
<button id="close-panel"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="source-list" id="source-list">
|
||||
<!-- Nguồn sẽ được render ở đây -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
591
frontend/style.css
Normal file
591
frontend/style.css
Normal file
@@ -0,0 +1,591 @@
|
||||
:root {
|
||||
--bg-dark: #0f172a;
|
||||
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--primary: #06b6d4;
|
||||
--primary-glow: rgba(6, 182, 212, 0.3);
|
||||
--secondary: #8b5cf6;
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--ai-bubble: rgba(30, 41, 59, 0.7);
|
||||
--user-bubble: linear-gradient(135deg, #06b6d4, #3b82f6);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
/* Mesh Gradient cực kỳ Premium bằng CSS thuần */
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(6, 182, 212, 0.15) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 100%, rgba(6, 182, 212, 0.1) 0px, transparent 50%),
|
||||
radial-gradient(at 0% 100%, rgba(139, 92, 246, 0.1) 0px, transparent 50%);
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-main);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
width: 95vw;
|
||||
height: 90vh;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Sidebar Styling */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
border-right: 1px solid var(--glass-border);
|
||||
padding: 30px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--user-bubble);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 0 20px var(--primary-glow);
|
||||
}
|
||||
|
||||
.logo-area h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 14px 18px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nav-item:hover, .nav-item.active {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
border-left: 4px solid var(--primary);
|
||||
}
|
||||
|
||||
.system-status {
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 10px #10b981;
|
||||
}
|
||||
|
||||
/* Main Chat Area */
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 25px 40px;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-info h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.header-info p {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.header-actions button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.header-actions button:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--glass-border) transparent;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-thumb {
|
||||
background-color: var(--glass-border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
max-width: 85%;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai .avatar { background: rgba(255, 255, 255, 0.1); color: var(--primary); border: 1px solid var(--glass-border); }
|
||||
.user .avatar { background: var(--user-bubble); color: white; }
|
||||
|
||||
.content {
|
||||
background: var(--ai-bubble);
|
||||
padding: 16px 20px;
|
||||
border-radius: 18px;
|
||||
border-bottom-left-radius: 4px;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.user .content {
|
||||
background: var(--user-bubble);
|
||||
border-bottom-left-radius: 18px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.citation-tag {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
border: 1px solid rgba(6, 182, 212, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.citation-tag:hover {
|
||||
background: rgba(6, 182, 212, 0.2);
|
||||
}
|
||||
|
||||
/* Footer Input Area */
|
||||
.chat-footer {
|
||||
padding: 30px 40px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 15px var(--primary-glow);
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-main);
|
||||
resize: none;
|
||||
outline: none;
|
||||
padding: 10px 5px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
#send-btn {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
background: var(--user-bubble);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
#send-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 15px var(--primary-glow);
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Source Panel */
|
||||
.source-panel {
|
||||
position: absolute;
|
||||
right: -350px;
|
||||
top: 0;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
backdrop-filter: blur(25px);
|
||||
border-left: 1px solid var(--glass-border);
|
||||
transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 10;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.source-panel.active {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.source-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.source-item h4 { font-size: 14px; margin-bottom: 5px; color: var(--primary); }
|
||||
.source-item p { font-size: 12px; color: var(--text-muted); line-height: 1.4; }
|
||||
|
||||
/* Login Screen */
|
||||
.login-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(30px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 24px;
|
||||
padding: 50px 40px;
|
||||
width: 400px;
|
||||
text-align: center;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-logo .logo-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 20px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.login-logo h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-logo p {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 20px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.input-group:focus-within {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 15px var(--primary-glow);
|
||||
}
|
||||
|
||||
.input-group i {
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-main);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: var(--user-bubble);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 20px var(--primary-glow);
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-divider::before,
|
||||
.login-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--glass-border);
|
||||
}
|
||||
|
||||
.sso-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
color: var(--text-main);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sso-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* User Info */
|
||||
.user-info {
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: var(--text-main);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#logout-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
#logout-btn:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Sync Button */
|
||||
.sync-btn {
|
||||
width: 100%;
|
||||
padding: 12px 18px;
|
||||
margin-top: 10px;
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
border: 1px solid rgba(6, 182, 212, 0.2);
|
||||
border-radius: 12px;
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sync-btn:hover {
|
||||
background: rgba(6, 182, 212, 0.2);
|
||||
}
|
||||
|
||||
.sync-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Sync Status */
|
||||
.sync-status {
|
||||
padding: 12px;
|
||||
margin-top: 10px;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
border: 1px solid rgba(139, 92, 246, 0.2);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.sync-status.done {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.sync-status.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
Reference in New Issue
Block a user