retriever / templates /index.html
ntdservices's picture
Update templates/index.html
b6677b2 verified
<!-- 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>