om4r932 commited on
Commit
2903edf
·
1 Parent(s): 405abe1

Front-end implementation

Browse files
Files changed (4) hide show
  1. app.py +11 -5
  2. static/script.js +446 -0
  3. static/style.css +536 -0
  4. templates/index.html +172 -0
app.py CHANGED
@@ -101,6 +101,8 @@ app = FastAPI(
101
  openapi_tags=tags_metadata
102
  )
103
 
 
 
104
  app.add_middleware(
105
  CORSMiddleware,
106
  allow_origins=["*"],
@@ -118,6 +120,10 @@ valid_3gpp_spec_format = re.compile(r'^\d{2}\.\d{3}(?:-\d+)?')
118
  valid_etsi_doc_format = re.compile(r'^(?:SET|SCP|SETTEC|SETREQ|SCPTEC|SCPREQ)\(\d+\)\d+(?:r\d+)?', flags=re.IGNORECASE)
119
  valid_etsi_spec_format = re.compile(r'^\d{3} \d{3}(?:-\d+)?')
120
 
 
 
 
 
121
  @app.post("/find/single", response_model=DocResponse, tags=["Document Retrieval"], summary="Retrieve a single document by ID", responses={
122
  200: {
123
  "description": "Document found successfully",
@@ -231,7 +237,7 @@ def search_specifications(request: KeywordRequest):
231
  source = request.source
232
  spec_metadatas = spec_metadatas_3gpp if source == "3GPP" else spec_metadatas_etsi if source == "ETSI" else spec_metadatas_3gpp + spec_metadatas_etsi
233
  spec_type = request.spec_type
234
- keywords = [string.lower() if boolSensitiveCase else string for string in request.keywords.split(",")]
235
  print(keywords)
236
  unique_specs = set()
237
  results = []
@@ -250,7 +256,7 @@ def search_specifications(request: KeywordRequest):
250
 
251
  if request.mode == "and":
252
  string = f"{spec['id']}+-+{spec['title']}+-+{spec['type']}+-+{spec['version']}"
253
- if all(keyword in (string.lower() if boolSensitiveCase else string) for keyword in keywords):
254
  valid = True
255
  if search_mode == "deep":
256
  if docValid:
@@ -258,12 +264,12 @@ def search_specifications(request: KeywordRequest):
258
  section_title = doc[x]
259
  section_content = doc[x+1]
260
  if "reference" not in section_title.lower() and "void" not in section_title.lower() and "annex" not in section_content.lower():
261
- if all(keyword in (section_content.lower() if boolSensitiveCase else section_content) for keyword in keywords):
262
  valid = True
263
  contents.append({section_title: section_content})
264
  elif request.mode == "or":
265
  string = f"{spec['id']}+-+{spec['title']}+-+{spec['type']}+-+{spec['version']}"
266
- if any(keyword in (string.lower() if boolSensitiveCase else string) for keyword in keywords):
267
  valid = True
268
  if search_mode == "deep":
269
  if docValid:
@@ -271,7 +277,7 @@ def search_specifications(request: KeywordRequest):
271
  section_title = doc[x]
272
  section_content = doc[x+1]
273
  if "reference" not in section_title.lower() and "void" not in section_title.lower() and "annex" not in section_content.lower():
274
- if any(keyword in (section_content.lower() if boolSensitiveCase else section_content) for keyword in keywords):
275
  valid = True
276
  contents.append({section_title: section_content})
277
  if valid:
 
101
  openapi_tags=tags_metadata
102
  )
103
 
104
+ app.mount("/static", StaticFiles(directory="static"), name="static")
105
+
106
  app.add_middleware(
107
  CORSMiddleware,
108
  allow_origins=["*"],
 
120
  valid_etsi_doc_format = re.compile(r'^(?:SET|SCP|SETTEC|SETREQ|SCPTEC|SCPREQ)\(\d+\)\d+(?:r\d+)?', flags=re.IGNORECASE)
121
  valid_etsi_spec_format = re.compile(r'^\d{3} \d{3}(?:-\d+)?')
122
 
123
+ @app.get("/", tags=["Misc"], summary="Returns index.html file")
124
+ def frontend():
125
+ return FileResponse(os.path.join('templates', 'index.html'))
126
+
127
  @app.post("/find/single", response_model=DocResponse, tags=["Document Retrieval"], summary="Retrieve a single document by ID", responses={
128
  200: {
129
  "description": "Document found successfully",
 
237
  source = request.source
238
  spec_metadatas = spec_metadatas_3gpp if source == "3GPP" else spec_metadatas_etsi if source == "ETSI" else spec_metadatas_3gpp + spec_metadatas_etsi
239
  spec_type = request.spec_type
240
+ keywords = [string.lower() if not boolSensitiveCase else string for string in request.keywords.split(",")]
241
  print(keywords)
242
  unique_specs = set()
243
  results = []
 
256
 
257
  if request.mode == "and":
258
  string = f"{spec['id']}+-+{spec['title']}+-+{spec['type']}+-+{spec['version']}"
259
+ if all(keyword in (string.lower() if not boolSensitiveCase else string) for keyword in keywords):
260
  valid = True
261
  if search_mode == "deep":
262
  if docValid:
 
264
  section_title = doc[x]
265
  section_content = doc[x+1]
266
  if "reference" not in section_title.lower() and "void" not in section_title.lower() and "annex" not in section_content.lower():
267
+ if all(keyword in (section_content.lower() if not boolSensitiveCase else section_content) for keyword in keywords):
268
  valid = True
269
  contents.append({section_title: section_content})
270
  elif request.mode == "or":
271
  string = f"{spec['id']}+-+{spec['title']}+-+{spec['type']}+-+{spec['version']}"
272
+ if any(keyword in (string.lower() if not boolSensitiveCase else string) for keyword in keywords):
273
  valid = True
274
  if search_mode == "deep":
275
  if docValid:
 
277
  section_title = doc[x]
278
  section_content = doc[x+1]
279
  if "reference" not in section_title.lower() and "void" not in section_title.lower() and "annex" not in section_content.lower():
280
+ if any(keyword in (section_content.lower() if not boolSensitiveCase else section_content) for keyword in keywords):
281
  valid = True
282
  contents.append({section_title: section_content})
283
  if valid:
static/script.js ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // Global application state
3
+ let currentResults = [];
4
+ let currentMode = 'single';
5
+
6
+ // Initialization
7
+ document.addEventListener('DOMContentLoaded', function() {
8
+ initializeApp();
9
+ });
10
+
11
+ function initializeApp() {
12
+ setupTabHandlers();
13
+ setupKeyboardHandlers();
14
+ updateHeaderStats('Ready');
15
+ }
16
+
17
+ // Tab management
18
+ function setupTabHandlers() {
19
+ document.querySelectorAll('.tab-button').forEach(button => {
20
+ button.addEventListener('click', function() {
21
+ const mode = this.dataset.mode;
22
+ switchMode(mode);
23
+ });
24
+ });
25
+ }
26
+
27
+ function switchMode(mode) {
28
+ currentMode = mode;
29
+
30
+ // Update tabs
31
+ document.querySelectorAll('.tab-button').forEach(btn => {
32
+ btn.classList.remove('active');
33
+ });
34
+ document.querySelector(`[data-mode="${mode}"]`).classList.add('active');
35
+
36
+ // Update forms
37
+ document.querySelectorAll('.search-form').forEach(form => {
38
+ form.classList.remove('active');
39
+ });
40
+ document.getElementById(`${mode}-form`).classList.add('active');
41
+
42
+ // Reset results
43
+ hideResults();
44
+ }
45
+
46
+ // Keyboard shortcuts management
47
+ function setupKeyboardHandlers() {
48
+ document.getElementById('doc-id').addEventListener('keypress', function(e) {
49
+ if (e.key === 'Enter') searchSingle();
50
+ });
51
+
52
+ document.getElementById('keywords').addEventListener('keypress', function(e) {
53
+ if (e.key === 'Enter') searchKeyword();
54
+ });
55
+
56
+ document.getElementById('bm25-keywords').addEventListener('keypress', function(e) {
57
+ if (e.key === 'Enter') searchBM25();
58
+ });
59
+ }
60
+
61
+ // Search functions
62
+ async function searchSingle() {
63
+ const docId = document.getElementById('doc-id').value.trim();
64
+
65
+ if (!docId) {
66
+ showError('Please enter a document ID');
67
+ return;
68
+ }
69
+
70
+ showLoading();
71
+ updateHeaderStats('Searching...');
72
+
73
+ try {
74
+ const response = await fetch(`/find/single`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ },
79
+ body: JSON.stringify({ doc_id: docId })
80
+ });
81
+
82
+ const data = await response.json();
83
+
84
+ if (response.ok) {
85
+ displaySingleResult(data);
86
+ updateHeaderStats(`Found in ${data.search_time.toFixed(3)}s`);
87
+ } else {
88
+ showError(data.detail);
89
+ updateHeaderStats('Error');
90
+ }
91
+ } catch (error) {
92
+ showError('Error connecting to server');
93
+ updateHeaderStats('Error');
94
+ console.error('Error:', error);
95
+ } finally {
96
+ hideLoading();
97
+ }
98
+ }
99
+
100
+ async function searchBatch() {
101
+ const batchText = document.getElementById('batch-ids').value.trim();
102
+
103
+ if (!batchText) {
104
+ showError('Please enter at least one document ID');
105
+ return;
106
+ }
107
+
108
+ const docIds = batchText.split('\n')
109
+ .map(id => id.trim())
110
+ .filter(id => id !== '');
111
+
112
+ if (docIds.length === 0) {
113
+ showError('Please enter at least one valid document ID');
114
+ return;
115
+ }
116
+
117
+ showLoading();
118
+ updateHeaderStats('Searching...');
119
+
120
+ try {
121
+ const response = await fetch(`/find/batch`, {
122
+ method: 'POST',
123
+ headers: {
124
+ 'Content-Type': 'application/json',
125
+ },
126
+ body: JSON.stringify({ doc_ids: docIds })
127
+ });
128
+
129
+ const data = await response.json();
130
+
131
+ if (response.ok) {
132
+ displayBatchResults(data);
133
+ updateHeaderStats(`${Object.keys(data.results).length} found, ${data.missing.length} missing - ${data.search_time.toFixed(3)}s`);
134
+ } else {
135
+ showError(data.detail);
136
+ updateHeaderStats('Error');
137
+ }
138
+ } catch (error) {
139
+ showError('Error connecting to server');
140
+ updateHeaderStats('Error');
141
+ console.error('Error:', error);
142
+ } finally {
143
+ hideLoading();
144
+ }
145
+ }
146
+
147
+ async function searchKeyword() {
148
+ const keywords = document.getElementById('keywords').value.trim();
149
+ const searchMode = document.getElementById('search-mode-filter').value;
150
+
151
+ if (!keywords && searchMode === 'deep') {
152
+ showError('Please enter at least one keyword in deep search mode');
153
+ return;
154
+ }
155
+
156
+ showLoading();
157
+ updateHeaderStats('Searching...');
158
+
159
+ try {
160
+ const body = {
161
+ keywords: keywords,
162
+ search_mode: searchMode,
163
+ case_sensitive: document.getElementById('case-sensitive-filter').checked,
164
+ source: document.getElementById('source-filter').value,
165
+ mode: document.getElementById('mode-filter').value
166
+ };
167
+
168
+ const specType = document.getElementById('spec-type-filter').value;
169
+ if (specType) {
170
+ body.spec_type = specType;
171
+ }
172
+
173
+ const response = await fetch(`/search`, {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ },
178
+ body: JSON.stringify(body)
179
+ });
180
+
181
+ const data = await response.json();
182
+
183
+ if (response.ok) {
184
+ displaySearchResults(data);
185
+ updateHeaderStats(`${data.results.length} result(s) - ${data.search_time.toFixed(3)}s`);
186
+ } else {
187
+ showError(data.detail);
188
+ updateHeaderStats('Error');
189
+ }
190
+ } catch (error) {
191
+ showError('Error connecting to server');
192
+ updateHeaderStats('Error');
193
+ console.error('Error:', error);
194
+ } finally {
195
+ hideLoading();
196
+ }
197
+ }
198
+
199
+ async function searchBM25() {
200
+ const keywords = document.getElementById('bm25-keywords').value.trim();
201
+
202
+ if (!keywords) {
203
+ showError('Please enter a search query');
204
+ return;
205
+ }
206
+
207
+ showLoading();
208
+ updateHeaderStats('Searching...');
209
+
210
+ try {
211
+ const body = {
212
+ keywords: keywords,
213
+ source: document.getElementById('bm25-source-filter').value,
214
+ threshold: parseInt(document.getElementById('threshold').value) || 60
215
+ };
216
+
217
+ const specType = document.getElementById('bm25-spec-type-filter').value;
218
+ if (specType) {
219
+ body.spec_type = specType;
220
+ }
221
+
222
+ const response = await fetch(`/search/bm25`, {
223
+ method: 'POST',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ },
227
+ body: JSON.stringify(body)
228
+ });
229
+
230
+ const data = await response.json();
231
+
232
+ if (response.ok) {
233
+ displaySearchResults(data);
234
+ updateHeaderStats(`${data.results.length} result(s) - ${data.search_time.toFixed(3)}s`);
235
+ } else {
236
+ showError(data.detail);
237
+ updateHeaderStats('Error');
238
+ }
239
+ } catch (error) {
240
+ showError('Error connecting to server');
241
+ updateHeaderStats('Error');
242
+ console.error('Error:', error);
243
+ } finally {
244
+ hideLoading();
245
+ }
246
+ }
247
+
248
+ // Results display functions
249
+ function displaySingleResult(data) {
250
+ const resultsContent = document.getElementById('results-content');
251
+
252
+ resultsContent.innerHTML = `
253
+ <div class="result-item">
254
+ <div class="result-header">
255
+ <div class="result-id">${data.doc_id}</div>
256
+ <div class="result-status status-found">Found</div>
257
+ </div>
258
+ <div class="result-details">
259
+ ${data.version ? `<div class="result-detail"><strong>Version:</strong> ${data.version}</div>` : ''}
260
+ ${data.scope ? `<div class="result-detail"><strong>Scope:</strong> ${data.scope}</div>` : ''}
261
+ <div class="result-detail result-url">
262
+ <strong>URL:</strong> <a href="${data.url}" target="_blank">${data.url}</a>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ `;
267
+
268
+ showResults();
269
+ }
270
+
271
+ function displayBatchResults(data) {
272
+ const resultsContent = document.getElementById('results-content');
273
+ let html = '';
274
+
275
+ // Found results
276
+ Object.entries(data.results).forEach(([docId, url]) => {
277
+ html += `
278
+ <div class="result-item">
279
+ <div class="result-header">
280
+ <div class="result-id">${docId}</div>
281
+ <div class="result-status status-found">Found</div>
282
+ </div>
283
+ <div class="result-details">
284
+ <div class="result-detail result-url">
285
+ <strong>URL:</strong> <a href="${url}" target="_blank">${url}</a>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ `;
290
+ });
291
+
292
+ // Missing documents
293
+ data.missing.forEach(docId => {
294
+ html += `
295
+ <div class="result-item">
296
+ <div class="result-header">
297
+ <div class="result-id">${docId}</div>
298
+ <div class="result-status status-missing">Not Found</div>
299
+ </div>
300
+ <div class="result-details">
301
+ <div class="result-detail">Document not found or not indexed</div>
302
+ </div>
303
+ </div>
304
+ `;
305
+ });
306
+
307
+ resultsContent.innerHTML = html;
308
+ showResults();
309
+ }
310
+
311
+ function displaySearchResults(data) {
312
+ const resultsContent = document.getElementById('results-content');
313
+ currentResults = data.results;
314
+
315
+ let html = '';
316
+
317
+ data.results.forEach((spec, index) => {
318
+ const hasContent = spec.contains && Object.keys(spec.contains).length > 0;
319
+
320
+ html += `
321
+ <div class="result-item">
322
+ <div class="result-header">
323
+ <div class="result-id">${spec.id}</div>
324
+ <div class="result-status status-found">${spec.type || 'Specification'}</div>
325
+ </div>
326
+ <div class="result-details">
327
+ <div class="result-detail"><strong>Title:</strong> ${spec.title}</div>
328
+ ${spec.version ? `<div class="result-detail"><strong>Version:</strong> ${spec.version}</div>` : ''}
329
+ ${spec.working_group ? `<div class="result-detail"><strong>Working Group:</strong> ${spec.working_group}</div>` : ''}
330
+ ${spec.type ? `<div class="result-detail"><strong>Type:</strong> ${spec.type}</div>` : ''}
331
+ ${spec.scope ? `<div class="result-detail"><strong>Scope:</strong> ${spec.scope}</div>` : ''}
332
+ ${hasContent ? `<button class="view-content-btn" onclick="viewContent(${index})">View Content</button>` : ''}
333
+ </div>
334
+ </div>
335
+ `;
336
+ });
337
+
338
+ resultsContent.innerHTML = html;
339
+ showResults();
340
+ }
341
+
342
+ // Content display functions
343
+ function viewContent(index) {
344
+ const spec = currentResults[index];
345
+
346
+ if (!spec.contains) return;
347
+
348
+ document.getElementById('content-title').textContent = `${spec.id} - ${spec.title}`;
349
+
350
+ const contentSections = document.getElementById('content-sections');
351
+ let html = '';
352
+
353
+ Object.entries(spec.contains).forEach(([sectionTitle, content]) => {
354
+ html += `
355
+ <div class="content-section">
356
+ <h3>${sectionTitle}</h3>
357
+ <p>${content}</p>
358
+ <button class="copy-section-btn" onclick="copyText('${content.replace(/'/g, "\\'")}')">
359
+ Copy this section
360
+ </button>
361
+ </div>
362
+ `;
363
+ });
364
+
365
+ contentSections.innerHTML = html;
366
+ showContentPage();
367
+ }
368
+
369
+ function closeContentPage() {
370
+ hideContentPage();
371
+ }
372
+
373
+ function copyAllContent() {
374
+ const sections = document.querySelectorAll('.content-section p');
375
+ const allText = Array.from(sections).map(p => p.textContent).join('\n\n');
376
+ copyText(allText);
377
+ }
378
+
379
+ function copyText(text) {
380
+ navigator.clipboard.writeText(text).then(() => {
381
+ showSuccess('Text copied to clipboard');
382
+ }).catch(() => {
383
+ showError('Error copying text');
384
+ });
385
+ }
386
+
387
+ // Interface utilities
388
+ function showLoading() {
389
+ document.getElementById('loading-container').style.display = 'flex';
390
+ hideResults();
391
+ hideError();
392
+ }
393
+
394
+ function hideLoading() {
395
+ document.getElementById('loading-container').style.display = 'none';
396
+ }
397
+
398
+ function showResults() {
399
+ document.getElementById('results-container').style.display = 'block';
400
+ hideError();
401
+ }
402
+
403
+ function hideResults() {
404
+ document.getElementById('results-container').style.display = 'none';
405
+ }
406
+
407
+ function showContentPage() {
408
+ document.getElementById('content-page').classList.add('active');
409
+ }
410
+
411
+ function hideContentPage() {
412
+ document.getElementById('content-page').classList.remove('active');
413
+ }
414
+
415
+ function showError(message) {
416
+ hideError();
417
+ const errorDiv = document.createElement('div');
418
+ errorDiv.className = 'error-message';
419
+ errorDiv.textContent = message;
420
+ document.querySelector('.search-container').appendChild(errorDiv);
421
+
422
+ setTimeout(() => {
423
+ hideError();
424
+ }, 5000);
425
+ }
426
+
427
+ function showSuccess(message) {
428
+ hideError();
429
+ const successDiv = document.createElement('div');
430
+ successDiv.className = 'success-message';
431
+ successDiv.textContent = message;
432
+ document.querySelector('.search-container').appendChild(successDiv);
433
+
434
+ setTimeout(() => {
435
+ hideError();
436
+ }, 3000);
437
+ }
438
+
439
+ function hideError() {
440
+ const existingMessages = document.querySelectorAll('.error-message, .success-message');
441
+ existingMessages.forEach(msg => msg.remove());
442
+ }
443
+
444
+ function updateHeaderStats(text) {
445
+ document.getElementById('header-stats').innerHTML = `<span class="stat-item">${text}</span>`;
446
+ }
static/style.css ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Dark Theme Variables */
2
+ :root {
3
+ --bg-primary: #0d1117;
4
+ --bg-secondary: #161b22;
5
+ --bg-tertiary: #21262d;
6
+ --bg-accent: #30363d;
7
+ --text-primary: #e6edf3;
8
+ --text-secondary: #7d8590;
9
+ --text-accent: #58a6ff;
10
+ --border-color: #30363d;
11
+ --border-accent: #58a6ff;
12
+ --success-color: #238636;
13
+ --warning-color: #d29922;
14
+ --error-color: #da3633;
15
+ --shadow: rgba(0, 0, 0, 0.3);
16
+ }
17
+
18
+ /* Reset and Base Styles */
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
27
+ background-color: var(--bg-primary);
28
+ color: var(--text-primary);
29
+ line-height: 1.6;
30
+ }
31
+
32
+ .app-container {
33
+ min-height: 100vh;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* Header */
39
+ .header {
40
+ background-color: var(--bg-secondary);
41
+ border-bottom: 1px solid var(--border-color);
42
+ padding: 1rem 0;
43
+ position: sticky;
44
+ top: 0;
45
+ z-index: 1000;
46
+ }
47
+
48
+ .header-content {
49
+ max-width: 1200px;
50
+ margin: 0 auto;
51
+ padding: 0 2rem;
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ }
56
+
57
+ .logo {
58
+ font-size: 1.5rem;
59
+ font-weight: 600;
60
+ color: var(--text-accent);
61
+ }
62
+
63
+ .header-stats {
64
+ display: flex;
65
+ gap: 1rem;
66
+ }
67
+
68
+ .stat-item {
69
+ font-size: 0.875rem;
70
+ color: var(--text-secondary);
71
+ }
72
+
73
+ /* Main Content */
74
+ .main-content {
75
+ flex: 1;
76
+ max-width: 1200px;
77
+ margin: 0 auto;
78
+ padding: 2rem;
79
+ width: 100%;
80
+ }
81
+
82
+ /* Search Tabs */
83
+ .search-tabs {
84
+ display: flex;
85
+ gap: 0.5rem;
86
+ margin-bottom: 2rem;
87
+ border-bottom: 1px solid var(--border-color);
88
+ }
89
+
90
+ .tab-button {
91
+ background: none;
92
+ border: none;
93
+ color: var(--text-secondary);
94
+ padding: 0.75rem 1.5rem;
95
+ font-size: 0.875rem;
96
+ font-weight: 500;
97
+ cursor: pointer;
98
+ transition: all 0.2s;
99
+ border-bottom: 2px solid transparent;
100
+ }
101
+
102
+ .tab-button.active {
103
+ color: var(--text-accent);
104
+ border-bottom-color: var(--border-accent);
105
+ }
106
+
107
+ .tab-button:hover {
108
+ color: var(--text-primary);
109
+ }
110
+
111
+ /* Search Container */
112
+ .search-container {
113
+ background-color: var(--bg-secondary);
114
+ border-radius: 0.75rem;
115
+ padding: 2rem;
116
+ margin-bottom: 2rem;
117
+ border: 1px solid var(--border-color);
118
+ }
119
+
120
+ .search-form {
121
+ display: none;
122
+ }
123
+
124
+ .search-form.active {
125
+ display: block;
126
+ }
127
+
128
+ .form-group {
129
+ margin-bottom: 1.5rem;
130
+ }
131
+
132
+ .form-group label {
133
+ display: block;
134
+ margin-bottom: 0.5rem;
135
+ font-weight: 500;
136
+ color: var(--text-primary);
137
+ }
138
+
139
+ .input-group {
140
+ display: flex;
141
+ gap: 0.75rem;
142
+ }
143
+
144
+ .input-group input {
145
+ flex: 1;
146
+ }
147
+
148
+ input[type="text"],
149
+ input[type="number"],
150
+ textarea,
151
+ select {
152
+ background-color: var(--bg-tertiary);
153
+ border: 1px solid var(--border-color);
154
+ border-radius: 0.5rem;
155
+ padding: 0.75rem;
156
+ color: var(--text-primary);
157
+ font-size: 0.875rem;
158
+ transition: border-color 0.2s;
159
+ }
160
+
161
+ input[type="text"]:focus,
162
+ input[type="number"]:focus,
163
+ textarea:focus,
164
+ select:focus {
165
+ outline: none;
166
+ border-color: var(--border-accent);
167
+ box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
168
+ }
169
+
170
+ textarea {
171
+ resize: vertical;
172
+ min-height: 120px;
173
+ }
174
+
175
+ /* Filters */
176
+ .filters-container {
177
+ display: grid;
178
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
179
+ gap: 1rem;
180
+ margin-bottom: 1.5rem;
181
+ padding: 1.5rem;
182
+ background-color: var(--bg-tertiary);
183
+ border-radius: 0.5rem;
184
+ border: 1px solid var(--border-color);
185
+ }
186
+
187
+ .filter-group {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 0.5rem;
191
+ }
192
+
193
+ .filter-group label {
194
+ font-size: 0.75rem;
195
+ font-weight: 500;
196
+ color: var(--text-secondary);
197
+ text-transform: uppercase;
198
+ letter-spacing: 0.025em;
199
+ }
200
+
201
+ .checkbox-group {
202
+ display: flex;
203
+ align-items: center;
204
+ }
205
+
206
+ .checkbox-label {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 0.5rem;
210
+ cursor: pointer;
211
+ font-size: 0.875rem;
212
+ }
213
+
214
+ .checkbox-label input[type="checkbox"] {
215
+ width: 1rem;
216
+ height: 1rem;
217
+ accent-color: var(--text-accent);
218
+ }
219
+
220
+ /* Buttons */
221
+ .btn {
222
+ background-color: var(--text-accent);
223
+ color: var(--bg-primary);
224
+ border: none;
225
+ border-radius: 0.5rem;
226
+ padding: 0.75rem 1.5rem;
227
+ font-size: 0.875rem;
228
+ font-weight: 500;
229
+ cursor: pointer;
230
+ transition: all 0.2s;
231
+ }
232
+
233
+ .btn:hover {
234
+ background-color: #4493f8;
235
+ transform: translateY(-1px);
236
+ }
237
+
238
+ .btn-secondary {
239
+ background-color: var(--bg-accent);
240
+ color: var(--text-primary);
241
+ }
242
+
243
+ .btn-secondary:hover {
244
+ background-color: #484f58;
245
+ }
246
+
247
+ .btn-outline {
248
+ background-color: transparent;
249
+ color: var(--text-accent);
250
+ border: 1px solid var(--border-accent);
251
+ }
252
+
253
+ .btn-outline:hover {
254
+ background-color: var(--text-accent);
255
+ color: var(--bg-primary);
256
+ }
257
+
258
+ /* Results Container */
259
+ .results-container {
260
+ background-color: var(--bg-secondary);
261
+ border-radius: 0.75rem;
262
+ padding: 2rem;
263
+ border: 1px solid var(--border-color);
264
+ display: none;
265
+ }
266
+
267
+ .results-header {
268
+ display: flex;
269
+ justify-content: space-between;
270
+ align-items: center;
271
+ margin-bottom: 1.5rem;
272
+ padding-bottom: 1rem;
273
+ border-bottom: 1px solid var(--border-color);
274
+ }
275
+
276
+ .results-stats {
277
+ color: var(--text-secondary);
278
+ font-size: 0.875rem;
279
+ }
280
+
281
+ .results-content {
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 1rem;
285
+ }
286
+
287
+ .result-item {
288
+ background-color: var(--bg-tertiary);
289
+ border: 1px solid var(--border-color);
290
+ border-radius: 0.5rem;
291
+ padding: 1.5rem;
292
+ transition: all 0.2s;
293
+ }
294
+
295
+ .result-item:hover {
296
+ border-color: var(--border-accent);
297
+ transform: translateY(-2px);
298
+ }
299
+
300
+ .result-header {
301
+ display: flex;
302
+ justify-content: space-between;
303
+ align-items: center;
304
+ margin-bottom: 1rem;
305
+ }
306
+
307
+ .result-id {
308
+ font-size: 1.125rem;
309
+ font-weight: 600;
310
+ color: var(--text-accent);
311
+ }
312
+
313
+ .result-status {
314
+ padding: 0.25rem 0.75rem;
315
+ border-radius: 1rem;
316
+ font-size: 0.75rem;
317
+ font-weight: 500;
318
+ text-transform: uppercase;
319
+ }
320
+
321
+ .status-found {
322
+ background-color: rgba(35, 134, 54, 0.2);
323
+ color: var(--success-color);
324
+ }
325
+
326
+ .status-missing {
327
+ background-color: rgba(218, 54, 51, 0.2);
328
+ color: var(--error-color);
329
+ }
330
+
331
+ .result-details {
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: 0.5rem;
335
+ margin-bottom: 1rem;
336
+ }
337
+
338
+ .result-detail {
339
+ font-size: 0.875rem;
340
+ }
341
+
342
+ .result-detail strong {
343
+ color: var(--text-primary);
344
+ }
345
+
346
+ .result-url {
347
+ word-break: break-all;
348
+ }
349
+
350
+ .result-url a {
351
+ color: var(--text-accent);
352
+ text-decoration: none;
353
+ }
354
+
355
+ .result-url a:hover {
356
+ text-decoration: underline;
357
+ }
358
+
359
+ .view-content-btn {
360
+ background-color: var(--bg-accent);
361
+ color: var(--text-primary);
362
+ border: 1px solid var(--border-color);
363
+ border-radius: 0.25rem;
364
+ padding: 0.5rem 1rem;
365
+ font-size: 0.75rem;
366
+ cursor: pointer;
367
+ transition: all 0.2s;
368
+ }
369
+
370
+ .view-content-btn:hover {
371
+ background-color: var(--text-accent);
372
+ color: var(--bg-primary);
373
+ }
374
+
375
+ /* Loading */
376
+ .loading-container {
377
+ display: none;
378
+ flex-direction: column;
379
+ align-items: center;
380
+ justify-content: center;
381
+ padding: 3rem;
382
+ color: var(--text-secondary);
383
+ }
384
+
385
+ .spinner {
386
+ width: 2rem;
387
+ height: 2rem;
388
+ border: 2px solid var(--border-color);
389
+ border-top: 2px solid var(--text-accent);
390
+ border-radius: 50%;
391
+ animation: spin 1s linear infinite;
392
+ margin-bottom: 1rem;
393
+ }
394
+
395
+ @keyframes spin {
396
+ 0% { transform: rotate(0deg); }
397
+ 100% { transform: rotate(360deg); }
398
+ }
399
+
400
+ /* Content Page */
401
+ .content-page {
402
+ position: fixed;
403
+ top: 0;
404
+ left: 0;
405
+ right: 0;
406
+ bottom: 0;
407
+ background-color: var(--bg-primary);
408
+ z-index: 2000;
409
+ overflow-y: auto;
410
+ display: none;
411
+ }
412
+
413
+ .content-page.active {
414
+ display: block;
415
+ }
416
+
417
+ .content-header {
418
+ background-color: var(--bg-secondary);
419
+ border-bottom: 1px solid var(--border-color);
420
+ padding: 1rem 2rem;
421
+ display: flex;
422
+ justify-content: space-between;
423
+ align-items: center;
424
+ position: sticky;
425
+ top: 0;
426
+ z-index: 100;
427
+ }
428
+
429
+ .content-sections {
430
+ max-width: 1200px;
431
+ margin: 0 auto;
432
+ padding: 2rem;
433
+ }
434
+
435
+ .content-section {
436
+ background-color: var(--bg-secondary);
437
+ border: 1px solid var(--border-color);
438
+ border-radius: 0.5rem;
439
+ padding: 1.5rem;
440
+ margin-bottom: 1.5rem;
441
+ }
442
+
443
+ .content-section h3 {
444
+ color: var(--text-accent);
445
+ margin-bottom: 1rem;
446
+ font-size: 1.125rem;
447
+ }
448
+
449
+ .content-section p {
450
+ color: var(--text-primary);
451
+ line-height: 1.7;
452
+ margin-bottom: 1rem;
453
+ }
454
+
455
+ .copy-section-btn {
456
+ background-color: var(--bg-accent);
457
+ color: var(--text-primary);
458
+ border: 1px solid var(--border-color);
459
+ border-radius: 0.25rem;
460
+ padding: 0.5rem 1rem;
461
+ font-size: 0.75rem;
462
+ cursor: pointer;
463
+ transition: all 0.2s;
464
+ }
465
+
466
+ .copy-section-btn:hover {
467
+ background-color: var(--text-accent);
468
+ color: var(--bg-primary);
469
+ }
470
+
471
+ /* Responsive Design */
472
+ @media (max-width: 768px) {
473
+ .header-content {
474
+ padding: 0 1rem;
475
+ }
476
+
477
+ .main-content {
478
+ padding: 1rem;
479
+ }
480
+
481
+ .search-container {
482
+ padding: 1.5rem;
483
+ }
484
+
485
+ .filters-container {
486
+ grid-template-columns: 1fr;
487
+ }
488
+
489
+ .input-group {
490
+ flex-direction: column;
491
+ }
492
+
493
+ .results-header {
494
+ flex-direction: column;
495
+ gap: 0.5rem;
496
+ align-items: flex-start;
497
+ }
498
+
499
+ .content-header {
500
+ flex-direction: column;
501
+ gap: 1rem;
502
+ align-items: flex-start;
503
+ }
504
+
505
+ .content-sections {
506
+ padding: 1rem;
507
+ }
508
+
509
+ .search-tabs {
510
+ flex-wrap: wrap;
511
+ }
512
+
513
+ .tab-button {
514
+ flex: 1;
515
+ min-width: 120px;
516
+ }
517
+ }
518
+
519
+ /* Error States */
520
+ .error-message {
521
+ background-color: rgba(218, 54, 51, 0.1);
522
+ color: var(--error-color);
523
+ padding: 1rem;
524
+ border-radius: 0.5rem;
525
+ border: 1px solid rgba(218, 54, 51, 0.2);
526
+ margin-bottom: 1rem;
527
+ }
528
+
529
+ .success-message {
530
+ background-color: rgba(35, 134, 54, 0.1);
531
+ color: var(--success-color);
532
+ padding: 1rem;
533
+ border-radius: 0.5rem;
534
+ border: 1px solid rgba(35, 134, 54, 0.2);
535
+ margin-bottom: 1rem;
536
+ }
templates/index.html ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>3GPP & ETSI Document Finder</title>
7
+ <link rel="stylesheet" href="/static/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="app-container">
11
+ <!-- Header -->
12
+ <header class="header">
13
+ <div class="header-content">
14
+ <h1 class="logo">3GPP & ETSI Document Finder</h1>
15
+ <div class="header-stats" id="header-stats">
16
+ <span class="stat-item">Ready</span>
17
+ </div>
18
+ </div>
19
+ </header>
20
+
21
+ <!-- Main Content -->
22
+ <main class="main-content">
23
+ <!-- Search Tabs -->
24
+ <div class="search-tabs">
25
+ <button class="tab-button active" data-mode="single">Single Document</button>
26
+ <button class="tab-button" data-mode="batch">Batch Search</button>
27
+ <button class="tab-button" data-mode="keyword">Keyword Search</button>
28
+ <button class="tab-button" data-mode="bm25">BM25 Search</button>
29
+ </div>
30
+
31
+ <!-- Search Forms -->
32
+ <div class="search-container">
33
+ <!-- Single Document Search -->
34
+ <div class="search-form active" id="single-form">
35
+ <div class="form-group">
36
+ <label for="doc-id">Document ID</label>
37
+ <div class="input-group">
38
+ <input type="text" id="doc-id" placeholder="e.g., 23.401, S1-123456, SET(24)123">
39
+ <button class="btn btn-primary" onclick="searchSingle()">Search</button>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Batch Search -->
45
+ <div class="search-form" id="batch-form">
46
+ <div class="form-group">
47
+ <label for="batch-ids">List of IDs (one per line)</label>
48
+ <textarea id="batch-ids" placeholder="23.401&#10;S1-123456&#10;103 666" rows="6"></textarea>
49
+ <button class="btn btn-primary" onclick="searchBatch()">Search Batch</button>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Keyword Search -->
54
+ <div class="search-form" id="keyword-form">
55
+ <div class="form-group">
56
+ <label for="keywords">Keywords</label>
57
+ <input type="text" id="keywords" placeholder="e.g., 5G NR,authentication">
58
+ </div>
59
+
60
+ <div class="filters-container">
61
+ <div class="filter-group">
62
+ <label>Source</label>
63
+ <select id="source-filter">
64
+ <option value="all">All Sources</option>
65
+ <option value="3GPP">3GPP Only</option>
66
+ <option value="ETSI">ETSI Only</option>
67
+ </select>
68
+ </div>
69
+
70
+ <div class="filter-group">
71
+ <label>Search Mode</label>
72
+ <select id="search-mode-filter">
73
+ <option value="quick">Quick Search</option>
74
+ <option value="deep">Deep Search</option>
75
+ </select>
76
+ </div>
77
+
78
+ <div class="filter-group">
79
+ <label>Logic</label>
80
+ <select id="mode-filter">
81
+ <option value="and">AND (all words)</option>
82
+ <option value="or">OR (any word)</option>
83
+ </select>
84
+ </div>
85
+
86
+ <div class="filter-group">
87
+ <label>Specification Type</label>
88
+ <select id="spec-type-filter">
89
+ <option value="">All Types</option>
90
+ <option value="TS">TS (Technical Specification)</option>
91
+ <option value="TR">TR (Technical Report)</option>
92
+ </select>
93
+ </div>
94
+
95
+ <div class="checkbox-group">
96
+ <label class="checkbox-label">
97
+ <input type="checkbox" id="case-sensitive-filter">
98
+ <span class="checkmark"></span>
99
+ Case Sensitive
100
+ </label>
101
+ </div>
102
+ </div>
103
+
104
+ <button class="btn btn-primary" onclick="searchKeyword()">Search</button>
105
+ </div>
106
+
107
+ <!-- BM25 Search -->
108
+ <div class="search-form" id="bm25-form">
109
+ <div class="form-group">
110
+ <label for="bm25-keywords">Search Query</label>
111
+ <input type="text" id="bm25-keywords" placeholder="e.g., 5G authentication procedures">
112
+ </div>
113
+
114
+ <div class="filters-container">
115
+ <div class="filter-group">
116
+ <label>Source</label>
117
+ <select id="bm25-source-filter">
118
+ <option value="all">All Sources</option>
119
+ <option value="3GPP">3GPP Only</option>
120
+ <option value="ETSI">ETSI Only</option>
121
+ </select>
122
+ </div>
123
+
124
+ <div class="filter-group">
125
+ <label>Specification Type</label>
126
+ <select id="bm25-spec-type-filter">
127
+ <option value="">All Types</option>
128
+ <option value="TS">TS (Technical Specification)</option>
129
+ <option value="TR">TR (Technical Report)</option>
130
+ </select>
131
+ </div>
132
+
133
+ <div class="filter-group">
134
+ <label>Relevance Threshold (%)</label>
135
+ <input type="number" id="threshold" min="0" max="100" value="60">
136
+ </div>
137
+ </div>
138
+
139
+ <button class="btn btn-primary" onclick="searchBM25()">Search</button>
140
+ </div>
141
+ </div>
142
+
143
+ <!-- Results Container -->
144
+ <div class="results-container" id="results-container">
145
+ <div class="results-header">
146
+ <h2>Search Results</h2>
147
+ <div class="results-stats" id="results-stats"></div>
148
+ </div>
149
+ <div class="results-content" id="results-content"></div>
150
+ </div>
151
+
152
+ <!-- Loading Indicator -->
153
+ <div class="loading-container" id="loading-container">
154
+ <div class="spinner"></div>
155
+ <p>Searching...</p>
156
+ </div>
157
+ </main>
158
+
159
+ <!-- Content Detail Page -->
160
+ <div class="content-page" id="content-page">
161
+ <div class="content-header">
162
+ <button class="btn btn-secondary" onclick="closeContentPage()">← Back to Results</button>
163
+ <h2 id="content-title"></h2>
164
+ <button class="btn btn-outline" onclick="copyAllContent()">Copy All</button>
165
+ </div>
166
+ <div class="content-sections" id="content-sections"></div>
167
+ </div>
168
+ </div>
169
+
170
+ <script src="/static/script.js"></script>
171
+ </body>
172
+ </html>