import { escapeHtml, debounce, DEBOUNCE_DELAY } from '../utils/helpers.js';
export class SplitViewManager {
constructor() {
this.currentMatchIndex = null;
this.currentData = null;
this.queryPage = 0;
this.refPage = 0;
this.pageSize = 50;
this.querySearchTerm = '';
this.refSearchTerm = '';
this.filteredQueryContigs = [];
this.filteredRefContigs = [];
this.originalResults = null;
this.originalInput = null;
this.originalConfig = null;
this.debouncedFilterQuery = debounce(
(term) => this.filterQueryContigs(term),
DEBOUNCE_DELAY
);
this.debouncedFilterRef = debounce(
(term) => this.filterReferenceContigs(term),
DEBOUNCE_DELAY
);
}
async openComparison(matchIndex) {
if (!this.originalResults || !this.originalInput || !this.originalConfig) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--error); color: white; padding: 1rem; border-radius: 6px; z-index: 10000;';
errorDiv.textContent = 'Please refresh the page and run Identify again before using Compare Contigs.';
document.body.appendChild(errorDiv);
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.parentNode.removeChild(errorDiv);
}
}, 5000);
return;
}
this.currentMatchIndex = matchIndex;
document.getElementById('split-view-modal').style.display = 'block';
document.getElementById('modal-title').textContent = 'Loading contig comparison...';
try {
const formData = new FormData();
if (this.originalInput.type === 'text') {
formData.append('header_text', this.originalInput.content);
if (this.originalInput.filename) {
formData.append('filename', this.originalInput.filename);
}
} else if (this.originalInput.type === 'file') {
formData.append('file', this.originalInput.file);
}
formData.append('config', JSON.stringify(this.originalConfig));
const response = await fetch(`/api/identify?mode=detailed&match_id=${matchIndex}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to get detailed data: ${response.status} - ${errorText}`);
}
try {
this.currentData = await response.json();
} catch (jsonError) {
throw new Error(`Invalid JSON response from server: ${jsonError.message}`);
}
this.renderSplitView();
} catch (error) {
document.getElementById('modal-title').textContent = 'Error loading contig comparison';
document.getElementById('query-contigs').innerHTML = `<div class="error">Failed to load data: ${escapeHtml(error.message)}</div>`;
}
}
renderSplitView() {
const data = this.currentData;
if (!data) {
throw new Error('No currentData available for split view');
}
if (!data.query || !data.query.contigs) {
throw new Error('Invalid query data structure');
}
if (!data.reference || !data.reference.display_name) {
throw new Error('Invalid reference data structure');
}
document.getElementById('modal-title').textContent =
`Contig Comparison - ${data.reference.display_name}`;
document.getElementById('reference-title').textContent =
`Reference Contigs (${data.reference.display_name})`;
this.applyFilters();
this.renderQueryContigs();
this.renderReferenceContigs();
this.renderConnections();
}
applyFilters() {
const data = this.currentData;
if (!data || !data.query || !data.query.contigs) {
this.filteredQueryContigs = [];
} else {
this.filteredQueryContigs = data.query.contigs.filter(contig =>
contig && contig.name && contig.name.toLowerCase().includes(this.querySearchTerm.toLowerCase())
);
}
if (!data || !data.reference || !data.reference.contigs) {
this.filteredRefContigs = [];
} else {
this.filteredRefContigs = data.reference.contigs.filter(contig =>
contig && contig.name && contig.name.toLowerCase().includes(this.refSearchTerm.toLowerCase())
);
}
}
renderQueryContigs() {
if (!Array.isArray(this.filteredQueryContigs)) {
this.filteredQueryContigs = [];
}
const startIdx = this.queryPage * this.pageSize;
const endIdx = Math.min(startIdx + this.pageSize, this.filteredQueryContigs.length);
const pageContigs = this.filteredQueryContigs.slice(startIdx, endIdx);
let html = '';
for (let i = 0; i < pageContigs.length; i++) {
const contig = pageContigs[i];
const statusClass = this.getStatusClass(contig.match_status);
const contigIndex = startIdx + i;
const refgetBadge = this.hasRefgetMatch(contig)
? '<span class="refget-badge" title="Found in refget" style="background: var(--accent); color: white; padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.7rem; margin-left: 0.25rem;">refget</span>'
: '';
html += `
<div class="contig-item ${statusClass}" data-contig-index="${contigIndex}" data-contig-type="query">
<div class="contig-name">${escapeHtml(contig.name)}${refgetBadge}</div>
<div class="contig-details">
<span class="contig-length">${contig.length.toLocaleString()} bp</span>
<span class="contig-status ${statusClass}">${escapeHtml(contig.match_status)}</span>
${contig.md5 ? `<span class="contig-md5" title="${escapeHtml(contig.md5)}">${contig.md5.substring(0, 8)}...</span>` : ''}
<span class="click-hint">Click for details</span>
</div>
</div>
`;
}
document.getElementById('query-contigs').innerHTML = html;
document.querySelectorAll('#query-contigs .contig-item').forEach((item) => {
item.addEventListener('click', () => {
const contigIndex = parseInt(item.dataset.contigIndex, 10);
const contig = this.filteredQueryContigs[contigIndex];
this.showContigDetails('query', contig);
});
});
this.updateQueryPagination();
}
renderReferenceContigs() {
const startIdx = this.refPage * this.pageSize;
const endIdx = Math.min(startIdx + this.pageSize, this.filteredRefContigs.length);
const pageContigs = this.filteredRefContigs.slice(startIdx, endIdx);
let html = '';
for (let i = 0; i < pageContigs.length; i++) {
const contig = pageContigs[i];
const statusClass = this.getStatusClass(contig.match_status);
const contigIndex = startIdx + i;
html += `
<div class="contig-item ${statusClass}" data-contig-index="${contigIndex}" data-contig-type="reference">
<div class="contig-name">${escapeHtml(contig.name)}</div>
<div class="contig-details">
<span class="contig-length">${contig.length.toLocaleString()} bp</span>
<span class="contig-status ${statusClass}">${escapeHtml(contig.match_status)}</span>
${contig.md5 ? `<span class="contig-md5" title="${escapeHtml(contig.md5)}">${contig.md5.substring(0, 8)}...</span>` : ''}
<span class="click-hint">Click for details</span>
</div>
</div>
`;
}
document.getElementById('reference-contigs').innerHTML = html;
document.querySelectorAll('#reference-contigs .contig-item').forEach((item) => {
item.addEventListener('click', () => {
const contigIndex = parseInt(item.dataset.contigIndex, 10);
const contig = this.filteredRefContigs[contigIndex];
this.showContigDetails('reference', contig);
});
});
this.updateReferencePagination();
}
renderConnections() {
const svg = document.getElementById('connections-svg');
svg.innerHTML = '';
const data = this.currentData;
const queryStartIdx = this.queryPage * this.pageSize;
const refStartIdx = this.refPage * this.pageSize;
data.mappings.exact_matches.forEach(match => {
if (this.isInCurrentPage(match.query_index, queryStartIdx, this.pageSize) &&
this.isInCurrentPage(match.reference_index, refStartIdx, this.pageSize)) {
this.drawConnection(svg, match.query_index - queryStartIdx, match.reference_index - refStartIdx, 'exact');
}
});
data.mappings.renamed_matches.forEach(match => {
if (this.isInCurrentPage(match.query_index, queryStartIdx, this.pageSize) &&
this.isInCurrentPage(match.reference_index, refStartIdx, this.pageSize)) {
this.drawConnection(svg, match.query_index - queryStartIdx, match.reference_index - refStartIdx, 'renamed');
}
});
data.mappings.conflicts.forEach(match => {
if (this.isInCurrentPage(match.query_index, queryStartIdx, this.pageSize) &&
match.reference_index && this.isInCurrentPage(match.reference_index, refStartIdx, this.pageSize)) {
this.drawConnection(svg, match.query_index - queryStartIdx, match.reference_index - refStartIdx, 'conflict');
}
});
}
isInCurrentPage(index, startIdx, pageSize) {
return index >= startIdx && index < startIdx + pageSize;
}
drawConnection(svg, queryIdx, refIdx, type) {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const queryY = queryIdx * 60 + 30; const refY = refIdx * 60 + 30;
const svgWidth = svg.clientWidth;
const startX = 0;
const endX = svgWidth;
const midX = svgWidth / 2;
const path = `M ${startX} ${queryY} C ${midX} ${queryY} ${midX} ${refY} ${endX} ${refY}`;
line.setAttribute('d', path);
line.setAttribute('stroke', this.getConnectionColor(type));
line.setAttribute('stroke-width', '2');
line.setAttribute('fill', 'none');
line.setAttribute('class', `connection connection-${type}`);
svg.appendChild(line);
}
getConnectionColor(type) {
switch (type) {
case 'exact': return 'var(--success)';
case 'renamed': return 'var(--accent)';
case 'conflict': return 'var(--error)';
default: return 'var(--text-muted)';
}
}
hasRefgetMatch(contig) {
return contig.refget_metadata && contig.refget_metadata.status === 'found';
}
getStatusClass(status) {
switch (status) {
case 'exact': return 'status-exact';
case 'renamed': return 'status-renamed';
case 'conflict': return 'status-conflict';
case 'missing': return 'status-missing';
default: return 'status-unknown';
}
}
updateQueryPagination() {
const totalPages = Math.ceil(this.filteredQueryContigs.length / this.pageSize);
const currentPage = this.queryPage + 1;
document.getElementById('query-pagination-info').textContent =
`Page ${currentPage} of ${totalPages} (${this.filteredQueryContigs.length} contigs)`;
this.renderPaginationControls('query-pagination', this.queryPage, totalPages,
(page) => {
this.queryPage = page;
this.renderQueryContigs();
this.renderConnections();
});
}
updateReferencePagination() {
const totalPages = Math.ceil(this.filteredRefContigs.length / this.pageSize);
const currentPage = this.refPage + 1;
document.getElementById('ref-pagination-info').textContent =
`Page ${currentPage} of ${totalPages} (${this.filteredRefContigs.length} contigs)`;
this.renderPaginationControls('ref-pagination', this.refPage, totalPages,
(page) => {
this.refPage = page;
this.renderReferenceContigs();
this.renderConnections();
});
}
renderPaginationControls(containerId, currentPage, totalPages, onPageChange) {
const container = document.getElementById(containerId);
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
container.innerHTML = '';
if (currentPage > 0) {
const prevBtn = document.createElement('button');
prevBtn.className = 'page-btn';
prevBtn.textContent = 'Prev';
prevBtn.onclick = () => onPageChange(currentPage - 1);
container.appendChild(prevBtn);
}
const start = Math.max(0, currentPage - 2);
const end = Math.min(totalPages, currentPage + 3);
for (let i = start; i < end; i++) {
const pageBtn = document.createElement('button');
pageBtn.className = i === currentPage ? 'page-btn active' : 'page-btn';
pageBtn.textContent = i + 1;
pageBtn.onclick = () => onPageChange(i);
container.appendChild(pageBtn);
}
if (currentPage < totalPages - 1) {
const nextBtn = document.createElement('button');
nextBtn.className = 'page-btn';
nextBtn.textContent = 'Next';
nextBtn.onclick = () => onPageChange(currentPage + 1);
container.appendChild(nextBtn);
}
}
filterQueryContigs(searchTerm) {
this.querySearchTerm = searchTerm;
this.queryPage = 0; this.applyFilters();
this.renderQueryContigs();
this.renderConnections();
}
filterReferenceContigs(searchTerm) {
this.refSearchTerm = searchTerm;
this.refPage = 0; this.applyFilters();
this.renderReferenceContigs();
this.renderConnections();
}
closeComparison() {
document.getElementById('split-view-modal').style.display = 'none';
this.currentMatchIndex = null;
this.currentData = null;
this.queryPage = 0;
this.refPage = 0;
this.querySearchTerm = '';
this.refSearchTerm = '';
document.getElementById('query-search').value = '';
document.getElementById('ref-search').value = '';
}
showContigDetails(type, contigData) {
try {
const contig = typeof contigData === 'string' ? JSON.parse(contigData.replace(/"/g, '"')) : contigData;
const modalHtml = `
<div id="contig-details-modal" class="modal" style="display: block; z-index: 1100;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>${escapeHtml(contig.name)} Details</h2>
<span class="modal-close" onclick="document.getElementById('contig-details-modal').remove()">×</span>
</div>
<div class="modal-body" style="padding: 1.5rem;">
<div class="contig-detail-grid">
<!-- Always show name and length -->
<div class="detail-row">
<span class="detail-label">Name:</span>
<span class="detail-value">${escapeHtml(contig.name)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Length:</span>
<span class="detail-value">${contig.length.toLocaleString()} bp</span>
</div>
<!-- Only show MD5 if it exists -->
${contig.md5 ? `
<div class="detail-row">
<span class="detail-label">MD5 Hash:</span>
<span class="detail-value monospace">${escapeHtml(contig.md5)}</span>
</div>
` : ''}
<!-- Only show sha512t24u if it exists -->
${contig.sha512t24u ? `
<div class="detail-row">
<span class="detail-label">sha512t24u:</span>
<span class="detail-value monospace">${escapeHtml(contig.sha512t24u)}</span>
</div>
` : ''}
<!-- Only show sequence role if it exists -->
${contig.sequence_role ? `
<div class="detail-row">
<span class="detail-label">Sequence Role:</span>
<span class="detail-value">${escapeHtml(contig.sequence_role)}</span>
</div>
` : ''}
<!-- Only show aliases if they exist -->
${contig.aliases && contig.aliases.length > 0 ? `
<div class="detail-row">
<span class="detail-label">Aliases:</span>
<span class="detail-value">${contig.aliases.map(alias => escapeHtml(alias)).join(', ')}</span>
</div>
` : ''}
<!-- Only show description if it exists -->
${contig.description ? `
<div class="detail-row">
<span class="detail-label">Description:</span>
<span class="detail-value">${escapeHtml(contig.description)}</span>
</div>
` : ''}
<!-- Only show assembly info if it exists -->
${contig.assembly_info ? `
<div class="detail-row">
<span class="detail-label">Assembly:</span>
<span class="detail-value">${escapeHtml(contig.assembly_info.assembly)}${contig.assembly_info.organism ? ` (${escapeHtml(contig.assembly_info.organism)})` : ''}</span>
</div>
` : ''}
<!-- Refget metadata for missing contigs -->
${this.hasRefgetMatch(contig) ? `
<div class="detail-row" style="grid-column: 1 / -1; margin-top: 0.5rem;">
<span class="detail-label" style="font-weight: bold; color: var(--accent);">Refget Aliases:</span>
</div>
${contig.refget_metadata.aliases && contig.refget_metadata.aliases.length > 0 ? contig.refget_metadata.aliases.map(alias => `
<div class="detail-row">
<span class="detail-label" style="padding-left: 1rem;">${escapeHtml(alias.naming_authority)}:</span>
<span class="detail-value">${escapeHtml(alias.value)}</span>
</div>
`).join('') : `
<div class="detail-row">
<span class="detail-value" style="padding-left: 1rem;">(no aliases)</span>
</div>
`}
${contig.refget_metadata.sha512t24u ? `
<div class="detail-row">
<span class="detail-label" style="padding-left: 1rem;">sha512t24u:</span>
<span class="detail-value monospace">${escapeHtml(contig.refget_metadata.sha512t24u)}</span>
</div>
` : ''}
${contig.refget_metadata.circular ? `
<div class="detail-row">
<span class="detail-label" style="padding-left: 1rem;">Circular:</span>
<span class="detail-value">Yes</span>
</div>
` : ''}
` : ''}
${contig.refget_metadata && contig.refget_metadata.status === 'not_found' ? `
<div class="detail-row">
<span class="detail-label">Refget:</span>
<span class="detail-value" style="color: var(--text-muted);">Not found in refget server</span>
</div>
` : ''}
<!-- Always show match status and source -->
<div class="detail-row">
<span class="detail-label">Match Status:</span>
<span class="detail-value status-badge ${this.getStatusClass(contig.match_status)}">${escapeHtml(contig.match_status)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Source:</span>
<span class="detail-value">${type === 'query' ? 'Your Input' : 'Reference Genome'}</span>
</div>
</div>
<div style="margin-top: 1.5rem; text-align: center;">
<button onclick="document.getElementById('contig-details-modal').remove()" style="
background: var(--success);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
">Close</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
} catch (error) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--error); color: white; padding: 1rem; border-radius: 6px; z-index: 10000;';
errorDiv.textContent = 'Error showing contig details: ' + error.message;
document.body.appendChild(errorDiv);
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.parentNode.removeChild(errorDiv);
}
}, 5000);
}
}
}