DoAn / core /api /static /index.html
Nguyen Ba Hung
change retrieval
ba44ab9
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HUST RAG — Trợ lý Học vụ</title>
<meta name="description" content="Hệ thống hỏi đáp quy chế sinh viên Đại học Bách khoa Hà Nội">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎓</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']]
},
startup: { typeset: false } // We'll trigger it manually
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script>
</head>
<body>
<div id="app">
<!-- Header -->
<header>
<div class="logo">BK</div>
<div class="header-text">
<h1>HUST RAG Assistant</h1>
<p>Trợ lý học vụ Đại học Bách khoa Hà Nội</p>
</div>
<div class="status-dot" title="Online"></div>
</header>
<!-- Welcome Screen (shown initially) -->
<div id="welcome">
<div class="icon">🎓</div>
<h2>Xin chào!</h2>
<p>Tôi là trợ lý học vụ HUST. Hãy hỏi tôi bất kỳ câu hỏi nào về quy chế, quy định sinh viên.</p>
<div class="suggestions">
<button onclick="askSuggestion(this)">Điều kiện tốt nghiệp đại học?</button>
<button onclick="askSuggestion(this)">Cách tính điểm học kỳ?</button>
<button onclick="askSuggestion(this)">Điều kiện đổi ngành?</button>
<button onclick="askSuggestion(this)">Đăng ký hoãn thi thế nào?</button>
</div>
</div>
<!-- Chat Messages (hidden initially) -->
<div id="messages" style="display: none;"></div>
<!-- Input Area -->
<div id="input-area">
<div class="input-row">
<textarea id="input" rows="1" placeholder="Nhập câu hỏi của bạn..."
onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button id="send-btn" onclick="sendMessage()" title="Gửi">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
<div class="hint">Enter để gửi · Shift+Enter để xuống dòng</div>
</div>
</div>
<script>
// ====== CẤU HÌNH ======
// Nếu mở HTML trực tiếp (double-click file): dùng URL đầy đủ
// Nếu mở qua server (http://127.0.0.1:8000): để rỗng ""
const API_BASE = "";
const messagesEl = document.getElementById('messages');
const welcomeEl = document.getElementById('welcome');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send-btn');
let isStreaming = false;
let _apiKey = "";
// Lấy API key từ server khi trang load
(async function loadConfig() {
try {
const res = await fetch(`${API_BASE}/api/config`);
const data = await res.json();
_apiKey = data.api_key || "";
} catch (e) {
console.warn("Could not load API config:", e);
}
})();
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function askSuggestion(btn) {
inputEl.value = btn.textContent;
sendMessage();
}
function renderMarkdown(text) {
// 1. Loại bỏ thẻ <think> (Che giấu suy nghĩ của model)
text = text.replace(/<think>[\s\S]*?<\/think>\n?/g, '');
if (text.includes('<think>')) {
// Nếu thẻ think chưa được đóng (đang stream)
text = text.replace(/<think>[\s\S]*/g, '<span style="color: #666; font-style: italic;">[Đang suy nghĩ...]</span>');
}
// 2. Bảo vệ các công thức toán (MathJax) khỏi bị lỗi khi render Markdown
let mathBlocks = [];
// Protect block math: $$...$$ and \[...\]
text = text.replace(/\$\$([\s\S]*?)\$\$/g, function(match) {
mathBlocks.push(match);
return `__MATH_BLOCK_${mathBlocks.length - 1}__`;
});
text = text.replace(/\\\[([\s\S]*?)\\\]/g, function(match) {
mathBlocks.push(match);
return `__MATH_BLOCK_${mathBlocks.length - 1}__`;
});
// Protect inline math: \(...\) and $...$
text = text.replace(/\\\(([\s\S]*?)\\\)/g, function(match) {
mathBlocks.push(match);
return `__MATH_BLOCK_${mathBlocks.length - 1}__`;
});
text = text.replace(/\$([^\$\n]+?)\$/g, function(match) {
mathBlocks.push(match);
return `__MATH_BLOCK_${mathBlocks.length - 1}__`;
});
// 3. Render Markdown cơ bản
let html = text
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^[-*] (.+)$/gm, '<li>$1</li>')
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
.replace(/\n{2,}/g, '</p><p>')
.replace(/\n/g, '<br>');
html = html.replace(/((?:<li>.*?<\/li>\s*)+)/gs, '<ul>$1</ul>');
// Khôi phục lại công thức toán
for (let i = 0; i < mathBlocks.length; i++) {
html = html.replace(`__MATH_BLOCK_${i}__`, mathBlocks[i]);
}
return `<p>${html}</p>`.replace(/<p><\/p>/g, '');
}
async function sendMessage() {
const text = inputEl.value.trim();
if (!text || isStreaming) return;
welcomeEl.style.display = 'none';
messagesEl.style.display = 'flex';
const userMsg = document.createElement('div');
userMsg.className = 'msg user';
userMsg.textContent = text;
messagesEl.appendChild(userMsg);
inputEl.value = '';
inputEl.style.height = 'auto';
scrollToBottom();
const botMsg = document.createElement('div');
botMsg.className = 'msg bot';
botMsg.innerHTML = `
<div class="label">Trợ lý HUST</div>
<div class="content">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>`;
messagesEl.appendChild(botMsg);
scrollToBottom();
const contentEl = botMsg.querySelector('.content');
isStreaming = true;
sendBtn.disabled = true;
try {
const headers = { 'Content-Type': 'application/json' };
if (_apiKey) headers['X-API-Key'] = _apiKey;
const res = await fetch(`${API_BASE}/api/chat`, {
method: 'POST',
headers: headers,
body: JSON.stringify({ message: text }),
});
if (res.headers.get('content-type')?.includes('application/json')) {
const data = await res.json();
contentEl.innerHTML = renderMarkdown(data.answer || data.error || 'Không có phản hồi.');
scrollToBottom();
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const payload = line.slice(6).trim();
if (payload === '[DONE]') continue;
try {
const parsed = JSON.parse(payload);
if (parsed.token) {
fullText += parsed.token;
contentEl.innerHTML = renderMarkdown(fullText);
scrollToBottom();
}
} catch {}
}
}
}
if (fullText) {
contentEl.innerHTML = renderMarkdown(fullText);
// Chạy lại MathJax để render công thức Toán sau khi đã stream hoàn tất
if (window.MathJax && window.MathJax.typesetPromise) {
try {
await window.MathJax.typesetPromise([contentEl]);
} catch(err) {}
}
}
} catch (err) {
contentEl.innerHTML = '<span style="color: var(--red-accent)">Lỗi kết nối. Vui lòng thử lại.</span>';
} finally {
isStreaming = false;
sendBtn.disabled = false;
scrollToBottom();
inputEl.focus();
}
}
function scrollToBottom() {
requestAnimationFrame(() => {
messagesEl.scrollTop = messagesEl.scrollHeight;
});
}
inputEl.focus();
</script>
</body>
</html>