om4r932 commited on
Commit
ec64056
·
1 Parent(s): 7aa5e73

V2 (Add keyword search)

Browse files
Files changed (6) hide show
  1. app.py +178 -101
  2. indexed_docs.json +0 -6
  3. schemas.py +39 -0
  4. static/script.js +275 -93
  5. static/style.css +182 -58
  6. templates/index.html +63 -5
app.py CHANGED
@@ -1,96 +1,61 @@
1
- from io import StringIO
2
- from dotenv import load_dotenv
3
- import numpy as np
4
- import pandas as pd
5
- import requests
6
- from bs4 import BeautifulSoup
7
- import json
8
- import os
9
- import traceback
10
- import uuid
11
- import zipfile
12
- import io
13
- import subprocess
14
- import os
15
- import re
16
  import time
17
  from datetime import datetime
18
- import warnings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  from fastapi import FastAPI, HTTPException
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from fastapi.responses import FileResponse
22
  from fastapi.staticfiles import StaticFiles
23
- from pydantic import BaseModel
24
- from typing import Any, Dict, List, Literal, Optional
25
-
26
- load_dotenv()
27
 
28
- warnings.filterwarnings("ignore")
29
 
30
- app = FastAPI(title="ETSI Document Finder API",
31
- description="API to find ETSI documents based on document IDs")
 
32
 
33
- app.mount("/static", StaticFiles(directory="static"), name="static")
 
34
 
35
- origins = [
36
- "*",
37
- ]
 
 
 
38
 
 
 
39
  app.add_middleware(
40
  CORSMiddleware,
41
- allow_origins=origins,
42
  allow_credentials=True,
43
  allow_methods=["*"],
44
  allow_headers=["*"],
45
  )
46
 
47
- class DocRequest(BaseModel):
48
- doc_id: str
49
-
50
- class DocResponse(BaseModel):
51
- doc_id: str
52
- url: str
53
- release: Optional[str] = None
54
- scope: Optional[str] = None
55
- search_time: float
56
-
57
- class MultiDocResponse(BaseModel):
58
- doc_id: str
59
- results: List[DocResponse]
60
- search_time: float
61
-
62
- class BatchDocRequest(BaseModel):
63
- doc_ids: List[str]
64
- release: Optional[int] = None
65
-
66
- class BatchDocResponse(BaseModel):
67
- results: Dict[str, str]
68
- missing: List[str]
69
- search_time: float
70
-
71
  class DocFinder:
72
  def __init__(self):
73
  self.main_ftp_url = "https://docbox.etsi.org/SET"
74
  self.session = requests.Session()
