Add full transcript viewer with clickable timestamps

This commit is contained in:
knight 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);
});

View File

@ -223,3 +223,94 @@ mark {
padding: 2px 8px;
font-size: 12px;
}
.transcript-toggle {
margin-top: 8px;
padding: 6px 12px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
color: #0366d6;
transition: background 0.2s;
}
.transcript-toggle:hover {
background: #e8e8e8;
}
.transcript-toggle:disabled {
cursor: not-allowed;
color: #999;
}
.full-transcript {
margin-top: 12px;
padding: 12px;
background: #fafafa;
border: 1px solid #e1e1e1;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
}
.transcript-segment {
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ececec;
}
.transcript-segment:last-child {
border-bottom: none;
margin-bottom: 0;
}
.timestamp-link {
display: inline-block;
color: #0366d6;
text-decoration: none;
font-weight: bold;
font-size: 11px;
font-family: monospace;
margin-right: 8px;
padding: 2px 6px;
background: #e8f4ff;
border-radius: 3px;
transition: background 0.2s;
}
.timestamp-link:hover {
background: #cce5ff;
text-decoration: underline;
}
.transcript-text {
color: #333;
line-height: 1.5;
}
.transcript-header {
font-weight: bold;
margin-bottom: 8px;
color: #444;
display: flex;
align-items: center;
justify-content: space-between;
}
.transcript-close {
cursor: pointer;
color: #666;
font-size: 18px;
padding: 0 4px;
}
.transcript-close:hover {
color: #000;
}
.loading-text {
color: #666;
font-style: italic;
}