dashboard.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>mem0 dashboard</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com">
  8. <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">
  9. <style>
  10. :root {
  11. --bg: #0a0a0a;
  12. --surface: #111111;
  13. --surface2: #181818;
  14. --border: #242424;
  15. --border2: #2e2e2e;
  16. --text: #d4d0c8;
  17. --muted: #555550;
  18. --accent: #c8a96e;
  19. --accent2: #6e9ec8;
  20. --danger: #c86e6e;
  21. --ok: #6ec87a;
  22. --mono: 'IBM Plex Mono', monospace;
  23. --sans: 'IBM Plex Sans', sans-serif;
  24. }
  25. *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  26. body {
  27. background: var(--bg);
  28. color: var(--text);
  29. font-family: var(--sans);
  30. font-size: 14px;
  31. font-weight: 300;
  32. min-height: 100vh;
  33. 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");
  34. }
  35. /* ── HEADER ─────────────────────────────────────────────── */
  36. header {
  37. display: flex;
  38. align-items: center;
  39. justify-content: space-between;
  40. padding: 18px 32px;
  41. border-bottom: 1px solid var(--border);
  42. background: var(--surface);
  43. position: sticky;
  44. top: 0;
  45. z-index: 100;
  46. }
  47. .header-left { display: flex; align-items: baseline; gap: 14px; }
  48. .logo {
  49. font-family: var(--mono);
  50. font-size: 15px;
  51. font-weight: 500;
  52. letter-spacing: 0.08em;
  53. color: var(--accent);
  54. }
  55. .logo span { color: var(--muted); font-weight: 300; }
  56. .tagline { font-family: var(--mono); font-size: 11px; color: var(--muted); letter-spacing: 0.04em; }
  57. .header-right { display: flex; align-items: center; gap: 20px; }
  58. .status-pill {
  59. font-family: var(--mono);
  60. font-size: 11px;
  61. display: flex;
  62. align-items: center;
  63. gap: 6px;
  64. padding: 4px 10px;
  65. border: 1px solid var(--border2);
  66. border-radius: 2px;
  67. background: var(--surface2);
  68. }
  69. .status-dot {
  70. width: 6px; height: 6px;
  71. border-radius: 50%;
  72. background: var(--muted);
  73. transition: background 0.3s;
  74. }
  75. .status-dot.ok { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
  76. .status-dot.err { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
  77. .btn-refresh {
  78. font-family: var(--mono);
  79. font-size: 11px;
  80. letter-spacing: 0.06em;
  81. background: none;
  82. border: 1px solid var(--border2);
  83. color: var(--muted);
  84. padding: 5px 14px;
  85. cursor: pointer;
  86. border-radius: 2px;
  87. transition: color 0.2s, border-color 0.2s;
  88. }
  89. .btn-refresh:hover { color: var(--text); border-color: var(--accent); }
  90. /* ── LAYOUT ─────────────────────────────────────────────── */
  91. main {
  92. display: grid;
  93. grid-template-columns: 1fr 1fr;
  94. gap: 0;
  95. max-width: 1400px;
  96. margin: 0 auto;
  97. }
  98. .panel {
  99. padding: 28px 32px;
  100. border-right: 1px solid var(--border);
  101. animation: fadeIn 0.4s ease both;
  102. }
  103. .panel:last-child { border-right: none; }
  104. @keyframes fadeIn {
  105. from { opacity: 0; transform: translateY(8px); }
  106. to { opacity: 1; transform: translateY(0); }
  107. }
  108. .panel-header {
  109. display: flex;
  110. align-items: baseline;
  111. justify-content: space-between;
  112. margin-bottom: 22px;
  113. padding-bottom: 14px;
  114. border-bottom: 1px solid var(--border);
  115. }
  116. .panel-title {
  117. font-family: var(--mono);
  118. font-size: 11px;
  119. font-weight: 500;
  120. letter-spacing: 0.12em;
  121. text-transform: uppercase;
  122. color: var(--accent);
  123. }
  124. .panel-count { font-family: var(--mono); font-size: 11px; color: var(--muted); }
  125. /* ── BOOK GROUPS ─────────────────────────────────────────── */
  126. .book-group {
  127. border: 1px solid var(--border);
  128. border-radius: 2px;
  129. margin-bottom: 10px;
  130. background: var(--surface);
  131. overflow: hidden;
  132. animation: fadeIn 0.3s ease both;
  133. }
  134. .book-header {
  135. display: flex;
  136. align-items: center;
  137. justify-content: space-between;
  138. padding: 11px 14px;
  139. cursor: pointer;
  140. user-select: none;
  141. transition: background 0.15s;
  142. }
  143. .book-header:hover { background: var(--surface2); }
  144. .book-header-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
  145. .chevron {
  146. font-size: 10px;
  147. color: var(--muted);
  148. transition: transform 0.2s;
  149. flex-shrink: 0;
  150. }
  151. .book-group.open .chevron { transform: rotate(90deg); }
  152. .book-name {
  153. font-family: var(--mono);
  154. font-size: 12px;
  155. color: var(--text);
  156. white-space: nowrap;
  157. overflow: hidden;
  158. text-overflow: ellipsis;
  159. }
  160. .book-badge {
  161. font-family: var(--mono);
  162. font-size: 10px;
  163. color: var(--muted);
  164. background: var(--surface2);
  165. border: 1px solid var(--border);
  166. padding: 2px 7px;
  167. border-radius: 2px;
  168. flex-shrink: 0;
  169. }
  170. /* ── SHARED DELETE BUTTON STYLE ──────────────────────────── */
  171. .btn-del {
  172. font-family: var(--mono);
  173. font-size: 10px;
  174. letter-spacing: 0.05em;
  175. background: none;
  176. border: 1px solid transparent;
  177. color: var(--muted);
  178. padding: 3px 8px;
  179. cursor: pointer;
  180. border-radius: 2px;
  181. transition: color 0.15s, border-color 0.15s, background 0.15s;
  182. flex-shrink: 0;
  183. white-space: nowrap;
  184. }
  185. .btn-del:hover { color: var(--danger); border-color: var(--danger); }
  186. .btn-del.confirming {
  187. color: var(--danger);
  188. border-color: var(--danger);
  189. background: rgba(200, 110, 110, 0.08);
  190. }
  191. /* ── BOOK ENTRIES ────────────────────────────────────────── */
  192. .book-entries {
  193. display: none;
  194. border-top: 1px solid var(--border);
  195. }
  196. .book-group.open .book-entries { display: block; }
  197. .entry {
  198. padding: 10px 14px;
  199. border-bottom: 1px solid var(--border);
  200. display: grid;
  201. grid-template-columns: 1fr auto auto;
  202. gap: 8px;
  203. align-items: start;
  204. transition: opacity 0.3s;
  205. }
  206. .entry:last-child { border-bottom: none; }
  207. .entry-text { font-size: 12px; line-height: 1.5; color: var(--text); }
  208. .entry-meta {
  209. font-family: var(--mono);
  210. font-size: 10px;
  211. color: var(--muted);
  212. text-align: right;
  213. white-space: nowrap;
  214. padding-top: 2px;
  215. }
  216. /* ── CONVERSATIONAL MEMORIES ─────────────────────────────── */
  217. .memory-item {
  218. padding: 10px 0;
  219. border-bottom: 1px solid var(--border);
  220. display: grid;
  221. grid-template-columns: 1fr auto auto;
  222. gap: 10px;
  223. align-items: start;
  224. animation: fadeIn 0.3s ease both;
  225. transition: opacity 0.3s;
  226. }
  227. .memory-item:last-child { border-bottom: none; }
  228. .memory-text { font-size: 13px; line-height: 1.55; color: var(--text); }
  229. .memory-time {
  230. font-family: var(--mono);
  231. font-size: 10px;
  232. color: var(--muted);
  233. white-space: nowrap;
  234. padding-top: 2px;
  235. }
  236. .user-section { margin-bottom: 20px; }
  237. .user-label {
  238. font-family: var(--mono);
  239. font-size: 10px;
  240. letter-spacing: 0.1em;
  241. color: var(--accent2);
  242. text-transform: uppercase;
  243. margin-bottom: 8px;
  244. display: flex;
  245. align-items: center;
  246. gap: 8px;
  247. }
  248. .user-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
  249. /* ── EMPTY / LOADING ─────────────────────────────────────── */
  250. .state-msg {
  251. font-family: var(--mono);
  252. font-size: 11px;
  253. color: var(--muted);
  254. padding: 24px 0;
  255. text-align: center;
  256. letter-spacing: 0.04em;
  257. }
  258. .loading-bar { height: 1px; background: var(--border); overflow: hidden; margin-bottom: 20px; }
  259. .loading-bar::after {
  260. content: '';
  261. display: block;
  262. height: 100%;
  263. width: 40%;
  264. background: var(--accent);
  265. animation: slide 1.2s ease-in-out infinite;
  266. }
  267. @keyframes slide {
  268. 0% { transform: translateX(-100%); }
  269. 100% { transform: translateX(350%); }
  270. }
  271. /* ── TOAST ───────────────────────────────────────────────── */
  272. #toast {
  273. position: fixed;
  274. bottom: 24px;
  275. right: 24px;
  276. font-family: var(--mono);
  277. font-size: 12px;
  278. padding: 10px 18px;
  279. border-radius: 2px;
  280. border: 1px solid var(--border2);
  281. background: var(--surface);
  282. color: var(--text);
  283. opacity: 0;
  284. transform: translateY(8px);
  285. transition: opacity 0.25s, transform 0.25s;
  286. z-index: 999;
  287. pointer-events: none;
  288. }
  289. #toast.show { opacity: 1; transform: translateY(0); }
  290. #toast.ok { border-color: var(--ok); color: var(--ok); }
  291. #toast.err { border-color: var(--danger); color: var(--danger); }
  292. @media (max-width: 900px) {
  293. main { grid-template-columns: 1fr; }
  294. .panel { border-right: none; border-bottom: 1px solid var(--border); }
  295. }
  296. </style>
  297. </head>
  298. <body>
  299. <header>
  300. <div class="header-left">
  301. <div class="logo">mem0<span>/dashboard</span></div>
  302. <div class="tagline">192.168.0.200:8420</div>
  303. </div>
  304. <div class="header-right">
  305. <div class="status-pill">
  306. <div class="status-dot" id="statusDot"></div>
  307. <span id="statusText" style="font-family:var(--mono);font-size:11px;color:var(--muted)">connecting</span>
  308. </div>
  309. <button class="btn-refresh" onclick="loadAll()">⟳ refresh</button>
  310. </div>
  311. </header>
  312. <main>
  313. <div class="panel" style="animation-delay:0.05s">
  314. <div class="panel-header">
  315. <div class="panel-title">knowledge base</div>
  316. <div class="panel-count" id="knowledgeCount">—</div>
  317. </div>
  318. <div id="knowledgeLoading" class="loading-bar"></div>
  319. <div id="knowledgeContent"></div>
  320. </div>
  321. <div class="panel" style="animation-delay:0.1s">
  322. <div class="panel-header">
  323. <div class="panel-title">conversational memory</div>
  324. <div class="panel-count" id="memoriesCount">—</div>
  325. </div>
  326. <div id="memoriesLoading" class="loading-bar"></div>
  327. <div id="memoriesContent"></div>
  328. </div>
  329. </main>
  330. <div id="toast"></div>
  331. <script>
  332. const BASE = 'http://192.168.0.200:8420';
  333. // ── TOAST ──────────────────────────────────────────────────────
  334. function toast(msg, type = 'ok') {
  335. const el = document.getElementById('toast');
  336. el.textContent = msg;
  337. el.className = `show ${type}`;
  338. clearTimeout(el._t);
  339. el._t = setTimeout(() => el.className = '', 2800);
  340. }
  341. // ── HEALTH ─────────────────────────────────────────────────────
  342. async function checkHealth() {
  343. try {
  344. const r = await fetch(`${BASE}/health`);
  345. if (r.ok) {
  346. document.getElementById('statusDot').className = 'status-dot ok';
  347. document.getElementById('statusText').textContent = 'online';
  348. } else { throw new Error(); }
  349. } catch {
  350. document.getElementById('statusDot').className = 'status-dot err';
  351. document.getElementById('statusText').textContent = 'unreachable';
  352. }
  353. }
  354. // ── HELPERS ────────────────────────────────────────────────────
  355. function fmtDate(iso) {
  356. if (!iso) return '—';
  357. const d = new Date(iso);
  358. return d.toLocaleDateString('en-GB', { day:'2-digit', month:'short' })
  359. + ' ' + d.toLocaleTimeString('en-GB', { hour:'2-digit', minute:'2-digit' });
  360. }
  361. function stopLoading(id) {
  362. const el = document.getElementById(id);
  363. if (el) el.style.display = 'none';
  364. }
  365. // Shared confirm-on-first-click pattern for any delete button
  366. function armDeleteBtn(btn, onConfirm) {
  367. btn.addEventListener('click', (e) => {
  368. e.stopPropagation();
  369. if (!btn.classList.contains('confirming')) {
  370. btn.classList.add('confirming');
  371. btn.textContent = '✕?';
  372. setTimeout(() => {
  373. if (btn.classList.contains('confirming')) {
  374. btn.classList.remove('confirming');
  375. btn.textContent = '✕';
  376. }
  377. }, 2500);
  378. } else {
  379. btn.classList.remove('confirming');
  380. onConfirm();
  381. }
  382. });
  383. }
  384. // Fade out and remove a row element
  385. function fadeRemove(el, onDone) {
  386. el.style.opacity = '0';
  387. setTimeout(() => { el.remove(); if (onDone) onDone(); }, 300);
  388. }
  389. // ── DELETE A SINGLE MEMORY BY ID (shared for both collections) ──
  390. async function deleteMemoryById(id, collection, rowEl, onDone) {
  391. const endpoint = collection === 'knowledge'
  392. ? `${BASE}/knowledge`
  393. : `${BASE}/memories`;
  394. try {
  395. // mem0 delete expects the memory_id directly
  396. // We go straight to Chroma + SQLite via the server
  397. const r = await fetch(`${BASE}/memory/${id}`, {
  398. method: 'DELETE',
  399. headers: { 'Content-Type': 'application/json' },
  400. body: JSON.stringify({ memory_id: id, collection })
  401. });
  402. if (r.ok) {
  403. fadeRemove(rowEl, onDone);
  404. toast('entry deleted');
  405. } else {
  406. toast('delete failed', 'err');
  407. }
  408. } catch {
  409. toast('delete failed — server unreachable', 'err');
  410. }
  411. }
  412. // ── KNOWLEDGE ──────────────────────────────────────────────────
  413. async function loadKnowledge() {
  414. document.getElementById('knowledgeLoading').style.display = 'block';
  415. document.getElementById('knowledgeContent').innerHTML = '';
  416. try {
  417. const r = await fetch(`${BASE}/knowledge/sources`, {
  418. method: 'POST',
  419. headers: { 'Content-Type': 'application/json' },
  420. body: JSON.stringify({ user_id: 'knowledge_base' })
  421. });
  422. const data = await r.json();
  423. stopLoading('knowledgeLoading');
  424. renderKnowledge(data.sources || [], data.total || 0);
  425. } catch (e) {
  426. stopLoading('knowledgeLoading');
  427. document.getElementById('knowledgeContent').innerHTML =
  428. '<div class="state-msg">failed to load — is the server reachable?</div>';
  429. }
  430. }
  431. function renderKnowledge(sources, total) {
  432. const container = document.getElementById('knowledgeContent');
  433. if (!sources.length) {
  434. container.innerHTML = '<div class="state-msg">no knowledge entries found</div>';
  435. document.getElementById('knowledgeCount').textContent = '0 entries';
  436. return;
  437. }
  438. document.getElementById('knowledgeCount').textContent =
  439. `${total} entries · ${sources.length} books`;
  440. container.innerHTML = '';
  441. for (const { source_file, count } of sources) {
  442. container.appendChild(buildBookGroup(source_file, count));
  443. }
  444. }
  445. async function loadBookEntries(src, entriesDiv) {
  446. entriesDiv.innerHTML = '<div class="state-msg">loading…</div>';
  447. try {
  448. const r = await fetch(`${BASE}/knowledge/search`, {
  449. method: 'POST',
  450. headers: { 'Content-Type': 'application/json' },
  451. body: JSON.stringify({
  452. query: src.replace(/\.pdf$/i, '').replace(/[-_]/g, ' '),
  453. user_id: 'knowledge_base',
  454. limit: 200
  455. })
  456. });
  457. const data = await r.json();
  458. const entries = (data.results || []).sort(
  459. (a, b) => (a.metadata?.page ?? 0) - (b.metadata?.page ?? 0)
  460. );
  461. entriesDiv.innerHTML = '';
  462. if (!entries.length) {
  463. entriesDiv.innerHTML = '<div class="state-msg">no entries found</div>';
  464. return;
  465. }
  466. for (const entry of entries) {
  467. entriesDiv.appendChild(buildEntryRow(entry, entriesDiv));
  468. }
  469. } catch {
  470. entriesDiv.innerHTML = '<div class="state-msg">failed to load entries</div>';
  471. }
  472. }
  473. function buildEntryRow(entry, parentDiv) {
  474. const row = document.createElement('div');
  475. row.className = 'entry';
  476. row.dataset.id = entry.id;
  477. const page = entry.metadata?.page ? `p.${entry.metadata.page}` : '';
  478. const ch = entry.metadata?.chapter ? `ch.${entry.metadata.chapter}` : '';
  479. const loc = [ch, page].filter(Boolean).join(' · ');
  480. const text = document.createElement('div');
  481. text.className = 'entry-text';
  482. text.textContent = entry.memory || '';
  483. const meta = document.createElement('div');
  484. meta.className = 'entry-meta';
  485. meta.innerHTML = `${loc}<br>${fmtDate(entry.created_at)}`;
  486. const delBtn = document.createElement('button');
  487. delBtn.className = 'btn-del';
  488. delBtn.textContent = '✕';
  489. delBtn.title = 'delete this entry';
  490. armDeleteBtn(delBtn, () => {
  491. deleteEntryById(entry.id, row, () => {
  492. // Update book badge count
  493. const group = parentDiv.closest('.book-group');
  494. if (group) {
  495. const badge = group.querySelector('.book-badge');
  496. if (badge) badge.textContent = Math.max(0, parseInt(badge.textContent) - 1);
  497. }
  498. // Update panel total
  499. const totalEl = document.getElementById('knowledgeCount');
  500. const match = totalEl.textContent.match(/(\d+) entries/);
  501. if (match) {
  502. const newTotal = Math.max(0, parseInt(match[1]) - 1);
  503. totalEl.textContent = totalEl.textContent.replace(/\d+ entries/, `${newTotal} entries`);
  504. }
  505. });
  506. });
  507. row.appendChild(text);
  508. row.appendChild(meta);
  509. row.appendChild(delBtn);
  510. return row;
  511. }
  512. async function deleteEntryById(id, rowEl, onDone) {
  513. try {
  514. const r = await fetch(`${BASE}/memory/${id}`, {
  515. method: 'DELETE',
  516. headers: { 'Content-Type': 'application/json' },
  517. body: JSON.stringify({ collection: 'knowledge' })
  518. });
  519. if (r.ok) {
  520. fadeRemove(rowEl, onDone);
  521. toast('entry deleted');
  522. } else {
  523. const data = await r.json().catch(() => ({}));
  524. toast(data.error || 'delete failed', 'err');
  525. }
  526. } catch {
  527. toast('delete failed — server unreachable', 'err');
  528. }
  529. }
  530. function buildBookGroup(src, count) {
  531. const group = document.createElement('div');
  532. group.className = 'book-group';
  533. group.dataset.src = src;
  534. const header = document.createElement('div');
  535. header.className = 'book-header';
  536. header.innerHTML = `
  537. <div class="book-header-left">
  538. <span class="chevron">▶</span>
  539. <span class="book-name" title="${src}">${src}</span>
  540. <span class="book-badge">${count}</span>
  541. </div>
  542. <button class="btn-del" data-src="${src}">delete book</button>
  543. `;
  544. const entriesDiv = document.createElement('div');
  545. entriesDiv.className = 'book-entries';
  546. header.querySelector('.book-header-left').addEventListener('click', () => {
  547. group.classList.toggle('open');
  548. if (group.classList.contains('open') && !group.dataset.loaded) {
  549. group.dataset.loaded = 'true';
  550. loadBookEntries(src, entriesDiv);
  551. }
  552. });
  553. const delBtn = header.querySelector('.btn-del');
  554. armDeleteBtn(delBtn, () => deleteBook(src, group));
  555. group.appendChild(header);
  556. group.appendChild(entriesDiv);
  557. return group;
  558. }
  559. async function deleteBook(src, groupEl) {
  560. try {
  561. const r = await fetch(`${BASE}/knowledge/by-source`, {
  562. method: 'DELETE',
  563. headers: { 'Content-Type': 'application/json' },
  564. body: JSON.stringify({ source_file: src, user_id: 'knowledge_base' })
  565. });
  566. const data = await r.json();
  567. if (r.ok && data.deleted > 0) {
  568. fadeRemove(groupEl, () => {
  569. const remaining = document.querySelectorAll('.book-group').length;
  570. const total = [...document.querySelectorAll('.book-badge')]
  571. .reduce((sum, el) => sum + parseInt(el.textContent || 0), 0);
  572. document.getElementById('knowledgeCount').textContent =
  573. `${total} entries · ${remaining} books`;
  574. });
  575. toast(`deleted ${data.deleted} entries — ${src}`);
  576. } else if (data.deleted === 0) {
  577. toast('nothing to delete — try refreshing first', 'err');
  578. } else {
  579. toast('delete failed', 'err');
  580. }
  581. } catch {
  582. toast('delete failed — server unreachable', 'err');
  583. }
  584. }
  585. // ── MEMORIES ───────────────────────────────────────────────────
  586. async function loadMemories() {
  587. document.getElementById('memoriesLoading').style.display = 'block';
  588. document.getElementById('memoriesContent').innerHTML = '';
  589. const userIds = ['main', 'testuser', 'default'];
  590. const allResults = {};
  591. try {
  592. await Promise.all(userIds.map(async uid => {
  593. const r = await fetch(`${BASE}/memories/all`, {
  594. method: 'POST',
  595. headers: { 'Content-Type': 'application/json' },
  596. body: JSON.stringify({ user_id: uid })
  597. });
  598. const data = await r.json();
  599. const items = data.results || [];
  600. if (items.length) allResults[uid] = items;
  601. }));
  602. stopLoading('memoriesLoading');
  603. renderMemories(allResults);
  604. } catch {
  605. stopLoading('memoriesLoading');
  606. document.getElementById('memoriesContent').innerHTML =
  607. '<div class="state-msg">failed to load</div>';
  608. }
  609. }
  610. function renderMemories(byUser) {
  611. const container = document.getElementById('memoriesContent');
  612. const allEntries = Object.values(byUser).flat();
  613. if (!allEntries.length) {
  614. container.innerHTML = '<div class="state-msg">no memories found</div>';
  615. document.getElementById('memoriesCount').textContent = '0 memories';
  616. return;
  617. }
  618. document.getElementById('memoriesCount').textContent =
  619. `${allEntries.length} memories · ${Object.keys(byUser).length} users`;
  620. container.innerHTML = '';
  621. for (const [uid, items] of Object.entries(byUser)) {
  622. const section = document.createElement('div');
  623. section.className = 'user-section';
  624. const label = document.createElement('div');
  625. label.className = 'user-label';
  626. label.textContent = uid;
  627. section.appendChild(label);
  628. for (const item of items) {
  629. section.appendChild(buildMemoryRow(item, section));
  630. }
  631. container.appendChild(section);
  632. }
  633. }
  634. function buildMemoryRow(item, sectionEl) {
  635. const row = document.createElement('div');
  636. row.className = 'memory-item';
  637. row.dataset.id = item.id;
  638. const text = document.createElement('div');
  639. text.className = 'memory-text';
  640. text.textContent = item.memory || '';
  641. const time = document.createElement('div');
  642. time.className = 'memory-time';
  643. time.textContent = fmtDate(item.created_at);
  644. const delBtn = document.createElement('button');
  645. delBtn.className = 'btn-del';
  646. delBtn.textContent = '✕';
  647. delBtn.title = 'delete this memory';
  648. armDeleteBtn(delBtn, () => {
  649. deleteConvMemory(item.id, row, () => {
  650. // Update panel count
  651. const countEl = document.getElementById('memoriesCount');
  652. const match = countEl.textContent.match(/(\d+) memories/);
  653. if (match) {
  654. const newCount = Math.max(0, parseInt(match[1]) - 1);
  655. countEl.textContent = countEl.textContent.replace(/\d+ memories/, `${newCount} memories`);
  656. }
  657. });
  658. });
  659. row.appendChild(text);
  660. row.appendChild(time);
  661. row.appendChild(delBtn);
  662. return row;
  663. }
  664. async function deleteConvMemory(id, rowEl, onDone) {
  665. try {
  666. const r = await fetch(`${BASE}/memory/${id}`, {
  667. method: 'DELETE',
  668. headers: { 'Content-Type': 'application/json' },
  669. body: JSON.stringify({ collection: 'conversational' })
  670. });
  671. if (r.ok) {
  672. fadeRemove(rowEl, onDone);
  673. toast('memory deleted');
  674. } else {
  675. const data = await r.json().catch(() => ({}));
  676. toast(data.error || 'delete failed', 'err');
  677. }
  678. } catch {
  679. toast('delete failed — server unreachable', 'err');
  680. }
  681. }
  682. // ── LOAD ALL ───────────────────────────────────────────────────
  683. async function loadAll() {
  684. await checkHealth();
  685. await Promise.all([loadKnowledge(), loadMemories()]);
  686. }
  687. loadAll();
  688. </script>
  689. </body>
  690. </html>