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:
@@ -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&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 => ({
|
||||
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||
}[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&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>`;
|
||||
|
||||
Reference in New Issue
Block a user