/** * Batch result page JavaScript - Pagination, search, and detail view */ // State let currentPage = 1; let pageSize = 25; let totalResults = 0; let totalPages = 1; let searchQuery = ''; let jobInfo = null; // DOM Elements const loadingState = document.getElementById('loading-state'); const loadingText = document.getElementById('loading-text'); const resultsContainer = document.getElementById('results-container'); const errorState = document.getElementById('error-state'); const errorMessageEl = document.getElementById('error-message'); const jobTitleEl = document.getElementById('job-title'); const resultsTableBody = document.getElementById('results-table-body'); const searchInput = document.getElementById('search-results'); // Pagination elements const pageStart = document.getElementById('page-start'); const pageEnd = document.getElementById('page-end'); const totalResultsEl = document.getElementById('total-results'); const pageButtons = document.getElementById('page-buttons'); // Export links const exportCsv = document.getElementById('export-csv'); // Polling configuration const POLL_INTERVAL = 2000; // 2 seconds const MAX_POLLS = 120; // 4 minutes max let pollCount = 0; /** * Initialize the page */ document.addEventListener('DOMContentLoaded', () => { if (typeof jobId === 'undefined' || !jobId) { showError('No job ID provided'); return; } // Set export links exportCsv.href = `/api/export/${jobId}/csv`; // Event listeners if (searchInput) { let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { searchQuery = e.target.value.trim(); currentPage = 1; loadResults(); }, 300); }); } // Start loading fetchJobInfo(); }); /** * Fetch job information first */ async function fetchJobInfo() { try { const response = await fetch(`/api/result/${jobId}`); if (response.status === 404) { showError('Job not found. It may have expired.'); return; } if (!response.ok) { throw new Error('Failed to fetch job info'); } const data = await response.json(); jobInfo = data; // Check status if (data.status === 'finished' || data.status === 'completed') { // Job is done, load results updateJobHeader(); loadResults(); } else if (data.status === 'failed') { showError(data.error || 'Batch processing failed'); } else if (data.status === 'running' || data.status === 'queued') { // Still processing loadingText.textContent = `Processing batch... (${pollCount * 2}s)`; pollCount++; if (pollCount < MAX_POLLS) { setTimeout(fetchJobInfo, POLL_INTERVAL); } else { showError('Request timed out. Please try again later.'); } } else { // Try to show results anyway updateJobHeader(); loadResults(); } } catch (error) { console.error('Error fetching job info:', error); showError('Failed to load job information'); } } /** * Update job header with title */ function updateJobHeader() { if (jobInfo && jobInfo.job_title) { jobTitleEl.textContent = jobInfo.job_title; } } /** * Load paginated results */ async function loadResults() { try { const params = new URLSearchParams({ page: currentPage, page_size: pageSize }); if (searchQuery) { params.append('search', searchQuery); } const response = await fetch(`/api/batch/${jobId}/results?${params.toString()}`); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to load results'); } const data = await response.json(); // Update table totalResults = data.total; totalPages = data.total_pages; renderResults(data.results); renderPagination(); // Show results container loadingState.classList.add('hidden'); resultsContainer.classList.remove('hidden'); } catch (error) { console.error('Error loading results:', error); showError(error.message); } } /** * Render results table */ function renderResults(results) { resultsTableBody.innerHTML = ''; for (const result of results) { const row = document.createElement('tr'); row.className = 'hover:bg-gray-50 cursor-pointer'; row.onclick = () => { window.location.href = `/batch/${jobId}/sequence/${result.index}`; }; const statusBadge = getStatusBadge(result.status); const psiDisplay = result.status === 'success' && result.psi !== null ? result.psi.toFixed(3) : '-'; const psiClass = result.status === 'success' ? getPsiColorClass(result.psi) : 'text-gray-400'; // Truncate sequence const truncatedSeq = result.sequence.length > 30 ? result.sequence.substring(0, 30) + '...' : result.sequence; row.innerHTML = ` ${escapeHtml(result.name)} ${escapeHtml(truncatedSeq)} ${psiDisplay} ${statusBadge} `; resultsTableBody.appendChild(row); } } /** * Get status badge HTML */ function getStatusBadge(status) { if (status === 'success') { return 'Success'; } else if (status === 'invalid') { return 'Invalid'; } else { return `${status}`; } } /** * Get PSI color class */ function getPsiColorClass(psi) { if (psi >= 0.8) return 'text-green-600'; if (psi >= 0.3) return 'text-yellow-600'; return 'text-red-600'; } /** * Render pagination controls */ function renderPagination() { const start = (currentPage - 1) * pageSize + 1; const end = Math.min(currentPage * pageSize, totalResults); pageStart.textContent = totalResults > 0 ? start : 0; pageEnd.textContent = end; totalResultsEl.textContent = totalResults; // Clear existing buttons pageButtons.innerHTML = ''; if (totalPages <= 1) return; // Previous button const prevBtn = document.createElement('button'); prevBtn.className = `relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium ${currentPage === 1 ? 'text-gray-300 cursor-not-allowed' : 'text-gray-500 hover:bg-gray-50'}`; prevBtn.innerHTML = ''; prevBtn.disabled = currentPage === 1; prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; loadResults(); } }); pageButtons.appendChild(prevBtn); // Page numbers const maxButtons = 5; let startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2)); let endPage = Math.min(totalPages, startPage + maxButtons - 1); if (endPage - startPage + 1 < maxButtons) { startPage = Math.max(1, endPage - maxButtons + 1); } for (let i = startPage; i <= endPage; i++) { const btn = document.createElement('button'); btn.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium ${i === currentPage ? 'bg-primary-50 border-primary-500 text-primary-600 z-10' : 'bg-white text-gray-500 hover:bg-gray-50'}`; btn.textContent = i; btn.addEventListener('click', () => { currentPage = i; loadResults(); }); pageButtons.appendChild(btn); } // Next button const nextBtn = document.createElement('button'); nextBtn.className = `relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium ${currentPage === totalPages ? 'text-gray-300 cursor-not-allowed' : 'text-gray-500 hover:bg-gray-50'}`; nextBtn.innerHTML = ''; nextBtn.disabled = currentPage === totalPages; nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; loadResults(); } }); pageButtons.appendChild(nextBtn); } /** * Show error state */ function showError(message) { loadingState.classList.add('hidden'); resultsContainer.classList.add('hidden'); errorState.classList.remove('hidden'); errorMessageEl.textContent = message; } /** * Copy result link to clipboard */ function copyLink() { const url = window.location.href; navigator.clipboard.writeText(url).then(() => { alert('Link copied to clipboard!'); }).catch(err => { console.error('Failed to copy:', err); prompt('Copy this link:', url); }); } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ============================================================================ // Inline Name Editing // ============================================================================ function startEditName(element, index, currentName) { if (element.querySelector('input')) return; element.innerHTML = ` `; const input = element.querySelector('input'); input.focus(); input.select(); } function handleEditKeydown(event, input, index, originalName) { event.stopPropagation(); if (event.key === 'Enter') { event.preventDefault(); saveName(input, index, originalName); } else if (event.key === 'Escape') { event.preventDefault(); cancelEdit(input, index, originalName); } } function handleEditBlur(input, index, originalName) { const newName = input.value.trim(); if (!newName || newName === originalName) { cancelEdit(input, index, originalName); } else { saveName(input, index, originalName); } } async function saveName(input, index, originalName) { const newName = input.value.trim(); const parent = input.parentElement; if (!newName || newName === originalName) { cancelEdit(input, index, originalName); return; } input.disabled = true; input.classList.add('opacity-50'); try { const response = await fetch(`/api/batch/${jobId}/sequence/${index}/name`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to save'); } restoreNameDisplay(parent, index, newName); parent.classList.add('bg-green-100'); setTimeout(() => parent.classList.remove('bg-green-100'), 1000); } catch (error) { console.error('Error saving name:', error); alert('Failed to save name: ' + error.message); input.disabled = false; input.classList.remove('opacity-50'); input.focus(); } } function cancelEdit(input, index, originalName) { restoreNameDisplay(input.parentElement, index, originalName); } function restoreNameDisplay(parent, index, name) { parent.innerHTML = ` ${escapeHtml(name)} `; } // Make functions available globally window.copyLink = copyLink; window.startEditName = startEditName; window.handleEditKeydown = handleEditKeydown; window.handleEditBlur = handleEditBlur;