// Global state
let allGraphData = null;
let cy = null;
let debugPanes = [];
let debugPaneCounter = 0;
// Load data from API
async function loadGraphData() {
try {
const response = await fetch('/api/graph');
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}
Retry
`;
}
}
}
// 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 => `
${row.id}
${row.text}
${row.context}
${row.date}
${row.entities}
`).join('');
}
// Initialize graph with filtering
function initGraph(nodeLimit, layoutName) {
if (!allGraphData) return;
// Filter nodes to limit
const limitedNodes = allGraphData.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 = allGraphData.edges.filter(e =>
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
);
// Update count display
document.getElementById('node-count').textContent =
`Showing ${limitedNodes.length} of ${allGraphData.nodes.length} nodes`;
// Destroy existing graph if any
if (cy) {
cy.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
cy = cytoscape({
container: document.getElementById('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']
});
// Simple tooltip on hover
let tooltip = null;
cy.on('mouseover', 'node', function(evt) {
const node = evt.target;
const data = node.data();
const renderedPosition = node.renderedPosition();
// Create tooltip
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);
});
cy.on('mouseout', 'node', function(evt) {
if (tooltip) {
tooltip.remove();
tooltip = null;
}
});
}
// Reload graph with current settings
function reloadGraph() {
const nodeLimit = parseInt(document.getElementById('node-limit').value) || 50;
const layoutName = document.getElementById('layout-select').value;
initGraph(nodeLimit, layoutName);
}
// Load available agents
let agentsLoaded = false;
async function loadAgents() {
if (agentsLoaded) return;
try {
const response = await fetch('/api/agents');
const data = await response.json();
const select = document.getElementById('search-agent-id');
select.innerHTML = '';
if (data.agents && data.agents.length > 0) {
data.agents.forEach(agent => {
const option = document.createElement('option');
option.value = agent;
option.textContent = agent;
select.appendChild(option);
});
agentsLoaded = true;
} else {
select.innerHTML = 'default ';
}
} catch (e) {
console.error('Error loading agents:', e);
const select = document.getElementById('search-agent-id');
select.innerHTML = 'default ';
}
}
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
if (tabName === 'graph') {
document.getElementById('graph-tab').classList.add('active');
document.querySelectorAll('.tab-button')[0].classList.add('active');
if (cy) cy.resize();
} else if (tabName === 'table') {
document.getElementById('table-tab').classList.add('active');
document.querySelectorAll('.tab-button')[1].classList.add('active');
} else if (tabName === 'debug') {
document.getElementById('debug-tab').classList.add('active');
document.querySelectorAll('.tab-button')[2].classList.add('active');
// Initialize with one pane if empty
if (debugPanes.length === 0) {
addDebugPane();
}
// Resize all debug graphs
debugPanes.forEach(pane => {
if (pane.cy) {
pane.cy.resize();
}
});
} else if (tabName === 'locomo') {
document.getElementById('locomo-tab').classList.add('active');
document.querySelectorAll('.tab-button')[3].classList.add('active');
}
}
// Debug pane management
function addDebugPane() {
const paneId = debugPaneCounter++;
const container = document.getElementById('debug-panes-container');
const paneDiv = document.createElement('div');
paneDiv.className = 'debug-pane';
paneDiv.id = `debug-pane-${paneId}`;
paneDiv.innerHTML = `
Ready to search
Graph View
Decision Log
Results Table
Show pruned nodes
Highlight top result path
`;
container.appendChild(paneDiv);
// Add event listeners for view mode toggle
document.getElementById(`debug-mode-graph-${paneId}`).addEventListener('change', function() {
if (this.checked) {
document.getElementById(`debug-cy-${paneId}`).style.display = 'block';
document.getElementById(`decision-log-${paneId}`).style.display = 'none';
document.getElementById(`results-table-${paneId}`).style.display = 'none';
document.getElementById(`graph-controls-${paneId}`).style.display = 'inline';
const pane = debugPanes.find(p => p.id === paneId);
if (pane && pane.cy) {
setTimeout(() => pane.cy.resize(), 10);
}
}
});
document.getElementById(`debug-mode-log-${paneId}`).addEventListener('change', function() {
if (this.checked) {
document.getElementById(`debug-cy-${paneId}`).style.display = 'none';
document.getElementById(`decision-log-${paneId}`).style.display = 'block';
document.getElementById(`results-table-${paneId}`).style.display = 'none';
document.getElementById(`graph-controls-${paneId}`).style.display = 'none';
}
});
document.getElementById(`debug-mode-table-${paneId}`).addEventListener('change', function() {
if (this.checked) {
document.getElementById(`debug-cy-${paneId}`).style.display = 'none';
document.getElementById(`decision-log-${paneId}`).style.display = 'none';
document.getElementById(`results-table-${paneId}`).style.display = 'block';
document.getElementById(`graph-controls-${paneId}`).style.display = 'none';
}
});
// Add event listeners for graph controls
document.getElementById(`debug-show-pruned-${paneId}`).addEventListener('change', function() {
const pane = debugPanes.find(p => p.id === paneId);
if (pane && pane.trace) {
visualizeTrace(paneId, pane.trace);
}
});
document.getElementById(`debug-highlight-path-${paneId}`).addEventListener('change', function() {
const pane = debugPanes.find(p => p.id === paneId);
if (pane && pane.trace) {
visualizeTrace(paneId, pane.trace);
}
});
// Add search input listener for finding nodes
document.getElementById(`graph-search-${paneId}`).addEventListener('input', function(e) {
const pane = debugPanes.find(p => p.id === paneId);
if (pane && pane.cy) {
highlightMatchingNodes(paneId, e.target.value);
}
});
debugPanes.push({
id: paneId,
element: paneDiv,
cy: null,
trace: null
});
// Load agents for this pane after DOM is ready
setTimeout(() => loadAgentsForPane(paneId), 10);
}
async function loadAgentsForPane(paneId) {
const select = document.getElementById(`search-agent-${paneId}`);
if (!select) {
console.error(`Could not find select element for pane ${paneId}`);
return;
}
try {
const response = await fetch('/api/agents');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
select.innerHTML = '';
if (data.agents && data.agents.length > 0) {
data.agents.forEach(agent => {
const option = document.createElement('option');
option.value = agent;
option.textContent = agent;
select.appendChild(option);
});
} else {
select.innerHTML = 'default ';
}
} catch (e) {
console.error('Error loading agents for pane:', paneId, e);
select.innerHTML = 'default ';
}
}
window.runSearchInPane = async function(paneId) {
const pane = debugPanes.find(p => p.id === paneId);
if (!pane) return;
const query = document.getElementById(`search-query-${paneId}`).value;
const agentId = document.getElementById(`search-agent-${paneId}`).value;
const thinkingBudget = parseInt(document.getElementById(`search-budget-${paneId}`).value);
const topK = parseInt(document.getElementById(`search-top-k-${paneId}`).value);
const mmrLambda = parseFloat(document.getElementById(`search-mmr-lambda-${paneId}`).value);
const statusBar = document.getElementById(`debug-status-${paneId}`);
if (!query) {
alert('Please enter a query');
return;
}
try {
statusBar.innerHTML = 'đ Searching... ';
const response = await fetch('/api/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: query,
agent_id: agentId,
thinking_budget: thinkingBudget,
top_k: topK,
mmr_lambda: mmrLambda,
trace: true
})
});
const data = await response.json();
if (data.detail) {
statusBar.innerHTML = `â Error: ${data.detail} `;
return;
}
// Update status bar with stats
const summary = data.trace.summary;
statusBar.innerHTML = `
â Search complete
|
Nodes visited: ${summary.total_nodes_visited}
|
Entry points: ${summary.entry_points_found}
|
Budget used: ${summary.budget_used} / ${summary.budget_used + summary.budget_remaining}
|
Results: ${summary.results_returned}
|
Duration: ${summary.total_duration_seconds.toFixed(2)}s
`;
// Visualize the trace and results
pane.trace = data.trace;
pane.results = data.results;
visualizeTrace(paneId, data.trace);
renderDecisionLog(paneId, data.trace);
renderResultsTable(paneId, data.results, data.trace);
} catch (e) {
statusBar.innerHTML = `â Error: ${e.message} `;
console.error('Error running search:', e);
}
}
window.removeSpecificDebugPane = function(paneId) {
const paneIndex = debugPanes.findIndex(p => p.id === paneId);
if (paneIndex === -1) return;
const pane = debugPanes[paneIndex];
if (pane.cy) {
pane.cy.destroy();
}
pane.element.remove();
debugPanes.splice(paneIndex, 1);
}
function removeDebugPane() {
if (debugPanes.length === 0) return;
const pane = debugPanes.pop();
if (pane.cy) {
pane.cy.destroy();
}
pane.element.remove();
}
function renderResultsTable(paneId, results, trace) {
const tableDiv = document.getElementById(`results-table-${paneId}`);
if (!tableDiv) return;
if (!results || results.length === 0) {
tableDiv.innerHTML = 'No results returned
';
return;
}
// Check if MMR was used
const mmrUsed = results.some(r => r.mmr_score !== null && r.mmr_score !== undefined);
let html = `
Search Results (${results.length} memories)
Query: "${trace.query.query_text}"
${mmrUsed ? `
MMR Diversification Active:
đ¯ = Diversified pick (selected for variety) |
Orange background = Rank changed by MMR |
Orig Rank shows position before MMR reranking
` : ''}
Rank
Orig Rank
Text
Context
Date
Final Score
Activation
Similarity
Recency
Frequency
MMR Score
MMR Rel
MMR Sim
`;
results.forEach((result, idx) => {
// Find corresponding visit in trace
const visit = trace.visits.find(v => v.node_id === result.id);
// Get scores from visit weights
const finalScore = visit ? visit.weights.final_weight : (result.score || 0);
const activation = visit ? visit.weights.activation : 0;
const similarity = visit ? visit.weights.semantic_similarity : 0;
const recency = visit ? (visit.weights.recency || 0) : 0;
const frequency = visit ? (visit.weights.frequency || 0) : 0;
// Get MMR information
const originalRank = result.original_rank || (idx + 1);
const mmrScore = result.mmr_score;
const mmrRelevance = result.mmr_relevance;
const mmrMaxSim = result.mmr_max_similarity;
const isDiversified = result.mmr_diversified || false;
// Highlight diversified results
const rowBg = isDiversified ? '#fff3e0' : 'white';
const rankDisplay = originalRank !== (idx + 1) ? `â${originalRank} ` : originalRank;
html += `
#${idx + 1}${isDiversified ? ' đ¯' : ''}
${rankDisplay}
${result.text}
${result.context || 'N/A'}
${result.event_date ? new Date(result.event_date).toLocaleDateString() : 'N/A'}
${finalScore.toFixed(4)}
${activation.toFixed(4)}
${similarity.toFixed(4)}
${recency.toFixed(4)}
${frequency.toFixed(4)}
${mmrScore !== null && mmrScore !== undefined ? mmrScore.toFixed(4) : '-'}
${mmrRelevance !== null && mmrRelevance !== undefined ? mmrRelevance.toFixed(4) : '-'}
${mmrMaxSim !== null && mmrMaxSim !== undefined ? mmrMaxSim.toFixed(4) : '-'}
`;
});
html += `
`;
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 = `
`;
const maxStep = Math.max(...Object.keys(stepGroups).map(k => parseInt(k)));
for (let step = 0; step <= maxStep; step++) {
const visits = stepGroups[step] || [];
if (visits.length === 0) continue;
html += ``;
html += ``;
if (step === 0) {
html += `
đ¯ 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.
`;
}
visits.forEach((visit, idx) => {
const isEntry = visit.is_entry_point;
const isResult = visit.final_rank !== null;
const hasParent = visit.parent_node_id !== null;
let cardClass = 'log-card';
if (isEntry) cardClass += ' log-card-entry';
if (isResult) cardClass += ' log-card-result';
html += `
`;
// Header
if (isEntry) {
html += ``;
} else if (hasParent) {
const linkTypeIcon = {
'temporal': 'âąī¸',
'semantic': 'đ',
'entity': 'đ¤'
};
const icon = linkTypeIcon[visit.link_type] || 'âĄī¸';
html += ``;
}
// Memory text
html += `
"${visit.text}"
`;
// Decision details
html += `
`;
if (isEntry) {
html += `
Why selected:
Semantic similarity to query = ${visit.weights.semantic_similarity.toFixed(3)}
`;
} else if (hasParent) {
html += `
Activated from:
Node ${visit.parent_node_id.substring(0, 8)}... via ${visit.link_type} link
Link weight:
${visit.link_weight.toFixed(3)}
`;
}
html += `
Activation:
${visit.weights.activation.toFixed(3)}
âšī¸
Semantic similarity:
${visit.weights.semantic_similarity.toFixed(3)}
âšī¸
Final weight:
${visit.weights.final_weight.toFixed(3)}
âšī¸
`;
html += `
`; // log-details
html += `
`; // log-card
});
html += `
`; // log-step
}
// Add pruned nodes section if any
if (trace.pruned && trace.pruned.length > 0) {
html += ``;
html += ``;
html += `
âī¸ Budget Limit Reached: These nodes were not explored to conserve computational resources.
`;
trace.pruned.forEach(pruned => {
html += `
`;
html += ``;
html += `
`;
html += `
Reason:
${pruned.reason}
`;
html += `
Activation:
${pruned.activation.toFixed(3)}
`;
html += `
`;
});
html += `
`;
}
logDiv.innerHTML = html;
}
function visualizeTrace(paneId, trace) {
const pane = debugPanes.find(p => p.id === paneId);
if (!pane || !trace) return;
const cyDiv = document.getElementById(`debug-cy-${paneId}`);
if (!cyDiv) return;
const showPruned = document.getElementById(`debug-show-pruned-${paneId}`).checked;
const highlightPath = document.getElementById(`debug-highlight-path-${paneId}`).checked;
try {
// Build graph from trace
const nodes = [];
const edges = [];
const visitedNodeIds = new Set();
// Add all visited nodes
trace.visits.forEach((visit, idx) => {
visitedNodeIds.add(visit.node_id);
// Determine node color based on properties
let color = '#90caf9'; // default
if (visit.is_entry_point) {
color = '#66bb6a'; // green for entry points
} else if (visit.final_rank !== null) {
color = '#ffd54f'; // yellow for results
}
nodes.push({
data: {
id: visit.node_id,
label: `${visit.text.substring(0, 30)}...`,
text: visit.text,
step: visit.step,
rank: visit.final_rank,
activation: visit.weights.activation.toFixed(3),
similarity: visit.weights.semantic_similarity.toFixed(3),
finalWeight: visit.weights.final_weight.toFixed(3),
isEntry: visit.is_entry_point,
color: color
}
});
// Add edge from parent if exists
if (visit.parent_node_id) {
let edgeColor = '#999';
if (visit.link_type === 'temporal') edgeColor = '#00bcd4';
else if (visit.link_type === 'semantic') edgeColor = '#ff69b4';
else if (visit.link_type === 'entity') edgeColor = '#ffd700';
edges.push({
data: {
id: `${visit.parent_node_id}-${visit.node_id}`,
source: visit.parent_node_id,
target: visit.node_id,
linkType: visit.link_type,
linkWeight: visit.link_weight,
color: edgeColor
}
});
}
});
// Add pruned nodes if requested
if (showPruned && trace.pruned) {
trace.pruned.forEach(pruned => {
if (!visitedNodeIds.has(pruned.node_id)) {
nodes.push({
data: {
id: pruned.node_id,
label: 'Pruned',
text: `Pruned: ${pruned.reason}`,
activation: pruned.activation.toFixed(3),
color: '#ef5350' // red for pruned
}
});
}
});
}
// Destroy existing graph
if (pane.cy) {
pane.cy.destroy();
}
// Add legend/explanation to the graph view
const existingLegend = cyDiv.querySelector('.search-graph-legend');
if (existingLegend) {
existingLegend.remove();
}
const legend = document.createElement('div');
legend.className = 'search-graph-legend';
legend.innerHTML = `
đ Graph Legend
Node Colors:
Entry points - semantically similar to query
Results - returned as answers
Visited - explored but not in results
Link Types:
Temporal - memories close in time
Semantic - similar content/meaning
Entity - same person/place/thing
Layout: Rows represent search depth from entry points
`;
cyDiv.appendChild(legend);
// Create new graph
pane.cy = cytoscape({
container: cyDiv,
elements: [...nodes, ...edges],
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': 50,
'height': 50,
'border-width': 2,
'border-color': '#333'
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': 'data(color)',
'target-arrow-shape': 'triangle',
'target-arrow-color': 'data(color)',
'curve-style': 'bezier',
'opacity': 0.8
}
},
{
selector: 'node:selected',
style: {
'border-width': 4,
'border-color': '#000'
}
}
],
layout: {
name: 'breadthfirst',
directed: true,
spacingFactor: 1.5,
animate: false
}
});
// Highlight path to top result if requested
if (highlightPath && trace.visits.length > 0) {
const topResult = trace.visits.find(v => v.final_rank === 1);
if (topResult) {
const pathNodes = [];
let current = topResult;
while (current) {
pathNodes.push(current.node_id);
current = trace.visits.find(v => v.node_id === current.parent_node_id);
}
pathNodes.forEach(nodeId => {
pane.cy.$(`#${nodeId}`).style({
'border-width': 5,
'border-color': '#ff5722'
});
});
}
}
// Add tooltips
let tooltip = null;
pane.cy.on('mouseover', 'node', function(evt) {
const node = evt.target;
const data = node.data();
const pos = node.renderedPosition();
tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = `
Text: ${data.text}
${data.step ? `Step: ${data.step} ` : ''}
${data.rank !== null && data.rank !== undefined ? `Rank: ${data.rank} ` : ''}
${data.activation ? `Activation: ${data.activation} ` : ''}
${data.similarity ? `Similarity: ${data.similarity} ` : ''}
${data.finalWeight ? `Final Weight: ${data.finalWeight}` : ''}
`;
// Get container bounds
const container = cyDiv.getBoundingClientRect();
// Position tooltip relative to container
let left = pos.x + 20;
let top = pos.y;
// Append to body temporarily to get dimensions
document.body.appendChild(tooltip);
const tooltipRect = tooltip.getBoundingClientRect();
// Adjust if tooltip would go off right edge
if (container.left + left + tooltipRect.width > window.innerWidth) {
left = pos.x - tooltipRect.width - 20;
}
// Adjust if tooltip would go off bottom edge
if (container.top + top + tooltipRect.height > window.innerHeight) {
top = pos.y - tooltipRect.height;
}
// Adjust if tooltip would go off left edge
if (container.left + left < 0) {
left = 20;
}
// Adjust if tooltip would go off top edge
if (container.top + top < 0) {
top = 20;
}
tooltip.style.left = (container.left + left) + 'px';
tooltip.style.top = (container.top + top) + 'px';
});
pane.cy.on('mouseout', 'node', function() {
if (tooltip) {
tooltip.remove();
tooltip = null;
}
});
} catch (e) {
console.error('Error visualizing trace:', e);
// Show error in status bar
const statusBar = document.getElementById(`debug-status-${paneId}`);
if (statusBar) {
statusBar.innerHTML = `â Error visualizing trace: ${e.message} `;
}
}
}
function highlightMatchingNodes(paneId, searchText) {
const pane = debugPanes.find(p => p.id === paneId);
if (!pane || !pane.cy) return;
const countSpan = document.getElementById(`graph-search-count-${paneId}`);
// If search is empty, reset all styles
if (!searchText || searchText.trim() === '') {
pane.cy.nodes().style({
'opacity': 1,
'border-width': 2,
'border-color': '#333'
});
pane.cy.edges().style({
'opacity': 0.8
});
if (countSpan) countSpan.textContent = '';
return;
}
const searchLower = searchText.toLowerCase();
let matchCount = 0;
const totalNodes = pane.cy.nodes().length;
// Check each node for matches
pane.cy.nodes().forEach(node => {
const data = node.data();
const text = (data.text || '').toLowerCase();
const label = (data.label || '').toLowerCase();
const matches = text.includes(searchLower) || label.includes(searchLower);
if (matches) {
matchCount++;
// Highlight matching nodes - full opacity with thicker orange border
node.style({
'opacity': 1,
'border-width': 4,
'border-color': '#ff6f00'
});
} else {
// Dim non-matching nodes
node.style({
'opacity': 0.2,
'border-width': 2,
'border-color': '#333'
});
}
});
// Dim all edges
pane.cy.edges().style({
'opacity': 0.2
});
// Update counter
if (countSpan) {
if (matchCount === 0) {
countSpan.textContent = '(no matches)';
countSpan.style.color = '#d32f2f';
} else {
countSpan.textContent = `(${matchCount} of ${totalNodes})`;
countSpan.style.color = '#43a047';
}
}
}
// Table filtering
document.getElementById('table-filter').addEventListener('input', function() {
const filterValue = this.value.toLowerCase();
const rows = document.querySelectorAll('#memory-table tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
if (text.includes(filterValue)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
// Don't auto-load data on page load - wait for user to click load button