Spaces:
Sleeping
Sleeping
| <!-- templates/index.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> | |
| </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('&','&') | |
| .replaceAll('<','<') | |
| .replaceAll('>','>') | |
| .replaceAll('"','"') | |
| .replaceAll("'",'''); | |
| } | |
| 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> | |