Spaces:
Sleeping
Sleeping
sachin1801 commited on
Commit ·
78ccf41
1
Parent(s): 2fc42e1
refactor: replace batch result modal with full results page navigation
Browse files- Change row click to navigate to /batch/{job_id}/sequence/{index}
- Remove unused modal HTML from batch_result.html
- Remove modal-related JS functions (showDetail, closeDetailModal, interpretPsi, createForcePlot)
- Update cache-busting version to v=3
webapp/static/js/batch-result.js
CHANGED
|
@@ -20,12 +20,6 @@ const jobTitleEl = document.getElementById('job-title');
|
|
| 20 |
const resultsTableBody = document.getElementById('results-table-body');
|
| 21 |
const searchInput = document.getElementById('search-results');
|
| 22 |
|
| 23 |
-
// Stats elements
|
| 24 |
-
const statTotal = document.getElementById('stat-total');
|
| 25 |
-
const statSuccess = document.getElementById('stat-success');
|
| 26 |
-
const statInvalid = document.getElementById('stat-invalid');
|
| 27 |
-
const statAvgPsi = document.getElementById('stat-avg-psi');
|
| 28 |
-
|
| 29 |
// Pagination elements
|
| 30 |
const pageStart = document.getElementById('page-start');
|
| 31 |
const pageEnd = document.getElementById('page-end');
|
|
@@ -148,9 +142,6 @@ async function loadResults() {
|
|
| 148 |
|
| 149 |
const data = await response.json();
|
| 150 |
|
| 151 |
-
// Update stats
|
| 152 |
-
updateStats(data);
|
| 153 |
-
|
| 154 |
// Update table
|
| 155 |
totalResults = data.total;
|
| 156 |
totalPages = data.total_pages;
|
|
@@ -167,21 +158,6 @@ async function loadResults() {
|
|
| 167 |
}
|
| 168 |
}
|
| 169 |
|
| 170 |
-
/**
|
| 171 |
-
* Update stats display
|
| 172 |
-
*/
|
| 173 |
-
function updateStats(data) {
|
| 174 |
-
statTotal.textContent = data.total_sequences || 0;
|
| 175 |
-
statSuccess.textContent = data.successful_count || 0;
|
| 176 |
-
statInvalid.textContent = data.invalid_count || 0;
|
| 177 |
-
|
| 178 |
-
if (data.average_psi !== null && data.average_psi !== undefined) {
|
| 179 |
-
statAvgPsi.textContent = data.average_psi.toFixed(3);
|
| 180 |
-
} else {
|
| 181 |
-
statAvgPsi.textContent = '-';
|
| 182 |
-
}
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
/**
|
| 186 |
* Render results table
|
| 187 |
*/
|
|
@@ -191,7 +167,9 @@ function renderResults(results) {
|
|
| 191 |
for (const result of results) {
|
| 192 |
const row = document.createElement('tr');
|
| 193 |
row.className = 'hover:bg-gray-50 cursor-pointer';
|
| 194 |
-
row.onclick = () =>
|
|
|
|
|
|
|
| 195 |
|
| 196 |
const statusBadge = getStatusBadge(result.status);
|
| 197 |
const psiDisplay = result.status === 'success' && result.psi !== null
|
|
@@ -320,228 +298,6 @@ function renderPagination() {
|
|
| 320 |
pageButtons.appendChild(nextBtn);
|
| 321 |
}
|
| 322 |
|
| 323 |
-
/**
|
| 324 |
-
* Show sequence detail modal
|
| 325 |
-
*/
|
| 326 |
-
async function showDetail(index) {
|
| 327 |
-
const modal = document.getElementById('detail-modal');
|
| 328 |
-
const detailTitle = document.getElementById('detail-title');
|
| 329 |
-
const detailContent = document.getElementById('detail-content');
|
| 330 |
-
const detailLoading = document.getElementById('detail-loading');
|
| 331 |
-
|
| 332 |
-
// Show modal with loading
|
| 333 |
-
modal.classList.remove('hidden');
|
| 334 |
-
detailLoading.classList.remove('hidden');
|
| 335 |
-
detailContent.innerHTML = '';
|
| 336 |
-
detailContent.appendChild(detailLoading);
|
| 337 |
-
|
| 338 |
-
try {
|
| 339 |
-
const response = await fetch(`/api/batch/${jobId}/sequence/${index}`, {
|
| 340 |
-
cache: 'no-store'
|
| 341 |
-
});
|
| 342 |
-
|
| 343 |
-
if (!response.ok) {
|
| 344 |
-
const error = await response.json();
|
| 345 |
-
throw new Error(error.detail || 'Failed to load sequence details');
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
const data = await response.json();
|
| 349 |
-
detailLoading.classList.add('hidden');
|
| 350 |
-
|
| 351 |
-
// Update title
|
| 352 |
-
detailTitle.textContent = data.name || `Sequence ${index + 1}`;
|
| 353 |
-
|
| 354 |
-
// Build detail content
|
| 355 |
-
let html = '';
|
| 356 |
-
|
| 357 |
-
if (data.status === 'invalid') {
|
| 358 |
-
// Show invalid sequence info
|
| 359 |
-
html = `
|
| 360 |
-
<div class="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
|
| 361 |
-
<svg class="h-12 w-12 text-red-400 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 362 |
-
<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" />
|
| 363 |
-
</svg>
|
| 364 |
-
<h4 class="mt-4 text-lg font-medium text-red-800">Invalid Sequence</h4>
|
| 365 |
-
<p class="mt-2 text-red-700">${escapeHtml(data.validation_error || 'Sequence validation failed')}</p>
|
| 366 |
-
</div>
|
| 367 |
-
<div class="bg-gray-50 rounded-lg p-4">
|
| 368 |
-
<h4 class="text-sm font-medium text-gray-500 uppercase mb-2">Sequence</h4>
|
| 369 |
-
<p class="font-mono text-sm text-gray-900 break-all">${escapeHtml(data.sequence)}</p>
|
| 370 |
-
</div>
|
| 371 |
-
`;
|
| 372 |
-
} else {
|
| 373 |
-
// Show successful prediction details
|
| 374 |
-
const psi = data.psi;
|
| 375 |
-
const interp = interpretPsi(psi);
|
| 376 |
-
|
| 377 |
-
html = `
|
| 378 |
-
<!-- PSI Value -->
|
| 379 |
-
<div class="rounded-lg p-6 text-center ${interp.colorClass}">
|
| 380 |
-
<p class="text-sm font-medium text-gray-500 uppercase tracking-wide">Predicted PSI</p>
|
| 381 |
-
<p class="mt-2 text-4xl font-bold ${interp.textClass}">${psi.toFixed(3)}</p>
|
| 382 |
-
<p class="mt-2 text-sm ${interp.textClass}">${interp.text}: ${interp.description}</p>
|
| 383 |
-
</div>
|
| 384 |
-
|
| 385 |
-
<!-- Info Grid -->
|
| 386 |
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 387 |
-
<div class="bg-gray-50 rounded-lg p-4">
|
| 388 |
-
<h4 class="text-sm font-medium text-gray-500 uppercase mb-2">RNA Secondary Structure</h4>
|
| 389 |
-
<p class="font-mono text-sm text-gray-900 break-all">${escapeHtml(data.structure || 'N/A')}</p>
|
| 390 |
-
</div>
|
| 391 |
-
<div class="bg-gray-50 rounded-lg p-4">
|
| 392 |
-
<h4 class="text-sm font-medium text-gray-500 uppercase mb-2">Minimum Free Energy</h4>
|
| 393 |
-
<p class="text-2xl font-bold text-gray-900">${data.mfe ? data.mfe.toFixed(2) : 'N/A'} <span class="text-base font-normal text-gray-500">kcal/mol</span></p>
|
| 394 |
-
</div>
|
| 395 |
-
</div>
|
| 396 |
-
|
| 397 |
-
<!-- Sequence -->
|
| 398 |
-
<div class="bg-gray-50 rounded-lg p-4">
|
| 399 |
-
<h4 class="text-sm font-medium text-gray-500 uppercase mb-2">Sequence</h4>
|
| 400 |
-
<p class="font-mono text-sm text-gray-900 break-all">${escapeHtml(data.sequence)}</p>
|
| 401 |
-
</div>
|
| 402 |
-
|
| 403 |
-
<!-- Force Plot -->
|
| 404 |
-
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
| 405 |
-
<h4 class="text-sm font-medium text-gray-500 uppercase mb-4">
|
| 406 |
-
Position-wise Contribution to PSI
|
| 407 |
-
<span class="ml-2 text-gray-400 font-normal normal-case">
|
| 408 |
-
(green = promotes inclusion, red = promotes skipping)
|
| 409 |
-
</span>
|
| 410 |
-
</h4>
|
| 411 |
-
<div id="detail-force-plot" class="w-full" style="height: 350px;"></div>
|
| 412 |
-
</div>
|
| 413 |
-
`;
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
detailContent.innerHTML = html;
|
| 417 |
-
|
| 418 |
-
// Create force plot if we have data
|
| 419 |
-
if (data.status === 'success' && data.force_plot_data && data.force_plot_data.length > 0) {
|
| 420 |
-
setTimeout(() => createForcePlot('detail-force-plot', data.force_plot_data), 100);
|
| 421 |
-
}
|
| 422 |
-
|
| 423 |
-
} catch (error) {
|
| 424 |
-
console.error('Error loading detail:', error);
|
| 425 |
-
detailLoading.classList.add('hidden');
|
| 426 |
-
detailContent.innerHTML = `
|
| 427 |
-
<div class="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
|
| 428 |
-
<p class="text-red-700">${escapeHtml(error.message)}</p>
|
| 429 |
-
</div>
|
| 430 |
-
`;
|
| 431 |
-
}
|
| 432 |
-
}
|
| 433 |
-
|
| 434 |
-
/**
|
| 435 |
-
* Close detail modal
|
| 436 |
-
*/
|
| 437 |
-
function closeDetailModal() {
|
| 438 |
-
document.getElementById('detail-modal').classList.add('hidden');
|
| 439 |
-
}
|
| 440 |
-
|
| 441 |
-
// Close modal on escape key
|
| 442 |
-
document.addEventListener('keydown', (e) => {
|
| 443 |
-
if (e.key === 'Escape') {
|
| 444 |
-
closeDetailModal();
|
| 445 |
-
}
|
| 446 |
-
});
|
| 447 |
-
|
| 448 |
-
// Close modal on backdrop click
|
| 449 |
-
document.getElementById('detail-modal').addEventListener('click', (e) => {
|
| 450 |
-
if (e.target === e.currentTarget) {
|
| 451 |
-
closeDetailModal();
|
| 452 |
-
}
|
| 453 |
-
});
|
| 454 |
-
|
| 455 |
-
/**
|
| 456 |
-
* Interpret PSI value
|
| 457 |
-
*/
|
| 458 |
-
function interpretPsi(psi) {
|
| 459 |
-
if (psi >= 0.8) {
|
| 460 |
-
return {
|
| 461 |
-
text: 'High Inclusion',
|
| 462 |
-
description: 'This exon is predicted to be included in most transcripts.',
|
| 463 |
-
colorClass: 'bg-green-50 border border-green-200',
|
| 464 |
-
textClass: 'text-green-600'
|
| 465 |
-
};
|
| 466 |
-
} else if (psi >= 0.3) {
|
| 467 |
-
return {
|
| 468 |
-
text: 'Variable/Regulated',
|
| 469 |
-
description: 'This exon shows intermediate inclusion, suggesting regulation.',
|
| 470 |
-
colorClass: 'bg-yellow-50 border border-yellow-200',
|
| 471 |
-
textClass: 'text-yellow-600'
|
| 472 |
-
};
|
| 473 |
-
} else {
|
| 474 |
-
return {
|
| 475 |
-
text: 'High Skipping',
|
| 476 |
-
description: 'This exon is predicted to be skipped in most transcripts.',
|
| 477 |
-
colorClass: 'bg-red-50 border border-red-200',
|
| 478 |
-
textClass: 'text-red-600'
|
| 479 |
-
};
|
| 480 |
-
}
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
/**
|
| 484 |
-
* Create force plot using Plotly
|
| 485 |
-
*/
|
| 486 |
-
function createForcePlot(elementId, forceData) {
|
| 487 |
-
const element = document.getElementById(elementId);
|
| 488 |
-
if (!element) return;
|
| 489 |
-
|
| 490 |
-
const positions = Array.from({ length: forceData.length }, (_, i) => i + 1);
|
| 491 |
-
const colors = forceData.map(v => v >= 0 ? 'rgba(34, 197, 94, 0.8)' : 'rgba(239, 68, 68, 0.8)');
|
| 492 |
-
|
| 493 |
-
const trace = {
|
| 494 |
-
x: positions,
|
| 495 |
-
y: forceData,
|
| 496 |
-
type: 'bar',
|
| 497 |
-
marker: { color: colors },
|
| 498 |
-
hovertemplate: 'Position %{x}<br>Contribution: %{y:.4f}<extra></extra>'
|
| 499 |
-
};
|
| 500 |
-
|
| 501 |
-
const layout = {
|
| 502 |
-
margin: { t: 20, r: 20, b: 50, l: 50 },
|
| 503 |
-
xaxis: {
|
| 504 |
-
title: { text: 'Position', font: { size: 11 } },
|
| 505 |
-
tickmode: 'linear',
|
| 506 |
-
dtick: 10,
|
| 507 |
-
range: [0, 91]
|
| 508 |
-
},
|
| 509 |
-
yaxis: {
|
| 510 |
-
title: { text: 'Contribution to PSI', font: { size: 11 } },
|
| 511 |
-
zeroline: true,
|
| 512 |
-
zerolinecolor: '#888',
|
| 513 |
-
zerolinewidth: 1
|
| 514 |
-
},
|
| 515 |
-
shapes: [{
|
| 516 |
-
type: 'rect',
|
| 517 |
-
xref: 'x',
|
| 518 |
-
yref: 'paper',
|
| 519 |
-
x0: 10.5,
|
| 520 |
-
x1: 80.5,
|
| 521 |
-
y0: 0,
|
| 522 |
-
y1: 1,
|
| 523 |
-
fillcolor: 'rgba(59, 130, 246, 0.05)',
|
| 524 |
-
line: { width: 0 }
|
| 525 |
-
}],
|
| 526 |
-
annotations: [
|
| 527 |
-
{ x: 5, y: 1.05, xref: 'x', yref: 'paper', text: "5' flank", showarrow: false, font: { size: 9, color: '#888' } },
|
| 528 |
-
{ x: 45, y: 1.05, xref: 'x', yref: 'paper', text: 'Exon', showarrow: false, font: { size: 9, color: '#3b82f6' } },
|
| 529 |
-
{ x: 85, y: 1.05, xref: 'x', yref: 'paper', text: "3' flank", showarrow: false, font: { size: 9, color: '#888' } }
|
| 530 |
-
],
|
| 531 |
-
paper_bgcolor: 'rgba(0,0,0,0)',
|
| 532 |
-
plot_bgcolor: 'rgba(0,0,0,0)',
|
| 533 |
-
font: { family: 'system-ui, -apple-system, sans-serif' }
|
| 534 |
-
};
|
| 535 |
-
|
| 536 |
-
const config = {
|
| 537 |
-
responsive: true,
|
| 538 |
-
displayModeBar: true,
|
| 539 |
-
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
| 540 |
-
displaylogo: false
|
| 541 |
-
};
|
| 542 |
-
|
| 543 |
-
Plotly.newPlot(element, [trace], layout, config);
|
| 544 |
-
}
|
| 545 |
|
| 546 |
/**
|
| 547 |
* Show error state
|
|
@@ -673,8 +429,6 @@ function restoreNameDisplay(parent, index, name) {
|
|
| 673 |
|
| 674 |
// Make functions available globally
|
| 675 |
window.copyLink = copyLink;
|
| 676 |
-
window.closeDetailModal = closeDetailModal;
|
| 677 |
-
window.showDetail = showDetail;
|
| 678 |
window.startEditName = startEditName;
|
| 679 |
window.handleEditKeydown = handleEditKeydown;
|
| 680 |
window.handleEditBlur = handleEditBlur;
|
|
|
|
| 20 |
const resultsTableBody = document.getElementById('results-table-body');
|
| 21 |
const searchInput = document.getElementById('search-results');
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
// Pagination elements
|
| 24 |
const pageStart = document.getElementById('page-start');
|
| 25 |
const pageEnd = document.getElementById('page-end');
|
|
|
|
| 142 |
|
| 143 |
const data = await response.json();
|
| 144 |
|
|
|
|
|
|
|
|
|
|
| 145 |
// Update table
|
| 146 |
totalResults = data.total;
|
| 147 |
totalPages = data.total_pages;
|
|
|
|
| 158 |
}
|
| 159 |
}
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
/**
|
| 162 |
* Render results table
|
| 163 |
*/
|
|
|
|
| 167 |
for (const result of results) {
|
| 168 |
const row = document.createElement('tr');
|
| 169 |
row.className = 'hover:bg-gray-50 cursor-pointer';
|
| 170 |
+
row.onclick = () => {
|
| 171 |
+
window.location.href = `/batch/${jobId}/sequence/${result.index}`;
|
| 172 |
+
};
|
| 173 |
|
| 174 |
const statusBadge = getStatusBadge(result.status);
|
| 175 |
const psiDisplay = result.status === 'success' && result.psi !== null
|
|
|
|
| 298 |
pageButtons.appendChild(nextBtn);
|
| 299 |
}
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
/**
|
| 303 |
* Show error state
|
|
|
|
| 429 |
|
| 430 |
// Make functions available globally
|
| 431 |
window.copyLink = copyLink;
|
|
|
|
|
|
|
| 432 |
window.startEditName = startEditName;
|
| 433 |
window.handleEditKeydown = handleEditKeydown;
|
| 434 |
window.handleEditBlur = handleEditBlur;
|
webapp/templates/batch_result.html
CHANGED
|
@@ -48,26 +48,6 @@
|
|
| 48 |
|
| 49 |
<!-- Results Container (hidden initially) -->
|
| 50 |
<div id="results-container" class="hidden space-y-6">
|
| 51 |
-
<!-- Summary Stats -->
|
| 52 |
-
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
| 53 |
-
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 text-center">
|
| 54 |
-
<p class="text-sm font-medium text-gray-500">Total Sequences</p>
|
| 55 |
-
<p id="stat-total" class="mt-1 text-2xl font-bold text-gray-900">0</p>
|
| 56 |
-
</div>
|
| 57 |
-
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 text-center">
|
| 58 |
-
<p class="text-sm font-medium text-gray-500">Successful</p>
|
| 59 |
-
<p id="stat-success" class="mt-1 text-2xl font-bold text-green-600">0</p>
|
| 60 |
-
</div>
|
| 61 |
-
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 text-center">
|
| 62 |
-
<p class="text-sm font-medium text-gray-500">Invalid</p>
|
| 63 |
-
<p id="stat-invalid" class="mt-1 text-2xl font-bold text-red-600">0</p>
|
| 64 |
-
</div>
|
| 65 |
-
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 text-center">
|
| 66 |
-
<p class="text-sm font-medium text-gray-500">Average PSI</p>
|
| 67 |
-
<p id="stat-avg-psi" class="mt-1 text-2xl font-bold text-primary-600">-</p>
|
| 68 |
-
</div>
|
| 69 |
-
</div>
|
| 70 |
-
|
| 71 |
<!-- Search and Export -->
|
| 72 |
<div class="flex flex-wrap gap-4 items-center justify-between">
|
| 73 |
<div class="flex-1 min-w-[200px] max-w-md">
|
|
@@ -139,34 +119,11 @@
|
|
| 139 |
</div>
|
| 140 |
</div>
|
| 141 |
|
| 142 |
-
<!-- Detail Modal -->
|
| 143 |
-
<div id="detail-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
| 144 |
-
<div class="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
| 145 |
-
<div class="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center">
|
| 146 |
-
<h3 id="detail-title" class="text-lg font-medium text-gray-900">Sequence Details</h3>
|
| 147 |
-
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-500">
|
| 148 |
-
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 149 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
| 150 |
-
</svg>
|
| 151 |
-
</button>
|
| 152 |
-
</div>
|
| 153 |
-
<div id="detail-content" class="p-6 space-y-6">
|
| 154 |
-
<!-- Loading spinner for detail -->
|
| 155 |
-
<div id="detail-loading" class="text-center py-8">
|
| 156 |
-
<svg class="animate-spin h-8 w-8 text-primary-600 mx-auto" fill="none" viewBox="0 0 24 24">
|
| 157 |
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
| 158 |
-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 159 |
-
</svg>
|
| 160 |
-
</div>
|
| 161 |
-
<!-- Detail will be populated here -->
|
| 162 |
-
</div>
|
| 163 |
-
</div>
|
| 164 |
-
</div>
|
| 165 |
{% endblock %}
|
| 166 |
|
| 167 |
{% block scripts %}
|
| 168 |
<script>
|
| 169 |
const jobId = "{{ job_id }}";
|
| 170 |
</script>
|
| 171 |
-
<script src="/static/js/batch-result.js"></script>
|
| 172 |
{% endblock %}
|
|
|
|
| 48 |
|
| 49 |
<!-- Results Container (hidden initially) -->
|
| 50 |
<div id="results-container" class="hidden space-y-6">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
<!-- Search and Export -->
|
| 52 |
<div class="flex flex-wrap gap-4 items-center justify-between">
|
| 53 |
<div class="flex-1 min-w-[200px] max-w-md">
|
|
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
{% endblock %}
|
| 123 |
|
| 124 |
{% block scripts %}
|
| 125 |
<script>
|
| 126 |
const jobId = "{{ job_id }}";
|
| 127 |
</script>
|
| 128 |
+
<script src="/static/js/batch-result.js?v=3"></script>
|
| 129 |
{% endblock %}
|