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:
138
static/app.js
138
static/app.js
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user