thaidinhz1 commited on
Commit
862a5f6
·
1 Parent(s): 4e3e867

feat: add compare pipeline tab + has_context badge in UI

Browse files
Files changed (3) hide show
  1. api.py +13 -0
  2. src/rag.py +1 -0
  3. static/index.html +97 -6
api.py CHANGED
@@ -31,6 +31,7 @@ class Source(BaseModel):
31
  page: int
32
  rrf_score: float
33
  rerank_score: float
 
34
 
35
 
36
  class QueryResponse(BaseModel):
@@ -107,6 +108,18 @@ async def stream(req: QueryRequest):
107
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
108
 
109
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  @app.get("/health")
111
  def health():
112
  return {"status": "ok"}
 
31
  page: int
32
  rrf_score: float
33
  rerank_score: float
34
+ has_context: bool = False
35
 
36
 
37
  class QueryResponse(BaseModel):
 
108
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
109
 
110
 
111
+ @app.post("/compare")
112
+ def compare(req: QueryRequest):
113
+ """So sánh Text RAG vs ColPali retrieval trên cùng câu hỏi."""
114
+ from src.colpali_retriever import query as colpali_query
115
+ _, text_sources = retrieve(req.question, top_k=req.top_k)
116
+ try:
117
+ colpali_hits = colpali_query(req.question, top_k=req.top_k)
118
+ except Exception as e:
119
+ colpali_hits = []
120
+ return {"text_rag": text_sources, "colpali": colpali_hits}
121
+
122
+
123
  @app.get("/health")
124
  def health():
125
  return {"status": "ok"}
src/rag.py CHANGED
@@ -48,6 +48,7 @@ def retrieve(question: str, top_k: int = 5):
48
  "page": c["metadata"]["page"],
49
  "rrf_score": round(c["score"], 3),
50
  "rerank_score": round(c["rerank_score"], 3),
 
51
  }
52
  for c in contexts
53
  ]
 
48
  "page": c["metadata"]["page"],
49
  "rrf_score": round(c["score"], 3),
50
  "rerank_score": round(c["rerank_score"], 3),
51
+ "has_context": c["metadata"].get("has_context", False),
52
  }
53
  for c in contexts
54
  ]
static/index.html CHANGED
@@ -120,6 +120,33 @@
120
  .cursor { animation: blink 0.8s step-end infinite; color: var(--primary); }
121
  @keyframes blink { 50% { opacity: 0; } }
