rag-vietnamese / static /index.html
thaidinhz1's picture
improve chat UI, fix score bars, lower rerank threshold
f8ff11b
Raw History Blame
7.9 kB
<!DOCTYPE html>
<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>