feat(верификация): рабочая очередь с пагинацией + /skip
- автодогрузка порций + кнопка «Загрузить ещё»; разбор сверху по уверенности - POST /skip фиксирует пропуск (таблица skipped_item), очередь его исключает Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+14
-1
@@ -36,6 +36,8 @@ app.add_middleware(
|
|||||||
def db() -> sqlite3.Connection:
|
def db() -> sqlite3.Connection:
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
# Отложенные оператором позиции (чтобы пропущенное не возвращалось в начало очереди).
|
||||||
|
conn.execute("CREATE TABLE IF NOT EXISTS skipped_item (item_id INTEGER PRIMARY KEY)")
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +157,7 @@ def unmatched(limit: int = 50, offset: int = 0):
|
|||||||
JOIN partner p ON pi.partner_id = p.partner_id
|
JOIN partner p ON pi.partner_id = p.partner_id
|
||||||
LEFT JOIN service s ON pi.suggested_service_id = s.service_id
|
LEFT JOIN service s ON pi.suggested_service_id = s.service_id
|
||||||
WHERE pi.service_id IS NULL AND pi.is_active = 1
|
WHERE pi.service_id IS NULL AND pi.is_active = 1
|
||||||
|
AND pi.item_id NOT IN (SELECT item_id FROM skipped_item)
|
||||||
ORDER BY pi.suggested_score DESC, pi.item_id
|
ORDER BY pi.suggested_score DESC, pi.item_id
|
||||||
LIMIT ? OFFSET ?"""
|
LIMIT ? OFFSET ?"""
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
@@ -184,12 +187,22 @@ def unmatched(limit: int = 50, offset: int = 0):
|
|||||||
def unmatched_count():
|
def unmatched_count():
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
n = conn.execute(
|
n = conn.execute(
|
||||||
"SELECT COUNT(*) FROM price_item WHERE service_id IS NULL AND is_active = 1"
|
"""SELECT COUNT(*) FROM price_item
|
||||||
|
WHERE service_id IS NULL AND is_active = 1
|
||||||
|
AND item_id NOT IN (SELECT item_id FROM skipped_item)"""
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
learned = conn.execute("SELECT COUNT(*) FROM learned_synonym").fetchone()[0]
|
learned = conn.execute("SELECT COUNT(*) FROM learned_synonym").fetchone()[0]
|
||||||
return {"remaining": n, "learned_synonyms": learned}
|
return {"remaining": n, "learned_synonyms": learned}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/skip", summary="Отложить позицию (пропустить в очереди верификации)")
|
||||||
|
def skip_item(item_id: int):
|
||||||
|
with db() as conn:
|
||||||
|
conn.execute("INSERT OR IGNORE INTO skipped_item (item_id) VALUES (?)", [item_id])
|
||||||
|
conn.commit()
|
||||||
|
return {"item_id": item_id, "status": "skipped"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/match", summary="Ручное сопоставление позиции (с дообучением)")
|
@app.post("/match", summary="Ручное сопоставление позиции (с дообучением)")
|
||||||
def manual_match(item_id: int, service_id: str):
|
def manual_match(item_id: int, service_id: str):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
|
|||||||
+15
-3
@@ -57,6 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-tg mt-1">Система предлагает кандидата из справочника — подтвердите, выберите другой или пропустите. Подтверждение обучает систему.</p>
|
<p class="text-sm text-tg mt-1">Система предлагает кандидата из справочника — подтвердите, выберите другой или пропустите. Подтверждение обучает систему.</p>
|
||||||
<div id="queue" class="mt-3 space-y-2"></div>
|
<div id="queue" class="mt-3 space-y-2"></div>
|
||||||
|
<button id="loadmore" onclick="loadQueue()" class="mt-3 text-sm text-nomad hover:underline hidden">Загрузить ещё →</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
@@ -145,11 +146,17 @@ async function loadAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadQueue() {
|
async function loadQueue() {
|
||||||
const items = await fetch(`${API}/unmatched?limit=40`).then(r => r.json());
|
const items = await fetch(`${API}/unmatched?limit=25`).then(r => r.json());
|
||||||
$('queue').innerHTML = items.map(renderCard).join('') || '<div class="text-tg p-4">Очередь пуста 🎉</div>';
|
$('queue').innerHTML = items.map(renderCard).join('') || '<div class="text-tg p-4">Очередь пуста 🎉</div>';
|
||||||
|
$('loadmore').classList.toggle('hidden', items.length === 0);
|
||||||
loadCounts();
|
loadCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRefill() {
|
||||||
|
// Рабочая очередь: когда необработанных карточек мало — подгружаем следующую порцию.
|
||||||
|
if ($('queue').querySelectorAll('[id^="card-"]').length < 6) loadQueue();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCounts() {
|
async function loadCounts() {
|
||||||
const c = await fetch(`${API}/unmatched/count`).then(r => r.json());
|
const c = await fetch(`${API}/unmatched/count`).then(r => r.json());
|
||||||
$('vprogress').innerHTML = `осталось <b>${c.remaining}</b> · обучено синонимов <b class="text-nomad">${c.learned_synonyms}</b>`;
|
$('vprogress').innerHTML = `осталось <b>${c.remaining}</b> · обучено синонимов <b class="text-nomad">${c.learned_synonyms}</b>`;
|
||||||
@@ -189,11 +196,16 @@ async function doMatch(itemId, serviceId, btn) {
|
|||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
await fetch(`${API}/match?item_id=${itemId}&service_id=${encodeURIComponent(serviceId)}`, { method: 'POST' });
|
await fetch(`${API}/match?item_id=${itemId}&service_id=${encodeURIComponent(serviceId)}`, { method: 'POST' });
|
||||||
const card = $(`card-${itemId}`); if (card) card.remove();
|
const card = $(`card-${itemId}`); if (card) card.remove();
|
||||||
loadCounts();
|
loadCounts(); maybeRefill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePick(id) { $(`pick-${id}`).classList.toggle('hidden'); }
|
function togglePick(id) { $(`pick-${id}`).classList.toggle('hidden'); }
|
||||||
function skipCard(id) { const c = $(`card-${id}`); if (c) c.remove(); }
|
|
||||||
|
async function skipCard(id) {
|
||||||
|
await fetch(`${API}/skip?item_id=${id}`, { method: 'POST' });
|
||||||
|
const c = $(`card-${id}`); if (c) c.remove();
|
||||||
|
loadCounts(); maybeRefill();
|
||||||
|
}
|
||||||
|
|
||||||
let dictTimer;
|
let dictTimer;
|
||||||
function searchDict(itemId, q) {
|
function searchDict(itemId, q) {
|
||||||
|
|||||||
Reference in New Issue
Block a user