752 lines
28 KiB
HTML
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>
|