Skip to main content
Glama
web_ui.html30.5 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Graphiti MCP - Web UI</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary: #6366f1; --primary-dark: #4f46e5; --secondary: #8b5cf6; --success: #10b981; --danger: #ef4444; --warning: #f59e0b; --bg: #0f172a; --bg-light: #1e293b; --bg-lighter: #334155; --text: #f1f5f9; --text-muted: #94a3b8; --border: #475569; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); color: var(--text); min-height: 100vh; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; } header { text-align: center; margin-bottom: 40px; padding: 30px; background: var(--bg-light); border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); } h1 { font-size: 2.5rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 10px; } .subtitle { color: var(--text-muted); font-size: 1.1rem; } .status-badge { display: inline-block; padding: 6px 12px; border-radius: 20px; font-size: 0.85rem; margin-top: 10px; background: var(--bg-lighter); color: var(--text-muted); } .status-badge.connected { background: var(--success); color: white; } .status-badge.error { background: var(--danger); color: white; } .tabs { display: flex; gap: 10px; margin-bottom: 30px; flex-wrap: wrap; } .tab { padding: 12px 24px; background: var(--bg-light); border: 2px solid transparent; border-radius: 8px; cursor: pointer; transition: all 0.3s; font-weight: 500; color: var(--text-muted); } .tab:hover { background: var(--bg-lighter); color: var(--text); } .tab.active { background: var(--primary); color: white; border-color: var(--primary-dark); } .tab-content { display: none; } .tab-content.active { display: block; } .card { background: var(--bg-light); border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); } .card h2 { font-size: 1.5rem; margin-bottom: 20px; color: var(--text); } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 8px; color: var(--text); font-weight: 500; } input, textarea, select { width: 100%; padding: 12px; background: var(--bg-lighter); border: 2px solid var(--border); border-radius: 8px; color: var(--text); font-size: 1rem; font-family: inherit; transition: border-color 0.3s; } input:focus, textarea:focus, select:focus { outline: none; border-color: var(--primary); } textarea { min-height: 120px; resize: vertical; } .tags-input { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px; background: var(--bg-lighter); border: 2px solid var(--border); border-radius: 8px; min-height: 50px; } .tag { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--primary); border-radius: 20px; font-size: 0.9rem; } .tag-remove { cursor: pointer; font-weight: bold; opacity: 0.8; } .tag-remove:hover { opacity: 1; } .tag-input { flex: 1; min-width: 100px; border: none; background: transparent; color: var(--text); padding: 0; } .tag-input:focus { outline: none; } button { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.3s; } button:hover { background: var(--primary-dark); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); } button:active { transform: translateY(0); } button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .btn-secondary { background: var(--bg-lighter); color: var(--text); } .btn-secondary:hover { background: var(--border); } .result { margin-top: 20px; padding: 20px; background: var(--bg-lighter); border-radius: 8px; border-left: 4px solid var(--primary); max-height: 500px; overflow-y: auto; } .result pre { color: var(--text); white-space: pre-wrap; word-wrap: break-word; font-family: 'Courier New', monospace; font-size: 0.9rem; } .memory-item { background: var(--bg-lighter); padding: 16px; margin-bottom: 12px; border-radius: 8px; border-left: 3px solid var(--primary); } .memory-item h3 { font-size: 1.1rem; margin-bottom: 8px; color: var(--text); } .memory-item .meta { font-size: 0.85rem; color: var(--text-muted); margin-top: 8px; } .memory-item .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .memory-item .tag-small { padding: 4px 8px; background: var(--primary); border-radius: 12px; font-size: 0.75rem; } .loading { display: inline-block; width: 20px; height: 20px; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; } .alert-success { background: rgba(16, 185, 129, 0.2); border-left: 4px solid var(--success); color: var(--success); } .alert-error { background: rgba(239, 68, 68, 0.2); border-left: 4px solid var(--danger); color: var(--danger); } .metadata-editor { background: var(--bg-lighter); padding: 12px; border-radius: 8px; min-height: 100px; } .metadata-row { display: flex; gap: 10px; margin-bottom: 10px; } .metadata-row input { flex: 1; } .metadata-row button { padding: 8px 16px; font-size: 0.9rem; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } .relationship-selector { display: grid; grid-template-columns: 1fr auto 1fr; gap: 15px; align-items: center; } .relationship-selector select { width: 100%; } .arrow { font-size: 1.5rem; color: var(--primary); } @media (max-width: 768px) { .tabs { flex-direction: column; } .tab { width: 100%; } .relationship-selector { grid-template-columns: 1fr; } .arrow { transform: rotate(90deg); } } </style> </head> <body> <div class="container"> <header> <h1>🧠 Graphiti MCP</h1> <p class="subtitle">Persistent Memory & Context Continuity Demo</p> <div class="status-badge" id="statusBadge">Checking connection...</div> </header> <div class="tabs"> <div class="tab active" onclick="switchTab('store')">Store Memory</div> <div class="tab" onclick="switchTab('retrieve')">Retrieve Memories</div> <div class="tab" onclick="switchTab('context')">Get Context</div> <div class="tab" onclick="switchTab('relationship')">Create Relationship</div> <div class="tab" onclick="switchTab('search')">Search Graph</div> <div class="tab" onclick="switchTab('browse')">Browse All</div> </div> <!-- Store Memory Tab --> <div id="store" class="tab-content active"> <div class="card"> <h2>Store a Memory</h2> <form id="storeForm"> <div class="form-group"> <label for="memoryContent">Content *</label> <textarea id="memoryContent" placeholder="Enter the memory or context you want to store..." required></textarea> </div> <div class="form-group"> <label>Tags</label> <div class="tags-input" id="tagsContainer"> <input type="text" class="tag-input" id="tagInput" placeholder="Add tags (press Enter)"> </div> </div> <div class="form-group"> <label>Metadata (Key-Value Pairs)</label> <div class="metadata-editor" id="metadataEditor"> <div class="metadata-row"> <input type="text" placeholder="Key" class="meta-key"> <input type="text" placeholder="Value" class="meta-value"> <button type="button" class="btn-secondary" onclick="addMetadataRow()">Add</button> </div> </div> </div> <button type="submit">Store Memory</button> </form> <div id="storeResult"></div> </div> </div> <!-- Retrieve Memories Tab --> <div id="retrieve" class="tab-content"> <div class="card"> <h2>Retrieve Memories</h2> <form id="retrieveForm"> <div class="form-group"> <label for="retrieveQuery">Search Query *</label> <input type="text" id="retrieveQuery" placeholder="What memories are you looking for?" required> </div> <div class="form-group"> <label for="retrieveLimit">Limit</label> <input type="number" id="retrieveLimit" value="10" min="1" max="50"> </div> <button type="submit">Search Memories</button> </form> <div id="retrieveResult"></div> </div> </div> <!-- Get Context Tab --> <div id="context" class="tab-content"> <div class="card"> <h2>Get Synthesized Context</h2> <form id="contextForm"> <div class="form-group"> <label for="contextQuery">Query *</label> <textarea id="contextQuery" placeholder="What context do you need?" required></textarea> </div> <div class="form-group"> <label for="contextMaxMemories">Max Memories</label> <input type="number" id="contextMaxMemories" value="20" min="1" max="50"> </div> <button type="submit">Get Context</button> </form> <div id="contextResult"></div> </div> </div> <!-- Create Relationship Tab --> <div id="relationship" class="tab-content"> <div class="card"> <h2>Create Relationship</h2> <form id="relationshipForm"> <div class="form-group"> <label>Source Memory ID *</label> <input type="text" id="sourceId" placeholder="Enter source memory ID" required> </div> <div class="form-group"> <label>Relationship Type *</label> <select id="relationshipType" required> <option value="">Select relationship type</option> <option value="relates_to">Relates To</option> <option value="has_details">Has Details</option> <option value="follows">Follows</option> <option value="references">References</option> <option value="part_of">Part Of</option> <option value="enables">Enables</option> <option value="generated">Generated</option> </select> </div> <div class="form-group"> <label>Target Memory ID *</label> <input type="text" id="targetId" placeholder="Enter target memory ID" required> </div> <button type="submit">Create Relationship</button> </form> <div id="relationshipResult"></div> </div> </div> <!-- Search Graph Tab --> <div id="search" class="tab-content"> <div class="card"> <h2>Search Graph (Cypher Query)</h2> <form id="searchForm"> <div class="form-group"> <label for="cypherQuery">Cypher Query *</label> <textarea id="cypherQuery" placeholder="MATCH (m:Memory) RETURN m LIMIT 10" required></textarea> </div> <button type="submit">Execute Query</button> </form> <div id="searchResult"></div> </div> </div> <!-- Browse All Tab --> <div id="browse" class="tab-content"> <div class="card"> <h2>Browse All Memories</h2> <button onclick="loadAllMemories()">Load All Memories</button> <div id="browseResult"></div> </div> </div> </div> <script> const API_BASE = window.location.origin; let tags = []; let metadata = {}; // Check connection on load async function checkConnection() { try { const response = await fetch(`${API_BASE}/api/health`); const data = await response.json(); const badge = document.getElementById('statusBadge'); if (data.status === 'ok') { badge.textContent = '✓ Connected'; badge.className = 'status-badge connected'; } else { badge.textContent = '✗ Error: ' + data.message; badge.className = 'status-badge error'; } } catch (error) { const badge = document.getElementById('statusBadge'); badge.textContent = '✗ Connection Error'; badge.className = 'status-badge error'; } } // Tab switching function switchTab(tabName) { document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); event.target.classList.add('active'); document.getElementById(tabName).classList.add('active'); } // Tags handling document.getElementById('tagInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') { e.preventDefault(); const tag = this.value.trim(); if (tag && !tags.includes(tag)) { tags.push(tag); renderTags(); this.value = ''; } } }); function renderTags() { const container = document.getElementById('tagsContainer'); const input = document.getElementById('tagInput'); container.innerHTML = ''; tags.forEach(tag => { const span = document.createElement('span'); span.className = 'tag'; span.innerHTML = `${tag} <span class="tag-remove" onclick="removeTag('${tag}')">×</span>`; container.appendChild(span); }); container.appendChild(input); input.focus(); } function removeTag(tag) { tags = tags.filter(t => t !== tag); renderTags(); } // Metadata handling function addMetadataRow() { const editor = document.getElementById('metadataEditor'); const rows = editor.querySelectorAll('.metadata-row'); const lastRow = rows[rows.length - 1]; const key = lastRow.querySelector('.meta-key').value.trim(); const value = lastRow.querySelector('.meta-value').value.trim(); if (key && value) { metadata[key] = value; lastRow.querySelector('.meta-key').value = ''; lastRow.querySelector('.meta-value').value = ''; } } // Store Memory document.getElementById('storeForm').addEventListener('submit', async function(e) { e.preventDefault(); const resultDiv = document.getElementById('storeResult'); resultDiv.innerHTML = '<div class="loading"></div> Loading...'; try { const response = await fetch(`${API_BASE}/api/store_memory`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: document.getElementById('memoryContent').value, tags: tags, metadata: metadata }) }); const data = await response.json(); if (response.ok) { resultDiv.innerHTML = ` <div class="alert alert-success">Memory stored successfully!</div> <div class="result"> <pre>${JSON.stringify(data, null, 2)}</pre> </div> `; // Reset form document.getElementById('storeForm').reset(); tags = []; metadata = {}; renderTags(); } else { resultDiv.innerHTML = ` <div class="alert alert-error">Error: ${data.detail || JSON.stringify(data)}</div> `; } } catch (error) { resultDiv.innerHTML = ` <div class="alert alert-error">Error: ${error.message}</div> `; } }); // Retrieve Memories document.getElementById('retrieveForm').addEventListener('submit', async function(e) { e.preventDefault(); const resultDiv = document.getElementById('retrieveResult'); resultDiv.innerHTML = '<div class="loading"></div> Loading...'; try { const response = await fetch(`${API_BASE}/api/retrieve_memories`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: document.getElementById('retrieveQuery').value, limit: parseInt(document.getElementById('retrieveLimit').value) }) }); const data = await response.json(); if (response.ok) { let html = `<div class="alert alert-success">Found ${data.count} memories</div>`; if (data.memories && data.memories.length > 0) { data.memories.forEach(mem => { html += ` <div class="memory-item"> <h3>${mem.content.substring(0, 100)}${mem.content.length > 100 ? '...' : ''}</h3> <div class="meta">ID: ${mem.id}</div> ${mem.tags && mem.tags.length > 0 ? ` <div class="tags"> ${mem.tags.map(t => `<span class="tag-small">${t}</span>`).join('')} </div> ` : ''} ${mem.metadata && Object.keys(mem.metadata).length > 0 ? ` <div class="meta">Metadata: ${JSON.stringify(mem.metadata)}</div> ` : ''} </div> `; }); } resultDiv.innerHTML = html; } else { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${data.detail || JSON.stringify(data)}</div>`; } } catch (error) { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; } }); // Get Context document.getElementById('contextForm').addEventListener('submit', async function(e) { e.preventDefault(); const resultDiv = document.getElementById('contextResult'); resultDiv.innerHTML = '<div class="loading"></div> Loading...'; try { const response = await fetch(`${API_BASE}/api/get_context`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: document.getElementById('contextQuery').value, max_memories: parseInt(document.getElementById('contextMaxMemories').value) }) }); const data = await response.json(); if (response.ok) { resultDiv.innerHTML = ` <div class="alert alert-success">Context synthesized from ${data.memories_used} memories</div> <div class="result"> <h3>Context:</h3> <p>${data.context}</p> <pre style="margin-top: 20px;">${JSON.stringify(data, null, 2)}</pre> </div> `; } else { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${data.detail || JSON.stringify(data)}</div>`; } } catch (error) { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; } }); // Create Relationship document.getElementById('relationshipForm').addEventListener('submit', async function(e) { e.preventDefault(); const resultDiv = document.getElementById('relationshipResult'); resultDiv.innerHTML = '<div class="loading"></div> Loading...'; try { const response = await fetch(`${API_BASE}/api/create_relationship`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source_id: document.getElementById('sourceId').value, target_id: document.getElementById('targetId').value, relationship_type: document.getElementById('relationshipType').value, properties: {} }) }); const data = await response.json(); if (response.ok && data.success) { resultDiv.innerHTML = ` <div class="alert alert-success">Relationship created successfully!</div> <div class="result"> <pre>${JSON.stringify(data, null, 2)}</pre> </div> `; } else { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${data.error || data.detail || JSON.stringify(data)}</div>`; } } catch (error) { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; } }); // Search Graph document.getElementById('searchForm').addEventListener('submit', async function(e) { e.preventDefault(); const resultDiv = document.getElementById('searchResult'); resultDiv.innerHTML = '<div class="loading"></div> Loading...'; try { const response = await fetch(`${API_BASE}/api/search_graph`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cypher_query: document.getElementById('cypherQuery').value, parameters: {} }) }); const data = await response.json(); if (response.ok) { resultDiv.innerHTML = ` <div class="alert alert-success">Query executed: ${data.count} results</div> <div class="result"> <pre>${JSON.stringify(data, null, 2)}</pre> </div> `; } else { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${data.error || data.detail || JSON.stringify(data)}</div>`; } } catch (error) { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; } }); // Browse All Memories async function loadAllMemories() { const resultDiv = document.getElementById('browseResult'); resultDiv.innerHTML = '<div class="loading"></div> Loading...'; try { const response = await fetch(`${API_BASE}/api/list_all_memories?limit=50`); const data = await response.json(); if (response.ok && data.success) { let html = `<div class="alert alert-success">Found ${data.count} memories</div>`; if (data.results && data.results.length > 0) { data.results.forEach(mem => { html += ` <div class="memory-item"> <h3>${mem.content ? mem.content.substring(0, 150) + (mem.content.length > 150 ? '...' : '') : 'No content'}</h3> <div class="meta">ID: ${mem.id || 'N/A'}</div> ${mem.tags && mem.tags.length > 0 ? ` <div class="tags"> ${mem.tags.map(t => `<span class="tag-small">${t}</span>`).join('')} </div> ` : ''} </div> `; }); } else { html += '<p>No memories found.</p>'; } resultDiv.innerHTML = html; } else { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${data.error || data.detail || JSON.stringify(data)}</div>`; } } catch (error) { resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; } } // Initialize checkConnection(); setInterval(checkConnection, 30000); // Check every 30 seconds </script> </body> </html>

Latest Blog Posts

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/apexneural-hansika/graphiti_mcp'

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