knowledge-base/frontend/graph.html
2025-09-04 17:07:49 +05:30

545 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Graph Visualization</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
height: 100vh;
overflow: hidden;
}
.container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: white;
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 24px;
color: #333;
margin: 0;
}
.stats {
display: flex;
gap: 20px;
font-size: 14px;
color: #666;
}
.stat-item {
background: #f8f9fa;
padding: 5px 10px;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.controls {
display: flex;
gap: 10px;
align-items: center;
}
.control-btn {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
}
.control-btn:hover {
background: #0056b3;
}
.control-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.graph-container {
flex: 1;
background: white;
position: relative;
overflow: hidden;
}
.graph-svg {
width: 100%;
height: 100%;
cursor: grab;
}
.graph-svg:active {
cursor: grabbing;
}
.node {
stroke: #333;
stroke-width: 1.5px;
cursor: pointer;
}
.node:hover {
stroke-width: 3px;
stroke: #007bff;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1.5px;
}
.link:hover {
stroke: #007bff;
stroke-opacity: 1;
stroke-width: 3px;
}
.node-label {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11px;
fill: #333;
text-anchor: middle;
pointer-events: none;
user-select: none;
}
.relationship-label {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 9px;
fill: #666;
text-anchor: middle;
pointer-events: none;
user-select: none;
opacity: 0;
transition: opacity 0.3s;
}
.status-indicator {
position: absolute;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-indicator.loading {
background: #ffc107;
color: #333;
}
.status-indicator.error {
background: #dc3545;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
color: #666;
z-index: 1000;
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
z-index: 1001;
opacity: 0;
transition: opacity 0.3s;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Memory Graph Visualization</h1>
<div class="stats">
<div class="stat-item">
Memories: <span id="totalMemories">0</span>
</div>
<div class="stat-item">
Relationships: <span id="totalRelationships">0</span>
</div>
<div class="stat-item">
Entities: <span id="totalEntities">0</span>
</div>
</div>
<div class="controls">
<button class="control-btn" id="pauseBtn">Pause</button>
<button class="control-btn" id="refreshBtn">Refresh</button>
<button class="control-btn" id="resetBtn">Reset View</button>
</div>
</div>
<div class="graph-container">
<div class="loading-overlay" id="loadingOverlay">
Loading graph data...
</div>
<div class="status-indicator" id="statusIndicator">Connecting...</div>
<div class="tooltip" id="tooltip"></div>
<svg class="graph-svg" id="graphSvg"></svg>
</div>
</div>
<script>
// Configuration
const API_BASE = 'http://localhost:8000';
const USER_ID = 'pratik';
const POLL_INTERVAL = 30000; // 30 seconds
// Global variables
let simulation;
let svg, g;
let nodes = [];
let links = [];
let isPolling = true;
let pollTimer;
let currentData = null;
// DOM elements
const loadingOverlay = document.getElementById('loadingOverlay');
const statusIndicator = document.getElementById('statusIndicator');
const totalMemories = document.getElementById('totalMemories');
const totalRelationships = document.getElementById('totalRelationships');
const totalEntities = document.getElementById('totalEntities');
const pauseBtn = document.getElementById('pauseBtn');
const refreshBtn = document.getElementById('refreshBtn');
const resetBtn = document.getElementById('resetBtn');
const tooltip = document.getElementById('tooltip');
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initializeGraph();
setupEventListeners();
loadGraphData();
startPolling();
});
function setupEventListeners() {
pauseBtn.addEventListener('click', togglePolling);
refreshBtn.addEventListener('click', loadGraphData);
resetBtn.addEventListener('click', resetView);
}
function initializeGraph() {
const container = document.querySelector('.graph-container');
const width = container.clientWidth;
const height = container.clientHeight;
svg = d3.select('#graphSvg')
.attr('width', width)
.attr('height', height);
// Create main group for zoom/pan
g = svg.append('g');
// Setup zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Initialize force simulation
simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(80))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(20));
}
async function loadGraphData() {
try {
showStatus('Loading...', 'loading');
const response = await fetch(`${API_BASE}/graph/relationships/${USER_ID}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
currentData = data;
// Update stats display
totalMemories.textContent = data.total_memories || 0;
totalRelationships.textContent = data.total_relationships || 0;
totalEntities.textContent = data.entities ? data.entities.length : 0;
// Process data for D3
processDataAndUpdateGraph(data);
showStatus('Connected', 'success');
hideLoading();
} catch (error) {
console.error('Error loading graph data:', error);
showStatus('Error: ' + error.message, 'error');
hideLoading();
}
}
function processDataAndUpdateGraph(data) {
// Extract unique entities from relationships and entities array
const entitySet = new Set();
// Add entities from the entities array
if (data.entities) {
data.entities.forEach(entity => {
entitySet.add(entity.name);
});
}
// Add entities from relationships (in case some are missing from entities array)
if (data.relationships) {
data.relationships.forEach(rel => {
entitySet.add(rel.source);
entitySet.add(rel.target);
});
}
// Create nodes array
nodes = Array.from(entitySet).map(name => ({
id: name,
name: name
}));
// Create links array from relationships
links = data.relationships ? data.relationships.map(rel => ({
source: rel.source,
target: rel.target,
relationship: rel.relationship
})) : [];
updateGraph();
}
function updateGraph() {
// Clear existing elements
g.selectAll('.link').remove();
g.selectAll('.node').remove();
g.selectAll('.node-label').remove();
// Create links
const link = g.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link')
.on('mouseover', showLinkTooltip)
.on('mouseout', hideTooltip);
// Create nodes
const node = g.selectAll('.node')
.data(nodes)
.enter().append('circle')
.attr('class', 'node')
.attr('r', 8)
.attr('fill', '#69b3ff')
.on('mouseover', showNodeTooltip)
.on('mouseout', hideTooltip)
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded));
// Create node labels
const label = g.selectAll('.node-label')
.data(nodes)
.enter().append('text')
.attr('class', 'node-label')
.text(d => d.name)
.attr('dy', -12);
// Update simulation
simulation.nodes(nodes);
simulation.force('link').links(links);
simulation.alpha(1).restart();
// Update positions on each tick
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
label
.attr('x', d => d.x)
.attr('y', d => d.y);
});
}
function showNodeTooltip(event, d) {
const connections = links.filter(link =>
link.source.id === d.id || link.target.id === d.id
).length;
tooltip.innerHTML = `
<strong>${d.name}</strong><br>
Connections: ${connections}
`;
tooltip.style.opacity = 1;
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY - 10) + 'px';
}
function showLinkTooltip(event, d) {
tooltip.innerHTML = `
<strong>${d.source.name}</strong><br>
<em>${d.relationship}</em><br>
<strong>${d.target.name}</strong>
`;
tooltip.style.opacity = 1;
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY - 10) + 'px';
}
function hideTooltip() {
tooltip.style.opacity = 0;
}
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 startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
if (isPolling) {
loadGraphData();
}
}, POLL_INTERVAL);
}
function togglePolling() {
isPolling = !isPolling;
pauseBtn.textContent = isPolling ? 'Pause' : 'Resume';
if (isPolling) {
startPolling();
showStatus('Connected', 'success');
} else {
clearInterval(pollTimer);
showStatus('Paused', 'loading');
}
}
function resetView() {
const container = document.querySelector('.graph-container');
const width = container.clientWidth;
const height = container.clientHeight;
svg.transition()
.duration(750)
.call(
d3.zoom().transform,
d3.zoomIdentity.translate(0, 0).scale(1)
);
// Restart simulation to recenter
if (simulation) {
simulation.force('center', d3.forceCenter(width / 2, height / 2));
simulation.alpha(1).restart();
}
}
function showStatus(message, type) {
statusIndicator.textContent = message;
statusIndicator.className = 'status-indicator';
if (type) {
statusIndicator.classList.add(type);
}
}
function hideLoading() {
loadingOverlay.style.display = 'none';
}
// Handle window resize
window.addEventListener('resize', () => {
const container = document.querySelector('.graph-container');
const width = container.clientWidth;
const height = container.clientHeight;
svg.attr('width', width).attr('height', height);
if (simulation) {
simulation.force('center', d3.forceCenter(width / 2, height / 2));
simulation.alpha(1).restart();
}
});
// Handle page visibility change (pause polling when tab is hidden)
document.addEventListener('visibilitychange', () => {
if (document.hidden && isPolling) {
clearInterval(pollTimer);
} else if (!document.hidden && isPolling) {
startPolling();
}
});
</script>
</body>
</html>