// 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}

`; } } } // 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 = ''; } } catch (e) { console.error('Error loading agents:', e); const select = document.getElementById('search-agent-id'); select.innerHTML = ''; } } // 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 = `
Search Trace #${paneId + 1}
Ready to search
`; 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 = ''; } } catch (e) { console.error('Error loading agents for pane:', paneId, e); select.innerHTML = ''; } } 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
` : ''} `; 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 += ` `; }); html += `
Rank Orig Rank Text Context Date Final Score Activation Similarity Recency Frequency MMR Score MMR Rel MMR Sim
#${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) : '-'}
`; 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.

`; 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 += `
Step ${step}
`; 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 += `
Entry Point ${isResult ? `Rank #${visit.final_rank}` : ''}
`; } else if (hasParent) { const linkTypeIcon = { 'temporal': 'âąī¸', 'semantic': '🔗', 'entity': '👤' }; const icon = linkTypeIcon[visit.link_type] || 'âžĄī¸'; html += `
${icon} ${visit.link_type} link ${isResult ? `Rank #${visit.final_rank}` : ''}
`; } // 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 += `
Pruned Nodes
`; html += `
âœ‚ī¸ Budget Limit Reached: These nodes were not explored to conserve computational resources.
`; trace.pruned.forEach(pruned => { html += `
`; html += `
Pruned
`; 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