diff --git a/static/app.js b/static/app.js index 3324fb4..22e259e 100644 --- a/static/app.js +++ b/static/app.js @@ -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); }); diff --git a/static/style.css b/static/style.css index e61aa61..1d7ac30 100644 --- a/static/style.css +++ b/static/style.css @@ -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; +}