Size: 7.0 KB Modified: 6/07/2026 4:05 AM
@page "/memory"
@model MemoryPageModel
@{
    ViewData["Title"] = "Memory - KaiChat";
}

<div class="page-header">
    <a href="/" class="back-link">&larr; Back to Chat</a>
    <h1>Persistent Memory</h1>
    <p class="page-subtitle">Directory-backed memory files loaded into KaiChat context.</p>
</div>

<div class="memory-page">
    <div class="memory-toolbar">
        <button class="btn" onclick="loadMemory()">Refresh</button>
        <label class="memory-instruct-label">
            <input type="checkbox" id="includeTrash" onchange="loadMemory()" />
            Include trash
        </label>
    </div>

    <div class="memory-update-panel" id="memoryUpdatePanel">
        <div class="memory-update-status">
            <span class="memory-update-label">Memory update</span>
            <span class="memory-update-state" id="memoryUpdateState">Checking...</span>
            <span class="memory-update-detail" id="memoryUpdateDetail"></span>
        </div>
        <button type="button" id="memoryUpdateForceBtn" class="btn btn-memory-update" title="Run memory update now">Run now</button>
        <button type="button" id="memoryUpdateResetFuseBtn" class="btn btn-memory-update" title="Reset failure fuse and run now" style="display:none">Reset fuse + run now</button>
    </div>

    <div class="memory-instruct-form">
        <p class="memory-instruct-label">Kai can create, update, and soft-delete memory files through chat. Deletions move files to <code>_trash</code> with a tombstone.</p>
    </div>

    <button class="btn" onclick="showAddFile()" style="margin-bottom:12px">+ Add Memory File</button>

    <div id="addFileForm" style="display:none" class="memory-section">
        <div class="memory-section-header">
            <h3>New Memory File</h3>
        </div>
        <div class="memory-edit-form" style="display:flex">
            <input type="text" id="newFilePath" class="form-input" placeholder="recent.md" />
            <textarea id="newFileContent" class="form-input memory-textarea" placeholder="# Recent Memory&#10;&#10;..."></textarea>
            <div class="memory-edit-actions">
                <button class="btn btn-primary" onclick="saveNewFile()">Save</button>
                <button class="btn" onclick="hideAddFile()">Cancel</button>
            </div>
        </div>
    </div>

    <div id="memoryContent" class="memory-content">
        <div class="conv-loading">Loading memory...</div>
    </div>
</div>

<script>
function loadMemory() {
    var includeTrash = document.getElementById('includeTrash').checked;
    fetch('/api/memory/files?includeTrash=' + encodeURIComponent(includeTrash))
        .then(function(r) { return r.json(); })
        .then(function(files) {
            var container = document.getElementById('memoryContent');
            if (!files || files.length === 0) {
                container.innerHTML = '<div class="memory-empty">No memory files yet.</div>';
                return;
            }

            container.innerHTML = '';
            files.forEach(function(file, idx) {
                var section = document.createElement('div');
                section.className = 'memory-section';
                section.dataset.path = file.path;

                var actions = '<button class="btn-action" onclick="editFile(' + idx + ')" title="Edit">E</button>';
                if (!file.isTrash) {
                    actions += '<button class="btn-action" onclick="deleteFile(' + idx + ')" title="Soft delete">X</button>';
                }

                section.innerHTML =
                    '<div class="memory-section-header">'
                    + '<h3>' + escapeHtml(file.path) + '</h3>'
                    + '<div class="memory-section-actions">' + actions + '</div>'
                    + '</div>'
                    + '<div class="memory-section-body" id="memoryBody' + idx + '">'
                    + '<pre class="memory-pre">Loading...</pre>'
                    + '</div>'
                    + '<div class="memory-edit-form" id="memoryEdit' + idx + '" style="display:none">'
                    + '<textarea id="memoryEditInput' + idx + '" class="form-input memory-textarea"></textarea>'
                    + '<div class="memory-edit-actions">'
                    + '<button class="btn btn-primary" onclick="saveEditFile(' + idx + ')">Save</button>'
                    + '<button class="btn" onclick="cancelEditFile(' + idx + ')">Cancel</button>'
                    + '</div></div>';

                container.appendChild(section);
                readFile(file.path, idx);
            });

            window._memoryFiles = files;
        });
}

function readFile(path, idx) {
    fetch('/api/memory/file/read?path=' + encodeURIComponent(path))
        .then(function(r) { return r.json(); })
        .then(function(data) {
            var content = data.content || '';
            document.querySelector('#memoryBody' + idx + ' .memory-pre').textContent = content;
            document.getElementById('memoryEditInput' + idx).value = content;
        });
}

function editFile(idx) {
    document.getElementById('memoryBody' + idx).style.display = 'none';
    document.getElementById('memoryEdit' + idx).style.display = 'block';
}

function cancelEditFile(idx) {
    document.getElementById('memoryBody' + idx).style.display = 'block';
    document.getElementById('memoryEdit' + idx).style.display = 'none';
}

function saveEditFile(idx) {
    var file = window._memoryFiles[idx];
    var content = document.getElementById('memoryEditInput' + idx).value;
    saveFile(file.path, content).then(loadMemory);
}

function deleteFile(idx) {
    var file = window._memoryFiles[idx];
    var reason = prompt('Reason for soft-deleting ' + file.path + '?');
    if (!reason || !reason.trim()) return;

    fetch('/api/memory/file/delete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ path: file.path, reason: reason.trim() })
    }).then(function() { loadMemory(); });
}

function showAddFile() {
    document.getElementById('addFileForm').style.display = 'block';
}

function hideAddFile() {
    document.getElementById('addFileForm').style.display = 'none';
}

function saveNewFile() {
    var path = document.getElementById('newFilePath').value.trim();
    var content = document.getElementById('newFileContent').value;
    if (!path || !content.trim()) return;

    saveFile(path, content).then(function() {
        document.getElementById('newFilePath').value = '';
        document.getElementById('newFileContent').value = '';
        hideAddFile();
        loadMemory();
    });
}

function saveFile(path, content) {
    return fetch('/api/memory/file/save', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ path: path, content: content })
    }).then(function(r) { return r.json(); });
}

function escapeHtml(text) {
    var d = document.createElement('div');
    d.textContent = text;
    return d.innerHTML;
}

document.addEventListener('DOMContentLoaded', loadMemory);
</script>
Offline