75
- self.indexer_file = "indexed_docs.json"
76
- self.indexer, self.last_indexer_date = self.load_indexer()
77
  req = self.session.post("https://portal.etsi.org/ETSIPages/LoginEOL.ashx", verify=False, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"}, data=json.dumps({"username": os.environ.get("EOL_USER"), "password": os.environ.get("EOL_PASSWORD")}))
78
  print(req.content, req.status_code)
79
 
80
- def load_indexer(self):
81
- if os.path.exists(self.indexer_file):
82
- with open(self.indexer_file, "r", encoding="utf-8") as f:
83
- x = json.load(f)
84
- return x["docs"], x["last_indexed_date"]
85
- return {}, None
86
-
87
- def save_indexer(self):
88
- today = datetime.today()
89
- self.last_indexer_date = today.strftime("%d/%m/%Y-%H:%M:%S")
90
- with open(self.indexer_file, "w", encoding="utf-8") as f:
91
- output = {"docs": self.indexer, "last_indexed_date": self.last_indexer_date}
92
- json.dump(output, f, indent=4, ensure_ascii=False)
93
-
94
  def get_workgroup(self, doc: str):
95
  main_tsg = "SET-WG-R" if any(doc.startswith(kw) for kw in ["SETREQ", "SCPREQ"]) else "SET-WG-T" if any(doc.startswith(kw) for kw in ["SETTEC", "SCPTEC"]) else "SET" if any(doc.startswith(kw) for kw in ["SET", "SCP"]) else None
96
  if main_tsg is None:
@@ -120,12 +85,6 @@ class DocFinder:
120
 
121
  def search_document(self, doc_id: str):
122
  original = doc_id
123
-
124
- if original in self.indexer:
125
- return self.indexer[original]
126
- for doc in self.indexer:
127
- if doc.startswith(original):
128
- return self.indexer[doc]
129
 
130
  main_tsg, workgroup, doc = self.get_workgroup(doc_id)
131
  urls = []
@@ -139,34 +98,14 @@ class DocFinder:
139
  if doc in f.lower() or original in f:
140
  print(f)
141
  doc_url = f"{wg_url}/{f}"
142
-
143
- self.indexer[original] = doc_url
144
- self.save_indexer()
145
  urls.append(doc_url)
146
  return urls[0] if len(urls) == 1 else urls[-2] if len(urls) > 1 else f"Document {doc_id} not found"
147
 
148
  class SpecFinder:
149
  def __init__(self):
150
  self.main_url = "https://www.etsi.org/deliver/etsi_ts"
151
- self.indexer_file = "indexed_specifications.json"
152
- self.indexer, self.last_indexer_date = self.load_indexer()
153
  self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"}
154
 
155
-
156
- def load_indexer(self):
157
- if os.path.exists(self.indexer_file):
158
- with open(self.indexer_file, "r", encoding="utf-8") as f:
159
- x = json.load(f)
160
- return x["specs"], x["last_indexed_date"]
161
- return {}, None
162
-
163
- def save_indexer(self):
164
- today = datetime.today()
165
- self.last_indexer_date = today.strftime("%d/%m/%Y-%H:%M:%S")
166
- with open(self.indexer_file, "w", encoding="utf-8") as f:
167
- output = {"specs": self.indexer, "last_indexed_date": self.last_indexer_date}
168
- json.dump(output, f, indent=4, ensure_ascii=False)
169
-
170
  def get_spec_path(self, doc_id: str):
171
  if "-" in doc_id:
172
  position, part = doc_id.split("-")
@@ -194,12 +133,6 @@ class SpecFinder:
194
  # Example : 103 666[-2 opt]
195
  original = doc_id
196
 
197
- if original in self.indexer:
198
- return self.indexer[original]
199
- for doc in self.indexer:
200
- if doc.startswith(original):
201
- return self.indexer[doc]
202
-
203
  url = f"{self.main_url}/{self.get_spec_path(original)}/"
204
  print(url)
205
 
@@ -253,4 +186,148 @@ def find_documents_batch(request: BatchDocRequest):
253
  results=results,
254
  missing=missing,
255
  search_time=time.time() - start_time
256
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import time
2
  from datetime import datetime
3
+ import os, warnings, nltk, json, re
4
+ import numpy as np
5
+ from nltk.stem import WordNetLemmatizer
6
+ from dotenv import load_dotenv
7
+ from sklearn.preprocessing import MinMaxScaler
8
+
9
+ os.environ['CURL_CA_BUNDLE'] = ""
10
+ warnings.filterwarnings('ignore')
11
+ nltk.download('wordnet')
12
+ load_dotenv()
13
+
14
+ from datasets import load_dataset
15
+ import bm25s
16
+ from bm25s.hf import BM25HF
17
+
18
  from fastapi import FastAPI, HTTPException
19
  from fastapi.middleware.cors import CORSMiddleware
20
  from fastapi.responses import FileResponse
21
  from fastapi.staticfiles import StaticFiles
22
+ from schemas import *
23
+ from bs4 import BeautifulSoup
24
+ import requests
 
25
 
26
+ lemmatizer = WordNetLemmatizer()
27
 
28
+ spec_metadatas = load_dataset("OrganizedProgrammers/ETSISpecMetadata", token=os.environ["HF_TOKEN"])
29
+ spec_contents = load_dataset("OrganizedProgrammers/ETSISpecContent", token=os.environ["HF_TOKEN"])
30
+ bm25_index = BM25HF.load_from_hub("OrganizedProgrammers/ETSIBM25IndexSingle", load_corpus=True, token=os.environ["HF_TOKEN"])
31
 
32
+ spec_metadatas = spec_metadatas["train"].to_list()
33
+ spec_contents = spec_contents["train"].to_list()
34
 
35
+ def get_document(spec_id: str, spec_title: Optional[str]):
36
+ text = [f"{spec_id} - {spec_title}" if spec_title else f"{spec_id}"]
37
+ for section in spec_contents:
38
+ if spec_id == section["doc_id"]:
39
+ text.extend([section['section'], section['content']])
40
+ return text
41
 
42
+ app = FastAPI(title="3GPP Document Finder Back-End", description="Backend for 3GPPDocFinder - Searching technical documents & specifications from 3GPP FTP server")
43
+ app.mount("/static", StaticFiles(directory="static"), name="static")
44
  app.add_middleware(
45
  CORSMiddleware,
46
+ allow_origins=["*"],
47
  allow_credentials=True,
48
  allow_methods=["*"],
49
  allow_headers=["*"],
50
  )
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  class DocFinder:
53
  def __init__(self):
54
  self.main_ftp_url = "https://docbox.etsi.org/SET"
55
  self.session = requests.Session()
 
 
56
  req = self.session.post("https://portal.etsi.org/ETSIPages/LoginEOL.ashx", verify=False, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"}, data=json.dumps({"username": os.environ.get("EOL_USER"), "password": os.environ.get("EOL_PASSWORD")}))
57
  print(req.content, req.status_code)
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  def get_workgroup(self, doc: str):
60
  main_tsg = "SET-WG-R" if any(doc.startswith(kw) for kw in ["SETREQ", "SCPREQ"]) else "SET-WG-T" if any(doc.startswith(kw) for kw in ["SETTEC", "SCPTEC"]) else "SET" if any(doc.startswith(kw) for kw in ["SET", "SCP"]) else None
61
  if main_tsg is None:
 
85
 
86
  def search_document(self, doc_id: str):
87
  original = doc_id
 
 
 
 
 
 
88
 
89
  main_tsg, workgroup, doc = self.get_workgroup(doc_id)
90
  urls = []
 
98
  if doc in f.lower() or original in f:
99
  print(f)
100
  doc_url = f"{wg_url}/{f}"
 
 
 
101
  urls.append(doc_url)
102
  return urls[0] if len(urls) == 1 else urls[-2] if len(urls) > 1 else f"Document {doc_id} not found"
103
 
104
  class SpecFinder:
105
  def __init__(self):
106
  self.main_url = "https://www.etsi.org/deliver/etsi_ts"
 
 
107
  self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"}
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  def get_spec_path(self, doc_id: str):
110
  if "-" in doc_id:
111
  position, part = doc_id.split("-")
 
133
  # Example : 103 666[-2 opt]
134
  original = doc_id
135
 
 
 
 
 
 
 
136
  url = f"{self.main_url}/{self.get_spec_path(original)}/"
137
  print(url)
138
 
 
186
  results=results,
187
  missing=missing,
188
  search_time=time.time() - start_time
189
+ )
190
+
191
+
192
+ @app.post("/search-spec", response_model=KeywordResponse)
193
+ def search_specification_by_keywords(request: KeywordRequest):
194
+ start_time = time.time()
195
+ boolSensitiveCase = request.case_sensitive
196
+ search_mode = request.search_mode
197
+ spec_type = request.spec_type
198
+ keywords = [string.lower() if boolSensitiveCase else string for string in request.keywords.split(",")]
199
+ print(keywords)
200
+ unique_specs = set()
201
+ results = []
202
+
203
+ if keywords == [""] and search_mode == "deep":
204
+ raise HTTPException(status_code=400, detail="You must enter keywords in deep search mode !")
205
+
206
+ for spec in spec_metadatas:
207
+ valid = False
208
+ if spec['id'] in unique_specs: continue
209
+ if spec.get('type', None) is None or (spec_type is not None and spec["type"] != spec_type): continue
210
+
211
+ if search_mode == "deep":
212
+ contents = []
213
+ doc = get_document(spec["id"], spec["title"])
214
+ docValid = len(doc) > 1
215
+
216
+ if request.mode == "and":
217
+ string = f"{spec['id']}+-+{spec['title']}+-+{spec['type']}+-+{spec['version']}"
218
+ if all(keyword in (string.lower() if boolSensitiveCase else string) for keyword in keywords):
219
+ valid = True
220
+ if search_mode == "deep":
221
+ if docValid:
222
+ for x in range(1, len(doc) - 1, 2):
223
+ section_title = doc[x]
224
+ section_content = doc[x+1]
225
+ if "reference" not in section_title.lower() and "void" not in section_title.lower() and "annex" not in section_content.lower():
226
+ if all(keyword in (section_content.lower() if boolSensitiveCase else section_content) for keyword in keywords):
227
+ valid = True
228
+ contents.append({section_title: section_content})
229
+ elif request.mode == "or":
230
+ string = f"{spec['id']}+-+{spec['title']}+-+{spec['type']}+-+{spec['version']}"
231
+ if any(keyword in (string.lower() if boolSensitiveCase else string) for keyword in keywords):
232
+ valid = True
233
+ if search_mode == "deep":
234
+ if docValid:
235
+ for x in range(1, len(doc) - 1, 2):
236
+ section_title = doc[x]
237
+ section_content = doc[x+1]
238
+ if "reference" not in section_title.lower() and "void" not in section_title.lower() and "annex" not in section_content.lower():
239
+ if any(keyword in (section_content.lower() if boolSensitiveCase else section_content) for keyword in keywords):
240
+ valid = True
241
+ contents.append({section_title: section_content})
242
+ if valid:
243
+ spec_content = spec
244
+ if search_mode == "deep":
245
+ spec_content["contains"] = {k: v for d in contents for k, v in d.items()}
246
+ results.append(spec_content)
247
+ else:
248
+ unique_specs.add(spec['id'])
249
+
250
+ if len(results) > 0:
251
+ return KeywordResponse(
252
+ results=results,
253
+ search_time=time.time() - start_time
254
+ )
255
+ else:
256
+ raise HTTPException(status_code=404, detail="Specifications not found")
257
+
258
+ @app.post("/search-spec/experimental", response_model=KeywordResponse)
259
+ def bm25_search_specification(request: BM25KeywordRequest):
260
+ start_time = time.time()
261
+ spec_type = request.spec_type
262
+ threshold = request.threshold
263
+ query = request.keywords
264
+
265
+ results_out = []
266
+ query_tokens = bm25s.tokenize(query)
267
+ results, scores = bm25_index.retrieve(query_tokens, k=len(bm25_index.corpus))
268
+ print("BM25 raw scores:", scores)
269
+
270
+ def calculate_boosted_score(metadata, score, query):
271
+ title = set(metadata['title'].lower().split())
272
+ q = set(query.lower().split())
273
+ spec_id_presence = 0.5 if metadata['id'].lower() in q else 0
274
+ booster = len(q & title) * 0.5
275
+ return score + spec_id_presence + booster
276
+
277
+ spec_scores = {}
278
+ spec_indices = {}
279
+ spec_details = {}
280
+
281
+ for i in range(results.shape[1]):
282
+ doc = results[0, i]
283
+ score = scores[0, i]
284
+ spec = doc["metadata"]["id"]
285
+
286
+ boosted_score = calculate_boosted_score(doc['metadata'], score, query)
287
+
288
+ if spec not in spec_scores or boosted_score > spec_scores[spec]:
289
+ spec_scores[spec] = boosted_score
290
+ spec_indices[spec] = i
291
+ spec_details[spec] = {
292
+ 'original_score': score,
293
+ 'boosted_score': boosted_score,
294
+ 'doc': doc
295
+ }
296
+
297
+ def normalize_scores(scores_dict):
298
+ if not scores_dict:
299
+ return {}
300
+
301
+ scores_array = np.array(list(scores_dict.values())).reshape(-1, 1)
302
+ scaler = MinMaxScaler()
303
+ normalized_scores = scaler.fit_transform(scores_array).flatten()
304
+
305
+ normalized_dict = {}
306
+ for i, spec in enumerate(scores_dict.keys()):
307
+ normalized_dict[spec] = normalized_scores[i]
308
+
309
+ return normalized_dict
310
+
311
+ normalized_scores = normalize_scores(spec_scores)
312
+
313
+ for spec in spec_details:
314
+ spec_details[spec]["normalized_score"] = normalized_scores[spec]
315
+
316
+ unique_specs = sorted(normalized_scores.keys(), key=lambda x: normalized_scores[x], reverse=True)
317
+
318
+ for rank, spec in enumerate(unique_specs, 1):
319
+ details = spec_details[spec]
320
+ metadata = details['doc']['metadata']
321
+ if metadata.get('type', None) is None or (spec_type is not None and metadata["type"] != spec_type):
322
+ continue
323
+ if details['normalized_score'] < threshold / 100:
324
+ break
325
+ results_out.append(metadata)
326
+
327
+ if len(results_out) > 0:
328
+ return KeywordResponse(
329
+ results=results_out,
330
+ search_time=time.time() - start_time
331
+ )
332
+ else:
333
+ raise HTTPException(status_code=404, detail="Specifications not found")
indexed_docs.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "docs": {
3
- "SET(25)000002": "https://docbox.etsi.org/SET/SET/05-CONTRIBUTIONS/2025/SET(25)000002_Draft_report_of_SET__116.docx"
4
- },
5
- "last_indexed_date": "03/06/2025-14:20:45"
6
- }
 
 
 
 
 
 
 
