lukawskikacper's picture
Fix global search
0ad6b1b
// Video page functionality
document.addEventListener('DOMContentLoaded', () => {
const playerElement = document.getElementById('youtube-player');
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
const transcriptContainer = document.getElementById('transcript-container');
const loadingIndicator = document.getElementById('loading');
const toggleTranscriptButton = document.getElementById('toggle-transcript');
let transcriptSegments = [];
let ytPlayer = null;
let isProcessingUrl = false;
// Check if there's a search query or timestamp in the URL
const urlParams = new URLSearchParams(window.location.search);
const searchQuery = urlParams.get('q');
const processingUrl = urlParams.get('processing');
const startTime = urlParams.get('t');
// Format time to display as HH:MM:SS
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
}
// Handle error display
function handleError(error) {
console.error(error);
return `<div class="alert alert-error">Error: ${error.message}</div>`;
}
// Initialize YouTube iframe API
function initYouTubePlayer() {
// Get the existing iframe
const iframeId = playerElement.getAttribute('id');
// Load the YouTube iframe API if it's not already loaded
if (!window.YT) {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = function() {
createYouTubePlayer(iframeId);
};
} else {
createYouTubePlayer(iframeId);
}
}
// Create YouTube player object
function createYouTubePlayer(iframeId) {
ytPlayer = new YT.Player(iframeId, {
events: {
'onReady': onPlayerReady
}
});
}
// When player is ready
function onPlayerReady(event) {
console.log('Player ready');
// Seeking will be handled by the dedicated timestamp handler code at the bottom
}
// Load transcript segments
function loadTranscript() {
transcriptContainer.innerHTML = '<div class="flex justify-center my-4"><span class="loading loading-spinner loading-md"></span><span class="ml-2">Loading transcript...</span></div>';
// Check if video ID is valid before making API call
if (!videoId || videoId === 'undefined' || videoId === 'null') {
transcriptContainer.innerHTML = `
<div class="alert alert-error">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>Invalid video ID. Please return to the home page and select a valid video.</span>
</div>
</div>
`;
return;
}
fetch(`/api/video/segments/${videoId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to load transcript: ' + response.status);
}
return response.json();
})
.then(segments => {
transcriptSegments = segments;
if (!segments || segments.length === 0) {
transcriptContainer.innerHTML = `
<div class="alert alert-info">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>No transcript available for this video. Try processing the video first from the home page.</span>
</div>
</div>
`;
} else {
displayTranscript(segments);
// If we have a timestamp in the URL, highlight the relevant segment
if (startTime) {
const timeInSeconds = parseFloat(startTime);
if (!isNaN(timeInSeconds)) {
// Find segment containing this time
setTimeout(() => {
highlightSegment(timeInSeconds);
}, 500); // Short delay to ensure segments are rendered
}
}
}
})
.catch(error => {
console.error('Error loading transcript:', error);
transcriptContainer.innerHTML = `
<div class="alert alert-error">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>Error loading transcript: ${error.message}</span>
</div>
</div>
<p class="mt-4">This may happen if:</p>
<ul class="list-disc ml-8 mt-2">
<li>The video hasn't been processed yet</li>
<li>The video ID is incorrect</li>
<li>The server is experiencing issues</li>
</ul>
<p class="mt-4">Try processing this video from the home page first.</p>
`;
});
}
// Display transcript segments
function displayTranscript(segments) {
const html = segments.map((segment, index) => {
const formattedTime = formatTime(segment.start);
return `
<div class="transcript-segment" data-start="${segment.start}" data-end="${segment.end}" data-index="${index}">
<span class="timestamp">${formattedTime}</span>
<span class="segment-text">${segment.text}</span>
</div>
`;
}).join('');
transcriptContainer.innerHTML = html;
// Add click handlers to segments
document.querySelectorAll('.transcript-segment').forEach(segment => {
segment.addEventListener('click', () => {
const startTime = parseFloat(segment.dataset.start);
seekToTime(startTime);
});
});
}
// Seek to specific time in the video
function seekToTime(seconds) {
console.log('Seeking to time:', seconds);
if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
try {
// Ensure seconds is a number
seconds = parseFloat(seconds);
if (isNaN(seconds)) {
console.error('Invalid seconds value:', seconds);
return;
}
// Seek to time
ytPlayer.seekTo(seconds, true);
// Try to play the video (may be blocked by browser autoplay policies)
try {
ytPlayer.playVideo();
} catch (e) {
console.warn('Could not autoplay video:', e);
}
// Highlight the current segment
setTimeout(() => {
highlightSegment(seconds);
}, 300); // Short delay to ensure seek completes first
} catch (error) {
console.error('Error seeking to time:', error);
}
} else {
console.error('YouTube player is not ready yet or seekTo method is not available');
// Queue the seek operation for when the player becomes available
console.log('Queueing seek operation for later...');
setTimeout(() => {
if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
console.log('Player now ready, executing queued seek');
seekToTime(seconds);
}
}, 1000); // Try again in 1 second
}
}
// Highlight segment containing the current time
function highlightSegment(time) {
// Remove highlight from all segments
document.querySelectorAll('.transcript-segment').forEach(segment => {
segment.classList.remove('highlight');
});
// Wait until segments are available in the DOM
if (document.querySelectorAll('.transcript-segment').length === 0) {
console.log('No transcript segments found, waiting...');
// Retry after a short delay to allow transcript to load
setTimeout(() => highlightSegment(time), 500);
return;
}
// Find the segment containing current time
// Need to find by approximate match since floating point exact matches may not work
const segments = document.querySelectorAll('.transcript-segment');
let currentSegment = null;
for (const segment of segments) {
const start = parseFloat(segment.dataset.start);
const end = parseFloat(segment.dataset.end);
if (time >= start && time <= end) {
currentSegment = segment;
break;
}
}
// If exact time match not found, find the closest segment by time
if (!currentSegment && segments.length > 0) {
// First try exact match
const exactMatch = document.querySelector(`.transcript-segment[data-start="${time}"]`);
if (exactMatch) {
currentSegment = exactMatch;
} else {
// Find closest segment
let closestSegment = segments[0];
let closestDistance = Math.abs(parseFloat(closestSegment.dataset.start) - time);
segments.forEach(segment => {
const distance = Math.abs(parseFloat(segment.dataset.start) - time);
if (distance < closestDistance) {
closestDistance = distance;
closestSegment = segment;
}
});
currentSegment = closestSegment;
}
}
if (currentSegment) {
currentSegment.classList.add('highlight');
// Ensure the segment is visible in the transcript container
currentSegment.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// Search functionality
searchButton.addEventListener('click', performSearch);
searchInput.addEventListener('keypress', e => {
if (e.key === 'Enter') performSearch();
});
function performSearch() {
const query = searchInput.value.trim();
if (!query) {
transcriptContainer.innerHTML = '<div class="alert alert-warning">Please enter a search query</div>';
return;
}
// Validate video ID before searching
if (!videoId || videoId === 'undefined' || videoId === 'null') {
transcriptContainer.innerHTML = `
<div class="alert alert-error">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>Invalid video ID. Please return to the home page and select a valid video.</span>
</div>
</div>
`;
return;
}
// Show loading indicator
loadingIndicator.classList.remove('hidden');
// Send search request
fetch(`/api/video/search?query=${encodeURIComponent(query)}&video_id=${videoId}`)
.then(response => {
if (!response.ok) {
throw new Error('Search failed');
}
return response.json();
})
.then(results => {
// Hide loading indicator
loadingIndicator.classList.add('hidden');
if (results.length === 0) {
// Show "no results" message in transcript container
transcriptContainer.innerHTML = `
<div role="alert" class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>No results found for "${query}". <a href="#" id="reset-search" class="link link-primary">Show all transcript</a></span>
</div>`;
// Add click handler to reset search link
document.getElementById('reset-search').addEventListener('click', (e) => {
e.preventDefault();
resetTranscriptFilter();
displayTranscript(transcriptSegments);
});
return;
}
// Display search results as filtered transcript
filterTranscript(results);
// Add a header with search info and reset option
const searchInfoHeader = document.createElement('div');
searchInfoHeader.className = 'mb-4 flex justify-between items-center';
searchInfoHeader.innerHTML = `
<div class="badge badge-accent">${results.length} results for "${query}"</div>
<a href="#" id="reset-search" class="link link-primary text-sm">Show all transcript</a>
`;
// Insert the header before transcript segments
transcriptContainer.insertBefore(searchInfoHeader, transcriptContainer.firstChild);
// Add click handler to reset search link
document.getElementById('reset-search').addEventListener('click', (e) => {
e.preventDefault();
resetTranscriptFilter();
displayTranscript(transcriptSegments);
});
})
.catch(error => {
// Hide loading indicator
loadingIndicator.classList.add('hidden');
// Show error
transcriptContainer.innerHTML = handleError(error);
});
}
// Filter transcript to show only matching segments
function filterTranscript(results) {
// Create a highlighted version of the transcript with only matching segments
const html = results.map(result => {
const segment = result.segment;
const formattedTime = formatTime(segment.start);
const score = (result.score * 100).toFixed(0);
const index = transcriptSegments.findIndex(s => s.segment_id === segment.segment_id);
return `
<div class="transcript-segment search-result" data-start="${segment.start}" data-end="${segment.end}" data-index="${index}">
<div class="flex justify-between items-center">
<span class="timestamp">${formattedTime}</span>
<div class="badge badge-primary">${score}% match</div>
</div>
<span class="segment-text mt-1">${segment.text}</span>
</div>
`;
}).join('');
// Replace transcript with filtered results
transcriptContainer.innerHTML = html;
// Add click handlers to segments
document.querySelectorAll('.transcript-segment').forEach(segment => {
segment.addEventListener('click', () => {
const startTime = parseFloat(segment.dataset.start);
seekToTime(startTime);
});
});
}
// Transcript is always visible - toggle functionality removed
// Reset transcript filter to show all segments
function resetTranscriptFilter() {
searchInput.value = '';
}
// Show processing indicator if URL was just processed
function showProcessingIndicator() {
if (processingUrl === 'true') {
isProcessingUrl = true;
transcriptContainer.innerHTML = `
<div class="flex items-center justify-center my-4">
<span class="loading loading-spinner loading-md text-primary"></span>
<span class="ml-2">Processing video from URL... This may take a few moments</span>
</div>
`;
// Check for segments every second
const processingInterval = setInterval(() => {
// Validate video ID before making API call
if (!videoId || videoId === 'undefined' || videoId === 'null') {
clearInterval(processingInterval);
transcriptContainer.innerHTML = `
<div class="alert alert-error">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>Invalid video ID. Please return to the home page and select a valid video.</span>
</div>
</div>
`;
return;
}
fetch(`/api/video/segments/${videoId}`)
.then(response => {
if (!response.ok) {
return null;
}
return response.json();
})
.then(segments => {
if (segments && segments.length > 0) {
clearInterval(processingInterval);
isProcessingUrl = false;
loadTranscript();
}
})
.catch(error => {
console.error('Error checking segments:', error);
});
}, 2000);
// Set timeout to stop checking after 2 minutes
setTimeout(() => {
clearInterval(processingInterval);
if (isProcessingUrl) {
transcriptContainer.innerHTML = `
<div class="alert alert-warning">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>Processing is taking longer than expected. Refresh the page to check progress.</span>
</div>
</div>
`;
isProcessingUrl = false;
}
}, 120000);
return true;
}
return false;
}
// Initialize
initYouTubePlayer();
// Show processing indicator or load transcript
if (!showProcessingIndicator()) {
loadTranscript();
}
// If there's a search query in the URL, apply it after transcript loads
if (searchQuery) {
const checkTranscriptInterval = setInterval(() => {
if (transcriptSegments.length > 0) {
clearInterval(checkTranscriptInterval);
// Set the search input value and trigger search
searchInput.value = searchQuery;
performSearch();
}
}, 500);
// Set timeout to stop checking after 10 seconds
setTimeout(() => clearInterval(checkTranscriptInterval), 10000);
}
// If there's a timestamp in the URL, ensure it will be seeked to after transcript loads
if (startTime) { // Handle timestamp regardless of search query
let timeInSeconds = parseFloat(startTime);
// If parsing fails, try to parse as HH:MM:SS format
if (isNaN(timeInSeconds) && typeof startTime === 'string') {
// Try to parse HH:MM:SS or MM:SS format
const timeParts = startTime.split(':').map(part => parseInt(part, 10));
if (timeParts.length === 3) {
// HH:MM:SS format
timeInSeconds = timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2];
} else if (timeParts.length === 2) {
// MM:SS format
timeInSeconds = timeParts[0] * 60 + timeParts[1];
}
}
if (!isNaN(timeInSeconds)) {
console.log('Will seek to timestamp:', timeInSeconds, 'seconds');
// Try immediately if player is ready
if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
console.log('YouTube player is ready, seeking now');
seekToTime(timeInSeconds);
// Also try to play the video as a fallback if autoplay doesn't work
try {
ytPlayer.playVideo();
// Unmute the video after a short delay
setTimeout(() => {
try {
ytPlayer.unMute();
// Set volume to a reasonable level
ytPlayer.setVolume(80);
} catch (e) {
console.warn('Could not unmute video:', e);
}
}, 1000);
} catch (e) {
console.warn('Could not autoplay video:', e);
}
}
// Also set up a backup interval to ensure we seek once everything is ready
const checkReadyInterval = setInterval(() => {
if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
if (transcriptSegments.length > 0) {
clearInterval(checkReadyInterval);
console.log('Everything loaded, seeking to timestamp:', timeInSeconds);
seekToTime(timeInSeconds);
// Also try to play the video
try {
ytPlayer.playVideo();
// Unmute the video after a short delay
setTimeout(() => {
try {
ytPlayer.unMute();
ytPlayer.setVolume(80);
} catch (e) {
console.warn('Could not unmute video after delay:', e);
}
}, 1000);
} catch (e) {
console.warn('Could not autoplay video after delay:', e);
}
} else {
console.log('Waiting for transcript segments to load...');
}
} else {
console.log('Waiting for YouTube player to be ready...');
}
}, 500);
// Set timeout to stop checking after 10 seconds
setTimeout(() => {
clearInterval(checkReadyInterval);
// Final attempt
if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
console.log('Final attempt to seek to:', timeInSeconds);
seekToTime(timeInSeconds);
// One final attempt to play
try {
ytPlayer.playVideo();
// Unmute the video after a short delay
setTimeout(() => {
try {
ytPlayer.unMute();
ytPlayer.setVolume(80);
} catch (e) {
console.warn('Could not unmute video on final attempt:', e);
}
}, 1000);
} catch (e) {
console.warn('Could not autoplay video on final attempt:', e);
}
}
}, 10000);
} else {
console.warn('Could not parse timestamp from URL:', startTime);
}
}
});