Download core/api/static/index.html from hungnha/DoAn: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/hungnha/DoAn/resolve/main/core/api/static/index.html
- Command line
-
hf download hf://hungnha/DoAn/core/api/static/index.html
-
curl -L -o index.html https://huggingface.co/hungnha/DoAn/resolve/main/core/api/static/index.html
11.1 kB
| <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> | |