schemas.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import *
3
+
4
+ class DocRequest(BaseModel):
5
+ doc_id: str
6
+
7
+ class DocResponse(BaseModel):
8
+ doc_id: str
9
+ url: str
10
+ release: Optional[str] = None
11
+ scope: Optional[str] = None
12
+ search_time: float
13
+
14
+ class BatchDocRequest(BaseModel):
15
+ doc_ids: List[str]
16
+ release: Optional[int] = None
17
+
18
+ class BatchDocResponse(BaseModel):
19
+ results: Dict[str, str]
20
+ missing: List[str]
21
+ search_time: float
22
+
23
+ class BM25KeywordRequest(BaseModel):
24
+ keywords: Optional[str] = ""
25
+ threshold: Optional[int] = 60
26
+ release: Optional[str] = None
27
+ spec_type: Optional[Literal["TS", "TR"]] = None
28
+
29
+ class KeywordRequest(BaseModel):
30
+ keywords: Optional[str] = ""
31
+ search_mode: Literal["quick", "deep"]
32
+ case_sensitive: Optional[bool] = False
33
+ release: Optional[str] = None
34
+ spec_type: Optional[Literal["TS", "TR"]] = None
35
+ mode: Optional[Literal["and", "or"]] = "and"
36
+
37
+ class KeywordResponse(BaseModel):
38
+ results: List[Dict[str, Any]]
39
+ search_time: float
static/script.js CHANGED
@@ -4,19 +4,35 @@ const dynamicTitle = document.getElementById("dynamicTitle");
4
 
5
  const singleModeBtn = document.getElementById('single-mode-btn');
6
  const batchModeBtn = document.getElementById('batch-mode-btn');
7
- // const keywordModeBtn = document.getElementById("keyword-mode-btn");
 
8
 
9
- const singleInput = document.querySelector('.single-input');
10
- const batchInput = document.querySelector('.batch-input');
11
- // const keywordSearchInput = document.querySelector(".keyword-input");
 
12
 
13
  const docIdInput = document.getElementById('doc-id');
14
  const batchIdsInput = document.getElementById('batch-ids');
15
- // const keywordInput = document.getElementById("keywords");
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  const searchBtn = document.getElementById('search-btn');
18
  const batchSearchBtn = document.getElementById('batch-search-btn');
19
- // const keywordSearchBtn = document.getElementById("keyword-search-btn");
 
20
 
21
  const loader = document.getElementById('loader');
22
  const resultsContainer = document.getElementById('results-container');
@@ -27,75 +43,167 @@ const errorMessage = document.getElementById('error-message');
27
  // Search mode toggle
28
  singleModeBtn.addEventListener('click', () => {
29
  dynamicTitle.textContent = "Find ETSI Documents";
30
- dynamicDesc.textContent = "Enter a SET/SCP/TS document ID to locate the document in the ETSI DocBox server.";
31
 
32
  singleModeBtn.classList.add('active');
33
- // keywordModeBtn.classList.remove("active");
34
  batchModeBtn.classList.remove('active');
 
35
 
36
- singleInput.style.display = 'block';
37
- batchInput.style.display = 'none';
38
- // keywordSearchInput.style.display = "none";
 
39
  });
40
 
41
  batchModeBtn.addEventListener('click', () => {
42
  dynamicTitle.textContent = "Find multiple ETSI Documents";
43
- dynamicDesc.textContent = "Enter a list of SET/SCP/TS document ID to locate all specified documents in the ETSI DocBox server.";
44
 
45
  batchModeBtn.classList.add('active');
46
- //keywordModeBtn.classList.remove("active");
47
  singleModeBtn.classList.remove('active');
 
48
 
49
- batchInput.style.display = 'block';
50
- //keywordSearchInput.style.display = "none";
51
- singleInput.style.display = 'none';
 
52
  });
53
 
54
- // keywordModeBtn.addEventListener('click', () => {
55
- // dynamicTitle.textContent = "Search 3GPP specifications";
56
- // 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)";
57
-
58
- // keywordModeBtn.classList.add("active");
59
- // singleModeBtn.classList.remove('active');
60
- // batchModeBtn.classList.remove("active");
61
-
62
- // singleInput.style.display = "none";
63
- // batchInput.style.display = "none";
64
- // keywordSearchInput.style.display = "block";
65
- // })
66
-
67
- // keywordSearchBtn.addEventListener("click", async ()=>{
68
- // const keywords = keywordInput.value.trim();
69
- // if (!keywords) {
70
- // showError("Please enter at least one keyword");
71
- // return;
72
- // }
73
-
74
- // showLoader();
75
- // hideError();
76
-
77
- // try{
78
- // const response = await fetch("/search-spec", {
79
- // method: "POST",
80
- // headers: {
81
- // "Content-Type": "application/json"
82
- // },
83
- // body: JSON.stringify({ keywords })
84
- // });
85
-
86
- // const data = await response.json();
87
- // if (response.ok){
88
- // displayKeywordResults(data);
89
- // } else {
90
- // showError('Error processing batch request');
91
- // }
92
- // } catch (error) {
93
- // showError('Error connecting to the server. Please check if the API is running.');
94
- // console.error('Error:', error);
95
- // } finally {
96
- // hideLoader();
97
- // }
98
- // })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
 
101
  // Single document search
@@ -115,7 +223,7 @@ searchBtn.addEventListener('click', async () => {
115
  headers: {
116
  'Content-Type': 'application/json'
117
  },
118
- body: JSON.stringify({ doc_id: docId })
119
  });
120
 
121
  const data = await response.json();
