|
|
| |
| let currentResults = []; |
| let currentMode = 'single'; |
|
|
| |
| document.addEventListener('DOMContentLoaded', function() { |
| initializeApp(); |
| }); |
|
|
| function initializeApp() { |
| setupTabHandlers(); |
| setupKeyboardHandlers(); |
| updateHeaderStats('Ready'); |
| } |
|
|
| |
| function setupTabHandlers() { |
| document.querySelectorAll('.tab-button').forEach(button => { |
| button.addEventListener('click', function() { |
| const mode = this.dataset.mode; |
| switchMode(mode); |
| }); |
| }); |
| } |
|
|
| function switchMode(mode) { |
| currentMode = mode; |
| |
| |
| document.querySelectorAll('.tab-button').forEach(btn => { |
| btn.classList.remove('active'); |
| }); |
| document.querySelector(`[data-mode="${mode}"]`).classList.add('active'); |
| |
| |
| document.querySelectorAll('.search-form').forEach(form => { |
| form.classList.remove('active'); |
| }); |
| document.getElementById(`${mode}-form`).classList.add('active'); |
| |
| |
| hideResults(); |
| } |
|
|
| |
| 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'; |
| } |
|
|
| |
| 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(); |
| }); |
| } |
|
|
| |
| 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(); |
| } |
| } |
|
|
| |
| 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 = ''; |
| |
| |
| 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> |
| `; |
| }); |
| |
| |
| 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(); |
| } |
|
|
| |
| 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>`; |
| } |