3GPPDocFinder / static /script.js
om4r932's picture
Add experimental keyword search
8555b30
// DOM elements
const dynamicDesc = document.getElementById("dynamicDesc");
const dynamicTitle = document.getElementById("dynamicTitle");
const singleModeBtn = document.getElementById('single-mode-btn');
const batchModeBtn = document.getElementById('batch-mode-btn');
const keywordModeBtn = document.getElementById("keyword-mode-btn");
const expModeBtn = document.getElementById("exp-mode-btn");
const singleInputField = document.querySelector('.single-input');
const batchInputField = document.querySelector('.batch-input');
const keywordInputField = document.querySelector(".keyword-input");
const expKeywordInputField = document.querySelector(".experimental-input");
const docIdInput = document.getElementById('doc-id');
const batchIdsInput = document.getElementById('batch-ids');
const keywordInput = document.getElementById("keywords");
const expKeywordInput = document.getElementById("exp-keywords")
const thresholdInput = document.getElementById("threshold");
const releaseFilter = document.querySelector("input[name=release]")
const modeFilter = document.querySelector("select[name=mode]")
const specTypeFilter = document.querySelector("select[name=spec_type]")
const workingGroupFilter = document.querySelector("select[name=working_group]")
const caseSensitiveFilter = document.querySelector("input[name=case_sensitive]")
const searchMode = document.querySelector("select[name=search_mode]")
const releaseFilter2 = document.querySelector("input[name=release2]")
const specTypeFilter2 = document.querySelector("select[name=spec_type2]")
const workingGroupFilter2 = document.querySelector("select[name=working_group2]")
const searchBtn = document.getElementById('search-btn');
const batchSearchBtn = document.getElementById('batch-search-btn');
const keywordSearchBtn = document.getElementById("keyword-search-btn");
const expKeywordSearchBtn = document.getElementById("exp-search-btn");
const loader = document.getElementById('loader');
const resultsContainer = document.getElementById('results-container');
const resultsList = document.getElementById('results-list');
const resultsStats = document.getElementById('results-stats');
const errorMessage = document.getElementById('error-message');
const sectionPopup = document.getElementById('sectionPopup');
const popupTitle = document.getElementById('popupTitle');
const popupTextareas = document.getElementById('popupTextareas');
const copyAllBtn = document.getElementById('copyAllBtn');
const closePopupBtn = document.querySelector('.close-popup');
// Search mode toggle
singleModeBtn.addEventListener('click', () => {
dynamicTitle.textContent = "Find 3GPP Documents";
dynamicDesc.textContent = "Enter a TSG document ID / specification ID (e.g., S1-123456, C2-987654 or 31.102) to locate the document in the 3GPP FTP server.";
singleModeBtn.classList.add('active');
keywordModeBtn.classList.remove("active");
batchModeBtn.classList.remove('active');
expModeBtn.classList.remove('active');
singleInputField.style.display = 'block';
batchInputField.style.display = 'none';
keywordInputField.style.display = "none";
expKeywordInputField.style.display = "none";
});
batchModeBtn.addEventListener('click', () => {
dynamicTitle.textContent = "Find multiple 3GPP Documents";
dynamicDesc.textContent = "Enter a list of TSG document ID / specification ID (e.g., S1-123456, C2-987654 or 31.102) to locate all of the specified documents in the 3GPP FTP server.";
batchModeBtn.classList.add('active');
keywordModeBtn.classList.remove("active");
singleModeBtn.classList.remove('active');
expModeBtn.classList.remove('active');
batchInputField.style.display = 'block';
keywordInputField.style.display = "none";
singleInputField.style.display = 'none';
expKeywordInputField.style.display = "none";
});
keywordModeBtn.addEventListener('click', () => {
dynamicTitle.textContent = "Search 3GPP specifications";
dynamicDesc.textContent = "With keywords and filters, find all of 3GPP's specifications that matches your needs (with keywords, specification number, release or even working group (C1, S5, SP, CP: always the first letter of the group followed by the workgroup number)";
keywordModeBtn.classList.add("active");
singleModeBtn.classList.remove('active');
batchModeBtn.classList.remove("active");
expModeBtn.classList.remove('active');
singleInputField.style.display = "none";
batchInputField.style.display = "none";
expKeywordInputField.style.display = "none";
keywordInputField.style.display = "block";
})
expModeBtn.addEventListener('click', () => {
dynamicTitle.textContent = "[EXPERIMENTAL] Search 3GPP specifications";
dynamicDesc.textContent = "With keywords and filters, find all of 3GPP's specifications that matches your needs (with keywords, specification number, release or even working group (C1, S5, SP, CP: always the first letter of the group followed by the workgroup number)";
keywordModeBtn.classList.remove("active");
singleModeBtn.classList.remove('active');
batchModeBtn.classList.remove("active");
expModeBtn.classList.add('active');
singleInputField.style.display = "none";
batchInputField.style.display = "none";
expKeywordInputField.style.display = "block";
keywordInputField.style.display = "none";
})
document.getElementById('toggleFilters').onclick = function() {
var target = document.getElementById('filtersForm');
target.style.display = (target.style.display === 'none' || target.style.display === '') ? 'flex' : 'none';
};
document.getElementById('toggleFilters2').onclick = function() {
var target = document.getElementById('filtersForm2');
target.style.display = (target.style.display === 'none' || target.style.display === '') ? 'flex' : 'none';
};
expKeywordSearchBtn.addEventListener("click", async ()=>{
let keywords = expKeywordInput.value.trim();
let release = releaseFilter2.value.trim();
let wg = workingGroupFilter2.value.trim();
let specType = specTypeFilter2.value.trim();
let threshold = thresholdInput.value.trim();
if (!keywords){
showError("Please enter at least one keyword");
return;
}
showLoader();
hideError();
try{
let body = {
keywords,
threshold
};
if (release != ""){body["release"] = release}
if (wg != ""){body["working_group"] = wg}
if (specType != ""){body["spec_type"] = specType}
const response = await fetch("/search-spec/experimental", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const data = await response.json();
if (response.ok){
displayKeywordResults(data, "");
} else if (response.status == 404) {
showError('No specification has been found');
} else {
showError(`Error processing keyword request: ${data.detail}`)
}
} catch (error) {
showError('Error connecting to the server. Please check if the API is running.');
console.error('Error:', error);
} finally {
hideLoader();
}
})
keywordSearchBtn.addEventListener("click", async ()=>{
let keywords = keywordInput.value.trim();
let release = releaseFilter.value;
let wg = workingGroupFilter.value;
let specType = specTypeFilter.value;
let search = searchMode.value;
let checked = caseSensitiveFilter.checked;
let mode = modeFilter.value;
if (!keywords && searchMode == "deep") {
showError("Please enter at least one keyword in deep search mode");
return;
}
showLoader();
hideError();
try{
let body = {
keywords,
"search_mode": search,
"case_sensitive": checked,
"mode": mode
};
if (release != ""){body.release = release}
if (wg != ""){body["working_group"] = wg}
if (specType != ""){body["spec_type"] = specType}
const response = await fetch("/search-spec", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const data = await response.json();
if (response.ok){
displayKeywordResults(data, search);
} else if (response.status == 404) {
showError('No specification has been found');
} else {
showError(`Error processing keyword request: ${data.detail}`)
}
} catch (error) {
showError('Error connecting to the server. Please check if the API is running.');
console.error('Error:', error);
} finally {
hideLoader();
}
})
// Single document search
searchBtn.addEventListener('click', async () => {
const docId = docIdInput.value.trim();
if (!docId) {
showError('Please enter a document ID');
return;
}
showLoader();
hideError();
try {
const response = await fetch(`/find`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ doc_id: docId, release: null })
});
const data = await response.json();
if (response.ok) {
displaySingleResult(data);
} else {
displaySingleNotFound(docId, data.detail);
}
} catch (error) {
showError('Error connecting to the server. Please check if the API is running.');
console.error('Error:', error);
} finally {
hideLoader();
}
});
// Batch document search
batchSearchBtn.addEventListener('click', async () => {
const batchText = batchIdsInput.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;
}
showLoader();
hideError();
try {
const response = await fetch(`/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ doc_ids: docIds })
});
const data = await response.json();
if (response.ok) {
displayBatchResults(data);
} else if (response.status == 404) {
showError('No document has been found');
} else {
showError('Error processing batch request')
}
} catch (error) {
showError('Error connecting to the server. Please check if the API is running.');
console.error('Error:', error);
} finally {
hideLoader();
}
});
// Display single result
function displaySingleResult(data) {
resultsList.innerHTML = '';
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
let scopeItem = data.scope ? `<p>Scope : ${data.scope}</p>` : ""
resultItem.innerHTML = `
<div class="result-header">
<div class="result-id">${data.doc_id}</div>
<div class="result-status status-found">Found</div>
</div>
<div class="result-url">
<a href="${data.url}" target="_blank">${data.url}</a>
${scopeItem}
</div>
`;
resultsList.appendChild(resultItem);
resultsStats.textContent = `Found in ${data.search_time.toFixed(2)} seconds`;
resultsContainer.style.display = 'block';
}
// Display single not found result
function displaySingleNotFound(docId, message) {
resultsList.innerHTML = '';
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
resultItem.innerHTML = `
<div class="result-header">
<div class="result-id">${docId}</div>
<div class="result-status status-not-found">Not Found</div>
</div>
<div>${message}</div>
`;
resultsList.appendChild(resultItem);
resultsStats.textContent = 'Document not found';
resultsContainer.style.display = 'block';
}
function displayKeywordResults(data, mode) {
resultsList.innerHTML = '';
data.results.forEach(spec => {
const resultItem = document.createElement("div");
resultItem.className = "result-item";
resultItem.innerHTML = `
<div class="result-header">
<div class="result-id">${spec.id}</div>
<div class="result-status status-found">Found</div>
</div>
<div class="result-url">
<p>Title: ${spec.title}</p>
<p>Type: ${spec.type}</p>
<p>Release: ${spec.release}</p>
<p>Version: ${spec.version}</p>
<p>WG: ${spec.working_group}</p>
<p>URL: <a target="_blank" href="${spec.url}">${spec.url}</a></p>
<p>Scope: ${spec.scope}</p>
</div>
`;
if(mode == "deep"){
resultItem.innerHTML += `
<div class="result-actions">
<button class="get-section-btn btn" data-spec-id="${spec.id}">Get section</button>
</div>
`
}
// Ajouter le bouton au DOM
resultsList.appendChild(resultItem);
// Récupérer le bouton nouvellement créé
if(mode == "deep"){
const button1 = resultItem.querySelector('.get-section-btn');
button1._sections = spec.contains;
}
});
document.querySelectorAll('.get-section-btn').forEach(button => {
button.addEventListener('click', function() {
let specId = this.getAttribute("data-spec-id");
let sections = this._sections;
openSectionPopup(specId, sections);
});
});
resultsStats.textContent = `Found ${data.results.length} in ${data.search_time.toFixed(2)} seconds`
resultsContainer.style.display = 'block';
}
function openSectionPopup(specId, sections) {
const newTab = window.open('', '_blank');
let htmlContent =
`
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Sections of specification number ${specId}</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
</head>
<body>
<div class="popup-header">
<h2 id="popupTitle">Sections of specification number ${specId}</h2>
</div>
<div id="popupTextareas" class="popup-textareas">
`;
Object.entries(sections).forEach(([sectionTitle, content], index) => {
htmlContent +=
`
<div class="textarea-container">
<h2>${sectionTitle}</h2>
<p>${content.replace(/\n/g, '<br>')}</p>
</div>
`;
});
htmlContent += `
</div>
</body>
</html>
`;
newTab.document.open();
newTab.document.write(htmlContent);
newTab.document.close()
// popupTitle.textContent = `Sections of specification ${specId}`;
// popupTextareas.innerHTML = '';
// Object.entries(sections).forEach(([section, content], index) => {
// const container = document.createElement("div");
// container.className = "textarea-container";
// const textarea = document.createElement("textarea");
// textarea.id = `section-${index}`;
// textarea.value = `${section}\n\n${content}`
// textarea.readOnly = true;
// const copyBtn = document.createElement('button');
// copyBtn.className = 'copy-btn';
// copyBtn.textContent = 'Copy';
// copyBtn.onclick = () => copyTextarea(`section-${index}`);
// container.appendChild(textarea);
// container.appendChild(copyBtn);
// popupTextareas.appendChild(container);
// });
// sectionPopup.style.display = 'block';
// document.body.style.overflow = 'hidden';
}
// Display batch results
function displayBatchResults(data) {
resultsList.innerHTML = '';
// Found documents
Object.entries(data.results).forEach(([docId, url]) => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
resultItem.innerHTML = `
<div class="result-header">
<div class="result-id">${docId}</div>
<div class="result-status status-found">Found</div>
</div>
<div class="result-url">
<a href="${url}" target="_blank">${url}</a>
</div>
`;
resultsList.appendChild(resultItem);
});
// Not found documents
data.missing.forEach(docId => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
resultItem.innerHTML = `
<div class="result-header">
<div class="result-id">${docId}</div>
<div class="result-status status-not-found">Not Found</div>
</div>
`;
resultsList.appendChild(resultItem);
});
const foundCount = Object.keys(data.results).length;
const totalCount = foundCount + data.missing.length;
resultsStats.textContent = `Found ${foundCount} of ${totalCount} documents in ${data.search_time.toFixed(2)} seconds`;
resultsContainer.style.display = 'block';
}
// Show loader
function showLoader() {
loader.style.display = 'block';
}
// Hide loader
function hideLoader() {
loader.style.display = 'none';
}
// Show error message
function showError(message) {
resultsList.innerHTML = "";
resultsStats.textContent = `Found 0 documents`;
errorMessage.textContent = message;
errorMessage.style.display = 'block';
}
// Hide error message
function hideError() {
errorMessage.style.display = 'none';
}
// Enter key event for single search
docIdInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchBtn.click();
}
});
keywordInput.addEventListener('keypress', (event)=>{
if (event.key === "Enter"){
keywordSearchBtn.click();
}
})
expKeywordInput.addEventListener('keypress', (event)=>{
if (event.key === "Enter"){
keywordSearchBtn.click();
}
})