From 2c197f5c43f8f8b80b880ae5b93e9a38777be1f5 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 11 May 2026 12:24:15 +0100 Subject: [PATCH] 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 --- templates/article.html | 106 +++++++++++++++++++++++++++++++++++------ templates/base.html | 3 ++ web_view.py | 76 ++++++++--------------------- 3 files changed, 114 insertions(+), 71 deletions(-) diff --git a/templates/article.html b/templates/article.html index 4d5c97f..5b11226 100644 --- a/templates/article.html +++ b/templates/article.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ title }} in {{ hit_title }}{% endblock %} +{% block title %}{{ title }}{% endblock %} {% block style %} @@ -16,31 +16,49 @@
-

Link "{{ title }}" in "{{ hit_title }}"

+

Find links to "{{ title }}"

{{ title }} ↗ - {{ hit_title }} ↗
-
+
{{ total }} mentions total {{ with_link }} already linked ({{ "{:.0%}".format(with_link / total) }})
-
- {{ diff | safe }}
+
+
+
+ Searching… +
+ Searching… +
-
- -
- - Skip + + + {% if hits %} -
- {{ hits | length }} other candidates +
+ {{ hits | length }} candidates
    {% for hit in hits %}
  1. @@ -52,3 +70,63 @@ {% endif %}
{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 7043811..324d9fc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -20,6 +20,9 @@ {% if g.user %} {{ g.user }} + {% if session.get("saves") %} + {{ session["saves"] }} saved + {% endif %} Log out {% else %} Log in with Wikipedia diff --git a/web_view.py b/web_view.py index c90865e..92f7bc8 100755 --- a/web_view.py +++ b/web_view.py @@ -292,31 +292,6 @@ def match_type(q: str, snippet: str) -> str | None: return match -class NoGoodHit(Exception): - """No good hit.""" - - -def get_best_hit(title: str, hits: list[Hit]) -> tuple[Hit, dict[str, typing.Any]]: - """Find the best hit within the search results.""" - for hit in hits: - if hit["title"].lower() == title.lower(): - continue - # if match_type(title, hit["snippet"]) != "exact": - # continue - - try: - print(f'get diff: {hit["title"]}, {title}') - found = get_diff(title, hit["title"], None) - except NoMatch: - print("no match") - continue - except api.MediawikiError as e: - print(f"MediawikiError for {hit['title']!r}: {e}") - continue - - return (hit, found) - - raise NoGoodHit def handle_post(url_title: str) -> Response: @@ -329,6 +304,7 @@ def handle_post(url_title: str) -> Response: return flask.redirect(flask.url_for("start_oauth")) except mediawiki_api.APIError as e: return flask.make_response(f"Save failed: {e}", 502) + flask.session["saves"] = flask.session.get("saves", 0) + 1 return flask.redirect( flask.url_for("article_page", url_title=url_title, after=hit_title) ) @@ -341,47 +317,33 @@ def article_page(url_title: str) -> str | Response: return handle_post(url_title) from_title = url_title.replace("_", " ").strip() - article_title = flask.request.args.get("title") total = search_count(from_title) with_link = search_count_with_link(from_title) + _no_link_count, hits = search_no_link(from_title) - no_link_count, hits = search_no_link(from_title) + # If a specific candidate was requested, move it to the front + title_param = flask.request.args.get("title") + if title_param: + hits = [h for h in hits if h["title"] == title_param] + \ + [h for h in hits if h["title"] != title_param] - by_title = {hit["title"]: hit for hit in hits} + # Skip past already-processed candidates + after = flask.request.args.get("after") + if after: + hits_iter = itertools.dropwhile(lambda h: h["title"] != after, hits) + next(hits_iter, None) # consume the "after" entry itself + hits = list(hits_iter) - found = None - if article_title in by_title: - hit = by_title[article_title] - try: - found = get_diff(from_title, hit["title"], None) - except NoMatch: - pass - - if not found: - after = flask.request.args.get("after") - if after: - print(after) - hits_iter = itertools.dropwhile(lambda hit: hit["title"] != after, hits) - skip = next(hits_iter, None) - if skip: - hits = list(hits_iter) - - try: - hit, found = get_best_hit(from_title, hits) - except NoGoodHit: - return flask.render_template("all_done.html") + if not hits: + return flask.render_template("all_done.html") return flask.render_template( "article.html", title=from_title, total=total, with_link=with_link, - hit_title=hit["title"], hits=hits, - replacement=found["replacement"], - diff=found["diff"], - found=found, url_title=url_title, ) @@ -428,16 +390,16 @@ def api_hits() -> werkzeug.wrappers.response.Response: @app.route("/api/1/valid_hit") def api_valid_hit() -> werkzeug.wrappers.response.Response: - """Return candidates for the given article title.""" - link_from = flask.request.args["link_from"] + """Check if a candidate article has a valid unlinked mention.""" link_to = flask.request.args["link_to"] + link_from = flask.request.args["link_from"] try: - diff, replacement = get_diff(link_to, link_from, None) + found = get_diff(link_to, link_from, None) except NoMatch: return flask.jsonify(valid=False) - return flask.jsonify(valid=True, diff=diff, replacement=replacement) + return flask.jsonify(valid=True, diff=found["diff"], replacement=found["replacement"]) @app.route("/favicon.ico")