Spaces:
Sleeping
Sleeping
| <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">🔍</div> | |
| <div class="brand-text"> | |
| <h1>Vector DB Explorer</h1> | |
| <p>Semantic search · cosine similarity · 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">🔎</span> Semantic Search</span> | |
| </div> | |
| <div class="card-body"> | |
| <div class="search-row"> | |
| <div class="search-input-wrap"> | |
| <span class="search-icon">🔍</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">🤔</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">➕</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">📚</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">📋</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">⚠️</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">🔍</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">📋</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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| document.getElementById('search-input').addEventListener('keydown', e => { | |
| if (e.key === 'Enter') doSearch(); | |
| }); | |
| loadDocuments(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |