<!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>