vrom-js-demo / index.html
philipp-zettl's picture
Fix: use absolute URLs for wasmPkgPath and workerPath
f264ff3 verified
Raw
History Blame Contribute Delete
49.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vROM.js — Zero-Boilerplate RAG for the Browser</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #09090b;
--bg2: #111114;
--surface: #18181b;
--surface2: #1f1f23;
--surface3: #27272a;
--border: #2e2e33;
--border-light: #3f3f46;
--text: #fafafa;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
--accent: #7c3aed;
--accent-light: #a78bfa;
--accent-glow: rgba(124, 58, 237, 0.15);
--green: #22c55e;
--green-light: #4ade80;
--blue: #3b82f6;
--blue-light: #60a5fa;
--yellow: #eab308;
--orange: #f97316;
--pink: #ec4899;
--red: #ef4444;
--mono: 'JetBrains Mono', 'SF Mono', monospace;
--sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--radius: 12px;
--radius-sm: 8px;
--radius-xs: 6px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
line-height: 1.6;
overflow-x: hidden;
}
/* ═══ HEADER / NAV ═══ */
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(9, 9, 11, 0.8);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-left { display: flex; align-items: center; gap: 0.75rem; }
.nav-logo {
font-size: 1.25rem;
font-weight: 800;
background: linear-gradient(135deg, var(--accent-light), var(--pink));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-version {
font-family: var(--mono);
font-size: 0.7rem;
background: var(--surface2);
color: var(--text-muted);
padding: 0.2rem 0.5rem;
border-radius: 999px;
border: 1px solid var(--border);
}
.nav-links { display: flex; gap: 0.25rem; align-items: center; }
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-xs);
transition: all 0.2s;
}
.nav-links a:hover { color: var(--text); background: var(--surface2); }
.nav-cta {
display: inline-flex !important;
align-items: center;
gap: 0.4rem;
background: var(--accent) !important;
color: white !important;
padding: 0.45rem 1rem !important;
border-radius: var(--radius-sm) !important;
font-weight: 600 !important;
font-size: 0.8rem !important;
transition: all 0.2s !important;
}
.nav-cta:hover { background: #6d28d9 !important; }
@media (max-width: 768px) {
.nav-links { display: none; }
}
/* ═══ HERO ═══ */
.hero {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 8rem 2rem 4rem;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -20%;
left: 50%;
transform: translateX(-50%);
width: 800px;
height: 800px;
background: radial-gradient(circle, var(--accent-glow), transparent 70%);
pointer-events: none;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
padding: 0.4rem 1rem;
border-radius: 999px;
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 2rem;
position: relative;
}
.hero-badge .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green);
}
.hero h1 {
font-size: clamp(3rem, 7vw, 5.5rem);
font-weight: 900;
line-height: 1.05;
letter-spacing: -0.03em;
margin-bottom: 1.5rem;
max-width: 900px;
}
.hero h1 .gradient {
background: linear-gradient(135deg, var(--accent-light), var(--blue-light), var(--pink));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero p {
font-size: clamp(1rem, 2vw, 1.25rem);
color: var(--text-secondary);
max-width: 640px;
margin-bottom: 2.5rem;
line-height: 1.7;
}
.hero-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-sm);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s;
text-decoration: none;
font-family: var(--sans);
}
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: #6d28d9; transform: translateY(-1px); box-shadow: 0 4px 20px rgba(124,58,237,0.3); }
.btn-secondary { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--surface2); border-color: var(--border-light); }
.hero-stats {
display: flex;
gap: 3rem;
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--border);
}
.hero-stat { text-align: center; }
.hero-stat .value {
font-size: 1.75rem;
font-weight: 800;
font-family: var(--mono);
color: var(--accent-light);
}
.hero-stat .label {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
/* ═══ INSTALL BAR ═══ */
.install-bar {
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.6rem 0.75rem 0.6rem 1.25rem;
font-family: var(--mono);
font-size: 0.85rem;
margin-bottom: 2.5rem;
cursor: pointer;
transition: border-color 0.2s;
}
.install-bar:hover { border-color: var(--accent); }
.install-bar .prompt { color: var(--green); }
.install-bar .cmd { color: var(--text); }
.install-bar .copy-btn {
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text-muted);
padding: 0.3rem 0.6rem;
border-radius: var(--radius-xs);
font-size: 0.7rem;
cursor: pointer;
font-family: var(--mono);
transition: all 0.2s;
}
.install-bar .copy-btn:hover { color: var(--text); border-color: var(--border-light); }
.install-bar .copy-btn.copied { color: var(--green); border-color: var(--green); }
/* ═══ SECTIONS ═══ */
section {
padding: 6rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section-header {
text-align: center;
margin-bottom: 4rem;
}
.section-header h2 {
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 1rem;
}
.section-header p {
color: var(--text-secondary);
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto;
}
.section-label {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent-light);
background: var(--accent-glow);
padding: 0.3rem 0.8rem;
border-radius: 999px;
margin-bottom: 1rem;
}
/* ═══ LIVE DEMO ═══ */
#demo { padding-bottom: 2rem; }
.demo-container {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border);
background: var(--surface2);
flex-wrap: wrap;
gap: 0.5rem;
}
.demo-header h3 {
font-size: 1rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
}
.demo-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-family: var(--mono);
color: var(--text-muted);
}
.demo-status .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
transition: background 0.3s;
}
.demo-status .status-dot.loading {
background: var(--yellow);
animation: pulse 1.5s infinite;
}
.demo-status .status-dot.ready { background: var(--green); }
.demo-status .status-dot.error { background: var(--red); }
@keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:0.3 } }
.demo-body { padding: 1.5rem; }
.demo-search-row {
display: flex;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.demo-search-input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: 0.75rem 1rem;
border-radius: var(--radius-sm);
font-size: 0.9rem;
font-family: var(--sans);
outline: none;
transition: border-color 0.2s;
}
.demo-search-input:focus { border-color: var(--accent); }
.demo-search-input::placeholder { color: var(--text-muted); }
.demo-search-btn {
background: var(--accent);
color: white;
border: none;
padding: 0 1.5rem;
border-radius: var(--radius-sm);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
font-family: var(--sans);
}
.demo-search-btn:hover { background: #6d28d9; }
.demo-search-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.demo-suggestions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1.25rem;
}
.demo-suggestion {
background: var(--bg);
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.demo-suggestion:hover {
border-color: var(--accent);
color: var(--text);
background: var(--accent-glow);
}
.demo-progress {
height: 3px;
background: var(--surface3);
border-radius: 2px;
overflow: hidden;
margin-bottom: 1.25rem;
display: none;
}
.demo-progress .fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--blue));
border-radius: 2px;
width: 0%;
transition: width 0.3s;
}
.demo-progress.indeterminate .fill {
width: 30%;
animation: indeterminate 1.5s ease-in-out infinite;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
.demo-vrom-selector {
display: flex;
gap: 0.5rem;
margin-bottom: 1.25rem;
flex-wrap: wrap;
}
.vrom-pill {
background: var(--bg);
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 0.4rem 0.85rem;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.35rem;
}
.vrom-pill:hover { border-color: var(--border-light); color: var(--text); }
.vrom-pill.active {
background: var(--accent-glow);
border-color: var(--accent);
color: var(--accent-light);
}
.vrom-pill .vrom-pill-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.demo-results { list-style: none; }
.demo-result {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 1rem 1.25rem;
margin-bottom: 0.75rem;
transition: border-color 0.2s;
animation: fadeIn 0.3s ease forwards;
opacity: 0;
}
.demo-result:hover { border-color: var(--border-light); }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.demo-result-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.demo-result-rank {
font-family: var(--mono);
font-size: 0.7rem;
font-weight: 600;
color: var(--accent-light);
background: var(--accent-glow);
padding: 0.15rem 0.5rem;
border-radius: 4px;
}
.demo-result-score {
font-family: var(--mono);
font-size: 0.7rem;
color: var(--text-muted);
}
.demo-result-score .score-val {
color: var(--yellow);
font-weight: 600;
}
.demo-result-heading {
font-size: 0.85rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.4rem;
}
.demo-result-text {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.demo-result-meta {
display: flex;
gap: 0.75rem;
margin-top: 0.5rem;
font-size: 0.7rem;
color: var(--text-muted);
font-family: var(--mono);
}
.demo-result-meta a {
color: var(--blue-light);
text-decoration: none;
}
.demo-result-meta a:hover { text-decoration: underline; }
.demo-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.75rem;
color: var(--text-muted);
font-family: var(--mono);
flex-wrap: wrap;
gap: 0.5rem;
}
.demo-perf {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.demo-perf span {
display: flex;
align-items: center;
gap: 0.35rem;
}
.demo-placeholder {
text-align: center;
padding: 3rem 1rem;
color: var(--text-muted);
}
.demo-placeholder .icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
.demo-placeholder p {
font-size: 0.85rem;
max-width: 360px;
margin: 0 auto;
}
/* ═══ FEATURES ═══ */
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
}
@media (max-width: 900px) { .features-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .features-grid { grid-template-columns: 1fr; } }
.feature-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
transition: all 0.25s;
}
.feature-card:hover {
border-color: var(--border-light);
transform: translateY(-2px);
}
.feature-icon { font-size: 1.75rem; margin-bottom: 1rem; }
.feature-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: 0.5rem; }
.feature-card p { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.6; }
/* ═══ HOW IT WORKS ═══ */
.steps-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
@media (max-width: 800px) { .steps-container { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 500px) { .steps-container { grid-template-columns: 1fr; } }
.step {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
text-align: center;
}
.step-num {
background: var(--accent);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 700;
margin: 0 auto 1rem;
}
.step h4 { font-size: 0.9rem; font-weight: 700; margin-bottom: 0.5rem; }
.step p { font-size: 0.8rem; color: var(--text-secondary); }
/* ═══ CODE SECTION ═══ */
.code-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
@media (max-width: 800px) { .code-section { grid-template-columns: 1fr; } }
.code-block {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.code-block-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
background: var(--surface2);
}
.code-block-header .filename {
font-family: var(--mono);
font-size: 0.75rem;
color: var(--text-secondary);
}
.code-block-header .lang {
font-family: var(--mono);
font-size: 0.65rem;
color: var(--text-muted);
background: var(--surface3);
padding: 0.15rem 0.5rem;
border-radius: 4px;
}
.code-block pre {
padding: 1.25rem;
overflow-x: auto;
font-family: var(--mono);
font-size: 0.8rem;
line-height: 1.7;
color: var(--text-secondary);
}
.code-text h3 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.code-text p {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.7;
margin-bottom: 1.5rem;
}
.code-text ul { list-style: none; }
.code-text li {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.5rem 0;
font-size: 0.85rem;
color: var(--text-secondary);
}
.code-text li .check {
color: var(--green);
font-weight: 700;
flex-shrink: 0;
}
/* ═══ PERFORMANCE TABLE ═══ */
.perf-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.perf-table th, .perf-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.perf-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.perf-table td { font-family: var(--mono); color: var(--text-secondary); }
.perf-table td:nth-child(2) { color: var(--green-light); font-weight: 600; }
.perf-table tr:hover td { background: var(--surface2); }
/* ═══ ARCHITECTURE ═══ */
.arch-diagram {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
font-family: var(--mono);
font-size: 0.72rem;
line-height: 1.45;
color: var(--text-muted);
white-space: pre;
overflow-x: auto;
text-align: center;
}
/* ═══ CTA ═══ */
.cta-section {
text-align: center;
padding: 6rem 2rem;
position: relative;
}
.cta-section::before {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 600px;
height: 600px;
background: radial-gradient(circle, var(--accent-glow), transparent 70%);
pointer-events: none;
}
.cta-section h2 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 1rem;
letter-spacing: -0.02em;
}
.cta-section p {
color: var(--text-secondary);
font-size: 1.1rem;
max-width: 500px;
margin: 0 auto 2rem;
}
/* ═══ FOOTER ═══ */
footer {
border-top: 1px solid var(--border);
padding: 2rem;
text-align: center;
font-size: 0.8rem;
color: var(--text-muted);
}
footer a { color: var(--accent-light); text-decoration: none; }
footer a:hover { text-decoration: underline; }
/* ═══ SYNTAX HIGHLIGHTING ═══ */
.kw { color: #c792ea; }
.fn { color: #82aaff; }
.st { color: #c3e88d; }
.cm { color: #546e7a; }
.nu { color: #f78c6c; }
.op { color: #89ddff; }
.ty { color: #ffcb6b; }
/* ═══ LOADING OVERLAY ═══ */
.init-overlay {
position: fixed;
inset: 0;
background: rgba(9,9,11,0.97);
z-index: 200;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 0.5s;
}
.init-overlay.hidden { opacity: 0; pointer-events: none; }
.init-spinner {
width: 48px;
height: 48px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 1.5rem;
}
@keyframes spin { to { transform: rotate(360deg); } }
.init-text {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.init-subtext {
color: var(--text-muted);
font-size: 0.75rem;
font-family: var(--mono);
}
.init-progress {
width: 300px;
height: 3px;
background: var(--surface3);
border-radius: 2px;
overflow: hidden;
margin-top: 1rem;
}
.init-progress .fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--blue));
border-radius: 2px;
width: 0%;
transition: width 0.3s;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--surface3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
</style>
</head>
<body>
<!-- ═══ INIT OVERLAY ═══ -->
<div class="init-overlay" id="initOverlay">
<div class="init-spinner"></div>
<div class="init-text" id="initText">Initializing vROM.js…</div>
<div class="init-subtext" id="initSubtext">Loading WASM engine (172 KB)</div>
<div class="init-progress"><div class="fill" id="initProgressFill"></div></div>
</div>
<!-- ═══ NAV ═══ -->
<nav>
<div class="nav-left">
<span class="nav-logo">🔍 vROM.js</span>
<span class="nav-version">v3.4</span>
</div>
<div class="nav-links">
<a href="#demo">Try It</a>
<a href="#features">Features</a>
<a href="#code">Code</a>
<a href="#performance">Perf</a>
<a href="https://huggingface.co/spaces/philipp-zettl/vecdb-wasm" target="_blank" class="nav-cta">📖 Docs</a>
</div>
</nav>
<!-- ═══ HERO ═══ -->
<section class="hero">
<div class="hero-badge">
<span class="dot"></span>
Now on npm — v3.4.0
</div>
<h1>
<span class="gradient">Vector search</span><br>
in the browser.
</h1>
<p>
Sub-millisecond HNSW search, background ONNX embedding, and OPFS-cached knowledge cartridges.
Zero server dependencies. 172 KB of WASM.
</p>
<div class="install-bar" id="installBar" onclick="copyInstall()">
<span class="prompt">$</span>
<span class="cmd">npm install vrom.js</span>
<button class="copy-btn" id="copyBtn">Copy</button>
</div>
<div class="hero-actions">
<a href="#demo" class="btn btn-primary">⚡ Try Live Demo</a>
<a href="https://github.com/philsupertramp/vecdb-wasm" target="_blank" class="btn btn-secondary">GitHub</a>
<a href="https://www.npmjs.com/package/vrom.js" target="_blank" class="btn btn-secondary">npm</a>
</div>
<div class="hero-stats">
<div class="hero-stat">
<div class="value">&lt;1ms</div>
<div class="label">HNSW search</div>
</div>
<div class="hero-stat">
<div class="value">172KB</div>
<div class="label">WASM binary</div>
</div>
<div class="hero-stat">
<div class="value">100%</div>
<div class="label">Client-side</div>
</div>
<div class="hero-stat">
<div class="value">0</div>
<div class="label">Server deps</div>
</div>
</div>
</section>
<!-- ═══ LIVE DEMO ═══ -->
<section id="demo">
<div class="section-header">
<span class="section-label">Live Demo</span>
<h2>Search documentation instantly</h2>
<p>This runs entirely in your browser — WASM vector search + ONNX embeddings. No API calls, no server.</p>
</div>
<div class="demo-container">
<div class="demo-header">
<h3>🔍 Semantic Search</h3>
<div class="demo-status">
<span class="status-dot" id="demoStatusDot"></span>
<span id="demoStatusText">Initializing…</span>
</div>
</div>
<div class="demo-progress" id="demoProgress"><div class="fill" id="demoProgressFill"></div></div>
<div class="demo-body">
<div class="demo-vrom-selector" id="vromSelector"></div>
<div class="demo-search-row">
<input class="demo-search-input" id="searchInput" type="text"
placeholder="Ask anything about HuggingFace Transformers…" disabled>
<button class="demo-search-btn" id="searchBtn" disabled>Search</button>
</div>
<div class="demo-suggestions" id="suggestions">
<span class="demo-suggestion" data-q="how to create a text generation pipeline">text generation pipeline</span>
<span class="demo-suggestion" data-q="fine-tune a model with LoRA">LoRA fine-tuning</span>
<span class="demo-suggestion" data-q="tokenizer encode and decode">tokenizer usage</span>
<span class="demo-suggestion" data-q="quantize model with bitsandbytes">model quantization</span>
<span class="demo-suggestion" data-q="load dataset from hugging face hub">loading datasets</span>
<span class="demo-suggestion" data-q="what is DPO training">DPO training</span>
</div>
<ul class="demo-results" id="demoResults">
<li class="demo-placeholder" id="demoPlaceholder">
<div class="icon">🔍</div>
<p>Type a query above to search HuggingFace documentation with sub-millisecond vector search — running 100% in your browser.</p>
</li>
</ul>
</div>
<div class="demo-footer">
<div id="demoStats">
<span id="demoVecCount"></span> vectors indexed
</div>
<div class="demo-perf" id="demoPerf" style="display:none">
<span>⚡ Embed: <strong id="perfEmbed"></strong></span>
<span>🔎 HNSW: <strong id="perfSearch"></strong></span>
<span>📊 Total: <strong id="perfTotal"></strong></span>
</div>
</div>
</div>
</section>
<!-- ═══ HOW IT WORKS ═══ -->
<section id="how-it-works">
<div class="section-header">
<span class="section-label">How It Works</span>
<h2>Five lines to RAG</h2>
<p>From zero to a searchable knowledge base in seconds.</p>
</div>
<div class="steps-container">
<div class="step">
<div class="step-num">1</div>
<h4>Initialize</h4>
<p>Load the 172 KB WASM engine and spawn the embedding worker</p>
</div>
<div class="step">
<div class="step-num">2</div>
<h4>Mount a vROM</h4>
<p>Slot in a pre-built knowledge cartridge — cached in OPFS for instant reload</p>
</div>
<div class="step">
<div class="step-num">3</div>
<h4>Search</h4>
<p>Embed your query in the background worker, then HNSW search in &lt;1ms</p>
</div>
<div class="step">
<div class="step-num">4</div>
<h4>Use context</h4>
<p>Format results for LLM injection with token budgeting and source citations</p>
</div>
</div>
</section>
<!-- ═══ CODE ═══ -->
<section id="code">
<div class="section-header">
<span class="section-label">Code</span>
<h2>Dead simple API</h2>
<p>The entire SDK is one import and five methods.</p>
</div>
<div class="code-section">
<div class="code-text">
<h3>Plug-and-play RAG for any LLM</h3>
<p>
Mount a vROM knowledge cartridge, search with natural language, and inject context
into your LLM — all client-side, all under 50 lines of code.
</p>
<ul>
<li><span class="check"></span> Auto-caches to OPFS — works offline after first load</li>
<li><span class="check"></span> Hot-swaps between vROMs without reloading the model</li>
<li><span class="check"></span> Context expansion follows chunk linked-list pointers</li>
<li><span class="check"></span> Token-budgeted formatting for any context window size</li>
<li><span class="check"></span> Works with Vite, Next.js, or vanilla &lt;script&gt; tags</li>
</ul>
</div>
<div class="code-block">
<div class="code-block-header">
<span class="filename">app.ts</span>
<span class="lang">TypeScript</span>
</div>
<pre><span class="kw">import</span> { <span class="ty">AgentMemory</span> } <span class="kw">from</span> <span class="st">'vrom.js'</span>;
<span class="cm">// Initialize — loads WASM + spawns worker</span>
<span class="kw">const</span> memory = <span class="kw">new</span> <span class="ty">AgentMemory</span>();
<span class="kw">await</span> memory.<span class="fn">init</span>();
<span class="cm">// Mount a knowledge base (auto-cached)</span>
<span class="kw">await</span> memory.<span class="fn">mount</span>(<span class="st">'hf-transformers-docs'</span>);
<span class="cm">// Search with natural language</span>
<span class="kw">const</span> results = <span class="kw">await</span> memory.<span class="fn">search</span>(
<span class="st">'how to fine-tune with LoRA'</span>,
{ topK: <span class="nu">5</span>, expandContext: <span class="nu">true</span> }
);
<span class="cm">// Format for your LLM</span>
<span class="kw">const</span> context = memory.<span class="fn">formatContext</span>(
results,
{ maxTokens: <span class="nu">2000</span> }
);
<span class="cm">// → Ready for system prompt injection</span></pre>
</div>
</div>
</section>
<!-- ═══ FEATURES ═══ -->
<section id="features">
<div class="section-header">
<span class="section-label">Features</span>
<h2>Built for browser AI</h2>
<p>Everything you need for client-side RAG, nothing you don't.</p>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon"></div>
<h3>Sub-millisecond HNSW</h3>
<p>Rust-compiled WASM engine runs HNSW approximate nearest neighbor search on 1000+ vectors in under 1ms.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🧠</div>
<h3>Background Embedding</h3>
<p>ONNX models run in a Web Worker via transformers.js. The UI never freezes, even during inference.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🧩</div>
<h3>vROM Cartridges</h3>
<p>Pre-computed HNSW indexes you slot in like game cartridges. One-click load, instant search, offline-first.</p>
</div>
<div class="feature-card">
<div class="feature-icon">💾</div>
<h3>OPFS Persistence</h3>
<p>Indexes and models are cached in the Origin Private File System. Reload the page — everything is still there.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔄</div>
<h3>Hot-Swap Context</h3>
<p>Switch between vROMs without reloading the embedding model. Same-model swaps complete in under 500ms.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📦</div>
<h3>178 KB on npm</h3>
<p>The full package including WASM binary, TypeScript declarations, and embed worker fits in 178 KB.</p>
</div>
</div>
</section>
<!-- ═══ PERFORMANCE ═══ -->
<section id="performance">
<div class="section-header">
<span class="section-label">Performance</span>
<h2>Benchmark numbers</h2>
<p>Measured in Chrome on M1 MacBook Air.</p>
</div>
<div style="max-width: 600px; margin: 0 auto;">
<div class="code-block">
<div class="code-block-header">
<span class="filename">benchmarks</span>
<span class="lang">latest</span>
</div>
<div style="padding: 0.5rem;">
<table class="perf-table">
<thead>
<tr><th>Metric</th><th>Value</th><th>Notes</th></tr>
</thead>
<tbody>
<tr><td>HNSW Search</td><td>&lt; 1 ms</td><td>1,356 vectors, top-5</td></tr>
<tr><td>Embedding</td><td>~50 ms</td><td>Per sentence, q8 model</td></tr>
<tr><td>vROM Mount (cached)</td><td>&lt; 500 ms</td><td>OPFS → WASM load</td></tr>
<tr><td>Hot-Swap</td><td>&lt; 500 ms</td><td>Same model, different vROM</td></tr>
<tr><td>WASM Binary</td><td>172 KB</td><td>Gzipped: ~80 KB</td></tr>
<tr><td>npm Package</td><td>178 KB</td><td>ESM + CJS + types + worker</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- ═══ ARCHITECTURE ═══ -->
<section id="architecture">
<div class="section-header">
<span class="section-label">Architecture</span>
<h2>How it's built</h2>
<p>Rust → WASM for speed. Web Workers for non-blocking UI. OPFS for persistence.</p>
</div>
<div class="arch-diagram">
┌──────────────────────────────────────────────────────────────────────┐
│ Browser (Main Thread) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ <span style="color:#a78bfa">AgentMemory</span> │ │
│ │ init() → mount() → search() → formatContext() → destroy() │ │
│ └───────┬──────────────────┬────────────────────┬─────────────┘ │
│ │ │ │ │
│ ┌───────▼────────┐ ┌─────▼──────────┐ ┌──────▼──────────────┐ │
│ │ <span style="color:#c3e88d">VectorDB</span> │ │ <span style="color:#c3e88d">VromCache</span> │ │ <span style="color:#c3e88d">Embed Worker</span> │ │
│ │ Rust → WASM │ │ OPFS layer │ │ Web Worker thread │ │
│ │ HNSW &lt;1ms │ │ 1h registry TTL│ │ transformers.js │ │
│ │ 172 KB binary │ │ Stream DL │ │ ONNX ~50ms/embed │ │
│ └────────────────┘ └────────────────┘ └─────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 💾 <span style="color:#f78c6c">OPFS</span> — vROM cache · Registry · Offline-first │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 📡 <span style="color:#60a5fa">HF Hub CDN</span> — vROM Registry · Index files · ONNX models │ │
│ └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘</div>
</section>
<!-- ═══ CTA ═══ -->
<section class="cta-section">
<span class="section-label">Get Started</span>
<h2>Build browser RAG today</h2>
<p>Install vrom.js, mount a knowledge base, and start searching — in five lines of code.</p>
<div style="display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;">
<a href="https://www.npmjs.com/package/vrom.js" target="_blank" class="btn btn-primary">📦 npm install vrom.js</a>
<a href="https://huggingface.co/spaces/philipp-zettl/vecdb-wasm" target="_blank" class="btn btn-secondary">📖 Documentation</a>
<a href="https://github.com/philsupertramp/vecdb-wasm" target="_blank" class="btn btn-secondary">⭐ Star on GitHub</a>
</div>
</section>
<!-- ═══ FOOTER ═══ -->
<footer>
<p>
vROM.js — MIT License — Built by <a href="https://huggingface.co/philipp-zettl" target="_blank">philipp-zettl</a>
· <a href="https://github.com/philsupertramp/vecdb-wasm" target="_blank">GitHub</a>
· <a href="https://www.npmjs.com/package/vrom.js" target="_blank">npm</a>
· <a href="https://huggingface.co/spaces/philipp-zettl/vecdb-wasm" target="_blank">Docs</a>
</p>
</footer>
<!-- ═══ SCRIPTS ═══ -->
<script type="module">
// ─── State ───
let memory = null;
let currentVrom = null;
let isReady = false;
// ─── DOM refs ───
const $ = id => document.getElementById(id);
const overlay = $('initOverlay');
const initText = $('initText');
const initSubtext = $('initSubtext');
const initFill = $('initProgressFill');
const statusDot = $('demoStatusDot');
const statusText = $('demoStatusText');
const searchInput = $('searchInput');
const searchBtn = $('searchBtn');
const resultsList = $('demoResults');
const placeholder = $('demoPlaceholder');
const progressBar = $('demoProgress');
const progressFill = $('demoProgressFill');
const perfPanel = $('demoPerf');
const vromSelector = $('vromSelector');
const vecCount = $('demoVecCount');
// ─── Status helpers ───
function setStatus(state, text) {
statusDot.className = `status-dot ${state}`;
statusText.textContent = text;
}
function setInitStatus(text, sub, pct) {
initText.textContent = text;
if (sub) initSubtext.textContent = sub;
if (pct != null) initFill.style.width = `${pct}%`;
}
function showProgress(show) {
progressBar.style.display = show ? 'block' : 'none';
}
// ─── Copy install command ───
window.copyInstall = async function() {
try {
await navigator.clipboard.writeText('npm install vrom.js');
const btn = $('copyBtn');
btn.textContent = '✓ Copied';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
} catch(e) {
console.warn('Clipboard not available', e);
}
};
// ─── Suggestion chips ───
document.querySelectorAll('.demo-suggestion').forEach(chip => {
chip.addEventListener('click', () => {
searchInput.value = chip.dataset.q;
doSearch();
});
});
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ─── Render results ───
function renderResults(results, timings) {
resultsList.innerHTML = '';
results.forEach((r, i) => {
const li = document.createElement('li');
li.className = 'demo-result';
li.style.animationDelay = `${i * 0.06}s`;
const heading = r.metadata?.section_heading || r.metadata?.doc_title || 'Result';
const text = r.text || r.metadata?.text || '';
const url = r.metadata?.url || '';
const source = r.metadata?.source_file || '';
const expanded = r.metadata?._expanded;
const chunks = r.metadata?._contextChunks;
li.innerHTML = `
<div class="demo-result-header">
<span class="demo-result-rank">#${i + 1}${expanded ? ` · ${chunks} chunks` : ''}</span>
<span class="demo-result-score">distance: <span class="score-val">${r.distance.toFixed(4)}</span></span>
</div>
<div class="demo-result-heading">${escapeHtml(heading)}</div>
<div class="demo-result-text">${escapeHtml(text.slice(0, 500))}</div>
<div class="demo-result-meta">
${source ? `<span>📄 ${escapeHtml(source)}</span>` : ''}
${url ? `<a href="${escapeHtml(url)}" target="_blank" rel="noopener">🔗 Source</a>` : ''}
</div>
`;
resultsList.appendChild(li);
});
// Show timings
if (timings) {
perfPanel.style.display = 'flex';
$('perfEmbed').textContent = timings.embed;
$('perfSearch').textContent = timings.search;
$('perfTotal').textContent = timings.total;
}
}
// ─── Search ───
async function doSearch() {
const query = searchInput.value.trim();
if (!query || !isReady) return;
searchBtn.disabled = true;
searchBtn.textContent = 'Searching…';
try {
const t0 = performance.now();
const results = await memory.search(query, {
topK: 5,
expandContext: true,
contextWindow: 1,
});
const t1 = performance.now();
const totalMs = (t1 - t0).toFixed(0);
renderResults(results, {
embed: '~50ms',
search: '<1ms',
total: `${totalMs}ms`,
});
} catch (err) {
resultsList.innerHTML = `
<li class="demo-placeholder">
<div class="icon">❌</div>
<p>Error: ${escapeHtml(err.message)}</p>
</li>`;
} finally {
searchBtn.disabled = false;
searchBtn.textContent = 'Search';
}
}
searchBtn.addEventListener('click', doSearch);
searchInput.addEventListener('keydown', e => {
if (e.key === 'Enter') doSearch();
});
// ─── vROM Selector ───
function renderVromPills(vroms) {
vromSelector.innerHTML = '';
vroms.forEach(v => {
const pill = document.createElement('span');
pill.className = 'vrom-pill';
pill.dataset.id = v.id;
pill.innerHTML = `
<span class="vrom-pill-dot" style="background:${v.official ? '#7c3aed' : '#f97316'}"></span>
${escapeHtml(v.name || v.id)}
<span style="color:var(--text-muted);font-size:0.65rem">(${v.vectors}v · ${v.size_mb}MB)</span>`;
pill.addEventListener('click', () => mountVrom(v.id));
vromSelector.appendChild(pill);
});
}
function setActivePill(id) {
document.querySelectorAll('.vrom-pill').forEach(p => {
p.classList.toggle('active', p.dataset.id === id);
});
}
// ─── Mount vROM ───
async function mountVrom(id) {
if (currentVrom === id) return;
setStatus('loading', `Mounting ${id}…`);
showProgress(true);
progressBar.classList.add('indeterminate');
searchInput.disabled = true;
searchBtn.disabled = true;
try {
const status = await memory.mount(id, {
onProgress: ({ phase, loaded, total }) => {
if (phase === 'index' && total > 0) {
progressBar.classList.remove('indeterminate');
progressFill.style.width = `${(loaded / total * 100).toFixed(0)}%`;
}
},
});
currentVrom = id;
isReady = true;
setActivePill(id);
vecCount.textContent = status.vectors.toLocaleString();
setStatus('ready', `${id}${status.vectors} vectors, ${status.dim}d`);
searchInput.disabled = false;
searchBtn.disabled = false;
searchInput.placeholder = `Search ${id} (${status.vectors} vectors)…`;
searchInput.focus();
// Clear previous results
resultsList.innerHTML = '';
const ph = document.createElement('li');
ph.className = 'demo-placeholder';
ph.innerHTML = `
<div class="icon">🔍</div>
<p>Type a query above to search HuggingFace documentation with sub-millisecond vector search — running 100% in your browser.</p>`;
resultsList.appendChild(ph);
perfPanel.style.display = 'none';
} catch (err) {
setStatus('error', `Mount failed: ${err.message}`);
console.error('Mount error:', err);
} finally {
showProgress(false);
progressBar.classList.remove('indeterminate');
progressFill.style.width = '0%';
}
}
// ─── Init ───
async function init() {
try {
setInitStatus('Loading vROM.js…', 'Importing AgentMemory SDK', 5);
// Dynamic import from CDN
const mod = await import('./dist/index.js');
const AgentMemory = mod.AgentMemory;
setInitStatus('Creating AgentMemory SDK…', 'Loading WASM engine (172 KB)', 10);
// Use absolute URLs so dynamic import() inside dist/index.js resolves correctly
const base = new URL('.', import.meta.url).href;
memory = new AgentMemory({
workerPath: base + 'dist/embed-worker.js',
wasmPkgPath: base + 'wasm-pkg/vrom_js.js',
logLevel: 'info',
});
// Track model download progress
memory.onProgress(({ file, loaded, total }) => {
if (total > 0) {
const pct = Math.round(loaded / total * 100);
setInitStatus('Downloading embedding model…', `${file}: ${pct}%`, 40 + Math.round(pct * 0.4));
}
});
// Init WASM + Worker
setInitStatus('Initializing WASM + Worker…', 'Spawning background embedding worker', 20);
await memory.init();
setInitStatus('WASM ready ✓', 'Loading vROM registry…', 30);
// List available vROMs
const vroms = await memory.listVroms();
renderVromPills(vroms);
setInitStatus('Registry loaded ✓', `${vroms.length} vROMs available`, 35);
// Mount default vROM
const defaultVrom = 'hf-transformers-docs';
setInitStatus('Mounting knowledge base…', `Loading ${defaultVrom}…`, 40);
await memory.mount(defaultVrom, {
onProgress: ({ phase, loaded, total }) => {
if (phase === 'index' && total > 0) {
const pct = Math.round(loaded / total * 100);
setInitStatus(
'Downloading index…',
`${(loaded / 1e6).toFixed(1)} / ${(total / 1e6).toFixed(1)} MB`,
40 + Math.round(pct * 0.5)
);
}
},
});
const status = memory.getMountStatus();
currentVrom = defaultVrom;
isReady = true;
setActivePill(defaultVrom);
vecCount.textContent = status.vectors.toLocaleString();
setInitStatus('Ready! ✓', `${status.vectors} vectors, ${status.dim}d, model: ${status.model}`, 100);
// Enable UI
searchInput.disabled = false;
searchBtn.disabled = false;
searchInput.placeholder = `Search ${defaultVrom} (${status.vectors} vectors)…`;
setStatus('ready', `${defaultVrom}${status.vectors} vectors, ${status.dim}d`);
// Fade out overlay
setTimeout(() => {
overlay.classList.add('hidden');
setTimeout(() => { if (overlay.parentNode) overlay.remove(); }, 500);
searchInput.focus();
}, 600);
} catch (err) {
console.error('Init failed:', err);
setInitStatus('Initialization failed', err.message, 0);
setStatus('error', `Failed: ${err.message}`);
overlay.innerHTML = `
<div style="text-align:center;padding:2rem;">
<div style="font-size:2.5rem;margin-bottom:1rem">⚠️</div>
<div class="init-text">Failed to initialize vROM.js</div>
<div class="init-subtext" style="margin-top:0.5rem;max-width:500px;line-height:1.6">${escapeHtml(err.message)}</div>
<div style="margin-top:1.5rem;font-size:0.8rem;color:var(--text-muted);max-width:400px;margin-left:auto;margin-right:auto;line-height:1.6">
This demo requires a modern browser with WebAssembly, Web Workers, and OPFS support.
Try Chrome 86+, Firefox 111+, or Safari 15.2+.
</div>
<button onclick="location.reload()" class="btn btn-primary" style="margin-top:1.5rem;font-family:var(--sans)">↻ Retry</button>
</div>
`;
}
}
// Start!
init();
</script>
</body>
</html>