Add full transcript viewer with clickable timestamps

This commit is contained in:
2025-11-02 01:17:47 -04:00
parent fcdc6ecb9b
commit a3c9377ef7
2 changed files with 234 additions and 1 deletions

View File

@@ -303,8 +303,140 @@
return n;
}
function 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);
// Transcript viewer functionality removed.
if (hours > 0) {
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function getYouTubeTimestampUrl(baseUrl, seconds) {
if (!baseUrl) return '#';
const url = new URL(baseUrl);
url.searchParams.set('t', Math.floor(seconds) + 's');
return url.toString();
}
function renderTranscriptSegment(segment, videoUrl) {
const segmentDiv = document.createElement('div');
segmentDiv.className = 'transcript-segment';
const startSeconds = segment.start_seconds || segment.start || 0;
const timestampText = formatTimestamp(startSeconds);
const timestampUrl = getYouTubeTimestampUrl(videoUrl, startSeconds);
const timestampLink = document.createElement('a');
timestampLink.href = timestampUrl;
timestampLink.className = 'timestamp-link';
timestampLink.textContent = timestampText;
timestampLink.target = '_blank';
timestampLink.rel = 'noopener';
const textSpan = document.createElement('span');
textSpan.className = 'transcript-text';
textSpan.textContent = segment.text || '';
segmentDiv.appendChild(timestampLink);
segmentDiv.appendChild(textSpan);
return segmentDiv;
}
async function fetchAndDisplayTranscript(videoId, videoUrl, containerElement, button) {
const existingTranscript = containerElement.querySelector('.full-transcript');
if (existingTranscript) {
existingTranscript.remove();
button.textContent = 'View Full Transcript';
return;
}
button.disabled = true;
button.textContent = 'Loading...';
try {
const res = await fetch(`/api/transcript?video_id=${encodeURIComponent(videoId)}`);
if (!res.ok) {
throw new Error(`Failed to fetch transcript: ${res.status}`);
}
const data = await res.json();
const transcriptDiv = document.createElement('div');
transcriptDiv.className = 'full-transcript';
const header = document.createElement('div');
header.className = 'transcript-header';
const title = document.createElement('span');
title.textContent = 'Full Transcript';
const closeBtn = document.createElement('span');
closeBtn.className = 'transcript-close';
closeBtn.textContent = '×';
closeBtn.title = 'Close transcript';
closeBtn.onclick = () => {
transcriptDiv.remove();
button.textContent = 'View Full Transcript';
button.disabled = false;
};
header.appendChild(title);
header.appendChild(closeBtn);
transcriptDiv.appendChild(header);
const primaryParts = data.transcript_parts || [];
const secondaryParts = data.transcript_secondary_parts || [];
if (!primaryParts.length && !secondaryParts.length) {
const noTranscript = document.createElement('div');
noTranscript.className = 'muted';
noTranscript.textContent = 'No transcript available for this video.';
transcriptDiv.appendChild(noTranscript);
} else {
if (primaryParts.length) {
const primaryHeader = document.createElement('div');
primaryHeader.style.marginBottom = '8px';
primaryHeader.style.fontWeight = 'bold';
primaryHeader.style.fontSize = '12px';
primaryHeader.style.color = '#666';
primaryHeader.textContent = 'Primary Transcript';
transcriptDiv.appendChild(primaryHeader);
primaryParts.forEach(segment => {
transcriptDiv.appendChild(renderTranscriptSegment(segment, videoUrl));
});
}
if (secondaryParts.length) {
const secondaryHeader = document.createElement('div');
secondaryHeader.style.marginTop = primaryParts.length ? '16px' : '0';
secondaryHeader.style.marginBottom = '8px';
secondaryHeader.style.fontWeight = 'bold';
secondaryHeader.style.fontSize = '12px';
secondaryHeader.style.color = '#666';
secondaryHeader.textContent = 'Secondary Transcript';
transcriptDiv.appendChild(secondaryHeader);
secondaryParts.forEach(segment => {
transcriptDiv.appendChild(renderTranscriptSegment(segment, videoUrl));
});
}
}
containerElement.appendChild(transcriptDiv);
button.textContent = 'Hide Transcript';
button.disabled = false;
} catch (err) {
console.error('Error fetching transcript:', err);
button.textContent = 'View Full Transcript';
button.disabled = false;
alert('Failed to load transcript. Please try again.');
}
}
function renderMetrics(data) {
if (!metricsContent) return;
@@ -626,6 +758,16 @@ async function updateFrequencyChart(term, channels, queryMode) {
}
}
if (item.video_id) {
const transcriptBtn = document.createElement("button");
transcriptBtn.className = "transcript-toggle";
transcriptBtn.textContent = "View Full Transcript";
transcriptBtn.onclick = () => {
fetchAndDisplayTranscript(item.video_id, item.url, el, transcriptBtn);
};
el.appendChild(transcriptBtn);
}
resultsDiv.appendChild(el);
});