Spaces:
Sleeping
Sleeping
Commit ·
0918009
1
Parent(s): 24cbd41
Remove upload feature: broken with scanned PDFs, replaced by fixed corpus
Browse files- api.py +1 -17
- static/index.html +2 -96
api.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
| 1 |
import json
|
| 2 |
from contextlib import asynccontextmanager
|
| 3 |
-
from fastapi import FastAPI,
|
| 4 |
from fastapi.staticfiles import StaticFiles
|
| 5 |
from fastapi.responses import HTMLResponse, StreamingResponse
|
| 6 |
from pydantic import BaseModel
|
| 7 |
from src.rag import answer, retrieve
|
| 8 |
from src.reranker import _get_model
|
| 9 |
-
from src.uploader import ingest_pdf, get_uploaded_files
|
| 10 |
|
| 11 |
|
| 12 |
@asynccontextmanager
|
|
@@ -57,21 +56,6 @@ def query(req: QueryRequest):
|
|
| 57 |
)
|
| 58 |
|
| 59 |
|
| 60 |
-
@app.post("/upload")
|
| 61 |
-
async def upload(file: UploadFile = File(...)):
|
| 62 |
-
if not file.filename.endswith(".pdf"):
|
| 63 |
-
raise HTTPException(status_code=400, detail="Chỉ hỗ trợ file PDF")
|
| 64 |
-
content = await file.read()
|
| 65 |
-
if len(content) > 20 * 1024 * 1024:
|
| 66 |
-
raise HTTPException(status_code=400, detail="File quá lớn (tối đa 20MB)")
|
| 67 |
-
result = ingest_pdf(file.filename, content)
|
| 68 |
-
return result
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
@app.get("/documents")
|
| 72 |
-
def documents():
|
| 73 |
-
return get_uploaded_files()
|
| 74 |
-
|
| 75 |
|
| 76 |
@app.post("/stream")
|
| 77 |
async def stream(req: QueryRequest):
|
|
|
|
| 1 |
import json
|
| 2 |
from contextlib import asynccontextmanager
|
| 3 |
+
from fastapi import FastAPI, HTTPException
|
| 4 |
from fastapi.staticfiles import StaticFiles
|
| 5 |
from fastapi.responses import HTMLResponse, StreamingResponse
|
| 6 |
from pydantic import BaseModel
|
| 7 |
from src.rag import answer, retrieve
|
| 8 |
from src.reranker import _get_model
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
@asynccontextmanager
|
|
|
|
| 56 |
)
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
@app.post("/stream")
|
| 61 |
async def stream(req: QueryRequest):
|
static/index.html
CHANGED
|
@@ -38,26 +38,6 @@
|
|
| 38 |
.card { background: var(--surface); border-radius: 12px; padding: 16px; box-shadow: 0 2px 8px var(--shadow); border: 1px solid var(--border); }
|
| 39 |
.card h3 { font-size: 0.83rem; font-weight: 700; color: var(--text2); margin-bottom: 10px; }
|
| 40 |
|
| 41 |
-
.drop-zone { border: 2px dashed var(--border); border-radius: 8px; padding: 18px 12px; text-align: center; cursor: pointer; transition: all 0.2s; }
|
| 42 |
-
.drop-zone:hover, .drop-zone.drag-over { border-color: var(--primary); background: var(--primary-light); }
|
| 43 |
-
.drop-zone .drop-icon { font-size: 1.6rem; margin-bottom: 6px; }
|
| 44 |
-
.drop-zone p { font-size: 0.76rem; color: var(--text2); }
|
| 45 |
-
.drop-zone span { font-size: 0.7rem; color: var(--text3); }
|
| 46 |
-
#file-input { display: none; }
|
| 47 |
-
|
| 48 |
-
.upload-btn { width: 100%; margin-top: 8px; padding: 8px; background: var(--primary); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 0.82rem; font-weight: 600; transition: background 0.2s; }
|
| 49 |
-
.upload-btn:hover { background: var(--primary-dark); }
|
| 50 |
-
.upload-btn:disabled { background: var(--border); color: var(--text3); cursor: not-allowed; }
|
| 51 |
-
.upload-status { margin-top: 8px; font-size: 0.76rem; min-height: 16px; }
|
| 52 |
-
.upload-status.ok { color: var(--green); }
|
| 53 |
-
.upload-status.err { color: var(--red); }
|
| 54 |
-
.upload-status.loading { color: var(--text3); }
|
| 55 |
-
|
| 56 |
-
.doc-list-empty { font-size: 0.76rem; color: var(--text3); text-align: center; padding: 10px 0; }
|
| 57 |
-
.doc-item { display: flex; align-items: flex-start; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--border); }
|
| 58 |
-
.doc-item:last-child { border-bottom: none; }
|
| 59 |
-
.doc-name { font-size: 0.76rem; color: var(--text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 60 |
-
.doc-meta { font-size: 0.68rem; color: var(--text3); margin-top: 2px; }
|
| 61 |
|
| 62 |
/* Sample questions */
|
| 63 |
.sample-q { display: block; width: 100%; text-align: left; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; margin-bottom: 6px; cursor: pointer; font-size: 0.76rem; color: var(--text2); transition: all 0.15s; }
|
|
@@ -119,7 +99,6 @@
|
|
| 119 |
.error-text { color: var(--red); font-size: 0.88rem; }
|
| 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 |
.rewrite-badge { font-size: 0.74rem; color: var(--text3); font-style: italic; padding: 3px 0 0 4px; }
|
|
@@ -178,25 +157,6 @@
|
|
| 178 |
|
| 179 |
<div class="layout">
|
| 180 |
<div class="sidebar">
|
| 181 |
-
<div class="card">
|
| 182 |
-
<h3>📤 Upload tài liệu</h3>
|
| 183 |
-
<div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
|
| 184 |
-
<div class="drop-icon">📄</div>
|
| 185 |
-
<p>Kéo thả PDF vào đây</p>
|
| 186 |
-
<span>hoặc bấm để chọn file</span>
|
| 187 |
-
</div>
|
| 188 |
-
<input type="file" id="file-input" accept=".pdf" onchange="handleFileSelect(this)">
|
| 189 |
-
<button class="upload-btn" id="upload-btn" onclick="uploadFile()" disabled>Upload</button>
|
| 190 |
-
<div class="upload-status" id="upload-status"></div>
|
| 191 |
-
</div>
|
| 192 |
-
|
| 193 |
-
<div class="card">
|
| 194 |
-
<h3>📚 Tài liệu đã upload</h3>
|
| 195 |
-
<div id="doc-list-content">
|
| 196 |
-
<div class="doc-list-empty">Chưa có tài liệu nào</div>
|
| 197 |
-
</div>
|
| 198 |
-
</div>
|
| 199 |
-
|
| 200 |
<div class="card">
|
| 201 |
<h3>💡 Câu hỏi mẫu</h3>
|
| 202 |
<button class="sample-q" onclick="askSample(this)">FPT hoạt động trong những lĩnh vực kinh doanh nào?</button>
|
|
@@ -217,7 +177,7 @@
|
|
| 217 |
<div class="empty-state">
|
| 218 |
<div class="icon">💬</div>
|
| 219 |
<h2>Hãy đặt câu hỏi về tài liệu</h2>
|
| 220 |
-
<p>Báo cáo tài chính
|
| 221 |
</div>
|
| 222 |
</div>
|
| 223 |
</div>
|
|
@@ -323,58 +283,6 @@
|
|
| 323 |
ask();
|
| 324 |
}
|
| 325 |
|
| 326 |
-
// Upload
|
| 327 |
-
let selectedFile = null;
|
| 328 |
-
const dropZone = document.getElementById('drop-zone');
|
| 329 |
-
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
| 330 |
-
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
| 331 |
-
dropZone.addEventListener('drop', e => {
|
| 332 |
-
e.preventDefault(); dropZone.classList.remove('drag-over');
|
| 333 |
-
const file = e.dataTransfer.files[0];
|
| 334 |
-
if (file && file.name.endsWith('.pdf')) setFile(file);
|
| 335 |
-
});
|
| 336 |
-
|
| 337 |
-
function handleFileSelect(input) { if (input.files[0]) setFile(input.files[0]); }
|
| 338 |
-
function setFile(file) {
|
| 339 |
-
selectedFile = file;
|
| 340 |
-
document.getElementById('upload-btn').disabled = false;
|
| 341 |
-
setStatus(`Đã chọn: ${file.name}`, '');
|
| 342 |
-
}
|
| 343 |
-
function setStatus(msg, type) {
|
| 344 |
-
const el = document.getElementById('upload-status');
|
| 345 |
-
el.textContent = msg; el.className = 'upload-status ' + type;
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
async function uploadFile() {
|
| 349 |
-
if (!selectedFile) return;
|
| 350 |
-
document.getElementById('upload-btn').disabled = true;
|
| 351 |
-
setStatus('Đang xử lý...', 'loading');
|
| 352 |
-
const formData = new FormData();
|
| 353 |
-
formData.append('file', selectedFile);
|
| 354 |
-
try {
|
| 355 |
-
const res = await fetch('/upload', { method: 'POST', body: formData });
|
| 356 |
-
if (!res.ok) { const err = await res.json(); setStatus('Lỗi: ' + err.detail, 'err'); document.getElementById('upload-btn').disabled = false; return; }
|
| 357 |
-
const data = await res.json();
|
| 358 |
-
setStatus(`✓ ${data.name} (${data.pages} trang, ${data.chunks} chunks)`, 'ok');
|
| 359 |
-
selectedFile = null; document.getElementById('file-input').value = '';
|
| 360 |
-
refreshDocList();
|
| 361 |
-
} catch (e) { setStatus('Lỗi kết nối', 'err'); document.getElementById('upload-btn').disabled = false; }
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
async function refreshDocList() {
|
| 365 |
-
const res = await fetch('/documents');
|
| 366 |
-
const docs = await res.json();
|
| 367 |
-
const el = document.getElementById('doc-list-content');
|
| 368 |
-
if (!docs.length) { el.innerHTML = '<div class="doc-list-empty">Chưa có tài liệu nào</div>'; return; }
|
| 369 |
-
el.innerHTML = docs.map(d => `
|
| 370 |
-
<div class="doc-item">
|
| 371 |
-
<div>
|
| 372 |
-
<div class="doc-name" title="${d.name}">📄 ${d.name}</div>
|
| 373 |
-
<div class="doc-meta">${d.pages} trang · ${d.chunks} chunks</div>
|
| 374 |
-
</div>
|
| 375 |
-
</div>`).join('');
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
function highlightCitations(text) {
|
| 379 |
return text.replace(/\[(\d+)\]/g, '<span class="cite">[$1]</span>');
|
| 380 |
}
|
|
@@ -385,7 +293,6 @@
|
|
| 385 |
const items = sources.map((s, i) => {
|
| 386 |
const rrfPct = Math.min(s.rrf_score * 3000, 100).toFixed(0);
|
| 387 |
const rerankPct = Math.min(s.rerank_score * 150, 100).toFixed(0);
|
| 388 |
-
const badge = s.uploaded ? '<span class="uploaded-badge">uploaded</span>' : '';
|
| 389 |
const ctxBadge = s.has_context ? '<span class="ctx-badge">✦ context</span>' : '';
|
| 390 |
const sid = `src-${++_srcCounter}`;
|
| 391 |
const escapedText = s.text ? s.text.replace(/</g, '<').replace(/>/g, '>') : '';
|
|
@@ -396,7 +303,7 @@
|
|
| 396 |
return `<div class="source-item" style="flex-direction:column;align-items:stretch;gap:6px">
|
| 397 |
<div style="display:grid;grid-template-columns:1fr 130px;align-items:center;gap:10px">
|
| 398 |
<div>
|
| 399 |
-
<div class="source-name">📄 ${s.source}${
|
| 400 |
<div class="source-page">Trang ${s.page}</div>
|
| 401 |
</div>
|
| 402 |
<div class="score-wrap">
|
|
@@ -509,7 +416,6 @@
|
|
| 509 |
item.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
| 510 |
}
|
| 511 |
|
| 512 |
-
refreshDocList();
|
| 513 |
</script>
|
| 514 |
</body>
|
| 515 |
</html>
|
|
|
|
| 38 |
.card { background: var(--surface); border-radius: 12px; padding: 16px; box-shadow: 0 2px 8px var(--shadow); border: 1px solid var(--border); }
|
| 39 |
.card h3 { font-size: 0.83rem; font-weight: 700; color: var(--text2); margin-bottom: 10px; }
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
/* Sample questions */
|
| 43 |
.sample-q { display: block; width: 100%; text-align: left; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; margin-bottom: 6px; cursor: pointer; font-size: 0.76rem; color: var(--text2); transition: all 0.15s; }
|
|
|
|
| 99 |
.error-text { color: var(--red); font-size: 0.88rem; }
|
| 100 |
.cursor { animation: blink 0.8s step-end infinite; color: var(--primary); }
|
| 101 |
@keyframes blink { 50% { opacity: 0; } }
|
|
|
|
| 102 |
.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; }
|
| 103 |
[data-theme="dark"] .ctx-badge { background: #3a2a0a; color: #fbbf24; }
|
| 104 |
.rewrite-badge { font-size: 0.74rem; color: var(--text3); font-style: italic; padding: 3px 0 0 4px; }
|
|
|
|
| 157 |
|
| 158 |
<div class="layout">
|
| 159 |
<div class="sidebar">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
<div class="card">
|
| 161 |
<h3>💡 Câu hỏi mẫu</h3>
|
| 162 |
<button class="sample-q" onclick="askSample(this)">FPT hoạt động trong những lĩnh vực kinh doanh nào?</button>
|
|
|
|
| 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>Báo cáo tài chính VNM · FPT · HPG · MSN (2022–2023)</p>
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
</div>
|
|
|
|
| 283 |
ask();
|
| 284 |
}
|
| 285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
function highlightCitations(text) {
|
| 287 |
return text.replace(/\[(\d+)\]/g, '<span class="cite">[$1]</span>');
|
| 288 |
}
|
|
|
|
| 293 |
const items = sources.map((s, i) => {
|
| 294 |
const rrfPct = Math.min(s.rrf_score * 3000, 100).toFixed(0);
|
| 295 |
const rerankPct = Math.min(s.rerank_score * 150, 100).toFixed(0);
|
|
|
|
| 296 |
const ctxBadge = s.has_context ? '<span class="ctx-badge">✦ context</span>' : '';
|
| 297 |
const sid = `src-${++_srcCounter}`;
|
| 298 |
const escapedText = s.text ? s.text.replace(/</g, '<').replace(/>/g, '>') : '';
|
|
|
|
| 303 |
return `<div class="source-item" style="flex-direction:column;align-items:stretch;gap:6px">
|
| 304 |
<div style="display:grid;grid-template-columns:1fr 130px;align-items:center;gap:10px">
|
| 305 |
<div>
|
| 306 |
+
<div class="source-name">📄 ${s.source}${ctxBadge}</div>
|
| 307 |
<div class="source-page">Trang ${s.page}</div>
|
| 308 |
</div>
|
| 309 |
<div class="score-wrap">
|
|
|
|
| 416 |
item.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
| 417 |
}
|
| 418 |
|
|
|
|
| 419 |
</script>
|
| 420 |
</body>
|
| 421 |
</html>
|