Update keyword search: Add 'Get section' and 'Get document' functions
Browse files- app.py +1 -0
- static/script.js +137 -5
- static/style.css +124 -0
- templates/index.html +16 -2
app.py
CHANGED
@@ -495,6 +495,7 @@ def search_spec(request: KeywordRequest):
|
|
495 |
|
496 |
if put:
|
497 |
spec_content = spec
|
|
|
498 |
spec_content["contains"] = {chap: doc[chap] for chap in contents}
|
499 |
|
500 |
results.append(spec_content)
|
|
|
495 |
|
496 |
if put:
|
497 |
spec_content = spec
|
498 |
+
spec_content["full_doc"] = doc
|
499 |
spec_content["contains"] = {chap: doc[chap] for chap in contents}
|
500 |
|
501 |
results.append(spec_content)
|
static/script.js
CHANGED
@@ -30,6 +30,12 @@ const resultsList = document.getElementById('results-list');
|
|
30 |
const resultsStats = document.getElementById('results-stats');
|
31 |
const errorMessage = document.getElementById('error-message');
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
// Search mode toggle
|
34 |
singleModeBtn.addEventListener('click', () => {
|
35 |
dynamicTitle.textContent = "Find 3GPP Documents";
|
@@ -86,6 +92,14 @@ keywordSearchBtn.addEventListener("click", async ()=>{
|
|
86 |
hideError();
|
87 |
|
88 |
try{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
const response = await fetch("/search-spec", {
|
90 |
method: "POST",
|
91 |
headers: {
|
@@ -94,10 +108,7 @@ keywordSearchBtn.addEventListener("click", async ()=>{
|
|
94 |
body: JSON.stringify({
|
95 |
keywords,
|
96 |
"case_sensitive": caseSensitiveFilter.checked,
|
97 |
-
"
|
98 |
-
"mode": modeFilter.value,
|
99 |
-
"working_group": workingGroupFilter != '' ? workingGroupFilter.value : null,
|
100 |
-
"spec_type": specTypeFilter.value
|
101 |
})
|
102 |
});
|
103 |
|
@@ -242,7 +253,8 @@ function displayKeywordResults(data) {
|
|
242 |
|
243 |
data.results.forEach(spec => {
|
244 |
const resultItem = document.createElement("div");
|
245 |
-
resultItem.className = "result-item"
|
|
|
246 |
resultItem.innerHTML = `
|
247 |
<div class="result-header">
|
248 |
<div class="result-id">${spec.id}</div>
|
@@ -257,13 +269,133 @@ function displayKeywordResults(data) {
|
|
257 |
<p>URL: <a target="_blank" href="${spec.url}">${spec.url}</a></p>
|
258 |
<p>Scope: ${spec.scope}</p>
|
259 |
</div>
|
|
|
|
|
|
|
|
|
260 |
`;
|
|
|
|
|
261 |
resultsList.appendChild(resultItem);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
262 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
263 |
resultsStats.textContent = `Found ${data.results.length} in ${data.search_time.toFixed(2)} seconds`
|
264 |
resultsContainer.style.display = 'block';
|
265 |
}
|
266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
267 |
// Display batch results
|
268 |
function displayBatchResults(data) {
|
269 |
resultsList.innerHTML = '';
|
|
|
30 |
const resultsStats = document.getElementById('results-stats');
|
31 |
const errorMessage = document.getElementById('error-message');
|
32 |
|
33 |
+
const sectionPopup = document.getElementById('sectionPopup');
|
34 |
+
const popupTitle = document.getElementById('popupTitle');
|
35 |
+
const popupTextareas = document.getElementById('popupTextareas');
|
36 |
+
const copyAllBtn = document.getElementById('copyAllBtn');
|
37 |
+
const closePopupBtn = document.querySelector('.close-popup');
|
38 |
+
|
39 |
// Search mode toggle
|
40 |
singleModeBtn.addEventListener('click', () => {
|
41 |
dynamicTitle.textContent = "Find 3GPP Documents";
|
|
|
92 |
hideError();
|
93 |
|
94 |
try{
|
95 |
+
let body = {
|
96 |
+
keywords,
|
97 |
+
"case_sensitive": caseSensitiveFilter.checked,
|
98 |
+
"mode": modeFilter.value
|
99 |
+
};
|
100 |
+
if (releaseFilter.value != ""){body.release = releaseFilter.value}
|
101 |
+
if (workingGroupFilter.value != ""){body["working_group"] = workingGroupFilter.value}
|
102 |
+
if (specTypeFilter.value != ""){body["spec_type"] = specTypeFilter.value}
|
103 |
const response = await fetch("/search-spec", {
|
104 |
method: "POST",
|
105 |
headers: {
|
|
|
108 |
body: JSON.stringify({
|
109 |
keywords,
|
110 |
"case_sensitive": caseSensitiveFilter.checked,
|
111 |
+
"mode": modeFilter.value
|
|
|
|
|
|
|
112 |
})
|
113 |
});
|
114 |
|
|
|
253 |
|
254 |
data.results.forEach(spec => {
|
255 |
const resultItem = document.createElement("div");
|
256 |
+
resultItem.className = "result-item";
|
257 |
+
|
258 |
resultItem.innerHTML = `
|
259 |
<div class="result-header">
|
260 |
<div class="result-id">${spec.id}</div>
|
|
|
269 |
<p>URL: <a target="_blank" href="${spec.url}">${spec.url}</a></p>
|
270 |
<p>Scope: ${spec.scope}</p>
|
271 |
</div>
|
272 |
+
<div class="result-actions">
|
273 |
+
<button class="get-section-btn btn" data-spec-id="${spec.id}">Get section</button>
|
274 |
+
<button class="get-all-text-btn btn" data-spec-id="${spec.id}">Get document content</button>
|
275 |
+
</div>
|
276 |
`;
|
277 |
+
|
278 |
+
// Ajouter le bouton au DOM
|
279 |
resultsList.appendChild(resultItem);
|
280 |
+
|
281 |
+
// Récupérer le bouton nouvellement créé
|
282 |
+
const button1 = resultItem.querySelector('.get-section-btn');
|
283 |
+
const button2 = resultItem.querySelector('.get-all-text-btn');
|
284 |
+
|
285 |
+
// Stocker l'objet directement sur l'élément DOM
|
286 |
+
button1._sections = spec.contains;
|
287 |
+
button2._sections = spec.full_doc;
|
288 |
});
|
289 |
+
|
290 |
+
document.querySelectorAll('.get-section-btn').forEach(button => {
|
291 |
+
button.addEventListener('click', function() {
|
292 |
+
let specId = this.getAttribute("data-spec-id");
|
293 |
+
let sections = this._sections;
|
294 |
+
openSectionPopup(specId, sections);
|
295 |
+
});
|
296 |
+
});
|
297 |
+
|
298 |
+
document.querySelectorAll('.get-all-text-btn').forEach(button => {
|
299 |
+
button.addEventListener('click', function() {
|
300 |
+
let specId = this.getAttribute("data-spec-id");
|
301 |
+
let sections = this._sections;
|
302 |
+
openSectionPopup(specId, sections);
|
303 |
+
})
|
304 |
+
});
|
305 |
+
|
306 |
resultsStats.textContent = `Found ${data.results.length} in ${data.search_time.toFixed(2)} seconds`
|
307 |
resultsContainer.style.display = 'block';
|
308 |
}
|
309 |
|
310 |
+
function openSectionPopup(specId, sections) {
|
311 |
+
popupTitle.textContent = `Sections of specification ${specId}`;
|
312 |
+
|
313 |
+
popupTextareas.innerHTML = '';
|
314 |
+
Object.entries(sections).forEach(([section, content], index) => {
|
315 |
+
const container = document.createElement("div");
|
316 |
+
container.className = "textarea-container";
|
317 |
+
|
318 |
+
const textarea = document.createElement("textarea");
|
319 |
+
textarea.id = `section-${index}`;
|
320 |
+
textarea.value = `${section}\n\n${content}`
|
321 |
+
textarea.readOnly = true;
|
322 |
+
|
323 |
+
const copyBtn = document.createElement('button');
|
324 |
+
copyBtn.className = 'copy-btn';
|
325 |
+
copyBtn.textContent = 'Copy';
|
326 |
+
copyBtn.onclick = () => copyTextarea(`section-${index}`);
|
327 |
+
|
328 |
+
container.appendChild(textarea);
|
329 |
+
container.appendChild(copyBtn);
|
330 |
+
popupTextareas.appendChild(container);
|
331 |
+
});
|
332 |
+
|
333 |
+
sectionPopup.style.display = 'block';
|
334 |
+
document.body.style.overflow = 'hidden';
|
335 |
+
}
|
336 |
+
|
337 |
+
function copyTextarea(id) {
|
338 |
+
const textarea = document.getElementById(id);
|
339 |
+
textarea.select();
|
340 |
+
document.execCommand("copy");
|
341 |
+
|
342 |
+
// Effet visuel pour confirmer la copie
|
343 |
+
const btn = textarea.nextElementSibling;
|
344 |
+
const originalText = btn.textContent;
|
345 |
+
btn.textContent = 'Copied !';
|
346 |
+
btn.style.backgroundColor = '#34a853';
|
347 |
+
btn.style.color = 'white';
|
348 |
+
|
349 |
+
setTimeout(() => {
|
350 |
+
btn.textContent = originalText;
|
351 |
+
btn.style.backgroundColor = '';
|
352 |
+
btn.style.color = '';
|
353 |
+
}, 1500);
|
354 |
+
}
|
355 |
+
|
356 |
+
// Fonction pour copier tout le contenu
|
357 |
+
copyAllBtn.addEventListener('click', () => {
|
358 |
+
const textareas = popupTextareas.querySelectorAll('textarea');
|
359 |
+
let allContent = '';
|
360 |
+
|
361 |
+
textareas.forEach((textarea, index) => {
|
362 |
+
allContent += textarea.value;
|
363 |
+
if (index < textareas.length - 1) {
|
364 |
+
allContent += '\n\n---\n\n';
|
365 |
+
}
|
366 |
+
});
|
367 |
+
|
368 |
+
// Créer un textarea temporaire pour copier le contenu
|
369 |
+
const tempTextarea = document.createElement('textarea');
|
370 |
+
tempTextarea.value = allContent;
|
371 |
+
document.body.appendChild(tempTextarea);
|
372 |
+
tempTextarea.select();
|
373 |
+
document.execCommand('copy');
|
374 |
+
document.body.removeChild(tempTextarea);
|
375 |
+
|
376 |
+
// Effet visuel pour confirmer la copie
|
377 |
+
const originalText = copyAllBtn.textContent;
|
378 |
+
copyAllBtn.textContent = 'Copied all !';
|
379 |
+
|
380 |
+
setTimeout(() => {
|
381 |
+
copyAllBtn.textContent = originalText;
|
382 |
+
}, 1500);
|
383 |
+
});
|
384 |
+
|
385 |
+
// Fermer la popup
|
386 |
+
closePopupBtn.addEventListener('click', () => {
|
387 |
+
sectionPopup.style.display = 'none';
|
388 |
+
document.body.style.overflow = ''; // Rétablir le défilement du body
|
389 |
+
});
|
390 |
+
|
391 |
+
// Fermer la popup en cliquant à l'extérieur
|
392 |
+
window.addEventListener('click', (event) => {
|
393 |
+
if (event.target === sectionPopup) {
|
394 |
+
sectionPopup.style.display = 'none';
|
395 |
+
document.body.style.overflow = '';
|
396 |
+
}
|
397 |
+
});
|
398 |
+
|
399 |
// Display batch results
|
400 |
function displayBatchResults(data) {
|
401 |
resultsList.innerHTML = '';
|
static/style.css
CHANGED
@@ -10,6 +10,130 @@
|
|
10 |
--shadow-color: rgba(0, 0, 0, 0.1);
|
11 |
}
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
.filter-tab-container {
|
14 |
margin-top: 20px;
|
15 |
margin-bottom: 15px;
|
|
|
10 |
--shadow-color: rgba(0, 0, 0, 0.1);
|
11 |
}
|
12 |
|
13 |
+
.section-popup {
|
14 |
+
display: none;
|
15 |
+
position: fixed;
|
16 |
+
top: 0;
|
17 |
+
left: 0;
|
18 |
+
width: 100%;
|
19 |
+
height: 100%;
|
20 |
+
background-color: rgba(0, 0, 0, 0.5);
|
21 |
+
z-index: 1000;
|
22 |
+
overflow: auto;
|
23 |
+
}
|
24 |
+
|
25 |
+
.section-popup-content {
|
26 |
+
position: relative;
|
27 |
+
background-color: white;
|
28 |
+
width: 80%;
|
29 |
+
max-width: 800px;
|
30 |
+
margin: 50px auto;
|
31 |
+
padding: 30px;
|
32 |
+
border-radius: 8px;
|
33 |
+
box-shadow: 0 4px 15px var(--shadow-color);
|
34 |
+
animation: popupFadeIn 0.3s;
|
35 |
+
}
|
36 |
+
|
37 |
+
@keyframes popupFadeIn {
|
38 |
+
from { opacity: 0; transform: translateY(-20px); }
|
39 |
+
to { opacity: 1; transform: translateY(0); }
|
40 |
+
}
|
41 |
+
|
42 |
+
.popup-header {
|
43 |
+
display: flex;
|
44 |
+
justify-content: space-between;
|
45 |
+
align-items: center;
|
46 |
+
margin-bottom: 20px;
|
47 |
+
padding-bottom: 15px;
|
48 |
+
border-bottom: 1px solid var(--border-color);
|
49 |
+
}
|
50 |
+
|
51 |
+
.popup-header h2 {
|
52 |
+
font-size: 22px;
|
53 |
+
font-weight: 500;
|
54 |
+
color: var(--text-color);
|
55 |
+
}
|
56 |
+
|
57 |
+
.close-popup {
|
58 |
+
font-size: 28px;
|
59 |
+
font-weight: bold;
|
60 |
+
color: var(--light-text);
|
61 |
+
cursor: pointer;
|
62 |
+
transition: color 0.3s;
|
63 |
+
}
|
64 |
+
|
65 |
+
.close-popup:hover {
|
66 |
+
color: var(--primary-color);
|
67 |
+
}
|
68 |
+
|
69 |
+
.popup-textareas {
|
70 |
+
display: flex;
|
71 |
+
flex-direction: column;
|
72 |
+
gap: 20px;
|
73 |
+
max-height: 60vh;
|
74 |
+
overflow-y: auto;
|
75 |
+
padding-right: 10px;
|
76 |
+
}
|
77 |
+
|
78 |
+
.textarea-container {
|
79 |
+
display: flex;
|
80 |
+
flex-direction: column;
|
81 |
+
gap: 8px;
|
82 |
+
}
|
83 |
+
|
84 |
+
.textarea-container textarea {
|
85 |
+
width: 100%;
|
86 |
+
height: 100px;
|
87 |
+
padding: 12px 16px;
|
88 |
+
border: 1px solid var(--border-color);
|
89 |
+
border-radius: 4px;
|
90 |
+
font-size: 16px;
|
91 |
+
font-family: 'Roboto', sans-serif;
|
92 |
+
resize: vertical;
|
93 |
+
outline: none;
|
94 |
+
}
|
95 |
+
|
96 |
+
.textarea-container textarea:focus {
|
97 |
+
border-color: var(--primary-color);
|
98 |
+
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
|
99 |
+
}
|
100 |
+
|
101 |
+
.copy-btn {
|
102 |
+
align-self: flex-end;
|
103 |
+
background-color: var(--secondary-color);
|
104 |
+
color: var(--primary-color);
|
105 |
+
border: 1px solid var(--border-color);
|
106 |
+
border-radius: 4px;
|
107 |
+
padding: 8px 16px;
|
108 |
+
font-size: 14px;
|
109 |
+
font-weight: 500;
|
110 |
+
cursor: pointer;
|
111 |
+
transition: background-color 0.3s;
|
112 |
+
}
|
113 |
+
|
114 |
+
.copy-btn:hover {
|
115 |
+
background-color: #e8f0fe;
|
116 |
+
}
|
117 |
+
|
118 |
+
.copy-all-btn {
|
119 |
+
display: block;
|
120 |
+
width: 100%;
|
121 |
+
background-color: var(--primary-color);
|
122 |
+
color: white;
|
123 |
+
border: none;
|
124 |
+
border-radius: 4px;
|
125 |
+
padding: 12px 24px;
|
126 |
+
font-size: 16px;
|
127 |
+
font-weight: 500;
|
128 |
+
cursor: pointer;
|
129 |
+
transition: background-color 0.3s;
|
130 |
+
margin-top: 20px;
|
131 |
+
}
|
132 |
+
|
133 |
+
.copy-all-btn:hover {
|
134 |
+
background-color: var(--accent-color);
|
135 |
+
}
|
136 |
+
|
137 |
.filter-tab-container {
|
138 |
margin-top: 20px;
|
139 |
margin-bottom: 15px;
|
templates/index.html
CHANGED
@@ -73,8 +73,9 @@
|
|
73 |
</select>
|
74 |
|
75 |
<select name="spec_type" class="filter-select">
|
76 |
-
<option value="
|
77 |
-
<option value="
|
|
|
78 |
</select>
|
79 |
|
80 |
<select name="working_group" class="filter-select">
|
@@ -119,6 +120,19 @@
|
|
119 |
</div>
|
120 |
<div class="results-list" id="results-list"></div>
|
121 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
</div>
|
123 |
|
124 |
<footer>
|
|
|
73 |
</select>
|
74 |
|
75 |
<select name="spec_type" class="filter-select">
|
76 |
+
<option value="">All types</option>
|
77 |
+
<option value="TR">Technical Report (TR)</option>
|
78 |
+
<option value="TS">Technical Specification (TS)</option>
|
79 |
</select>
|
80 |
|
81 |
<select name="working_group" class="filter-select">
|
|
|
120 |
</div>
|
121 |
<div class="results-list" id="results-list"></div>
|
122 |
</div>
|
123 |
+
|
124 |
+
<div id="sectionPopup" class="section-popup">
|
125 |
+
<div class="section-popup-content">
|
126 |
+
<div class="popup-header">
|
127 |
+
<h2 id="popupTitle">Document Sections</h2>
|
128 |
+
<span class="close-popup">×</span>
|
129 |
+
</div>
|
130 |
+
<div id="popupTextareas" class="popup-textareas">
|
131 |
+
<!-- Les textareas seront générés ici dynamiquement -->
|
132 |
+
</div>
|
133 |
+
<button id="copyAllBtn" class="copy-all-btn">Copy all</button>
|
134 |
+
</div>
|
135 |
+
</div>
|
136 |
</div>
|
137 |
|
138 |
<footer>
|