|
@@ -32,7 +32,6 @@
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
font-weight: 300;
|
|
font-weight: 300;
|
|
|
min-height: 100vh;
|
|
min-height: 100vh;
|
|
|
- /* subtle noise texture */
|
|
|
|
|
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");
|
|
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");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -48,13 +47,7 @@
|
|
|
top: 0;
|
|
top: 0;
|
|
|
z-index: 100;
|
|
z-index: 100;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .header-left {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: baseline;
|
|
|
|
|
- gap: 14px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .header-left { display: flex; align-items: baseline; gap: 14px; }
|
|
|
.logo {
|
|
.logo {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 15px;
|
|
font-size: 15px;
|
|
@@ -62,22 +55,9 @@
|
|
|
letter-spacing: 0.08em;
|
|
letter-spacing: 0.08em;
|
|
|
color: var(--accent);
|
|
color: var(--accent);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
.logo span { color: var(--muted); font-weight: 300; }
|
|
.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;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .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 {
|
|
.status-pill {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
@@ -89,16 +69,14 @@
|
|
|
border-radius: 2px;
|
|
border-radius: 2px;
|
|
|
background: var(--surface2);
|
|
background: var(--surface2);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
.status-dot {
|
|
.status-dot {
|
|
|
width: 6px; height: 6px;
|
|
width: 6px; height: 6px;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
background: var(--muted);
|
|
background: var(--muted);
|
|
|
transition: background 0.3s;
|
|
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); }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .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 {
|
|
.btn-refresh {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
@@ -121,7 +99,6 @@
|
|
|
max-width: 1400px;
|
|
max-width: 1400px;
|
|
|
margin: 0 auto;
|
|
margin: 0 auto;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
.panel {
|
|
.panel {
|
|
|
padding: 28px 32px;
|
|
padding: 28px 32px;
|
|
|
border-right: 1px solid var(--border);
|
|
border-right: 1px solid var(--border);
|
|
@@ -142,7 +119,6 @@
|
|
|
padding-bottom: 14px;
|
|
padding-bottom: 14px;
|
|
|
border-bottom: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
.panel-title {
|
|
.panel-title {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
@@ -151,12 +127,7 @@
|
|
|
text-transform: uppercase;
|
|
text-transform: uppercase;
|
|
|
color: var(--accent);
|
|
color: var(--accent);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .panel-count {
|
|
|
|
|
- font-family: var(--mono);
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- color: var(--muted);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .panel-count { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
|
|
|
|
|
|
|
/* ── BOOK GROUPS ─────────────────────────────────────────── */
|
|
/* ── BOOK GROUPS ─────────────────────────────────────────── */
|
|
|
.book-group {
|
|
.book-group {
|
|
@@ -167,7 +138,6 @@
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
animation: fadeIn 0.3s ease both;
|
|
animation: fadeIn 0.3s ease both;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
.book-header {
|
|
.book-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -178,14 +148,7 @@
|
|
|
transition: background 0.15s;
|
|
transition: background 0.15s;
|
|
|
}
|
|
}
|
|
|
.book-header:hover { background: var(--surface2); }
|
|
.book-header:hover { background: var(--surface2); }
|
|
|
-
|
|
|
|
|
- .book-header-left {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .book-header-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
|
.chevron {
|
|
.chevron {
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
|
color: var(--muted);
|
|
color: var(--muted);
|
|
@@ -193,7 +156,6 @@
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
.book-group.open .chevron { transform: rotate(90deg); }
|
|
.book-group.open .chevron { transform: rotate(90deg); }
|
|
|
-
|
|
|
|
|
.book-name {
|
|
.book-name {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
@@ -202,7 +164,6 @@
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
.book-badge {
|
|
.book-badge {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
@@ -214,29 +175,29 @@
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .btn-delete-book {
|
|
|
|
|
|
|
+ /* ── SHARED DELETE BUTTON STYLE ──────────────────────────── */
|
|
|
|
|
+ .btn-del {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
|
letter-spacing: 0.05em;
|
|
letter-spacing: 0.05em;
|
|
|
background: none;
|
|
background: none;
|
|
|
border: 1px solid transparent;
|
|
border: 1px solid transparent;
|
|
|
color: var(--muted);
|
|
color: var(--muted);
|
|
|
- padding: 3px 10px;
|
|
|
|
|
|
|
+ padding: 3px 8px;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
border-radius: 2px;
|
|
border-radius: 2px;
|
|
|
- transition: color 0.2s, border-color 0.2s;
|
|
|
|
|
|
|
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
- .btn-delete-book:hover {
|
|
|
|
|
- color: var(--danger);
|
|
|
|
|
- border-color: var(--danger);
|
|
|
|
|
- }
|
|
|
|
|
- .btn-delete-book.confirming {
|
|
|
|
|
|
|
+ .btn-del:hover { color: var(--danger); border-color: var(--danger); }
|
|
|
|
|
+ .btn-del.confirming {
|
|
|
color: var(--danger);
|
|
color: var(--danger);
|
|
|
border-color: var(--danger);
|
|
border-color: var(--danger);
|
|
|
background: rgba(200, 110, 110, 0.08);
|
|
background: rgba(200, 110, 110, 0.08);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /* ── BOOK ENTRIES ────────────────────────────────────────── */
|
|
|
.book-entries {
|
|
.book-entries {
|
|
|
display: none;
|
|
display: none;
|
|
|
border-top: 1px solid var(--border);
|
|
border-top: 1px solid var(--border);
|
|
@@ -247,44 +208,35 @@
|
|
|
padding: 10px 14px;
|
|
padding: 10px 14px;
|
|
|
border-bottom: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- grid-template-columns: 1fr auto;
|
|
|
|
|
|
|
+ grid-template-columns: 1fr auto auto;
|
|
|
gap: 8px;
|
|
gap: 8px;
|
|
|
align-items: start;
|
|
align-items: start;
|
|
|
|
|
+ transition: opacity 0.3s;
|
|
|
}
|
|
}
|
|
|
.entry:last-child { border-bottom: none; }
|
|
.entry:last-child { border-bottom: none; }
|
|
|
-
|
|
|
|
|
- .entry-text {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- line-height: 1.5;
|
|
|
|
|
- color: var(--text);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .entry-text { font-size: 12px; line-height: 1.5; color: var(--text); }
|
|
|
.entry-meta {
|
|
.entry-meta {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
|
color: var(--muted);
|
|
color: var(--muted);
|
|
|
text-align: right;
|
|
text-align: right;
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
|
|
+ padding-top: 2px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* ── CONVERSATIONAL MEMORIES ─────────────────────────────── */
|
|
/* ── CONVERSATIONAL MEMORIES ─────────────────────────────── */
|
|
|
.memory-item {
|
|
.memory-item {
|
|
|
- padding: 12px 0;
|
|
|
|
|
|
|
+ padding: 10px 0;
|
|
|
border-bottom: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- grid-template-columns: 1fr auto;
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
|
|
+ grid-template-columns: 1fr auto auto;
|
|
|
|
|
+ gap: 10px;
|
|
|
align-items: start;
|
|
align-items: start;
|
|
|
animation: fadeIn 0.3s ease both;
|
|
animation: fadeIn 0.3s ease both;
|
|
|
|
|
+ transition: opacity 0.3s;
|
|
|
}
|
|
}
|
|
|
.memory-item:last-child { border-bottom: none; }
|
|
.memory-item:last-child { border-bottom: none; }
|
|
|
-
|
|
|
|
|
- .memory-text {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- line-height: 1.55;
|
|
|
|
|
- color: var(--text);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .memory-text { font-size: 13px; line-height: 1.55; color: var(--text); }
|
|
|
.memory-time {
|
|
.memory-time {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
@@ -293,10 +245,7 @@
|
|
|
padding-top: 2px;
|
|
padding-top: 2px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .user-section {
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .user-section { margin-bottom: 20px; }
|
|
|
.user-label {
|
|
.user-label {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
@@ -308,14 +257,9 @@
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
gap: 8px;
|
|
|
}
|
|
}
|
|
|
- .user-label::after {
|
|
|
|
|
- content: '';
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- height: 1px;
|
|
|
|
|
- background: var(--border);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .user-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
|
|
|
|
|
|
|
- /* ── EMPTY / LOADING STATES ──────────────────────────────── */
|
|
|
|
|
|
|
+ /* ── EMPTY / LOADING ─────────────────────────────────────── */
|
|
|
.state-msg {
|
|
.state-msg {
|
|
|
font-family: var(--mono);
|
|
font-family: var(--mono);
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
@@ -324,13 +268,7 @@
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
letter-spacing: 0.04em;
|
|
letter-spacing: 0.04em;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .loading-bar {
|
|
|
|
|
- height: 1px;
|
|
|
|
|
- background: var(--border);
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .loading-bar { height: 1px; background: var(--border); overflow: hidden; margin-bottom: 20px; }
|
|
|
.loading-bar::after {
|
|
.loading-bar::after {
|
|
|
content: '';
|
|
content: '';
|
|
|
display: block;
|
|
display: block;
|
|
@@ -363,10 +301,9 @@
|
|
|
pointer-events: none;
|
|
pointer-events: none;
|
|
|
}
|
|
}
|
|
|
#toast.show { opacity: 1; transform: translateY(0); }
|
|
#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); }
|
|
|
|
|
|
|
+ #toast.ok { border-color: var(--ok); color: var(--ok); }
|
|
|
|
|
+ #toast.err { border-color: var(--danger); color: var(--danger); }
|
|
|
|
|
|
|
|
- /* ── RESPONSIVE ──────────────────────────────────────────── */
|
|
|
|
|
@media (max-width: 900px) {
|
|
@media (max-width: 900px) {
|
|
|
main { grid-template-columns: 1fr; }
|
|
main { grid-template-columns: 1fr; }
|
|
|
.panel { border-right: none; border-bottom: 1px solid var(--border); }
|
|
.panel { border-right: none; border-bottom: 1px solid var(--border); }
|
|
@@ -390,7 +327,6 @@
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
|
<main>
|
|
<main>
|
|
|
- <!-- ── KNOWLEDGE PANEL ── -->
|
|
|
|
|
<div class="panel" style="animation-delay:0.05s">
|
|
<div class="panel" style="animation-delay:0.05s">
|
|
|
<div class="panel-header">
|
|
<div class="panel-header">
|
|
|
<div class="panel-title">knowledge base</div>
|
|
<div class="panel-title">knowledge base</div>
|
|
@@ -400,7 +336,6 @@
|
|
|
<div id="knowledgeContent"></div>
|
|
<div id="knowledgeContent"></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- ── MEMORIES PANEL ── -->
|
|
|
|
|
<div class="panel" style="animation-delay:0.1s">
|
|
<div class="panel" style="animation-delay:0.1s">
|
|
|
<div class="panel-header">
|
|
<div class="panel-header">
|
|
|
<div class="panel-title">conversational memory</div>
|
|
<div class="panel-title">conversational memory</div>
|
|
@@ -429,14 +364,10 @@ function toast(msg, type = 'ok') {
|
|
|
async function checkHealth() {
|
|
async function checkHealth() {
|
|
|
try {
|
|
try {
|
|
|
const r = await fetch(`${BASE}/health`);
|
|
const r = await fetch(`${BASE}/health`);
|
|
|
- const dot = document.getElementById('statusDot');
|
|
|
|
|
- const txt = document.getElementById('statusText');
|
|
|
|
|
if (r.ok) {
|
|
if (r.ok) {
|
|
|
- dot.className = 'status-dot ok';
|
|
|
|
|
- txt.textContent = 'online';
|
|
|
|
|
- } else {
|
|
|
|
|
- throw new Error();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ document.getElementById('statusDot').className = 'status-dot ok';
|
|
|
|
|
+ document.getElementById('statusText').textContent = 'online';
|
|
|
|
|
+ } else { throw new Error(); }
|
|
|
} catch {
|
|
} catch {
|
|
|
document.getElementById('statusDot').className = 'status-dot err';
|
|
document.getElementById('statusDot').className = 'status-dot err';
|
|
|
document.getElementById('statusText').textContent = 'unreachable';
|
|
document.getElementById('statusText').textContent = 'unreachable';
|
|
@@ -456,23 +387,69 @@ function stopLoading(id) {
|
|
|
if (el) el.style.display = 'none';
|
|
if (el) el.style.display = 'none';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ── KNOWLEDGE ──────────────────────────────────────────────────
|
|
|
|
|
|
|
+// 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);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// Phase 1: fetch just enough to build the book list (headers only)
|
|
|
|
|
|
|
+// ── 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() {
|
|
async function loadKnowledge() {
|
|
|
document.getElementById('knowledgeLoading').style.display = 'block';
|
|
document.getElementById('knowledgeLoading').style.display = 'block';
|
|
|
document.getElementById('knowledgeContent').innerHTML = '';
|
|
document.getElementById('knowledgeContent').innerHTML = '';
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- const r = await fetch(`${BASE}/knowledge/recent`, {
|
|
|
|
|
|
|
+ const r = await fetch(`${BASE}/knowledge/sources`, {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ user_id: 'knowledge_base', limit: 50 })
|
|
|
|
|
|
|
+ body: JSON.stringify({ user_id: 'knowledge_base' })
|
|
|
});
|
|
});
|
|
|
const data = await r.json();
|
|
const data = await r.json();
|
|
|
- const items = data.results || [];
|
|
|
|
|
stopLoading('knowledgeLoading');
|
|
stopLoading('knowledgeLoading');
|
|
|
- renderKnowledge(items);
|
|
|
|
|
|
|
+ renderKnowledge(data.sources || [], data.total || 0);
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
stopLoading('knowledgeLoading');
|
|
stopLoading('knowledgeLoading');
|
|
|
document.getElementById('knowledgeContent').innerHTML =
|
|
document.getElementById('knowledgeContent').innerHTML =
|
|
@@ -480,34 +457,21 @@ async function loadKnowledge() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function renderKnowledge(items) {
|
|
|
|
|
|
|
+function renderKnowledge(sources, total) {
|
|
|
const container = document.getElementById('knowledgeContent');
|
|
const container = document.getElementById('knowledgeContent');
|
|
|
-
|
|
|
|
|
- if (!items.length) {
|
|
|
|
|
|
|
+ if (!sources.length) {
|
|
|
container.innerHTML = '<div class="state-msg">no knowledge entries found</div>';
|
|
container.innerHTML = '<div class="state-msg">no knowledge entries found</div>';
|
|
|
document.getElementById('knowledgeCount').textContent = '0 entries';
|
|
document.getElementById('knowledgeCount').textContent = '0 entries';
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Group by source_file to build headers
|
|
|
|
|
- const groups = {};
|
|
|
|
|
- for (const item of items) {
|
|
|
|
|
- const src = item.metadata?.source_file || '(no source)';
|
|
|
|
|
- if (!groups[src]) groups[src] = 0;
|
|
|
|
|
- groups[src]++;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const sorted = Object.entries(groups).sort((a, b) => b[1] - a[1]);
|
|
|
|
|
document.getElementById('knowledgeCount').textContent =
|
|
document.getElementById('knowledgeCount').textContent =
|
|
|
- `${items.length}+ entries · ${sorted.length} books`;
|
|
|
|
|
-
|
|
|
|
|
|
|
+ `${total} entries · ${sources.length} books`;
|
|
|
container.innerHTML = '';
|
|
container.innerHTML = '';
|
|
|
- for (const [src, count] of sorted) {
|
|
|
|
|
- container.appendChild(buildBookGroup(src, count));
|
|
|
|
|
|
|
+ for (const { source_file, count } of sources) {
|
|
|
|
|
+ container.appendChild(buildBookGroup(source_file, count));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Phase 2: fetch entries for a specific book on expand (lazy)
|
|
|
|
|
async function loadBookEntries(src, entriesDiv) {
|
|
async function loadBookEntries(src, entriesDiv) {
|
|
|
entriesDiv.innerHTML = '<div class="state-msg">loading…</div>';
|
|
entriesDiv.innerHTML = '<div class="state-msg">loading…</div>';
|
|
|
try {
|
|
try {
|
|
@@ -530,22 +494,78 @@ async function loadBookEntries(src, entriesDiv) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
for (const entry of entries) {
|
|
for (const entry of entries) {
|
|
|
- const row = document.createElement('div');
|
|
|
|
|
- row.className = 'entry';
|
|
|
|
|
- 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(' · ');
|
|
|
|
|
- row.innerHTML = `
|
|
|
|
|
- <div class="entry-text">${entry.memory || ''}</div>
|
|
|
|
|
- <div class="entry-meta">${loc}<br>${fmtDate(entry.created_at)}</div>
|
|
|
|
|
- `;
|
|
|
|
|
- entriesDiv.appendChild(row);
|
|
|
|
|
|
|
+ entriesDiv.appendChild(buildEntryRow(entry, entriesDiv));
|
|
|
}
|
|
}
|
|
|
- } catch (e) {
|
|
|
|
|
|
|
+ } catch {
|
|
|
entriesDiv.innerHTML = '<div class="state-msg">failed to load entries</div>';
|
|
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) {
|
|
function buildBookGroup(src, count) {
|
|
|
const group = document.createElement('div');
|
|
const group = document.createElement('div');
|
|
|
group.className = 'book-group';
|
|
group.className = 'book-group';
|
|
@@ -559,13 +579,12 @@ function buildBookGroup(src, count) {
|
|
|
<span class="book-name" title="${src}">${src}</span>
|
|
<span class="book-name" title="${src}">${src}</span>
|
|
|
<span class="book-badge">${count}</span>
|
|
<span class="book-badge">${count}</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <button class="btn-delete-book" data-src="${src}">delete</button>
|
|
|
|
|
|
|
+ <button class="btn-del" data-src="${src}">delete book</button>
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
const entriesDiv = document.createElement('div');
|
|
const entriesDiv = document.createElement('div');
|
|
|
entriesDiv.className = 'book-entries';
|
|
entriesDiv.className = 'book-entries';
|
|
|
|
|
|
|
|
- // Lazy load on first expand
|
|
|
|
|
header.querySelector('.book-header-left').addEventListener('click', () => {
|
|
header.querySelector('.book-header-left').addEventListener('click', () => {
|
|
|
group.classList.toggle('open');
|
|
group.classList.toggle('open');
|
|
|
if (group.classList.contains('open') && !group.dataset.loaded) {
|
|
if (group.classList.contains('open') && !group.dataset.loaded) {
|
|
@@ -574,21 +593,8 @@ function buildBookGroup(src, count) {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // Delete — confirm on first click, execute on second
|
|
|
|
|
- const delBtn = header.querySelector('.btn-delete-book');
|
|
|
|
|
- delBtn.addEventListener('click', (e) => {
|
|
|
|
|
- e.stopPropagation();
|
|
|
|
|
- if (!delBtn.classList.contains('confirming')) {
|
|
|
|
|
- delBtn.classList.add('confirming');
|
|
|
|
|
- delBtn.textContent = 'confirm?';
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- delBtn.classList.remove('confirming');
|
|
|
|
|
- delBtn.textContent = 'delete';
|
|
|
|
|
- }, 2500);
|
|
|
|
|
- } else {
|
|
|
|
|
- deleteBook(src, group);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const delBtn = header.querySelector('.btn-del');
|
|
|
|
|
+ armDeleteBtn(delBtn, () => deleteBook(src, group));
|
|
|
|
|
|
|
|
group.appendChild(header);
|
|
group.appendChild(header);
|
|
|
group.appendChild(entriesDiv);
|
|
group.appendChild(entriesDiv);
|
|
@@ -597,23 +603,23 @@ function buildBookGroup(src, count) {
|
|
|
|
|
|
|
|
async function deleteBook(src, groupEl) {
|
|
async function deleteBook(src, groupEl) {
|
|
|
try {
|
|
try {
|
|
|
- const r = await fetch(`${BASE}/knowledge`, {
|
|
|
|
|
|
|
+ const r = await fetch(`${BASE}/knowledge/by-source`, {
|
|
|
method: 'DELETE',
|
|
method: 'DELETE',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ filter: { 'metadata.source_file': src } })
|
|
|
|
|
|
|
+ body: JSON.stringify({ source_file: src, user_id: 'knowledge_base' })
|
|
|
});
|
|
});
|
|
|
- if (r.ok) {
|
|
|
|
|
- groupEl.style.transition = 'opacity 0.3s';
|
|
|
|
|
- groupEl.style.opacity = '0';
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- groupEl.remove();
|
|
|
|
|
- // update count
|
|
|
|
|
|
|
+ const data = await r.json();
|
|
|
|
|
+ if (r.ok && data.deleted > 0) {
|
|
|
|
|
+ fadeRemove(groupEl, () => {
|
|
|
const remaining = document.querySelectorAll('.book-group').length;
|
|
const remaining = document.querySelectorAll('.book-group').length;
|
|
|
- const entries = document.querySelectorAll('.entry').length;
|
|
|
|
|
|
|
+ const total = [...document.querySelectorAll('.book-badge')]
|
|
|
|
|
+ .reduce((sum, el) => sum + parseInt(el.textContent || 0), 0);
|
|
|
document.getElementById('knowledgeCount').textContent =
|
|
document.getElementById('knowledgeCount').textContent =
|
|
|
- `${entries} entries · ${remaining} books`;
|
|
|
|
|
- }, 300);
|
|
|
|
|
- toast(`deleted: ${src}`);
|
|
|
|
|
|
|
+ `${total} entries · ${remaining} books`;
|
|
|
|
|
+ });
|
|
|
|
|
+ toast(`deleted ${data.deleted} entries — ${src}`);
|
|
|
|
|
+ } else if (data.deleted === 0) {
|
|
|
|
|
+ toast('nothing to delete — try refreshing first', 'err');
|
|
|
} else {
|
|
} else {
|
|
|
toast('delete failed', 'err');
|
|
toast('delete failed', 'err');
|
|
|
}
|
|
}
|
|
@@ -627,7 +633,6 @@ async function loadMemories() {
|
|
|
document.getElementById('memoriesLoading').style.display = 'block';
|
|
document.getElementById('memoriesLoading').style.display = 'block';
|
|
|
document.getElementById('memoriesContent').innerHTML = '';
|
|
document.getElementById('memoriesContent').innerHTML = '';
|
|
|
|
|
|
|
|
- // Fetch for known user IDs — extend this array as needed
|
|
|
|
|
const userIds = ['main', 'testuser', 'default'];
|
|
const userIds = ['main', 'testuser', 'default'];
|
|
|
const allResults = {};
|
|
const allResults = {};
|
|
|
|
|
|
|
@@ -642,10 +647,9 @@ async function loadMemories() {
|
|
|
const items = data.results || [];
|
|
const items = data.results || [];
|
|
|
if (items.length) allResults[uid] = items;
|
|
if (items.length) allResults[uid] = items;
|
|
|
}));
|
|
}));
|
|
|
-
|
|
|
|
|
stopLoading('memoriesLoading');
|
|
stopLoading('memoriesLoading');
|
|
|
renderMemories(allResults);
|
|
renderMemories(allResults);
|
|
|
- } catch (e) {
|
|
|
|
|
|
|
+ } catch {
|
|
|
stopLoading('memoriesLoading');
|
|
stopLoading('memoriesLoading');
|
|
|
document.getElementById('memoriesContent').innerHTML =
|
|
document.getElementById('memoriesContent').innerHTML =
|
|
|
'<div class="state-msg">failed to load</div>';
|
|
'<div class="state-msg">failed to load</div>';
|
|
@@ -677,26 +681,74 @@ function renderMemories(byUser) {
|
|
|
section.appendChild(label);
|
|
section.appendChild(label);
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
for (const item of items) {
|
|
|
- const row = document.createElement('div');
|
|
|
|
|
- row.className = 'memory-item';
|
|
|
|
|
- row.innerHTML = `
|
|
|
|
|
- <div class="memory-text">${item.memory || ''}</div>
|
|
|
|
|
- <div class="memory-time">${fmtDate(item.created_at)}</div>
|
|
|
|
|
- `;
|
|
|
|
|
- section.appendChild(row);
|
|
|
|
|
|
|
+ section.appendChild(buildMemoryRow(item, section));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
container.appendChild(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 ───────────────────────────────────────────────────
|
|
// ── LOAD ALL ───────────────────────────────────────────────────
|
|
|
async function loadAll() {
|
|
async function loadAll() {
|
|
|
await checkHealth();
|
|
await checkHealth();
|
|
|
await Promise.all([loadKnowledge(), loadMemories()]);
|
|
await Promise.all([loadKnowledge(), loadMemories()]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Init
|
|
|
|
|
loadAll();
|
|
loadAll();
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|