Spaces:
Running
Running
| <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 ; | |
| align-items: center; | |
| gap: 0.4rem; | |
| background: var(--accent) ; | |
| color: white ; | |
| padding: 0.45rem 1rem ; | |
| border-radius: var(--radius-sm) ; | |
| font-weight: 600 ; | |
| font-size: 0.8rem ; | |
| transition: all 0.2s ; | |
| } | |
| .nav-cta:hover { background: #6d28d9 ; } | |
| @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"><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 <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 <script> 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>< 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>< 500 ms</td><td>OPFS → WASM load</td></tr> | |
| <tr><td>Hot-Swap</td><td>< 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 <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> | |