brainy/examples/demo.html
David Snelling 9051d566f8 **feat(demo): enhance UI/UX & content of interactive demo**
### Changes:
- **Favicon Update**: Replaced the relative favicon path with an absolute URL pointing to the GitHub-hosted file.
- **Header Redesign**: Improved header layout with a more modern and aligned flexbox structure. Adjusted logo size and added a left-aligned text section.
- **Styling Enhancements**:
  - Introduced new styles for zoom controls, Google-like search results, and result clusters.
  - Updated padding, alignment, and hover states for buttons and interactive elements.
  - Refined visual hierarchy in sections with updated margins, sizes, and typography.
- **Added `What Can You Build?` Section**: Highlighted potential use cases for Brainy (e.g., semantic search, recommendation systems, adaptive experiences).
- **Improved Demo Steps**:
  - Refactored interactive demo into guided "steps" for better usability.
  - Added tooltips to clarify button functionality.
  - Updated step titles and descriptions for clarity and conciseness.
- **Code Samples**:
  - Simplified JavaScript import instructions for Node.js and browser environments.
  - Highlighted CDN options for easier library access.
- **Advanced Features & New Controls**:
  - Introduced new backup, restore, and additional operation buttons for database management.
  - Added zoom-in, zoom-out, and zoom-to-fit controls for visualizations.
  - Enhanced semantic search with dynamic filtering and guidance for queries.

### Purpose:
Updated the interactive demo to improve its usability, visual appeal, and feature coverage. These enhancements make the demo more user-friendly and better illustrate Brainy's capabilities, aiding developers in exploring and understanding the platform.
2025-06-20 13:49:59 -07:00

