Ability to edit and add memories from the web UI with fixed escapeHtml

This commit is contained in:
2026-02-10 21:41:28 +02:00
parent fbd940e711
commit 6ba8e19d99

View File

@@ -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(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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, '&amp;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
</script>
</body>