radio: index UI exposes min_score / exclude_banter + score badges

Adds quality-filter controls to the search UI: a "min score" select
(any/2+/3+/4+/5) and a "hide banter" checkbox. Q/A hits gain a small
color-coded usefulness badge (1-5, red->green) and a topic_class tag
(computer-help, banter, off-topic, promo). Low-score and banter rows
render dimmed by default so they're visible but de-emphasized.

Defaults to "any" + banter visible to preserve existing search habits.
Mike toggles up when he wants quality. URL-encoded params built via
URLSearchParams so empty values don't leak into requests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 05:54:45 -07:00
parent 48c8b311bf
commit b9af34fbd8

View File

@@ -260,6 +260,18 @@ INDEX_HTML = """<!doctype html>
mark { background: #ffec99; padding: 0 .15em; }
.stats { font-size: 12px; color:#666; margin-top: 2em; }
.empty { color:#999; padding: 1em 0; }
.controls select, .controls input[type=checkbox] { margin-right: .35em; }
.badge { display: inline-block; min-width: 1.6em; padding: 0 .35em; margin-right: .35em;
font-size: 11px; font-weight: 600; text-align: center; border-radius: 3px;
color: #fff; background: #999; vertical-align: 1px; }
.badge.s5 { background: #2a8f43; }
.badge.s4 { background: #5aa54b; }
.badge.s3 { background: #999; }
.badge.s2 { background: #c08a3a; }
.badge.s1 { background: #b85a4a; }
.topic { font-size: 11px; color: #888; padding: 0 .35em; border-radius: 3px;
background: #f0f0f0; }
.hit.dim { opacity: .55; }
</style>
<h1>Computer Guru Radio Archive</h1>
<div class=sub id=sub>...</div>
@@ -268,6 +280,18 @@ INDEX_HTML = """<!doctype html>
<label><input type=radio name=kind value=both checked> both</label>
<label><input type=radio name=kind value=qa> Q&amp;A only</label>
<label><input type=radio name=kind value=segments> transcript only</label>
<span style="border-left:1px solid #ddd; padding-left:.6em">
<label>min score
<select id=min_score>
<option value=0>any</option>
<option value=2>2+</option>
<option value=3>3+</option>
<option value=4>4+</option>
<option value=5>5</option>
</select>
</label>
<label><input type=checkbox id=exclude_banter> hide banter</label>
</span>
</div>
<div id=results></div>
<div class=stats id=stats></div>
@@ -288,6 +312,8 @@ q.addEventListener('input', () => {
timer = setTimeout(runSearch, 250);
});
document.querySelectorAll('input[name=kind]').forEach(el => el.addEventListener('change', runSearch));
document.getElementById('min_score').addEventListener('change', runSearch);
document.getElementById('exclude_banter').addEventListener('change', runSearch);
function fmtTime(s) {
if (s == null) return '';
@@ -295,20 +321,41 @@ function fmtTime(s) {
return `${m}:${sec.toString().padStart(2,'0')}`;
}
function escapeHtml(s) {
return (s ?? '').replace(/[&<>"']/g, c => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[c]));
}
async function runSearch() {
const term = q.value.trim();
if (term.length < 2) { results.innerHTML = ''; return; }
const kind = document.querySelector('input[name=kind]:checked').value;
const r = await fetch(`/api/search?q=${encodeURIComponent(term)}&kind=${kind}&limit=40`);
const minScore = document.getElementById('min_score').value;
const excludeBanter = document.getElementById('exclude_banter').checked;
const params = new URLSearchParams({ q: term, kind, limit: '40' });
if (minScore !== '0') params.set('min_score', minScore);
if (excludeBanter) params.set('exclude_banter', 'true');
const r = await fetch(`/api/search?${params}`);
const j = await r.json();
let html = '';
if (j.qa.length) {
html += '<div class=group><h3>Q&amp;A Pairs</h3>';
for (const h of j.qa) {
const ad = h.air_date ? ` (${h.air_date})` : '';
const cn = h.caller_name ? ` — ${h.caller_name}` : '';
html += `<div class=hit>
<div class=meta>${h.year} · ${h.title}${ad}${cn} · @ ${fmtTime(h.question_start_sec)}</div>
const cn = h.caller_name ? ` — ${escapeHtml(h.caller_name)}` : '';
const score = h.usefulness_score;
const topic = h.topic_class;
const banter = h.is_banter === 1;
const badge = score != null
? `<span class="badge s${score}" title="usefulness ${score}/5">${score}</span>`
: '';
const topicTag = topic
? `<span class=topic>${escapeHtml(topic)}</span> `
: '';
const dim = (score != null && score <= 2) || banter ? ' dim' : '';
html += `<div class="hit${dim}">
<div class=meta>${badge}${topicTag}${h.year} · ${escapeHtml(h.title)}${ad}${cn} · @ ${fmtTime(h.question_start_sec)}</div>
<div><b>Q:</b> ${h.q_snippet}</div>
<div><b>A:</b> ${h.a_snippet}</div>
</div>`;