lukawskikacper's picture
Fix global search
0ad6b1b
// Index page functionality
document.addEventListener('DOMContentLoaded', () => {
const youtubeUrlInput = document.getElementById('youtube-url');
const processButton = document.getElementById('process-button');
const processStatus = document.getElementById('process-status');
const processingIndicator = document.getElementById('processing');
const recentlyProcessedCard = document.getElementById('recently-processed');
const videoListContainer = document.getElementById('video-list');
// Check for search parameter in URL
const urlParams = new URLSearchParams(window.location.search);
const searchQuery = urlParams.get('search');
if (searchQuery) {
// Display search results
displaySearchResults(searchQuery);
}
// Example video buttons
const exampleButtons = document.querySelectorAll('.example-video');
// Process button click handler
processButton.addEventListener('click', () => processVideo());
// Enter key in input field
youtubeUrlInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') processVideo();
});
// Example video buttons
exampleButtons.forEach(button => {
button.addEventListener('click', () => {
youtubeUrlInput.value = button.dataset.url;
processVideo();
});
});
// Process video function
function processVideo() {
const youtubeUrl = youtubeUrlInput.value.trim();
if (!youtubeUrl) {
processStatus.innerHTML = '<div class="alert alert-warning">Please enter a YouTube URL</div>';
return;
}
// Extract video ID
const videoId = extractVideoId(youtubeUrl);
if (!videoId) {
processStatus.innerHTML = '<div class="alert alert-error">Invalid YouTube URL</div>';
return;
}
// Show loading indicator with spinner and text
processStatus.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... This may take a few moments</span>
</div>
`;
// Set a timeout to handle overly long processing
const timeoutId = setTimeout(() => {
processStatus.innerHTML = `
<div class="alert alert-warning">
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Processing is taking longer than expected. Please wait...</span>
</div>
`;
}, 20000); // 20 seconds
// Send request to process the video
fetch('/api/video/process', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: youtubeUrl })
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to process video');
}
return response.json();
})
.then(data => {
// Clear timeout for long-running process
clearTimeout(timeoutId);
// Extract video ID from response (handles both old and new API formats)
const videoId = data.video ? data.video.video_id : data.video_id;
const isNewlyProcessed = data.newly_processed !== undefined ? data.newly_processed : true;
if (!videoId) {
throw new Error('Invalid response: Missing video ID');
}
// Get video title (for display)
const videoTitle = data.video ? data.video.title : (data.title || `Video ${videoId}`);
// Log for debugging
console.log('Process response:', {videoId, isNewlyProcessed, data});
// Show success message
processStatus.innerHTML = `
<div role="alert" class="alert alert-success">
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>${isNewlyProcessed ? 'Video processed successfully!' : 'Video was already processed!'}</span>
<div>
<a href="/video/${videoId}" class="btn btn-sm btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Open Video
</a>
</div>
</div>
`;
// Update recent videos lists
displayRecentVideos();
loadFooterRecentVideos(); // Update footer videos as well
})
.catch(error => {
// Clear timeout for long-running process
clearTimeout(timeoutId);
// Show error message
console.error('Process error:', error);
processStatus.innerHTML = handleError(error);
});
}
// Display recently processed videos
function displayRecentVideos() {
// Show loading state
recentlyProcessedCard.classList.remove('hidden');
videoListContainer.innerHTML = `
<div class="flex justify-center items-center p-4">
<span class="loading loading-spinner loading-md"></span>
<span class="ml-2">Loading recent videos...</span>
</div>
`;
const carouselPrev = document.getElementById('carousel-prev');
const carouselNext = document.getElementById('carousel-next');
// Fetch recent videos from server
fetch('/api/video/recent?limit=5')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch recent videos');
}
return response.json();
})
.then(videos => {
if (videos && videos.length > 0) {
// Limit to 5 videos
const limitedVideos = videos.slice(0, 5);
// Generate carousel items
const carouselItems = limitedVideos.map((video, index) => {
// Format date if available
let formattedDate = '';
if (video.created_at) {
const date = new Date(video.created_at * 1000); // Convert Unix timestamp to milliseconds
formattedDate = date.toLocaleDateString();
}
// Use title or default
const videoTitle = video.title || `Video ${video.video_id}`;
return `
<div id="video-${index}" class="carousel-item">
<a href="/video/${video.video_id}" class="card bg-base-100 shadow-sm hover:shadow-md transition-all w-64 md:w-72 flex flex-col">
<figure class="w-full h-36 overflow-hidden">
<img src="https://img.youtube.com/vi/${video.video_id}/mqdefault.jpg" alt="Thumbnail" class="w-full h-full object-cover">
</figure>
<div class="card-body p-3">
<h3 class="card-title text-sm line-clamp-2">${videoTitle}</h3>
<div class="text-xs opacity-70">${formattedDate}</div>
</div>
</a>
</div>
`;
}).join('');
// Add carousel items to container
videoListContainer.innerHTML = carouselItems;
// Setup navigation arrows
if (limitedVideos.length > 1) {
// Show arrows for multiple videos
let currentIndex = 0;
const maxIndex = limitedVideos.length - 1;
// Show navigation arrows
carouselPrev.classList.remove('hidden');
carouselNext.classList.remove('hidden');
// Left button is disabled by default (we're at the start)
const prevButton = carouselPrev.querySelector('button');
const nextButton = carouselNext.querySelector('button');
prevButton.classList.add('btn-disabled');
// Functions to update button states
const updateButtonStates = () => {
if (currentIndex === 0) {
prevButton.classList.add('btn-disabled');
} else {
prevButton.classList.remove('btn-disabled');
}
if (currentIndex === maxIndex) {
nextButton.classList.add('btn-disabled');
} else {
nextButton.classList.remove('btn-disabled');
}
};
// Setup navigation buttons
prevButton.addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--;
document.getElementById(`video-${currentIndex}`).scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
updateButtonStates();
}
});
nextButton.addEventListener('click', () => {
if (currentIndex < maxIndex) {
currentIndex++;
document.getElementById(`video-${currentIndex}`).scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
updateButtonStates();
}
});
} else {
// Hide arrows for single video
carouselPrev.classList.add('hidden');
carouselNext.classList.add('hidden');
}
} else {
recentlyProcessedCard.classList.add('hidden');
carouselPrev.classList.add('hidden');
carouselNext.classList.add('hidden');
}
})
.catch(error => {
console.error('Error fetching recent videos:', error);
videoListContainer.innerHTML = `
<div class="alert alert-error">
<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>Failed to load recent videos</span>
</div>
`;
carouselPrev.classList.add('hidden');
carouselNext.classList.add('hidden');
});
}
// Display recent videos on page load
displayRecentVideos();
// Function to display search results
function displaySearchResults(query) {
// Try to use the modal if available
const searchModal = document.getElementById('search-results-modal');
const searchResultsContainer = document.getElementById('search-results-container');
if (searchModal && searchResultsContainer) {
// Update modal title with search term
const modalTitle = searchModal.querySelector('h3');
if (modalTitle) {
modalTitle.textContent = `Search Results for "${query}"`;
}
// Show the modal
searchModal.showModal();
// Clear previous results and show loading state
searchResultsContainer.innerHTML = `
<div class="flex justify-center items-center p-4">
<span class="loading loading-spinner loading-md"></span>
<span class="ml-2">Searching...</span>
</div>
`;
// Fetch and display results in the modal
fetchAndDisplayResults(query, searchResultsContainer);
} else {
// Fallback to the old implementation if modal is not available
// Show a search results card with loading indicator
processStatus.innerHTML = `
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<h2 class="card-title">Search Results for "${query}"</h2>
<div class="mt-4">
<div class="flex justify-center items-center p-4">
<span class="loading loading-spinner loading-md"></span>
<span class="ml-2">Searching...</span>
</div>
</div>
</div>
</div>
`;
// Fetch and display results in the processStatus element
fetchAndDisplayResults(query, null, processStatus);
}
}
// Helper function to fetch and display search results
function fetchAndDisplayResults(query, container, fallbackContainer) {
// Fetch search results from API
fetch(`/api/video/search?query=${encodeURIComponent(query)}&limit=10`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to perform search');
}
return response.json();
})
.then(results => {
if (results && results.length > 0) {
// Group results by video
const videoGroups = {};
// First pass: collect all video IDs that need titles
const videoIds = [];
results.forEach(result => {
const videoId = result.segment.video_id;
if (!videoGroups[videoId]) {
videoGroups[videoId] = {
videoId: videoId,
title: `Video ${videoId}`, // Default title, will be updated
segments: []
};
// Add to list of IDs to fetch titles for
videoIds.push(videoId);
}
videoGroups[videoId].segments.push(result);
});
// If we have video IDs, fetch their proper titles
const videoPromises = videoIds.map(videoId => {
return fetch(`/api/video/info/${videoId}`)
.then(response => response.ok ? response.json() : null)
.then(videoInfo => {
if (videoInfo && videoInfo.title && videoGroups[videoId]) {
videoGroups[videoId].title = videoInfo.title;
}
})
.catch(error => console.error(`Error fetching video info for ${videoId}:`, error));
});
// After all video titles are fetched, continue with rendering
Promise.all(videoPromises).then(() => {
// Generate results HTML
const resultsHTML = Object.values(videoGroups).map(group => {
const segmentsHTML = group.segments.map(result => {
return `
<a href="/video/${group.videoId}?t=${Math.floor(result.segment.start)}"
class="block p-2 hover:bg-base-200 rounded-md transition-colors duration-150 mb-2">
<div class="flex items-start">
<div class="text-primary font-mono mr-2">${formatTime(result.segment.start)}</div>
<div class="flex-grow">
<p class="truncate-3-lines">${result.segment.text}</p>
<div class="text-xs opacity-70 mt-1">Score: ${(result.score * 100).toFixed(1)}%</div>
</div>
</div>
</a>
`;
}).join('');
return `
<div class="mb-6">
<div class="flex items-center mb-2">
<img src="https://img.youtube.com/vi/${group.videoId}/default.jpg" alt="Thumbnail"
class="w-12 h-12 rounded-md mr-2">
<h3 class="text-lg font-bold">
<a href="/video/${group.videoId}" class="link">${group.title}</a>
</h3>
</div>
<div class="pl-4 border-l-2 border-primary">
${segmentsHTML}
</div>
</div>
`;
}).join('');
// Display results in the appropriate container
if (container) {
container.innerHTML = resultsHTML;
} else if (fallbackContainer) {
fallbackContainer.innerHTML = `
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<h2 class="card-title">Search Results for "${query}"</h2>
<div class="mt-4">
${resultsHTML}
</div>
</div>
</div>
`;
}
});
} else {
// No results found - display immediately, no need to wait for titles
const noResultsHTML = `
<div class="alert alert-info">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>No results found. Try a different search term or process more videos.</span>
</div>
`;
if (container) {
container.innerHTML = noResultsHTML;
} else if (fallbackContainer) {
fallbackContainer.innerHTML = `
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<h2 class="card-title">Search Results for "${query}"</h2>
<div class="mt-4">
${noResultsHTML}
</div>
</div>
</div>
`;
}
}
})
.catch(error => {
console.error('Search error:', error);
const errorHTML = `
<div class="alert alert-error">
<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 performing search: ${error.message}</span>
</div>
`;
if (container) {
container.innerHTML = errorHTML;
} else if (fallbackContainer) {
fallbackContainer.innerHTML = `
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<h2 class="card-title">Search Results for "${query}"</h2>
<div class="mt-4">
${errorHTML}
</div>
</div>
</div>
`;
}
});
}
});