feat(ebook): add admin and book-detail UI for protected phrase pipeline
Expose the protected phrase extraction pipeline through the web UI:
- Admin routes: POST /admin/build-phrases, /admin/generate-ngrams, and
/admin/judge-ngrams, each wrapping the protected_phrases.lib backfill
helpers, committing on success, rolling back and rendering an error
partial on failure, and reporting per-book/candidate/mention counts.
- Book detail page: show candidate, judged, and protected phrase counts,
list top candidate n-grams (with kept/rejected status) and protected
phrases, and add a POST /books/{id}/recalculate-phrases action that
clears and regenerates candidates, then redirects back with a status
message.
- Admin template: add Generate/Judge n-gram buttons.
Also reflows admin.html to 2-space HTML formatting.
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
{% if source %}
|
||||
<h1>{{ source.title }}</h1>
|
||||
<p class="meta">{{ source.author or "Unknown author" }}</p>
|
||||
{% if phrase_status_message %}
|
||||
<p class="status">{{ phrase_status_message }}</p>
|
||||
{% endif %}
|
||||
<dl class="card">
|
||||
<dt>File</dt>
|
||||
<dd>{{ source.file_path }}</dd>
|
||||
@@ -13,7 +16,84 @@
|
||||
<dd>{{ chapter_count }}</dd>
|
||||
<dt>Chunks</dt>
|
||||
<dd>{{ chunk_count }}</dd>
|
||||
<dt>Candidates</dt>
|
||||
<dd>{{ candidate_count }}</dd>
|
||||
<dt>Judged</dt>
|
||||
<dd>{{ judged_candidate_count }}</dd>
|
||||
<dt>Protected</dt>
|
||||
<dd>{{ protected_count }}</dd>
|
||||
</dl>
|
||||
<form
|
||||
method="post"
|
||||
action="/books/{{ source.id }}/recalculate-phrases"
|
||||
onsubmit="return confirm('Remove old phrases for this book and generate new candidates?');"
|
||||
>
|
||||
<button type="submit">Recalculate phrases</button>
|
||||
</form>
|
||||
|
||||
<section>
|
||||
<h2>Candidate n-grams</h2>
|
||||
{% if candidates %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Phrase</th>
|
||||
<th>Status</th>
|
||||
<th>Score</th>
|
||||
<th>Count</th>
|
||||
<th>Chapters</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td>{{ candidate.phrase_text }}</td>
|
||||
<td>
|
||||
{% if candidate.llm_judged %}
|
||||
{% if candidate.llm_keep %}Kept{% else %}Rejected{% endif %}
|
||||
{% else %}
|
||||
Candidate
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ "%.2f"|format(candidate.candidate_score) }}</td>
|
||||
<td>{{ candidate.raw_count }}</td>
|
||||
<td>{{ candidate.chapter_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No candidate n-grams.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Protected phrases</h2>
|
||||
{% if protected_phrases %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Phrase</th>
|
||||
<th>Type</th>
|
||||
<th>Confidence</th>
|
||||
<th>Importance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for phrase in protected_phrases %}
|
||||
<tr>
|
||||
<td>{{ phrase.phrase_text }}</td>
|
||||
<td>{{ phrase.phrase_type or "phrase" }}</td>
|
||||
<td>{{ "%.2f"|format(phrase.confidence) }}</td>
|
||||
<td>{{ "%.2f"|format(phrase.importance) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No protected phrases.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% else %}
|
||||
<h1>Book not found</h1>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user