@@ -166,8 +274,10 @@ batchSearchBtn.addEventListener('click', async () => {
166
 
167
  if (response.ok) {
168
  displayBatchResults(data);
 
 
169
  } else {
170
- showError('Error processing batch request');
171
  }
172
  } catch (error) {
173
  showError('Error connecting to the server. Please check if the API is running.');
@@ -219,32 +329,96 @@ function displaySingleNotFound(docId, message) {
219
  resultsContainer.style.display = 'block';
220
  }
221
 
222
- // function displayKeywordResults(data) {
223
- // resultsList.innerHTML = '';
224
 
225
- // data.results.forEach(spec => {
226
- // const resultItem = document.createElement("div");
227
- // resultItem.className = "result-item"
228
- // resultItem.innerHTML = `
229
- // <div class="result-header">
230
- // <div class="result-id">${spec.id}</div>
231
- // <div class="result-status status-found">Found</div>
232
- // </div>
233
- // <div class="result-url">
234
- // <p>Title: ${spec.title}</p>
235
- // <p>Type: ${spec.type}</p>
236
- // <p>Release: ${spec.release}</p>
237
- // <p>Version: ${spec.version}</p>
238
- // <p>WG: ${spec.working_group}</p>
239
- // <p>URL: <a target="_blank" href="${spec.url}">${spec.url}</a></p>
240
- // <p>Scope: ${spec.scope}</p>
241
- // </div>
242
- // `;
243
- // resultsList.appendChild(resultItem);
244
- // });
245
- // resultsStats.textContent = `Found in ${data.search_time.toFixed(2)} seconds`
246
- // resultsContainer.style.display = 'block';
247
- // }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
  // Display batch results
250
  function displayBatchResults(data) {
@@ -298,6 +472,8 @@ function hideLoader() {
298
 
299
  // Show error message
300
  function showError(message) {
 
 
301
  errorMessage.textContent = message;
302
  errorMessage.style.display = 'block';
303
  }
@@ -314,8 +490,14 @@ docIdInput.addEventListener('keypress', (e) => {
314
  }
315
  });
316
 
317
- // keywordInput.addEventListener('keypress', (event)=>{
318
- // if (event.key === "Enter"){
319
- // keywordSearchBtn.click();
320
- // }
321
- // })
 
 
 
 
 
 
 
4
 
5
  const singleModeBtn = document.getElementById('single-mode-btn');
6
  const batchModeBtn = document.getElementById('batch-mode-btn');
7
+ const keywordModeBtn = document.getElementById("keyword-mode-btn");
8
+ const expModeBtn = document.getElementById("exp-mode-btn");
9
 
10
+ const singleInputField = document.querySelector('.single-input');
11
+ const batchInputField = document.querySelector('.batch-input');
12
+ const keywordInputField = document.querySelector(".keyword-input");
13
+ const expKeywordInputField = document.querySelector(".experimental-input");
14
 
15
  const docIdInput = document.getElementById('doc-id');
16
  const batchIdsInput = document.getElementById('batch-ids');
17
+ const keywordInput = document.getElementById("keywords");
18
+ const expKeywordInput = document.getElementById("exp-keywords")
19
+ const thresholdInput = document.getElementById("threshold");
20
+
21
+ const releaseFilter = document.querySelector("input[name=release]")
22
+ const modeFilter = document.querySelector("select[name=mode]")
23
+ const specTypeFilter = document.querySelector("select[name=spec_type]")
24
+ const workingGroupFilter = document.querySelector("select[name=working_group]")
25
+ const caseSensitiveFilter = document.querySelector("input[name=case_sensitive]")
26
+ const searchMode = document.querySelector("select[name=search_mode]")
27
+
28
+ const releaseFilter2 = document.querySelector("input[name=release2]")
29
+ const specTypeFilter2 = document.querySelector("select[name=spec_type2]")
30
+ const workingGroupFilter2 = document.querySelector("select[name=working_group2]")
31
 
32
  const searchBtn = document.getElementById('search-btn');
33
  const batchSearchBtn = document.getElementById('batch-search-btn');
34
+ const keywordSearchBtn = document.getElementById("keyword-search-btn");
35
+ const expKeywordSearchBtn = document.getElementById("exp-search-btn");
36
 
37
  const loader = document.getElementById('loader');
38
  const resultsContainer = document.getElementById('results-container');
 
43
  // Search mode toggle
44
  singleModeBtn.addEventListener('click', () => {
45
  dynamicTitle.textContent = "Find ETSI Documents";
46
+ dynamicDesc.textContent = "Enter a SET/SCP/TS document ID to locate the document in the ETSI's server.";
47
 
48
  singleModeBtn.classList.add('active');
49
+ keywordModeBtn.classList.remove("active");
50
  batchModeBtn.classList.remove('active');
51
+ expModeBtn.classList.remove('active');
52
 
53
+ singleInputField.style.display = 'block';
54
+ batchInputField.style.display = 'none';
55
+ keywordInputField.style.display = "none";
56
+ expKeywordInputField.style.display = "none";
57
  });
58
 
59
  batchModeBtn.addEventListener('click', () => {
60
  dynamicTitle.textContent = "Find multiple ETSI Documents";
61
+ dynamicDesc.textContent = "Enter a list of SET/SCP/TS document ID to locate all of the specified documents in the ETSI's server.";
62
 
63
  batchModeBtn.classList.add('active');
64
+ keywordModeBtn.classList.remove("active");
65
  singleModeBtn.classList.remove('active');
66
+ expModeBtn.classList.remove('active');
67
 
68
+ batchInputField.style.display = 'block';
69
+ keywordInputField.style.display = "none";
70
+ singleInputField.style.display = 'none';
71
+ expKeywordInputField.style.display = "none";
72
  });
73
 
74
+ keywordModeBtn.addEventListener('click', () => {
75
+ dynamicTitle.textContent = "Search ETSI specifications";
76
+ dynamicDesc.textContent = "With keywords and filters, find all of ETSI's specifications that matches your needs (with keywords, specification number, release";
77
+
78
+ keywordModeBtn.classList.add("active");
79
+ singleModeBtn.classList.remove('active');
80
+ batchModeBtn.classList.remove("active");
81
+ expModeBtn.classList.remove('active');
82
+
83
+ singleInputField.style.display = "none";
84
+ batchInputField.style.display = "none";
85
+ expKeywordInputField.style.display = "none";
86
+ keywordInputField.style.display = "block";
87
+ })
88
+
89
+ expModeBtn.addEventListener('click', () => {
90
+ dynamicTitle.textContent = "[EXPERIMENTAL] Search 3GPP specifications";
91
+ dynamicDesc.textContent = "With keywords and filters, find all of ETSI's specifications that matches your needs (with keywords, specification number, release";
92
+
93
+ keywordModeBtn.classList.remove("active");
94
+ singleModeBtn.classList.remove('active');
95
+ batchModeBtn.classList.remove("active");
96
+ expModeBtn.classList.add('active');
97
+
98
+ singleInputField.style.display = "none";
99
+ batchInputField.style.display = "none";
100
+ expKeywordInputField.style.display = "block";
101
+ keywordInputField.style.display = "none";
102
+ })
103
+
104
+ document.getElementById('toggleFilters').onclick = function() {
105
+ var target = document.getElementById('filtersForm');
106
+ target.style.display = (target.style.display === 'none' || target.style.display === '') ? 'flex' : 'none';
107
+ };
108
+
109
+ document.getElementById('toggleFilters2').onclick = function() {
110
+ var target = document.getElementById('filtersForm2');
111
+ target.style.display = (target.style.display === 'none' || target.style.display === '') ? 'flex' : 'none';
112
+ };
113
+
114
+ expKeywordSearchBtn.addEventListener("click", async ()=>{
115
+ let keywords = expKeywordInput.value.trim();
116
+ let release = releaseFilter2.value.trim();
117
+ let specType = specTypeFilter2.value.trim();
118
+ let threshold = thresholdInput.value.trim() != '' ? thresholdInput.value.trim() : 60
119
+
120
+ if (!keywords){
121
+ showError("Please enter at least one keyword");
122
+ return;
123
+ }
124
+
125
+ showLoader();
126
+ hideError();
127
+
128
+ try{
129
+ let body = {
130
+ keywords,
131
+ threshold
132
+ };
133
+ if (release != ""){body["release"] = release}
134
+ if (specType != ""){body["spec_type"] = specType}
135
+ const response = await fetch("/search-spec/experimental", {
136
+ method: "POST",
137
+ headers: {
138
+ "Content-Type": "application/json"
139
+ },
140
+ body: JSON.stringify(body)
141
+ });
142
+
143
+ const data = await response.json();
144
+ if (response.ok){
145
+ displayKeywordResults(data, "");
146
+ } else if (response.status == 404) {
147
+ showError('No specification has been found');
148
+ } else {
149
+ showError(`Error processing keyword request: ${data.detail}`)
150
+ }
151
+ } catch (error) {
152
+ showError('Error connecting to the server. Please check if the API is running.');
153
+ console.error('Error:', error);
154
+ } finally {
155
+ hideLoader();
156
+ }
157
+ })
158
+
159
+ keywordSearchBtn.addEventListener("click", async ()=>{
160
+ let keywords = keywordInput.value.trim();
161
+ let release = releaseFilter.value;
162
+ let specType = specTypeFilter.value;
163
+ let search = searchMode.value;
164
+ let checked = caseSensitiveFilter.checked;
165
+ let mode = modeFilter.value;
166
+
167
+ if (!keywords && searchMode == "deep") {
168
+ showError("Please enter at least one keyword in deep search mode");
169
+ return;
170
+ }
171
+
172
+ showLoader();
173
+ hideError();
174
+
175
+ try{
176
+ let body = {
177
+ keywords,
178
+ "search_mode": search,
179
+ "case_sensitive": checked,
180
+ "mode": mode
181
+ };
182
+ if (release != ""){body.release = release}
183
+ if (specType != ""){body["spec_type"] = specType}
184
+ const response = await fetch("/search-spec", {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json"
188
+ },
189
+ body: JSON.stringify(body)
190
+ });
191
+
192
+ const data = await response.json();
193
+ if (response.ok){
194
+ displayKeywordResults(data, search);
195
+ } else if (response.status == 404) {
196
+ showError('No specification has been found');
197
+ } else {
198
+ showError(`Error processing keyword request: ${data.detail}`)
199
+ }
200
+ } catch (error) {
201
+ showError('Error connecting to the server. Please check if the API is running.');
202
+ console.error('Error:', error);
203
+ } finally {
204
+ hideLoader();
205
+ }
206
+ })
207
 
208
 
209
  // Single document search
 
223
  headers: {
224
  'Content-Type': 'application/json'
225
  },
226
+ body: JSON.stringify({ doc_id: docId, release: null })
227
  });
