Skip to main content
Glama

MemoryMesh

by CheMiguel23
MemoryViewer.html32.3 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Memory Viewer</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.2/ace.js"></script> <style> /* --- Base Styles --- */ :root { --primary: #2563eb; --primary-dark: #1d4ed8; --primary-light: #60a5fa; --success: #22c55e; --danger: #ef4444; --warning: #f59e0b; --gray-50: #f9fafb; --gray-100: #f3f4f6; --gray-200: #e5e7eb; --gray-300: #d1d5db; --gray-400: #9ca3af; --gray-500: #6b7280; --gray-600: #4b5563; --gray-700: #374151; --gray-800: #1f2937; --gray-900: #111827; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; padding: 2rem; max-width: 1400px; margin: 0 auto; color: var(--gray-800); background-color: var(--gray-50); } /* --- Typography --- */ h1 { margin-bottom: 2rem; color: var(--gray-900); font-size: 2rem; font-weight: 600; } /* --- Layout Components --- */ .container { background: white; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); padding: 2rem; margin-bottom: 2rem; } /* --- Buttons --- */ .btn { padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-size: 1rem; background: var(--primary); color: white; transition: all 0.2s ease; } .btn:hover { background: var(--primary-dark); } /* --- Tabs --- */ .tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 2px solid var(--gray-200); padding-bottom: 0.5rem; } .tab { padding: 0.5rem 1rem; border: none; background: none; cursor: pointer; font-size: 1rem; color: var(--gray-600); border-bottom: 2px solid transparent; margin-bottom: -0.5rem; transition: all 0.2s ease; } .tab:hover { color: var(--primary); } .tab.active { color: var(--primary); border-bottom: 2px solid var(--primary); } .tab-content { display: none; } .tab-content.active { display: block; } /* --- File Selection --- */ .file-section { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; padding: 1rem; background: white; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .file-path { flex: 1; font-family: monospace; padding: 0.5rem; background: var(--gray-50); border: 1px solid var(--gray-300); border-radius: 0.25rem; } /* --- Table View --- */ .table-container { overflow-x: auto; margin-bottom: 2rem; } table { width: 100%; border-collapse: collapse; background: white; } th, td { padding: 0.75rem; border: 1px solid var(--gray-200); text-align: left; } th { background: var(--gray-100); font-weight: 600; position: sticky; top: 0; z-index: 10; } tbody tr:hover { background: var(--gray-50); } /* --- Metadata and Expandable Controls --- */ .expand-button { padding: 0.25rem 0.5rem; background: var(--gray-100); border: 1px solid var(--gray-300); border-radius: 0.25rem; cursor: pointer; font-size: 0.75rem; margin-right: 0.5rem; color: var(--gray-700); transition: all 0.2s ease; } .expand-button:hover { background: var(--gray-200); } .metadata-header { display: flex; align-items: center; margin-bottom: 0.5rem; } .metadata-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.5rem; margin-top: 0.5rem; } .metadata-item { background: var(--gray-100); padding: 0.5rem; border-radius: 0.25rem; font-size: 0.875rem; } .metadata-key { color: var(--gray-700); font-weight: 500; } .metadata-value { color: var(--gray-900); } .expandable-cell { cursor: pointer; position: relative; } .expandable-cell::after { content: '▼'; position: absolute; right: 0.5rem; color: var(--gray-500); transition: transform 0.2s; } .expandable-cell.expanded::after { transform: rotate(180deg); } .metadata-summary { color: var(--gray-600); font-size: 0.875rem; margin-bottom: 0.5rem; } /* --- Edge Links --- */ .edge-link { color: var(--primary); text-decoration: none; cursor: pointer; display: inline-flex; align-items: center; gap: 0.25rem; } .edge-link:hover { text-decoration: underline; } .edge-count { background: var(--primary-light); color: white; padding: 0.125rem 0.375rem; border-radius: 1rem; font-size: 0.75rem; } .edge-list { margin-top: 0.5rem; padding-left: 1rem; border-left: 2px solid var(--gray-200); } .edge-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0; font-size: 0.875rem; } .edge-type { color: var(--gray-600); } .edge-target { color: var(--primary); cursor: pointer; } .edge-target:hover { text-decoration: underline; } /* --- Stats Panel --- */ .stats-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; } .stat-card { background: white; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .stat-title { font-size: 0.875rem; color: var(--gray-600); margin-bottom: 0.5rem; } .stat-value { font-size: 1.5rem; font-weight: 600; color: var(--gray-900); } /* --- Toast Notifications --- */ #toastContainer { position: fixed; top: 1rem; right: 1rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 1000; } .toast { min-width: 250px; padding: 1rem 1.5rem; border-radius: 0.25rem; color: white; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); opacity: 0; transform: translateX(100%); animation: slideIn 0.5s forwards, fadeOut 0.5s forwards 2.5s; } .toast-success { background-color: var(--success); } .toast-error { background-color: var(--danger); } @keyframes slideIn { to { opacity: 1; transform: translateX(0); } } @keyframes fadeOut { to { opacity: 0; transform: translateX(100%); } } /* --- Search and Filter --- */ .search-container { display: flex; gap: 1rem; margin-bottom: 1rem; } .search-input { flex: 1; padding: 0.5rem; border: 1px solid var(--gray-300); border-radius: 0.25rem; font-size: 1rem; } .filter-select { padding: 0.5rem; border: 1px solid var(--gray-300); border-radius: 0.25rem; font-size: 1rem; min-width: 150px; } /* --- JSON Editor --- */ .editor-container { height: 600px; border: 1px solid var(--gray-300); border-radius: 0.25rem; margin-bottom: 1rem; overflow: hidden; } #jsonEditor { height: 100%; font-size: 14px; } /* --- Responsive Design --- */ @media (max-width: 768px) { body { padding: 1rem; } .search-container { flex-direction: column; } .metadata-grid { grid-template-columns: 1fr; } } </style> </head> <body> <h1>Memory Viewer</h1> <div class="file-section"> <button class="btn" id="selectMemoryFileBtn">Select Memory File</button> <div class="file-path" id="filePath">No file selected</div> </div> <!-- Toast Notifications Container --> <div id="toastContainer"></div> <!-- Stats Panel --> <div class="stats-panel"> <div class="stat-card"> <div class="stat-title">Total Nodes</div> <div class="stat-value" id="totalNodes">0</div> </div> <div class="stat-card"> <div class="stat-title">Total Edges</div> <div class="stat-value" id="totalEdges">0</div> </div> <div class="stat-card"> <div class="stat-title">Node Types</div> <div class="stat-value" id="nodeTypes">0</div> </div> <div class="stat-card"> <div class="stat-title">Edge Types</div> <div class="stat-value" id="edgeTypes">0</div> </div> </div> <!-- Tabs --> <div class="tabs"> <button class="tab active" data-tab="table">Table View</button> <button class="tab" data-tab="raw">Raw JSON</button> </div> <!-- Table View --> <div id="tableView" class="tab-content active"> <div class="search-container"> <input type="text" class="search-input" id="searchInput" placeholder="Search..."> <select class="filter-select" id="typeFilter"> <option value="all">All Types</option> </select> <select class="filter-select" id="viewFilter"> <option value="all">All Items</option> <option value="nodes">Nodes Only</option> <option value="edges">Edges Only</option> </select> </div> <div class="table-container"> <table id="memoryTable"> <thead> <tr> <th>Type</th> <th>Name/From</th> <th>NodeType/To</th> <th>Metadata/EdgeType</th> </tr> </thead> <tbody></tbody> </table> </div> </div> <!-- Raw JSON View --> <div id="rawView" class="tab-content"> <div class="editor-container"> <div id="jsonEditor"></div> </div> </div> <script> // --- Constants and State --- const DB_NAME = 'MemoryViewerDB'; const STORE_NAME = 'file'; let db = null; let fileHandle = null; let jsonEditor = null; let currentData = null; // --- Utility Functions --- function showToast(message, type = 'success', duration = 3000) { const toastContainer = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.classList.add('toast', type === 'success' ? 'toast-success' : 'toast-error'); toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => toast.remove(), duration + 1000); } function parseMetadata(metadata) { if (!metadata || !Array.isArray(metadata)) return []; return metadata.map(item => { const colonIndex = item.indexOf(':'); if (colonIndex === -1) return { key: '', value: item }; const key = item.substring(0, colonIndex).trim(); const value = item.substring(colonIndex + 1).trim(); return { key, value }; }); } function createMetadataDisplay(metadata) { if (!metadata || !Array.isArray(metadata)) return ''; const parsedMetadata = parseMetadata(metadata); const container = document.createElement('div'); // Add summary const summary = document.createElement('div'); summary.className = 'metadata-summary'; summary.textContent = `${parsedMetadata.length} metadata entries`; container.appendChild(summary); // Create metadata grid const grid = document.createElement('div'); grid.className = 'metadata-grid'; parsedMetadata.forEach(({ key, value }) => { const item = document.createElement('div'); item.className = 'metadata-item'; if (key) { item.innerHTML = `<span class="metadata-key">${key}:</span> <span class="metadata-value">${value}</span>`; } else { item.innerHTML = `<span class="metadata-value">${value}</span>`; } grid.appendChild(item); }); container.appendChild(grid); return container; } // --- IndexedDB Operations --- const idb = { initDB: async () => { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 1); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME); } }; request.onsuccess = (event) => { db = event.target.result; resolve(); }; request.onerror = (event) => reject(event.target.error); }); }, saveFileHandle: async (handle) => { return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const request = store.put(handle, 'memoryFile'); request.onsuccess = () => resolve(); request.onerror = (event) => reject(event.target.error); }); }, getFileHandleFromDB: async () => { return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const request = store.get('memoryFile'); request.onsuccess = async (event) => { const handle = event.target.result; if (handle) { const permission = await handle.queryPermission({mode: 'read'}); if (permission === 'granted') { resolve(handle); return; } } resolve(null); }; request.onerror = (event) => reject(event.target.error); }); }, removeFileHandleFromDB: async () => { return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const request = store.delete('memoryFile'); request.onsuccess = () => resolve(); request.onerror = (event) => reject(event.target.error); }); } }; // --- File System Operations --- const fileSystem = { selectFile: async () => { try { let options = {id: 'memory-file', mode: 'read'}; const storedHandle = await idb.getFileHandleFromDB(); options.startIn = storedHandle || 'documents'; [fileHandle] = await window.showOpenFilePicker(options); if (storedHandle && fileHandle.name !== storedHandle.name) { await idb.removeFileHandleFromDB(); } document.getElementById('filePath').textContent = fileHandle.name; await fileSystem.loadFile(); showToast('File selected successfully!', 'success'); await idb.saveFileHandle(fileHandle); } catch (error) { if (error.name !== 'AbortError') { showToast('Error selecting file: ' + error.message, 'error'); } } }, loadFile: async () => { if (!fileHandle) { showToast('Please select a memory file first', 'error'); return; } try { const file = await fileHandle.getFile(); const content = await file.text(); // Split content into lines and parse each line as JSON const lines = content.split('\n'); const jsonObjects = lines .filter(line => line.trim() !== '') .map(line => JSON.parse(line)); currentData = jsonObjects; updateViews(jsonObjects); showToast('Memory file loaded successfully!', 'success'); } catch (error) { showToast('Error loading memory file: ' + error.message, 'error'); } } }; // --- Update Views --- function updateStats(data) { const nodes = data.filter(item => item.type === 'node'); const edges = data.filter(item => item.type === 'edge'); const nodeTypeSet = new Set(nodes.map(node => node.nodeType)); const edgeTypeSet = new Set(edges.map(edge => edge.edgeType)); document.getElementById('totalNodes').textContent = nodes.length; document.getElementById('totalEdges').textContent = edges.length; document.getElementById('nodeTypes').textContent = nodeTypeSet.size; document.getElementById('edgeTypes').textContent = edgeTypeSet.size; // Update filters const typeFilter = document.getElementById('typeFilter'); typeFilter.innerHTML = '<option value="all">All Types</option>'; [...nodeTypeSet].sort().forEach(type => { const option = document.createElement('option'); option.value = `node:${type}`; option.textContent = `Node: ${type}`; typeFilter.appendChild(option); }); [...edgeTypeSet].sort().forEach(type => { const option = document.createElement('option'); option.value = `edge:${type}`; option.textContent = `Edge: ${type}`; typeFilter.appendChild(option); }); } function updateTable(data, searchTerm = '', typeFilter = 'all', viewFilter = 'all') { const tbody = document.querySelector('#memoryTable tbody'); tbody.innerHTML = ''; // Create a map of node relationships const nodeEdges = new Map(); data.forEach(item => { if (item.type === 'edge') { if (!nodeEdges.has(item.from)) { nodeEdges.set(item.from, []); } nodeEdges.get(item.from).push(item); } }); const filteredData = data.filter(item => { if (viewFilter === 'nodes' && item.type !== 'node') return false; if (viewFilter === 'edges' && item.type !== 'edge') return false; if (typeFilter !== 'all') { const [filterType, filterValue] = typeFilter.split(':'); if (filterType === 'node' && (item.type !== 'node' || item.nodeType !== filterValue)) return false; if (filterType === 'edge' && (item.type !== 'edge' || item.edgeType !== filterValue)) return false; } if (searchTerm) { const searchLower = searchTerm.toLowerCase(); const itemString = JSON.stringify(item).toLowerCase(); return itemString.includes(searchLower); } return true; }); // Show only nodes in the main table if edge integration is enabled const displayData = viewFilter === 'edges' ? filteredData : filteredData.filter(item => item.type === 'node'); displayData.forEach(item => { const row = document.createElement('tr'); // Type column const typeCell = document.createElement('td'); typeCell.textContent = item.type; row.appendChild(typeCell); // Name column const nameCell = document.createElement('td'); nameCell.textContent = item.name; // Add edge information if available const edges = nodeEdges.get(item.name); if (edges && edges.length > 0) { const edgeLink = document.createElement('div'); edgeLink.innerHTML = ` <a class="edge-link"> View Edges <span class="edge-count">${edges.length}</span> </a> `; const edgeList = document.createElement('div'); edgeList.className = 'edge-list'; edgeList.style.display = 'none'; edges.forEach(edge => { const edgeItem = document.createElement('div'); edgeItem.className = 'edge-item'; edgeItem.innerHTML = ` <span class="edge-type">${edge.edgeType}</span> <span class="edge-target" data-node="${edge.to}">→ ${edge.to}</span> `; edgeList.appendChild(edgeItem); }); edgeLink.querySelector('.edge-link').addEventListener('click', () => { edgeList.style.display = edgeList.style.display === 'none' ? 'block' : 'none'; }); nameCell.appendChild(edgeLink); nameCell.appendChild(edgeList); } row.appendChild(nameCell); // NodeType column const nodeTypeCell = document.createElement('td'); nodeTypeCell.textContent = item.nodeType; row.appendChild(nodeTypeCell); // Metadata column const metadataCell = document.createElement('td'); if (item.metadata) { const metadataHeader = document.createElement('div'); metadataHeader.className = 'metadata-header'; const expandButton = document.createElement('button'); expandButton.className = 'expand-button'; expandButton.textContent = 'Show Metadata'; const metadataCount = document.createElement('span'); metadataCount.className = 'metadata-summary'; metadataCount.textContent = `${item.metadata.length} entries`; metadataHeader.appendChild(expandButton); metadataHeader.appendChild(metadataCount); const metadataContent = createMetadataDisplay(item.metadata); metadataContent.querySelector('.metadata-grid').style.display = 'none'; expandButton.addEventListener('click', () => { const grid = metadataContent.querySelector('.metadata-grid'); const isExpanded = grid.style.display !== 'none'; grid.style.display = isExpanded ? 'none' : 'grid'; expandButton.textContent = isExpanded ? 'Show Metadata' : 'Hide Metadata'; }); metadataCell.appendChild(metadataHeader); metadataCell.appendChild(metadataContent); } row.appendChild(metadataCell); tbody.appendChild(row); }); // Add click handlers for edge targets document.querySelectorAll('.edge-target').forEach(target => { target.addEventListener('click', () => { const nodeName = target.dataset.node; const nodeRow = [...tbody.rows].find(row => row.cells[1].textContent.trim() === nodeName ); if (nodeRow) { nodeRow.scrollIntoView({ behavior: 'smooth', block: 'center' }); nodeRow.style.backgroundColor = '#fff2cc'; setTimeout(() => { nodeRow.style.backgroundColor = ''; }, 2000); } }); }); } function updateViews(data) { // Update statistics updateStats(data); // Update table view const searchInput = document.getElementById('searchInput'); const typeFilter = document.getElementById('typeFilter'); const viewFilter = document.getElementById('viewFilter'); updateTable(data, searchInput.value, typeFilter.value, viewFilter.value); // Update raw JSON view if (jsonEditor) { jsonEditor.setValue(JSON.stringify(data, null, 2)); jsonEditor.clearSelection(); } } // --- UI Management --- const ui = { switchTab: (tabName) => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active'); document.getElementById(`${tabName}View`).classList.add('active'); }, setupEditor: () => { jsonEditor = ace.edit("jsonEditor"); jsonEditor.setTheme("ace/theme/monokai"); jsonEditor.session.setMode("ace/mode/json"); jsonEditor.setOptions({ fontSize: "14px", showPrintMargin: false, showGutter: true, highlightActiveLine: true, readOnly: true }); } }; // --- Event Handlers --- function setupEventListeners() { document.getElementById('selectMemoryFileBtn').addEventListener('click', fileSystem.selectFile); // Tab switching document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => ui.switchTab(tab.dataset.tab)); }); // Search and filter handlers document.getElementById('searchInput').addEventListener('input', (e) => { if (currentData) { const typeFilter = document.getElementById('typeFilter').value; const viewFilter = document.getElementById('viewFilter').value; updateTable(currentData, e.target.value, typeFilter, viewFilter); } }); document.getElementById('typeFilter').addEventListener('change', (e) => { if (currentData) { const searchTerm = document.getElementById('searchInput').value; const viewFilter = document.getElementById('viewFilter').value; updateTable(currentData, searchTerm, e.target.value, viewFilter); } }); document.getElementById('viewFilter').addEventListener('change', (e) => { if (currentData) { const searchTerm = document.getElementById('searchInput').value; const typeFilter = document.getElementById('typeFilter').value; updateTable(currentData, searchTerm, typeFilter, e.target.value); } }); } // --- Initialization --- async function initialize() { try { await idb.initDB(); } catch (error) { showToast('Error initializing database: ' + error.message, 'error'); return; } if (!('showOpenFilePicker' in window)) { showToast('Your browser does not support the File System Access API. Please use a modern browser like Chrome or Edge.', 'error', 0); return; } try { const storedHandle = await idb.getFileHandleFromDB(); if (storedHandle) { const permission = await storedHandle.queryPermission({mode: 'read'}); if (permission === 'granted') { fileHandle = storedHandle; document.getElementById('filePath').textContent = fileHandle.name; await fileSystem.loadFile(); showToast('Loaded previously selected file.', 'success'); } else { const requestPermission = await storedHandle.requestPermission({mode: 'read'}); if (requestPermission === 'granted') { fileHandle = storedHandle; document.getElementById('filePath').textContent = fileHandle.name; await fileSystem.loadFile(); showToast('Loaded previously selected file.', 'success'); } else { await idb.removeFileHandleFromDB(); showToast('Permission to access the previously selected file was denied.', 'error'); } } } } catch (error) { showToast('Error accessing stored file: ' + error.message, 'error'); } ui.setupEditor(); setupEventListeners(); } // --- Start the application --- initialize(); </script> </body> </html>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CheMiguel23/MemoryMesh'

If you have feedback or need assistance with the MCP directory API, please join our Discord server