122
  .uploaded-badge { display: inline-block; background: var(--green-bg); color: var(--green-text); border-radius: 4px; padding: 1px 5px; font-size: 0.68rem; font-weight: 600; margin-left: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  /* Mobile */
125
  @media (max-width: 680px) {
@@ -173,11 +200,41 @@
173
  </div>
174
 
175
  <div class="chat-area">
176
- <div id="history">
177
- <div class="empty-state">
178
- <div class="icon">💬</div>
179
- <h2>Hãy đặt câu hỏi về tài liệu</h2>
180
- <p>Hóa đơn bán lẻ · Báo cáo tài chính · hoặc upload PDF của bạn</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  </div>
182
  </div>
183
  </div>
@@ -193,6 +250,39 @@
193
  </div>
194
 
195
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  // Theme
197
  function toggleTheme() {
198
  const html = document.documentElement;
@@ -280,9 +370,10 @@
280
  const rrfPct = Math.min(s.rrf_score * 3000, 100).toFixed(0);
281
  const rerankPct = Math.min(s.rerank_score * 150, 100).toFixed(0);
282
  const badge = s.uploaded ? '<span class="uploaded-badge">uploaded</span>' : '';
 
283
  return `<div class="source-item">
284
  <div>
285
- <div class="source-name">📄 ${s.source}${badge}</div>
286
  <div class="source-page">Trang ${s.page}</div>
287
  </div>
288
  <div class="score-wrap">
 
120
  .cursor { animation: blink 0.8s step-end infinite; color: var(--primary); }
121
  @keyframes blink { 50% { opacity: 0; } }
122
  .uploaded-badge { display: inline-block; background: var(--green-bg); color: var(--green-text); border-radius: 4px; padding: 1px 5px; font-size: 0.68rem; font-weight: 600; margin-left: 4px; }
123
+ .ctx-badge { display: inline-block; background: #fef3c7; color: #92400e; border-radius: 4px; padding: 1px 5px; font-size: 0.65rem; font-weight: 600; margin-left: 4px; }
124
+ [data-theme="dark"] .ctx-badge { background: #3a2a0a; color: #fbbf24; }
125
+
126
+ /* Tabs */
127
+ .tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid var(--border); }
128
+ .tab-btn { padding: 8px 16px; font-size: 0.85rem; font-weight: 600; border: none; background: none; color: var(--text2); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.15s; }
129
+ .tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
130
+ .tab-btn:hover:not(.active) { color: var(--text); }
131
+ .tab-panel { display: none; }
132
+ .tab-panel.active { display: block; }
133
+
134
+ /* Compare panel */
135
+ .compare-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
136
+ .compare-col h4 { font-size: 0.8rem; font-weight: 700; padding: 8px 12px; border-radius: 8px 8px 0 0; margin-bottom: 0; }
137
+ .col-text h4 { background: var(--primary-light); color: var(--primary); }
138
+ .col-colpali h4 { background: #fef3c7; color: #92400e; }
139
+ [data-theme="dark"] .col-colpali h4 { background: #3a2a0a; color: #fbbf24; }
140
+ .compare-result { background: var(--surface); border: 1px solid var(--border); border-radius: 0 0 8px 8px; padding: 10px; min-height: 80px; }
141
+ .compare-hit { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 0.78rem; }
142
+ .compare-hit:last-child { border-bottom: none; }
143
+ .compare-hit .hit-source { color: var(--text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
144
+ .compare-hit .hit-meta { color: var(--text3); font-size: 0.7rem; margin-top: 2px; }
145
+ .compare-empty { color: var(--text3); font-size: 0.76rem; text-align: center; padding: 16px 0; }
146
+ .compare-loading { color: var(--text3); font-size: 0.76rem; text-align: center; padding: 16px 0; }
147
+ @media (max-width: 680px) {
148
+ .compare-cols { grid-template-columns: 1fr; }
149
+ }
150
 
151
  /* Mobile */
152
  @media (max-width: 680px) {
 
200
  </div>
201
 
202
  <div class="chat-area">
203
+ <div class="tabs">
204
+ <button class="tab-btn active" onclick="switchTab('chat')">💬 Chat</button>
205
+ <button class="tab-btn" onclick="switchTab('compare')">⚖️ So sánh Pipeline</button>
206
+ </div>
207
+
208
+ <div id="tab-chat" class="tab-panel active">
209
+ <div id="history">
210
+ <div class="empty-state">
211
+ <div class="icon">💬</div>
212
+ <h2>Hãy đặt câu hỏi về tài liệu</h2>
213
+ <p>Hóa đơn bán lẻ · Báo cáo tài chính · hoặc upload PDF của bạn</p>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <div id="tab-compare" class="tab-panel">
219
+ <div class="card">
220
+ <h3>⚖️ So sánh Text RAG vs ColPali Visual Retrieval</h3>
221
+ <p style="font-size:0.78rem;color:var(--text2);margin-bottom:12px">Nhập câu hỏi và xem 2 pipeline truy xuất tài liệu như thế nào khác nhau.</p>
222
+ <div style="display:flex;gap:8px">
223
+ <input type="text" id="cmp-q" placeholder="Nhập câu hỏi để so sánh..." style="flex:1;border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:0.88rem;background:var(--surface2);color:var(--text);outline:none" onkeydown="if(event.key==='Enter')runCompare()">
224
+ <button onclick="runCompare()" style="padding:8px 16px;background:var(--primary);color:white;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:0.85rem">So sánh</button>
225
+ </div>
226
+ <div id="compare-result">
227
+ <div class="compare-cols">
228
+ <div class="compare-col col-text">
229
+ <h4>📝 Text RAG (BM25 + Vector + Rerank)</h4>
230
+ <div class="compare-result"><div class="compare-empty">Nhập câu hỏi để xem kết quả</div></div>
231
+ </div>
232
+ <div class="compare-col col-colpali">
233
+ <h4>🖼️ ColPali Visual Retrieval (MaxSim)</h4>
234
+ <div class="compare-result"><div class="compare-empty">Nhập câu hỏi để xem kết quả</div></div>
235
+ </div>
236
+ </div>
237
+ </div>
238
  </div>
239
  </div>
240
  </div>
 
250
  </div>
251
 
252
  <script>
253
+ // Tabs
254
+ function switchTab(name) {
255
+ document.querySelectorAll('.tab-btn').forEach((b, i) => b.classList.toggle('active', ['chat','compare'][i] === name));
256
+ document.getElementById('tab-chat').classList.toggle('active', name === 'chat');
257
+ document.getElementById('tab-compare').classList.toggle('active', name === 'compare');
258
+ }
259
+
260
+ // Compare
261
+ async function runCompare() {
262
+ const q = document.getElementById('cmp-q').value.trim();
263
+ if (!q) return;
264
+ const cols = document.querySelectorAll('#compare-result .compare-result');
265
+ cols.forEach(c => c.innerHTML = '<div class="compare-loading">Đang tìm kiếm...</div>');
266
+ try {
267
+ const res = await fetch('/compare', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({question: q, top_k: 5}) });
268
+ const data = await res.json();
269
+ // Text RAG
270
+ cols[0].innerHTML = data.text_rag.length ? data.text_rag.map((s,i) =>
271
+ `<div class="compare-hit">
272
+ <div class="hit-source">[${i+1}] 📄 ${s.source}${s.has_context ? '<span class="ctx-badge">✦ context</span>' : ''}</div>
273
+ <div class="hit-meta">Trang ${s.page} · RRF ${s.rrf_score} · Rerank ${s.rerank_score}</div>
274
+ </div>`).join('') : '<div class="compare-empty">Không có kết quả</div>';
275
+ // ColPali
276
+ cols[1].innerHTML = data.colpali.length ? data.colpali.map((s,i) =>
277
+ `<div class="compare-hit">
278
+ <div class="hit-source">[${i+1}] 🖼️ ${s.source || 'N/A'}</div>
279
+ <div class="hit-meta">Trang ${s.page || '?'} · Score ${s.score || 'N/A'}</div>
280
+ </div>`).join('') : '<div class="compare-empty">Không có kết quả (ColPali chạy local)</div>';
281
+ } catch(e) {
282
+ cols.forEach(c => c.innerHTML = `<div class="compare-empty">Lỗi: ${e.message}</div>`);
283
+ }
284
+ }
285
+
286
  // Theme
287
  function toggleTheme() {
288
  const html = document.documentElement;
 
370
  const rrfPct = Math.min(s.rrf_score * 3000, 100).toFixed(0);
371
  const rerankPct = Math.min(s.rerank_score * 150, 100).toFixed(0);
372
  const badge = s.uploaded ? '<span class="uploaded-badge">uploaded</span>' : '';
373
+ const ctxBadge = s.has_context ? '<span class="ctx-badge">✦ context</span>' : '';
374
  return `<div class="source-item">
375
  <div>
376
+ <div class="source-name">📄 ${s.source}${badge}${ctxBadge}</div>
377
  <div class="source-page">Trang ${s.page}</div>
378
  </div>
379
  <div class="score-wrap">