// Global state
let allGraphData = null;
let cy = null;
let debugPanes = [];
let debugPaneCounter = 0;
let currentAgentId = null; // Global agent context
let dataGraphs = {
world: null,
agent: null,
opinions: null
};
let dataCache = {
world: null,
agent: null,
opinions: null
};
let currentDataSubTab = 'world';
// Main tab switching (Data, Debug, Think, Benchmark)
window.switchMainTab = function(tabName) {
// Remove active class from all main tabs and buttons
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
// Activate the selected tab
const tabElement = document.getElementById(`${tabName}-tab`);
if (tabElement) {
tabElement.classList.add('active');
}
// Find and activate the corresponding button
const buttons = document.querySelectorAll('.tab-button');
buttons.forEach(btn => {
const btnText = btn.textContent.toLowerCase();
if (
(tabName === 'data' && btnText.includes('data')) ||
(tabName === 'debug' && btnText.includes('debug')) ||
(tabName === 'think' && btnText.includes('think')) ||
(tabName === 'benchmark' && btnText.includes('benchmark'))
) {
btn.classList.add('active');
}
});
// Tab-specific logic
if (tabName === 'data') {
// Resize current data graph if exists
const factType = currentDataSubTab;
if (dataGraphs[factType]) {
setTimeout(() => dataGraphs[factType].resize(), 10);
}
} else if (tabName === 'debug') {
if (debugPanes.length === 0) {
addDebugPane();
}
debugPanes.forEach(pane => {
if (pane.cy) {
pane.cy.resize();
}
});
} else if (tabName === 'think') {
// Think tab uses global agent selector
}
}
// Data subtab switching (World, Agent, Opinions)
window.switchDataSubTab = function(subTab) {
currentDataSubTab = subTab;
// Remove active class from all subtab buttons and content
document.querySelectorAll('.data-sub-tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.data-subtab-content').forEach(content => {
content.classList.remove('active');
});
// Activate selected subtab
const buttons = document.querySelectorAll('.data-sub-tab-button');
buttons.forEach(btn => {
if (btn.textContent.toLowerCase().includes(subTab.toLowerCase())) {
btn.classList.add('active');
}
});
const subtabElement = document.getElementById(`${subTab}-subtab`);
if (subtabElement) {
subtabElement.classList.add('active');
}
// Resize graph if exists
if (dataGraphs[subTab]) {
setTimeout(() => dataGraphs[subTab].resize(), 10);
}
}
// Switch between graph and table view for a fact type
window.switchDataView = function(factType, viewType) {
const graphView = document.getElementById(`${factType}-graph-view`);
const tableView = document.getElementById(`${factType}-table-view`);
const buttons = document.querySelectorAll(`#${factType}-subtab .view-toggle-button`);
// Update button states
buttons.forEach(btn => {
btn.classList.remove('active');
if ((viewType === 'graph' && btn.textContent.includes('Graph')) ||
(viewType === 'table' && btn.textContent.includes('Table'))) {
btn.classList.add('active');
}
});
// Show/hide views
if (viewType === 'graph') {
graphView.style.display = 'block';
tableView.style.display = 'none';
if (dataGraphs[factType]) {
setTimeout(() => dataGraphs[factType].resize(), 10);
}
} else {
graphView.style.display = 'none';
tableView.style.display = 'block';
}
}
// Load data for a specific fact type
window.loadDataView = async function(factType) {
if (!currentAgentId) {
alert('Please select an agent first');
return;
}
try {
// Build URL with agent filter and fact_type filter
let url = `api/graph?agent_id=${encodeURIComponent(currentAgentId)}`;
if (factType !== 'all') {
url += `&fact_type=${factType}`;
}
const response = await fetch(url);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || `HTTP ${response.status}`);
}
const data = await response.json();
// Validate response structure
if (!data || !data.nodes || !data.edges) {
throw new Error('Invalid response format from server');
}
// Cache the data
dataCache[factType] = data;
// Update table
updateDataTable(factType, data);
// Update graph if in graph view
const graphView = document.getElementById(`${factType}-graph-view`);
if (graphView && graphView.style.display !== 'none') {
reloadDataGraph(factType);
}
return data;
} catch (e) {
console.error(`Error loading ${factType} data:`, e);
alert(`Error loading ${factType} data: ` + e.message);
}
}
// Reload graph for a specific fact type
window.reloadDataGraph = function(factType) {
const data = dataCache[factType];
if (!data) return;
const nodeLimit = parseInt(document.getElementById(`${factType}-node-limit`).value) || 50;
const layoutName = document.getElementById(`${factType}-layout-select`).value;
// Filter nodes to limit
const limitedNodes = data.nodes.slice(0, nodeLimit);
const nodeIds = new Set(limitedNodes.map(n => n.data.id));
// Filter edges to only include those between visible nodes
const limitedEdges = data.edges.filter(e =>
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
);
// Update count display
document.getElementById(`${factType}-node-count`).textContent =
`Showing ${limitedNodes.length} of ${data.nodes.length} nodes`;
// Destroy existing graph if any
if (dataGraphs[factType]) {
dataGraphs[factType].destroy();
}
// Layout configurations
const layouts = {
'circle': {
name: 'circle',
animate: false,
radius: 300,
spacingFactor: 1.5
},
'grid': {
name: 'grid',
animate: false,
rows: Math.ceil(Math.sqrt(limitedNodes.length)),
cols: Math.ceil(Math.sqrt(limitedNodes.length)),
spacingFactor: 2
},
'cose': {
name: 'cose',
animate: false,
nodeRepulsion: 15000,
idealEdgeLength: 150,
edgeElasticity: 100,
nestingFactor: 1.2,
gravity: 1,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0
}
};
// Initialize Cytoscape
dataGraphs[factType] = cytoscape({
container: document.getElementById(`${factType}-cy`),
elements: [
...limitedNodes.map(n => ({ data: n.data })),
...limitedEdges.map(e => ({ data: e.data }))
],
style: [
{
selector: 'node',
style: {
'background-color': 'data(color)',
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '10px',
'font-weight': 'bold',
'text-wrap': 'wrap',
'text-max-width': '100px',
'width': 40,
'height': 40,
'border-width': 2,
'border-color': '#333'
}
},
{
selector: 'edge',
style: {
'width': 1,
'line-color': 'data(color)',
'line-style': 'data(lineStyle)',
'target-arrow-shape': 'triangle',
'target-arrow-color': 'data(color)',
'curve-style': 'bezier',
'opacity': 0.7
}
},
{
selector: 'node:selected',
style: {
'border-width': 4,
'border-color': '#000'
}
}
],
layout: layouts[layoutName] || layouts['circle']
});
// Add tooltip on hover
let tooltip = null;
dataGraphs[factType].on('mouseover', 'node', function(evt) {
const node = evt.target;
const data = node.data();
const renderedPosition = node.renderedPosition();
tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = `
Text: ${data.text} Context: ${data.context} Date: ${data.date} Entities: ${data.entities}
`;
tooltip.style.left = renderedPosition.x + 20 + 'px';
tooltip.style.top = renderedPosition.y + 'px';
document.body.appendChild(tooltip);
});
dataGraphs[factType].on('mouseout', 'node', function(evt) {
if (tooltip) {
tooltip.remove();
tooltip = null;
}
});
}
// Update table for a specific fact type
function updateDataTable(factType, data) {
if (!data) return;
const tbody = document.getElementById(`${factType}-table-body`);
const countSpan = document.getElementById(`${factType}-table-count`);
if (countSpan) {
countSpan.textContent = `(${data.total_units})`;
}
if (tbody) {
tbody.innerHTML = data.table_rows.map(row => `
${row.id}
${row.text}
${row.context}
${row.date}
${row.entities}
`).join('');
}
// Setup table filter
const filterInput = document.getElementById(`${factType}-table-filter`);
if (filterInput) {
filterInput.removeEventListener('input', filterInput._filterHandler);
filterInput._filterHandler = function() {
const filterValue = this.value.toLowerCase();
const rows = document.querySelectorAll(`#${factType}-table-body tr`);
rows.forEach(row => {
const text = row.textContent.toLowerCase();
if (text.includes(filterValue)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
};
filterInput.addEventListener('input', filterInput._filterHandler);
}
}
// Load data from API (old function - kept for backward compatibility)
async function loadGraphData() {
try {
// Require agent selection
if (!currentAgentId) {
alert('Please select an agent first');
return;
}
// Build URL with agent filter
let url = `api/graph?agent_id=${encodeURIComponent(currentAgentId)}`;
const response = await fetch(url);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || `HTTP ${response.status}`);
}
allGraphData = await response.json();
// Validate response structure
if (!allGraphData || !allGraphData.nodes || !allGraphData.edges) {
throw new Error('Invalid response format from server');
}
// Update table
updateTable();
// Initialize graph
if (document.getElementById('graph-tab').classList.contains('active')) {
reloadGraph();
}
return allGraphData;
} catch (e) {
console.error('Error loading graph data:', e);
alert('Error loading graph data: ' + e.message);
// Show error in the UI
const cyDiv = document.getElementById('cy');
if (cyDiv) {
cyDiv.innerHTML = `
Failed to load graph data
${e.message}
`;
}
}
}
// Refresh data from server
function refreshData() {
loadGraphData();
}
// Update table with current data
function updateTable() {
if (!allGraphData) return;
const tbody = document.getElementById('table-body');
const countSpan = document.getElementById('table-count');
countSpan.textContent = `(${allGraphData.total_units})`;
tbody.innerHTML = allGraphData.table_rows.map(row => `
`;
tableDiv.innerHTML = html;
}
function renderDecisionLog(paneId, trace) {
const logDiv = document.getElementById(`decision-log-${paneId}`);
if (!logDiv || !trace) return;
// Group visits by step
const stepGroups = {};
trace.visits.forEach(visit => {
if (!stepGroups[visit.step]) {
stepGroups[visit.step] = [];
}
stepGroups[visit.step].push(visit);
});
// Build HTML for decision log
let html = `
Search Execution Trace
Query: "${trace.query.query_text}"
This log shows the step-by-step decision process of the spreading activation search algorithm.
The search starts from entry points (semantically similar memories) and spreads through connected memories,
following temporal, semantic, and entity links to find relevant results.
đ¯ Finding Entry Points: Searching for memories semantically similar to the query.
These are the starting points for spreading activation.
`;
} else {
html += `
đ Spreading Activation: Following links from previously activated memories.
The algorithm explores connected memories and calculates their relevance.