Add clickable reference badges and improve UI layout

- Add clickable badges for backlinks and references that trigger query string searches
- Improve toggle checkbox layout with better styling
- Add description block styling with scrollable container
- Update results styling with bordered cards and shadows
- Add favicon support across pages
- Enhance .env loading with logging for debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-05 14:56:43 -05:00
parent d8d2c5e34c
commit 14d37f23e4
7 changed files with 255 additions and 99 deletions

View File

@@ -212,6 +212,36 @@
}
}
function ensureQueryStringMode() {
if (!queryToggle) return;
if (!queryToggle.checked) {
rememberToggleState();
queryToggle.checked = true;
applyQueryMode();
}
}
function escapeQueryValue(value) {
return value.replace(/(["\\])/g, "\\$1");
}
function buildFieldClause(field, ids) {
if (!Array.isArray(ids)) return null;
const seen = new Set();
const collected = [];
ids.forEach((raw) => {
if (!raw && raw !== 0) return;
const value = String(raw).trim();
if (!value) return;
if (seen.has(value)) return;
seen.add(value);
collected.push(value);
});
if (!collected.length) return null;
const escaped = collected.map((id) => `"${escapeQueryValue(id)}"`);
return `${field}:(${escaped.join(" OR ")})`;
}
if (channelOptions) {
channelOptions.addEventListener("change", (event) => {
const target = event.target;
@@ -920,7 +950,7 @@ function renderFrequencyChart(buckets, channelTotals) {
freqChart.appendChild(legend);
}
async function updateFrequencyChart(term, channels, year, queryMode) {
async function updateFrequencyChart(term, channels, year, queryMode, toggles = {}) {
if (!freqChart || typeof d3 === "undefined") {
return;
}
@@ -944,6 +974,10 @@ async function updateFrequencyChart(term, channels, year, queryMode) {
if (queryMode) {
params.set("query_string", "1");
}
const { exact = true, fuzzy = true, phrase = true } = toggles || {};
params.set("exact", exact ? "1" : "0");
params.set("fuzzy", fuzzy ? "1" : "0");
params.set("phrase", phrase ? "1" : "0");
clearFrequency("Loading timeline…");
try {
@@ -962,11 +996,16 @@ async function updateFrequencyChart(term, channels, year, queryMode) {
freqSummary.textContent = `Matches: ${total.toLocaleString()} • Interval: ${payload.interval || "month"}`;
}
}
const buckets = payload.buckets || [];
if (total === 0) {
freqChart.innerHTML = "";
return;
}
renderFrequencyChart(payload.buckets || [], payload.channels || []);
if (!buckets.length) {
clearFrequency("Timeline unavailable for this query (missing video dates).");
return;
}
renderFrequencyChart(buckets, payload.channels || []);
} catch (err) {
console.error(err);
clearFrequency("Timeline unavailable.");
@@ -988,46 +1027,97 @@ async function updateFrequencyChart(term, channels, year, queryMode) {
item.descriptionHtml || escapeHtml(item.description || "");
const header = document.createElement("div");
const badges = [];
if (item.highlightSource && item.highlightSource.primary) badges.push('primary transcript');
if (item.highlightSource && item.highlightSource.secondary) badges.push('secondary transcript');
const badgeDefs = [];
if (item.highlightSource && item.highlightSource.primary) {
badgeDefs.push({ label: "primary transcript" });
}
if (item.highlightSource && item.highlightSource.secondary) {
badgeDefs.push({ label: "secondary transcript" });
}
// Add reference count badges
const refByCount = item.referenced_by_count || 0;
const refToCount = item.internal_references_count || 0;
const refByIds = Array.isArray(item.referenced_by) ? item.referenced_by : [];
const refToIds = Array.isArray(item.internal_references) ? item.internal_references : [];
// Debug: log reference counts for first item
if (badges.length === 0 || badges.length <= 2) {
console.log('Reference counts:', {
title: item.title,
referenced_by_count: refByCount,
internal_references_count: refToCount,
hasReferencedBy: 'referenced_by_count' in item,
hasInternalRefs: 'internal_references_count' in item
if (refByCount > 0) {
let query = null;
if (item.video_id) {
query = buildFieldClause("internal_references", [item.video_id]);
}
if (!query) {
query = buildFieldClause("video_id", refByIds);
}
badgeDefs.push({
label: `${refByCount} backlink${refByCount !== 1 ? "s" : ""}`,
query,
title: query
? "Show videos that reference this one"
: "Reference list unavailable in this result",
});
}
if (refToCount > 0) {
const query = buildFieldClause("video_id", refToIds);
badgeDefs.push({
label: `${refToCount} reference${refToCount !== 1 ? "s" : ""}`,
query,
title: query
? "Show videos referenced by this one"
: "Reference list unavailable in this result",
});
}
if (refByCount > 0) badges.push(`${refByCount} backlink${refByCount !== 1 ? 's' : ''}`);
if (refToCount > 0) badges.push(`${refToCount} reference${refToCount !== 1 ? 's' : ''}`);
const badgeHtml = badges.length
? `<div class="badge-row">${badges
.map((b) => `<span class="badge">${escapeHtml(b)}</span>` )
.join('')}</div>`
: '';
header.innerHTML = `
<strong>${titleHtml}</strong>
<div class="muted">${escapeHtml(item.channel_name || "")}${fmtDate(
item.date
)}</div>
<div class="muted"><a href="${item.url}" target="_blank" rel="noopener">Open on YouTube</a></div>
${badgeHtml}
`;
if (badgeDefs.length) {
const badgeRow = document.createElement("div");
badgeRow.className = "badge-row";
badgeDefs.forEach((badge) => {
if (!badge || !badge.label) return;
const badgeEl = document.createElement("span");
badgeEl.className = "badge";
badgeEl.textContent = badge.label;
if (badge.title) {
badgeEl.title = badge.title;
}
if (badge.query) {
badgeEl.classList.add("badge-clickable");
badgeEl.setAttribute("role", "button");
badgeEl.tabIndex = 0;
const triggerSearch = () => {
if (!badge.query) return;
qInput.value = badge.query;
ensureQueryStringMode();
runSearch(0);
};
badgeEl.addEventListener("click", (event) => {
event.preventDefault();
triggerSearch();
});
badgeEl.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
triggerSearch();
}
});
}
badgeRow.appendChild(badgeEl);
});
if (badgeRow.childElementCount) {
header.appendChild(badgeRow);
}
}
el.appendChild(header);
if (descriptionHtml) {
const desc = document.createElement("div");
desc.className = "muted";
desc.className = "muted description-block";
desc.innerHTML = descriptionHtml;
el.appendChild(desc);
}
@@ -1130,7 +1220,7 @@ async function updateFrequencyChart(term, channels, year, queryMode) {
const res = await fetch(`/api/search?${params.toString()}`);
const payload = await res.json();
renderResults(payload, page);
updateFrequencyChart(q, channels, year, queryMode);
updateFrequencyChart(q, channels, year, queryMode, { exact, fuzzy, phrase });
}
searchBtn.addEventListener("click", () => runSearch(0));