DocFinder / static /script.js
heymenn's picture
fix ui issue when downloading CR
1e83f3a
// Global application state
let currentResults = [];
let currentMode = 'single';
// Initialization
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
function initializeApp() {
setupTabHandlers();
setupKeyboardHandlers();
updateHeaderStats('Ready');
}
// Tab management
function setupTabHandlers() {
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', function() {
const mode = this.dataset.mode;
switchMode(mode);
});
});
}
function switchMode(mode) {
currentMode = mode;
// Update tabs
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-mode="${mode}"]`).classList.add('active');
// Update forms
document.querySelectorAll('.search-form').forEach(form => {
form.classList.remove('active');
});
document.getElementById(`${mode}-form`).classList.add('active');
// Reset results
hideResults();
}
// Show version field only when the input looks like a spec (ETSI or 3GPP)
const ETSI_SPEC_RE = /^\d{3} \d{3}/;
const ETSI_TDOC_RE = /^(?:SET|SCP|SETTEC|SETREQ|SCPTEC|SCPREQ)\(\d+\)\d+/i;
const GPP_SPEC_RE = /^\d{2}\.\d{3}/;
function toggleVersionField() {
const docId = document.getElementById('doc-id').value.trim();
const versionGroup = document.getElementById('single-version-group');
const formatGroup = document.getElementById('single-format-group');
const isSpec = ETSI_SPEC_RE.test(docId) || GPP_SPEC_RE.test(docId);
const isEtsiSpec = ETSI_SPEC_RE.test(docId);
versionGroup.style.display = isSpec ? 'block' : 'none';
formatGroup.style.display = isEtsiSpec ? 'block' : 'none';
if (!isSpec) document.getElementById('doc-version').value = '';
if (!isEtsiSpec) document.getElementById('doc-format').value = 'pdf';
}
// Keyboard shortcuts management
function setupKeyboardHandlers() {
document.getElementById('doc-id').addEventListener('keypress', function(e) {
if (e.key === 'Enter') searchSingle();
});
document.getElementById('keywords').addEventListener('keypress', function(e) {
if (e.key === 'Enter') searchKeyword();
});
document.getElementById('bm25-keywords').addEventListener('keypress', function(e) {
if (e.key === 'Enter') searchBM25();
});
}
// Search functions
async function searchSingle() {
const docId = document.getElementById('doc-id').value.trim();
if (!docId) {
showError('Please enter a document ID');
return;
}
const format = document.getElementById('doc-format').value || 'pdf';
const isDocx = ETSI_SPEC_RE.test(docId) && format === 'docx';
if (isDocx) {
downloadDocx(docId);
return;
}
showLoading();
updateHeaderStats('Searching...');
try {
const version = document.getElementById('doc-version').value.trim() || null;
const body = { doc_id: docId };
if (version) body.version = version;
const response = await fetch(`/find/single`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
});
const data = await response.json();
if (response.ok) {
displaySingleResult(data);
updateHeaderStats(`Found in ${data.search_time.toFixed(3)}s`);
} else {
showError(data.detail);
updateHeaderStats('Error');
}
} catch (error) {
showError('Error connecting to server');
updateHeaderStats('Error');
console.error('Error:', error);
} finally {
hideLoading();
}
}
async function downloadDocx(docId) {
showLoading();
updateHeaderStats('Downloading DOCX...');
try {
const version = document.getElementById('doc-version').value.trim() || null;
const body = { doc_id: docId };
if (version) body.version = version;
const response = await fetch('/find/docx', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (response.ok) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition') || '';
const fnMatch = disposition.match(/filename="?([^";\n]+)"?/);
const filename = fnMatch ? fnMatch[1] : `${docId.replace(/ /g, '_')}.docx`;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
updateHeaderStats('DOCX downloaded');
} else {
const data = await response.json();
showError(data.detail);
updateHeaderStats('Error');
}
} catch (error) {
showError('Error connecting to server');
updateHeaderStats('Error');
console.error('Error:', error);
} finally {
hideLoading();
}
}
async function searchBatch() {
const batchText = document.getElementById('batch-ids').value.trim();
if (!batchText) {
showError('Please enter at least one document ID');
return;
}
const docIds = batchText.split('\n')
.map(id => id.trim())
.filter(id => id !== '');
if (docIds.length === 0) {
showError('Please enter at least one valid document ID');
return;
}
showLoading();
updateHeaderStats('Searching...');
try {
const version = document.getElementById('batch-version').value.trim() || null;
const body = { doc_ids: docIds };
if (version) body.version = version;
const response = await fetch(`/find/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
});
const data = await response.json();
if (response.ok) {
displayBatchResults(data);
updateHeaderStats(`${Object.keys(data.results).length} found, ${data.missing.length} missing - ${data.search_time.toFixed(3)}s`);
} else {
showError(data.detail);
updateHeaderStats('Error');
}
} catch (error) {
showError('Error connecting to server');
updateHeaderStats('Error');
console.error('Error:', error);
} finally {
hideLoading();
}
}
async function searchKeyword() {
const keywords = document.getElementById('keywords').value.trim();
const searchMode = document.getElementById('search-mode-filter').value;
if (!keywords && searchMode === 'deep') {
showError('Please enter at least one keyword in deep search mode');
return;
}
showLoading();
updateHeaderStats('Searching...');
try {
const body = {
keywords: keywords,
search_mode: searchMode,
case_sensitive: document.getElementById('case-sensitive-filter').checked,
source: document.getElementById('source-filter').value,
mode: document.getElementById('mode-filter').value
};
const specType = document.getElementById('spec-type-filter').value;
if (specType) {
body.spec_type = specType;
}
const response = await fetch(`/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
});
const data = await response.json();
if (response.ok) {
displaySearchResults(data);
updateHeaderStats(`${data.results.length} result(s) - ${data.search_time.toFixed(3)}s`);
} else {
showError(data.detail);
updateHeaderStats('Error');
}
} catch (error) {
showError('Error connecting to server');
updateHeaderStats('Error');
console.error('Error:', error);
} finally {
hideLoading();
}
}
async function searchBM25() {
const keywords = document.getElementById('bm25-keywords').value.trim();
if (!keywords) {
showError('Please enter a search query');
return;
}
showLoading();
updateHeaderStats('Searching...');
try {
const body = {
keywords: keywords,
source: document.getElementById('bm25-source-filter').value,
threshold: parseInt(document.getElementById('threshold').value) || 60
};
const specType = document.getElementById('bm25-spec-type-filter').value;
if (specType) {
body.spec_type = specType;
}
const response = await fetch(`/search/bm25`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
});
const data = await response.json();
if (response.ok) {
displaySearchResults(data);
updateHeaderStats(`${data.results.length} result(s) - ${data.search_time.toFixed(3)}s`);
} else {
showError(data.detail);
updateHeaderStats('Error');
}
} catch (error) {
showError('Error connecting to server');
updateHeaderStats('Error');
console.error('Error:', error);
} finally {
hideLoading();
}
}
// Results display functions
function displaySingleResult(data) {
const resultsContent = document.getElementById('results-content');
const isEtsiTdoc = ETSI_TDOC_RE.test(data.doc_id);
const urlBlock = isEtsiTdoc
? `<div class="result-detail">
<strong>URL:</strong> <span class="result-url-text">${data.url}</span>
<button class="btn btn-primary download-btn" onclick="downloadTdoc('${data.doc_id}')">Download</button>
</div>`
: `<div class="result-detail result-url">
<strong>URL:</strong> <a href="${data.url}" target="_blank">${data.url}</a>
</div>`;
resultsContent.innerHTML = `
<div class="result-item">
<div class="result-header">
<div class="result-id">${data.doc_id}</div>
<div class="result-status status-found">Found</div>
</div>
<div class="result-details">
${data.version ? `<div class="result-detail"><strong>Version:</strong> ${data.version}</div>` : ''}
${data.scope ? `<div class="result-detail"><strong>Scope:</strong> ${data.scope}</div>` : ''}
${urlBlock}
</div>
</div>
`;
showResults();
}
async function downloadTdoc(docId) {
showLoading();
updateHeaderStats('Downloading...');
try {
const response = await fetch('/find/tdoc/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ doc_id: docId })
});
if (response.ok) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition') || '';
const fnMatch = disposition.match(/filename="?([^";\n]+)"?/);
const filename = fnMatch ? fnMatch[1] : `${docId}.docx`;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
updateHeaderStats('Downloaded');
} else {
const data = await response.json();
showError(data.detail);
updateHeaderStats('Error');
}
} catch (error) {
showError('Error connecting to server');
updateHeaderStats('Error');
console.error('Error:', error);
} finally {
hideLoading();
}
}
function displayBatchResults(data) {
const resultsContent = document.getElementById('results-content');
let html = '';
// Found results
Object.entries(data.results).forEach(([docId, url]) => {
html += `
<div class="result-item">
<div class="result-header">
<div class="result-id">${docId}</div>
<div class="result-status status-found">Found</div>
</div>
<div class="result-details">
<div class="result-detail result-url">
<strong>URL:</strong> <a href="${url}" target="_blank">${url}</a>
</div>
</div>
</div>
`;
});
// Missing documents
data.missing.forEach(docId => {
html += `
<div class="result-item">
<div class="result-header">
<div class="result-id">${docId}</div>
<div class="result-status status-missing">Not Found</div>
</div>
<div class="result-details">
<div class="result-detail">Document not found or not indexed</div>
</div>
</div>
`;
});
resultsContent.innerHTML = html;
showResults();
}
function displaySearchResults(data) {
const resultsContent = document.getElementById('results-content');
currentResults = data.results;
let html = '';
data.results.forEach((spec, index) => {
const hasContent = spec.contains && Object.keys(spec.contains).length > 0;
html += `
<div class="result-item">
<div class="result-header">
<div class="result-id">${spec.id}</div>
<div class="result-status status-found">${spec.type || 'Specification'}</div>
</div>
<div class="result-details">
<div class="result-detail"><strong>Title:</strong> ${spec.title}</div>
${spec.version ? `<div class="result-detail"><strong>Version:</strong> ${spec.version}</div>` : ''}
${spec.working_group ? `<div class="result-detail"><strong>Working Group:</strong> ${spec.working_group}</div>` : ''}
${spec.type ? `<div class="result-detail"><strong>Type:</strong> ${spec.type}</div>` : ''}
${spec.scope ? `<div class="result-detail"><strong>Scope:</strong> ${spec.scope}</div>` : ''}
${spec.url ? `<div class="result-detail result-url"><strong>URL:</strong> <a href="${spec.url}" target="_blank">${spec.url}</a></div>` : ''}
${hasContent ? `<button class="view-content-btn" onclick="viewContent(${index})">View Content</button>` : ''}
</div>
</div>
`;
});
resultsContent.innerHTML = html;
showResults();
}
// Content display functions
function viewContent(index) {
const spec = currentResults[index];
if (!spec.contains) return;
document.getElementById('content-title').textContent = `${spec.id} - ${spec.title}`;
const contentSections = document.getElementById('content-sections');
let html = '';
Object.entries(spec.contains).forEach(([sectionTitle, content]) => {
html += `
<div class="content-section">
<h3>${sectionTitle}</h3>
<p>${content}</p>
<button class="copy-section-btn" onclick="copyText('${content.replace(/'/g, "\\'")}')">
Copy this section
</button>
</div>
`;
});
contentSections.innerHTML = html;
showContentPage();
}
function closeContentPage() {
hideContentPage();
}
function copyAllContent() {
const sections = document.querySelectorAll('.content-section p');
const allText = Array.from(sections).map(p => p.textContent).join('\n\n');
copyText(allText);
}
function copyText(text) {
navigator.clipboard.writeText(text).then(() => {
showSuccess('Text copied to clipboard');
}).catch(() => {
showError('Error copying text');
});
}
// Interface utilities
function showLoading() {
document.getElementById('loading-container').style.display = 'flex';
hideResults();
hideError();
}
function hideLoading() {
document.getElementById('loading-container').style.display = 'none';
}
function showResults() {
document.getElementById('results-container').style.display = 'block';
hideError();
}
function hideResults() {
document.getElementById('results-container').style.display = 'none';
}
function showContentPage() {
document.getElementById('content-page').classList.add('active');
}
function hideContentPage() {
document.getElementById('content-page').classList.remove('active');
}
function showError(message) {
hideError();
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
document.querySelector('.search-container').appendChild(errorDiv);
setTimeout(() => {
hideError();
}, 5000);
}
function showSuccess(message) {
hideError();
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
document.querySelector('.search-container').appendChild(successDiv);
setTimeout(() => {
hideError();
}, 3000);
}
function hideError() {
const existingMessages = document.querySelectorAll('.error-message, .success-message');
existingMessages.forEach(msg => msg.remove());
}
function updateHeaderStats(text) {
document.getElementById('header-stats').innerHTML = `<span class="stat-item">${text}</span>`;
}