From 14d37f23e425b6ffe4ab3cf0ce15a1e9b5bbc2f4 Mon Sep 17 00:00:00 2001 From: knight Date: Wed, 5 Nov 2025 14:56:43 -0500 Subject: [PATCH] Add clickable reference badges and improve UI layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- config.py | 7 ++- search_app.py | 76 ++++++++++++----------- static/app.js | 138 ++++++++++++++++++++++++++++++++++-------- static/favicon.png | Bin 0 -> 1950 bytes static/frequency.html | 2 +- static/index.html | 33 ++++++---- static/style.css | 98 ++++++++++++++++++++++-------- 7 files changed, 255 insertions(+), 99 deletions(-) create mode 100644 static/favicon.png diff --git a/config.py b/config.py index 393470d..5017497 100644 --- a/config.py +++ b/config.py @@ -19,9 +19,14 @@ from typing import Optional # Load .env file if it exists try: from dotenv import load_dotenv + import logging + _logger = logging.getLogger(__name__) + _env_path = Path(__file__).parent / ".env" if _env_path.exists(): - load_dotenv(_env_path) + _logger.info(f"Loading .env from: {_env_path}") + result = load_dotenv(_env_path, override=True) + _logger.info(f"load_dotenv result: {result}") except ImportError: pass # python-dotenv not installed diff --git a/search_app.py b/search_app.py index 00f2da1..ca5f836 100644 --- a/search_app.py +++ b/search_app.py @@ -748,7 +748,9 @@ def create_app(config: AppConfig = CONFIG) -> Flask: "secondary": bool(highlight_map.get("transcript_secondary_full")), }, "internal_references_count": source.get("internal_references_count", 0), + "internal_references": source.get("internal_references", []), "referenced_by_count": source.get("referenced_by_count", 0), + "referenced_by": source.get("referenced_by", []), } ) @@ -802,48 +804,50 @@ def create_app(config: AppConfig = CONFIG) -> Flask: start = request.args.get("start", type=str) end = request.args.get("end", type=str) - filters: List[Dict] = [] - channel_filter = build_channel_filter(channels) - if channel_filter: - filters.append(channel_filter) - year_filter = build_year_filter(year) - if year_filter: - filters.append(year_filter) + def parse_flag(name: str, default: bool = True) -> bool: + value = request.args.get(name) + if value is None: + return default + lowered = value.lower() + return lowered not in {"0", "false", "no"} + + use_exact = parse_flag("exact", True) + use_fuzzy = parse_flag("fuzzy", True) + use_phrase = parse_flag("phrase", True) + if use_query_string: + use_exact = use_fuzzy = use_phrase = False + + search_payload = build_query_payload( + term, + channels=channels, + year=year, + sort="relevant", + use_exact=use_exact, + use_fuzzy=use_fuzzy, + use_phrase=use_phrase, + use_query_string=use_query_string, + ) + query = search_payload.get("query", {"match_all": {}}) + if start or end: range_filter: Dict[str, Dict[str, Dict[str, str]]] = {"range": {"date": {}}} if start: range_filter["range"]["date"]["gte"] = start if end: range_filter["range"]["date"]["lte"] = end - filters.append(range_filter) - - base_fields = ["title^3", "description^2", "transcript_full", "transcript_secondary_full"] - if use_query_string: - qs_query = term or "*" - must_clause: List[Dict[str, Any]] = [ - { - "query_string": { - "query": qs_query, - "default_operator": "AND", - "fields": base_fields, - } - } - ] - else: - must_clause = [ - { - "multi_match": { - "query": term, - "fields": base_fields, - "type": "best_fields", - "operator": "and", - } - } - ] - - query: Dict[str, Any] = {"bool": {"must": must_clause}} - if filters: - query["bool"]["filter"] = filters + if "bool" in query: + bool_clause = query.setdefault("bool", {}) + existing_filter = bool_clause.get("filter") + if existing_filter is None: + bool_clause["filter"] = [range_filter] + elif isinstance(existing_filter, list): + bool_clause["filter"].append(range_filter) + else: + bool_clause["filter"] = [existing_filter, range_filter] + elif query.get("match_all") is not None: + query = {"bool": {"filter": [range_filter]}} + else: + query = {"bool": {"must": [query], "filter": [range_filter]}} histogram: Dict[str, Any] = { "field": "date", diff --git a/static/app.js b/static/app.js index 83c0f09..5f0bfd2 100644 --- a/static/app.js +++ b/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 - ? `
${badges - .map((b) => `${escapeHtml(b)}` ) - .join('')}
` - : ''; header.innerHTML = ` ${titleHtml}
${escapeHtml(item.channel_name || "")} • ${fmtDate( item.date )}
- ${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)); diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..653704a38e515c0471e1aff29fd62af0644f00c8 GIT binary patch literal 1950 zcmV;P2VwY$P)1_9PTt%7?k=SK(*d{n7to=i~$=9B=&88{n>2a zfSO?;SNhF_3B0+QrWHF=zcmf$Zd9v7kIjLKWV{+Vm?z(W1t?=D)6Ikm=ZX?tb?L2| zd^)xOOyHdjm%XlH1qTY2C}x>`1B&-@CetP0AlOzMa9*wGH_Z}7l&#^_ycj}^qJrIW zc2IEg?~a>aGE?R0~D>cH=rZlkxb)mG0ph(H(v8bL^~Bk%1o#gx%d#* zhj|-h;rZ{w`nqLamM>y0h)YqBbTes9KY*QM z0@Q*NzstKclwm;3y@XAQt-(wfWPSt&*qI})if4bG&d3aEvP~0RtIS(RQ{;mN)D`zh=TMsK1_ z=P+iEHK9Y*GlYXIX6$hv+yMt`un?}t+8^nAvT*r@gj)T-Ye1`6i+`umx5(Sgy4?Vt zW$Yy#C7f@bGbaeL`2+~TLQs=o(`7B3DLEf$q9Ruxf_=PIib%)*7Z8pRUYwa><#q!| z1ZGi@^@l~$!)=1$N~{|;F6%kepJiF{c>~hwE$%g4sdey_6Bht;F9NS!+JK>qYwQr+ z7kBv-Mnr;$l_)rw_#$+c_B$ZJuNMQVq@ntg`C$B< z!500Dt!*yxgw}u-68Bm89cX5x3W$shHH7HhYh=5_&{vMF0e5JK4SHKqOA^x5#*39+ zgj#;p2Id+WlzYUQuFrrQB{YcxP7$XneeKXSr~*|9I@)3M^id)xtklH4_wn>0!U`X_ zaOVdLIBgKRHh^>p-M)~&%&`p`abv~i8PTmsLlxiXDUmc6wz&90v;hg)jU}^lTQgeY zLchlKg)F*{d7Bo6-*PE$vx|?j;;3}8Zh&E0C3jqE-oa{uU43%Yk+*j>vSRt`5{=WS zIpca-H-LGWtrnthBMF-mH`rZRxi}b!Mo+zCEh~7(Gp`#!up>J)=80i(4o_K6 zabXK=h-CvBq_Kq$H((!(jrDhE?F%vu2yP_}(%kzEc#f8Np}7=iXwqyQ8hB}F4ZzEC zEI+FPAl|ByC17sC6YAr*X`+>Y{~s0+=*q*V9rZecL+ud%`iWm_R(^+YO!98yXPYnZ{X!Ek@GG ziMYj*q>zZ8W41Ai|3o|{tl+PF^bI)pRTt=1Ix~RIiiJ`i)(5=5JfUjF352fOV|rlopU0aMlB|B}q)PbRQzBa3Ki| zRG3Te2FhL`gv`J>h`_dz?f`lC0bkAJLyS+M>Ucr1{D*YtYewee=E#0OebxX=&dy3M z>T7h?4r58X)GITmRd)SCt`bu0vj!xNHs(b%$4uLS>K10ngReUJwBD{julx%D0RR7z kG?Op@000I_L_t&o0Bhh@#%)1Xn*aa+07*qoM6N<$f*^UClK=n! literal 0 HcmV?d00001 diff --git a/static/frequency.html b/static/frequency.html index be24ec9..d537f21 100644 --- a/static/frequency.html +++ b/static/frequency.html @@ -4,6 +4,7 @@ Term Frequency Explorer +