File size: 17,723 Bytes
366abe4
75b13ff
3185437
75b13ff
3185437
75b13ff
3185437
75b13ff
3185437
e3f391f
 
3185437
 
 
 
 
e3f391f
3185437
 
 
 
 
 
 
e3f391f
 
3185437
 
 
 
 
 
 
 
 
 
 
 
 
e3f391f
 
3185437
 
0f84df9
e3f391f
 
3185437
e3f391f
3185437
 
e3f391f
3185437
e3f391f
 
3185437
e3f391f
 
3185437
e3f391f
3185437
e3f391f
 
3185437
 
0f84df9
3185437
366abe4
0f84df9
e3f391f
 
 
 
 
 
 
 
 
366abe4
e3f391f
 
366abe4
 
 
 
 
 
 
 
 
 
 
 
 
75b13ff
 
 
3185437
 
 
 
 
 
 
 
 
 
 
 
 
 
e3f391f
3185437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3f391f
 
3185437
e3f391f
3185437
 
 
 
 
 
 
 
 
 
 
75b13ff
3185437
 
 
 
 
 
 
 
2fa33cf
3185437
 
2fa33cf
3185437
 
2fa33cf
3185437
 
 
 
 
 
 
e3f391f
3185437
 
 
 
 
 
 
 
 
 
e3f391f
 
 
 
 
3185437
e3f391f
 
 
 
 
366abe4
b6677b2
366abe4
b6677b2
 
366abe4
b6677b2
 
 
 
 
 
e3f391f
 
 
366abe4
 
 
 
 
 
 
 
 
 
 
3185437
 
 
 
75b13ff
0f84df9
 
 
 
e3f391f
0f84df9
e3f391f
 
0f84df9
 
 
e3f391f
 
 
0f84df9
 
75b13ff
3185437
 
e58eeff
e3f391f
 
 
 
3185437
e3f391f
 
3185437
e3f391f
3185437
 
 
 
e3f391f
3185437
 
 
e3f391f
3185437
e3f391f
3185437
 
 
 
 
e3f391f
3185437
e3f391f
3185437
e3f391f
75b13ff
e3f391f
 
 
 
75b13ff
e3f391f
 
3185437
 
 
 
 
 
e3f391f
 
3185437
6241b0e
e3f391f
3185437
 
 
 
 
 
 
75b13ff
e3f391f
0f84df9
 
 
 
e3f391f
 
 
0f84df9
 
 
 
 
 
 
 
 
 
e3f391f
0f84df9
 
 
 
366abe4
 
 
e3f391f
 
 
 
 
 
 
 
 
 
 
0f84df9
 
 
 
 
 
 
e3f391f
 
 
 
 
 
366abe4
e3f391f
 
 
366abe4
0f84df9
 
 
 
 
 
e3f391f
 
0f84df9
 
 
 
 
366abe4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6677b2
 
 
 
 
 