228
 
229
  const data = await response.json();
 
274
 
275
  if (response.ok) {
276
  displayBatchResults(data);
277
+ } else if (response.status == 404) {
278
+ showError('No document has been found');
279
  } else {
280
+ showError('Error processing batch request')
281
  }
282
  } catch (error) {
283
  showError('Error connecting to the server. Please check if the API is running.');
 
329
  resultsContainer.style.display = 'block';
330
  }
331
 
332
+ function displayKeywordResults(data, mode) {
333
+ resultsList.innerHTML = '';
334
 
335
+ data.results.forEach(spec => {
336
+ const resultItem = document.createElement("div");
337
+ resultItem.className = "result-item";
338
+
339
+ resultItem.innerHTML = `
340
+ <div class="result-header">
341
+ <div class="result-id">${spec.id}</div>
342
+ <div class="result-status status-found">Found</div>
343
+ </div>
344
+ <div class="result-url">
345
+ <p>Title: ${spec.title}</p>
346
+ <p>Type: ${spec.type}</p>
347
+ <p>Version: ${spec.version}</p>
348
+ <p>URL: <a target="_blank" href="${spec.url}">${spec.url}</a></p>
349
+ <p>Scope: ${spec.scope}</p>
350
+ </div>
351
+ `;
352
+
353
+ if(mode == "deep"){
354
+ resultItem.innerHTML += `
355
+ <div class="result-actions">
356
+ <button class="get-section-btn btn" data-spec-id="${spec.id}">Get section</button>
357
+ </div>
358
+ `
359
+ }
360
+
361
+ // Ajouter le bouton au DOM
362
+ resultsList.appendChild(resultItem);
363
+
364
+ // Récupérer le bouton nouvellement créé
365
+ if(mode == "deep"){
366
+ const button1 = resultItem.querySelector('.get-section-btn');
367
+ button1._sections = spec.contains;
368
+ }
369
+ });
370
+
371
+ document.querySelectorAll('.get-section-btn').forEach(button => {
372
+ button.addEventListener('click', function() {
373
+ let specId = this.getAttribute("data-spec-id");
374
+ let sections = this._sections;
375
+ openSectionPopup(specId, sections);
376
+ });
377
+ });
378
+
379
+ resultsStats.textContent = `Found ${data.results.length} in ${data.search_time.toFixed(2)} seconds`
380
+ resultsContainer.style.display = 'block';
381
+ }
382
+
383
+ function openSectionPopup(specId, sections) {
384
+ const newTab = window.open('', '_blank');
385
+ let htmlContent =
386
+ `
387
+ <!DOCTYPE html>
388
+ <html lang="fr">
389
+ <head>
390
+ <meta charset="UTF-8">
391
+ <title>Sections of specification number ${specId}</title>
392
+ <link rel="stylesheet" href="/static/style.css">
393
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
394
+ </head>
395
+ <body>
396
+ <div class="popup-header">
397
+ <h2 id="popupTitle">Sections of specification number ${specId}</h2>
398
+ </div>
399
+ <div id="popupTextareas" class="popup-textareas">
400
+ `;
401
+
402
+ Object.entries(sections).forEach(([sectionTitle, content], index) => {
403
+ htmlContent +=
404
+ `
405
+ <div class="textarea-container">
406
+ <h2>${sectionTitle}</h2>
407
+ <p>${content.replace(/\n/g, '<br>')}</p>
408
+ </div>
409
+ `;
410
+ });
411
+
412
+ htmlContent += `
413
+ </div>
414
+ </body>
415
+ </html>
416
+ `;
417
+
418
+ newTab.document.open();
419
+ newTab.document.write(htmlContent);
420
+ newTab.document.close()
421
+ }
422
 
423
  // Display batch results
424
  function displayBatchResults(data) {
 
472
 
473
  // Show error message
474
  function showError(message) {
475
+ resultsList.innerHTML = "";
476
+ resultsStats.textContent = `Found 0 documents`;
477
  errorMessage.textContent = message;
478
  errorMessage.style.display = 'block';
479
  }
 
490
  }
491
  });
492
 
493
+ keywordInput.addEventListener('keypress', (event)=>{
494
+ if (event.key === "Enter"){
495
+ keywordSearchBtn.click();
496
+ }
497
+ })
498
+
499
+ expKeywordInput.addEventListener('keypress', (event)=>{
500
+ if (event.key === "Enter"){
501
+ expKeywordSearchBtn.click();
502
+ }
503
+ })
static/style.css CHANGED
@@ -10,27 +10,192 @@
10
  --shadow-color: rgba(0, 0, 0, 0.7);
11
  }
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  * {
14
  margin: 0;
15
  padding: 0;
16
  box-sizing: border-box;
17
  }
18
-
19
  body {
20
  font-family: 'Roboto', sans-serif;
21
  background-color: var(--secondary-color);
22
  color: var(--text-color);
23
  line-height: 1.6;
24
- padding: 0;
25
- margin: 0;
26
  }
