Ability to edit and add memories from the web UI with fixed escapeHtml
This commit is contained in:
@@ -1613,10 +1613,20 @@
|
||||
<div style="background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||
<h4 style="margin: 0;">📚 Declarative Facts</h4>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button onclick="showCreateMemoryModal('declarative')" style="background: #2a9955; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
|
||||
➕ Add Fact
|
||||
</button>
|
||||
<button onclick="loadFacts()" style="background: #2a5599; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
|
||||
🔄 Load Facts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 0.5rem;">
|
||||
<input type="text" id="facts-search" placeholder="🔍 Search facts..."
|
||||
oninput="filterMemories('facts-list', this.value)"
|
||||
style="width: 100%; padding: 0.4rem; background: #242424; color: #fff; border: 1px solid #444; border-radius: 4px; box-sizing: border-box;">
|
||||
</div>
|
||||
<div id="facts-list" style="max-height: 400px; overflow-y: auto;">
|
||||
<div style="text-align: center; color: #666; padding: 2rem;">Click "Load Facts" to view stored knowledge</div>
|
||||
</div>
|
||||
@@ -1626,10 +1636,20 @@
|
||||
<div style="background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||
<h4 style="margin: 0;">📝 Episodic Memories</h4>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button onclick="showCreateMemoryModal('episodic')" style="background: #2a9955; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
|
||||
➕ Add Memory
|
||||
</button>
|
||||
<button onclick="loadEpisodicMemories()" style="background: #2a5599; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
|
||||
🔄 Load Memories
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 0.5rem;">
|
||||
<input type="text" id="episodic-search" placeholder="🔍 Search memories..."
|
||||
oninput="filterMemories('episodic-list', this.value)"
|
||||
style="width: 100%; padding: 0.4rem; background: #242424; color: #fff; border: 1px solid #444; border-radius: 4px; box-sizing: border-box;">
|
||||
</div>
|
||||
<div id="episodic-list" style="max-height: 400px; overflow-y: auto;">
|
||||
<div style="text-align: center; color: #666; padding: 2rem;">Click "Load Memories" to view conversation snippets</div>
|
||||
</div>
|
||||
@@ -1694,6 +1714,57 @@
|
||||
|
||||
<div id="notification"></div>
|
||||
|
||||
<!-- Edit Memory Modal -->
|
||||
<div id="edit-memory-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; align-items: center; justify-content: center;">
|
||||
<div style="background: #1e1e1e; border: 2px solid #555; border-radius: 8px; padding: 2rem; max-width: 600px; width: 90%;">
|
||||
<h3 style="margin: 0 0 1rem 0; color: #61dafb;">✏️ Edit Memory</h3>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Content:</label>
|
||||
<textarea id="edit-memory-content" rows="5" style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: monospace; box-sizing: border-box; resize: vertical;"></textarea>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Source:</label>
|
||||
<input type="text" id="edit-memory-source" style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;">
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
||||
<button onclick="closeEditMemoryModal()" style="background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Cancel
|
||||
</button>
|
||||
<button onclick="saveMemoryEdit()" style="background: #2a5599; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||
💾 Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Memory Modal -->
|
||||
<div id="create-memory-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; align-items: center; justify-content: center;">
|
||||
<div style="background: #1e1e1e; border: 2px solid #555; border-radius: 8px; padding: 2rem; max-width: 600px; width: 90%;">
|
||||
<h3 style="margin: 0 0 1rem 0; color: #6fdc6f;" id="create-modal-title">➕ Create New Memory</h3>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Content:</label>
|
||||
<textarea id="create-memory-content" rows="5" placeholder="Enter the fact or memory content..." style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: monospace; box-sizing: border-box; resize: vertical;"></textarea>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">User ID (optional):</label>
|
||||
<input type="text" id="create-memory-user-id" placeholder="e.g., discord_123456789" style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;">
|
||||
<small style="color: #888;">Leave empty for general facts, or specify a Discord user ID for user-specific facts</small>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Source (optional):</label>
|
||||
<input type="text" id="create-memory-source" placeholder="e.g., manual_admin, wiki, etc." style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;">
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
||||
<button onclick="closeCreateMemoryModal()" style="background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Cancel
|
||||
</button>
|
||||
<button onclick="saveNewMemory()" style="background: #2a9955; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||
✨ Create Memory
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global variables
|
||||
let currentMood = 'neutral';
|
||||
@@ -5282,17 +5353,23 @@ async function loadFacts() {
|
||||
data.facts.forEach((fact, i) => {
|
||||
const source = fact.metadata?.source || 'unknown';
|
||||
const when = fact.metadata?.when ? new Date(fact.metadata.when * 1000).toLocaleString() : 'unknown';
|
||||
const factDataJson = escapeJsonForAttribute(fact);
|
||||
html += `
|
||||
<div style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a9955; display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div class="memory-item" style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a9955; display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div style="flex: 1;">
|
||||
<div style="color: #ddd; font-size: 0.9rem;">${escapeHtml(fact.content)}</div>
|
||||
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">
|
||||
Source: ${escapeHtml(source)} · ${when}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.3rem; flex-shrink: 0;">
|
||||
<button data-memory='${factDataJson}' onclick='showEditMemoryModalFromButton(this, "declarative", "${fact.id}")'
|
||||
style="background: none; border: none; color: #5599cc; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
||||
title="Edit this fact">✏️</button>
|
||||
<button onclick="deleteMemoryPoint('declarative', '${fact.id}', this)"
|
||||
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem; flex-shrink: 0;"
|
||||
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
||||
title="Delete this fact">🗑️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
@@ -5319,17 +5396,23 @@ async function loadEpisodicMemories() {
|
||||
data.memories.forEach((mem, i) => {
|
||||
const source = mem.metadata?.source || 'unknown';
|
||||
const when = mem.metadata?.when ? new Date(mem.metadata.when * 1000).toLocaleString() : 'unknown';
|
||||
const memDataJson = escapeJsonForAttribute(mem);
|
||||
html += `
|
||||
<div style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a5599; display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div class="memory-item" style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a5599; display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div style="flex: 1;">
|
||||
<div style="color: #ddd; font-size: 0.9rem;">${escapeHtml(mem.content)}</div>
|
||||
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">
|
||||
Source: ${escapeHtml(source)} · ${when}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.3rem; flex-shrink: 0;">
|
||||
<button data-memory='${memDataJson}' onclick='showEditMemoryModalFromButton(this, "episodic", "${mem.id}")'
|
||||
style="background: none; border: none; color: #5599cc; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
||||
title="Edit this memory">✏️</button>
|
||||
<button onclick="deleteMemoryPoint('episodic', '${mem.id}', this)"
|
||||
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem; flex-shrink: 0;"
|
||||
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
||||
title="Delete this memory">🗑️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
@@ -5443,6 +5526,174 @@ function resetDeleteFlow() {
|
||||
updateDeleteButton();
|
||||
}
|
||||
|
||||
// Memory Edit/Create Modal Functions
|
||||
let currentEditMemory = null;
|
||||
|
||||
function showEditMemoryModalFromButton(button, collection, pointId) {
|
||||
const memoryJson = button.getAttribute('data-memory');
|
||||
// Unescape HTML entities back to JSON
|
||||
const unescapedJson = memoryJson
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&');
|
||||
const memory = JSON.parse(unescapedJson);
|
||||
showEditMemoryModal(collection, pointId, memory);
|
||||
}
|
||||
|
||||
function showEditMemoryModal(collection, pointId, memoryData) {
|
||||
const memory = typeof memoryData === 'string' ? JSON.parse(memoryData) : memoryData;
|
||||
currentEditMemory = { collection, pointId, memory };
|
||||
|
||||
const modal = document.getElementById('edit-memory-modal');
|
||||
const contentField = document.getElementById('edit-memory-content');
|
||||
const sourceField = document.getElementById('edit-memory-source');
|
||||
|
||||
contentField.value = memory.content || '';
|
||||
sourceField.value = memory.metadata?.source || '';
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeEditMemoryModal() {
|
||||
document.getElementById('edit-memory-modal').style.display = 'none';
|
||||
currentEditMemory = null;
|
||||
}
|
||||
|
||||
async function saveMemoryEdit() {
|
||||
if (!currentEditMemory) return;
|
||||
|
||||
const content = document.getElementById('edit-memory-content').value.trim();
|
||||
const source = document.getElementById('edit-memory-source').value.trim();
|
||||
|
||||
if (!content) {
|
||||
showNotification('Content cannot be empty', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const { collection, pointId } = currentEditMemory;
|
||||
const saveBtn = document.querySelector('#edit-memory-modal button[onclick="saveMemoryEdit()"]');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/memory/point/${collection}/${pointId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
metadata: { source: source || 'manual_edit' }
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('Memory updated successfully', 'success');
|
||||
closeEditMemoryModal();
|
||||
// Reload the appropriate list
|
||||
if (collection === 'declarative') {
|
||||
loadFacts();
|
||||
} else if (collection === 'episodic') {
|
||||
loadEpisodicMemories();
|
||||
}
|
||||
} else {
|
||||
showNotification('Failed to update: ' + (data.error || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showNotification('Error: ' + err.message, 'error');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Save Changes';
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateMemoryModal(collection) {
|
||||
const modal = document.getElementById('create-memory-modal');
|
||||
document.getElementById('create-memory-collection').value = collection;
|
||||
document.getElementById('create-memory-content').value = '';
|
||||
document.getElementById('create-memory-user-id').value = '';
|
||||
document.getElementById('create-memory-source').value = 'manual';
|
||||
|
||||
// Update modal title based on collection type
|
||||
const title = collection === 'declarative' ? 'Add New Fact' : 'Add New Memory';
|
||||
document.querySelector('#create-memory-modal h3').textContent = title;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeCreateMemoryModal() {
|
||||
document.getElementById('create-memory-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function saveNewMemory() {
|
||||
const collection = document.getElementById('create-memory-collection').value;
|
||||
const content = document.getElementById('create-memory-content').value.trim();
|
||||
const userId = document.getElementById('create-memory-user-id').value.trim();
|
||||
const source = document.getElementById('create-memory-source').value.trim();
|
||||
|
||||
if (!content) {
|
||||
showNotification('Content cannot be empty', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const createBtn = document.querySelector('#create-memory-modal button[onclick="saveNewMemory()"]');
|
||||
createBtn.disabled = true;
|
||||
createBtn.textContent = 'Creating...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/memory/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
collection: collection,
|
||||
content: content,
|
||||
user_id: userId || null,
|
||||
source: source || 'manual',
|
||||
metadata: {}
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`${collection === 'declarative' ? 'Fact' : 'Memory'} created successfully`, 'success');
|
||||
closeCreateMemoryModal();
|
||||
// Reload the appropriate list
|
||||
if (collection === 'declarative') {
|
||||
loadFacts();
|
||||
} else if (collection === 'episodic') {
|
||||
loadEpisodicMemories();
|
||||
}
|
||||
refreshMemoryStats();
|
||||
} else {
|
||||
showNotification('Failed to create: ' + (data.error || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showNotification('Error: ' + err.message, 'error');
|
||||
} finally {
|
||||
createBtn.disabled = false;
|
||||
createBtn.textContent = 'Create Memory';
|
||||
}
|
||||
}
|
||||
|
||||
// Search/Filter Function
|
||||
function filterMemories(listId, searchTerm) {
|
||||
const listDiv = document.getElementById(listId);
|
||||
const items = listDiv.querySelectorAll('.memory-item');
|
||||
const term = searchTerm.toLowerCase().trim();
|
||||
|
||||
items.forEach(item => {
|
||||
const content = item.textContent.toLowerCase();
|
||||
if (term === '' || content.includes(term)) {
|
||||
item.style.display = 'flex';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
@@ -5450,6 +5701,16 @@ function escapeHtml(str) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsonForAttribute(obj) {
|
||||
// Properly escape JSON for use in HTML attributes
|
||||
return JSON.stringify(obj)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user