ntdservices commited on
Commit
e3f391f
·
verified ·
1 Parent(s): 2b041e7

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +110 -303
templates/index.html CHANGED
@@ -6,24 +6,14 @@
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
  <style>
8
  :root{
9
- --bg: #0f1226;
10
- --bg2:#111639;
11
- --card:#151a3b;
12
- --ink:#e9ecff;
13
- --muted:#b7c0ffcc;
14
- --accent:#7c9cff;
15
- --accent2:#61e7ff;
16
- --danger:#ff6b6b;
17
- --ok:#35d39e;
18
- --ring: 0 0 0 3px color-mix(in oklab, var(--accent) 35%, transparent);
19
  --radius: 18px;
20
  --shadow: 0 10px 30px rgba(0,0,0,.35), 0 2px 8px rgba(0,0,0,.25);
21
  }
22
-
23
  html,body{height:100%;}
24
  body{
25
- margin:0;
26
- color:var(--ink);
27
  background:
28
  radial-gradient(1200px 800px at 10% -10%, #2630a540, transparent 60%),
29
  radial-gradient(1000px 700px at 110% 20%, #1fb6ff26, transparent 60%),
@@ -31,22 +21,13 @@
31
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
32
  line-height:1.45;
33
  }
34
-
35
- .wrap{
36
- max-width: 980px;
37
- margin: 48px auto 120px;
38
- padding: 0 20px;
39
- }
40
- .title{
41
- display:flex; align-items:center; gap:12px; margin:0 0 18px;
42
- letter-spacing:.25px;
43
- }
44
  .title .badge{
45
  padding:6px 10px; font-size:12px; border:1px solid #ffffff22; border-radius:999px; color:var(--muted);
46
  background: linear-gradient(180deg,#ffffff05,#00000020);
47
  box-shadow: inset 0 0 0 1px #ffffff08;
48
  }
49
-
50
  .card{
51
  background: linear-gradient(180deg, #ffffff08, #00000022);
52
  border: 1px solid #ffffff1c;
@@ -55,138 +36,46 @@
55
  overflow: clip;
56
  backdrop-filter: blur(8px);
57
  }
58
- .card .hdr{
59
- padding:14px 18px;
60
- border-bottom:1px solid #ffffff12;
61
- display:flex; align-items:center; gap:10px; justify-content:space-between;
62
- }
63
- .card .hdr h2{margin:0; font-size:18px; font-weight:700; color:#fff;}
64
  .card .body{ padding:18px; }
65
 
66
  .uploader{ display:grid; gap:14px; }
67
-
68
- #fileUpload{
69
- position: absolute;
70
- width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0);
71
- white-space:nowrap; border:0; padding:0; margin:-1px;
72
- }
73
-
74
- .drop{
75
- display:grid; place-items:center; text-align:center;
76
- padding:28px; border:1.5px dashed #a8b0ff55; border-radius: calc(var(--radius) - 6px);
77
- background: linear-gradient(180deg,#ffffff08,#00000018);
78
- cursor: pointer;
79
- transition: .18s ease;
80
- outline: none;
81
- position: relative;
82
- min-height: 150px;
83
- }
84
  .drop:hover{ border-color:#c7d0ff77; transform: translateY(-1px);}
85
- .drop:focus-visible{ box-shadow: var(--ring); }
86
- .drop.dragover{
87
- border-color: var(--accent2);
88
- box-shadow: 0 0 0 2px #61e7ff33 inset, 0 0 48px #61e7ff25;
89
- transform: translateY(-1px);
90
- }
91
- .drop .big{
92
- font-size:28px; font-weight:800; margin-bottom:6px;
93
- background: linear-gradient(90deg, var(--accent), var(--accent2));
94
- -webkit-background-clip:text; background-clip:text; color:transparent;
95
- }
96
  .drop .small{ color:var(--muted); font-size:14px; }
97
- .drop .hint{ margin-top:10px; font-size:13px; color:#cbd3ff; opacity:.85; }
98
-
99
  .row{ display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
100
-
101
- .btn{
102
- --btn-bg: #2331a6;
103
- --btn-fg: #eaf0ff;
104
- display:inline-flex; align-items:center; gap:8px;
105
- padding:10px 14px; border-radius:12px; border:1px solid #ffffff18;
106
- background: linear-gradient(180deg, color-mix(in oklab, var(--btn-bg) 88%, #fff 0%), #0a0f3a);
107
- color:var(--btn-fg); font-weight:700; letter-spacing:.2px; text-decoration:none;
108
- box-shadow: 0 6px 18px rgba(15, 30, 120, .35), inset 0 0 0 1px #ffffff10;
109
- cursor:pointer; transition:.18s ease;
110
- }
111
- .btn:hover{ transform: translateY(-1px); }
112
- .btn:active{ transform: translateY(0px) scale(.99); }
113
  .btn.secondary{ --btn-bg:#1a213f; --btn-fg:#dbe2ff; }
114
- .btn.danger { --btn-bg:#4a1020; --btn-fg:#ffd7df; border-color:#ff6b6b44; }
115
-
116
- progress{
117
- width: 320px; height: 14px; border-radius: 999px; overflow:hidden; vertical-align: middle;
118
- background: #293079;
119
- border:1px solid #ffffff22;
120
- }
121
  progress::-webkit-progress-bar{ background:#293079; }
122
- progress::-webkit-progress-value{
123
- background: linear-gradient(90deg, var(--accent), var(--accent2));
124
- }
125
- #uploadStatus, #searchStatus{ margin-left:10px; font-weight:700; color:#d5dcff; }
126
-
127
  #uploadedListClient ul{ list-style:none; padding:0; margin:8px 0 0; display:flex; flex-wrap:wrap; gap:8px;}
128
- #uploadedListClient li{
129
- padding:6px 10px; border-radius:999px; font-size:12px; color:#dfe5ff;
130
- background: linear-gradient(180deg,#ffffff0a,#00000025); border:1px solid #ffffff1a;
131
- }
132
-
133
  label{ color:#cdd4ff; font-weight:600; letter-spacing:.2px; }
134
- textarea, input[type="number"]{
135
- width:100%; box-sizing:border-box; color:#fff; background:#0d1333; border:1px solid #ffffff22;
136
- border-radius:12px; padding:12px 14px; outline:none; transition:.18s ease;
137
- box-shadow: inset 0 0 0 1px #ffffff08;
138
- }
139
- textarea:focus, input[type="number"]:focus{ box-shadow: var(--ring); border-color:#9fb2ff77; }
140
-
141
  hr{ border:none; height:1px; background:#ffffff1a; margin:26px 0; }
142
-
143
  ol{ padding-left: 22px; }
144
  ol li{ margin: 12px 0; }
145
  a{ color: var(--accent2); }
146
-
147
  .muted{ color:var(--muted); font-size:13px; }
148
- .cluster{ display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
149
-
150
- /* Sidebar for context */
151
- .sidebar-backdrop{
152
- position: fixed;
153
- inset: 0;
154
- background: rgba(0,0,0,.35);
155
- opacity: 0;
156
- pointer-events: none;
157
- transition: .18s ease;
158
- z-index: 60;
159
- }
160
- .sidebar{
161
- position: fixed;
162
- top: 0; right: 0; bottom: 0;
163
- width: min(520px, 90vw);
164
- background: linear-gradient(180deg, #0e1330, #0a0f28);
165
- border-left: 1px solid #ffffff22;
166
- box-shadow: -16px 0 40px rgba(0,0,0,.35);
167
- transform: translateX(100%);
168
- transition: transform .22s ease;
169
- z-index: 70;
170
- display: grid;
171
- grid-template-rows: auto 1fr;
172
- }
173
- .sidebar.open{ transform: translateX(0); }
174
- .sidebar-backdrop.open{ opacity: 1; pointer-events: auto; }
175
- .sidebar .sbar-hdr{
176
- display:flex; align-items:center; justify-content:space-between; gap:10px;
177
- padding:14px 16px; border-bottom:1px solid #ffffff1a;
178
- }
179
- .sidebar .sbar-hdr h3{ margin:0; font-size:16px; }
180
- .sidebar .sbar-body{
181
- overflow: auto; padding: 16px;
182
- }
183
- .ctx-p{ margin: 12px 0; padding: 10px 12px; border-radius: 12px; background: #ffffff06; border:1px solid #ffffff14; }
184
- .ctx-p.hl{ outline: 2px solid #61e7ff; box-shadow: 0 0 0 3px #61e7ff22; }
185
 
186
- .smallbtn{
187
- font-size:12px; padding:6px 8px; border-radius:10px;
188
- background: #1a213f; color:#dbe2ff; border:1px solid #ffffff18; cursor:pointer;
189
- }
 
 
 
 
 
 
 
190
  </style>
191
  </head>
192
  <body>
@@ -204,7 +93,7 @@
204
  <section class="card" aria-labelledby="upl">
205
  <div class="hdr">
206
  <h2 id="upl">Upload Text or PDF Files</h2>
207
- <div class="cluster">
208
  <button id="showFilesBtn" class="btn secondary" type="button">📄 Show Uploaded Files</button>
209
  </div>
210
  </div>
@@ -214,7 +103,6 @@
214
  <div id="dropZone" class="drop" tabindex="0" role="button" aria-label="Drop files here or click to browse">
215
  <div class="big">Drag & Drop files here</div>
216
  <div class="small">.txt, .pdf supported</div>
217
- <div class="hint">or <span style="text-decoration:underline">click to browse</span></div>
218
  </div>
219
 
220
  <div class="row">
@@ -222,16 +110,16 @@
222
  <span id="uploadStatus"></span>
223
  </div>
224
 
 
 
225
  {% if uploaded_filenames %}
226
- <h4 style="margin:14px 0 6px;">Uploaded Files</h4>
227
  <ul>
228
  {% for fname in uploaded_filenames %}
229
  <li>{{ fname }}</li>
230
  {% endfor %}
231
  </ul>
232
  {% endif %}
233
-
234
- <div id="uploadedListClient" style="display:none; margin-top:10px;"></div>
235
  </div>
236
  </div>
237
  </section>
@@ -260,7 +148,7 @@
260
  </div>
261
 
262
  <p class="muted" style="margin-top:10px;">
263
- ⚠️ Uploaded files may take a few seconds to process before they’re searchable. If results don’t appear when you first click “Retrieve,” wait a moment and try again.
264
  </p>
265
  </form>
266
 
@@ -271,21 +159,21 @@
271
  </form>
272
 
273
  {% if results %}
274
- <br>
275
- <div class="row">
276
- <a class="btn secondary" href="{{ url_for('download', sid=sid) }}">⬇️ Download these results</a>
277
- <a class="btn secondary" href="{{ url_for('download_merged', sid=sid) }}">📦 Download full merged text</a>
278
- </div>
279
 
280
- <h3 style="margin-top:18px;">Matching Paragraphs</h3>
281
- <ol>
282
- {% for r in results %}
283
- <li>
284
- <p>{{ r.text }}</p>
285
- <button type="button" class="smallbtn" onclick="openContext({{ r.idx }})">🔎 View context</button>
286
- </li>
287
- {% endfor %}
288
- </ol>
289
  {% endif %}
290
  </div>
291
  </section>
@@ -295,80 +183,71 @@
295
  <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
296
  <aside id="sidebar" class="sidebar" aria-hidden="true">
297
  <div class="sbar-hdr">
298
- <h3>Paragraph context</h3>
299
  <div style="display:flex; gap:8px; align-items:center;">
300
- <button class="smallbtn" id="expandLess" title="Show less context">−</button>
301
- <button class="smallbtn" id="expandMore" title="Show more context">+</button>
302
  <button class="smallbtn" id="closeSidebar">✕</button>
303
  </div>
304
  </div>
305
- <div id="sidebarBody" class="sbar-body"></div>
 
 
306
  </aside>
307
 
308
  <script>
309
  const SID = "{{ sid }}";
310
  let uploadedNames = [];
311
- let currentWindow = 3;
312
 
313
- // Upload (unchanged behavior)
314
- document.getElementById("fileUpload").addEventListener("change", function () {
315
- const files = this.files;
316
- if (files.length === 0) return;
317
 
 
 
318
  const formData = new FormData();
319
- for (const file of files) {
320
- formData.append("file", file);
321
- uploadedNames.push(file.name);
322
- }
323
 
324
  const xhr = new XMLHttpRequest();
325
  const progressBar = document.getElementById("uploadProgress");
326
  const statusText = document.getElementById("uploadStatus");
327
-
328
- xhr.open("POST", `/upload?sid=${SID}`, true);
329
 
330
  xhr.upload.onprogress = function (e) {
331
  if (e.lengthComputable) {
332
- const percent = Math.round((e.loaded / e.total) * 100);
333
- progressBar.value = percent;
334
  progressBar.style.display = "inline-block";
335
- statusText.textContent = `Uploading: ${percent}%`;
336
  }
337
  };
338
-
339
  xhr.onload = function () {
340
  progressBar.value = 100;
341
  statusText.textContent = "✅ Upload complete!";
342
- setTimeout(() => {
343
- progressBar.style.display = "none";
344
- statusText.textContent = "";
345
- }, 1500);
346
- };
347
-
348
- xhr.onerror = function () {
349
- statusText.textContent = "❌ Upload failed.";
350
  };
351
-
352
  xhr.send(formData);
353
- });
354
 
355
- // Show client-side list
356
- document.getElementById("showFilesBtn").addEventListener("click", function () {
357
- const listDiv = document.getElementById("uploadedListClient");
358
- listDiv.innerHTML = "";
359
 
 
 
360
  const ul = document.createElement("ul");
361
  for (const name of uploadedNames) {
362
  const li = document.createElement("li");
363
  li.textContent = name;
364
  ul.appendChild(li);
365
  }
366
-
367
- listDiv.appendChild(ul);
368
- listDiv.style.display = "block";
369
  });
370
 
371
- // Search progress (unchanged)
372
  document.querySelector('form[method="post"]').addEventListener("submit", function () {
373
  const progressBar = document.getElementById("searchProgress");
374
  const statusText = document.getElementById("searchStatus");
@@ -377,71 +256,14 @@
377
  statusText.textContent = "🔍 Searching...";
378
  });
379
 
380
- // Drag & drop (unchanged)
381
- const dropZone = document.getElementById('dropZone');
382
- const hiddenInput = document.getElementById('fileUpload');
383
- dropZone.addEventListener('click', () => hiddenInput.click());
384
- dropZone.addEventListener('keypress', (e) => {
385
- if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); hiddenInput.click(); }
386
- });
387
- ['dragenter','dragover'].forEach(evt => {
388
- dropZone.addEventListener(evt, e => {
389
- e.preventDefault(); e.stopPropagation();
390
- dropZone.classList.add('dragover');
391
- });
392
- });
393
- ['dragleave','dragend','drop'].forEach(evt => {
394
- dropZone.addEventListener(evt, () => dropZone.classList.remove('dragover'));
395
- });
396
- dropZone.addEventListener('drop', (e) => {
397
- e.preventDefault(); e.stopPropagation();
398
- const files = e.dataTransfer.files;
399
- if(!files || files.length === 0) return;
400
-
401
- const formData = new FormData();
402
- for (const file of files) {
403
- formData.append("file", file);
404
- uploadedNames.push(file.name);
405
- }
406
-
407
- const xhr = new XMLHttpRequest();
408
- const progressBar = document.getElementById("uploadProgress");
409
- const statusText = document.getElementById("uploadStatus");
410
-
411
- xhr.open("POST", `/upload?sid=${SID}`, true);
412
-
413
- xhr.upload.onprogress = function (e) {
414
- if (e.lengthComputable) {
415
- const percent = Math.round((e.loaded / e.total) * 100);
416
- progressBar.value = percent;
417
- progressBar.style.display = "inline-block";
418
- statusText.textContent = `Uploading: ${percent}%`;
419
- }
420
- };
421
-
422
- xhr.onload = function () {
423
- progressBar.value = 100;
424
- statusText.textContent = "✅ Upload complete!";
425
- setTimeout(() => {
426
- progressBar.style.display = "none";
427
- statusText.textContent = "";
428
- }, 1500);
429
- };
430
-
431
- xhr.onerror = function () {
432
- statusText.textContent = "❌ Upload failed.";
433
- };
434
-
435
- xhr.send(formData);
436
- });
437
-
438
- // ── Context sidebar logic ──────────────────────────────────────────────
439
  const sidebar = document.getElementById('sidebar');
440
  const sidebarBody = document.getElementById('sidebarBody');
441
  const backdrop = document.getElementById('sidebarBackdrop');
442
  const btnClose = document.getElementById('closeSidebar');
443
- const btnMore = document.getElementById('expandMore');
444
- const btnLess = document.getElementById('expandLess');
 
445
 
446
  function openSidebar(){
447
  sidebar.classList.add('open');
@@ -452,13 +274,24 @@
452
  sidebar.classList.remove('open');
453
  backdrop.classList.remove('open');
454
  sidebar.setAttribute('aria-hidden', 'true');
455
- sidebarBody.innerHTML = '';
456
  }
457
  btnClose.addEventListener('click', closeSidebar);
458
  backdrop.addEventListener('click', closeSidebar);
459
 
460
- async function fetchContext(idx, windowSize){
461
- const res = await fetch(`/api/context?sid=${encodeURIComponent(SID)}&idx=${idx}&window=${windowSize}`);
 
 
 
 
 
 
 
 
 
 
 
462
  if(!res.ok){
463
  const t = await res.text();
464
  throw new Error(t || 'Failed to fetch context');
@@ -466,59 +299,33 @@
466
  return res.json();
467
  }
468
 
469
- function renderContext(ctxJson){
470
- sidebarBody.innerHTML = '';
471
- ctxJson.paras.forEach((p, i) => {
472
- const div = document.createElement('div');
473
- div.className = 'ctx-p' + (i === ctxJson.center ? ' hl' : '');
474
- div.innerText = p;
475
- if(i === ctxJson.center){
476
- div.id = 'centerPara';
 
 
 
 
 
 
477
  }
478
- sidebarBody.appendChild(div);
479
  });
480
- // Scroll the highlighted one into view
481
- setTimeout(() => {
482
- const el = document.getElementById('centerPara');
483
- if(el){ el.scrollIntoView({behavior:'smooth', block:'center'}); }
484
- }, 0);
485
  }
486
 
487
- let lastIdx = null;
488
-
489
  window.openContext = async function(idx){
490
  try{
491
- lastIdx = idx;
492
- const data = await fetchContext(idx, currentWindow);
493
- renderContext(data);
494
  openSidebar();
495
  }catch(err){
496
  alert('Error: ' + err.message);
497
  }
498
  }
499
-
500
- btnMore.addEventListener('click', async () => {
501
- if(lastIdx === null) return;
502
- currentWindow = Math.min(currentWindow + 2, 20);
503
- const data = await fetchContext(lastIdx, currentWindow);
504
- renderContext(data);
505
- });
506
-
507
- btnLess.addEventListener('click', async () => {
508
- if(lastIdx === null) return;
509
- currentWindow = Math.max(1, currentWindow - 2);
510
- const data = await fetchContext(lastIdx, currentWindow);
511
- renderContext(data);
512
- });
513
-
514
- // Maintain existing search progress behavior
515
- document.querySelector('form[method="post"]').addEventListener("submit", function () {
516
- const progressBar = document.getElementById("searchProgress");
517
- const statusText = document.getElementById("searchStatus");
518
- progressBar.removeAttribute("value");
519
- progressBar.style.display = "inline-block";
520
- statusText.textContent = "🔍 Searching...";
521
- });
522
  </script>
523
  </body>
524
  </html>
 
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
  <style>
8
  :root{
9
+ --bg: #0f1226; --bg2:#111639; --ink:#e9ecff; --muted:#b7c0ffcc;
10
+ --accent:#7c9cff; --accent2:#61e7ff; --danger:#ff6b6b; --ok:#35d39e;
 
 
 
 
 
 
 
 
11
  --radius: 18px;
12
  --shadow: 0 10px 30px rgba(0,0,0,.35), 0 2px 8px rgba(0,0,0,.25);
13
  }
 
14
  html,body{height:100%;}
15
  body{
16
+ margin:0; color:var(--ink);
 
17
  background:
18
  radial-gradient(1200px 800px at 10% -10%, #2630a540, transparent 60%),
19
  radial-gradient(1000px 700px at 110% 20%, #1fb6ff26, transparent 60%),
 
21
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
22
  line-height:1.45;
23
  }
24
+ .wrap{ max-width: 980px; margin: 48px auto 120px; padding: 0 20px; }
25
+ .title{ display:flex; align-items:center; gap:12px; margin:0 0 18px; }
 
 
 
 
 
 
 
 
26
  .title .badge{
27
  padding:6px 10px; font-size:12px; border:1px solid #ffffff22; border-radius:999px; color:var(--muted);
28
  background: linear-gradient(180deg,#ffffff05,#00000020);
29
  box-shadow: inset 0 0 0 1px #ffffff08;
30
  }
 
31
  .card{
32
  background: linear-gradient(180deg, #ffffff08, #00000022);
33
  border: 1px solid #ffffff1c;
 
36
  overflow: clip;
37
  backdrop-filter: blur(8px);
38
  }
39
+ .card .hdr{ padding:14px 18px; border-bottom:1px solid #ffffff12; display:flex; align-items:center; justify-content:space-between; gap:10px; }
40
+ .card .hdr h2{ margin:0; font-size:18px; font-weight:700; color:#fff; }
 
 
 
 
41
  .card .body{ padding:18px; }
42
 
43
  .uploader{ display:grid; gap:14px; }
44
+ #fileUpload{ position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; padding:0; margin:-1px; }
45
+ .drop{ display:grid; place-items:center; text-align:center; padding:28px; border:1.5px dashed #a8b0ff55; border-radius: 12px; background: linear-gradient(180deg,#ffffff08,#00000018); cursor:pointer; transition:.18s ease; outline:none; position:relative; min-height:150px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  .drop:hover{ border-color:#c7d0ff77; transform: translateY(-1px);}
47
+ .drop .big{ font-size:28px; font-weight:800; margin-bottom:6px; background: linear-gradient(90deg, var(--accent), var(--accent2)); -webkit-background-clip:text; background-clip:text; color:transparent; }
 
 
 
 
 
 
 
 
 
 
48
  .drop .small{ color:var(--muted); font-size:14px; }
 
 
49
  .row{ display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
50
+ .btn{ --btn-bg:#2331a6; --btn-fg:#eaf0ff; display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border-radius:12px; border:1px solid #ffffff18; background: linear-gradient(180deg, color-mix(in oklab, var(--btn-bg) 88%, #fff 0%), #0a0f3a); color:var(--btn-fg); font-weight:700; letter-spacing:.2px; text-decoration:none; box-shadow: 0 6px 18px rgba(15, 30, 120, .35), inset 0 0 0 1px #ffffff10; cursor:pointer; transition:.18s ease; }
 
 
 
 
 
 
 
 
 
 
 
 
51
  .btn.secondary{ --btn-bg:#1a213f; --btn-fg:#dbe2ff; }
52
+ .btn.danger{ --btn-bg:#4a1020; --btn-fg:#ffd7df; border-color:#ff6b6b44; }
53
+ progress{ width:320px; height:14px; border-radius:999px; overflow:hidden; vertical-align:middle; background:#293079; border:1px solid #ffffff22; }
 
 
 
 
 
54
  progress::-webkit-progress-bar{ background:#293079; }
55
+ progress::-webkit-progress-value{ background: linear-gradient(90deg, var(--accent), var(--accent2)); }
56
+ #uploadStatus,#searchStatus{ margin-left:10px; font-weight:700; color:#d5dcff; }
 
 
 
57
  #uploadedListClient ul{ list-style:none; padding:0; margin:8px 0 0; display:flex; flex-wrap:wrap; gap:8px;}
58
+ #uploadedListClient li{ padding:6px 10px; border-radius:999px; font-size:12px; color:#dfe5ff; background: linear-gradient(180deg,#ffffff0a,#00000025); border:1px solid #ffffff1a; }
 
 
 
 
59
  label{ color:#cdd4ff; font-weight:600; letter-spacing:.2px; }
60
+ textarea, input[type="number"]{ width:100%; color:#fff; background:#0d1333; border:1px solid #ffffff22; border-radius:12px; padding:12px 14px; outline:none; transition:.18s ease; box-shadow: inset 0 0 0 1px #ffffff08; }
61
+ textarea:focus, input[type="number"]:focus{ box-shadow: 0 0 0 3px #7c9cff44; border-color:#9fb2ff77; }
 
 
 
 
 
62
  hr{ border:none; height:1px; background:#ffffff1a; margin:26px 0; }
 
63
  ol{ padding-left: 22px; }
64
  ol li{ margin: 12px 0; }
65
  a{ color: var(--accent2); }
 
66
  .muted{ color:var(--muted); font-size:13px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ /* Sidebar: full merged text */
69
+ .sidebar-backdrop{ position: fixed; inset: 0; background: rgba(0,0,0,.35); opacity:0; pointer-events:none; transition:.18s ease; z-index:60; }
70
+ .sidebar{ position: fixed; top:0; right:0; bottom:0; width:min(640px, 92vw); background: linear-gradient(180deg, #0e1330, #0a0f28); border-left:1px solid #ffffff22; box-shadow:-16px 0 40px rgba(0,0,0,.35); transform: translateX(100%); transition: transform .22s ease; z-index:70; display:grid; grid-template-rows:auto 1fr; }
71
+ .sidebar.open{ transform: translateX(0); }
72
+ .sidebar-backdrop.open{ opacity:1; pointer-events:auto; }
73
+ .sbar-hdr{ display:flex; align-items:center; justify-content:space-between; gap:10px; padding:14px 16px; border-bottom:1px solid #ffffff1a; }
74
+ .sbar-hdr h3{ margin:0; font-size:16px; }
75
+ .sbar-body{ overflow:auto; padding:16px; }
76
+ .smallbtn{ font-size:12px; padding:6px 8px; border-radius:10px; background:#1a213f; color:#dbe2ff; border:1px solid #ffffff18; cursor:pointer; }
77
+ pre#fullText{ margin:0; white-space:pre-wrap; word-wrap:break-word; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; line-height:1.5; }
78
+ mark#centerMark{ background:#61e7ff55; padding:0 .5px; border-radius:3px; }
79
  </style>
80
  </head>
81
  <body>
 
93
  <section class="card" aria-labelledby="upl">
94
  <div class="hdr">
95
  <h2 id="upl">Upload Text or PDF Files</h2>
96
+ <div class="row">
97
  <button id="showFilesBtn" class="btn secondary" type="button">📄 Show Uploaded Files</button>
98
  </div>
99
  </div>
 
103
  <div id="dropZone" class="drop" tabindex="0" role="button" aria-label="Drop files here or click to browse">
104
  <div class="big">Drag & Drop files here</div>
105
  <div class="small">.txt, .pdf supported</div>
 
106
  </div>
107
 
108
  <div class="row">
 
110
  <span id="uploadStatus"></span>
111
  </div>
112
 
113
+ <div id="uploadedListClient" style="display:none; margin-top:10px;"></div>
114
+
115
  {% if uploaded_filenames %}
116
+ <h4 style="margin:14px 0 6px;">Uploaded Files (server)</h4>
117
  <ul>
118
  {% for fname in uploaded_filenames %}
119
  <li>{{ fname }}</li>
120
  {% endfor %}
121
  </ul>
122
  {% endif %}
 
 
123
  </div>
124
  </div>
125
  </section>
 
148
  </div>
149
 
150
  <p class="muted" style="margin-top:10px;">
151
+ Tip: upload may take a moment to process before results appear.
152
  </p>
153
  </form>
154
 
 
159
  </form>
160
 
161
  {% if results %}
162
+ <br>
163
+ <div class="row">
164
+ <a class="btn secondary" href="{{ url_for('download', sid=sid) }}">⬇️ Download these results</a>
165
+ <a class="btn secondary" href="{{ url_for('download_merged', sid=sid) }}">📦 Download full merged text</a>
166
+ </div>
167
 
168
+ <h3 style="margin-top:18px;">Matching Paragraphs</h3>
169
+ <ol>
170
+ {% for r in results %}
171
+ <li>
172
+ <p>{{ r.text }}</p>
173
+ <button type="button" class="smallbtn" onclick="openContext({{ r.idx }})">🔎 View context</button>
174
+ </li>
175
+ {% endfor %}
176
+ </ol>
177
  {% endif %}
178
  </div>
179
  </section>
 
183
  <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
184
  <aside id="sidebar" class="sidebar" aria-hidden="true">
185
  <div class="sbar-hdr">
186
+ <h3>Full merged.txt (highlighted)</h3>
187
  <div style="display:flex; gap:8px; align-items:center;">
188
+ <button class="smallbtn" id="jumpTop" title="Jump to start">⤒ Top</button>
189
+ <button class="smallbtn" id="jumpBottom" title="Jump to end">⤓ Bottom</button>
190
  <button class="smallbtn" id="closeSidebar">✕</button>
191
  </div>
192
  </div>
193
+ <div id="sidebarBody" class="sbar-body">
194
+ <pre id="fullText"></pre>
195
+ </div>
196
  </aside>
197
 
198
  <script>
199
  const SID = "{{ sid }}";
200
  let uploadedNames = [];
 
201
 
202
+ // Upload (client-side list + POST)
203
+ const fileInput = document.getElementById("fileUpload");
204
+ const dropZone = document.getElementById('dropZone');
205
+ const uploadedListClient = document.getElementById('uploadedListClient');
206
 
207
+ function sendFiles(files){
208
+ if (!files || files.length === 0) return;
209
  const formData = new FormData();
210
+ for (const file of files) { formData.append("file", file); uploadedNames.push(file.name); }
 
 
 
211
 
212
  const xhr = new XMLHttpRequest();
213
  const progressBar = document.getElementById("uploadProgress");
214
  const statusText = document.getElementById("uploadStatus");
215
+ xhr.open("POST", `/upload?sid=${encodeURIComponent(SID)}`, true);
 
216
 
217
  xhr.upload.onprogress = function (e) {
218
  if (e.lengthComputable) {
219
+ progressBar.value = Math.round((e.loaded / e.total) * 100);
 
220
  progressBar.style.display = "inline-block";
221
+ statusText.textContent = `Uploading: ${progressBar.value}%`;
222
  }
223
  };
 
224
  xhr.onload = function () {
225
  progressBar.value = 100;
226
  statusText.textContent = "✅ Upload complete!";
227
+ setTimeout(() => { progressBar.style.display = "none"; statusText.textContent = ""; }, 1200);
 
 
 
 
 
 
 
228
  };
229
+ xhr.onerror = function () { statusText.textContent = "❌ Upload failed."; };
230
  xhr.send(formData);
231
+ }
232
 
233
+ fileInput.addEventListener("change", e => sendFiles(e.target.files));
234
+ dropZone.addEventListener('click', () => fileInput.click());
235
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); });
236
+ dropZone.addEventListener('drop', (e) => { e.preventDefault(); sendFiles(e.dataTransfer.files); });
237
 
238
+ document.getElementById("showFilesBtn").addEventListener("click", () => {
239
+ uploadedListClient.innerHTML = "";
240
  const ul = document.createElement("ul");
241
  for (const name of uploadedNames) {
242
  const li = document.createElement("li");
243
  li.textContent = name;
244
  ul.appendChild(li);
245
  }
246
+ uploadedListClient.appendChild(ul);
247
+ uploadedListClient.style.display = "block";
 
248
  });
249
 
250
+ // Search progress
251
  document.querySelector('form[method="post"]').addEventListener("submit", function () {
252
  const progressBar = document.getElementById("searchProgress");
253
  const statusText = document.getElementById("searchStatus");
 
256
  statusText.textContent = "🔍 Searching...";
257
  });
258
 
259
+ // Sidebar controls
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  const sidebar = document.getElementById('sidebar');
261
  const sidebarBody = document.getElementById('sidebarBody');
262
  const backdrop = document.getElementById('sidebarBackdrop');
263
  const btnClose = document.getElementById('closeSidebar');
264
+ const pre = document.getElementById('fullText');
265
+ const jumpTop = document.getElementById('jumpTop');
266
+ const jumpBottom = document.getElementById('jumpBottom');
267
 
268
  function openSidebar(){
269
  sidebar.classList.add('open');
 
274
  sidebar.classList.remove('open');
275
  backdrop.classList.remove('open');
276
  sidebar.setAttribute('aria-hidden', 'true');
277
+ pre.innerHTML = '';
278
  }
279
  btnClose.addEventListener('click', closeSidebar);
280
  backdrop.addEventListener('click', closeSidebar);
281
 
282
+ jumpTop.addEventListener('click', () => { sidebarBody.scrollTo({ top: 0, behavior: 'smooth' }); });
283
+ jumpBottom.addEventListener('click', () => { sidebarBody.scrollTo({ top: sidebarBody.scrollHeight, behavior: 'smooth' }); });
284
+
285
+ function escapeHtml(s){
286
+ return s.replaceAll('&','&amp;')
287
+ .replaceAll('<','&lt;')
288
+ .replaceAll('>','&gt;')
289
+ .replaceAll('"','&quot;')
290
+ .replaceAll("'",'&#39;');
291
+ }
292
+
293
+ async function fetchFullContext(idx){
294
+ const res = await fetch(`/api/context?sid=${encodeURIComponent(SID)}&idx=${idx}`);
295
  if(!res.ok){
296
  const t = await res.text();
297
  throw new Error(t || 'Failed to fetch context');
 
299
  return res.json();
300
  }
301
 
302
+ function renderFullTextHighlight(merged, start, end){
303
+ const before = escapeHtml(merged.slice(0, start));
304
+ const middle = escapeHtml(merged.slice(start, end));
305
+ const after = escapeHtml(merged.slice(end));
306
+ pre.innerHTML = before + '<mark id="centerMark">' + middle + '</mark>' + after;
307
+
308
+ // Smoothly center the highlight in view
309
+ requestAnimationFrame(() => {
310
+ const mark = document.getElementById('centerMark');
311
+ if(mark){
312
+ const rect = mark.getBoundingClientRect();
313
+ const bodyRect = sidebarBody.getBoundingClientRect();
314
+ const offset = (rect.top + rect.height/2) - (bodyRect.top + bodyRect.height/2);
315
+ sidebarBody.scrollBy({ top: offset, behavior: 'smooth' });
316
  }
 
317
  });
 
 
 
 
 
318
  }
319
 
 
 
320
  window.openContext = async function(idx){
321
  try{
322
+ const data = await fetchFullContext(idx);
323
+ renderFullTextHighlight(data.merged, data.start, data.end);
 
324
  openSidebar();
325
  }catch(err){
326
  alert('Error: ' + err.message);
327
  }
328
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  </script>
330
  </body>
331
  </html>