27
-
28
  .container {
29
  max-width: 1200px;
30
  margin: 0 auto;
31
  padding: 20px;
32
  }
33
-
34
  header {
35
  background-color: #23262f;
36
  box-shadow: 0 2px 10px var(--shadow-color);
@@ -39,29 +204,33 @@ header {
39
  top: 0;
40
  z-index: 100;
41
  }
42
-
 
 
 
 
 
 
 
 
43
  .header-content {
44
  display: flex;
45
  align-items: center;
46
  justify-content: space-between;
47
  }
48
-
49
  .logo {
50
  display: flex;
51
  align-items: center;
52
  }
53
-
54
  .logo img {
55
  height: 40px;
56
  margin-right: 10px;
57
  }
58
-
59
  .logo h1 {
60
  font-size: 24px;
61
  font-weight: 500;
62
  color: var(--primary-color);
63
  }
64
-
65
  .search-container {
66
  background-color: #23262f;
67
  border-radius: 8px;
@@ -69,46 +238,38 @@ header {
69
  padding: 30px;
70
  margin-top: 30px;
71
  }
72
-
73
  .search-header {
74
  margin-bottom: 20px;
75
  }
76
-
77
  .search-header h2 {
78
  font-size: 22px;
79
  font-weight: 500;
80
  color: var(--text-color);
81
  margin-bottom: 10px;
82
  }
83
-
84
  .search-header p {
85
  color: var(--light-text);
86
  font-size: 16px;
87
  }
88
-
89
  .search-form {
90
  display: flex;
91
  flex-direction: column;
92
  gap: 20px;
93
  }
94
-
95
  .input-group {
96
  display: flex;
97
  flex-direction: column;
98
  gap: 8px;
99
  }
100
-
101
  .input-group label {
102
  font-size: 14px;
103
  font-weight: 500;
104
  color: var(--light-text);
105
  }
106
-
107
  .input-field {
108
  display: flex;
109
  gap: 10px;
110
  }
111
-
112
  .input-field input {
113
  flex: 1;
114
  padding: 12px 16px;
@@ -120,12 +281,10 @@ header {
120
  outline: none;
121
  transition: border-color 0.3s;
122
  }
123
-
124
  .input-field input:focus {
125
  border-color: var(--primary-color);
126
  box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.15);
127
  }
128
-
129
  .btn {
130
  background-color: var(--primary-color);
131
  color: #181a20;
@@ -137,18 +296,15 @@ header {
137
  cursor: pointer;
138
  transition: background-color 0.3s;
139
  }
140
-
141
  .btn:hover {
142
  background-color: var(--accent-color);
143
  color: #fff;
144
  }
145
-
146
  .search-mode {
147
  display: flex;
148
  gap: 20px;
149
  margin-bottom: 20px;
150
  }
151
-
152
  .search-mode button {
153
  background: none;
154
  border: none;
@@ -160,20 +316,13 @@ header {
160
  border-bottom: 2px solid transparent;
161
  transition: all 0.3s;
162
  }
163
-
164
  .search-mode button.active {
165
  color: var(--primary-color);
166
  border-bottom: 2px solid var(--primary-color);
167
  }
168
-
169
- .batch-input {
170
- display: none;
171
- }
172
-
173
- .keyword-input {
174
  display: none;
175
  }
176
-
177
  .batch-input textarea {
178
  width: 100%;
179
  height: 120px;
@@ -187,18 +336,15 @@ header {
187
  background: #181a20;
188
  color: var(--text-color);
189
  }
190
-
191
  .batch-input textarea:focus {
192
  border-color: var(--primary-color);
193
  box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.15);
194
  }
195
-
196
  .batch-input .hint {
197
  font-size: 14px;
198
  color: var(--light-text);
199
  margin-top: 8px;
200
  }
201
-
202
  .results-container {
203
  margin-top: 30px;
204
  background-color: #23262f;
@@ -207,7 +353,6 @@ header {
207
  padding: 30px;
208
  display: none;
209
  }
210
-
211
  .results-header {
212
  display: flex;
213
  justify-content: space-between;
@@ -216,23 +361,19 @@ header {
216
  padding-bottom: 15px;
217
  border-bottom: 1px solid var(--border-color);
218
  }
219
-
220
  .results-header h2 {
221
  font-size: 22px;
222
  font-weight: 500;
223
  }
224
-
225
  .results-stats {
226
  color: var(--light-text);
227
  font-size: 14px;
228
  }
229
-
230
  .results-list {
231
  display: flex;
232
  flex-direction: column;
233
  gap: 15px;
234
  }
235
-
236
  .result-item {
237
  padding: 15px;
238
  border: 1px solid var(--border-color);
@@ -240,61 +381,50 @@ header {
240
  background: #181a20;
241
  transition: box-shadow 0.3s;
242
  }
243
-
244
  .result-item:hover {
245
  box-shadow: 0 4px 8px var(--shadow-color);
246
  }
247
-
248
  .result-header {
249
  display: flex;
250
  justify-content: space-between;
251
  align-items: center;
252
  margin-bottom: 10px;
253
  }
254
-
255
  .result-id {
256
  font-weight: 500;
257
  font-size: 18px;
258
  color: var(--primary-color);
259
  }
260
-
261
  .result-status {
262
  font-size: 14px;
263
  padding: 4px 12px;
264
  border-radius: 12px;
265
  }
266
-
267
  .status-found {
268
  background-color: rgba(52, 232, 158, 0.1);
269
  color: var(--success-color);
270
  }
271
-
272
  .status-not-found {
273
  background-color: rgba(255, 109, 109, 0.1);
274
  color: var(--error-color);
275
  }
276
-
277
  .result-url {
278
  word-break: break-all;
279
  margin-top: 10px;
280
  }
281
-
282
  .result-url a {
283
  color: var(--primary-color);
284
  text-decoration: none;
285
  transition: color 0.3s;
286
  }
287
-
288
  .result-url a:hover {
289
  text-decoration: underline;
290
  }
291
-
292
  .loader {
293
  display: none;
294
  text-align: center;
295
  padding: 20px;
296
  }
297
-
298
  .spinner {
299
  border: 4px solid rgba(255, 255, 255, 0.1);
300
  border-radius: 50%;
@@ -304,12 +434,10 @@ header {
304
  animation: spin 1s linear infinite;
305
  margin: 0 auto;
306
  }
307
-
308
  @keyframes spin {
309
- 0% { transform: rotate(0deg); }
310
- 100% { transform: rotate(360deg); }
311
  }
312
-
313
  .error-message {
314
  background-color: rgba(255, 109, 109, 0.1);
315
  color: var(--error-color);
@@ -318,7 +446,6 @@ header {
318
  margin-top: 20px;
319
  display: none;
320
  }
321
-
322
  footer {
323
  text-align: center;
324
  padding: 30px 0;
@@ -326,17 +453,14 @@ footer {
326
  color: var(--light-text);
327
  font-size: 14px;
328
  }
329
-
330
  @media (max-width: 768px) {
331
  .header-content {
332
  flex-direction: column;
333
  gap: 15px;
334
  }
335
-
336
  .input-field {
337
  flex-direction: column;
338
  }
339
-
340
  .search-mode {
341
  overflow-x: auto;
342
  padding-bottom: 5px;
 
10
  --shadow-color: rgba(0, 0, 0, 0.7);
11
  }
12
 
13
+ /* Section Popup */
14
+ .section-popup {
15
+ display: none;
16
+ position: fixed;
17
+ top: 0;
18
+ left: 0;
19
+ width: 100%;
20
+ height: 100%;
21
+ background-color: rgba(24, 26, 32, 0.85); /* Plus foncé, légèrement opaque */
22
+ z-index: 1000;
23
+ overflow: auto;
24
+ }
25
+ .section-popup-content {
26
+ position: relative;
27
+ background-color: #23262f;
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
+ @keyframes popupFadeIn {
37
+ from { opacity: 0; transform: translateY(-20px);}
38
+ to { opacity: 1; transform: translateY(0);}
39
+ }
40
+
41
+ /* Popup header */
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
+ .popup-header h2 {
51
+ font-size: 22px;
52
+ font-weight: 500;
53
+ color: var(--text-color);
54
+ }
55
+ .close-popup {
56
+ font-size: 28px;
57
+ font-weight: bold;
58
+ color: var(--light-text);
59
+ cursor: pointer;
60
+ transition: color 0.3s;
61
+ }
62
+ .close-popup:hover {
63
+ color: var(--primary-color);
64
+ }
65
+
66
+ /* Popup Textareas */
67
+ .popup-textareas {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 20px;
71
+ overflow-y: auto;
72
+ padding-right: 10px;
73
+ }
74
+ .textarea-container {
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 8px;
78
+ }
79
+ .textarea-container p {
80
+ width: 100%;
81
+ padding: 12px 16px;
82
+ border: 1px solid #383b44;
83
+ border-radius: 4px;
84
+ font-size: 16px;
85
+ font-family: 'Roboto', sans-serif;
86
+ background-color: #181a20;
87
+ color: var(--text-color);
88
+ resize: vertical;
89
+ outline: none;
90
+ }
91
+
92
+ /* Boutons de copie */
93
+ .copy-btn {
94
+ align-self: flex-end;
95
+ background-color: var(--secondary-color);
96
+ color: var(--primary-color);
97
+ border: 1px solid var(--border-color);
98
+ border-radius: 4px;
99
+ padding: 8px 16px;
100
+ font-size: 14px;
101
+ font-weight: 500;
102
+ cursor: pointer;
103
+ transition: background-color 0.3s;
104
+ }
105
+ .copy-btn:hover {
106
+ background-color: #23262f;
107
+ }
108
+ .copy-all-btn {
109
+ display: block;
110
+ width: 100%;
111
+ background-color: var(--primary-color);
112
+ color: #181a20;
113
+ border: none;
114
+ border-radius: 4px;
115
+ padding: 12px 24px;
116
+ font-size: 16px;
117
+ font-weight: 500;
118
+ cursor: pointer;
119
+ transition: background-color 0.3s;
120
+ margin-top: 20px;
121
+ }
122
+ .copy-all-btn:hover {
123
+ background-color: var(--accent-color);
124
+ color: #fff;
125
+ }
126
+
127
+ /* Filtres */
128
+ .filter-tab-container {
129
+ margin-top: 20px;
130
+ margin-bottom: 15px;
131
+ }
132
+ .filter-toggle-btn {
133
+ background: #487bbd;
134
+ color: #fff;
135
+ border: none;
136
+ border-radius: 4px 4px 0 0;
137
+ padding: 8px 14px;
138
+ cursor: pointer;
139
+ font-size: 16px;
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 5px;
143
+ transition: background 0.3s;
144
+ box-shadow: 0 2px 4px rgba(64, 122, 212, 0.1);
145
+ }
146
+ .filter-toggle-btn:hover {
147
+ background: #305070;
148
+ }
149
+ .filters-row {
150
+ display: flex;
151
+ gap: 12px;
152
+ align-items: center;
153
+ background: #181a20;
154
+ padding: 13px 14px;
155
+ border: 1px solid #23262f;
156
+ border-top: none;
157
+ border-radius: 0 0 4px 4px;
158
+ margin: 0;
159
+ margin-top: -1px;
160
+ box-shadow: 0 2px 4px rgba(44, 82, 130, 0.10);
161
+ }
162
+ .filter-input, .filter-select {
163
+ padding: 7px 10px;
164
+ border: 1px solid #23262f;
165
+ border-radius: 4px;
166
+ background: #23262f;
167
+ color: #e8eaed;
168
+ font-size: 15px;
169
+ }
170
+ .filter-checkbox-label {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 7px;
174
+ font-size: 15px;
175
+ color: #8ab4f8;
176
+ margin: 0;
177
+ }
178
+ .filter-checkbox {
179
+ accent-color: #8ab4f8;
180
+ }
181
+
182
+ /* Responsive et global */
183
  * {
184
  margin: 0;
185
  padding: 0;
186
  box-sizing: border-box;
187
  }
 
188
  body {
189
  font-family: 'Roboto', sans-serif;
190
  background-color: var(--secondary-color);
191
  color: var(--text-color);
192
  line-height: 1.6;
 
 
193
  }
 
194
  .container {
195
  max-width: 1200px;
196
  margin: 0 auto;
197
  padding: 20px;
198
  }
 
199
  header {
200
  background-color: #23262f;
201
  box-shadow: 0 2px 10px var(--shadow-color);
 
204
  top: 0;
205
  z-index: 100;
206
  }
207
+ .input-field input.filter-input{
208
+ flex: none;
209
+ padding: 7px 10px;
210
+ border: 1px solid #23262f;
211
+ border-radius: 4px;
212
+ background: #23262f;
213
+ color: #e8eaed;
214
+ font-size: 15px;
215
+ }
216
  .header-content {
217
  display: flex;
218
  align-items: center;
219
  justify-content: space-between;
220
  }
 
221
  .logo {
222
  display: flex;
223
  align-items: center;
224
  }
 
225
  .logo img {
226
  height: 40px;
227
  margin-right: 10px;
228
  }
 
229
  .logo h1 {
230
  font-size: 24px;
231
  font-weight: 500;
232
  color: var(--primary-color);
233
  }
 
234
  .search-container {
235
  background-color: #23262f;
236
  border-radius: 8px;
 
238
  padding: 30px;
239
  margin-top: 30px;
240
  }
 
241
  .search-header {
242
  margin-bottom: 20px;
243
  }
 
244
  .search-header h2 {
245
  font-size: 22px;
246
  font-weight: 500;
247
  color: var(--text-color);
248
  margin-bottom: 10px;
249
  }
 
250
  .search-header p {
251
  color: var(--light-text);
252
  font-size: 16px;
253
  }
 
254
  .search-form {
255
  display: flex;
256
  flex-direction: column;
257
  gap: 20px;
258
  }
 
259
  .input-group {
260
  display: flex;
261
  flex-direction: column;
262
  gap: 8px;
263
  }
 
264
  .input-group label {
265
  font-size: 14px;
266
  font-weight: 500;
267
  color: var(--light-text);
268
  }
 
269
  .input-field {
270
  display: flex;
271
  gap: 10px;
272
  }
 
273
  .input-field input {
274
  flex: 1;
275
  padding: 12px 16px;
 
281
  outline: none;
282
  transition: border-color 0.3s;
283
  }
 
284
  .input-field input:focus {
285
  border-color: var(--primary-color);
286
  box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.15);
287
  }
 
288
  .btn {
289
  background-color: var(--primary-color);
290
  color: #181a20;
 
296
  cursor: pointer;
297
  transition: background-color 0.3s;
298
  }
 
299
  .btn:hover {
300
  background-color: var(--accent-color);
301
  color: #fff;
302
  }
 
303
  .search-mode {
304
  display: flex;
305
  gap: 20px;
306
  margin-bottom: 20px;
307
  }
 
308
  .search-mode button {
309
  background: none;
310
  border: none;
 
316
  border-bottom: 2px solid transparent;
317
  transition: all 0.3s;
318
  }
 
319
  .search-mode button.active {
320
  color: var(--primary-color);
321
  border-bottom: 2px solid var(--primary-color);
322
  }
323
+ .batch-input, .keyword-input, .experimental-input {
 
 
 
 
 
324
  display: none;
325
  }
 
326
  .batch-input textarea {
327
  width: 100%;
328
  height: 120px;
 
336
  background: #181a20;
337
  color: var(--text-color);
338
  }
 
339
  .batch-input textarea:focus {
340
  border-color: var(--primary-color);
341
  box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.15);
342
  }
 
343
  .batch-input .hint {
344
  font-size: 14px;
345
  color: var(--light-text);
346
  margin-top: 8px;
347
  }
 
348
  .results-container {
349
  margin-top: 30px;
350
  background-color: #23262f;
 
353
  padding: 30px;
354
  display: none;
355
  }
 
356
  .results-header {
357
  display: flex;
358
  justify-content: space-between;
 
361
  padding-bottom: 15px;
362
  border-bottom: 1px solid var(--border-color);
363
  }
 
364
  .results-header h2 {
365
  font-size: 22px;
366
  font-weight: 500;
367
  }
 
368
  .results-stats {
369
  color: var(--light-text);
370
  font-size: 14px;
371
  }
 
372
  .results-list {
373
  display: flex;
374
  flex-direction: column;
375
  gap: 15px;
376
  }
 
377
  .result-item {
378
  padding: 15px;
379
  border: 1px solid var(--border-color);
 
381
  background: #181a20;
382
  transition: box-shadow 0.3s;
383
  }
 
384
  .result-item:hover {
385
  box-shadow: 0 4px 8px var(--shadow-color);
386
  }
 
387
  .result-header {
388
  display: flex;
389
  justify-content: space-between;
390
  align-items: center;
391
  margin-bottom: 10px;
392
  }
 
393
  .result-id {
394
  font-weight: 500;
395
  font-size: 18px;
396
  color: var(--primary-color);
397
  }
 
398
  .result-status {
399
  font-size: 14px;
400
  padding: 4px 12px;
401
  border-radius: 12px;
402
  }
 
403
  .status-found {
404
  background-color: rgba(52, 232, 158, 0.1);
405
  color: var(--success-color);
406
  }
 
407
  .status-not-found {
408
  background-color: rgba(255, 109, 109, 0.1);
409
  color: var(--error-color);
410
  }
 
411
  .result-url {
412
  word-break: break-all;
413
  margin-top: 10px;
414
  }
 
415
  .result-url a {
416
  color: var(--primary-color);
417
  text-decoration: none;
418
  transition: color 0.3s;
419
  }
 
420
  .result-url a:hover {
421
  text-decoration: underline;
422
  }
 
423
  .loader {
424
  display: none;
425
  text-align: center;
426
  padding: 20px;
427
  }
 
428
  .spinner {
429
  border: 4px solid rgba(255, 255, 255, 0.1);
430
  border-radius: 50%;
 
434
  animation: spin 1s linear infinite;
435
  margin: 0 auto;
436
  }
 
437
  @keyframes spin {
438
+ 0% { transform: rotate(0deg);}
439
+ 100% { transform: rotate(360deg);}
440
  }
 
441
  .error-message {
442
  background-color: rgba(255, 109, 109, 0.1);
443
  color: var(--error-color);
 
446
  margin-top: 20px;
447
  display: none;
448
  }
 
449
  footer {
450
  text-align: center;
451
  padding: 30px 0;
 
453
  color: var(--light-text);
454
  font-size: 14px;
455
  }
 
456
  @media (max-width: 768px) {
457
  .header-content {
458
  flex-direction: column;
459
  gap: 15px;
460
  }
 
461
  .input-field {
462
  flex-direction: column;
463
  }
 
464
  .search-mode {
465
  overflow-x: auto;
466
  padding-bottom: 5px;
templates/index.html CHANGED
@@ -26,7 +26,8 @@
26
  <div class="search-mode">
27
  <button id="single-mode-btn" class="active">Single Document</button>
28
  <button id="batch-mode-btn">Batch Search</button>
29
- <!--<button id="keyword-mode-btn">Keyword Search</button> -->
 
30
  </div>
31
 
32
  <div class="search-form">
@@ -44,14 +45,71 @@
44
  <div class="hint">Enter one document ID per line</div>
45
  <button id="batch-search-btn" class="btn" style="margin-top: 10px;">Search All</button>
46
  </div>
47
- <!--
48
  <div class="input-group keyword-input">
49
- <label for="keywords">Keywords</label>
50
  <div class="input-field">
51
- <input type="text" id="keywords" placeholder="Enter your keywords separated by space">
 
 
 
 
52
  <button id="keyword-search-btn" class="btn">Search</button>
53
  </div>
54
- </div> -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  </div>
56
 
57
  <div class="error-message" id="error-message"></div>
 
26
  <div class="search-mode">
27
  <button id="single-mode-btn" class="active">Single Document</button>
28
  <button id="batch-mode-btn">Batch Search</button>
29
+ <button id="keyword-mode-btn">Keyword Search</button>
30
+ <button id="exp-mode-btn">Experimental Search (Using BM25)</button>
31
  </div>
32
 
33
  <div class="search-form">
 
45
  <div class="hint">Enter one document ID per line</div>
46
  <button id="batch-search-btn" class="btn" style="margin-top: 10px;">Search All</button>
47
  </div>
48
+
49
  <div class="input-group keyword-input">
 
50
  <div class="input-field">
51
+ <select name="search_mode" class="filter-select" id="search-mode">
52
+ <option value="quick">Quick Search (only title & scope)</option>
53
+ <option value="deep">Deep Search</option>
54
+ </select>
55
+ <input type="text" id="keywords" placeholder="Enter your keywords separated by comma">
56
  <button id="keyword-search-btn" class="btn">Search</button>
57
  </div>
58
+ <div class="filter-tab-container">
59
+ <button type="button" id="toggleFilters" class="filter-toggle-btn">
60
+ <span>Filters</span>
61
+ <svg width="16" height="16" style="vertical-align:middle" fill="currentColor">
62
+ <path d="M4 7l4 4 4-4"></path>
63
+ </svg>
64
+ </button>
65
+ <form id="filtersForm" class="filters-row" style="display:none;">
66
+ <input type="number" min="0" max="21" name="release" placeholder="Release"
67
+ class="filter-input">
68
+
69
+ <select name="mode" class="filter-select">
70
+ <option value="and">AND</option>
71
+ <option value="or">OR</option>
72
+ </select>
73
+
74
+ <select name="spec_type" class="filter-select">
75
+ <option value="">All types</option>
76
+ <option value="TR">Technical Report (TR)</option>
77
+ <option value="TS">Technical Specification (TS)</option>
78
+ </select>
79
+
80
+ <label class="filter-checkbox-label">
81
+ <input type="checkbox" name="case_sensitive" class="filter-checkbox" />
82
+ Case sensitive
83
+ </label>
84
+ </form>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="input-group experimental-input">
89
+ <div class="input-field">
90
+ <input type="number" name="threshold" id="threshold" class="filter-input" min="30" max="100" placeholder="Min %">
91
+ <input type="text" id="exp-keywords" placeholder="Enter keywords separated by spaces">
92
+ <button id="exp-search-btn" class="btn">Search</button>
93
+ </div>
94
+ <div class="filter-tab-container">
95
+ <button type="button" id="toggleFilters2" class="filter-toggle-btn">
96
+ <span>Filters</span>
97
+ <svg width="16" height="16" style="vertical-align:middle" fill="currentColor">
98
+ <path d="M4 7l4 4 4-4"></path>
99
+ </svg>
100
+ </button>
101
+ <form id="filtersForm2" class="filters-row" style="display:none;">
102
+ <input type="number" min="0" max="21" name="release2" placeholder="Release"
103
+ class="filter-input">
104
+
105
+ <select name="spec_type2" class="filter-select">
106
+ <option value="">All types</option>
107
+ <option value="TR">Technical Report (TR)</option>
108
+ <option value="TS">Technical Specification (TS)</option>
109
+ </select>
110
+ </form>
111
+ </div>
112
+ </div>
113
  </div>
114
 
115
  <div class="error-message" id="error-message"></div>