vector-db / static /index.html
mittalsahab's picture
Vector DB example
e4d26ac
Raw
History Blame Contribute Delete
31.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vector DB Explorer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #eef2ff;
--surface: #ffffff;
--bg: #f8fafc;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--radius: 12px;
--shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
--shadow-md: 0 4px 16px rgba(0,0,0,.08), 0 2px 6px rgba(0,0,0,.04);
--shadow-lg: 0 8px 32px rgba(0,0,0,.10), 0 4px 12px rgba(0,0,0,.06);
--green: #16a34a; --green-bg: #f0fdf4;
--amber: #d97706; --amber-bg: #fffbeb;
--red: #dc2626; --red-bg: #fef2f2;
--t: 0.18s ease;
}
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
font-size: 14px;
line-height: 1.6;
}
/* ── Nav ─────────────────────────────────────────────────────────── */
nav {
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-sm);
}
.nav-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.nav-brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.brand-icon {
width: 36px; height: 36px;
flex-shrink: 0;
background: var(--primary);
border-radius: 9px;
display: grid; place-items: center;
font-size: 18px;
}
.brand-text { min-width: 0; }
.brand-text h1 { font-size: 15px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brand-text p { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.corpus-stat {
flex-shrink: 0;
display: flex; align-items: center; gap: 6px;
background: var(--primary-light);
color: var(--primary-dark);
border-radius: 99px;
padding: 5px 12px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.corpus-stat .dot {
width: 7px; height: 7px;
background: var(--primary);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
/* Hide nav subtitle on small screens */
@media (max-width: 480px) {
.brand-text p { display: none; }
.brand-text h1 { font-size: 14px; }
}
/* ── Main layout ─────────────────────────────────────────────────── */
main {
max-width: 1200px;
margin: 0 auto;
padding: 16px 12px 48px;
display: flex;
flex-direction: column;
gap: 16px;
}
@media (min-width: 640px) {
main { padding: 28px 20px 56px; gap: 24px; }
}
@media (min-width: 1024px) {
main { padding: 32px 24px 64px; gap: 32px; }
}
/* ── Cards ───────────────────────────────────────────────────────── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.card-header {
padding: 14px 16px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
@media (min-width: 640px) {
.card-header { padding: 18px 22px 0; }
}
.card-title {
font-size: 14px;
font-weight: 700;
color: var(--text);
display: flex;
align-items: center;
gap: 7px;
white-space: nowrap;
}
.card-title .icon { font-size: 16px; }
.card-body { padding: 14px 16px 16px; }
@media (min-width: 640px) {
.card-body { padding: 18px 22px 22px; }
}
/* ── Buttons ─────────────────────────────────────────────────────── */
.btn {
height: 46px;
padding: 0 18px;
font-family: inherit;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 10px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background var(--t), transform var(--t), box-shadow var(--t);
white-space: nowrap;
}
.btn:active { transform: scale(0.97); }
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-dark); box-shadow: var(--shadow-md); }
.btn-primary:disabled { background: #a5b4fc; cursor: not-allowed; transform: none; }
.spinner {
width: 15px; height: 15px;
border: 2px solid rgba(255,255,255,0.4);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Search section ──────────────────────────────────────────────── */
.search-row {
display: flex;
gap: 8px;
}
.search-input-wrap {
flex: 1;
position: relative;
min-width: 0;
}
.search-input-wrap .search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
pointer-events: none;
}
#search-input {
width: 100%;
height: 46px;
/* 16px prevents iOS Safari from zooming on focus */
font-size: 16px;
font-family: inherit;
padding: 0 12px 0 40px;
border: 2px solid var(--border);
border-radius: 10px;
outline: none;
transition: border-color var(--t), box-shadow var(--t);
background: var(--bg);
color: var(--text);
}
#search-input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
background: var(--surface);
}
#search-input::placeholder { color: var(--muted); font-size: 14px; }
/* Example chips */
.chips {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 7px;
margin-top: 10px;
}
.chips-label { font-size: 11px; color: var(--muted); font-weight: 500; }
.chip {
font-size: 12px;
font-weight: 500;
padding: 4px 11px;
border-radius: 99px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--muted);
cursor: pointer;
transition: background var(--t), color var(--t), border-color var(--t);
user-select: none;
}
.chip:active, .chip:hover {
background: var(--primary-light);
color: var(--primary-dark);
border-color: var(--primary);
}
/* ── Results ─────────────────────────────────────────────────────── */
#results-area { margin-top: 20px; }
.results-heading {
font-size: 12px;
color: var(--muted);
font-weight: 500;
margin-bottom: 14px;
}
.results-heading strong { color: var(--text); }
/* Mobile: single column. Desktop: 3 columns. */
.results-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
@media (min-width: 680px) {
.results-grid { grid-template-columns: repeat(3, 1fr); gap: 14px; }
}
.result-card {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
background: var(--surface);
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 12px;
animation: fadeUp 0.25s ease both;
}
/* On mobile show the 3 results side-scrollable in a row instead of stacked */
@media (max-width: 679px) {
.results-grid {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
gap: 10px;
padding-bottom: 8px;
}
.results-grid::-webkit-scrollbar { height: 4px; }
.results-grid::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
.result-card {
min-width: 80vw;
scroll-snap-align: start;
flex-shrink: 0;
}
}
.result-card:nth-child(2) { animation-delay: 0.05s; }
.result-card:nth-child(3) { animation-delay: 0.10s; }
@keyframes fadeUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
.result-rank {
display: flex;
align-items: center;
justify-content: space-between;
}
.rank-badge {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
padding: 3px 9px;
border-radius: 99px;
}
.rank-1 { background: #fef9c3; color: #854d0e; }
.rank-2 { background: #f1f5f9; color: #475569; }
.rank-3 { background: #f1f5f9; color: #475569; }
.score-pct { font-size: 20px; font-weight: 700; line-height: 1; }
.score-bar-wrap {
background: #f1f5f9;
border-radius: 99px;
height: 7px;
overflow: hidden;
}
.score-bar-fill {
height: 100%;
border-radius: 99px;
width: 0;
animation: growBar 0.6s cubic-bezier(0.4,0,0.2,1) forwards;
}
@keyframes growBar { from{width:0} to{width:var(--target-w)} }
.score-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badges { display: flex; gap: 6px; flex-wrap: wrap; }
.badge {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.result-text {
font-size: 13px;
color: var(--text);
line-height: 1.6;
flex: 1;
}
/* Swipe hint shown only on mobile */
.swipe-hint {
display: none;
font-size: 11px;
color: var(--muted);
text-align: center;
margin-top: 4px;
}
@media (max-width: 679px) {
.swipe-hint { display: block; }
}
/* Empty / loading states */
.state-box {
padding: 36px 20px;
text-align: center;
color: var(--muted);
}
.state-box .state-icon { font-size: 32px; margin-bottom: 10px; }
.state-box p { font-size: 13px; }
.state-box .hint { font-size: 12px; margin-top: 5px; }
/* ── Bottom grid ─────────────────────────────────────────────────── */
.bottom-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
align-items: start;
}
@media (min-width: 900px) {
.bottom-grid { grid-template-columns: 340px 1fr; gap: 24px; }
}
/* ── Add document ────────────────────────────────────────────────── */
#add-text {
width: 100%;
min-height: 100px;
padding: 12px 14px;
font-family: inherit;
font-size: 16px; /* prevent iOS zoom */
border: 2px solid var(--border);
border-radius: 10px;
resize: vertical;
outline: none;
transition: border-color var(--t), box-shadow var(--t);
color: var(--text);
line-height: 1.6;
}
#add-text:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
}
#add-text::placeholder { color: var(--muted); font-size: 14px; }
.add-meta-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 10px;
}
.meta-input {
width: 100%;
min-width: 0;
height: 44px;
padding: 0 12px;
font-family: inherit;
font-size: 16px; /* prevent iOS zoom */
border: 2px solid var(--border);
border-radius: 8px;
outline: none;
transition: border-color var(--t), box-shadow var(--t);
color: var(--text);
background: var(--bg);
}
.meta-input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
background: var(--surface);
}
.meta-input::placeholder { color: var(--muted); font-size: 13px; }
#add-btn {
margin-top: 10px;
width: 100%;
height: 44px;
font-size: 14px;
}
#add-status {
margin-top: 10px;
font-size: 13px;
border-radius: 8px;
overflow: hidden;
max-height: 0;
padding: 0 14px;
transition: max-height 0.3s ease, padding 0.3s ease;
}
#add-status.visible { padding: 10px 14px; max-height: 60px; }
#add-status.success { background: var(--green-bg); color: var(--green); }
#add-status.error { background: var(--red-bg); color: var(--red); }
/* ── Corpus ──────────────────────────────────────────────────────── */
/* Corpus card header stacks on mobile, goes side-by-side on desktop */
.corpus-card-header {
flex-direction: column;
align-items: stretch;
gap: 10px;
padding-bottom: 14px;
}
.corpus-title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
#corpus-count { font-size: 12px; color: var(--muted); font-weight: 500; }
#corpus-filter {
width: 100%;
height: 38px;
padding: 0 12px;
font-family: inherit;
font-size: 16px; /* prevent iOS zoom */
border: 1px solid var(--border);
border-radius: 8px;
outline: none;
transition: border-color var(--t), box-shadow var(--t);
background: var(--bg);
color: var(--text);
}
#corpus-filter:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
}
#corpus-filter::placeholder { color: var(--muted); font-size: 13px; }
/* On desktop, put title-row and filter side by side */
@media (min-width: 640px) {
.corpus-card-header { flex-direction: row; align-items: center; padding-bottom: 0; }
.corpus-title-row { flex: 1; }
#corpus-filter { width: 180px; flex-shrink: 0; }
}
#corpus-grid {
margin-top: 14px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px;
}
/* Limit height only on desktop to prevent the card from being too tall */
@media (min-width: 900px) {
#corpus-grid { max-height: 400px; overflow-y: auto; padding-right: 4px; }
#corpus-grid::-webkit-scrollbar { width: 5px; }
#corpus-grid::-webkit-scrollbar-track { background: transparent; }
#corpus-grid::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
}
.doc-card {
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
background: var(--bg);
cursor: pointer;
transition: border-color var(--t), box-shadow var(--t), background var(--t);
display: flex;
flex-direction: column;
gap: 7px;
}
.doc-card:active, .doc-card:hover {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.08);
background: var(--surface);
}
.doc-card .badge { align-self: flex-start; }
.doc-card .doc-text {
font-size: 12px;
color: var(--muted);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.5;
}
.doc-card .doc-topic { font-size: 11px; color: #94a3b8; font-weight: 500; }
/* ── Toast ───────────────────────────────────────────────────────── */
#toast {
position: fixed;
bottom: 20px;
right: 16px;
left: 16px;
max-width: 340px;
margin: 0 auto;
background: #1e293b;
color: #f8fafc;
padding: 12px 18px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
box-shadow: var(--shadow-lg);
transform: translateY(90px);
opacity: 0;
transition: transform 0.3s ease, opacity 0.3s ease;
z-index: 200;
text-align: center;
}
@media (min-width: 480px) {
#toast { left: auto; max-width: none; width: auto; }
}
#toast.show { transform: translateY(0); opacity: 1; }
</style>
</head>
<body>
<nav>
<div class="nav-inner">
<div class="nav-brand">
<div class="brand-icon">&#128269;</div>
<div class="brand-text">
<h1>Vector DB Explorer</h1>
<p>Semantic search &middot; cosine similarity &middot; embeddings</p>
</div>
</div>
<div class="corpus-stat">
<div class="dot"></div>
<span id="nav-count">...</span> docs
</div>
</div>
</nav>
<main>
<!-- Search card -->
<div class="card">
<div class="card-header">
<span class="card-title"><span class="icon">&#128270;</span> Semantic Search</span>
</div>
<div class="card-body">
<div class="search-row">
<div class="search-input-wrap">
<span class="search-icon">&#128269;</span>
<input
id="search-input"
type="text"
placeholder="Search by meaning..."
autocomplete="off"
autocorrect="off"
spellcheck="false"
/>
</div>
<button id="search-btn" class="btn btn-primary" onclick="doSearch()">
<span id="search-btn-text">Search</span>
</button>
</div>
<div class="chips">
<span class="chips-label">Try:</span>
<span class="chip" onclick="setQuery('how do computers learn from data?')">computers learning</span>
<span class="chip" onclick="setQuery('food from Italy')">Italian food</span>
<span class="chip" onclick="setQuery('space and the universe')">space</span>
<span class="chip" onclick="setQuery('athletic competition between nations')">Olympics</span>
<span class="chip" onclick="setQuery('storing and querying high-dimensional data')">vector DB</span>
<span class="chip" onclick="setQuery('living things and genetics')">biology</span>
</div>
<div id="results-area">
<div class="state-box">
<div class="state-icon">&#129300;</div>
<p>Enter a query to find the most semantically similar documents.</p>
<p class="hint">Results are ranked by cosine similarity of their embedding vectors.</p>
</div>
</div>
</div>
</div>
<!-- Bottom grid: Add + Corpus -->
<div class="bottom-grid">
<!-- Add Document -->
<div class="card">
<div class="card-header">
<span class="card-title"><span class="icon">&#10133;</span> Add Document</span>
</div>
<div class="card-body">
<textarea id="add-text" placeholder="Type or paste your document text here..."></textarea>
<div class="add-meta-row">
<input id="add-category" class="meta-input" type="text" placeholder="Category (e.g. tech)" />
<input id="add-topic" class="meta-input" type="text" placeholder="Topic (e.g. AI/ML)" />
</div>
<button id="add-btn" class="btn btn-primary" onclick="doAdd()">
<span id="add-btn-text">+ Add Document</span>
</button>
<div id="add-status"></div>
</div>
</div>
<!-- Corpus -->
<div class="card">
<!-- Restructured header: title+count on one row, filter below (stacks on mobile) -->
<div class="card-header corpus-card-header">
<div class="corpus-title-row">
<span class="card-title"><span class="icon">&#128218;</span> Corpus</span>
<span id="corpus-count"></span>
</div>
<input id="corpus-filter" type="text" placeholder="Filter documents..." oninput="filterCorpus()" />
</div>
<div class="card-body">
<div id="corpus-grid">
<div class="state-box">
<div class="state-icon">&#128203;</div>
<p>Loading documents...</p>
</div>
</div>
</div>
</div>
</div>
</main>
<div id="toast"></div>
<script>
const CATEGORY_STYLES = {
tech: { bg: '#dbeafe', fg: '#1d4ed8' },
science: { bg: '#ede9fe', fg: '#7c3aed' },
food: { bg: '#fce7f3', fg: '#be185d' },
history: { bg: '#fef3c7', fg: '#b45309' },
sports: { bg: '#dcfce7', fg: '#15803d' },
general: { bg: '#f3f4f6', fg: '#6b7280' },
};
function catStyle(cat) {
return CATEGORY_STYLES[(cat||'').toLowerCase()] || CATEGORY_STYLES.general;
}
function scoreStyle(s) {
if (s >= 0.5) return { bar:'#16a34a', label:'Strong match', fg:'#15803d' };
if (s >= 0.3) return { bar:'#d97706', label:'Partial match', fg:'#a16207' };
return { bar:'#dc2626', label:'Weak match', fg:'#b91c1c' };
}
function mkBadge(text, bg, fg) {
const el = document.createElement('span');
el.className = 'badge';
el.textContent = text;
el.style.background = bg;
el.style.color = fg;
return el;
}
let allDocs = [];
// ── Toast ────────────────────────────────────────────────────────────
let toastTimer;
function toast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 3000);
}
// ── Search ───────────────────────────────────────────────────────────
async function doSearch() {
const query = document.getElementById('search-input').value.trim();
if (!query) { document.getElementById('search-input').focus(); return; }
setSearchLoading(true);
try {
const res = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, top_k: 3 }),
});
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || 'Search failed'); }
renderResults(await res.json(), query);
} catch (e) {
document.getElementById('results-area').innerHTML =
`<div class="state-box"><div class="state-icon">&#9888;&#65039;</div><p style="color:#dc2626">${escHtml(e.message)}</p></div>`;
} finally {
setSearchLoading(false);
}
}
function setSearchLoading(on) {
const btn = document.getElementById('search-btn');
btn.disabled = on;
btn.innerHTML = on
? '<div class="spinner"></div><span>Searching...</span>'
: '<span id="search-btn-text">Search</span>';
}
function setQuery(q) {
document.getElementById('search-input').value = q;
doSearch();
}
// ── Render results ───────────────────────────────────────────────────
function renderResults(results, query) {
const area = document.getElementById('results-area');
if (!results.length) {
area.innerHTML = `<div class="state-box"><div class="state-icon">&#128269;</div>
<p>No results for "<strong>${escHtml(query)}</strong>"</p>
<p class="hint">Try adding more documents to the corpus.</p></div>`;
return;
}
const rankLabels = ['#1','#2','#3'];
const rankClasses = ['rank-1','rank-2','rank-3'];
const heading = document.createElement('p');
heading.className = 'results-heading';
heading.innerHTML = `Top <strong>${results.length}</strong> results for <strong>"${escHtml(query)}"</strong>`;
const grid = document.createElement('div');
grid.className = 'results-grid';
results.forEach((r, i) => {
const pct = (r.score * 100).toFixed(1);
const ss = scoreStyle(r.score);
const cs = catStyle(r.metadata.category);
const card = document.createElement('div');
card.className = 'result-card';
card.style.borderTop = `3px solid ${ss.bar}`;
// Rank + score
const topRow = document.createElement('div');
topRow.className = 'result-rank';
const rankEl = document.createElement('span');
rankEl.className = `rank-badge ${rankClasses[i]||'rank-3'}`;
rankEl.textContent = rankLabels[i]||`#${i+1}`;
const scoreEl = document.createElement('span');
scoreEl.className = 'score-pct';
scoreEl.style.color = ss.bar;
scoreEl.textContent = `${pct}%`;
topRow.appendChild(rankEl);
topRow.appendChild(scoreEl);
// Bar
const barWrap = document.createElement('div');
barWrap.className = 'score-bar-wrap';
const barFill = document.createElement('div');
barFill.className = 'score-bar-fill';
barFill.style.setProperty('--target-w', `${pct}%`);
barFill.style.background = ss.bar;
barWrap.appendChild(barFill);
// Label
const label = document.createElement('span');
label.className = 'score-label';
label.style.color = ss.fg;
label.textContent = `${ss.label} β€’ cosine = ${r.score.toFixed(4)}`;
// Badges
const badges = document.createElement('div');
badges.className = 'badges';
if (r.metadata.category) badges.appendChild(mkBadge(r.metadata.category, cs.bg, cs.fg));
if (r.metadata.topic) badges.appendChild(mkBadge(r.metadata.topic, '#f1f5f9', '#475569'));
// Text
const textEl = document.createElement('p');
textEl.className = 'result-text';
textEl.textContent = r.text;
card.append(topRow, barWrap, label, badges, textEl);
grid.appendChild(card);
});
const hint = document.createElement('p');
hint.className = 'swipe-hint';
hint.textContent = 'Swipe to see all results';
area.innerHTML = '';
area.appendChild(heading);
area.appendChild(grid);
area.appendChild(hint);
}
// ── Add document ─────────────────────────────────────────────────────
async function doAdd() {
const text = document.getElementById('add-text').value.trim();
const category = document.getElementById('add-category').value.trim() || 'general';
const topic = document.getElementById('add-topic').value.trim();
if (!text) { showAddStatus('Please enter some text.', 'error'); return; }
setAddLoading(true);
try {
const res = await fetch('/api/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, category, topic }),
});
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || 'Add failed'); }
document.getElementById('add-text').value = '';
document.getElementById('add-category').value = '';
document.getElementById('add-topic').value = '';
showAddStatus('Document added and embedded!', 'success');
await loadDocuments();
toast('Document added to corpus');
} catch (e) {
showAddStatus(e.message, 'error');
} finally {
setAddLoading(false);
}
}
function setAddLoading(on) {
const btn = document.getElementById('add-btn');
btn.disabled = on;
btn.innerHTML = on
? '<div class="spinner"></div><span>Embedding...</span>'
: '<span id="add-btn-text">+ Add Document</span>';
}
function showAddStatus(msg, type) {
const el = document.getElementById('add-status');
el.textContent = msg;
el.className = `visible ${type}`;
setTimeout(() => { el.className = ''; }, 4000);
}
// ── Corpus ───────────────────────────────────────────────────────────
async function loadDocuments() {
try {
const res = await fetch('/api/documents');
allDocs = await res.json();
document.getElementById('nav-count').textContent = allDocs.length;
renderCorpus(allDocs);
} catch (e) {
document.getElementById('corpus-grid').innerHTML =
`<div class="state-box"><p style="color:#dc2626">Failed to load documents.</p></div>`;
}
}
function renderCorpus(docs) {
document.getElementById('corpus-count').textContent =
`${docs.length} document${docs.length !== 1 ? 's' : ''}`;
const grid = document.getElementById('corpus-grid');
if (!docs.length) {
grid.innerHTML = `<div class="state-box"><div class="state-icon">&#128203;</div>
<p>No documents yet. Add some above!</p></div>`;
return;
}
grid.innerHTML = '';
docs.forEach(doc => {
const cs = catStyle(doc.metadata.category);
const card = document.createElement('div');
card.className = 'doc-card';
card.title = 'Tap to search for similar documents';
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = doc.metadata.category || 'general';
badge.style.background = cs.bg;
badge.style.color = cs.fg;
const textEl = document.createElement('p');
textEl.className = 'doc-text';
textEl.textContent = doc.text;
card.appendChild(badge);
if (doc.metadata.topic) {
const topicEl = document.createElement('span');
topicEl.className = 'doc-topic';
topicEl.textContent = doc.metadata.topic;
card.appendChild(topicEl);
}
card.appendChild(textEl);
card.onclick = () => setQuery(doc.text);
grid.appendChild(card);
});
}
function filterCorpus() {
const q = document.getElementById('corpus-filter').value.toLowerCase();
renderCorpus(allDocs.filter(d =>
d.text.toLowerCase().includes(q) ||
(d.metadata.category||'').toLowerCase().includes(q) ||
(d.metadata.topic||'').toLowerCase().includes(q)
));
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('search-input').addEventListener('keydown', e => {
if (e.key === 'Enter') doSearch();
});
loadDocuments();
});
</script>
</body>
</html>