3903 lines
124 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Brainy Interactive Demo - A Lightweight Graph & Vector Data Platform</title>
<link rel="icon" href="https://raw.githubusercontent.com/soulcraft/brainy/main/brainy.png" type="image/png">
<!--
IMPORTANT: This demo must be served over HTTP, not opened directly as a file.
Run the following command from the project root directory:
npx http-server
Then navigate to http://localhost:8080/examples/demo.html in your browser.
-->
<!-- D3.js for visualizations -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Material Icons font -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
:root {
--primary-color: #193c41;
--secondary-color: #3f6e65;
--accent-color: #c95e2f;
--danger-color: #b84c26;
--text-color: #333;
--light-bg: #ececec;
--dark-bg: #333;
--card-bg: #fff;
--border-radius: 8px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
/* Cartographer colors */
--md3-primary: #6750A4;
--md3-on-primary: #FFFFFF;
--md3-secondary-container: #E8DEF8;
--md3-on-secondary-container: #1D192B;
--md3-outline: #79747E;
--md3-surface: #FFFBFE;
--md3-shadow: rgba(0, 0, 0, 0.3);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-color);
margin: 0;
padding: 0;
background-color: var(--light-bg);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
display: flex;
align-items: center;
padding: 30px;
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
margin-bottom: 30px;
}
header img {
width: 120px;
margin-right: 20px;
}
.header-text {
text-align: left;
}
h1, h2, h3 {
color: var(--primary-color);
}
.demo-section {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 30px;
margin-bottom: 30px;
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
}
button:hover {
background-color: #388E3C;
transform: translateY(-2px);
}
button.secondary {
background-color: var(--secondary-color);
}
button.secondary:hover {
background-color: #1976D2;
}
button.accent {
background-color: var(--accent-color);
}
button.accent:hover {
background-color: #F57C00;
}
button.danger {
background-color: var(--danger-color);
}
button.danger:hover {
background-color: #D32F2F;
}
input, select, textarea {
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
flex-grow: 1;
}
textarea {
min-height: 100px;
font-family: monospace;
}
.visualization {
height: 500px;
background-color: var(--light-bg);
border-radius: var(--border-radius);
margin: 20px 0;
position: relative;
overflow: hidden;
}
/* Cartographer styles */
.zoom-controls {
position: absolute;
bottom: 1.5rem;
right: 1rem;
z-index: 10;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.zoom-fit-button {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background-color: var(--md3-primary);
color: var(--md3-on-primary);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0.125rem 0.25rem var(--md3-shadow);
transition: background-color 0.2s, transform 0.2s;
}
.zoom-fit-button:hover {
background-color: #7B68B5;
transform: scale(1.05);
}
.zoom-fit-button:active {
background-color: #553B93;
transform: scale(0.95);
}
.zoom-button {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background-color: var(--md3-primary);
color: var(--md3-on-primary);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0.125rem 0.25rem var(--md3-shadow);
transition: background-color 0.2s, transform 0.2s;
}
.zoom-button:hover {
background-color: #7B68B5;
transform: scale(1.05);
}
.zoom-button:active {
background-color: #553B93;
transform: scale(0.95);
}
.graph-container {
width: 100%;
height: 100%;
background-color: #FDF8FF;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
overflow: hidden;
}
.graph-container svg {
display: block;
width: 100%;
height: 100%;
font-family: 'Roboto', 'Inter', sans-serif;
}
.graph-container .edges line {
transition: stroke-opacity 0.2s ease-in-out;
}
.graph-container .node-group {
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), filter 0.3s ease;
transform-origin: center;
}
.graph-container .node-group:hover {
filter: drop-shadow(0 0.5rem 1rem rgba(0, 0, 0, 0.25));
animation: jiggle 0.5s ease;
}
.graph-container .node-group path {
transition: fill-opacity 0.15s ease-in-out;
}
.graph-container .overlay-container {
pointer-events: none;
}
.graph-container .overlay-container .node-info-popup {
pointer-events: auto;
}
@keyframes jiggle {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(0.5deg) scale(1.01);
}
50% {
transform: rotate(-0.5deg) scale(1.01);
}
75% {
transform: rotate(0.5deg) scale(1.01);
}
100% {
transform: rotate(0deg);
}
}
.results {
background-color: var(--light-bg);
border-radius: var(--border-radius);
padding: 15px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
}
/* Google-like search results styles */
.search-stats {
color: #70757a;
font-size: 14px;
margin-bottom: 10px;
font-family: Arial, sans-serif;
}
.search-results-list {
font-family: Arial, sans-serif;
white-space: normal;
}
.search-result-item {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #dfe1e5;
}
.result-title {
color: #1a0dab;
font-size: 18px;
margin: 0 0 5px 0;
font-weight: normal;
cursor: pointer;
}
.result-title:hover {
text-decoration: underline;
}
.result-score {
color: #70757a;
font-size: 14px;
font-weight: normal;
margin-left: 10px;
}
.result-url {
color: #006621;
font-size: 14px;
margin-bottom: 5px;
}
.result-snippet {
color: #4d5156;
font-size: 14px;
line-height: 1.58;
margin-bottom: 5px;
}
.result-metadata {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 5px;
}
.metadata-item {
background-color: #f1f3f4;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
color: #4d5156;
}
/* Cluster-specific styles */
.cluster-header {
margin: 25px 0 15px 0;
padding-bottom: 8px;
border-bottom: 2px solid #dadce0;
}
.cluster-header h3 {
color: #202124;
font-size: 20px;
font-weight: 500;
margin: 0;
}
.cluster-item {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #f1f3f4;
margin-left: 15px;
}
.tabs {
display: flex;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab {
padding: 10px 20px;
background-color: var(--light-bg);
cursor: pointer;
border-radius: var(--border-radius) var(--border-radius) 0 0;
margin-right: 5px;
margin-bottom: 5px;
}
.tab.active {
background-color: var(--primary-color);
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.section-content, .subsection-content {
display: block;
margin-bottom: 30px;
}
.quickstart-section {
background: linear-gradient(135deg, #193c41, #3f6e65);
color: white;
padding: 30px;
border-radius: var(--border-radius);
margin-bottom: 30px;
box-shadow: var(--box-shadow);
}
.quickstart-section h2 {
color: white;
margin-top: 0;
}
.code-tabs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-top: 20px;
}
.code-tab {
background-color: rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
padding: 20px;
}
.code-tab h3 {
margin-top: 0;
color: white;
}
.code-tab p {
color: rgba(255, 255, 255, 0.9);
}
.section-divider {
height: 1px;
background-color: var(--light-bg);
margin: 20px 0;
}
.code-block {
background-color: var(--dark-bg);
color: white;
padding: 15px;
border-radius: var(--border-radius);
overflow-x: auto;
margin: 20px 0;
font-family: monospace;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin: 30px 0;
}
.feature-card {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 20px;
transition: var(--transition);
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.feature-card h3 {
margin-top: 0;
color: var(--primary-color);
}
.tooltip {
position: absolute;
padding: 8px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 4px;
pointer-events: none;
z-index: 10;
font-size: 12px;
}
.cli-terminal {
background-color: var(--dark-bg);
color: #fff;
padding: 15px;
border-radius: var(--border-radius);
font-family: monospace;
margin: 20px 0;
position: relative;
}
.cli-prompt {
color: var(--accent-color);
}
.cli-input {
background: transparent;
border: none;
color: #fff;
font-family: monospace;
width: 80%;
outline: none;
}
.cli-output {
margin-top: 10px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
.cli-history {
margin-bottom: 10px;
}
.cli-command {
margin-bottom: 5px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.features-overview, .what-can-you-build-section {
background: linear-gradient(135deg, #193c41, #3f6e65);
color: white;
padding: 30px;
border-radius: var(--border-radius);
margin-bottom: 30px;
box-shadow: var(--box-shadow);
}
.features-overview h2, .what-can-you-build-section h2 {
color: white;
margin-top: 0;
}
.features-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 20px;
}
.feature-item {
background-color: var(--card-bg); /* Changed from transparent white to card background */
padding: 15px;
border-radius: var(--border-radius);
/* backdrop-filter: blur(10px); Removed as it's less relevant with solid background */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* Added a light shadow to feature items */
}
.feature-item h4 {
margin: 0 0 10px 0;
color: var(--secondary-color); /* Changed from white to secondary color for better contrast */
}
.feature-item p {
margin: 0;
font-size: 0.9em;
opacity: 1; /* Changed from 0.9 to 1 for better readability */
color: var(--text-color); /* Ensure paragraph text is also dark */
}
footer {
text-align: center;
padding: 20px;
margin-top: 40px;
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.demo-controls {
flex-direction: column;
}
.feature-grid {
grid-template-columns: 1fr;
}
.features-list {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div id="http-warning"
style="background-color: #f44336; color: white; padding: 15px; margin-bottom: 20px; border-radius: 8px; display: none;">
<strong>Warning:</strong> This demo must be served over HTTP to work properly. If you're seeing this message or
the demo isn't working, please:
<ol style="margin-top: 10px; margin-bottom: 5px;">
<li>Open a terminal in the project root directory</li>
<li>Run: <code style="background-color: rgba(0,0,0,0.2); padding: 3px 5px; border-radius: 3px;">npx
http-server</code></li>
<li>Navigate to <a href="http://localhost:8080/examples/demo.html"
style="color: white; text-decoration: underline;">http://localhost:8080/examples/demo.html</a>
</li>
</ol>
<button onclick="this.parentElement.style.display='none'"
style="background-color: white; color: #f44336; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; float: right;">
Dismiss
</button>
</div>
<header>
<img src="https://raw.githubusercontent.com/soulcraft/brainy/main/brainy.png" alt="Brainy Logo">
<div class="header-text">
<h1>Brainy Interactive Demo</h1>
<p>A lightweight but powerful graph & vector data platform for AI applications across any environment</p>
</div>
</header>
<!-- Features Overview Section -->
<section class="features-overview">
<h2>🚀 Key Features</h2>
<p>Explore all of Brainy's powerful capabilities in this interactive demo:</p>
<div class="features-list">
<div class="feature-item">
<h4>🏃‍♂️ Run Everywhere</h4>
<p>Run Brainy in a browser, container, serverless cloud service or the terminal!</p>
</div>
<div class="feature-item">
<h4>🔍 Vector Search</h4>
<p>Find semantically similar content using embeddings (like having ESP for your data!)</p>
</div>
<div class="feature-item">
<h4>🌐 Graph Database</h4>
<p>Connect data with meaningful relationships (your data's social network)</p>
</div>
<div class="feature-item">
<h4>🌊 Streaming Pipeline</h4>
<p>Process data in real-time as it flows through the system (like a data waterslide!)</p>
</div>
<div class="feature-item">
<h4>🧩 Extensible Augmentations</h4>
<p>Customize and extend functionality with pluggable components (LEGO blocks for your data!)</p>
</div>
<div class="feature-item">
<h4>🔌 Built-in Conduits</h4>
<p>Sync and scale across instances with WebSocket and WebRTC (your data's teleportation system!)</p>
</div>
<div class="feature-item">
<h4>🤖 TensorFlow Integration</h4>
<p>Use TensorFlow.js for high-quality embeddings (included as a required dependency)</p>
</div>
<div class="feature-item">
<h4>🧠 Adaptive Intelligence</h4>
<p>Automatically optimizes for your environment and usage patterns</p>
</div>
<div class="feature-item">
<h4>🌍 Cross-Platform</h4>
<p>Works everywhere you do: browsers, Node.js, and server environments</p>
</div>
<div class="feature-item">
<h4>💾 Persistent Storage</h4>
<p>Data persists across sessions and scales to any size (no memory loss here!)</p>
</div>
<div class="feature-item">
<h4>🔧 TypeScript Support</h4>
<p>Fully typed API with generics (for those who like their code tidy)</p>
</div>
<div class="feature-item">
<h4>📱 CLI Tools</h4>
<p>Powerful command-line interface for data management (command line wizardry)</p>
</div>
<div class="feature-item">
<h4>📊 Data Visualization</h4>
<p>Interactive graph and vector space visualizations</p>
</div>
<div class="feature-item">
<h4>☁️ Cloud Ready</h4>
<p>Easy deployment to Google, AWS, Azure, and other cloud platforms</p>
</div>
<div class="feature-item">
<h4>🎮 Model Control Protocol (MCP)</h4>
<p>Allow external AI models to access Brainy data and use augmentation pipeline as tools</p>
</div>
</div>
</section>
<!-- What Can You Build Section -->
<section class="what-can-you-build-section">
<h2>📊 What Can You Build?</h2>
<div class="features-list">
<div class="feature-item">
<h4>🔍 Semantic Search Engines</h4>
<p>Find content based on meaning, not just keywords</p>
</div>
<div class="feature-item">
<h4>👍 Recommendation Systems</h4>
<p>Suggest similar items based on vector similarity</p>
</div>
<div class="feature-item">
<h4>🧠 Knowledge Graphs</h4>
<p>Build connected data structures with relationships</p>
</div>
<div class="feature-item">
<h4>🤖 AI Applications</h4>
<p>Store and retrieve embeddings for machine learning models</p>
</div>
<div class="feature-item">
<h4>💡 AI-Enhanced Applications</h4>
<p>Build applications that leverage vector embeddings for intelligent data processing</p>
</div>
<div class="feature-item">
<h4>📁 Data Organization Tools</h4>
<p>Automatically categorize and connect related information</p>
</div>
<div class="feature-item">
<h4>🔄 Adaptive Experiences</h4>
<p>Create applications that learn and evolve with your users</p>
</div>
<div class="feature-item">
<h4>🔌 Model-Integrated Systems</h4>
<p>Connect external AI models to Brainy data and tools using MCP</p>
</div>
</div>
</section>
<!-- Quick Start Section -->
<section class="quickstart-section">
<h2>🏁 Quick Start</h2>
<div class="code-tabs">
<div class="code-tab">
<h3>Node.js Environment</h3>
<pre class="code-block">
import { BrainyData, NounType, VerbType } from '@soulcraft/brainy'
// Create and initialize the database
const db = new BrainyData()
await db.init()
// Add data (automatically converted to vectors)
const catId = await db.add("Cats are independent pets", {
noun: NounType.Thing,
category: 'animal'
})
const dogId = await db.add("Dogs are loyal companions", {
noun: NounType.Thing,
category: 'animal'
})
// Search for similar items
const results = await db.searchText("feline pets", 2)
console.log(results)
// Returns items similar to "feline pets" with similarity scores
// Add a relationship between items
await db.addVerb(catId, dogId, {
verb: VerbType.RelatedTo,
description: 'Both are common household pets'
})
</pre>
</div>
<div class="code-tab">
<h3>Browser Environment</h3>
<p>Brainy uses a unified build that automatically adapts to your environment:</p>
<h4>Option 1: Standard Import</h4>
<pre class="code-block">
// Standard import - automatically adapts to any environment
import { BrainyData, NounType, VerbType } from '@soulcraft/brainy'
// Use the same API as in Node.js
const db = new BrainyData()
await db.init()
// ...
</pre>
<h4>Option 2: Using a script tag in HTML</h4>
<pre class="code-block">
&lt;script type="module"&gt;
// Use the CDN to load the unified build
import { BrainyData, NounType, VerbType } from 'https://cdn.jsdelivr.net/npm/@soulcraft/brainy/dist/unified.js'
// Or minified version
// import { BrainyData, NounType, VerbType } from 'https://cdn.jsdelivr.net/npm/@soulcraft/brainy/dist/unified.min.js'
const db = new BrainyData()
await db.init()
// ...
&lt;/script&gt;
</pre>
<p>Modern bundlers like Webpack, Rollup, and Vite will automatically use the unified build which adapts to any
environment.</p>
</div>
</div>
</section>
<section class="demo-section">
<h2>Interactive Brainy Explorer</h2>
<p>This interactive demo guides you through Brainy's features step by step, from basic operations to advanced
capabilities.</p>
<!-- Step 1: Getting Started -->
<div class="section-content" id="step1-content">
<h3>Step 1: Getting Started with Brainy</h3>
<p>Let's start by initializing the database and exploring basic operations.</p>
<div class="demo-controls">
<button id="init-db-btn">Initialize Database</button>
<button id="add-sample-btn" class="secondary">Add Sample Data</button>
<button id="backup-db-btn" class="accent">Backup Data</button>
<button id="restore-db-btn" class="accent">Restore Data</button>
<button id="clear-db-btn" class="danger">Clear Database</button>
</div>
<div class="results" id="db-status">
Database not initialized. Click "Initialize Database" to start.
</div>
<div class="visualization" id="db-visualization">
<div class="zoom-controls">
<button class="zoom-fit-button" id="zoom-fit-btn" title="Zoom to fit all nodes">
<i class="material-icons">fit_screen</i>
</button>
</div>
<div class="graph-container">
<div style="text-align: center; padding-top: 200px; color: #666;">
Database visualization will appear here after initialization
</div>
</div>
</div>
</div>
<!-- Step 2: Adding Data -->
<div class="section-content" id="step2-content">
<h3>Step 2: Adding Entities (Nouns)</h3>
<p>Now that your database is initialized, let's add some entities (nouns) to it.</p>
<div class="form-group">
<label for="noun-type">Noun Type:</label>
<select id="noun-type">
<option value="person">Person</option>
<option value="place">Place</option>
<option value="thing">Thing</option>
<option value="event">Event</option>
<option value="concept">Concept</option>
<option value="content">Content</option>
<option value="group">Group</option>
<option value="list">List</option>
<option value="category">Category</option>
</select>
</div>
<div class="form-group">
<label for="noun-id">ID (optional):</label>
<input type="text" id="noun-id" placeholder="Custom ID for the entity (auto-generated if empty)">
</div>
<div class="form-group">
<label for="noun-name">Name:</label>
<input type="text" id="noun-name" placeholder="Name of the entity">
</div>
<div class="form-group">
<label for="noun-content">Description/Content:</label>
<textarea id="noun-content" placeholder="Description or content of the entity"></textarea>
</div>
<div class="form-group">
<label for="noun-tags">Tags (comma-separated):</label>
<input type="text" id="noun-tags" placeholder="tag1, tag2, tag3">
</div>
<div class="demo-controls">
<button id="add-noun-btn">Add Noun</button>
<button id="query-nouns-btn" class="accent">View All Nouns</button>
</div>
<div class="results" id="entity-results">
Entity operations will appear here...
</div>
</div>
<!-- Step 3: Creating Relationships -->
<div class="section-content" id="step3-content">
<h3>Step 3: Creating Relationships (Verbs)</h3>
<p>Connect your entities with meaningful relationships to build a knowledge graph.</p>
<div class="form-group">
<label for="source-id">Source Entity:</label>
<select id="source-id">
<option value="">-- Select Source Entity --</option>
</select>
<button id="refresh-nouns-btn" class="secondary" style="margin-top: 5px; padding: 5px 10px;">Refresh Entity
List
</button>
</div>
<div class="form-group">
<label for="target-id">Target Entity:</label>
<select id="target-id">
<option value="">-- Select Target Entity --</option>
</select>
</div>
<div class="form-group">
<label for="verb-type">Relationship Type:</label>
<select id="verb-type">
<option value="relatedTo">Related To</option>
<option value="attributedTo">Attributed To</option>
<option value="controls">Controls</option>
<option value="created">Created</option>
<option value="owns">Owns</option>
<option value="memberOf">Member Of</option>
<option value="worksWith">Works With</option>
<option value="friendOf">Friend Of</option>
</select>
</div>
<div class="form-group">
<label for="verb-description">Relationship Description:</label>
<textarea id="verb-description" placeholder="Describe the relationship between the entities"></textarea>
</div>
<div class="demo-controls">
<button id="add-verb-btn">Add Relationship</button>
<button id="query-verbs-btn" class="accent">View All Relationships</button>
</div>
</div>
<!-- Step 4: Searching -->
<div class="section-content" id="step4-content">
<h3>Step 4: Semantic Search</h3>
<p>Search your data using vector embeddings for semantic understanding.</p>
<div class="form-group">
<label for="search-query">Search Query:</label>
<input type="text" id="search-query" placeholder="Try: travel, AI, fitness, food, design...">
</div>
<p class="search-help" style="font-size: 0.9em; color: #666; margin-top: -10px; margin-bottom: 15px;">
Try searching for topics in the sample data like "travel", "AI", "fitness", "food", "design",
"sustainability", or specific concepts like "workout challenge", "social media", "photography", etc.
</p>
<div class="form-group">
<label for="search-type">Search Type:</label>
<select id="search-type">
<option value="semantic">Semantic Search</option>
<option value="keyword">Keyword Search</option>
<option value="hybrid">Hybrid Search</option>
</select>
</div>
<div class="demo-controls">
<button id="search-btn">Search</button>
<button id="similar-entities-btn" class="secondary">Find Similar Entities</button>
</div>
<div class="results" id="search-results">
Search results will appear here...
</div>
<div class="visualization" id="search-visualization">
<div class="zoom-controls">
<button class="zoom-fit-button" id="search-zoom-fit-btn" title="Zoom to fit all nodes">
<i class="material-icons">fit_screen</i>
</button>
<button class="zoom-button" id="search-zoom-in-btn" title="Zoom in">
<i class="material-icons">add</i>
</button>
<button class="zoom-button" id="search-zoom-out-btn" title="Zoom out">
<i class="material-icons">remove</i>
</button>
</div>
<div class="graph-container">
<div style="text-align: center; padding-top: 200px; color: #666;">
Search results visualization
</div>
</div>
</div>
</div>
<!-- Step 6: Advanced Features -->
<div class="section-content" id="step6-content">
<h3>Step 6: Advanced Features</h3>
<p>Explore Brainy's advanced capabilities for more complex use cases.</p>
<div class="feature-grid">
<div class="feature-card">
<h3>🔍 Vector Search</h3>
<p>Explore how HNSW makes semantic search fast and efficient.</p>
<button class="demo-feature-btn" data-feature="hnsw">Try Vector Search</button>
</div>
<div class="feature-card">
<h3>🌐 Graph Traversal</h3>
<p>Navigate through connected entities in your knowledge graph.</p>
<button class="demo-feature-btn" data-feature="graph-traversal">Try Graph Traversal</button>
</div>
<div class="feature-card">
<h3>💾 Data Management</h3>
<p>Backup, restore, and manage your data.</p>
<div class="demo-controls" style="margin-top: 10px;">
<button id="export-data-btn">Backup Data</button>
<button id="import-data-btn">Restore Data</button>
</div>
</div>
<div class="feature-card">
<h3>🧠 Embedding Pipeline</h3>
<p>See how text is converted to vectors for semantic understanding.</p>
<button class="demo-feature-btn" data-feature="embedding">Try Embedding Demo</button>
</div>
</div>
<div class="results" id="feature-demo-results">
Select a feature above to see a detailed demonstration...
</div>
</div>
<!-- Step 7: CLI Interface -->
<div class="section-content" id="step7-content">
<h3>Step 7: Command Line Interface</h3>
<p>Try Brainy's CLI commands for scripting and automation.</p>
<div class="cli-terminal">
<div class="cli-history" id="cli-history"></div>
<div class="cli-command">
<span class="cli-prompt">brainy$</span>
<input type="text" class="cli-input" id="cli-input" placeholder="Type a command...">
</div>
<div class="cli-output" id="cli-output"></div>
</div>
<div class="demo-controls">
<button id="help-cmd-btn">Help</button>
<button id="status-cmd-btn" class="secondary">Status</button>
<button id="init-cmd-btn" class="accent">Init</button>
<button id="query-cmd-btn" class="accent">Query</button>
<button id="clear-terminal-btn" class="danger">Clear</button>
</div>
<div class="code-block">
<strong>Common Commands:</strong><br>
brainy init - Initialize a new database<br>
brainy add &lt;data&gt; - Add data to the database<br>
brainy search &lt;query&gt; - Search for entities<br>
brainy relate &lt;from&gt; &lt;to&gt; - Create relationships<br>
brainy status - Show database status<br>
brainy help - Show all commands
</div>
</div>
</section>
<footer>
<p>&copy; 2024 Brainy - A lightweight graph & vector data platform</p>
<p>
<a href="https://github.com/soulcraft/brainy" target="_blank">GitHub</a> |
<a href="https://github.com/soulcraft/brainy/blob/main/README.md" target="_blank">Documentation</a> |
<a href="https://npm.im/@soulcraft/brainy" target="_blank">NPM</a>
</p>
</footer>
</div>
<script type="module">
// Import Brainy library directly as a module
import { BrainyData, NounType, VerbType } from 'https://cdn.jsdelivr.net/npm/@soulcraft/brainy/dist/unified.js'
// Make BrainyData and types available globally
window.BrainyData = BrainyData
window.NounType = NounType
window.VerbType = VerbType
// Initialize demo when DOM is loaded
document.addEventListener('DOMContentLoaded', async function() {
// Check if we're running over HTTP
if (window.location.protocol === 'file:') {
document.getElementById('http-warning').style.display = 'block'
}
try {
// Verify the imported module has the required exports
if (BrainyData) {
initializeDemo()
} else {
showError('Brainy library not found. Please build the project first by running: npm run build')
}
} catch (error) {
showError('Failed to initialize demo: ' + error.message)
}
})
// Tab switching functionality removed - all sections are now visible
// Initialize the demo
function initializeDemo() {
log('db-status', 'Brainy library loaded successfully!')
// Initialize event listeners
setupEventListeners()
// Initialize CLI
setupCLI()
// Initialize visualizations
setupVisualizations()
// Try to populate noun dropdowns with any existing data
setTimeout(() => {
try {
if (database) {
populateNounDropdowns()
}
} catch (error) {
console.log('No database initialized yet, noun dropdowns will be populated later')
}
}, 1000)
}
// Setup all event listeners
function setupEventListeners() {
// Step 1: Getting Started - Database operations
document.getElementById('init-db-btn').addEventListener('click', initializeDatabase)
document.getElementById('add-sample-btn').addEventListener('click', addSampleData)
document.getElementById('backup-db-btn').addEventListener('click', backupData)
document.getElementById('restore-db-btn').addEventListener('click', restoreData)
document.getElementById('clear-db-btn').addEventListener('click', clearDatabase)
// Step 2: Adding Entities - Noun operations
document.getElementById('add-noun-btn')?.addEventListener('click', addNoun)
document.getElementById('query-nouns-btn')?.addEventListener('click', queryNouns)
// Step 3: Creating Relationships - Verb operations
document.getElementById('add-verb-btn')?.addEventListener('click', addVerb)
document.getElementById('query-verbs-btn')?.addEventListener('click', queryVerbs)
document.getElementById('refresh-nouns-btn')?.addEventListener('click', populateNounDropdowns)
// Step 4: Searching - Search operations
document.getElementById('search-btn')?.addEventListener('click', performSearch)
document.getElementById('similar-entities-btn')?.addEventListener('click', findSimilarEntities)
// Step 6: Advanced Features - Data management
document.getElementById('export-data-btn')?.addEventListener('click', backupData)
document.getElementById('import-data-btn')?.addEventListener('click', restoreData)
// Step 7: CLI Interface - CLI commands
document.getElementById('help-cmd-btn')?.addEventListener('click', () => executeCLICommand('help'))
document.getElementById('status-cmd-btn')?.addEventListener('click', () => executeCLICommand('status'))
document.getElementById('init-cmd-btn')?.addEventListener('click', () => executeCLICommand('init'))
document.getElementById('query-cmd-btn')?.addEventListener('click', () => executeCLICommand('query sample'))
document.getElementById('clear-terminal-btn')?.addEventListener('click', clearTerminal)
// Feature demonstrations
document.querySelectorAll('.demo-feature-btn').forEach(btn => {
btn.addEventListener('click', function() {
const feature = this.dataset.feature
demonstrateFeature(feature)
})
})
// Always populate noun dropdowns for the verbs section
populateNounDropdowns()
}
// Setup CLI functionality
function setupCLI() {
const cliInput = document.getElementById('cli-input')
cliInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const command = this.value.trim()
if (command) {
executeCLICommand(command)
this.value = ''
}
}
})
}
// Setup visualizations
function setupVisualizations() {
// Initialize database visualization container
const dbVisualizationContainer = document.getElementById('db-visualization')
if (dbVisualizationContainer) {
const graphContainer = dbVisualizationContainer.querySelector('.graph-container')
if (graphContainer) {
graphContainer.innerHTML = '<div style="text-align: center; padding-top: 200px; color: #666;">Database visualization will appear here after initialization</div>'
}
}
// Hide search visualization container as per requirements
const searchVisualizationContainer = document.getElementById('search-visualization')
if (searchVisualizationContainer) {
searchVisualizationContainer.style.display = 'none'
}
// Setup zoom controls
const zoomFitButton = document.getElementById('zoom-fit-btn')
if (zoomFitButton) {
zoomFitButton.addEventListener('click', function() {
// This will be implemented when we have an active visualization
if (typeof updateDatabaseVisualization === 'function') {
updateDatabaseVisualization()
}
})
}
}
// Database operations
let database = null
async function initializeDatabase() {
try {
log('db-status', 'Initializing database...')
// Initialize Brainy database
database = new BrainyData({
storage: { type: 'opfs' },
augmentations: ['embedding', 'search']
})
await database.init()
log('db-status', 'Database initialized successfully!\nStorage: OPFS\nAugmentations: Embedding, Search')
updateDatabaseVisualization()
// Populate noun dropdowns after initialization
populateNounDropdowns()
} catch (error) {
log('db-status', 'Error initializing database: ' + error.message)
}
}
async function addSampleData() {
if (!database) {
log('db-status', 'Please initialize database first')
return
}
try {
log('db-status', 'Adding sample social media data...')
// Sample Nouns with social media personas and content
const sampleNouns = [
// User profiles
{
id: 'user1',
type: 'person',
noun: 'person',
name: 'Alex Johnson',
content: 'Tech enthusiast and software developer. Love hiking and photography. Working on AI projects in my spare time.',
tags: ['tech', 'developer', 'photography', 'hiking']
},
{
id: 'user2',
type: 'person',
noun: 'person',
name: 'Sophia Chen',
content: 'Digital marketing specialist with a passion for data analytics. Foodie and travel blogger on weekends.',
tags: ['marketing', 'analytics', 'food', 'travel']
},
{
id: 'user3',
type: 'person',
noun: 'person',
name: 'Marcus Williams',
content: 'Graphic designer and illustrator. Creating visual stories that connect people. Coffee addict.',
tags: ['design', 'illustration', 'art', 'coffee']
},
{
id: 'user4',
type: 'person',
noun: 'person',
name: 'Priya Patel',
content: 'Environmental scientist studying climate change impacts. Advocate for sustainable living and renewable energy.',
tags: ['science', 'environment', 'sustainability', 'climate']
},
{
id: 'user5',
type: 'person',
noun: 'person',
name: 'Jordan Taylor',
content: 'Fitness coach and nutrition expert. Helping people achieve their health goals through balanced lifestyle.',
tags: ['fitness', 'nutrition', 'health', 'wellness']
},
// Posts/Content
{
id: 'post1',
type: 'document',
noun: 'content',
title: 'The Future of AI in Everyday Applications',
content: 'Artificial intelligence is rapidly transforming how we interact with technology. From smart assistants to recommendation systems, AI is becoming an invisible part of our daily lives. What excites me most is how these technologies can be personalized to individual needs while respecting privacy.',
tags: ['AI', 'technology', 'future', 'personalization']
},
{
id: 'post2',
type: 'document',
noun: 'content',
title: 'My Southeast Asia Food Tour',
content: 'Just returned from an amazing culinary adventure across Thailand, Vietnam, and Malaysia. The street food scenes in Bangkok and Hanoi were incredible! Discovered so many new flavors and cooking techniques that I can\'t wait to try at home.',
tags: ['food', 'travel', 'asia', 'culinary']
},
{
id: 'post3',
type: 'document',
noun: 'content',
title: 'Minimalist Design Principles for Digital Interfaces',
content: 'Less is more when it comes to effective UI design. This article explores how minimalist principles can improve user experience, reduce cognitive load, and create more accessible digital products.',
tags: ['design', 'minimalism', 'UI', 'UX']
},
{
id: 'post4',
type: 'document',
noun: 'content',
title: 'Urban Gardening: Growing Food in Small Spaces',
content: 'You don\'t need a large yard to grow your own food. This guide shows how to create productive gardens in apartments, balconies, and small urban spaces using container gardening, vertical systems, and efficient planning.',
tags: ['gardening', 'sustainability', 'urban', 'food']
},
{
id: 'post5',
type: 'document',
noun: 'content',
title: '30-Day Home Workout Challenge',
content: 'Transform your fitness with this comprehensive 30-day workout plan that requires no equipment. Each day builds on the previous one, gradually increasing intensity while focusing on different muscle groups.',
tags: ['fitness', 'workout', 'health', 'challenge']
},
// Comments
{
id: 'comment1',
type: 'document',
noun: 'content',
title: 'Comment on AI post',
content: 'Great insights! I\'m particularly interested in how AI can be applied to healthcare. Have you explored that area?',
tags: ['comment', 'AI', 'healthcare']
},
{
id: 'comment2',
type: 'document',
noun: 'content',
title: 'Comment on food tour',
content: 'Your photos are amazing! What was your favorite dish from the entire trip?',
tags: ['comment', 'food', 'travel', 'photography']
},
{
id: 'comment3',
type: 'document',
noun: 'content',
title: 'Comment on design article',
content: 'Minimalism is definitely underrated. I\'ve found that my conversion rates improved significantly after simplifying our landing page.',
tags: ['comment', 'design', 'conversion', 'business']
},
// Groups/Communities
{
id: 'group1',
type: 'group',
noun: 'group',
name: 'Tech Innovators Network',
content: 'A community of developers, designers, and tech enthusiasts discussing emerging technologies and their applications.',
tags: ['technology', 'innovation', 'community', 'networking']
},
{
id: 'group2',
type: 'group',
noun: 'group',
name: 'Global Foodies',
content: 'Sharing culinary experiences, recipes, and food photography from around the world.',
tags: ['food', 'cooking', 'international', 'recipes']
},
{
id: 'group3',
type: 'group',
noun: 'group',
name: 'Sustainable Living Collective',
content: 'Exploring practical ways to reduce environmental impact and live more sustainably in modern society.',
tags: ['sustainability', 'environment', 'lifestyle', 'eco-friendly']
},
// Events
{
id: 'event1',
type: 'event',
noun: 'event',
name: 'Virtual Tech Meetup 2023',
content: 'Online gathering of tech professionals sharing insights on latest development frameworks and tools.',
tags: ['tech', 'virtual', 'networking', 'learning']
},
{
id: 'event2',
type: 'event',
noun: 'event',
name: 'International Food Festival',
content: 'Annual celebration of global cuisines featuring cooking demonstrations, tastings, and cultural performances.',
tags: ['food', 'festival', 'international', 'culture']
}
]
// Add Nouns to the database
for (const item of sampleNouns) {
// Pass the content as the data to vectorize, and the entire item as metadata
await database.add(item.content || item.name, item)
}
// Wait a moment to ensure nouns are fully indexed before adding verbs
await new Promise(resolve => setTimeout(resolve, 500))
// Verify nouns exist before adding verbs
const entities = await database.getAllNouns()
const nounIds = entities.map(entity => entity.id)
console.log('Available nouns before adding verbs:', nounIds)
// Sample Verbs with social media relationships
const sampleVerbs = [
// Created/Posted content
{
source: 'user1',
target: 'post1',
verb: 'created',
label: 'Posted',
data: { strength: 0.95, description: 'Alex wrote this article about AI applications' }
},
{
source: 'user2',
target: 'post2',
verb: 'created',
label: 'Posted',
data: { strength: 0.95, description: 'Sophia shared her food tour experiences' }
},
{
source: 'user3',
target: 'post3',
verb: 'created',
label: 'Posted',
data: { strength: 0.95, description: 'Marcus wrote this design article' }
},
{
source: 'user4',
target: 'post4',
verb: 'created',
label: 'Posted',
data: { strength: 0.95, description: 'Priya shared gardening tips' }
},
{
source: 'user5',
target: 'post5',
verb: 'created',
label: 'Posted',
data: { strength: 0.95, description: 'Jordan created this workout challenge' }
},
// Comments on posts
{
source: 'user4',
target: 'comment1',
verb: 'created',
label: 'Commented',
data: { strength: 0.8, description: 'Priya commented on the AI post' }
},
{
source: 'comment1',
target: 'post1',
verb: 'relatedTo',
label: 'Comment On',
data: { strength: 0.9, description: 'This comment is on the AI post' }
},
{
source: 'user3',
target: 'comment2',
verb: 'created',
label: 'Commented',
data: { strength: 0.8, description: 'Marcus commented on the food tour post' }
},
{
source: 'comment2',
target: 'post2',
verb: 'relatedTo',
label: 'Comment On',
data: { strength: 0.9, description: 'This comment is on the food tour post' }
},
{
source: 'user2',
target: 'comment3',
verb: 'created',
label: 'Commented',
data: { strength: 0.8, description: 'Sophia commented on the design article' }
},
{
source: 'comment3',
target: 'post3',
verb: 'relatedTo',
label: 'Comment On',
data: { strength: 0.9, description: 'This comment is on the design article' }
},
// Likes/Reactions
{
source: 'user2',
target: 'post1',
verb: 'likes',
label: 'Likes',
data: { strength: 0.7, description: 'Sophia liked Alex\'s AI post' }
},
{
source: 'user3',
target: 'post1',
verb: 'likes',
label: 'Likes',
data: { strength: 0.7, description: 'Marcus liked Alex\'s AI post' }
},
{
source: 'user1',
target: 'post2',
verb: 'likes',
label: 'Likes',
data: { strength: 0.7, description: 'Alex liked Sophia\'s food tour post' }
},
{
source: 'user5',
target: 'post2',
verb: 'likes',
label: 'Likes',
data: { strength: 0.7, description: 'Jordan liked Sophia\'s food tour post' }
},
{
source: 'user1',
target: 'post3',
verb: 'likes',
label: 'Likes',
data: { strength: 0.7, description: 'Alex liked Marcus\'s design article' }
},
{
source: 'user2',
target: 'post4',
verb: 'likes',
label: 'Likes',
data: { strength: 0.7, description: 'Sophia liked Priya\'s gardening post' }
},
{
source: 'user4',
target: 'post5',
verb: 'likes',
label: 'Likes',
data: { strength: 0.7, description: 'Priya liked Jordan\'s workout challenge' }
},
// Follows
{
source: 'user1',
target: 'user2',
verb: 'follows',
label: 'Follows',
data: { strength: 0.8, description: 'Alex follows Sophia' }
},
{
source: 'user2',
target: 'user1',
verb: 'follows',
label: 'Follows',
data: { strength: 0.8, description: 'Sophia follows Alex' }
},
{
source: 'user1',
target: 'user3',
verb: 'follows',
label: 'Follows',
data: { strength: 0.8, description: 'Alex follows Marcus' }
},
{
source: 'user2',
target: 'user4',
verb: 'follows',
label: 'Follows',
data: { strength: 0.8, description: 'Sophia follows Priya' }
},
{
source: 'user3',
target: 'user5',
verb: 'follows',
label: 'Follows',
data: { strength: 0.8, description: 'Marcus follows Jordan' }
},
{
source: 'user4',
target: 'user5',
verb: 'follows',
label: 'Follows',
data: { strength: 0.8, description: 'Priya follows Jordan' }
},
{
source: 'user5',
target: 'user1',
verb: 'follows',
label: 'Follows',
data: { strength: 0.8, description: 'Jordan follows Alex' }
},
// Group memberships
{
source: 'user1',
target: 'group1',
verb: 'memberOf',
label: 'Member Of',
data: { strength: 0.85, description: 'Alex is a member of Tech Innovators Network' }
},
{
source: 'user3',
target: 'group1',
verb: 'memberOf',
label: 'Member Of',
data: { strength: 0.85, description: 'Marcus is a member of Tech Innovators Network' }
},
{
source: 'user2',
target: 'group2',
verb: 'memberOf',
label: 'Member Of',
data: { strength: 0.85, description: 'Sophia is a member of Global Foodies' }
},
{
source: 'user5',
target: 'group2',
verb: 'memberOf',
label: 'Member Of',
data: { strength: 0.85, description: 'Jordan is a member of Global Foodies' }
},
{
source: 'user4',
target: 'group3',
verb: 'memberOf',
label: 'Member Of',
data: { strength: 0.85, description: 'Priya is a member of Sustainable Living Collective' }
},
{
source: 'user2',
target: 'group3',
verb: 'memberOf',
label: 'Member Of',
data: { strength: 0.85, description: 'Sophia is a member of Sustainable Living Collective' }
},
// Event attendance
{
source: 'user1',
target: 'event1',
verb: 'relatedTo',
label: 'Attending',
data: { strength: 0.75, description: 'Alex is attending the Virtual Tech Meetup' }
},
{
source: 'user3',
target: 'event1',
verb: 'relatedTo',
label: 'Attending',
data: { strength: 0.75, description: 'Marcus is attending the Virtual Tech Meetup' }
},
{
source: 'user2',
target: 'event2',
verb: 'relatedTo',
label: 'Attending',
data: { strength: 0.75, description: 'Sophia is attending the International Food Festival' }
},
{
source: 'user5',
target: 'event2',
verb: 'relatedTo',
label: 'Attending',
data: { strength: 0.75, description: 'Jordan is attending the International Food Festival' }
},
// Content relationships
{
source: 'post1',
target: 'post3',
verb: 'relatedTo',
label: 'Related To',
data: { strength: 0.6, description: 'AI applications post is related to design principles post' }
},
{
source: 'post2',
target: 'post5',
verb: 'relatedTo',
label: 'Related To',
data: { strength: 0.5, description: 'Food tour post is somewhat related to fitness challenge post' }
},
{
source: 'post4',
target: 'group3',
verb: 'relatedTo',
label: 'Related To',
data: { strength: 0.8, description: 'Urban gardening post is related to Sustainable Living Collective' }
}
]
// Add Verbs to the database
for (const item of sampleVerbs) {
try {
// Check if source and target nouns exist before adding the verb
if (!nounIds.includes(item.source)) {
throw new Error(`Source noun with ID ${item.source} not found. Available nouns: ${nounIds.join(', ')}`)
}
if (!nounIds.includes(item.target)) {
throw new Error(`Target noun with ID ${item.target} not found. Available nouns: ${nounIds.join(', ')}`)
}
await database.relate(item.source, item.target, item.verb, item.data)
console.log(`Added verb: ${item.source} ${item.verb} ${item.target}`)
} catch (verbError) {
console.error(`Failed to add verb: ${verbError.message}`)
log('db-status', `Failed to add verb: ${verbError.message}`)
}
}
// Count successfully added verbs
const addedVerbs = await database.getAllVerbs()
log('db-status', `Added ${sampleNouns.length} nouns and ${addedVerbs.length} verbs`)
updateDatabaseVisualization()
// Populate noun dropdowns after adding sample data
populateNounDropdowns()
} catch (error) {
log('db-status', 'Error adding sample data: ' + error.message)
}
}
async function clearDatabase() {
try {
if (database) {
await database.clear()
log('db-status', 'Database cleared successfully')
updateDatabaseVisualization()
populateNounDropdowns()
}
} catch (error) {
log('db-status', 'Error clearing database: ' + error.message)
}
}
async function backupData() {
if (!database) {
log('db-status', 'No database to backup')
return
}
try {
const data = await database.backup()
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'brainy-backup.json'
a.click()
URL.revokeObjectURL(url)
log('db-status', 'Data backed up successfully')
} catch (error) {
log('db-status', 'Error backing up data: ' + error.message)
}
}
async function restoreData() {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = async function(e) {
const file = e.target.files[0]
if (!file) return
try {
const text = await file.text()
const data = JSON.parse(text)
if (database) {
await database.restore(data)
log('db-status', 'Data restored successfully')
updateDatabaseVisualization()
// Show information about the restored data
let restoreInfo = `Restored ${data.nouns?.length || 0} nouns and ${data.verbs?.length || 0} verbs.`
// Check if the backup included vectors and index
const hasVectors = data.nouns?.some(n => n.vector && n.vector.length > 0)
const hasIndex = !!data.hnswIndex
if (!hasVectors) {
restoreInfo += ' Vectors were automatically created during restore.'
}
if (!hasIndex) {
restoreInfo += ' HNSW index was reconstructed during restore.'
}
log('db-status', restoreInfo)
}
} catch (error) {
log('db-status', 'Error restoring data: ' + error.message)
}
}
input.click()
}
// Function to create and import sparse data (without vectors)
async function importSparseData() {
if (!database) {
log('db-status', 'Please initialize database first')
return
}
try {
// Create sparse data without vectors
const sparseData = {
nouns: [
{
id: 'sparse_1',
metadata: {
noun: 'Thing',
text: 'This is a sparse data item without a vector'
}
},
{
id: 'sparse_2',
metadata: {
noun: 'Concept',
text: 'Vectors will be automatically created for this item'
}
}
],
verbs: [
{
id: 'sparse_verb_1',
sourceId: 'sparse_1',
targetId: 'sparse_2',
metadata: {
verb: 'RelatedTo',
description: 'This is a relationship between sparse data items'
}
}
],
version: '1.0.0'
}
// Import the sparse data
await database.importSparseData(sparseData)
log('db-status', 'Sparse data imported successfully')
log('db-status', 'Vectors were automatically created for items without vectors')
updateDatabaseVisualization()
} catch (error) {
log('db-status', 'Error importing sparse data: ' + error.message)
}
}
// Entity operations
async function addEntity() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
const type = document.getElementById('entity-type').value
const dataText = document.getElementById('entity-data').value
if (!dataText.trim()) {
log('entity-results', 'Please enter entity data')
return
}
const data = JSON.parse(dataText)
data.type = type
data.id = data.id || 'entity_' + Date.now()
// Use a text property for vectorization, or stringify the object if no text property exists
const textToVectorize = data.content || data.description || data.text ||
data.title || data.name || JSON.stringify(data)
await database.add(textToVectorize, data)
log('entity-results', `Added ${type} entity: ${data.id}`)
// Clear the form
document.getElementById('entity-data').value = ''
// Update visualization
updateDatabaseVisualization()
// Update noun dropdowns with the new entity
populateNounDropdowns()
} catch (error) {
log('entity-results', 'Error adding entity: ' + error.message)
}
}
async function createRelationship() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
// For demo purposes, create a relationship between first two entities
const entities = await database.search('', 2, { forceEmbed: false })
if (entities.length < 2) {
log('entity-results', 'Need at least 2 entities to create a relationship')
return
}
await database.relate(entities[0].id, entities[1].id, 'connected_to', {
created: new Date().toISOString(),
strength: Math.random()
})
log('entity-results', `Created relationship: ${entities[0].id} -> ${entities[1].id}`)
// Update visualization
updateDatabaseVisualization()
} catch (error) {
log('entity-results', 'Error creating relationship: ' + error.message)
}
}
async function queryEntities() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
const entities = await database.search('', 1000, { forceEmbed: false })
const relationships = await database.getAllVerbs()
let result = `Found ${entities.length} entities and ${relationships.length} relationships\n\n`
result += 'Entities:\n'
entities.forEach(entity => {
result += `- ${entity.id} (${entity.type}): ${entity.title || entity.name || 'Untitled'}\n`
})
result += '\nRelationships:\n'
relationships.forEach(rel => {
result += `- ${rel.sourceId || rel.from} ${rel.type} ${rel.targetId || rel.to}\n`
})
log('entity-results', result)
} catch (error) {
log('entity-results', 'Error querying entities: ' + error.message)
}
}
// Noun operations
async function addNoun() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
const nounType = document.getElementById('noun-type').value
const nounId = document.getElementById('noun-id').value.trim()
const nounName = document.getElementById('noun-name').value.trim()
const nounContent = document.getElementById('noun-content').value.trim()
const nounTags = document.getElementById('noun-tags').value.trim()
if (!nounName) {
log('entity-results', 'Please enter a name for the noun')
return
}
// Create data object from form fields
const data = {
noun: nounType,
id: nounId || 'noun_' + Date.now(),
name: nounName,
content: nounContent
}
// Add tags if provided
if (nounTags) {
data.tags = nounTags.split(',').map(tag => tag.trim())
}
// Use content for vectorization, or name if content is empty
const textToVectorize = nounContent || nounName
await database.add(textToVectorize, data)
log('entity-results', `Added ${nounType} noun: ${data.id}`)
// Clear the form
document.getElementById('noun-id').value = ''
document.getElementById('noun-name').value = ''
document.getElementById('noun-content').value = ''
document.getElementById('noun-tags').value = ''
// Update visualization
updateDatabaseVisualization()
// Update noun dropdowns with the new noun
populateNounDropdowns()
} catch (error) {
log('entity-results', 'Error adding noun: ' + error.message)
}
}
async function queryNouns() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
// Query all entities that have a 'noun' property
const entities = await database.search('', 1000, { forceEmbed: false })
const nouns = entities.filter(entity => entity.noun)
let result = `Found ${nouns.length} nouns\n\n`
result += 'Nouns:\n'
nouns.forEach(noun => {
result += `- ${noun.id} (${noun.noun}): ${noun.title || noun.name || 'Untitled'}\n`
})
log('entity-results', result)
} catch (error) {
log('entity-results', 'Error querying nouns: ' + error.message)
}
}
// Verb operations
async function addVerb() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
const verbType = document.getElementById('verb-type').value
const sourceId = document.getElementById('source-id').value.trim()
const targetId = document.getElementById('target-id').value.trim()
const verbStrength = document.getElementById('verb-strength').value
const verbDescription = document.getElementById('verb-description').value.trim()
if (!sourceId || !targetId) {
log('entity-results', 'Please enter source and target node IDs')
return
}
// Create data object from form fields
const data = {
strength: parseFloat(verbStrength) || 0.8,
description: verbDescription
}
// Verify nouns exist before adding verb
const entities = await database.search('', 1000, { forceEmbed: false })
const nounIds = entities.map(entity => entity.id)
// Check if source and target nouns exist
if (!nounIds.includes(sourceId)) {
throw new Error(`Source noun with ID ${sourceId} not found. Available nouns: ${nounIds.join(', ')}`)
}
if (!nounIds.includes(targetId)) {
throw new Error(`Target noun with ID ${targetId} not found. Available nouns: ${nounIds.join(', ')}`)
}
// Add the relationship
await database.relate(sourceId, targetId, verbType, data)
console.log(`Added verb: ${sourceId} ${verbType} ${targetId}`)
log('entity-results', `Added ${verbType} verb: ${sourceId} -> ${targetId}`)
// Clear the form
document.getElementById('source-id').value = ''
document.getElementById('target-id').value = ''
document.getElementById('verb-strength').value = '0.8'
document.getElementById('verb-description').value = ''
// Update visualization
updateDatabaseVisualization()
} catch (error) {
log('entity-results', 'Error adding verb: ' + error.message)
}
}
async function queryVerbs() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
const relationships = await database.getAllVerbs()
let result = `Found ${relationships.length} verbs (relationships)\n\n`
result += 'Verbs:\n'
relationships.forEach(rel => {
result += `- ${rel.sourceId || rel.from} ${rel.type} ${rel.targetId || rel.to}\n`
})
log('entity-results', result)
} catch (error) {
log('entity-results', 'Error querying verbs: ' + error.message)
}
}
// Populate noun dropdowns for verb creation
async function populateNounDropdowns() {
if (!database) {
log('entity-results', 'Please initialize database first')
return
}
try {
// Get all nouns from the database
const entities = await database.search('', 1000, { forceEmbed: false })
const nouns = entities.filter(entity => entity.noun)
// Get the dropdown elements
const sourceDropdown = document.getElementById('source-id')
const targetDropdown = document.getElementById('target-id')
// Clear existing options except the first one
while (sourceDropdown.options.length > 1) {
sourceDropdown.remove(1)
}
while (targetDropdown.options.length > 1) {
targetDropdown.remove(1)
}
// Add nouns to the dropdowns
nouns.forEach(noun => {
const displayName = noun.name || noun.title || noun.id
const option1 = document.createElement('option')
option1.value = noun.id
option1.text = `${displayName} (${noun.noun}, ID: ${noun.id})`
sourceDropdown.add(option1)
const option2 = document.createElement('option')
option2.value = noun.id
option2.text = `${displayName} (${noun.noun}, ID: ${noun.id})`
targetDropdown.add(option2)
})
log('entity-results', `Refreshed noun dropdowns with ${nouns.length} nouns`)
} catch (error) {
log('entity-results', 'Error populating noun dropdowns: ' + error.message)
}
}
// Search operations
async function performSearch() {
if (!database) {
log('search-results', 'Please initialize database first')
return
}
try {
const query = document.getElementById('search-query').value
const searchType = document.getElementById('search-type').value
if (!query.trim()) {
log('search-results', 'Please enter a search query')
return
}
log('search-results', `Searching for: "${query}" (${searchType})`)
let results
switch (searchType) {
case 'semantic':
results = await database.search(query, 10)
break
case 'keyword':
results = await database.searchText(query, 10)
break
case 'hybrid':
// For hybrid search, we'll combine results from both methods
const semanticResults = await database.search(query, 5)
const keywordResults = await database.searchText(query, 5)
// Combine and deduplicate results
const combinedResults = [...semanticResults]
keywordResults.forEach(kr => {
if (!combinedResults.some(sr => sr.id === kr.id)) {
combinedResults.push(kr)
}
})
results = combinedResults
break
}
// Sort results by score (highest first)
results.sort((a, b) => {
const scoreA = a.score || 0
const scoreB = b.score || 0
return scoreB - scoreA
})
// Create a Google-like search results display
const resultsContainer = document.getElementById('search-results')
resultsContainer.innerHTML = ''
// Add search stats
const statsDiv = document.createElement('div')
statsDiv.className = 'search-stats'
statsDiv.innerHTML = `<p>Found ${results.length} results</p>`
resultsContainer.appendChild(statsDiv)
// Create results list
const resultsList = document.createElement('div')
resultsList.className = 'search-results-list'
results.forEach((result, index) => {
const resultItem = document.createElement('div')
resultItem.className = 'search-result-item'
// Title with score
const title = document.createElement('h3')
title.className = 'result-title'
title.innerHTML = `${result.title || result.id} <span class="result-score">Score: ${result.score?.toFixed(4) || 'N/A'}</span>`
resultItem.appendChild(title)
// URL/ID
const url = document.createElement('div')
url.className = 'result-url'
url.textContent = result.id
resultItem.appendChild(url)
// Content snippet
if (result.content) {
const snippet = document.createElement('div')
snippet.className = 'result-snippet'
snippet.textContent = result.content.substring(0, 200) + (result.content.length > 200 ? '...' : '')
resultItem.appendChild(snippet)
}
// Metadata section
const metadata = document.createElement('div')
metadata.className = 'result-metadata'
// Add all metadata except vectors
let metadataHtml = ''
for (const [key, value] of Object.entries(result)) {
if (key !== 'vector' && key !== 'content' && key !== 'id' && key !== 'title' && key !== 'score') {
metadataHtml += `<span class="metadata-item"><strong>${key}:</strong> ${JSON.stringify(value)}</span>`
}
}
if (metadataHtml) {
metadata.innerHTML = metadataHtml
resultItem.appendChild(metadata)
}
resultsList.appendChild(resultItem)
})
resultsContainer.appendChild(resultsList)
// Hide the visualization container
const visualizationContainer = document.getElementById('search-visualization')
if (visualizationContainer) {
visualizationContainer.style.display = 'none'
}
} catch (error) {
log('search-results', 'Error performing search: ' + error.message)
}
}
async function findSimilarEntities() {
if (!database) {
log('search-results', 'Please initialize database first')
return
}
try {
const entities = await database.search('', 1, { forceEmbed: false })
if (entities.length === 0) {
log('search-results', 'No entities found to compare similarity')
return
}
const similar = await database.findSimilar(entities[0].id, { limit: 5 })
// Sort results by similarity score (highest first)
similar.sort((a, b) => {
const scoreA = a.similarity || 0
const scoreB = b.similarity || 0
return scoreB - scoreA
})
// Create a Google-like search results display
const resultsContainer = document.getElementById('search-results')
resultsContainer.innerHTML = ''
// Add search stats
const statsDiv = document.createElement('div')
statsDiv.className = 'search-stats'
statsDiv.innerHTML = `<p>Found ${similar.length} entities similar to "${entities[0].title || entities[0].id}"</p>`
resultsContainer.appendChild(statsDiv)
// Create results list
const resultsList = document.createElement('div')
resultsList.className = 'search-results-list'
similar.forEach((result, index) => {
const resultItem = document.createElement('div')
resultItem.className = 'search-result-item'
// Title with similarity score
const title = document.createElement('h3')
title.className = 'result-title'
title.innerHTML = `${result.title || result.id} <span class="result-score">Similarity: ${result.similarity?.toFixed(4) || 'N/A'}</span>`
resultItem.appendChild(title)
// URL/ID
const url = document.createElement('div')
url.className = 'result-url'
url.textContent = result.id
resultItem.appendChild(url)
// Content snippet
if (result.content) {
const snippet = document.createElement('div')
snippet.className = 'result-snippet'
snippet.textContent = result.content.substring(0, 200) + (result.content.length > 200 ? '...' : '')
resultItem.appendChild(snippet)
}
// Metadata section
const metadata = document.createElement('div')
metadata.className = 'result-metadata'
// Add all metadata except vectors
let metadataHtml = ''
for (const [key, value] of Object.entries(result)) {
if (key !== 'vector' && key !== 'content' && key !== 'id' && key !== 'title' && key !== 'similarity') {
metadataHtml += `<span class="metadata-item"><strong>${key}:</strong> ${JSON.stringify(value)}</span>`
}
}
if (metadataHtml) {
metadata.innerHTML = metadataHtml
resultItem.appendChild(metadata)
}
resultsList.appendChild(resultItem)
})
resultsContainer.appendChild(resultsList)
// Hide the visualization container
const visualizationContainer = document.getElementById('search-visualization')
if (visualizationContainer) {
visualizationContainer.style.display = 'none'
}
} catch (error) {
log('search-results', 'Error finding similar entities: ' + error.message)
}
}
async function performClusterAnalysis() {
if (!database) {
log('search-results', 'Please initialize database first')
return
}
try {
log('search-results', 'Performing cluster analysis...')
// Simulate cluster analysis
const entities = await database.search('', 1000, { forceEmbed: false })
const clusters = Math.min(3, Math.ceil(entities.length / 2))
// Create a Google-like search results display
const resultsContainer = document.getElementById('search-results')
resultsContainer.innerHTML = ''
// Add search stats
const statsDiv = document.createElement('div')
statsDiv.className = 'search-stats'
statsDiv.innerHTML = `<p>Cluster Analysis Results: ${entities.length} entities in ${clusters} clusters</p>`
resultsContainer.appendChild(statsDiv)
// Create results list
const resultsList = document.createElement('div')
resultsList.className = 'search-results-list'
// Group entities into clusters (simplified simulation)
for (let i = 0; i < clusters; i++) {
const clusterEntities = entities.filter((_, index) => index % clusters === i)
// Create cluster header
const clusterHeader = document.createElement('div')
clusterHeader.className = 'cluster-header'
clusterHeader.innerHTML = `<h3>Cluster ${i + 1} (${clusterEntities.length} entities)</h3>`
resultsList.appendChild(clusterHeader)
// Add entities in this cluster
clusterEntities.forEach((entity, index) => {
const resultItem = document.createElement('div')
resultItem.className = 'search-result-item cluster-item'
// Title
const title = document.createElement('h3')
title.className = 'result-title'
title.textContent = entity.title || entity.id
resultItem.appendChild(title)
// URL/ID
const url = document.createElement('div')
url.className = 'result-url'
url.textContent = entity.id
resultItem.appendChild(url)
// Content snippet (shorter for clusters)
if (entity.content) {
const snippet = document.createElement('div')
snippet.className = 'result-snippet'
snippet.textContent = entity.content.substring(0, 100) + (entity.content.length > 100 ? '...' : '')
resultItem.appendChild(snippet)
}
// Metadata section
const metadata = document.createElement('div')
metadata.className = 'result-metadata'
// Add all metadata except vectors
let metadataHtml = ''
for (const [key, value] of Object.entries(entity)) {
if (key !== 'vector' && key !== 'content' && key !== 'id' && key !== 'title') {
metadataHtml += `<span class="metadata-item"><strong>${key}:</strong> ${JSON.stringify(value)}</span>`
}
}
if (metadataHtml) {
metadata.innerHTML = metadataHtml
resultItem.appendChild(metadata)
}
resultsList.appendChild(resultItem)
})
}
resultsContainer.appendChild(resultsList)
// Hide the visualization container
const visualizationContainer = document.getElementById('search-visualization')
if (visualizationContainer) {
visualizationContainer.style.display = 'none'
}
} catch (error) {
log('search-results', 'Error performing cluster analysis: ' + error.message)
}
}
// Augmentation operations removed
// Helper function to create demo entities for visualization
function createDemoEntities() {
return [
// Person nouns
{
id: 'demo-person1',
type: 'person',
noun: 'person',
name: 'John Smith',
content: 'Software engineer with expertise in AI and machine learning.',
tags: ['engineer', 'developer'],
metadata: { type: 'person', noun: 'person' }
},
{
id: 'demo-person2',
type: 'person',
noun: 'person',
name: 'Emily Johnson',
content: 'Data scientist specializing in natural language processing.',
tags: ['scientist', 'researcher'],
metadata: { type: 'person', noun: 'person' }
},
// Thing nouns
{
id: 'demo-thing1',
type: 'thing',
noun: 'thing',
name: 'Neural Network',
content: 'Computational model inspired by the human brain.',
tags: ['AI', 'computing'],
metadata: { type: 'thing', noun: 'thing' }
},
{
id: 'demo-thing2',
type: 'thing',
noun: 'thing',
name: 'Graph Database',
content: 'Database that uses graph structures for semantic queries.',
tags: ['database', 'storage'],
metadata: { type: 'thing', noun: 'thing' }
},
// Concept nouns
{
id: 'demo-concept1',
type: 'concept',
noun: 'concept',
name: 'Machine Learning',
content: 'Field of study that gives computers the ability to learn without being explicitly programmed.',
tags: ['AI', 'algorithms'],
metadata: { type: 'concept', noun: 'concept' }
}
]
}
// Helper function to create demo relationships for visualization
function createDemoRelationships() {
return [
// Created verbs
{
id: 'demo-verb1',
source: 'demo-person1',
target: 'demo-thing1',
verb: 'created',
label: 'Created',
data: { strength: 0.9, description: 'John Smith created the neural network' }
},
// WorksWith verbs
{
id: 'demo-verb2',
source: 'demo-person1',
target: 'demo-person2',
verb: 'worksWith',
label: 'Works With',
data: { strength: 0.7, description: 'John and Emily collaborate on research projects' }
},
// RelatedTo verbs
{
id: 'demo-verb3',
source: 'demo-thing1',
target: 'demo-concept1',
verb: 'relatedTo',
label: 'Related To',
data: { strength: 0.75, description: 'Neural networks are related to machine learning' }
}
]
}
// Function to create a graph visualization with the provided data
function createGraphWithData(g, entities, relationships, width, height, zoom, svg) {
// Create a legend (outside the zoom group to keep it fixed)
const legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width - 150}, 20)`)
// Noun legend
legend.append('circle')
.attr('cx', 10)
.attr('cy', 10)
.attr('r', 8)
.attr('fill', '#4CAF50')
legend.append('text')
.attr('x', 25)
.attr('y', 15)
.text('Nouns')
.attr('font-size', '12px')
// Verb legend
legend.append('line')
.attr('x1', 0)
.attr('y1', 40)
.attr('x2', 20)
.attr('y2', 40)
.attr('stroke', '#2196F3')
.attr('stroke-width', 3)
legend.append('text')
.attr('x', 25)
.attr('y', 45)
.text('Verbs')
.attr('font-size', '12px')
// Create a force simulation
const simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(200))
.force('charge', d3.forceManyBody().strength(-500))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(80))
// Prepare the data for D3
const nodes = entities.map(entity => ({
id: entity.id,
type: entity.type || 'unknown',
noun: entity.noun || entity.type || 'unknown',
title: entity.title || entity.name || entity.id
}))
// Filter relationships to only include those where both source and target nodes exist
const links = relationships
.filter(rel => {
// Use sourceId and targetId (GraphVerb properties) or fallback to from/to or source/target
const sourceId = rel.sourceId || rel.from || rel.source
const targetId = rel.targetId || rel.to || rel.target
// Check if both source and target nodes exist in the nodes array
const sourceExists = nodes.some(node => node.id === sourceId)
const targetExists = nodes.some(node => node.id === targetId)
return sourceExists && targetExists
})
.map(rel => ({
source: rel.sourceId || rel.from || rel.source,
target: rel.targetId || rel.to || rel.target,
verb: rel.type || rel.verb,
label: (rel.type || rel.verb || '').charAt(0).toUpperCase() + (rel.type || rel.verb || '').slice(1).replace(/([A-Z])/g, ' $1'),
data: rel.data || {}
}))
// Create arrow marker for directed edges
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 25)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#2196F3')
// Draw verbs (edges)
const link = g.selectAll('.link')
.data(links)
.enter()
.append('g')
.attr('class', 'link')
const linkLine = link.append('line')
.attr('stroke', '#2196F3')
.attr('stroke-width', d => 1 + (d.data?.strength || 0.5) * 3)
.attr('marker-end', 'url(#arrowhead)')
// Add verb labels
const linkText = link.append('text')
.attr('text-anchor', 'middle')
.attr('fill', '#2196F3')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('background', 'white')
.attr('paint-order', 'stroke')
.attr('stroke', 'white')
.attr('stroke-width', '3px')
.text(d => d.label || d.verb)
// Draw nouns (nodes)
const node = g.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
node.append('circle')
.attr('r', 25)
.attr('fill', d => {
// Color by type
switch (d.type) {
case 'person':
return '#4CAF50'
case 'place':
return '#FF9800'
case 'thing':
return '#2196F3'
case 'event':
return '#F44336'
case 'concept':
return '#9C27B0'
case 'group':
return '#795548'
case 'document':
return '#607D8B'
default:
return '#BDBDBD'
}
})
.attr('stroke', '#388E3C')
.attr('stroke-width', 2)
// Add noun labels
node.append('text')
.attr('text-anchor', 'middle')
.attr('dy', 5)
.attr('fill', 'white')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.text(d => d.title)
// Add noun type labels
node.append('text')
.attr('text-anchor', 'middle')
.attr('dy', 40)
.attr('fill', '#333')
.attr('font-size', '10px')
.text(d => d.noun || d.type)
// Update positions on simulation tick
simulation.nodes(nodes).on('tick', ticked)
simulation.force('link').links(links)
// Function to fit the graph to the container
function zoomToFit() {
if (!nodes.length) return
// Calculate the bounds of the graph
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
nodes.forEach(node => {
if (node.x < minX) minX = node.x
if (node.y < minY) minY = node.y
if (node.x > maxX) maxX = node.x
if (node.y > maxY) maxY = node.y
})
// Add padding - increased to give more room for labels
const padding = 100
minX -= padding
minY -= padding
maxX += padding
maxY += padding
// Calculate scale and translate to fit the graph
const scale = Math.min(
width / (maxX - minX),
height / (maxY - minY)
)
const translate = [
width / 2 - scale * (minX + maxX) / 2,
height / 2 - scale * (minY + maxY) / 2
]
// Apply the zoom transform
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity
.translate(translate[0], translate[1])
.scale(scale)
)
}
// Call zoomToFit after the simulation has stabilized
simulation.on('end', zoomToFit)
// Also call zoomToFit after a timeout to ensure it happens even if simulation doesn't end
setTimeout(zoomToFit, 1000)
function ticked() {
linkLine
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y)
linkText
.attr('x', d => (d.source.x + d.target.x) / 2)
.attr('y', d => (d.source.y + d.target.y) / 2 - 15)
// Add a slight offset to prevent overlap with the line
linkText.each(function(d) {
const dx = d.target.x - d.source.x
const dy = d.target.y - d.source.y
const angle = Math.atan2(dy, dx)
// Calculate perpendicular offset
const perpX = -Math.sin(angle) * 8
const perpY = Math.cos(angle) * 8
// Apply the offset
d3.select(this)
.attr('x', (d.source.x + d.target.x) / 2 + perpX)
.attr('y', (d.source.y + d.target.y) / 2 - 15 + perpY)
})
node
.attr('transform', d => `translate(${d.x}, ${d.y})`)
}
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event, d) {
d.fx = event.x
d.fy = event.y
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
}
async function updateDatabaseVisualization() {
const visualizationContainer = document.getElementById('db-visualization')
const graphContainer = visualizationContainer.querySelector('.graph-container')
if (!database) {
graphContainer.innerHTML = '<div style="text-align: center; padding-top: 200px; color: #666;">Database not initialized</div>'
return
}
try {
// Query all entities and relationships from the database
const entities = await database.getAllNouns()
const relationships = await database.getAllVerbs()
// Clear the container
graphContainer.innerHTML = ''
// Remove any existing demo message
const existingDemoMessage = visualizationContainer.querySelector('div[style*="position: absolute"]')
if (existingDemoMessage) {
visualizationContainer.removeChild(existingDemoMessage)
}
// If the database is empty, show empty visualization
let nodes = []
let edges = []
if (entities.length === 0) {
// Show message that database is empty
const emptyMessage = document.createElement('div')
emptyMessage.style.position = 'absolute'
emptyMessage.style.top = '50%'
emptyMessage.style.left = '50%'
emptyMessage.style.transform = 'translate(-50%, -50%)'
emptyMessage.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'
emptyMessage.style.padding = '10px 20px'
emptyMessage.style.borderRadius = '5px'
emptyMessage.style.fontSize = '14px'
emptyMessage.style.color = '#666'
emptyMessage.textContent = 'Database is empty. Add data to see your graph.'
graphContainer.appendChild(emptyMessage)
return
} else {
// Transform real data to the format expected by the visualization
nodes = entities.map(entity => ({
id: entity.id,
noun: entity.noun || entity.type || 'concept',
data: {
displayName: entity.name || entity.title || entity.id,
handle: entity.id,
avatar: '',
...entity
}
}))
edges = relationships.map(rel => ({
source: rel.sourceId || rel.from || rel.source,
target: rel.targetId || rel.to || rel.target,
verb: rel.verb || rel.type || 'relatedTo',
confidence: 0.8,
data: rel.data || {}
}))
}
// Ensure all nodes referenced in edges exist in the nodes array
const nodeIds = new Set(nodes.map(node => node.id))
const validEdges = edges.filter(edge => {
const sourceId = typeof edge.source === 'string' ? edge.source : edge.source.id
const targetId = typeof edge.target === 'string' ? edge.target : edge.target.id
return nodeIds.has(sourceId) && nodeIds.has(targetId)
})
// Log any filtered edges for debugging
if (validEdges.length < edges.length) {
console.warn(`Filtered out ${edges.length - validEdges.length} edges with missing nodes`)
edges.forEach(edge => {
const sourceId = typeof edge.source === 'string' ? edge.source : edge.source.id
const targetId = typeof edge.target === 'string' ? edge.target : edge.target.id
if (!nodeIds.has(sourceId)) {
console.warn(`Edge references missing source node: ${sourceId}`)
}
if (!nodeIds.has(targetId)) {
console.warn(`Edge references missing target node: ${targetId}`)
}
})
}
// Create the cartographer-style visualization
createCartographerGraph(graphContainer, nodes, validEdges)
// Set up zoom-fit button
const zoomFitBtn = document.getElementById('zoom-fit-btn')
zoomFitBtn.addEventListener('click', () => {
zoomToFit()
})
// Auto zoom to fit the data view
setTimeout(() => {
zoomToFit()
}, 100)
} catch (error) {
console.error('Error updating visualization:', error)
graphContainer.innerHTML = `<div style="text-align: center; padding-top: 200px; color: #666;">Error updating visualization: ${error.message}</div>`
}
}
// Global variables for the graph
let svgElement = null
let zoomBehavior = null
let graphContentContainer = null
function createCartographerGraph(container, nodes, edges) {
// Remove any existing SVG
d3.select(container).select('svg').remove()
const width = container.clientWidth
const height = container.clientHeight
// Material Design 3 Inspired Colors
const md3Colors = {
primary: '#6750A4',
onPrimary: '#FFFFFF',
secondaryContainer: '#E8DEF8',
onSecondaryContainer: '#1D192B',
outline: '#79747E',
surface: '#FFFBFE',
shadow: 'rgba(0, 0, 0, 0.3)'
}
// Palette for nodes
const nodeColorPalette = [
md3Colors.primary,
'#4A5BF5',
'#278853',
'#A83A57',
'#D56A2C',
'#5D517F'
]
// Create SVG element
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', '#F7F2FA')
// Store reference to SVG element
svgElement = svg
// Create defs for filters
const defs = svg.append('defs')
// Add shadow filter
defs.append('filter')
.attr('id', 'md-shadow')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%')
.append('feDropShadow')
.attr('dx', '0')
.attr('dy', '4')
.attr('stdDeviation', '6')
.attr('flood-color', md3Colors.shadow)
.attr('flood-opacity', '0.25')
// Create arrow marker for directed edges (from explore view)
defs.append('marker')
.attr('id', 'arrowhead-cartographer')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 22) // Adjusted to match explore view
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 8) // Increased size to match explore view
.attr('markerHeight', 8) // Increased size to match explore view
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#2196F3') // Blue color from explore view
// Create zoom behavior
zoomBehavior = d3.zoom()
.scaleExtent([0.1, 5])
.on('zoom', (event) => {
graphContentContainer.attr('transform', event.transform)
})
svg.call(zoomBehavior)
.on('click', (event) => {
// Only handle clicks directly on the SVG background
if (event.target === svg.node()) {
// Deselect any selected nodes
d3.selectAll('.node-group').transition().duration(300).style('opacity', 1)
d3.selectAll('.edge-group').transition().duration(300).style('opacity', 1)
d3.selectAll('.node-group').classed('selected', false)
}
})
// Create graph container
graphContentContainer = svg.append('g')
.attr('class', 'graph-content-container')
// Create overlay container for popups
const overlayContainer = svg.append('g')
.attr('class', 'overlay-container')
// Create edge groups
const linkGroup = graphContentContainer
.append('g')
.attr('class', 'edges')
.selectAll('g')
.data(edges)
.join('g')
.attr('class', 'edge-group')
// Add lines with thickness based on confidence (using explore view styling)
const link = linkGroup
.append('line')
.attr('stroke', '#2196F3') // Blue color from explore view
.attr('stroke-opacity', 0.8)
.attr('stroke-width', (d) => {
return d.confidence ? 1 + d.confidence * 5 : 1.5
})
.attr('marker-end', 'url(#arrowhead-cartographer)')
// Add edge labels (styled to match explore view)
const labelGroup = linkGroup
.append('g')
.attr('class', 'edge-label-group')
.style('cursor', 'pointer')
// Add background pill for edge labels (matching explore view)
labelGroup
.append('rect')
.attr('class', 'edge-label-background')
.attr('fill', '#E3F2FD') // Light blue background from explore view
.attr('fill-opacity', 0.95)
.attr('stroke', '#2196F3') // Blue outline from explore view
.attr('stroke-width', 1)
.attr('stroke-opacity', 0.8)
.attr('rx', 12) // Rounded corners for pill shape
.attr('ry', 12)
.attr('width', 0) // Will be set dynamically based on text width
.attr('height', 24)
.attr('x', 0)
.attr('y', -12)
.style('filter', 'url(#md-shadow)')
// Add text for edge labels
const edgeText = labelGroup
.append('text')
.attr('class', 'edge-label')
.attr('text-anchor', 'middle')
.attr('fill', '#0D47A1') // Dark blue text from explore view
.attr('dy', '0.35em') // Vertical centering
.style('font-family', '\'Roboto\', \'Inter\', sans-serif')
.style('font-size', '11px')
.style('font-weight', '500')
.style('pointer-events', 'none')
.text(d => {
// Format verb name
const verb = d.verb
return verb.charAt(0).toUpperCase() + verb.slice(1).replace(/([A-Z])/g, ' $1')
})
// Adjust background rect width based on text width
setTimeout(() => {
labelGroup.each(function() {
const text = d3.select(this).select('text')
const textWidth = text.node().getComputedTextLength()
d3.select(this).select('rect')
.attr('width', textWidth + 16) // Add padding
.attr('x', -textWidth / 2 - 8) // Center the rect
})
}, 0)
// Node size for card layout
const nodeSize = { width: 280, height: 80 }
const nodeRadius = 8
// Create node groups
const node = graphContentContainer
.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(nodes)
.join('g')
.attr('class', 'node-group')
.style('cursor', 'pointer')
// Add card background with different shapes based on node type
node.append('path')
.attr('d', (d) => {
const w = nodeSize.width
const h = nodeSize.height
const r = nodeRadius
const x = -w / 2
const y = -h / 2
// Check if node is a Group or List
if (d.noun === 'group' || d.noun === 'list') {
// Circle for Groups and Lists
const radius = Math.max(w, h) / 1.5
return `
M ${x + w / 2},${y + h / 2 - radius}
a ${radius},${radius} 0 0 1 0,${2 * radius}
a ${radius},${radius} 0 0 1 0,${-2 * radius}
z
`
} else {
// Path with rounded corners for other nodes
return `
M ${x},${y + r}
a ${r},${r} 0 0 1 ${r},${-r}
h ${w - 2 * r}
a ${r},${r} 0 0 1 ${r},${r}
v ${h - 2 * r}
a ${r},${r} 0 0 1 ${-r},${r}
h ${-(w - 2 * r)}
a ${r},${r} 0 0 1 ${-r},${-r}
z
`
}
})
.attr('fill', (d, i) => {
// Assign colors based on node type
const nounType = d.noun || 'concept'
const colorIndex = Math.abs(nounType.charCodeAt(0) + nounType.charCodeAt(nounType.length - 1)) % nodeColorPalette.length
return nodeColorPalette[colorIndex]
})
.attr('fill-opacity', 0.8)
.attr('stroke', md3Colors.outline)
.attr('stroke-width', 1)
.attr('stroke-opacity', 0.3)
.style('filter', 'url(#md-shadow)')
// Add node labels
node.append('text')
.attr('class', 'node-label')
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.attr('y', 0)
.style('font-family', '\'Roboto\', \'Inter\', sans-serif')
.style('font-size', '16px')
.style('font-weight', 'bold')
.style('pointer-events', 'none')
.text(d => {
// Truncate long names
const name = d.data.displayName || d.id
return name.length > 30 ? name.substring(0, 27) + '...' : name
})
// Add node type labels
node.append('text')
.attr('class', 'node-type-label')
.attr('text-anchor', 'middle')
.attr('fill', 'rgba(255, 255, 255, 0.8)')
.attr('y', 20)
.style('font-family', '\'Roboto\', \'Inter\', sans-serif')
.style('font-size', '12px')
.style('font-style', 'italic')
.style('pointer-events', 'none')
.text(d => {
// Format noun type
const nounType = d.noun || 'concept'
return nounType.charAt(0).toUpperCase() + nounType.slice(1)
})
// Add drag behavior
node.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
// Add click behavior for nodes
node.on('click', (event, d) => {
event.stopPropagation()
// Highlight the selected node and connected edges
const selectedNodeId = d.id
// Dim all nodes and edges
d3.selectAll('.node-group').transition().duration(300).style('opacity', 0.3)
d3.selectAll('.edge-group').transition().duration(300).style('opacity', 0.1)
// Highlight the selected node
d3.selectAll('.node-group').classed('selected', false)
d3.select(event.currentTarget)
.classed('selected', true)
.transition()
.duration(300)
.style('opacity', 1)
// Find connected edges and nodes
const connectedEdges = edges.filter(e =>
e.source === selectedNodeId ||
e.target === selectedNodeId ||
(e.source.id && e.source.id === selectedNodeId) ||
(e.target.id && e.target.id === selectedNodeId)
)
const connectedNodeIds = new Set()
connectedEdges.forEach(e => {
const sourceId = typeof e.source === 'string' ? e.source : e.source.id
const targetId = typeof e.target === 'string' ? e.target : e.target.id
connectedNodeIds.add(sourceId)
connectedNodeIds.add(targetId)
})
// Highlight connected nodes
d3.selectAll('.node-group').each(function(nodeData) {
if (connectedNodeIds.has(nodeData.id)) {
d3.select(this).transition().duration(300).style('opacity', 1)
}
})
// Highlight connected edges
d3.selectAll('.edge-group').each(function(edgeData, i) {
const sourceId = typeof edgeData.source === 'string' ? edgeData.source : edgeData.source.id
const targetId = typeof edgeData.target === 'string' ? edgeData.target : edgeData.target.id
if (sourceId === selectedNodeId || targetId === selectedNodeId) {
d3.select(this).transition().duration(300).style('opacity', 1)
}
})
})
// Create force simulation
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(edges).id(d => d.id).distance(200))
.force('charge', d3.forceManyBody().strength(-500))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(100))
.on('tick', ticked)
// Tick function to update positions
function ticked() {
// Update node positions
node.attr('transform', d => `translate(${d.x},${d.y})`)
// Update link positions
link
.attr('x1', d => typeof d.source === 'string' ? d.source.x : d.source.x)
.attr('y1', d => typeof d.source === 'string' ? d.source.y : d.source.y)
.attr('x2', d => typeof d.target === 'string' ? d.target.x : d.target.x)
.attr('y2', d => typeof d.target === 'string' ? d.target.y : d.target.y)
// Update edge label positions (enhanced to match explore view)
labelGroup.attr('transform', d => {
const sourceX = typeof d.source === 'string' ? d.source.x : d.source.x
const sourceY = typeof d.source === 'string' ? d.source.y : d.source.y
const targetX = typeof d.target === 'string' ? d.target.x : d.target.x
const targetY = typeof d.target === 'string' ? d.target.y : d.target.y
// Position label at the midpoint of the edge
const x = (sourceX + targetX) / 2
const y = (sourceY + targetY) / 2
// Calculate angle for rotation (to align with edge direction)
const angle = Math.atan2(targetY - sourceY, targetX - sourceX) * 180 / Math.PI
// Apply a small offset perpendicular to the edge to avoid overlapping
const offsetDistance = 10
const perpX = -Math.sin(angle * Math.PI / 180) * offsetDistance
const perpY = Math.cos(angle * Math.PI / 180) * offsetDistance
// Normalize angle for readability (keep text upright)
const rotationAngle = angle > 90 || angle < -90 ? angle - 180 : angle
return `translate(${x + perpX},${y + perpY}) rotate(${rotationAngle})`
})
}
// Drag functions
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event, d) {
d.fx = event.x
d.fy = event.y
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
// Initial zoom to fit
zoomToFit()
}
function zoomToFit() {
if (!svgElement || !zoomBehavior || !graphContentContainer) return
// Get the bounds of the graph content
const bounds = graphContentContainer.node().getBBox()
const width = svgElement.attr('width')
const height = svgElement.attr('height')
// Add padding to the bounds to ensure labels are visible
const padding = 100
const paddedBounds = {
x: bounds.x - padding,
y: bounds.y - padding,
width: bounds.width + (padding * 2),
height: bounds.height + (padding * 2)
}
// Calculate the scale and translate to fit the graph
const scale = 0.7 * Math.min(
width / paddedBounds.width,
height / paddedBounds.height
)
const translateX = width / 2 - scale * (paddedBounds.x + paddedBounds.width / 2)
const translateY = height / 2 - scale * (paddedBounds.y + paddedBounds.height / 2)
// Apply the zoom transform
svgElement.transition()
.duration(750)
.call(
zoomBehavior.transform,
d3.zoomIdentity
.translate(translateX, translateY)
.scale(scale)
)
}
function updateSearchVisualization(results) {
// This function is now disabled as per requirements to remove the visual graph
return;
// Create SVG container
const svg = d3.select(graphContainer)
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'search-viz')
// Create a group for the graph content that will be transformed by zoom
const graphContent = svg.append('g')
.attr('class', 'graph-content')
// Add a title
graphContent.append('text')
.attr('x', width / 2)
.attr('y', 30)
.attr('text-anchor', 'middle')
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.text(`Search Results: ${results.length} matches`)
// Create zoom behavior
const zoomBehavior = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
graphContent.attr('transform', event.transform)
})
// Apply zoom behavior to SVG
svg.call(zoomBehavior)
// Create a force simulation
const simulation = d3.forceSimulation()
.force('center', d3.forceCenter(width / 2, height / 2))
.force('charge', d3.forceManyBody().strength(-200))
.force('collide', d3.forceCollide().radius(60))
// Set up zoom controls
const zoomFitBtn = document.getElementById('search-zoom-fit-btn')
const zoomInBtn = document.getElementById('search-zoom-in-btn')
const zoomOutBtn = document.getElementById('search-zoom-out-btn')
if (zoomFitBtn) {
zoomFitBtn.addEventListener('click', () => {
zoomToFit()
})
}
if (zoomInBtn) {
zoomInBtn.addEventListener('click', () => {
const currentTransform = d3.zoomTransform(svg.node())
svg.transition()
.duration(300)
.call(zoomBehavior.transform,
d3.zoomIdentity.translate(currentTransform.x, currentTransform.y).scale(currentTransform.k * 1.3))
})
}
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', () => {
const currentTransform = d3.zoomTransform(svg.node())
svg.transition()
.duration(300)
.call(zoomBehavior.transform,
d3.zoomIdentity.translate(currentTransform.x, currentTransform.y).scale(currentTransform.k / 1.3))
})
}
// Process the results to create nodes
const nodes = results.map((result, index) => {
// Extract type from result or default to 'document'
const type = result.type || 'document'
// Determine node color based on type
let color
switch (type) {
case 'person':
color = '#E91E63' // Pink
break
case 'document':
color = '#4CAF50' // Green
break
case 'group':
color = '#2196F3' // Blue
break
default:
color = '#FF9800' // Orange
}
return {
id: result.id,
title: result.title || result.id,
score: result.score || 0.5,
type: type,
color: color,
content: result.content || '',
index: index
}
})
// Add nodes to the simulation
simulation.nodes(nodes).on('tick', ticked)
// Create node elements
const nodeGroups = graphContent.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
// Add circles for nodes with size based on score
nodeGroups.append('circle')
.attr('r', d => 30 + d.score * 20)
.attr('fill', d => d.color)
.attr('opacity', 0.8)
.attr('stroke', '#fff')
.attr('stroke-width', 2)
// Add text labels
nodeGroups.append('text')
.attr('text-anchor', 'middle')
.attr('dy', 4)
.attr('fill', 'white')
.attr('font-weight', 'bold')
.text(d => d.title.substring(0, 10) + (d.title.length > 10 ? '...' : ''))
// Add score labels
nodeGroups.append('text')
.attr('text-anchor', 'middle')
.attr('dy', 20)
.attr('fill', 'white')
.attr('font-size', '10px')
.text(d => d.score ? `Score: ${d.score.toFixed(2)}` : '')
// Add tooltips
nodeGroups.append('title')
.text(d => `${d.title}\nType: ${d.type}\nScore: ${d.score?.toFixed(4) || 'N/A'}\n${d.content.substring(0, 100)}${d.content.length > 100 ? '...' : ''}`)
// Create a legend (outside the zoom group to keep it fixed)
const legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width - 120}, 50)`)
// Add legend title
legend.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('font-weight', 'bold')
.text('Result Types')
// Define legend items
const legendItems = [
{ type: 'Person', color: '#E91E63' },
{ type: 'Document', color: '#4CAF50' },
{ type: 'Group', color: '#2196F3' },
{ type: 'Other', color: '#FF9800' }
]
// Add legend items
legendItems.forEach((item, i) => {
const legendItem = legend.append('g')
.attr('transform', `translate(0, ${i * 25 + 20})`)
legendItem.append('circle')
.attr('r', 8)
.attr('fill', item.color)
legendItem.append('text')
.attr('x', 15)
.attr('y', 4)
.attr('font-size', '12px')
.text(item.type)
})
// Tick function to update positions
function ticked() {
nodeGroups
.attr('transform', d => `translate(${d.x}, ${d.y})`)
}
// Drag functions
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event, d) {
d.fx = event.x
d.fy = event.y
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
// Function to zoom to fit all nodes
function zoomToFit() {
if (!svg || !zoomBehavior || !graphContent.node()) return
// Get the bounds of the graph content
const bounds = graphContent.node().getBBox()
// Calculate the scale and translation to fit the content
const scale = 0.9 / Math.max(
bounds.width / width,
bounds.height / height
)
const translateX = width / 2 - scale * (bounds.x + bounds.width / 2)
const translateY = height / 2 - scale * (bounds.y + bounds.height / 2)
// Apply the zoom transform
svg.transition()
.duration(750)
.call(
zoomBehavior.transform,
d3.zoomIdentity
.translate(translateX, translateY)
.scale(scale)
)
}
// Auto zoom to fit after the simulation has stabilized
simulation.on('end', zoomToFit)
// Also call zoomToFit after a timeout to ensure it happens even if simulation doesn't end
setTimeout(zoomToFit, 1000)
}
// CLI functions
function executeCLICommand(command) {
const historyElement = document.getElementById('cli-history')
const outputElement = document.getElementById('cli-output')
// Add command to history
const commandElement = document.createElement('div')
commandElement.className = 'cli-command'
commandElement.innerHTML = `<span class="cli-prompt">brainy$</span> ${command}`
historyElement.appendChild(commandElement)
// Simulate command execution
let output = ''
switch (command.split(' ')[0]) {
case 'help':
output = `Available commands:
init Initialize a new database
add <data> Add data to the database
search <q> Search for entities
relate <f> <t> Create relationships
export Export database
status Show database status
clear Clear the terminal
help Show this help message`
break
case 'status':
output = database ?
'Database: Initialized\nStorage: OPFS\nEntities: Available\nAugmentations: Active' :
'Database: Not initialized\nRun "brainy init" to get started'
break
case 'init':
output = 'Initializing database...\nDatabase initialized successfully!'
if (!database) {
initializeDatabase()
}
break
case 'query':
output = database ?
'Querying database...\nFound entities matching your criteria' :
'Error: Database not initialized'
break
default:
output = `Unknown command: ${command}\nType "help" for available commands`
}
outputElement.textContent = output
// Scroll to bottom
const terminal = document.querySelector('.cli-terminal')
terminal.scrollTop = terminal.scrollHeight
}
function clearTerminal() {
document.getElementById('cli-history').innerHTML = ''
document.getElementById('cli-output').textContent = ''
}
// Feature demonstration functions
function demonstrateFeature(feature) {
const resultsContainer = document.getElementById('feature-demo-results')
switch (feature) {
case 'hnsw':
resultsContainer.innerHTML = `
<h4>🔍 HNSW Vector Search Demo</h4>
<p>Hierarchical Navigable Small World (HNSW) is an efficient algorithm for approximate nearest neighbor search.</p>
<div class="code-block">
// Initialize HNSW index
const index = new HNSWIndex(dimension: 384);
// Add vectors
index.addVector(vector1, 'doc1');
index.addVector(vector2, 'doc2');
// Search for similar vectors
const results = index.search(queryVector, k: 10);
</div>
<p><strong>Performance:</strong> O(log N) search time, ideal for large-scale vector databases.</p>
`
break
case 'graph-traversal':
resultsContainer.innerHTML = `
<h4>🌐 Graph Traversal Demo</h4>
<p>Explore relationships and paths through your data graph.</p>
<div class="code-block">
// Find shortest path between entities
const path = await brainy.findPath('entity1', 'entity2');
// Get all neighbors within 2 hops
const neighbors = await brainy.getNeighbors('entity1', { depth: 2 });
// Traverse by relationship type
const related = await brainy.traverse('entity1', 'RELATED_TO');
</div>
<p><strong>Algorithms:</strong> BFS, DFS, shortest path, centrality measures.</p>
`
break
case 'embedding':
resultsContainer.innerHTML = `
<h4>🧠 Embedding Pipeline Demo</h4>
<p>Automatic text-to-vector conversion using pre-trained models.</p>
<div class="code-block">
// Configure embedding pipeline
const pipeline = new EmbeddingPipeline({
model: 'universal-sentence-encoder',
dimension: 512
});
// Process text automatically
const result = await pipeline.process({
text: "Machine learning is transforming technology",
id: "ml-article"
});
// Vector is automatically generated and indexed
console.log(result.vector.length); // 512
</div>
<p><strong>Models:</strong> Universal Sentence Encoder, BERT, custom embeddings.</p>
`
break
case 'realtime':
resultsContainer.innerHTML = `
<h4>📊 Real-time Updates Demo</h4>
<p>Live data synchronization and event streaming.</p>
<div class="code-block">
// Listen for real-time updates
brainy.on('entity:added', (entity) => {
console.log('New entity:', entity.id);
updateVisualization();
});
brainy.on('relationship:created', (relationship) => {
console.log('New relationship:', relationship);
updateGraph();
});
// Subscribe to search result changes
brainy.subscribeToQuery('AI articles', (results) => {
updateSearchResults(results);
});
</div>
<p><strong>Features:</strong> WebSocket support, event streaming, reactive queries.</p>
`
break
case 'storage':
resultsContainer.innerHTML = `
<h4>💾 Storage Backends Demo</h4>
<p>Compare different storage options and their characteristics.</p>
<div class="code-block">
// Memory storage (fastest)
const memoryDB = new BrainyData({
storage: { type: 'memory' }
});
// File system storage (persistent)
const fileDB = new BrainyData({
storage: {
type: 'filesystem',
path: './data'
}
});
// Cloud storage (scalable)
const cloudDB = new BrainyData({
storage: {
type: 's3',
bucket: 'my-brainy-data',
region: 'us-east-1'
}
});
</div>
<p><strong>Options:</strong> Memory, FileSystem, S3, Azure Blob, custom adapters.</p>
`
break
case 'typescript':
resultsContainer.innerHTML = `
<h4>🔧 TypeScript Integration Demo</h4>
<p>Full type safety with comprehensive TypeScript definitions.</p>
<div class="code-block">
// Define your data types
interface Article {
id: string;
title: string;
content: string;
tags: string[];
publishedAt: Date;
}
// Type-safe database operations
const brainy = new BrainyData&lt;Article&gt;();
// TypeScript ensures type safety
const article: Article = {
id: 'article-1',
title: 'AI in 2024',
content: 'The future of AI...',
tags: ['AI', 'technology'],
publishedAt: new Date()
};
// Compile-time type checking
const results = await brainy.search&lt;Article&gt;('AI trends');
</div>
<p><strong>Benefits:</strong> IntelliSense, compile-time errors, better IDE support.</p>
`
break
}
}
// Simulation functions for augmentations
async function simulateTextEmbedding(text) {
await new Promise(resolve => setTimeout(resolve, 1000))
const vector = Array.from({ length: 384 }, () => Math.random() * 2 - 1)
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0))
return `Text Embedding Results:
Input: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"
Vector dimension: 384
Magnitude: ${magnitude.toFixed(6)}
Sample values: [${vector.slice(0, 5).map(v => v.toFixed(4)).join(', ')}...]
The text has been successfully converted to a 384-dimensional vector suitable for semantic search and similarity calculations.`
}
async function simulateImageEncoding(content) {
await new Promise(resolve => setTimeout(resolve, 1200))
return `Image Encoding Results:
Input: Image data or URL
Encoding: Base64 with compression
Size reduction: ~75%
Format: Optimized for embedding storage
The image has been encoded and optimized for storage in the vector database.`
}
async function simulateSentimentAnalysis(text) {
await new Promise(resolve => setTimeout(resolve, 800))
const sentiments = ['positive', 'negative', 'neutral']
const sentiment = sentiments[Math.floor(Math.random() * sentiments.length)]
const confidence = Math.random() * 0.5 + 0.5
return `Sentiment Analysis Results:
Input: "${text.substring(0, 100)}${text.length > 100 ? '...' : ''}"
Sentiment: ${sentiment.toUpperCase()}
Confidence: ${(confidence * 100).toFixed(1)}%
Polarity: ${(Math.random() * 2 - 1).toFixed(3)}
The text has been analyzed for emotional tone and sentiment indicators.`
}
async function simulateEntityExtraction(text) {
await new Promise(resolve => setTimeout(resolve, 900))
const entities = [
{ text: 'Machine Learning', type: 'TECHNOLOGY', start: 0, end: 16 },
{ text: 'AI', type: 'TECHNOLOGY', start: 20, end: 22 },
{ text: 'neural networks', type: 'CONCEPT', start: 35, end: 50 }
]
let result = `Entity Extraction Results:
Input: "${text.substring(0, 100)}${text.length > 100 ? '...' : ''}"
Extracted Entities:
`
entities.forEach(entity => {
result += `- ${entity.text} (${entity.type})\n`
})
result += `\nTotal entities found: ${entities.length}`
return result
}
// Utility functions
function log(elementId, message) {
const element = document.getElementById(elementId)
if (element) {
element.textContent = message
element.scrollTop = element.scrollHeight
}
}
function showError(message) {
console.error(message)
const errorDiv = document.createElement('div')
errorDiv.style.cssText = 'background-color: #f44336; color: white; padding: 15px; margin: 10px 0; border-radius: 8px;'
errorDiv.textContent = 'Error: ' + message
document.querySelector('.container').insertBefore(errorDiv, document.querySelector('.container').firstChild)
setTimeout(() => {
errorDiv.remove()
}, 5000)
}
</script>
</body>
</html>