Make highlights clickable to jump to transcript location

This commit is contained in:
knight 2025-11-02 01:20:30 -04:00
parent a3c9377ef7
commit 69bff7549c
2 changed files with 122 additions and 2 deletions

View File

@ -325,6 +325,7 @@
function renderTranscriptSegment(segment, videoUrl) { function renderTranscriptSegment(segment, videoUrl) {
const segmentDiv = document.createElement('div'); const segmentDiv = document.createElement('div');
segmentDiv.className = 'transcript-segment'; segmentDiv.className = 'transcript-segment';
segmentDiv.dataset.text = (segment.text || '').toLowerCase();
const startSeconds = segment.start_seconds || segment.start || 0; const startSeconds = segment.start_seconds || segment.start || 0;
const timestampText = formatTimestamp(startSeconds); const timestampText = formatTimestamp(startSeconds);
@ -347,14 +348,88 @@
return segmentDiv; return segmentDiv;
} }
async function fetchAndDisplayTranscript(videoId, videoUrl, containerElement, button) { function stripHtmlAndNormalize(html) {
const temp = document.createElement('div');
temp.innerHTML = html;
return temp.textContent.trim().toLowerCase().replace(/\s+/g, ' ');
}
function findMatchingSegment(transcriptDiv, searchText) {
const segments = transcriptDiv.querySelectorAll('.transcript-segment');
const normalized = searchText.toLowerCase().replace(/\s+/g, ' ').trim();
// First try exact match
for (const segment of segments) {
const segmentText = segment.dataset.text;
if (segmentText && segmentText.includes(normalized)) {
return segment;
}
}
// If no exact match, try matching by words (at least 70% of words match)
const searchWords = normalized.split(' ').filter(w => w.length > 2);
if (searchWords.length === 0) return null;
let bestMatch = null;
let bestScore = 0;
for (const segment of segments) {
const segmentText = segment.dataset.text;
if (!segmentText) continue;
let matchCount = 0;
for (const word of searchWords) {
if (segmentText.includes(word)) {
matchCount++;
}
}
const score = matchCount / searchWords.length;
if (score > bestScore && score >= 0.5) {
bestScore = score;
bestMatch = segment;
}
}
return bestMatch;
}
function scrollToSegment(segment) {
if (!segment) return;
// Remove any existing focus
const previousFocused = segment.parentElement.querySelectorAll('.transcript-segment.focused');
previousFocused.forEach(s => s.classList.remove('focused'));
// Add focus to this segment
segment.classList.add('focused');
// Scroll to it
segment.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Remove focus after animation
setTimeout(() => {
segment.classList.remove('focused');
}, 3000);
}
async function fetchAndDisplayTranscript(videoId, videoUrl, containerElement, button, highlightText = null) {
const existingTranscript = containerElement.querySelector('.full-transcript'); const existingTranscript = containerElement.querySelector('.full-transcript');
if (existingTranscript) { if (existingTranscript && !highlightText) {
existingTranscript.remove(); existingTranscript.remove();
button.textContent = 'View Full Transcript'; button.textContent = 'View Full Transcript';
return; return;
} }
// If transcript exists and we have highlight text, just scroll to it
if (existingTranscript && highlightText) {
const segment = findMatchingSegment(existingTranscript, highlightText);
if (segment) {
scrollToSegment(segment);
}
return;
}
button.disabled = true; button.disabled = true;
button.textContent = 'Loading...'; button.textContent = 'Loading...';
@ -430,6 +505,16 @@
containerElement.appendChild(transcriptDiv); containerElement.appendChild(transcriptDiv);
button.textContent = 'Hide Transcript'; button.textContent = 'Hide Transcript';
button.disabled = false; button.disabled = false;
// If highlight text provided, scroll to it after a brief delay
if (highlightText) {
setTimeout(() => {
const segment = findMatchingSegment(transcriptDiv, highlightText);
if (segment) {
scrollToSegment(segment);
}
}, 100);
}
} catch (err) { } catch (err) {
console.error('Error fetching transcript:', err); console.error('Error fetching transcript:', err);
button.textContent = 'View Full Transcript'; button.textContent = 'View Full Transcript';
@ -751,6 +836,17 @@ async function updateFrequencyChart(term, channels, queryMode) {
const row = document.createElement("div"); const row = document.createElement("div");
row.className = "highlight-row"; row.className = "highlight-row";
row.innerHTML = html; row.innerHTML = html;
row.title = "Click to jump to this location in the transcript";
// Make highlight clickable
row.onclick = () => {
const transcriptBtn = el.querySelector(".transcript-toggle");
if (transcriptBtn && item.video_id) {
const highlightText = stripHtmlAndNormalize(html);
fetchAndDisplayTranscript(item.video_id, item.url, el, transcriptBtn, highlightText);
}
};
highlights.appendChild(row); highlights.appendChild(row);
}); });
if (highlights.childElementCount) { if (highlights.childElementCount) {

View File

@ -187,6 +187,12 @@ button {
.highlight-row { .highlight-row {
padding: 4px 0; padding: 4px 0;
border-bottom: 1px solid #ececec; border-bottom: 1px solid #ececec;
cursor: pointer;
transition: background 0.2s;
}
.highlight-row:hover {
background: #f0f7ff;
} }
.highlight-row:last-child { .highlight-row:last-child {
@ -259,6 +265,8 @@ mark {
margin-bottom: 12px; margin-bottom: 12px;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 1px solid #ececec; border-bottom: 1px solid #ececec;
transition: background 0.3s, padding 0.3s;
border-radius: 4px;
} }
.transcript-segment:last-child { .transcript-segment:last-child {
@ -266,6 +274,22 @@ mark {
margin-bottom: 0; margin-bottom: 0;
} }
.transcript-segment.focused {
background: #fff3cd;
padding: 8px;
border: 2px solid #ffc107;
animation: pulse-highlight 1s ease-in-out;
}
@keyframes pulse-highlight {
0%, 100% {
background: #fff3cd;
}
50% {
background: #ffe69c;
}
}
.timestamp-link { .timestamp-link {
display: inline-block; display: inline-block;
color: #0366d6; color: #0366d6;