Spaces:
Sleeping
Sleeping
Download static/index.html from thaidinhz1/rag-vietnamese: direct link, hf CLI and curl.
- Browser
- Download file 7.9 kB
-
https://huggingface.co/spaces/thaidinhz1/rag-vietnamese/resolve/67adee2ea701e1bd57c03dda376fa9ba3d85a6d0/static/index.html
- Command line
-
hf download hf://spaces/thaidinhz1/rag-vietnamese@67adee2ea701e1bd57c03dda376fa9ba3d85a6d0/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/thaidinhz1/rag-vietnamese/resolve/67adee2ea701e1bd57c03dda376fa9ba3d85a6d0/static/index.html
7.9 kB
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Vietnamese RAG System</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f4f8; color: #2d3748; min-height: 100vh; display: flex; flex-direction: column; } | |
| header { background: linear-gradient(135deg, #1a73e8, #0d47a1); color: white; padding: 18px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } | |
| header h1 { font-size: 1.3rem; font-weight: 700; } | |
| header p { font-size: 0.82rem; opacity: 0.85; margin-top: 4px; } | |
| .main { flex: 1; max-width: 860px; width: 100%; margin: 0 auto; padding: 24px 16px 0; display: flex; flex-direction: column; gap: 16px; } | |
| #history { display: flex; flex-direction: column; gap: 20px; padding-bottom: 100px; } | |
| .chat-item { display: flex; flex-direction: column; gap: 8px; } | |
| .bubble-q { align-self: flex-end; background: #1a73e8; color: white; padding: 10px 16px; border-radius: 18px 18px 4px 18px; max-width: 70%; font-size: 0.95rem; word-break: break-word; } | |
| .bubble-a { align-self: flex-start; background: white; border-radius: 4px 18px 18px 18px; padding: 18px 20px; max-width: 92%; box-shadow: 0 2px 8px rgba(0,0,0,0.08); width: 100%; } | |
| .answer-text { font-size: 0.95rem; line-height: 1.8; white-space: pre-wrap; word-break: break-word; } | |
| .cite { display: inline-block; background: #e8f0fe; color: #1a73e8; border-radius: 4px; padding: 1px 6px; font-size: 0.78rem; font-weight: 700; } | |
| .sources-section { margin-top: 16px; border-top: 1px solid #edf2f7; padding-top: 12px; } | |
| .sources-title { font-size: 0.72rem; font-weight: 700; color: #a0aec0; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; } | |
| .source-item { display: grid; grid-template-columns: 1fr 140px; align-items: center; gap: 12px; padding: 7px 0; border-bottom: 1px solid #f7fafc; } | |
| .source-name { font-size: 0.8rem; color: #4a5568; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| .source-page { font-size: 0.72rem; color: #a0aec0; } | |
| .score-wrap { display: flex; flex-direction: column; gap: 3px; } | |
| .score-row { display: flex; align-items: center; gap: 6px; } | |
| .score-bar { flex: 1; height: 5px; background: #e2e8f0; border-radius: 3px; overflow: hidden; } | |
| .score-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; } | |
| .fill-rrf { background: #68d391; } | |
| .fill-rerank { background: #1a73e8; } | |
| .score-num { font-size: 0.68rem; color: #718096; width: 32px; text-align: right; flex-shrink: 0; } | |
| /* Loading */ | |
| .spinner-wrap { display: flex; align-items: center; gap: 10px; padding: 4px 0; } | |
| .spinner { width: 18px; height: 18px; border: 3px solid #e2e8f0; border-top-color: #1a73e8; border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .spinner-text { font-size: 0.88rem; color: #a0aec0; font-style: italic; } | |
| /* Input bar */ | |
| .input-bar { position: fixed; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, #f0f4f8 80%, transparent); padding: 12px 16px 20px; } | |
| .input-inner { max-width: 860px; margin: 0 auto; display: flex; gap: 8px; background: white; border-radius: 14px; padding: 8px 8px 8px 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.12); } | |
| input { flex: 1; border: none; outline: none; font-size: 1rem; background: transparent; color: #2d3748; } | |
| input::placeholder { color: #a0aec0; } | |
| button { padding: 10px 22px; background: #1a73e8; color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 0.92rem; font-weight: 600; transition: background 0.2s, transform 0.1s; white-space: nowrap; } | |
| button:hover { background: #1558b0; } | |
| button:active { transform: scale(0.97); } | |
| button:disabled { background: #cbd5e0; cursor: not-allowed; transform: none; } | |
| .empty-state { text-align: center; color: #cbd5e0; padding: 64px 0 32px; } | |
| .empty-state .icon { font-size: 3rem; margin-bottom: 12px; } | |
| .empty-state h2 { font-size: 1rem; color: #a0aec0; font-weight: 500; } | |
| .empty-state p { font-size: 0.82rem; margin-top: 6px; color: #cbd5e0; } | |
| .error-text { color: #e53e3e; font-size: 0.9rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>🇻🇳 Vietnamese RAG System</h1> | |
| <p>Hybrid Search · Reranking · Citation · Groq + Gemini Embeddings</p> | |
| </header> | |
| <div class="main"> | |
| <div id="history"> | |
| <div class="empty-state"> | |
| <div class="icon">💬</div> | |
| <h2>Hãy đặt câu hỏi về tài liệu</h2> | |
| <p>Hóa đơn bán lẻ · Báo cáo tài chính tiếng Việt</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="input-bar"> | |
| <div class="input-inner"> | |
| <input id="q" type="text" placeholder="Nhập câu hỏi..." onkeydown="if(event.key==='Enter')ask()"> | |
| <button id="btn" onclick="ask()">Hỏi →</button> | |
| </div> | |
| </div> | |
| <script> | |
| function highlightCitations(text) { | |
| return text.replace(/\[(\d+)\]/g, '<span class="cite">[$1]</span>'); | |
| } | |
| function renderSources(sources) { | |
| if (!sources || !sources.length) return ''; | |
| const items = sources.map(s => { | |
| const rrfPct = Math.min(s.rrf_score * 3000, 100).toFixed(0); | |
| const rerankPct = Math.min(s.rerank_score * 150, 100).toFixed(0); | |
| return ` | |
| <div class="source-item"> | |
| <div> | |
| <div class="source-name">📄 ${s.source}</div> | |
| <div class="source-page">Trang ${s.page}</div> | |
| </div> | |
| <div class="score-wrap"> | |
| <div class="score-row"> | |
| <div class="score-bar"><div class="score-fill fill-rrf" style="width:${rrfPct}%"></div></div> | |
| <div class="score-num">${s.rrf_score}</div> | |
| </div> | |
| <div class="score-row"> | |
| <div class="score-bar"><div class="score-fill fill-rerank" style="width:${rerankPct}%"></div></div> | |
| <div class="score-num">${s.rerank_score}</div> | |
| </div> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| return `<div class="sources-section"><div class="sources-title">Nguồn tham khảo</div>${items}</div>`; | |
| } | |
| async function ask() { | |
| const input = document.getElementById('q'); | |
| const q = input.value.trim(); | |
| if (!q) return; | |
| const btn = document.getElementById('btn'); | |
| const history = document.getElementById('history'); | |
| const empty = history.querySelector('.empty-state'); | |
| if (empty) empty.remove(); | |
| const item = document.createElement('div'); | |
| item.className = 'chat-item'; | |
| item.innerHTML = ` | |
| <div class="bubble-q">${q}</div> | |
| <div class="bubble-a"> | |
| <div class="spinner-wrap"> | |
| <div class="spinner"></div> | |
| <span class="spinner-text">Đang tìm kiếm và tổng hợp...</span> | |
| </div> | |
| </div>`; | |
| history.appendChild(item); | |
| item.scrollIntoView({ behavior: 'smooth', block: 'end' }); | |
| input.value = ''; | |
| btn.disabled = true; | |
| try { | |
| const res = await fetch('/query', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ question: q, top_k: 5 }) | |
| }); | |
| const data = await res.json(); | |
| item.querySelector('.bubble-a').innerHTML = | |
| `<div class="answer-text">${highlightCitations(data.answer)}</div>${renderSources(data.sources)}`; | |
| } catch (e) { | |
| item.querySelector('.bubble-a').innerHTML = `<span class="error-text">Lỗi: ${e.message}</span>`; | |
| } | |
| btn.disabled = false; | |
| input.focus(); | |
| item.scrollIntoView({ behavior: 'smooth', block: 'end' }); | |
| } | |
| </script> | |
| </body> | |
| </html> |