498 lines
14 KiB
HTML
498 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AgentDB - Hyperbolic Hierarchy Visualization</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #2d3561 0%, #c05c7e 100%);
|
|
padding: 20px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
h1 {
|
|
color: #2d3561;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.demo-section {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
h2 {
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 2px solid #2d3561;
|
|
}
|
|
|
|
#canvas {
|
|
width: 100%;
|
|
height: 600px;
|
|
background: #f8f9fa;
|
|
border-radius: 5px;
|
|
display: block;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
button {
|
|
background: #2d3561;
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: background 0.3s;
|
|
}
|
|
|
|
button:hover {
|
|
background: #1f2545;
|
|
}
|
|
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.info-card {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
border-left: 4px solid #2d3561;
|
|
}
|
|
|
|
.info-card h3 {
|
|
color: #2d3561;
|
|
font-size: 14px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.info-card p {
|
|
color: #666;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.legend {
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
footer {
|
|
text-align: center;
|
|
color: white;
|
|
margin-top: 30px;
|
|
opacity: 0.9;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>Hyperbolic Hierarchy Visualization</h1>
|
|
<p class="subtitle">Visualize hierarchical relationships in hyperbolic space using Poincaré ball model</p>
|
|
</header>
|
|
|
|
<div class="demo-section">
|
|
<h2>Interactive Visualization</h2>
|
|
<div class="controls">
|
|
<button id="loadTaxonomy">Load Animal Taxonomy</button>
|
|
<button id="loadOrgChart">Load Organization Chart</button>
|
|
<button id="loadFilesystem">Load File System</button>
|
|
<button id="computeDistances">Compute Hyperbolic Distances</button>
|
|
<button id="findNearest">Find Nearest Neighbors</button>
|
|
</div>
|
|
<canvas id="canvas"></canvas>
|
|
|
|
<div class="info-grid" id="stats">
|
|
<div class="info-card">
|
|
<h3>Total Nodes</h3>
|
|
<p id="nodeCount">0</p>
|
|
</div>
|
|
<div class="info-card">
|
|
<h3>Hierarchy Depth</h3>
|
|
<p id="depth">0</p>
|
|
</div>
|
|
<div class="info-card">
|
|
<h3>Avg Poincaré Distance</h3>
|
|
<p id="avgDistance">0.00</p>
|
|
</div>
|
|
<div class="info-card">
|
|
<h3>Computation Time</h3>
|
|
<p id="computeTime">0ms</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #e74c3c;"></div>
|
|
<span>Root Level</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #3498db;"></div>
|
|
<span>Level 1</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #2ecc71;"></div>
|
|
<span>Level 2</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #f39c12;"></div>
|
|
<span>Level 3+</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="demo-section">
|
|
<h2>About Hyperbolic Space</h2>
|
|
<p style="line-height: 1.8;">
|
|
Hyperbolic space provides a natural way to represent hierarchical structures.
|
|
Unlike Euclidean space, the Poincaré ball model allows exponentially more room
|
|
at the boundary, making it perfect for tree-like structures where the number
|
|
of nodes grows exponentially with depth.
|
|
</p>
|
|
<div class="info-grid" style="margin-top: 20px;">
|
|
<div class="info-card">
|
|
<h3>Poincaré Ball</h3>
|
|
<p style="font-size: 14px; font-weight: normal;">
|
|
All points lie within a unit circle. Distance grows exponentially near the boundary.
|
|
</p>
|
|
</div>
|
|
<div class="info-card">
|
|
<h3>Hierarchy Preservation</h3>
|
|
<p style="font-size: 14px; font-weight: normal;">
|
|
Parent-child relationships maintain consistent distances in hyperbolic space.
|
|
</p>
|
|
</div>
|
|
<div class="info-card">
|
|
<h3>Better Embeddings</h3>
|
|
<p style="font-size: 14px; font-weight: normal;">
|
|
Lower distortion when embedding trees compared to Euclidean space.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<p>AgentDB v2.0 | Hyperbolic Attention powered by WASM</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import {
|
|
AttentionBrowser,
|
|
createAttention
|
|
} from '../../dist/agentdb.browser.js';
|
|
|
|
const canvas = document.getElementById('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
let attention = null;
|
|
let currentHierarchy = null;
|
|
|
|
// Set canvas size
|
|
function resizeCanvas() {
|
|
canvas.width = canvas.clientWidth * window.devicePixelRatio;
|
|
canvas.height = canvas.clientHeight * window.devicePixelRatio;
|
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
}
|
|
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
// Initialize attention
|
|
async function initialize() {
|
|
attention = createAttention({
|
|
dimension: 64,
|
|
curvature: -1.0,
|
|
useWASM: true
|
|
});
|
|
await attention.initialize();
|
|
}
|
|
|
|
// Sample hierarchies
|
|
const hierarchies = {
|
|
taxonomy: {
|
|
name: 'Animal Kingdom',
|
|
children: [
|
|
{
|
|
name: 'Mammals',
|
|
children: [
|
|
{ name: 'Primates', children: [{ name: 'Humans' }, { name: 'Chimpanzees' }] },
|
|
{ name: 'Carnivores', children: [{ name: 'Cats' }, { name: 'Dogs' }] }
|
|
]
|
|
},
|
|
{
|
|
name: 'Birds',
|
|
children: [
|
|
{ name: 'Raptors', children: [{ name: 'Eagles' }, { name: 'Hawks' }] },
|
|
{ name: 'Songbirds', children: [{ name: 'Sparrows' }, { name: 'Robins' }] }
|
|
]
|
|
}
|
|
]
|
|
},
|
|
orgChart: {
|
|
name: 'CEO',
|
|
children: [
|
|
{
|
|
name: 'CTO',
|
|
children: [
|
|
{ name: 'Engineering', children: [{ name: 'Frontend' }, { name: 'Backend' }] },
|
|
{ name: 'DevOps', children: [{ name: 'Infrastructure' }] }
|
|
]
|
|
},
|
|
{
|
|
name: 'CFO',
|
|
children: [
|
|
{ name: 'Accounting', children: [{ name: 'Payroll' }, { name: 'Tax' }] }
|
|
]
|
|
}
|
|
]
|
|
},
|
|
filesystem: {
|
|
name: '/',
|
|
children: [
|
|
{
|
|
name: 'home',
|
|
children: [
|
|
{ name: 'user', children: [{ name: 'documents' }, { name: 'downloads' }] }
|
|
]
|
|
},
|
|
{
|
|
name: 'var',
|
|
children: [
|
|
{ name: 'log', children: [{ name: 'system.log' }] },
|
|
{ name: 'www', children: [{ name: 'html' }] }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
// Convert hierarchy to embeddings
|
|
function hierarchyToEmbeddings(node, depth = 0, angle = 0, angleSpan = Math.PI * 2) {
|
|
const embeddings = [];
|
|
const dim = 64;
|
|
|
|
// Map to Poincaré disk
|
|
const radius = Math.min(0.95, depth * 0.3);
|
|
const x = radius * Math.cos(angle);
|
|
const y = radius * Math.sin(angle);
|
|
|
|
// Create embedding
|
|
const embedding = new Float32Array(dim);
|
|
embedding[0] = x;
|
|
embedding[1] = y;
|
|
for (let i = 2; i < dim; i++) {
|
|
embedding[i] = (Math.random() - 0.5) * 0.1;
|
|
}
|
|
|
|
embeddings.push({
|
|
name: node.name,
|
|
embedding,
|
|
depth,
|
|
x,
|
|
y
|
|
});
|
|
|
|
// Process children
|
|
if (node.children) {
|
|
const childAngleSpan = angleSpan / node.children.length;
|
|
node.children.forEach((child, i) => {
|
|
const childAngle = angle - angleSpan / 2 + childAngleSpan * (i + 0.5);
|
|
embeddings.push(...hierarchyToEmbeddings(child, depth + 1, childAngle, childAngleSpan));
|
|
});
|
|
}
|
|
|
|
return embeddings;
|
|
}
|
|
|
|
// Draw visualization
|
|
function drawVisualization(embeddings) {
|
|
const width = canvas.clientWidth;
|
|
const height = canvas.clientHeight;
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
const radius = Math.min(width, height) * 0.4;
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
// Draw Poincaré disk
|
|
ctx.strokeStyle = '#ddd';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Draw grid
|
|
ctx.strokeStyle = '#eee';
|
|
ctx.lineWidth = 1;
|
|
for (let r = radius / 4; r < radius; r += radius / 4) {
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, r, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Draw nodes
|
|
const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12'];
|
|
embeddings.forEach(node => {
|
|
const px = centerX + node.x * radius;
|
|
const py = centerY + node.y * radius;
|
|
|
|
// Draw node
|
|
ctx.fillStyle = colors[Math.min(node.depth, colors.length - 1)];
|
|
ctx.beginPath();
|
|
ctx.arc(px, py, 8, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Draw label
|
|
ctx.fillStyle = '#333';
|
|
ctx.font = '12px sans-serif';
|
|
ctx.fillText(node.name, px + 12, py + 4);
|
|
});
|
|
|
|
// Update stats
|
|
const depths = embeddings.map(e => e.depth);
|
|
document.getElementById('nodeCount').textContent = embeddings.length;
|
|
document.getElementById('depth').textContent = Math.max(...depths);
|
|
}
|
|
|
|
// Compute hyperbolic distances
|
|
async function computeHyperbolicDistances(embeddings) {
|
|
const start = performance.now();
|
|
|
|
// Create embedding matrix
|
|
const dim = embeddings[0].embedding.length;
|
|
const numNodes = embeddings.length;
|
|
const keys = new Float32Array(numNodes * dim);
|
|
|
|
embeddings.forEach((node, i) => {
|
|
keys.set(node.embedding, i * dim);
|
|
});
|
|
|
|
// Compute distances for each node
|
|
let totalDistance = 0;
|
|
for (const node of embeddings) {
|
|
const similarities = await attention.hyperbolicAttention(node.embedding, keys);
|
|
const distances = Array.from(similarities).map(s => 1 - s);
|
|
totalDistance += distances.reduce((a, b) => a + b) / distances.length;
|
|
}
|
|
|
|
const avgDistance = totalDistance / embeddings.length;
|
|
const duration = performance.now() - start;
|
|
|
|
document.getElementById('avgDistance').textContent = avgDistance.toFixed(4);
|
|
document.getElementById('computeTime').textContent = `${duration.toFixed(2)}ms`;
|
|
|
|
return avgDistance;
|
|
}
|
|
|
|
// Load hierarchy
|
|
async function loadHierarchy(type) {
|
|
const hierarchy = hierarchies[type];
|
|
currentHierarchy = hierarchyToEmbeddings(hierarchy);
|
|
drawVisualization(currentHierarchy);
|
|
}
|
|
|
|
// Find nearest neighbors
|
|
async function findNearestNeighbors() {
|
|
if (!currentHierarchy) {
|
|
alert('Please load a hierarchy first');
|
|
return;
|
|
}
|
|
|
|
// Pick a random node
|
|
const queryNode = currentHierarchy[Math.floor(Math.random() * currentHierarchy.length)];
|
|
|
|
// Create keys from all nodes
|
|
const dim = queryNode.embedding.length;
|
|
const keys = new Float32Array(currentHierarchy.length * dim);
|
|
currentHierarchy.forEach((node, i) => {
|
|
keys.set(node.embedding, i * dim);
|
|
});
|
|
|
|
// Compute similarities
|
|
const similarities = await attention.hyperbolicAttention(queryNode.embedding, keys);
|
|
|
|
// Find top 5
|
|
const results = currentHierarchy
|
|
.map((node, i) => ({ node, similarity: similarities[i] }))
|
|
.sort((a, b) => b.similarity - a.similarity)
|
|
.slice(0, 5);
|
|
|
|
alert(
|
|
`Nearest neighbors to "${queryNode.name}":\n\n` +
|
|
results.map((r, i) =>
|
|
`${i + 1}. ${r.node.name} (similarity: ${r.similarity.toFixed(4)})`
|
|
).join('\n')
|
|
);
|
|
}
|
|
|
|
// Event listeners
|
|
document.getElementById('loadTaxonomy').addEventListener('click', () => loadHierarchy('taxonomy'));
|
|
document.getElementById('loadOrgChart').addEventListener('click', () => loadHierarchy('orgChart'));
|
|
document.getElementById('loadFilesystem').addEventListener('click', () => loadHierarchy('filesystem'));
|
|
document.getElementById('computeDistances').addEventListener('click', () => {
|
|
if (currentHierarchy) {
|
|
computeHyperbolicDistances(currentHierarchy);
|
|
} else {
|
|
alert('Please load a hierarchy first');
|
|
}
|
|
});
|
|
document.getElementById('findNearest').addEventListener('click', findNearestNeighbors);
|
|
|
|
// Initialize
|
|
initialize().then(() => {
|
|
loadHierarchy('taxonomy');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|