3185437
75b13ff
0f84df9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
<!-- templates/index.html -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Retriever</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    :root{
      --bg: #0f1226; --bg2:#111639; --ink:#e9ecff; --muted:#b7c0ffcc;
      --accent:#7c9cff; --accent2:#61e7ff; --danger:#ff6b6b; --ok:#35d39e;
      --radius: 18px;
      --shadow: 0 10px 30px rgba(0,0,0,.35), 0 2px 8px rgba(0,0,0,.25);
    }
    html,body{height:100%;}
    body{
      margin:0; color:var(--ink);
      background:
        radial-gradient(1200px 800px at 10% -10%, #2630a540, transparent 60%),
        radial-gradient(1000px 700px at 110% 20%, #1fb6ff26, transparent 60%),
        linear-gradient(180deg, var(--bg), var(--bg2));
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
      line-height:1.45;
    }
    .wrap{ max-width: 980px; margin: 48px auto 120px; padding: 0 20px; }
    .title{ display:flex; align-items:center; gap:12px; margin:0 0 18px; }
    .title .badge{
      padding:6px 10px; font-size:12px; border:1px solid #ffffff22; border-radius:999px; color:var(--muted);
      background: linear-gradient(180deg,#ffffff05,#00000020);
      box-shadow: inset 0 0 0 1px #ffffff08;
    }
    .card{
      background: linear-gradient(180deg, #ffffff08, #00000022);
      border: 1px solid #ffffff1c;
      border-radius: var(--radius);
      box-shadow: var(--shadow);
      overflow: clip;
      backdrop-filter: blur(8px);
    }
    .card .hdr{ padding:14px 18px; border-bottom:1px solid #ffffff12; display:flex; align-items:center; justify-content:space-between; gap:10px; }
    .card .hdr h2{ margin:0; font-size:18px; font-weight:700; color:#fff; }
    .card .body{ padding:18px; }

    .uploader{ display:grid; gap:14px; }
    #fileUpload{ position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; padding:0; margin:-1px; }
    .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; }
    .drop:hover{ border-color:#c7d0ff77; transform: translateY(-1px);}
    .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; }
    .drop .small{ color:var(--muted); font-size:14px; }
    .row{ display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
    .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; }
    .btn.secondary{ --btn-bg:#1a213f; --btn-fg:#dbe2ff; }
    .btn.danger{ --btn-bg:#4a1020; --btn-fg:#ffd7df; border-color:#ff6b6b44; }
    progress{ width:320px; height:14px; border-radius:999px; overflow:hidden; vertical-align:middle; background:#293079; border:1px solid #ffffff22; }
    progress::-webkit-progress-bar{ background:#293079; }
    progress::-webkit-progress-value{ background: linear-gradient(90deg, var(--accent), var(--accent2)); }
    #uploadStatus,#searchStatus{ margin-left:10px; font-weight:700; color:#d5dcff; }
    #uploadedListClient ul{ list-style:none; padding:0; margin:8px 0 0; display:flex; flex-wrap:wrap; gap:8px;}
    #uploadedListClient li{ padding:6px 10px; border-radius:999px; font-size:12px; color:#dfe5ff; background: linear-gradient(180deg,#ffffff0a,#00000025); border:1px solid #ffffff1a; }
    label{ color:#cdd4ff; font-weight:600; letter-spacing:.2px; }
    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; }
    textarea:focus, input[type="number"]:focus{ box-shadow: 0 0 0 3px #7c9cff44; border-color:#9fb2ff77; }
    hr{ border:none; height:1px; background:#ffffff1a; margin:26px 0; }
    ol{ padding-left: 22px; }
    ol li{ margin: 12px 0; }
    a{ color: var(--accent2); }
    .muted{ color:#b7c0ffcc; font-size:13px; }

    /* Sidebar: full merged text */
    .sidebar-backdrop{ position: fixed; inset: 0; background: rgba(0,0,0,.35); opacity:0; pointer-events:none; transition:.18s ease; z-index:60; }
    .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; }
    .sidebar.open{ transform: translateX(0); }
    .sidebar-backdrop.open{ opacity:1; pointer-events:auto; }
    .sbar-hdr{ display:flex; align-items:center; justify-content:space-between; gap:10px; padding:14px 16px; border-bottom:1px solid #ffffff1a; }
    .sbar-hdr h3{ margin:0; font-size:16px; }
    .sbar-body{ overflow:auto; padding:16px; }
    .smallbtn{ font-size:12px; padding:6px 8px; border-radius:10px; background:#1a213f; color:#dbe2ff; border:1px solid #ffffff18; cursor:pointer; }

    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; }
    mark#centerMark{ background:#61e7ff55; padding:0 .5px; border-radius:3px; }

    /* New: results toolbuttons + file label */
    .tools{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:6px; }
    .iconbtn{
      display:inline-flex; align-items:center; justify-content:center;
      width:28px; height:28px; border-radius:8px; border:1px solid #ffffff18;
      background:#1a213f; color:#e6ecff; cursor:pointer; font-size:14px;
    }
    .filetag{
      display:inline-flex; align-items:center; gap:6px;
      padding:4px 8px; border-radius:999px; border:1px solid #ffffff18;
      background:#0d1333; color:#cfe1ff; font-size:12px;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="title">
      <svg width="28" height="28" viewBox="0 0 24 24" fill="none" aria-hidden="true">
        <path d="M12 3l8 4.5v9L12 21 4 16.5v-9L12 3z" stroke="url(#g)" stroke-width="1.4" fill="#9fb4ff22"/>
        <defs><linearGradient id="g" x1="0" x2="24" y1="0" y2="24"><stop stop-color="#7c9cff"/><stop offset="1" stop-color="#61e7ff"/></linearGradient></defs>
      </svg>
      <h1 style="margin:0;font-size:26px;">Retriever</h1>
      <span class="badge">RAG helper</span>
    </div>

    <!-- Uploader card -->
    <section class="card" aria-labelledby="upl">
      <div class="hdr">
        <h2 id="upl">Upload Text or PDF Files</h2>
        <div class="row">
          <button id="showFilesBtn" class="btn secondary" type="button">πŸ“„ Show Uploaded Files</button>
        </div>
      </div>
      <div class="body">
        <div class="uploader">
          <input type="file" id="fileUpload" multiple>
          <div id="dropZone" class="drop" tabindex="0" role="button" aria-label="Drop files here or click to browse">
            <div class="big">Drag & Drop files here</div>
            <div class="small">.txt, .pdf supported</div>
          </div>

          <div class="row">
            <progress id="uploadProgress" value="0" max="100" style="display:none;"></progress>
            <span id="uploadStatus"></span>
          </div>

          <div id="uploadedListClient" style="display:none; margin-top:10px;"></div>

          {% if uploaded_filenames %}
            <h4 style="margin:14px 0 6px;">Uploaded Files (server)</h4>
            <ul>
              {% for fname in uploaded_filenames %}
                <li>{{ fname }}</li>
              {% endfor %}
            </ul>
          {% endif %}
        </div>
      </div>
    </section>

    <hr>

    <!-- Retrieve form card -->
    <section class="card" aria-labelledby="ret">
      <div class="hdr">
        <h2 id="ret">Search your uploads</h2>
      </div>
      <div class="body">
        <form method="post">
          <input type="hidden" name="sid" value="{{ sid }}">

          <label for="query">Enter your question or claim:</label><br>
          <textarea id="query" name="query" rows="4" required>{{ query }}</textarea><br><br>

          <label for="topk">Number of paragraphs to return:</label>
          <input id="topk" type="number" name="topk" value="{{ topk }}" min="1" max="50"><br><br>

          <div class="row">
            <button type="submit" class="btn">πŸ”Ž Retrieve</button>
            <progress id="searchProgress" max="100" style="width:320px; display:none;"></progress>
            <span id="searchStatus"></span>
          </div>

          <p class="muted" style="margin-top:10px;">
            Tip: upload may take a moment to process before results appear.
          </p>
        </form>

        <br>
        <form method="get" action="/reset" onsubmit="return confirm('Clear all uploaded files and results?');">
          <input type="hidden" name="sid" value="{{ sid }}">
          <button type="submit" class="btn danger">🧹 Start New Search</button>
        </form>

        {% if results %}
          <br>
          <div class="row">
            <a class="btn secondary" href="{{ url_for('download', sid=sid) }}">⬇️ Download these results</a>
            <a class="btn secondary" href="{{ url_for('download_merged', sid=sid) }}">πŸ“¦ Download full merged text</a>
          </div>

          <h3 style="margin-top:18px;">Matching Paragraphs</h3>
          <ol>
            {% for r in results %}
              <li>
                <p>{{ r.text }}</p>
                <div class="tools">
  <button type="button" class="smallbtn" onclick="openContext({{ r.idx }})" title="Open full context sidebar">πŸ”Ž View context</button>

  <!-- Clipboard icon: copies the 2nd line of the source file -->
  <button type="button" class="iconbtn" onclick="copySecond({{ r.idx }}, this)" title="Copy 2nd line of this file">πŸ“‹</button>

  <!-- Link icon: opens the 2nd line of the source file in new tab -->
  <button type="button" class="iconbtn" onclick="openLink({{ r.idx }})" title="Open URL in new tab">πŸ”—</button>

  <!-- File name label -->
  <span class="filetag">πŸ“„ {{ r.file }}</span>
</div>
              </li>
            {% endfor %}
          </ol>

          <!-- Provide per-paragraph metadata for JS (2nd line + filename) -->
          <script>
            window.PARA_META = {};
            {% for r in results %}
            PARA_META[{{ r.idx }}] = {
              second: {{ r.second_line | tojson }},
              file: {{ r.file | tojson }}
            };
            {% endfor %}
          </script>
        {% endif %}
      </div>
    </section>
  </div>

  <!-- Sidebar + backdrop -->
  <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
  <aside id="sidebar" class="sidebar" aria-hidden="true">
    <div class="sbar-hdr">
      <h3>Full merged.txt (highlighted)</h3>
      <div style="display:flex; gap:8px; align-items:center;">
        <button class="smallbtn" id="jumpTop" title="Jump to start">β€’ Top</button>
        <button class="smallbtn" id="jumpBottom" title="Jump to end">– Bottom</button>
        <button class="smallbtn" id="closeSidebar">βœ•</button>
      </div>
    </div>
    <div id="sidebarBody" class="sbar-body">
      <pre id="fullText"></pre>
    </div>
  </aside>

  <script>
    const SID = "{{ sid }}";
    let uploadedNames = [];

    // Upload (client-side list + POST)
    const fileInput = document.getElementById("fileUpload");
    const dropZone = document.getElementById('dropZone');
    const uploadedListClient = document.getElementById('uploadedListClient');

    function sendFiles(files){
      if (!files || files.length === 0) return;
      const formData = new FormData();
      for (const file of files) { formData.append("file", file); uploadedNames.push(file.name); }

      const xhr = new XMLHttpRequest();
      const progressBar = document.getElementById("uploadProgress");
      const statusText  = document.getElementById("uploadStatus");
      xhr.open("POST", `/upload?sid=${encodeURIComponent(SID)}`, true);

      xhr.upload.onprogress = function (e) {
        if (e.lengthComputable) {
          progressBar.value = Math.round((e.loaded / e.total) * 100);
          progressBar.style.display = "inline-block";
          statusText.textContent = `Uploading: ${progressBar.value}%`;
        }
      };
      xhr.onload = function () {
        progressBar.value = 100;
        statusText.textContent = "βœ… Upload complete!";
        setTimeout(() => { progressBar.style.display = "none"; statusText.textContent = ""; }, 1200);
      };
      xhr.onerror = function () { statusText.textContent = "❌ Upload failed."; };
      xhr.send(formData);
    }

    fileInput.addEventListener("change", e => sendFiles(e.target.files));
    dropZone.addEventListener('click', () => fileInput.click());
    dropZone.addEventListener('dragover', (e) => { e.preventDefault(); });
    dropZone.addEventListener('drop', (e) => { e.preventDefault(); sendFiles(e.dataTransfer.files); });

    document.getElementById("showFilesBtn").addEventListener("click", () => {
      uploadedListClient.innerHTML = "";
      const ul = document.createElement("ul");
      for (const name of uploadedNames) {
        const li = document.createElement("li");
        li.textContent = name;
        ul.appendChild(li);
      }
      uploadedListClient.appendChild(ul);
      uploadedListClient.style.display = "block";
    });

    // Search progress
    document.querySelector('form[method="post"]').addEventListener("submit", function () {
      const progressBar = document.getElementById("searchProgress");
      const statusText  = document.getElementById("searchStatus");
      progressBar.removeAttribute("value");
      progressBar.style.display = "inline-block";
      statusText.textContent = "πŸ” Searching...";
    });

    // Sidebar controls
    const sidebar = document.getElementById('sidebar');
    const sidebarBody = document.getElementById('sidebarBody');
    const backdrop = document.getElementById('sidebarBackdrop');
    const btnClose = document.getElementById('closeSidebar');
    const pre = document.getElementById('fullText');
    const jumpTop = document.getElementById('jumpTop');
    const jumpBottom = document.getElementById('jumpBottom');

    function openSidebar(){
      sidebar.classList.add('open');
      backdrop.classList.add('open');
      sidebar.setAttribute('aria-hidden', 'false');
    }
    function closeSidebar(){
      sidebar.classList.remove('open');
      backdrop.classList.remove('open');
      sidebar.setAttribute('aria-hidden', 'true');
      pre.innerHTML = '';
    }
    btnClose.addEventListener('click', closeSidebar);
    backdrop.addEventListener('click', closeSidebar);

    // Instant jumps for top/bottom
    jumpTop.addEventListener('click', () => { sidebarBody.scrollTop = 0; });
    jumpBottom.addEventListener('click', () => { sidebarBody.scrollTop = sidebarBody.scrollHeight; });

    function escapeHtml(s){
      return s.replaceAll('&','&amp;')
              .replaceAll('<','&lt;')
              .replaceAll('>','&gt;')
              .replaceAll('"','&quot;')
              .replaceAll("'",'&#39;');
    }

    async function fetchFullContext(idx){
      const res = await fetch(`/api/context?sid=${encodeURIComponent(SID)}&idx=${idx}`);
      if(!res.ok){
        const t = await res.text();
        throw new Error(t || 'Failed to fetch context');
      }
      return res.json();
    }

    function renderFullTextHighlight(merged, start, end){
      const before = escapeHtml(merged.slice(0, start));
      const middle = escapeHtml(merged.slice(start, end));
      const after  = escapeHtml(merged.slice(end));
      pre.innerHTML = before + '<mark id="centerMark">' + middle + '</mark>' + after;

      // Instantly jump to the highlight
      requestAnimationFrame(() => {
        const mark = document.getElementById('centerMark');
        if(mark){
          mark.scrollIntoView({ block: 'center', inline: 'nearest' });
        }
      });
    }

    window.openContext = async function(idx){
      try{
        const data = await fetchFullContext(idx);
        renderFullTextHighlight(data.merged, data.start, data.end);
        openSidebar();
      }catch(err){
        alert('Error: ' + err.message);
      }
    }

    // Clipboard: copy the 2nd line of the source file for this paragraph
    window.copySecond = function(idx, btn){
      try{
        const meta = window.PARA_META?.[idx];
        if(!meta){ throw new Error("Missing metadata"); }
        const text = meta.second || "";
        navigator.clipboard.writeText(text).then(() => {
          // simple visual feedback
          const old = btn.textContent;
          btn.textContent = 'βœ…';
          setTimeout(()=>{ btn.textContent = 'πŸ“‹'; }, 900);
        }, () => {
          alert("Clipboard copy failed");
        });
      }catch(e){
        alert("Clipboard error: " + e.message);
      }
    }
function openLink(idx) {
  const meta = window.PARA_META[idx];
  if (meta && meta.second) {
    window.open(meta.second, "_blank");
  }
}
  </script>
</body>
</html>