Make highlights clickable to jump to transcript location
This commit is contained in:
parent
a3c9377ef7
commit
69bff7549c
100
static/app.js
100
static/app.js
@ -325,6 +325,7 @@
|
||||
function renderTranscriptSegment(segment, videoUrl) {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'transcript-segment';
|
||||
segmentDiv.dataset.text = (segment.text || '').toLowerCase();
|
||||
|
||||
const startSeconds = segment.start_seconds || segment.start || 0;
|
||||
const timestampText = formatTimestamp(startSeconds);
|
||||
@ -347,14 +348,88 @@
|
||||
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');
|
||||
if (existingTranscript) {
|
||||
if (existingTranscript && !highlightText) {
|
||||
existingTranscript.remove();
|
||||
button.textContent = 'View Full Transcript';
|
||||
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.textContent = 'Loading...';
|
||||
|
||||
@ -430,6 +505,16 @@
|
||||
containerElement.appendChild(transcriptDiv);
|
||||
button.textContent = 'Hide Transcript';
|
||||
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) {
|
||||
console.error('Error fetching transcript:', err);
|
||||
button.textContent = 'View Full Transcript';
|
||||
@ -751,6 +836,17 @@ async function updateFrequencyChart(term, channels, queryMode) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "highlight-row";
|
||||
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);
|
||||
});
|
||||
if (highlights.childElementCount) {
|
||||
|
||||
@ -187,6 +187,12 @@ button {
|
||||
.highlight-row {
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #ececec;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.highlight-row:hover {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.highlight-row:last-child {
|
||||
@ -259,6 +265,8 @@ mark {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ececec;
|
||||
transition: background 0.3s, padding 0.3s;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.transcript-segment:last-child {
|
||||
@ -266,6 +274,22 @@ mark {
|
||||
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 {
|
||||
display: inline-block;
|
||||
color: #0366d6;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user