(() => { const queryInput = document.getElementById("vectorQuery"); const searchBtn = document.getElementById("vectorSearchBtn"); const resultsDiv = document.getElementById("vectorResults"); const metaDiv = document.getElementById("vectorMeta"); const transcriptCache = new Map(); if (!queryInput || !searchBtn || !resultsDiv || !metaDiv) { console.error("Vector search elements missing"); return; } /** Utility helpers **/ const escapeHtml = (str) => (str || "").replace(/[&<>"']/g, (ch) => { switch (ch) { case "&": return "&"; case "<": return "<"; case ">": return ">"; case '"': return """; case "'": return "'"; default: return ch; } }); const fmtDate = (value) => { try { return (value || "").split("T")[0]; } catch { return value; } }; const fmtSimilarity = (score) => { if (typeof score !== "number" || Number.isNaN(score)) return ""; return score.toFixed(3); }; const getVideoStatus = (item) => (item && item.video_status ? String(item.video_status).toLowerCase() : ""); const isLikelyDeleted = (item) => getVideoStatus(item) === "deleted"; const formatTimestamp = (seconds) => { if (!seconds && seconds !== 0) return "00:00"; const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${mins.toString().padStart(2, "0")}:${secs .toString() .padStart(2, "0")}`; } return `${mins}:${secs.toString().padStart(2, "0")}`; }; const formatSegmentTimestamp = (segment) => { if (!segment) return ""; if (segment.timestamp) return segment.timestamp; const fields = [ segment.start_seconds, segment.start, segment.offset, segment.time, ]; for (const value of fields) { if (value == null) continue; const num = parseFloat(value); if (!Number.isNaN(num)) { return formatTimestamp(num); } } return ""; }; const serializeTranscriptSection = (label, parts, fullText) => { let content = ""; if (typeof fullText === "string" && fullText.trim()) { content = fullText.trim(); } else if (Array.isArray(parts) && parts.length) { content = parts .map((segment) => { const ts = formatSegmentTimestamp(segment); const text = segment && segment.text ? segment.text : ""; return ts ? `[${ts}] ${text}` : text; }) .join("\n") .trim(); } if (!content) return ""; return `${label}\n${content}\n`; }; const fetchTranscriptData = async (videoId) => { if (!videoId) return null; if (transcriptCache.has(videoId)) { return transcriptCache.get(videoId); } const res = await fetch(`/api/transcript?video_id=${encodeURIComponent(videoId)}`); if (!res.ok) { throw new Error(`Transcript fetch failed (${res.status})`); } const data = await res.json(); transcriptCache.set(videoId, data); return data; }; const buildTranscriptDownloadText = (item, transcriptData) => { const lines = []; lines.push(`Title: ${item.title || "Untitled"}`); if (item.channel_name) lines.push(`Channel: ${item.channel_name}`); if (item.date) lines.push(`Published: ${item.date}`); if (item.url) lines.push(`URL: ${item.url}`); lines.push(""); const primaryText = serializeTranscriptSection( "Primary Transcript", transcriptData.transcript_parts, transcriptData.transcript_full ); const secondaryText = serializeTranscriptSection( "Secondary Transcript", transcriptData.transcript_secondary_parts, transcriptData.transcript_secondary_full ); if (primaryText) lines.push(primaryText); if (secondaryText) lines.push(secondaryText); if (!primaryText && !secondaryText) { lines.push("No transcript available."); } return lines.join("\n").trim() + "\n"; }; const flashButtonMessage = (button, message, duration = 1800) => { if (!button) return; const original = button.dataset.originalLabel || button.textContent; button.dataset.originalLabel = original; button.textContent = message; setTimeout(() => { button.textContent = button.dataset.originalLabel || original; }, duration); }; const handleTranscriptDownload = async (item, button) => { if (!item.video_id) return; button.disabled = true; try { const transcriptData = await fetchTranscriptData(item.video_id); if (!transcriptData) throw new Error("Transcript unavailable"); const text = buildTranscriptDownloadText(item, transcriptData); const blob = new Blob([text], { type: "text/plain" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${item.video_id}.txt`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); flashButtonMessage(button, "Downloaded"); } catch (err) { console.error("Download failed", err); alert("Unable to download transcript right now."); } finally { button.disabled = false; } }; const formatMlaDate = (value) => { if (!value) return "n.d."; const parsed = new Date(value); if (Number.isNaN(parsed.valueOf())) return value; const months = [ "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec.", ]; return `${parsed.getDate()} ${months[parsed.getMonth()]} ${parsed.getFullYear()}`; }; const buildMlaCitation = (item) => { const channel = (item.channel_name || item.channel_id || "Unknown").trim(); const title = (item.title || "Untitled").trim(); const url = item.url || ""; const publishDate = formatMlaDate(item.date); const today = formatMlaDate(new Date().toISOString().split("T")[0]); return `${channel}. "${title}." YouTube, uploaded by ${channel}, ${publishDate}, ${url}. Accessed ${today}.`; }; const handleCopyCitation = async (item, button) => { const citation = buildMlaCitation(item); try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(citation); } else { const textarea = document.createElement("textarea"); textarea.value = citation; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); } flashButtonMessage(button, "Copied!"); } catch (err) { console.error("Citation copy failed", err); alert(citation); } }; /** Rendering helpers **/ const createHighlightRows = (entries) => { if (!Array.isArray(entries) || !entries.length) return null; const container = document.createElement("div"); container.className = "transcript highlight-list"; entries.forEach((entry) => { if (!entry) return; const row = document.createElement("div"); row.className = "highlight-row"; const textBlock = document.createElement("div"); textBlock.className = "highlight-text"; const html = entry.html || entry.text || entry; textBlock.innerHTML = html || ""; row.appendChild(textBlock); const indicator = document.createElement("span"); indicator.className = "highlight-source-indicator highlight-source-indicator--primary"; indicator.title = "Vector highlight"; row.appendChild(indicator); container.appendChild(row); }); return container; }; const createActions = (item) => { const actions = document.createElement("div"); actions.className = "result-actions"; const downloadBtn = document.createElement("button"); downloadBtn.type = "button"; downloadBtn.className = "result-action-btn"; downloadBtn.textContent = "Download transcript"; downloadBtn.addEventListener("click", () => handleTranscriptDownload(item, downloadBtn)); actions.appendChild(downloadBtn); const citationBtn = document.createElement("button"); citationBtn.type = "button"; citationBtn.className = "result-action-btn"; citationBtn.textContent = "Copy citation"; citationBtn.addEventListener("click", () => handleCopyCitation(item, citationBtn)); actions.appendChild(citationBtn); const graphBtn = document.createElement("button"); graphBtn.type = "button"; graphBtn.className = "result-action-btn graph-launch-btn"; graphBtn.textContent = "Graph"; graphBtn.disabled = !item.video_id; graphBtn.addEventListener("click", () => { if (!item.video_id) return; const target = `/graph?video_id=${encodeURIComponent(item.video_id)}`; window.open(target, "_blank", "noopener"); }); actions.appendChild(graphBtn); return actions; }; const renderVectorResults = (payload) => { resultsDiv.innerHTML = ""; const items = payload.items || []; if (!items.length) { metaDiv.textContent = "No vector matches for this prompt."; return; } metaDiv.textContent = `Matches: ${items.length} (vector mode)`; items.forEach((item) => { const el = document.createElement("div"); el.className = "item"; const header = document.createElement("div"); header.className = "result-header"; const headerMain = document.createElement("div"); headerMain.className = "result-header-main"; const titleEl = document.createElement("strong"); titleEl.innerHTML = item.titleHtml || escapeHtml(item.title || "Untitled"); headerMain.appendChild(titleEl); const metaLine = document.createElement("div"); metaLine.className = "muted result-meta"; const channelLabel = item.channel_name || item.channel_id || "Unknown"; const dateLabel = fmtDate(item.date); let durationSeconds = null; if (typeof item.duration === "number") { durationSeconds = item.duration; } else if (typeof item.duration === "string" && item.duration.trim()) { const parsed = parseFloat(item.duration); if (!Number.isNaN(parsed)) { durationSeconds = parsed; } } const durationLabel = durationSeconds != null ? ` • ${formatTimestamp(durationSeconds)}` : ""; metaLine.textContent = channelLabel ? `${channelLabel} • ${dateLabel}${durationLabel}` : `${dateLabel}${durationLabel}`; if (isLikelyDeleted(item)) { metaLine.appendChild(document.createTextNode(" ")); const statusEl = document.createElement("span"); statusEl.className = "result-status result-status--deleted"; statusEl.textContent = "Likely deleted"; metaLine.appendChild(statusEl); } headerMain.appendChild(metaLine); if (item.url) { const linkLine = document.createElement("div"); linkLine.className = "muted"; const anchor = document.createElement("a"); anchor.href = item.url; anchor.target = "_blank"; anchor.rel = "noopener"; anchor.textContent = "Open on YouTube"; linkLine.appendChild(anchor); headerMain.appendChild(linkLine); } if (typeof item.distance === "number") { const scoreLine = document.createElement("div"); scoreLine.className = "muted"; scoreLine.textContent = `Similarity score: ${fmtSimilarity(item.distance)}`; headerMain.appendChild(scoreLine); } header.appendChild(headerMain); header.appendChild(createActions(item)); el.appendChild(header); if (item.descriptionHtml || item.description) { const desc = document.createElement("div"); desc.className = "muted description-block"; desc.innerHTML = item.descriptionHtml || escapeHtml(item.description); el.appendChild(desc); } if (item.chunkText) { const chunkBlock = document.createElement("div"); chunkBlock.className = "vector-chunk"; if (item.chunkTimestamp && item.url) { const tsObj = typeof item.chunkTimestamp === "object" ? item.chunkTimestamp : { timestamp: item.chunkTimestamp }; const ts = formatSegmentTimestamp(tsObj); const tsLink = document.createElement("a"); const paramValue = typeof item.chunkTimestamp === "number" ? Math.floor(item.chunkTimestamp) : item.chunkTimestamp; tsLink.href = `${item.url}${item.url.includes("?") ? "&" : "?"}t=${encodeURIComponent( paramValue )}`; tsLink.target = "_blank"; tsLink.rel = "noopener"; tsLink.textContent = ts ? `[${ts}]` : "[timestamp]"; chunkBlock.appendChild(tsLink); chunkBlock.appendChild(document.createTextNode(" ")); } const chunkTextSpan = document.createElement("span"); chunkTextSpan.textContent = item.chunkText; chunkBlock.appendChild(chunkTextSpan); el.appendChild(chunkBlock); } const highlights = createHighlightRows(item.toHighlight); if (highlights) { el.appendChild(highlights); } resultsDiv.appendChild(el); }); }; /** Search handler **/ const runVectorSearch = async () => { const query = queryInput.value.trim(); if (!query) { alert("Please enter a query."); return; } metaDiv.textContent = "Searching vector index…"; resultsDiv.innerHTML = ""; searchBtn.disabled = true; try { const res = await fetch("/api/vector-search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }), }); if (!res.ok) { throw new Error(`Vector search failed (${res.status})`); } const data = await res.json(); if (data.error) { metaDiv.textContent = "Vector search unavailable."; return; } renderVectorResults(data); } catch (err) { console.error(err); metaDiv.textContent = "Vector search unavailable."; } finally { searchBtn.disabled = false; } }; searchBtn.addEventListener("click", runVectorSearch); queryInput.addEventListener("keypress", (event) => { if (event.key === "Enter") { runVectorSearch(); } }); })();