Files

752 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RvLite Demo - SQL, SPARQL, Cypher + Persistence in Browser</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #1a1a2e;
color: #eaeaea;
}
.container {
background: #16213e;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
h1 {
color: #4ecca3;
border-bottom: 3px solid #4ecca3;
padding-bottom: 10px;
margin-top: 0;
}
h2 {
color: #4ecca3;
margin-top: 30px;
}
.status {
padding: 15px;
margin: 15px 0;
border-radius: 5px;
font-family: monospace;
}
.success {
background-color: #1e4d3d;
border-left: 4px solid #4ecca3;
color: #a3f7d8;
}
.info {
background-color: #1e3a5f;
border-left: 4px solid #3498db;
color: #87ceeb;
}
.error {
background-color: #4d1e1e;
border-left: 4px solid #e74c3c;
color: #f7a3a3;
}
.tabs {
display: flex;
gap: 5px;
margin-bottom: 20px;
}
.tab {
padding: 12px 24px;
border: none;
border-radius: 5px 5px 0 0;
cursor: pointer;
font-size: 16px;
background-color: #0f3460;
color: #aaa;
transition: all 0.3s ease;
}
.tab:hover {
background-color: #1a4d7c;
color: #fff;
}
.tab.active {
background-color: #4ecca3;
color: #16213e;
font-weight: bold;
}
.tab-content {
display: none;
padding: 20px;
background-color: #0f3460;
border-radius: 0 5px 5px 5px;
}
.tab-content.active {
display: block;
}
button {
background-color: #4ecca3;
color: #16213e;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin: 5px;
transition: all 0.3s ease;
}
button:hover {
background-color: #3db892;
transform: translateY(-2px);
}
button:disabled {
background-color: #333;
color: #666;
cursor: not-allowed;
transform: none;
}
textarea {
width: 100%;
height: 120px;
padding: 15px;
border-radius: 5px;
border: 2px solid #0f3460;
background-color: #1a1a2e;
color: #eaeaea;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 14px;
resize: vertical;
}
textarea:focus {
outline: none;
border-color: #4ecca3;
}
#output {
background-color: #1a1a2e;
padding: 15px;
border-radius: 5px;
border: 2px solid #0f3460;
margin-top: 20px;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 13px;
white-space: pre-wrap;
word-wrap: break-word;
}
.output-line {
margin: 2px 0;
padding: 2px 5px;
}
.output-success {
color: #4ecca3;
}
.output-error {
color: #e74c3c;
}
.output-info {
color: #3498db;
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
margin-left: 10px;
}
.badge-new {
background-color: #4ecca3;
color: #16213e;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background-color: #0f3460;
padding: 15px;
border-radius: 5px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #4ecca3;
}
.stat-label {
font-size: 12px;
color: #aaa;
margin-top: 5px;
}
.examples {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 10px 0;
}
.example-btn {
background-color: #1a4d7c;
padding: 8px 16px;
font-size: 12px;
}
.example-btn:hover {
background-color: #3498db;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin: 20px 0;
}
.feature-card {
background-color: #0f3460;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #4ecca3;
}
.feature-card h3 {
margin: 0 0 10px 0;
color: #4ecca3;
}
.feature-card p {
margin: 0;
color: #aaa;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>RvLite <span class="badge badge-new">v0.2.0</span></h1>
<p>Standalone vector database with SQL, SPARQL, Cypher + IndexedDB persistence - fully in the browser via WASM</p>
<div class="status info" id="statusDiv">
<strong>Status:</strong> Loading WASM module...
</div>
<div class="stats" id="statsContainer">
<div class="stat-card">
<div class="stat-value" id="vectorCount">-</div>
<div class="stat-label">Vectors</div>
</div>
<div class="stat-card">
<div class="stat-value" id="nodeCount">-</div>
<div class="stat-label">Graph Nodes</div>
</div>
<div class="stat-card">
<div class="stat-value" id="edgeCount">-</div>
<div class="stat-label">Graph Edges</div>
</div>
<div class="stat-card">
<div class="stat-value" id="tripleCount">-</div>
<div class="stat-label">RDF Triples</div>
</div>
</div>
<!-- Persistence Controls -->
<div style="background: #0f3460; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h3 style="margin: 0 0 10px 0; color: #4ecca3;">
Persistence (IndexedDB)
<span id="persistStatus" style="font-size: 12px; color: #aaa; font-weight: normal;"></span>
</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button onclick="saveToIndexedDB()">Save to Browser</button>
<button onclick="loadFromIndexedDB()">Load from Browser</button>
<button onclick="exportJson()">Export JSON</button>
<button onclick="importJson()">Import JSON</button>
<button onclick="clearStorage()" style="background-color: #e74c3c;">Clear Storage</button>
</div>
</div>
<div class="tabs">
<button class="tab active" data-tab="vectors">Vectors</button>
<button class="tab" data-tab="cypher">Cypher</button>
<button class="tab" data-tab="sparql">SPARQL</button>
<button class="tab" data-tab="sql">SQL</button>
</div>
<!-- Vectors Tab -->
<div class="tab-content active" id="vectors">
<h2>Vector Operations</h2>
<div class="examples">
<button class="example-btn" onclick="insertRandomVectors()">Insert 10 Random Vectors</button>
<button class="example-btn" onclick="searchSimilar()">Search Similar</button>
<button class="example-btn" onclick="getVectorById()">Get by ID</button>
</div>
<textarea id="vectorInput" placeholder="Enter vector as JSON array, e.g., [0.1, 0.2, 0.3, ...]">[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]</textarea>
<div style="margin-top: 10px;">
<button onclick="insertVector()">Insert Vector</button>
<button onclick="searchVector()">Search</button>
</div>
</div>
<!-- Cypher Tab -->
<div class="tab-content" id="cypher">
<h2>Cypher Graph Queries</h2>
<div class="examples">
<button class="example-btn" onclick="setCypherExample('create')">CREATE Node</button>
<button class="example-btn" onclick="setCypherExample('relationship')">CREATE Relationship</button>
<button class="example-btn" onclick="setCypherExample('match')">MATCH Nodes</button>
<button class="example-btn" onclick="setCypherExample('path')">Path Query</button>
</div>
<textarea id="cypherInput" placeholder="Enter Cypher query...">CREATE (alice:Person {name: 'Alice', age: 30})</textarea>
<div style="margin-top: 10px;">
<button onclick="executeCypher()">Execute Cypher</button>
<button onclick="clearGraph()">Clear Graph</button>
</div>
</div>
<!-- SPARQL Tab -->
<div class="tab-content" id="sparql">
<h2>SPARQL RDF Queries</h2>
<div class="examples">
<button class="example-btn" onclick="addSampleTriples()">Add Sample Data</button>
<button class="example-btn" onclick="setSparqlExample('select')">SELECT Query</button>
<button class="example-btn" onclick="setSparqlExample('filter')">Filter Query</button>
</div>
<textarea id="sparqlInput" placeholder="Enter SPARQL query...">SELECT ?name WHERE { ?person <http://example.org/name> ?name }</textarea>
<div style="margin-top: 10px;">
<button onclick="executeSparql()">Execute SPARQL</button>
<button onclick="clearTriples()">Clear Triples</button>
</div>
</div>
<!-- SQL Tab -->
<div class="tab-content" id="sql">
<h2>SQL Vector Queries</h2>
<div class="examples">
<button class="example-btn" onclick="setSqlExample('insert')">INSERT Vector</button>
<button class="example-btn" onclick="setSqlExample('select')">SELECT All</button>
<button class="example-btn" onclick="setSqlExample('search')">Vector Search</button>
</div>
<textarea id="sqlInput" placeholder="Enter SQL query...">SELECT * FROM vectors WHERE id = 'vec1'</textarea>
<div style="margin-top: 10px;">
<button onclick="executeSql()">Execute SQL</button>
</div>
</div>
<h2>Output</h2>
<div id="output">Waiting for WASM module to load...</div>
<button onclick="clearOutput()">Clear Output</button>
<div class="feature-grid">
<div class="feature-card">
<h3>Vector Search</h3>
<p>High-performance similarity search with cosine, euclidean, and dot product metrics</p>
</div>
<div class="feature-card">
<h3>Cypher Queries</h3>
<p>Property graph queries with CREATE, MATCH, WHERE, RETURN support</p>
</div>
<div class="feature-card">
<h3>SPARQL Support</h3>
<p>RDF triple store with SELECT queries and pattern matching</p>
</div>
<div class="feature-card">
<h3>SQL Interface</h3>
<p>Familiar SQL syntax for vector operations with pgvector compatibility</p>
</div>
<div class="feature-card">
<h3>IndexedDB Persistence</h3>
<p>Save and load database state in the browser using IndexedDB - data persists across sessions</p>
</div>
</div>
</div>
<!-- Hidden file input for JSON import -->
<input type="file" id="jsonFileInput" accept=".json" style="display: none;">
<script type="module">
import init, { RvLite, RvLiteConfig } from '../pkg/rvlite.js';
const statusDiv = document.getElementById('statusDiv');
const output = document.getElementById('output');
let db = null;
// Make functions available globally
window.insertVector = insertVector;
window.searchVector = searchVector;
window.insertRandomVectors = insertRandomVectors;
window.searchSimilar = searchSimilar;
window.getVectorById = getVectorById;
window.executeCypher = executeCypher;
window.clearGraph = clearGraph;
window.executeSparql = executeSparql;
window.clearTriples = clearTriples;
window.executeSql = executeSql;
window.setCypherExample = setCypherExample;
window.setSparqlExample = setSparqlExample;
window.setSqlExample = setSqlExample;
window.addSampleTriples = addSampleTriples;
window.clearOutput = clearOutput;
// Persistence functions
window.saveToIndexedDB = saveToIndexedDB;
window.loadFromIndexedDB = loadFromIndexedDB;
window.exportJson = exportJson;
window.importJson = importJson;
window.clearStorage = clearStorage;
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const className = type === 'error' ? 'output-error' :
type === 'success' ? 'output-success' : 'output-info';
output.innerHTML += `<div class="output-line ${className}">[${timestamp}] ${escapeHtml(message)}</div>`;
output.scrollTop = output.scrollHeight;
console.log(message);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function setStatus(message, className) {
statusDiv.className = `status ${className}`;
statusDiv.innerHTML = `<strong>Status:</strong> ${message}`;
}
function updateStats() {
if (!db) return;
try {
const vectorCount = db.len();
document.getElementById('vectorCount').textContent = vectorCount;
const cypherStats = db.cypher_stats();
document.getElementById('nodeCount').textContent = cypherStats?.node_count || 0;
document.getElementById('edgeCount').textContent = cypherStats?.edge_count || 0;
const tripleCount = db.triple_count();
document.getElementById('tripleCount').textContent = tripleCount;
} catch (e) {
console.error('Error updating stats:', e);
}
}
async function loadWasm() {
try {
setStatus('Loading WASM module...', 'info');
await init();
// Create database with 8 dimensions for demo
const config = new RvLiteConfig(8);
db = new RvLite(config);
setStatus('RvLite ready! Database initialized with 8-dimensional vectors', 'success');
log('RvLite WASM module loaded successfully!', 'success');
log(`Version: ${db.get_version()}`);
log(`Features: ${JSON.stringify(db.get_features())}`);
updateStats();
updatePersistStatus();
} catch (error) {
setStatus('Failed to load WASM module', 'error');
log(`Error: ${error.message || error}`, 'error');
console.error(error);
}
}
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.dataset.tab).classList.add('active');
});
});
// Vector operations
function insertVector() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const vector = JSON.parse(document.getElementById('vectorInput').value);
const id = db.insert(vector, null);
log(`Inserted vector with ID: ${id}`, 'success');
updateStats();
} catch (e) {
log(`Error: ${e.message || e}`, 'error');
}
}
function searchVector() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const vector = JSON.parse(document.getElementById('vectorInput').value);
const results = db.search(vector, 5);
log(`Search results: ${JSON.stringify(results, null, 2)}`, 'success');
} catch (e) {
log(`Error: ${e.message || e}`, 'error');
}
}
function insertRandomVectors() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
for (let i = 0; i < 10; i++) {
const vector = Array.from({length: 8}, () => Math.random());
const metadata = { index: i, label: `vector_${i}` };
db.insert(vector, metadata);
}
log('Inserted 10 random vectors', 'success');
updateStats();
} catch (e) {
log(`Error: ${e.message || e}`, 'error');
}
}
function searchSimilar() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const query = Array.from({length: 8}, () => Math.random());
const results = db.search(query, 3);
log(`Similar vectors: ${JSON.stringify(results, null, 2)}`, 'success');
} catch (e) {
log(`Error: ${e.message || e}`, 'error');
}
}
function getVectorById() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const id = prompt('Enter vector ID:');
if (id) {
const result = db.get(id);
log(`Vector: ${JSON.stringify(result, null, 2)}`, 'success');
}
} catch (e) {
log(`Error: ${e.message || e}`, 'error');
}
}
// Cypher operations
function executeCypher() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const query = document.getElementById('cypherInput').value;
log(`Executing Cypher: ${query}`);
const result = db.cypher(query);
log(`Result: ${JSON.stringify(result, null, 2)}`, 'success');
updateStats();
} catch (e) {
log(`Cypher error: ${e.message || e}`, 'error');
}
}
function clearGraph() {
if (!db) { log('Database not initialized', 'error'); return; }
db.cypher_clear();
log('Graph cleared', 'success');
updateStats();
}
function setCypherExample(type) {
const examples = {
create: "CREATE (alice:Person {name: 'Alice', age: 30})",
relationship: "CREATE (a:Person {name: 'Alice'})-[r:KNOWS {since: 2020}]->(b:Person {name: 'Bob'})",
match: "MATCH (p:Person) RETURN p",
path: "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a, r, b"
};
document.getElementById('cypherInput').value = examples[type] || '';
}
// SPARQL operations
function executeSparql() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const query = document.getElementById('sparqlInput').value;
log(`Executing SPARQL: ${query}`);
const result = db.sparql(query);
log(`Result: ${JSON.stringify(result, null, 2)}`, 'success');
} catch (e) {
log(`SPARQL error: ${e.message || e}`, 'error');
}
}
function clearTriples() {
if (!db) { log('Database not initialized', 'error'); return; }
db.clear_triples();
log('Triples cleared', 'success');
updateStats();
}
function addSampleTriples() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
db.add_triple('<http://example.org/alice>', '<http://example.org/name>', '"Alice"');
db.add_triple('<http://example.org/alice>', '<http://example.org/age>', '"30"');
db.add_triple('<http://example.org/bob>', '<http://example.org/name>', '"Bob"');
db.add_triple('<http://example.org/bob>', '<http://example.org/age>', '"25"');
db.add_triple('<http://example.org/alice>', '<http://example.org/knows>', '<http://example.org/bob>');
log('Added 5 sample triples', 'success');
updateStats();
} catch (e) {
log(`Error: ${e.message || e}`, 'error');
}
}
function setSparqlExample(type) {
const examples = {
select: "SELECT ?name WHERE { ?person <http://example.org/name> ?name }",
filter: "SELECT ?person ?age WHERE { ?person <http://example.org/age> ?age }"
};
document.getElementById('sparqlInput').value = examples[type] || '';
}
// SQL operations
function executeSql() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const query = document.getElementById('sqlInput').value;
log(`Executing SQL: ${query}`);
const result = db.sql(query);
log(`Result: ${JSON.stringify(result, null, 2)}`, 'success');
} catch (e) {
log(`SQL error: ${e.message || e}`, 'error');
}
}
function setSqlExample(type) {
const examples = {
insert: "INSERT INTO vectors (id, vector) VALUES ('vec1', '[0.1, 0.2, 0.3]')",
select: "SELECT * FROM vectors",
search: "SELECT id, vector <-> '[0.1, 0.2, 0.3]' AS distance FROM vectors ORDER BY distance LIMIT 5"
};
document.getElementById('sqlInput').value = examples[type] || '';
}
function clearOutput() {
output.innerHTML = '';
}
// ========== Persistence Functions ==========
async function saveToIndexedDB() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
log('Saving to IndexedDB...');
await db.save();
log('Database saved to IndexedDB!', 'success');
updatePersistStatus();
} catch (e) {
log(`Save error: ${e.message || e}`, 'error');
}
}
async function loadFromIndexedDB() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
log('Loading from IndexedDB...');
const hasData = await RvLite.has_saved_state();
if (!hasData) {
log('No saved state found in IndexedDB', 'info');
return;
}
// Export current state for re-import after creating new instance
const jsonState = db.export_json();
// Create fresh config
const config = new RvLiteConfig(8);
const result = await RvLite.load(config);
if (result === null) {
log('No saved state found', 'info');
return;
}
// Import the loaded state
db.import_json(jsonState);
log('Database loaded from IndexedDB!', 'success');
updateStats();
updatePersistStatus();
} catch (e) {
log(`Load error: ${e.message || e}`, 'error');
}
}
function exportJson() {
if (!db) { log('Database not initialized', 'error'); return; }
try {
const state = db.export_json();
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `rvlite-backup-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
log('Database exported as JSON file', 'success');
} catch (e) {
log(`Export error: ${e.message || e}`, 'error');
}
}
function importJson() {
document.getElementById('jsonFileInput').click();
}
// File input handler
document.getElementById('jsonFileInput').addEventListener('change', async (e) => {
if (!db) { log('Database not initialized', 'error'); return; }
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const json = JSON.parse(text);
db.import_json(json);
log(`Database imported from ${file.name}`, 'success');
updateStats();
} catch (e) {
log(`Import error: ${e.message || e}`, 'error');
}
// Reset file input
e.target.value = '';
});
async function clearStorage() {
try {
if (!confirm('Are you sure you want to clear all saved data from IndexedDB?')) {
return;
}
await RvLite.clear_storage();
log('IndexedDB storage cleared', 'success');
updatePersistStatus();
} catch (e) {
log(`Clear error: ${e.message || e}`, 'error');
}
}
async function updatePersistStatus() {
const statusEl = document.getElementById('persistStatus');
try {
const available = RvLite.is_storage_available();
const hasSaved = await RvLite.has_saved_state();
if (available) {
statusEl.textContent = hasSaved ? '(Saved state exists)' : '(No saved state)';
statusEl.style.color = hasSaved ? '#4ecca3' : '#aaa';
} else {
statusEl.textContent = '(Not available)';
statusEl.style.color = '#e74c3c';
}
} catch (e) {
statusEl.textContent = '(Error checking)';
statusEl.style.color = '#e74c3c';
}
}
// Load WASM on page load
loadWasm();
</script>
</body>
</html>