Add live search progress, session counter, and fix URLs

- Search candidates client-side with JS, showing "Checking X..." spinner
  instead of leaving user waiting on a blank page
- Fix broken api_valid_hit endpoint (get_diff returns dict, not tuple)
- Remove server-side get_best_hit; article_page now returns candidate list
  immediately and JS iterates via /api/1/valid_hit
- URL now reflects current article via history.replaceState (?title=X),
  Skip navigates to ?after=X to advance past it
- Track saves in session; show count as green badge in navbar
- Add session counter incremented on each successful save

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-05-11 12:24:15 +01:00
parent bc6265d4cd
commit 2c197f5c43
3 changed files with 114 additions and 71 deletions

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}{{ title }} in {{ hit_title }}{% endblock %}
{% block title %}{{ title }}{% endblock %}
{% block style %}
<link href="{{ url_for("static", filename="css/diff.css") }}" rel="stylesheet"/>
@ -16,31 +16,49 @@
</nav>
<div class="d-flex flex-wrap align-items-baseline gap-3 mb-1">
<h1 class="h4 mb-0">Link "{{ title }}" in "{{ hit_title }}"</h1>
<h1 class="h4 mb-0">Find links to "{{ title }}"</h1>
<a href="https://en.wikipedia.org/wiki/{{ title }}" target="_blank" class="text-muted small">{{ title }} ↗</a>
<a href="https://en.wikipedia.org/wiki/{{ hit_title }}" target="_blank" class="text-muted small">{{ hit_title }} ↗</a>
</div>
<div class="d-flex gap-3 mb-4 text-muted small">
<div class="d-flex gap-3 mb-3 text-muted small">
<span>{{ total }} mentions total</span>
<span>{{ with_link }} already linked ({{ "{:.0%}".format(with_link / total) }})</span>
</div>
<div class="mb-4">
<table class="diff">{{ diff | safe }}</table>
<div id="search-progress" class="my-4">
<div class="d-flex align-items-center gap-2 text-muted">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Searching…</span>
</div>
<span id="search-status">Searching…</span>
</div>
</div>
<form method="POST" class="mb-4">
<input type="hidden" name="hit" value="{{ hit_title }}">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">Save edit</button>
<a href="{{ url_for("article_page", url_title=url_title, after=hit_title) }}" class="btn btn-outline-secondary">Skip</a>
<div id="result" hidden>
<div class="d-flex flex-wrap align-items-baseline gap-2 mb-3">
<span class="text-muted small">Adding link in</span>
<a id="result-hit-link" href="#" target="_blank" class="small"><span id="result-hit-title"></span></a>
</div>
</form>
<div class="mb-4">
<table class="diff" id="diff-table"></table>
</div>
<form method="POST" class="mb-4">
<input type="hidden" name="hit" id="hit-input">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">Save edit</button>
<a id="skip-link" href="#" class="btn btn-outline-secondary">Skip</a>
</div>
</form>
</div>
<div id="all-done" hidden class="text-center mt-4">
<p class="text-muted mb-4">No more candidates found for this article.</p>
<a href="{{ url_for('index') }}" class="btn btn-primary">Search another article</a>
</div>
{% if hits %}
<details class="border rounded p-3">
<summary class="text-muted small" style="cursor:pointer">{{ hits | length }} other candidates</summary>
<details class="border rounded p-3 mt-2">
<summary class="text-muted small" style="cursor:pointer">{{ hits | length }} candidates</summary>
<ol class="mt-3 mb-0 small">
{% for hit in hits %}
<li class="mb-1">
@ -52,3 +70,63 @@
{% endif %}
</div>
{% endblock %}
{% block script %}
<script>
(function () {
const hits = {{ hits | map(attribute='title') | list | tojson }};
const linkTo = {{ title | tojson }};
const apiUrl = {{ url_for('api_valid_hit') | tojson }};
const pageUrl = new URL(window.location.href);
const elProgress = document.getElementById('search-progress');
const elStatus = document.getElementById('search-status');
const elResult = document.getElementById('result');
const elAllDone = document.getElementById('all-done');
async function search() {
for (const hitTitle of hits) {
elStatus.textContent = `Checking "${hitTitle}"…`;
let data;
try {
const params = new URLSearchParams({ link_to: linkTo, link_from: hitTitle });
const resp = await fetch(apiUrl + '?' + params);
if (!resp.ok) continue;
data = await resp.json();
} catch (e) {
continue;
}
if (!data.valid) continue;
elProgress.hidden = true;
document.getElementById('result-hit-title').textContent = hitTitle;
document.getElementById('result-hit-link').href =
'https://en.wikipedia.org/wiki/' + encodeURIComponent(hitTitle.replace(/ /g, '_'));
document.getElementById('diff-table').innerHTML = data.diff;
document.getElementById('hit-input').value = hitTitle;
const skipUrl = new URL(pageUrl);
skipUrl.searchParams.delete('title');
skipUrl.searchParams.set('after', hitTitle);
document.getElementById('skip-link').href = skipUrl.toString();
const currentUrl = new URL(pageUrl);
currentUrl.searchParams.delete('after');
currentUrl.searchParams.set('title', hitTitle);
history.replaceState(null, '', currentUrl.toString());
elResult.hidden = false;
return;
}
elProgress.hidden = true;
elAllDone.hidden = false;
}
search();
}());
</script>
{% endblock %}