Spaces:
Running
Running
<!-- 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> | |