| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>mem0 dashboard</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap" rel="stylesheet">
- <style>
- :root {
- --bg: #0a0a0a;
- --surface: #111111;
- --surface2: #181818;
- --border: #242424;
- --border2: #2e2e2e;
- --text: #d4d0c8;
- --muted: #555550;
- --accent: #c8a96e;
- --accent2: #6e9ec8;
- --danger: #c86e6e;
- --ok: #6ec87a;
- --mono: 'IBM Plex Mono', monospace;
- --sans: 'IBM Plex Sans', sans-serif;
- }
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
- body {
- background: var(--bg);
- color: var(--text);
- font-family: var(--sans);
- font-size: 14px;
- font-weight: 300;
- min-height: 100vh;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
- }
- /* ── HEADER ─────────────────────────────────────────────── */
- header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 18px 32px;
- border-bottom: 1px solid var(--border);
- background: var(--surface);
- position: sticky;
- top: 0;
- z-index: 100;
- }
- .header-left { display: flex; align-items: baseline; gap: 14px; }
- .logo {
- font-family: var(--mono);
- font-size: 15px;
- font-weight: 500;
- letter-spacing: 0.08em;
- color: var(--accent);
- }
- .logo span { color: var(--muted); font-weight: 300; }
- .tagline { font-family: var(--mono); font-size: 11px; color: var(--muted); letter-spacing: 0.04em; }
- .header-right { display: flex; align-items: center; gap: 20px; }
- .status-pill {
- font-family: var(--mono);
- font-size: 11px;
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 4px 10px;
- border: 1px solid var(--border2);
- border-radius: 2px;
- background: var(--surface2);
- }
- .status-dot {
- width: 6px; height: 6px;
- border-radius: 50%;
- background: var(--muted);
- transition: background 0.3s;
- }
- .status-dot.ok { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
- .status-dot.err { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
- .btn-refresh {
- font-family: var(--mono);
- font-size: 11px;
- letter-spacing: 0.06em;
- background: none;
- border: 1px solid var(--border2);
- color: var(--muted);
- padding: 5px 14px;
- cursor: pointer;
- border-radius: 2px;
- transition: color 0.2s, border-color 0.2s;
- }
- .btn-refresh:hover { color: var(--text); border-color: var(--accent); }
- /* ── LAYOUT ─────────────────────────────────────────────── */
- main {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 0;
- max-width: 1400px;
- margin: 0 auto;
- }
- .panel {
- padding: 28px 32px;
- border-right: 1px solid var(--border);
- animation: fadeIn 0.4s ease both;
- }
- .panel:last-child { border-right: none; }
- @keyframes fadeIn {
- from { opacity: 0; transform: translateY(8px); }
- to { opacity: 1; transform: translateY(0); }
- }
- .panel-header {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- margin-bottom: 22px;
- padding-bottom: 14px;
- border-bottom: 1px solid var(--border);
- }
- .panel-title {
- font-family: var(--mono);
- font-size: 11px;
- font-weight: 500;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- color: var(--accent);
- }
- .panel-count { font-family: var(--mono); font-size: 11px; color: var(--muted); }
- /* ── BOOK GROUPS ─────────────────────────────────────────── */
- .book-group {
- border: 1px solid var(--border);
- border-radius: 2px;
- margin-bottom: 10px;
- background: var(--surface);
- overflow: hidden;
- animation: fadeIn 0.3s ease both;
- }
- .book-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 11px 14px;
- cursor: pointer;
- user-select: none;
- transition: background 0.15s;
- }
- .book-header:hover { background: var(--surface2); }
- .book-header-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
- .chevron {
- font-size: 10px;
- color: var(--muted);
- transition: transform 0.2s;
- flex-shrink: 0;
- }
- .book-group.open .chevron { transform: rotate(90deg); }
- .book-name {
- font-family: var(--mono);
- font-size: 12px;
- color: var(--text);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .book-badge {
- font-family: var(--mono);
- font-size: 10px;
- color: var(--muted);
- background: var(--surface2);
- border: 1px solid var(--border);
- padding: 2px 7px;
- border-radius: 2px;
- flex-shrink: 0;
- }
- /* ── SHARED DELETE BUTTON STYLE ──────────────────────────── */
- .btn-del {
- font-family: var(--mono);
- font-size: 10px;
- letter-spacing: 0.05em;
- background: none;
- border: 1px solid transparent;
- color: var(--muted);
- padding: 3px 8px;
- cursor: pointer;
- border-radius: 2px;
- transition: color 0.15s, border-color 0.15s, background 0.15s;
- flex-shrink: 0;
- white-space: nowrap;
- }
- .btn-del:hover { color: var(--danger); border-color: var(--danger); }
- .btn-del.confirming {
- color: var(--danger);
- border-color: var(--danger);
- background: rgba(200, 110, 110, 0.08);
- }
- /* ── BOOK ENTRIES ────────────────────────────────────────── */
- .book-entries {
- display: none;
- border-top: 1px solid var(--border);
- }
- .book-group.open .book-entries { display: block; }
- .entry {
- padding: 10px 14px;
- border-bottom: 1px solid var(--border);
- display: grid;
- grid-template-columns: 1fr auto auto;
- gap: 8px;
- align-items: start;
- transition: opacity 0.3s;
- }
- .entry:last-child { border-bottom: none; }
- .entry-text { font-size: 12px; line-height: 1.5; color: var(--text); }
- .entry-meta {
- font-family: var(--mono);
- font-size: 10px;
- color: var(--muted);
- text-align: right;
- white-space: nowrap;
- padding-top: 2px;
- }
- /* ── CONVERSATIONAL MEMORIES ─────────────────────────────── */
- .memory-item {
- padding: 10px 0;
- border-bottom: 1px solid var(--border);
- display: grid;
- grid-template-columns: 1fr auto auto;
- gap: 10px;
- align-items: start;
- animation: fadeIn 0.3s ease both;
- transition: opacity 0.3s;
- }
- .memory-item:last-child { border-bottom: none; }
- .memory-text { font-size: 13px; line-height: 1.55; color: var(--text); }
- .memory-time {
- font-family: var(--mono);
- font-size: 10px;
- color: var(--muted);
- white-space: nowrap;
- padding-top: 2px;
- }
- .user-section { margin-bottom: 20px; }
- .user-label {
- font-family: var(--mono);
- font-size: 10px;
- letter-spacing: 0.1em;
- color: var(--accent2);
- text-transform: uppercase;
- margin-bottom: 8px;
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .user-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
- /* ── EMPTY / LOADING ─────────────────────────────────────── */
- .state-msg {
- font-family: var(--mono);
- font-size: 11px;
- color: var(--muted);
- padding: 24px 0;
- text-align: center;
- letter-spacing: 0.04em;
- }
- .loading-bar { height: 1px; background: var(--border); overflow: hidden; margin-bottom: 20px; }
- .loading-bar::after {
- content: '';
- display: block;
- height: 100%;
- width: 40%;
- background: var(--accent);
- animation: slide 1.2s ease-in-out infinite;
- }
- @keyframes slide {
- 0% { transform: translateX(-100%); }
- 100% { transform: translateX(350%); }
- }
- /* ── TOAST ───────────────────────────────────────────────── */
- #toast {
- position: fixed;
- bottom: 24px;
- right: 24px;
- font-family: var(--mono);
- font-size: 12px;
- padding: 10px 18px;
- border-radius: 2px;
- border: 1px solid var(--border2);
- background: var(--surface);
- color: var(--text);
- opacity: 0;
- transform: translateY(8px);
- transition: opacity 0.25s, transform 0.25s;
- z-index: 999;
- pointer-events: none;
- }
- #toast.show { opacity: 1; transform: translateY(0); }
- #toast.ok { border-color: var(--ok); color: var(--ok); }
- #toast.err { border-color: var(--danger); color: var(--danger); }
- @media (max-width: 900px) {
- main { grid-template-columns: 1fr; }
- .panel { border-right: none; border-bottom: 1px solid var(--border); }
- }
- </style>
- </head>
- <body>
- <header>
- <div class="header-left">
- <div class="logo">mem0<span>/dashboard</span></div>
- <div class="tagline">192.168.0.200:8420</div>
- </div>
- <div class="header-right">
- <div class="status-pill">
- <div class="status-dot" id="statusDot"></div>
- <span id="statusText" style="font-family:var(--mono);font-size:11px;color:var(--muted)">connecting</span>
- </div>
- <button class="btn-refresh" onclick="loadAll()">⟳ refresh</button>
- </div>
- </header>
- <main>
- <div class="panel" style="animation-delay:0.05s">
- <div class="panel-header">
- <div class="panel-title">knowledge base</div>
- <div class="panel-count" id="knowledgeCount">—</div>
- </div>
- <div id="knowledgeLoading" class="loading-bar"></div>
- <div id="knowledgeContent"></div>
- </div>
- <div class="panel" style="animation-delay:0.1s">
- <div class="panel-header">
- <div class="panel-title">conversational memory</div>
- <div class="panel-count" id="memoriesCount">—</div>
- </div>
- <div id="memoriesLoading" class="loading-bar"></div>
- <div id="memoriesContent"></div>
- </div>
- </main>
- <div id="toast"></div>
- <script>
- const BASE = 'http://192.168.0.200:8420';
- // ── TOAST ──────────────────────────────────────────────────────
- function toast(msg, type = 'ok') {
- const el = document.getElementById('toast');
- el.textContent = msg;
- el.className = `show ${type}`;
- clearTimeout(el._t);
- el._t = setTimeout(() => el.className = '', 2800);
- }
- // ── HEALTH ─────────────────────────────────────────────────────
- async function checkHealth() {
- try {
- const r = await fetch(`${BASE}/health`);
- if (r.ok) {
- document.getElementById('statusDot').className = 'status-dot ok';
- document.getElementById('statusText').textContent = 'online';
- } else { throw new Error(); }
- } catch {
- document.getElementById('statusDot').className = 'status-dot err';
- document.getElementById('statusText').textContent = 'unreachable';
- }
- }
- // ── HELPERS ────────────────────────────────────────────────────
- function fmtDate(iso) {
- if (!iso) return '—';
- const d = new Date(iso);
- return d.toLocaleDateString('en-GB', { day:'2-digit', month:'short' })
- + ' ' + d.toLocaleTimeString('en-GB', { hour:'2-digit', minute:'2-digit' });
- }
- function stopLoading(id) {
- const el = document.getElementById(id);
- if (el) el.style.display = 'none';
- }
- // Shared confirm-on-first-click pattern for any delete button
- function armDeleteBtn(btn, onConfirm) {
- btn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (!btn.classList.contains('confirming')) {
- btn.classList.add('confirming');
- btn.textContent = '✕?';
- setTimeout(() => {
- if (btn.classList.contains('confirming')) {
- btn.classList.remove('confirming');
- btn.textContent = '✕';
- }
- }, 2500);
- } else {
- btn.classList.remove('confirming');
- onConfirm();
- }
- });
- }
- // Fade out and remove a row element
- function fadeRemove(el, onDone) {
- el.style.opacity = '0';
- setTimeout(() => { el.remove(); if (onDone) onDone(); }, 300);
- }
- // ── DELETE A SINGLE MEMORY BY ID (shared for both collections) ──
- async function deleteMemoryById(id, collection, rowEl, onDone) {
- const endpoint = collection === 'knowledge'
- ? `${BASE}/knowledge`
- : `${BASE}/memories`;
- try {
- // mem0 delete expects the memory_id directly
- // We go straight to Chroma + SQLite via the server
- const r = await fetch(`${BASE}/memory/${id}`, {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ memory_id: id, collection })
- });
- if (r.ok) {
- fadeRemove(rowEl, onDone);
- toast('entry deleted');
- } else {
- toast('delete failed', 'err');
- }
- } catch {
- toast('delete failed — server unreachable', 'err');
- }
- }
- // ── KNOWLEDGE ──────────────────────────────────────────────────
- async function loadKnowledge() {
- document.getElementById('knowledgeLoading').style.display = 'block';
- document.getElementById('knowledgeContent').innerHTML = '';
- try {
- const r = await fetch(`${BASE}/knowledge/sources`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ user_id: 'knowledge_base' })
- });
- const data = await r.json();
- stopLoading('knowledgeLoading');
- renderKnowledge(data.sources || [], data.total || 0);
- } catch (e) {
- stopLoading('knowledgeLoading');
- document.getElementById('knowledgeContent').innerHTML =
- '<div class="state-msg">failed to load — is the server reachable?</div>';
- }
- }
- function renderKnowledge(sources, total) {
- const container = document.getElementById('knowledgeContent');
- if (!sources.length) {
- container.innerHTML = '<div class="state-msg">no knowledge entries found</div>';
- document.getElementById('knowledgeCount').textContent = '0 entries';
- return;
- }
- document.getElementById('knowledgeCount').textContent =
- `${total} entries · ${sources.length} books`;
- container.innerHTML = '';
- for (const { source_file, count } of sources) {
- container.appendChild(buildBookGroup(source_file, count));
- }
- }
- async function loadBookEntries(src, entriesDiv) {
- entriesDiv.innerHTML = '<div class="state-msg">loading…</div>';
- try {
- const r = await fetch(`${BASE}/knowledge/search`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- query: src.replace(/\.pdf$/i, '').replace(/[-_]/g, ' '),
- user_id: 'knowledge_base',
- limit: 200
- })
- });
- const data = await r.json();
- const entries = (data.results || []).sort(
- (a, b) => (a.metadata?.page ?? 0) - (b.metadata?.page ?? 0)
- );
- entriesDiv.innerHTML = '';
- if (!entries.length) {
- entriesDiv.innerHTML = '<div class="state-msg">no entries found</div>';
- return;
- }
- for (const entry of entries) {
- entriesDiv.appendChild(buildEntryRow(entry, entriesDiv));
- }
- } catch {
- entriesDiv.innerHTML = '<div class="state-msg">failed to load entries</div>';
- }
- }
- function buildEntryRow(entry, parentDiv) {
- const row = document.createElement('div');
- row.className = 'entry';
- row.dataset.id = entry.id;
- const page = entry.metadata?.page ? `p.${entry.metadata.page}` : '';
- const ch = entry.metadata?.chapter ? `ch.${entry.metadata.chapter}` : '';
- const loc = [ch, page].filter(Boolean).join(' · ');
- const text = document.createElement('div');
- text.className = 'entry-text';
- text.textContent = entry.memory || '';
- const meta = document.createElement('div');
- meta.className = 'entry-meta';
- meta.innerHTML = `${loc}<br>${fmtDate(entry.created_at)}`;
- const delBtn = document.createElement('button');
- delBtn.className = 'btn-del';
- delBtn.textContent = '✕';
- delBtn.title = 'delete this entry';
- armDeleteBtn(delBtn, () => {
- deleteEntryById(entry.id, row, () => {
- // Update book badge count
- const group = parentDiv.closest('.book-group');
- if (group) {
- const badge = group.querySelector('.book-badge');
- if (badge) badge.textContent = Math.max(0, parseInt(badge.textContent) - 1);
- }
- // Update panel total
- const totalEl = document.getElementById('knowledgeCount');
- const match = totalEl.textContent.match(/(\d+) entries/);
- if (match) {
- const newTotal = Math.max(0, parseInt(match[1]) - 1);
- totalEl.textContent = totalEl.textContent.replace(/\d+ entries/, `${newTotal} entries`);
- }
- });
- });
- row.appendChild(text);
- row.appendChild(meta);
- row.appendChild(delBtn);
- return row;
- }
- async function deleteEntryById(id, rowEl, onDone) {
- try {
- const r = await fetch(`${BASE}/memory/${id}`, {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ collection: 'knowledge' })
- });
- if (r.ok) {
- fadeRemove(rowEl, onDone);
- toast('entry deleted');
- } else {
- const data = await r.json().catch(() => ({}));
- toast(data.error || 'delete failed', 'err');
- }
- } catch {
- toast('delete failed — server unreachable', 'err');
- }
- }
- function buildBookGroup(src, count) {
- const group = document.createElement('div');
- group.className = 'book-group';
- group.dataset.src = src;
- const header = document.createElement('div');
- header.className = 'book-header';
- header.innerHTML = `
- <div class="book-header-left">
- <span class="chevron">▶</span>
- <span class="book-name" title="${src}">${src}</span>
- <span class="book-badge">${count}</span>
- </div>
- <button class="btn-del" data-src="${src}">delete book</button>
- `;
- const entriesDiv = document.createElement('div');
- entriesDiv.className = 'book-entries';
- header.querySelector('.book-header-left').addEventListener('click', () => {
- group.classList.toggle('open');
- if (group.classList.contains('open') && !group.dataset.loaded) {
- group.dataset.loaded = 'true';
- loadBookEntries(src, entriesDiv);
- }
- });
- const delBtn = header.querySelector('.btn-del');
- armDeleteBtn(delBtn, () => deleteBook(src, group));
- group.appendChild(header);
- group.appendChild(entriesDiv);
- return group;
- }
- async function deleteBook(src, groupEl) {
- try {
- const r = await fetch(`${BASE}/knowledge/by-source`, {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ source_file: src, user_id: 'knowledge_base' })
- });
- const data = await r.json();
- if (r.ok && data.deleted > 0) {
- fadeRemove(groupEl, () => {
- const remaining = document.querySelectorAll('.book-group').length;
- const total = [...document.querySelectorAll('.book-badge')]
- .reduce((sum, el) => sum + parseInt(el.textContent || 0), 0);
- document.getElementById('knowledgeCount').textContent =
- `${total} entries · ${remaining} books`;
- });
- toast(`deleted ${data.deleted} entries — ${src}`);
- } else if (data.deleted === 0) {
- toast('nothing to delete — try refreshing first', 'err');
- } else {
- toast('delete failed', 'err');
- }
- } catch {
- toast('delete failed — server unreachable', 'err');
- }
- }
- // ── MEMORIES ───────────────────────────────────────────────────
- async function loadMemories() {
- document.getElementById('memoriesLoading').style.display = 'block';
- document.getElementById('memoriesContent').innerHTML = '';
- const userIds = ['main', 'testuser', 'default'];
- const allResults = {};
- try {
- await Promise.all(userIds.map(async uid => {
- const r = await fetch(`${BASE}/memories/all`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ user_id: uid })
- });
- const data = await r.json();
- const items = data.results || [];
- if (items.length) allResults[uid] = items;
- }));
- stopLoading('memoriesLoading');
- renderMemories(allResults);
- } catch {
- stopLoading('memoriesLoading');
- document.getElementById('memoriesContent').innerHTML =
- '<div class="state-msg">failed to load</div>';
- }
- }
- function renderMemories(byUser) {
- const container = document.getElementById('memoriesContent');
- const allEntries = Object.values(byUser).flat();
- if (!allEntries.length) {
- container.innerHTML = '<div class="state-msg">no memories found</div>';
- document.getElementById('memoriesCount').textContent = '0 memories';
- return;
- }
- document.getElementById('memoriesCount').textContent =
- `${allEntries.length} memories · ${Object.keys(byUser).length} users`;
- container.innerHTML = '';
- for (const [uid, items] of Object.entries(byUser)) {
- const section = document.createElement('div');
- section.className = 'user-section';
- const label = document.createElement('div');
- label.className = 'user-label';
- label.textContent = uid;
- section.appendChild(label);
- for (const item of items) {
- section.appendChild(buildMemoryRow(item, section));
- }
- container.appendChild(section);
- }
- }
- function buildMemoryRow(item, sectionEl) {
- const row = document.createElement('div');
- row.className = 'memory-item';
- row.dataset.id = item.id;
- const text = document.createElement('div');
- text.className = 'memory-text';
- text.textContent = item.memory || '';
- const time = document.createElement('div');
- time.className = 'memory-time';
- time.textContent = fmtDate(item.created_at);
- const delBtn = document.createElement('button');
- delBtn.className = 'btn-del';
- delBtn.textContent = '✕';
- delBtn.title = 'delete this memory';
- armDeleteBtn(delBtn, () => {
- deleteConvMemory(item.id, row, () => {
- // Update panel count
- const countEl = document.getElementById('memoriesCount');
- const match = countEl.textContent.match(/(\d+) memories/);
- if (match) {
- const newCount = Math.max(0, parseInt(match[1]) - 1);
- countEl.textContent = countEl.textContent.replace(/\d+ memories/, `${newCount} memories`);
- }
- });
- });
- row.appendChild(text);
- row.appendChild(time);
- row.appendChild(delBtn);
- return row;
- }
- async function deleteConvMemory(id, rowEl, onDone) {
- try {
- const r = await fetch(`${BASE}/memory/${id}`, {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ collection: 'conversational' })
- });
- if (r.ok) {
- fadeRemove(rowEl, onDone);
- toast('memory deleted');
- } else {
- const data = await r.json().catch(() => ({}));
- toast(data.error || 'delete failed', 'err');
- }
- } catch {
- toast('delete failed — server unreachable', 'err');
- }
- }
- // ── LOAD ALL ───────────────────────────────────────────────────
- async function loadAll() {
- await checkHealth();
- await Promise.all([loadKnowledge(), loadMemories()]);
- }
- loadAll();
- </script>
- </body>
- </html>
|