- Created `scripts/check-code-style.js` to enforce coding standards. - Includes ESLint, Prettier checks, and custom semicolon rule validation. - Outputs actionable feedback for style violations. - Updated `CONTRIBUTING.md` to document new code style workflows and commands. - Added style commands to `package.json`: - `lint`, `lint:fix`, `format`, `check-format`, and `check-style`. - Refactored `demo/index.html` to use `Object.defineProperty` for better global property management and prevent conflicts. These changes enhance the automation of code quality checks, streamline developer workflows, and improve reliability in global property handling for the demo.
4274 lines
149 KiB
HTML
4274 lines
149 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="../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/demo/index.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;
|
|
|
|
--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;
|
|
}
|
|
|
|
.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 interface styles */
|
|
.google-search-container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.google-search-box-container {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.google-search-box {
|
|
display: flex;
|
|
align-items: center;
|
|
border: 1px solid #dfe1e5;
|
|
border-radius: 24px;
|
|
padding: 8px 16px;
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
box-shadow: 0 1px 6px rgba(32, 33, 36, 0.28);
|
|
transition: box-shadow 0.3s, border-color 0.3s;
|
|
}
|
|
|
|
.google-search-box:hover, .google-search-box:focus-within {
|
|
box-shadow: 0 1px 8px rgba(32, 33, 36, 0.35);
|
|
border-color: rgba(223, 225, 229, 0);
|
|
}
|
|
|
|
.google-search-icon {
|
|
margin-right: 12px;
|
|
color: #9aa0a6;
|
|
}
|
|
|
|
.google-search-box input {
|
|
flex: 1;
|
|
border: none;
|
|
outline: none;
|
|
font-size: 16px;
|
|
color: #202124;
|
|
background: transparent;
|
|
padding: 8px 0;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.google-search-clear {
|
|
cursor: pointer;
|
|
color: #70757a;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.google-search-options {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.search-type-container select {
|
|
padding: 8px 12px;
|
|
border: 1px solid #dfe1e5;
|
|
border-radius: 4px;
|
|
background-color: white;
|
|
font-size: 14px;
|
|
color: #202124;
|
|
outline: none;
|
|
}
|
|
|
|
.google-search-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.google-search-button {
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #f8f9fa;
|
|
border-radius: 4px;
|
|
color: #3c4043;
|
|
font-family: Arial, sans-serif;
|
|
font-size: 14px;
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
user-select: none;
|
|
transition: border-color 0.3s, box-shadow 0.3s;
|
|
}
|
|
|
|
.google-search-button:hover {
|
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
|
border-color: #dadce0;
|
|
}
|
|
|
|
.google-search-button:focus {
|
|
border-color: #4285f4;
|
|
outline: none;
|
|
}
|
|
|
|
.google-search-button.secondary {
|
|
color: #1a73e8;
|
|
}
|
|
|
|
.google-results-container {
|
|
max-width: 650px;
|
|
margin: 30px auto;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.google-results-placeholder {
|
|
color: #70757a;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
.match-reason {
|
|
background-color: #f8f9fa;
|
|
border-left: 3px solid #fbbc05;
|
|
padding: 8px 12px;
|
|
margin: 8px 0;
|
|
font-size: 13px;
|
|
color: #5f6368;
|
|
line-height: 1.5;
|
|
border-radius: 0 4px 4px 0;
|
|
}
|
|
|
|
.match-reason strong {
|
|
color: #202124;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* Highlighted search terms */
|
|
.highlight {
|
|
background-color: #FFEB3B;
|
|
font-weight: bold;
|
|
padding: 0 1px;
|
|
}
|
|
|
|
/* 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/demo/index.html"
|
|
style="color: white; text-decoration: underline;">http://localhost:8080/demo/index.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="../brainy.png" alt="Brainy Logo">
|
|
<div class="header-text">
|
|
<h1>Brainy Interactive Demo</h1>
|
|
<p>A 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>
|
|
|
|
<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: Searching -->
|
|
<div class="section-content" id="step2-content">
|
|
<h3>Step 2: Semantic Search</h3>
|
|
<p>Search your data using vector embeddings for semantic understanding.</p>
|
|
|
|
<!-- Google-like search interface -->
|
|
<div class="google-search-container">
|
|
<div class="google-search-box-container">
|
|
<div class="google-search-box">
|
|
<div class="google-search-icon">
|
|
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
|
|
<path
|
|
d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
|
fill="#9aa0a6"></path>
|
|
</svg>
|
|
</div>
|
|
<input type="text" id="search-query" placeholder="Try: travel, AI, fitness, food, design...">
|
|
<div class="google-search-clear" id="search-clear">
|
|
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
|
|
fill="#70757a"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="google-search-options">
|
|
<div class="search-type-container">
|
|
<select id="search-type">
|
|
<option value="semantic">Semantic Search</option>
|
|
<option value="keyword">Keyword Search</option>
|
|
<option value="hybrid">Hybrid Search</option>
|
|
<option value="verbs">Verb Search</option>
|
|
<option value="connected-nouns">Connected Nouns Search</option>
|
|
</select>
|
|
</div>
|
|
<div class="google-search-buttons">
|
|
<button id="search-btn" class="google-search-button">Search</button>
|
|
<!-- <button id="similar-entities-btn" class="google-search-button secondary">Find Similar Entities</button>-->
|
|
</div>
|
|
</div>
|
|
</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="google-results-container" id="search-results">
|
|
<div class="google-results-placeholder">
|
|
Search results will appear here...
|
|
</div>
|
|
</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 3: Adding Data -->
|
|
<div class="section-content" id="step3-content">
|
|
<h3>Step 3: 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 4: Creating Relationships -->
|
|
<div class="section-content" id="step4-content">
|
|
<h3>Step 4: 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 5: Advanced Features -->
|
|
<div class="section-content" id="step6-content">
|
|
<h3>Step 5: 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 6: CLI Interface -->
|
|
<div class="section-content" id="step7-content">
|
|
<h3>Step 6: 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 <data> - Add data to the database<br>
|
|
brainy search <query> - Search for entities<br>
|
|
brainy relate <from> <to> - Create relationships<br>
|
|
brainy status - Show database status<br>
|
|
brainy help - Show all commands
|
|
</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">
|
|
<script type="module">
|
|
// Use local files instead of CDN
|
|
// For GitHub Pages, use relative path: './dist/unified.js'
|
|
// For local development, use absolute path: '/dist/unified.js'
|
|
// This import is replaced by the dynamic import below
|
|
|
|
// Or minified version
|
|
// import { BrainyData, NounType, VerbType } from '/dist/unified.min.js'
|
|
|
|
const db = new BrainyData()
|
|
await db.init()
|
|
// ...
|
|
</script>
|
|
</pre>
|
|
<p>Modern bundlers like Webpack, Rollup and Vite will automatically use the unified build which adapts to any
|
|
environment.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
<p>© 2024 Brainy - A lightweight graph & vector data platform</p>
|
|
<p>
|
|
<a href="https://github.com/soulcraft-research/brainy" target="_blank">GitHub</a> |
|
|
<a href="https://github.com/soulcraft-research/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">
|
|
// Always use local files instead of CDN
|
|
// Use relative path for GitHub Pages compatibility
|
|
const isGitHubPages = window.location.hostname.includes('github.io')
|
|
const isCodeSoulcraft = window.location.hostname.includes('code.soulcraft.com')
|
|
|
|
// Use different paths based on hosting environment
|
|
let importPath
|
|
if (isGitHubPages) {
|
|
importPath = './dist/unified.js' // Direct access to dist folder in demo directory
|
|
} else if (isCodeSoulcraft) {
|
|
// Use relative path instead of absolute path for code.soulcraft.com
|
|
importPath = './dist/unified.js'
|
|
} else {
|
|
importPath = '/dist/unified.js' // Local development
|
|
}
|
|
|
|
// Dynamic import
|
|
const { BrainyData, NounType, VerbType } = await import(importPath)
|
|
|
|
// Make BrainyData and types available globally
|
|
// Use Object.defineProperty to avoid conflicts with existing properties
|
|
Object.defineProperty(window, 'BrainyData', { value: BrainyData, writable: true, configurable: true })
|
|
Object.defineProperty(window, 'NounType', { value: NounType, writable: true, configurable: true })
|
|
Object.defineProperty(window, 'VerbType', { value: VerbType, writable: true, configurable: true })
|
|
|
|
// Initialize demo immediately since modules already run after DOM is loaded
|
|
// 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: Searching - Search operations
|
|
document.getElementById('search-btn')?.addEventListener('click', performSearch)
|
|
document.getElementById('similar-entities-btn')?.addEventListener('click', findSimilarEntities)
|
|
|
|
// Google-like search functionality
|
|
const searchInput = document.getElementById('search-query')
|
|
const searchClear = document.getElementById('search-clear')
|
|
|
|
if (searchInput && searchClear) {
|
|
// Show/hide clear button based on input content
|
|
searchInput.addEventListener('input', function() {
|
|
searchClear.style.visibility = this.value ? 'visible' : 'hidden'
|
|
})
|
|
|
|
// Clear the search input when the clear button is clicked
|
|
searchClear.addEventListener('click', function() {
|
|
searchInput.value = ''
|
|
searchClear.style.visibility = 'hidden'
|
|
searchInput.focus()
|
|
})
|
|
|
|
// Allow pressing Enter to search
|
|
searchInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
performSearch()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Step 3: Adding Entities - Noun operations
|
|
document.getElementById('add-noun-btn')?.addEventListener('click', addNoun)
|
|
document.getElementById('query-nouns-btn')?.addEventListener('click', queryNouns)
|
|
|
|
// Step 4: 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 5: Advanced Features - Data management
|
|
document.getElementById('export-data-btn')?.addEventListener('click', backupData)
|
|
document.getElementById('import-data-btn')?.addEventListener('click', restoreData)
|
|
|
|
// Step 6: 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)
|
|
})
|
|
})
|
|
|
|
// We'll populate noun dropdowns after database initialization
|
|
// populateNounDropdowns() - This will be called after database is initialized
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Clear the database after initialization
|
|
await database.clear()
|
|
|
|
log('db-status', 'Database initialized and cleared 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 senior software developer with 10+ years of experience in AI and machine learning. I love hiking through national parks on weekends and capturing landscape photography. Currently working on several open-source AI projects that focus on natural language processing and computer vision applications for everyday use cases.',
|
|
tags: ['tech', 'developer', 'photography', 'hiking', 'AI', 'machine learning']
|
|
},
|
|
{
|
|
id: 'user2',
|
|
type: 'person',
|
|
noun: 'person',
|
|
name: 'Sophia Chen',
|
|
content: 'Digital marketing specialist with a passion for data analytics and consumer behavior research. I run a popular food and travel blog documenting culinary experiences across continents. My background in statistics helps me analyze food trends and cultural patterns in different regions. Recently completed a six-month journey through Southeast Asia exploring local cuisines.',
|
|
tags: ['marketing', 'analytics', 'food', 'travel', 'statistics', 'blogging']
|
|
},
|
|
{
|
|
id: 'user3',
|
|
type: 'person',
|
|
noun: 'person',
|
|
name: 'Marcus Williams',
|
|
content: 'Award-winning graphic designer and illustrator specializing in brand identity and visual storytelling. I believe in the power of minimalist design to communicate complex ideas effectively. My work has been featured in several international design publications and exhibitions. When not designing, I\'m exploring local coffee shops and perfecting my home brewing techniques.',
|
|
tags: ['design', 'illustration', 'art', 'coffee', 'branding', 'minimalism']
|
|
},
|
|
{
|
|
id: 'user4',
|
|
type: 'person',
|
|
noun: 'person',
|
|
name: 'Priya Patel',
|
|
content: 'Environmental scientist with a PhD in climate science, currently researching the long-term impacts of climate change on urban ecosystems. I advocate for sustainable living practices and renewable energy adoption through community workshops and educational programs. My research has been published in several peer-reviewed journals, and I consult with local governments on sustainability initiatives.',
|
|
tags: ['science', 'environment', 'sustainability', 'climate', 'research', 'education']
|
|
},
|
|
{
|
|
id: 'user5',
|
|
type: 'person',
|
|
noun: 'person',
|
|
name: 'Jordan Taylor',
|
|
content: 'Certified fitness coach and nutrition expert with specializations in functional training and plant-based nutrition. I develop personalized fitness programs that integrate physical training with mental wellness practices. My approach focuses on sustainable lifestyle changes rather than quick fixes. I\'ve helped hundreds of clients transform their health through balanced nutrition and consistent exercise routines.',
|
|
tags: ['fitness', 'nutrition', 'health', 'wellness', 'coaching', 'plant-based']
|
|
},
|
|
|
|
// 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 in ways that weren\'t possible just a few years ago. From smart assistants that understand context and nuance to recommendation systems that seem to know our preferences better than we do, AI is becoming an invisible yet powerful part of our daily lives. The most exciting developments are happening at the intersection of machine learning and edge computing, where AI models can run directly on our devices without sending sensitive data to the cloud. This preserves privacy while still delivering personalized experiences. Looking ahead, I believe we\'ll see AI becoming more explainable and transparent, addressing current concerns about "black box" algorithms. The challenge for developers will be creating systems that can be personalized to individual needs while maintaining ethical standards and avoiding algorithmic bias. The companies that succeed will be those that treat AI as an augmentation of human capabilities rather than a replacement.',
|
|
tags: ['AI', 'technology', 'future', 'personalization', 'machine learning', 'privacy', 'ethics']
|
|
},
|
|
{
|
|
id: 'post2',
|
|
type: 'document',
|
|
noun: 'content',
|
|
title: 'My Southeast Asia Food Tour: A Culinary Journey',
|
|
content: 'I\'ve just returned from an incredible three-month culinary adventure across Thailand, Vietnam, Malaysia, and Singapore that completely transformed my understanding of Asian cuisine. The vibrant street food scenes in Bangkok and Hanoi were particularly eye-opening - watching skilled vendors prepare dishes they\'ve perfected over decades, using techniques passed down through generations. In Bangkok\'s Chinatown, I discovered that the best pad thai comes from a 70-year-old woman who cooks only 50 portions each night, selling out within an hour. The complex interplay of sweet, sour, salty, and spicy flavors in authentic Thai cuisine is something that western adaptations rarely capture correctly. Moving to Vietnam, I was fascinated by the regional variations in pho - from the clearer, more delicate broths in the south to the richer, more aromatic versions in Hanoi. The Malaysian food scene in Penang revealed the beautiful fusion of Chinese, Indian, and Malay influences, creating dishes that tell the story of the country\'s multicultural history. I\'ve returned home with not just memories, but with cooking techniques, ingredient combinations, and flavor profiles that I\'m excited to incorporate into my own cooking. The experience has reinforced my belief that food is one of the most authentic ways to understand a culture\'s values and history.',
|
|
tags: ['food', 'travel', 'asia', 'culinary', 'culture', 'cooking', 'Thailand', 'Vietnam', 'Malaysia']
|
|
},
|
|
{
|
|
id: 'post3',
|
|
type: 'document',
|
|
noun: 'content',
|
|
title: 'Minimalist Design Principles for Digital Interfaces: Beyond Aesthetics',
|
|
content: 'The principle that "less is more" has never been more relevant than in today\'s overcrowded digital landscape. Minimalist design isn\'t just an aesthetic choice—it\'s a functional imperative for creating interfaces that users can navigate intuitively. Through my work with various clients, I\'ve observed that minimalist interfaces consistently outperform cluttered ones in key metrics like time-on-task, error rates, and user satisfaction. The core principles of minimalist design extend beyond simply removing elements. It involves a careful analysis of user needs, prioritizing functions based on frequency and importance, and creating visual hierarchies that guide attention naturally. Color should be used strategically, not decoratively—each color choice should serve a specific communicative purpose. Typography plays an equally crucial role; a well-chosen font hierarchy can eliminate the need for additional visual elements like dividers or boxes. White space (or negative space) is perhaps the most undervalued element in digital design. It\'s not just empty space—it\'s a powerful tool for creating focus, improving readability, and giving content room to breathe. When implementing minimalist principles, it\'s important to test with real users to ensure that simplification doesn\'t compromise usability. The goal is to reduce cognitive load while maintaining all necessary functionality, creating digital products that feel effortless to use while being fully featured. This approach not only improves user experience but also tends to create more accessible interfaces that work better for users with disabilities or those using assistive technologies.',
|
|
tags: ['design', 'minimalism', 'UI', 'UX', 'accessibility', 'typography', 'user experience', 'digital interfaces']
|
|
},
|
|
{
|
|
id: 'post4',
|
|
type: 'document',
|
|
noun: 'content',
|
|
title: 'Urban Gardening: Growing Food in Small Spaces',
|
|
content: 'The misconception that you need a large yard to grow your own food is preventing many urban dwellers from experiencing the satisfaction and benefits of home gardening. Through five years of experimentation in my 500-square-foot apartment, I\'ve developed systems for growing approximately 30% of my own produce year-round. Vertical gardening is the cornerstone of space-efficient food production. By utilizing wall space with hanging planters, tiered shelving, and trellis systems, even the smallest balcony can become surprisingly productive. I\'ve found that leafy greens like kale, spinach, and various lettuces offer the best return on investment in small spaces—they grow quickly, can be harvested continuously, and would otherwise be expensive to purchase organically. For those without outdoor space, windowsill herb gardens and microgreens under grow lights provide fresh flavors with minimal space requirements. The key to successful small-space gardening is understanding the specific light conditions of your space and selecting appropriate plants. Most vegetables need at least 6 hours of direct sunlight, but many herbs and leafy greens can thrive with less. Container selection is equally important—self-watering containers have revolutionized urban gardening by reducing maintenance and improving plant health. Soil quality becomes even more critical in container gardening; investing in high-quality potting mix and implementing a regular composting system (even a small worm bin under the sink) creates a sustainable cycle. Beyond the practical benefits of fresh, pesticide-free produce, urban gardening creates a connection to natural cycles that is often missing in city life. The psychological benefits of tending plants and watching them grow should not be underestimated, especially in high-stress urban environments.',
|
|
tags: ['gardening', 'sustainability', 'urban', 'food', 'containers', 'vertical gardening', 'organic', 'small spaces']
|
|
},
|
|
{
|
|
id: 'post5',
|
|
type: 'document',
|
|
noun: 'content',
|
|
title: '30-Day Home Workout Challenge: Progressive Fitness Without Equipment',
|
|
content: 'After years of designing fitness programs, I\'ve found that the biggest obstacle for most people isn\'t motivation—it\'s complexity and accessibility. This 30-day challenge addresses both issues by requiring zero equipment while providing a structured progression that prevents plateaus and reduces injury risk. The program is built around functional movement patterns that strengthen the body for real-world activities, not just gym performance. The first week focuses on establishing proper form in fundamental movements like squats, lunges, planks, and push-ups, with modifications provided for all fitness levels. Rather than counting reps, many exercises use time-based intervals, allowing each person to work at their own pace while still being challenged. As the program progresses, intensity increases through three primary mechanisms: increasing time under tension, incorporating unilateral (single-limb) variations, and adding rhythmic changes that challenge coordination and cardiovascular fitness simultaneously. Recovery is programmed as deliberately as the workouts themselves, with specific mobility routines for rest days that address common problem areas like hip flexors and shoulders that become tight from sedentary work. The nutrition guidance accompanying the program emphasizes timing and composition rather than strict calorie counting—particularly the importance of protein intake for recovery and carbohydrate timing around workouts. Participants who\'ve completed this challenge report not just physical changes, but improvements in sleep quality, energy levels, and mental clarity. The program includes a tracking system that helps identify patterns between workout performance and variables like sleep, stress, and nutrition, teaching participants to understand their body\'s unique responses and needs.',
|
|
tags: ['fitness', 'workout', 'health', 'challenge', 'home exercise', 'bodyweight', 'functional fitness', 'progressive training']
|
|
},
|
|
|
|
// Comments
|
|
{
|
|
id: 'comment1',
|
|
type: 'document',
|
|
noun: 'content',
|
|
title: 'Comment on AI post',
|
|
content: 'Your insights on the intersection of AI and privacy are particularly relevant given the recent controversies surrounding facial recognition technologies. I\'m currently researching how AI can be applied to healthcare diagnostics while maintaining patient confidentiality. Have you explored how federated learning might offer solutions in this domain? I\'d love to discuss this further as it seems aligned with your interest in edge computing applications.',
|
|
tags: ['comment', 'AI', 'healthcare', 'privacy', 'federated learning', 'edge computing']
|
|
},
|
|
{
|
|
id: 'comment2',
|
|
type: 'document',
|
|
noun: 'content',
|
|
title: 'Comment on food tour',
|
|
content: 'Your detailed description of regional Vietnamese pho variations brought back memories of my own travels there! The photos you shared of the Hanoi street food markets are absolutely stunning - the composition and lighting really capture the vibrant atmosphere. I\'m curious about your experience with Malaysian laksa - did you find significant differences between the Penang and Sarawak versions? I\'ve been trying to recreate authentic laksa at home but struggling to find the right balance of spices.',
|
|
tags: ['comment', 'food', 'travel', 'photography', 'Vietnam', 'Malaysia', 'cooking', 'recipes']
|
|
},
|
|
{
|
|
id: 'comment3',
|
|
type: 'document',
|
|
noun: 'content',
|
|
title: 'Comment on design article',
|
|
content: 'Your analysis of minimalism as a functional imperative rather than just an aesthetic choice resonates strongly with my experience in e-commerce design. After implementing many of the principles you outlined, particularly regarding strategic color usage and improved typography hierarchy, our conversion rates improved by 23% while customer support inquiries decreased by almost 30%. The point about accessibility benefits is especially important and often overlooked in design discussions. Have you found any effective methods for convincing stakeholders who equate visual complexity with feature richness?',
|
|
tags: ['comment', 'design', 'conversion', 'business', 'accessibility', 'minimalism', 'typography', 'e-commerce']
|
|
},
|
|
|
|
// Groups/Communities
|
|
{
|
|
id: 'group1',
|
|
type: 'group',
|
|
noun: 'group',
|
|
name: 'Tech Innovators Network',
|
|
content: 'A global community of developers, designers, and tech enthusiasts collaborating on emerging technologies and their practical applications. Our members range from startup founders to corporate innovators, all sharing insights on AI, blockchain, IoT, and other transformative technologies. We host monthly virtual meetups featuring expert speakers, maintain an active knowledge base of implementation case studies, and facilitate mentorship connections between experienced professionals and those entering the field.',
|
|
tags: ['technology', 'innovation', 'community', 'networking', 'AI', 'blockchain', 'IoT', 'collaboration']
|
|
},
|
|
{
|
|
id: 'group2',
|
|
type: 'group',
|
|
noun: 'group',
|
|
name: 'Global Foodies Collective',
|
|
content: 'An international community dedicated to exploring culinary traditions and innovations from around the world. Our members share authentic recipes, cooking techniques, and food photography that celebrates cultural diversity through cuisine. We organize themed cooking challenges, virtual cook-alongs with chefs from different countries, and discussions about sustainable food systems and the preservation of traditional cooking methods. The community also maintains a searchable database of member-tested recipes organized by region, dietary preferences, and difficulty level.',
|
|
tags: ['food', 'cooking', 'international', 'recipes', 'culture', 'sustainability', 'traditions', 'culinary']
|
|
},
|
|
{
|
|
id: 'group3',
|
|
type: 'group',
|
|
noun: 'group',
|
|
name: 'Sustainable Living Collective',
|
|
content: 'A community focused on practical approaches to reducing environmental impact in everyday life. We share evidence-based strategies for sustainable living in urban, suburban, and rural contexts, with special emphasis on solutions that are economically accessible. Our resources include guides for zero-waste home management, energy efficiency improvements, ethical consumption, and small-space gardening. Members collaborate on neighborhood initiatives like community gardens, repair cafés, and local policy advocacy for environmental protection and green infrastructure.',
|
|
tags: ['sustainability', 'environment', 'lifestyle', 'eco-friendly', 'zero-waste', 'community', 'gardening', 'advocacy']
|
|
},
|
|
|
|
// Events
|
|
{
|
|
id: 'event1',
|
|
type: 'event',
|
|
noun: 'event',
|
|
name: 'Virtual Tech Meetup 2023: Responsible AI Development',
|
|
content: 'An online gathering of technology professionals sharing insights on ethical AI development frameworks and implementation tools. The event features keynote presentations on bias detection in machine learning models, panel discussions on regulatory compliance across different jurisdictions, and hands-on workshops demonstrating techniques for making AI systems more transparent and explainable. Participants will have opportunities for structured networking with peers working in similar domains and access to a resource library of code samples, case studies, and best practice guidelines.',
|
|
tags: ['tech', 'virtual', 'networking', 'learning', 'AI', 'ethics', 'development', 'workshops']
|
|
},
|
|
{
|
|
id: 'event2',
|
|
type: 'event',
|
|
noun: 'event',
|
|
name: 'International Food Festival: Culinary Traditions in a Modern World',
|
|
content: 'Annual celebration of global cuisines featuring cooking demonstrations from renowned chefs representing diverse culinary traditions. The festival includes interactive tastings with detailed exploration of ingredient origins and cultural significance, panel discussions on preserving food heritage while embracing innovation, and cultural performances that contextualize food within broader cultural expressions. Special exhibition areas focus on sustainable food systems, the impact of climate change on traditional agriculture, and the role of technology in modern food production and distribution.',
|
|
tags: ['food', 'festival', 'international', 'culture', 'cooking', 'sustainability', 'traditions', 'innovation']
|
|
}
|
|
]
|
|
|
|
// Add Nouns to the database using batch processing for better performance
|
|
const nounItems = sampleNouns.map(item => ({
|
|
vectorOrData: item.content || item.name,
|
|
metadata: item
|
|
}))
|
|
|
|
await database.addBatch(nounItems, { batchSize: 50 })
|
|
|
|
// No need to wait as addBatch ensures all items are fully processed
|
|
|
|
// 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 using parallel processing for better performance
|
|
// First filter out verbs with invalid source or target nouns
|
|
const validVerbs = sampleVerbs.filter(item => {
|
|
if (!nounIds.includes(item.source)) {
|
|
console.error(`Source noun with ID ${item.source} not found. Available nouns: ${nounIds.join(', ')}`)
|
|
log('db-status', `Failed to add verb: Source noun with ID ${item.source} not found`)
|
|
return false
|
|
}
|
|
if (!nounIds.includes(item.target)) {
|
|
console.error(`Target noun with ID ${item.target} not found. Available nouns: ${nounIds.join(', ')}`)
|
|
log('db-status', `Failed to add verb: Target noun with ID ${item.target} not found`)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Process verbs in parallel using Promise.all
|
|
const verbPromises = validVerbs.map(item =>
|
|
database.relate(item.source, item.target, item.verb, item.data)
|
|
.then(() => {
|
|
console.log(`Added verb: ${item.source} ${item.verb} ${item.target}`)
|
|
return true
|
|
})
|
|
.catch(verbError => {
|
|
console.error(`Failed to add verb: ${verbError.message}`)
|
|
log('db-status', `Failed to add verb: ${verbError.message}`)
|
|
return false
|
|
})
|
|
)
|
|
|
|
// Wait for all verbs to be processed
|
|
await Promise.all(verbPromises)
|
|
|
|
// 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
|
|
// Helper function to highlight search terms
|
|
function highlightSearchTerms(text, searchQuery) {
|
|
if (!searchQuery || !text) return text
|
|
|
|
// Escape special regex characters in the search query
|
|
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
// Create a regex to match the search query (case insensitive)
|
|
const regex = new RegExp(`(${escapedQuery})`, 'gi')
|
|
|
|
// Replace matches with highlighted version
|
|
return text.replace(regex, '<span class="highlight">$1</span>')
|
|
}
|
|
|
|
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})`)
|
|
|
|
// Record start time for search
|
|
const startTime = performance.now()
|
|
|
|
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
|
|
case 'verbs':
|
|
// Search for verbs directly
|
|
results = await database.search(query, 10, {
|
|
searchVerbs: true
|
|
// Optionally filter by verb types
|
|
// verbTypes: ['RelatedTo', 'Created', 'Owns']
|
|
})
|
|
break
|
|
case 'connected-nouns':
|
|
// Search for nouns connected by verbs
|
|
results = await database.search(query, 10, {
|
|
searchConnectedNouns: true,
|
|
// Optionally filter by verb types
|
|
// verbTypes: ['RelatedTo', 'Created', 'Owns'],
|
|
verbDirection: 'both' // 'outgoing', 'incoming', or 'both'
|
|
})
|
|
break
|
|
}
|
|
|
|
// Calculate search time
|
|
const endTime = performance.now()
|
|
const searchTime = ((endTime - startTime) / 1000).toFixed(2)
|
|
|
|
// 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>About ${results.length} results (${searchTime} seconds)</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'
|
|
// Highlight search terms in the title
|
|
const titleText = result.title || result.id
|
|
title.innerHTML = `${highlightSearchTerms(titleText, query)} <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'
|
|
|
|
// Extract relevant parts of content that contain search terms
|
|
let snippetText = ''
|
|
if (query && query.trim()) {
|
|
const terms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0)
|
|
const content = result.content.toLowerCase()
|
|
|
|
// Find positions of all search terms in the content
|
|
let positions = []
|
|
terms.forEach(term => {
|
|
let pos = content.indexOf(term)
|
|
while (pos !== -1) {
|
|
positions.push({ start: pos, end: pos + term.length })
|
|
pos = content.indexOf(term, pos + 1)
|
|
}
|
|
})
|
|
|
|
// Sort positions by start index
|
|
positions.sort((a, b) => a.start - b.start)
|
|
|
|
// Merge overlapping or close positions
|
|
let mergedPositions = []
|
|
if (positions.length > 0) {
|
|
let current = positions[0]
|
|
for (let i = 1; i < positions.length; i++) {
|
|
if (positions[i].start <= current.end + 50) { // 50 chars buffer between matches
|
|
current.end = Math.max(current.end, positions[i].end)
|
|
} else {
|
|
mergedPositions.push(current)
|
|
current = positions[i]
|
|
}
|
|
}
|
|
mergedPositions.push(current)
|
|
}
|
|
|
|
// Extract snippets around each match position (with context)
|
|
if (mergedPositions.length > 0) {
|
|
mergedPositions.forEach((pos, idx) => {
|
|
const contextStart = Math.max(0, pos.start - 30)
|
|
const contextEnd = Math.min(result.content.length, pos.end + 30)
|
|
|
|
if (idx > 0) snippetText += ' [...] '
|
|
snippetText += result.content.substring(contextStart, contextEnd)
|
|
})
|
|
} else {
|
|
// Fallback to first 200 chars if no matches found
|
|
snippetText = result.content.substring(0, 200) + (result.content.length > 200 ? '...' : '')
|
|
}
|
|
} else {
|
|
// If no query, use first 200 chars
|
|
snippetText = result.content.substring(0, 200) + (result.content.length > 200 ? '...' : '')
|
|
}
|
|
|
|
// Highlight search terms in the snippet
|
|
snippet.innerHTML = highlightSearchTerms(snippetText, query)
|
|
|
|
resultItem.appendChild(snippet)
|
|
}
|
|
|
|
// Match reason section
|
|
if (query && query.trim()) {
|
|
const matchReason = document.createElement('div')
|
|
matchReason.className = 'match-reason'
|
|
|
|
const terms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0)
|
|
let matchDetails = []
|
|
|
|
// Check title matches
|
|
if (result.title) {
|
|
const titleLower = result.title.toLowerCase()
|
|
const titleMatches = terms.filter(term => titleLower.includes(term))
|
|
if (titleMatches.length > 0) {
|
|
matchDetails.push(`Title matches: "${titleMatches.join('", "')}"`)
|
|
}
|
|
}
|
|
|
|
// Check content matches
|
|
if (result.content) {
|
|
const contentLower = result.content.toLowerCase()
|
|
const contentMatches = terms.filter(term => contentLower.includes(term))
|
|
if (contentMatches.length > 0) {
|
|
const matchCounts = contentMatches.map(term => {
|
|
let count = 0
|
|
let pos = contentLower.indexOf(term)
|
|
while (pos !== -1) {
|
|
count++
|
|
pos = contentLower.indexOf(term, pos + 1)
|
|
}
|
|
return `"${term}" (${count}x)`
|
|
})
|
|
matchDetails.push(`Content matches: ${matchCounts.join(', ')}`)
|
|
}
|
|
}
|
|
|
|
// Check metadata matches
|
|
const metadataMatches = []
|
|
for (const [key, value] of Object.entries(result)) {
|
|
if (key !== 'vector' && key !== 'content' && key !== 'id' && key !== 'title' && key !== 'score') {
|
|
const valueStr = JSON.stringify(value).toLowerCase()
|
|
const termMatches = terms.filter(term => valueStr.includes(term))
|
|
if (termMatches.length > 0) {
|
|
metadataMatches.push(`${key}: "${termMatches.join('", "')}"`)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (metadataMatches.length > 0) {
|
|
matchDetails.push(`Metadata matches: ${metadataMatches.join(', ')}`)
|
|
}
|
|
|
|
// If we have match details, display them
|
|
if (matchDetails.length > 0) {
|
|
matchReason.innerHTML = `<strong>Why this matched:</strong> ${matchDetails.join(' | ')}`
|
|
resultItem.appendChild(matchReason)
|
|
}
|
|
}
|
|
|
|
// 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}`)
|
|
}
|
|
})
|
|
}
|
|
|
|
createVisualGraph(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 createVisualGraph(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')
|
|
.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)')
|
|
|
|
// 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<Article>();
|
|
|
|
// 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<Article>('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>
|