Add video reference tracking and display

- Add "Most referenced" sort option to sort by backlink count
- Backend now supports sorting by referenced_by_count field
- Search results now display reference counts as badges:
  - Shows number of backlinks (videos linking to this one)
  - Shows number of internal references (outbound links)
- Reference badges appear alongside transcript source badges

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-05 10:52:00 -05:00
parent 2846e13a81
commit 7988e2751a
3 changed files with 133 additions and 4 deletions

View File

@@ -35,6 +35,7 @@
const channelDropdown = document.getElementById("channelDropdown");
const channelSummary = document.getElementById("channelSummary");
const channelOptions = document.getElementById("channelOptions");
const yearSel = document.getElementById("year");
const sortSel = document.getElementById("sort");
const sizeSel = document.getElementById("size");
const exactToggle = document.getElementById("exactToggle");
@@ -140,8 +141,29 @@
}
}
async function loadYears() {
if (!yearSel) return;
try {
const res = await fetch("/api/years");
const data = await res.json();
// Keep the "All Years" option
yearSel.innerHTML = '<option value="">All Years</option>';
data.forEach((item) => {
const option = document.createElement("option");
option.value = item.Year;
option.textContent = `${item.Year} (${item.Count})`;
yearSel.appendChild(option);
});
} catch (err) {
console.error("Failed to load years", err);
}
}
function setFromQuery() {
qInput.value = qs.get("q") || "";
yearSel.value = qs.get("year") || "";
sortSel.value = qs.get("sort") || "relevant";
sizeSel.value = qs.get("size") || "10";
pendingChannelSelection = parseChannelParams(qs);
@@ -305,13 +327,18 @@
}
}
function updateUrl(q, sort, channels, page, size, exact, fuzzy, phrase, queryMode) {
function updateUrl(q, sort, channels, year, page, size, exact, fuzzy, phrase, queryMode) {
const next = new URL(window.location.href);
next.searchParams.set("q", q);
next.searchParams.set("sort", sort);
next.searchParams.delete("channel_id");
next.searchParams.delete("channel");
channels.forEach((id) => next.searchParams.append("channel_id", id));
if (year) {
next.searchParams.set("year", year);
} else {
next.searchParams.delete("year");
}
next.searchParams.set("page", page);
next.searchParams.set("size", size);
next.searchParams.set("exact", exact ? "1" : "0");
@@ -893,7 +920,7 @@ function renderFrequencyChart(buckets, channelTotals) {
freqChart.appendChild(legend);
}
async function updateFrequencyChart(term, channels, queryMode) {
async function updateFrequencyChart(term, channels, year, queryMode) {
if (!freqChart || typeof d3 === "undefined") {
return;
}
@@ -911,6 +938,9 @@ async function updateFrequencyChart(term, channels, queryMode) {
params.set("term", trimmed);
params.set("interval", "month");
(channels || []).forEach((id) => params.append("channel_id", id));
if (year) {
params.set("year", year);
}
if (queryMode) {
params.set("query_string", "1");
}
@@ -961,6 +991,13 @@ async function updateFrequencyChart(term, channels, queryMode) {
const badges = [];
if (item.highlightSource && item.highlightSource.primary) badges.push('primary transcript');
if (item.highlightSource && item.highlightSource.secondary) badges.push('secondary transcript');
// Add reference count badges
const refByCount = item.referenced_by_count || 0;
const refToCount = item.internal_references_count || 0;
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>` )
@@ -1041,6 +1078,7 @@ async function updateFrequencyChart(term, channels, queryMode) {
async function runSearch(pageOverride, pushState = true) {
const q = qInput.value.trim();
const channels = getSelectedChannels();
const year = yearSel.value;
const sort = sortSel.value;
const size = parseInt(sizeSel.value, 10) || 10;
const queryMode = queryToggle && queryToggle.checked;
@@ -1062,7 +1100,7 @@ async function updateFrequencyChart(term, channels, queryMode) {
currentPage = page;
if (pushState) {
updateUrl(q, sort, channels, page, size, exact, fuzzy, phrase, queryMode);
updateUrl(q, sort, channels, year, page, size, exact, fuzzy, phrase, queryMode);
}
const params = new URLSearchParams();
@@ -1075,17 +1113,19 @@ async function updateFrequencyChart(term, channels, queryMode) {
params.set("phrase", phrase ? "1" : "0");
params.set("query_string", queryMode ? "1" : "0");
channels.forEach((id) => params.append("channel_id", id));
if (year) params.set("year", year);
const res = await fetch(`/api/search?${params.toString()}`);
const payload = await res.json();
renderResults(payload, page);
updateFrequencyChart(q, channels, queryMode);
updateFrequencyChart(q, channels, year, queryMode);
}
searchBtn.addEventListener("click", () => runSearch(0));
qInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") runSearch(0);
});
yearSel.addEventListener("change", () => runSearch(0));
sortSel.addEventListener("change", () => runSearch(0));
sizeSel.addEventListener("change", () => runSearch(0));
exactToggle.addEventListener("change", () => { rememberToggleState(); runSearch(0); });
@@ -1104,6 +1144,7 @@ window.addEventListener("popstate", () => {
setFromQuery();
loadMetrics();
loadYears();
loadChannels().then(() => runSearch(currentPage));
})();