@christopherlittle51/postclaw 1.3.1 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -690
- package/dashboard/README.md +89 -0
- package/dashboard/public/app.js +1023 -543
- package/dashboard/public/index.html +283 -75
- package/dashboard/public/styles.css +583 -126
- package/dist/dashboard/routes/config.d.ts.map +1 -1
- package/dist/dashboard/routes/config.js +17 -1
- package/dist/dashboard/routes/config.js.map +1 -1
- package/dist/dashboard/routes/graph.d.ts +2 -2
- package/dist/dashboard/routes/graph.d.ts.map +1 -1
- package/dist/dashboard/routes/graph.js +61 -23
- package/dist/dashboard/routes/graph.js.map +1 -1
- package/dist/dashboard/routes/memories.d.ts.map +1 -1
- package/dist/dashboard/routes/memories.js +97 -11
- package/dist/dashboard/routes/memories.js.map +1 -1
- package/dist/dashboard/routes/scripts.d.ts.map +1 -1
- package/dist/dashboard/routes/scripts.js +3 -1
- package/dist/dashboard/routes/scripts.js.map +1 -1
- package/dist/dashboard/routes/workspace.d.ts +4 -3
- package/dist/dashboard/routes/workspace.d.ts.map +1 -1
- package/dist/dashboard/routes/workspace.js +119 -7
- package/dist/dashboard/routes/workspace.js.map +1 -1
- package/dist/dashboard/server.js +1 -1
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -43
- package/dist/index.js.map +1 -1
- package/dist/schemas/validation.d.ts +108 -12
- package/dist/schemas/validation.d.ts.map +1 -1
- package/dist/schemas/validation.js +66 -11
- package/dist/schemas/validation.js.map +1 -1
- package/dist/scripts/bootstrap_persona.d.ts.map +1 -1
- package/dist/scripts/bootstrap_persona.js +3 -3
- package/dist/scripts/bootstrap_persona.js.map +1 -1
- package/dist/scripts/setup-db.d.ts.map +1 -1
- package/dist/scripts/setup-db.js +74 -37
- package/dist/scripts/setup-db.js.map +1 -1
- package/dist/scripts/sleep_cycle.d.ts +1 -0
- package/dist/scripts/sleep_cycle.d.ts.map +1 -1
- package/dist/scripts/sleep_cycle.js +209 -38
- package/dist/scripts/sleep_cycle.js.map +1 -1
- package/dist/services/config.d.ts +1 -0
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +15 -29
- package/dist/services/config.js.map +1 -1
- package/dist/services/db.d.ts +10 -0
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/db.js +38 -1
- package/dist/services/db.js.map +1 -1
- package/dist/services/llm.d.ts.map +1 -1
- package/dist/services/llm.js +3 -1
- package/dist/services/llm.js.map +1 -1
- package/dist/services/memoryService.d.ts +4 -2
- package/dist/services/memoryService.d.ts.map +1 -1
- package/dist/services/memoryService.js +127 -43
- package/dist/services/memoryService.js.map +1 -1
- package/dist/services/personaService.d.ts +25 -0
- package/dist/services/personaService.d.ts.map +1 -1
- package/dist/services/personaService.js +79 -0
- package/dist/services/personaService.js.map +1 -1
- package/dist/tests/dashboard-schemas.test.js +71 -1
- package/dist/tests/dashboard-schemas.test.js.map +1 -1
- package/openclaw.plugin.json +5 -0
- package/package.json +2 -1
- package/schemas/README.md +64 -0
package/dashboard/public/app.js
CHANGED
|
@@ -1,99 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PostClaw Dashboard — Frontend
|
|
2
|
+
* PostClaw Dashboard — Frontend application
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Vanilla JS SPA: tab management, agent selection, CRUD for personas/memories,
|
|
5
|
+
* D3.js knowledge graph with persona nodes, memory linking, workspace import,
|
|
6
|
+
* script runner, and full config editor.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
// =============================================================================
|
|
8
|
-
//
|
|
10
|
+
// GLOBALS
|
|
9
11
|
// =============================================================================
|
|
10
12
|
|
|
11
|
-
let currentAgent = "main";
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
let currentAgent = localStorage.getItem("postclaw-selected-agent") || "main";
|
|
14
|
+
const API = "";
|
|
15
|
+
let memoryPage = { offset: 0, limit: 50, total: 0 };
|
|
16
|
+
let graphInstances = { simulation: null, svg: null, g: null, zoom: null };
|
|
17
|
+
let showLabels = true;
|
|
18
|
+
let allNodes = []; // cached for link search
|
|
19
|
+
let allEdges = []; // cached for link search
|
|
20
|
+
let shiftLinkState = null; // { sourceNode, line } for shift+drag linking
|
|
14
21
|
|
|
15
22
|
// =============================================================================
|
|
16
|
-
//
|
|
23
|
+
// UTILS
|
|
17
24
|
// =============================================================================
|
|
18
25
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
async function api(method, path, body = null) {
|
|
26
|
+
async function api(path, opts = {}) {
|
|
22
27
|
const sep = path.includes("?") ? "&" : "?";
|
|
23
|
-
const url = `${path}${sep}agentId=${encodeURIComponent(currentAgent)}`;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return data.data;
|
|
28
|
+
const url = `${API}${path}${sep}agentId=${encodeURIComponent(currentAgent)}`;
|
|
29
|
+
const res = await fetch(url, {
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
...opts,
|
|
32
|
+
});
|
|
33
|
+
return res.json();
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
function toast(
|
|
33
|
-
const container =
|
|
36
|
+
function toast(msg, type = "info") {
|
|
37
|
+
const container = document.getElementById("toast-container");
|
|
34
38
|
const el = document.createElement("div");
|
|
35
39
|
el.className = `toast toast-${type}`;
|
|
36
|
-
el.textContent =
|
|
40
|
+
el.textContent = msg;
|
|
37
41
|
container.appendChild(el);
|
|
38
42
|
setTimeout(() => el.remove(), 3000);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function
|
|
42
|
-
return `<span class="badge
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function boolBadge(val) {
|
|
46
|
-
return `<span class="badge badge-${val}">${val ? "✓" : "✗"}</span>`;
|
|
45
|
+
function badge(value, prefix = "badge") {
|
|
46
|
+
return `<span class="badge ${prefix}-${value}">${value}</span>`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function truncate(str,
|
|
49
|
+
function truncate(str, max = 80) {
|
|
50
50
|
if (!str) return "";
|
|
51
|
-
return str.length >
|
|
51
|
+
return str.length > max ? str.substring(0, max) + "…" : str;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// =============================================================================
|
|
55
55
|
// TAB MANAGEMENT
|
|
56
56
|
// =============================================================================
|
|
57
57
|
|
|
58
|
-
document.querySelectorAll(".tab-btn").forEach(btn => {
|
|
58
|
+
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|
59
59
|
btn.addEventListener("click", () => {
|
|
60
|
-
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
|
61
|
-
document.querySelectorAll(".tab-panel").forEach(p => p.classList.remove("active"));
|
|
60
|
+
document.querySelectorAll(".tab-btn").forEach((b) => b.classList.remove("active"));
|
|
61
|
+
document.querySelectorAll(".tab-panel").forEach((p) => p.classList.remove("active"));
|
|
62
62
|
btn.classList.add("active");
|
|
63
|
-
|
|
63
|
+
const panel = document.getElementById(`tab-${btn.dataset.tab}`);
|
|
64
|
+
if (panel) panel.classList.add("active");
|
|
64
65
|
|
|
65
|
-
// Load data for the active tab
|
|
66
|
-
if (btn.dataset.tab === "personas") loadPersonas();
|
|
67
|
-
if (btn.dataset.tab === "memories") loadMemories();
|
|
68
66
|
if (btn.dataset.tab === "graph") loadGraph();
|
|
69
67
|
if (btn.dataset.tab === "config") loadConfig();
|
|
70
68
|
});
|
|
71
69
|
});
|
|
72
70
|
|
|
73
71
|
// =============================================================================
|
|
74
|
-
// AGENT
|
|
72
|
+
// AGENT SELECTION
|
|
75
73
|
// =============================================================================
|
|
76
74
|
|
|
77
75
|
async function loadAgents() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const opt = document.createElement("option");
|
|
84
|
-
opt.value = a.id;
|
|
85
|
-
opt.textContent = a.name || a.id;
|
|
86
|
-
if (a.id === currentAgent) opt.selected = true;
|
|
87
|
-
select.appendChild(opt);
|
|
88
|
-
});
|
|
89
|
-
} catch { /* agents table might be empty */ }
|
|
76
|
+
const res = await api("/api/agents");
|
|
77
|
+
if (!res.ok) return;
|
|
78
|
+
const select = document.getElementById("agent-select");
|
|
79
|
+
select.innerHTML = res.data.map((a) => `<option value="${a.id}">${a.id}</option>`).join("");
|
|
80
|
+
select.value = currentAgent;
|
|
90
81
|
}
|
|
91
82
|
|
|
92
|
-
|
|
83
|
+
document.getElementById("agent-select").addEventListener("change", (e) => {
|
|
93
84
|
currentAgent = e.target.value;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
localStorage.setItem("postclaw-selected-agent", currentAgent);
|
|
86
|
+
loadPersonas();
|
|
87
|
+
loadMemories();
|
|
88
|
+
loadWorkspaceFiles();
|
|
97
89
|
});
|
|
98
90
|
|
|
99
91
|
// =============================================================================
|
|
@@ -101,110 +93,91 @@ $("agent-select").addEventListener("change", (e) => {
|
|
|
101
93
|
// =============================================================================
|
|
102
94
|
|
|
103
95
|
async function loadPersonas() {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
96
|
+
const res = await api("/api/personas");
|
|
97
|
+
if (!res.ok) return;
|
|
98
|
+
|
|
99
|
+
const container = document.getElementById("persona-list");
|
|
100
|
+
if (res.data.length === 0) {
|
|
101
|
+
container.innerHTML = '<p class="hint">No persona entries yet.</p>';
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
container.innerHTML = `
|
|
106
|
+
<table class="data-table">
|
|
107
|
+
<thead><tr><th>Category</th><th>Content</th><th>Always Active</th><th>Actions</th></tr></thead>
|
|
108
|
+
<tbody>
|
|
109
|
+
${res.data
|
|
110
|
+
.map(
|
|
111
|
+
(p) => `
|
|
117
112
|
<tr>
|
|
118
|
-
<td
|
|
119
|
-
<td title="${p.content.replace(/"/g,
|
|
120
|
-
<td>${
|
|
113
|
+
<td>${p.category}</td>
|
|
114
|
+
<td title="${p.content.replace(/"/g, """)}">${truncate(p.content, 60)}</td>
|
|
115
|
+
<td>${badge(String(p.is_always_active))}</td>
|
|
121
116
|
<td class="actions">
|
|
122
117
|
<button class="btn-sm btn-secondary" onclick="editPersona('${p.id}')">✏️</button>
|
|
123
|
-
<button class="btn-sm btn-danger" onclick="
|
|
118
|
+
<button class="btn-sm btn-danger" onclick="deletePersona('${p.id}')">🗑️</button>
|
|
124
119
|
</td>
|
|
125
|
-
</tr
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Store personas data for editing
|
|
132
|
-
let _personasCache = [];
|
|
133
|
-
|
|
134
|
-
async function editPersona(id) {
|
|
135
|
-
try {
|
|
136
|
-
const persona = await api("GET", `/api/personas/${id}`);
|
|
137
|
-
$("persona-form-id").value = id;
|
|
138
|
-
$("persona-category").value = persona.category;
|
|
139
|
-
$("persona-content").value = persona.content;
|
|
140
|
-
$("persona-always-active").checked = persona.is_always_active;
|
|
141
|
-
$("persona-form").style.display = "block";
|
|
142
|
-
} catch (err) { toast(err.message, "error"); }
|
|
120
|
+
</tr>`,
|
|
121
|
+
)
|
|
122
|
+
.join("")}
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
`;
|
|
143
126
|
}
|
|
144
|
-
window.editPersona = editPersona;
|
|
145
127
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} catch (err) { toast(err.message, "error"); }
|
|
153
|
-
}
|
|
154
|
-
window.deletePersonaRow = deletePersonaRow;
|
|
155
|
-
|
|
156
|
-
$("btn-new-persona").addEventListener("click", () => {
|
|
157
|
-
$("persona-form-id").value = "";
|
|
158
|
-
$("persona-category").value = "";
|
|
159
|
-
$("persona-content").value = "";
|
|
160
|
-
$("persona-always-active").checked = false;
|
|
161
|
-
$("persona-form").style.display = "block";
|
|
128
|
+
document.getElementById("btn-new-persona").addEventListener("click", () => {
|
|
129
|
+
document.getElementById("persona-form-id").value = "";
|
|
130
|
+
document.getElementById("persona-category").value = "";
|
|
131
|
+
document.getElementById("persona-content").value = "";
|
|
132
|
+
document.getElementById("persona-always-active").checked = false;
|
|
133
|
+
document.getElementById("persona-form").style.display = "block";
|
|
162
134
|
});
|
|
163
135
|
|
|
164
|
-
|
|
165
|
-
|
|
136
|
+
document.getElementById("btn-cancel-persona").addEventListener("click", () => {
|
|
137
|
+
document.getElementById("persona-form").style.display = "none";
|
|
166
138
|
});
|
|
167
139
|
|
|
168
|
-
|
|
169
|
-
const id =
|
|
170
|
-
const
|
|
171
|
-
category:
|
|
172
|
-
content:
|
|
173
|
-
is_always_active:
|
|
140
|
+
document.getElementById("btn-save-persona").addEventListener("click", async () => {
|
|
141
|
+
const id = document.getElementById("persona-form-id").value;
|
|
142
|
+
const body = {
|
|
143
|
+
category: document.getElementById("persona-category").value,
|
|
144
|
+
content: document.getElementById("persona-content").value,
|
|
145
|
+
is_always_active: document.getElementById("persona-always-active").checked,
|
|
174
146
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
$("persona-form").style.display = "none";
|
|
147
|
+
|
|
148
|
+
const res = id
|
|
149
|
+
? await api(`/api/personas/${id}`, { method: "PUT", body: JSON.stringify(body) })
|
|
150
|
+
: await api("/api/personas", { method: "POST", body: JSON.stringify(body) });
|
|
151
|
+
|
|
152
|
+
if (res.ok) {
|
|
153
|
+
toast(id ? "Persona updated" : "Persona created", "success");
|
|
154
|
+
document.getElementById("persona-form").style.display = "none";
|
|
184
155
|
loadPersonas();
|
|
185
|
-
}
|
|
156
|
+
} else {
|
|
157
|
+
toast(res.error || "Failed", "error");
|
|
158
|
+
}
|
|
186
159
|
});
|
|
187
160
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
161
|
+
window.editPersona = async function (id) {
|
|
162
|
+
const res = await api(`/api/personas/${id}`);
|
|
163
|
+
if (!res.ok) return toast("Not found", "error");
|
|
164
|
+
const p = res.data;
|
|
165
|
+
document.getElementById("persona-form-id").value = p.id;
|
|
166
|
+
document.getElementById("persona-category").value = p.category;
|
|
167
|
+
document.getElementById("persona-content").value = p.content;
|
|
168
|
+
document.getElementById("persona-always-active").checked = p.is_always_active;
|
|
169
|
+
document.getElementById("persona-form").style.display = "block";
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
window.deletePersona = async function (id) {
|
|
173
|
+
if (!confirm("Delete this persona entry?")) return;
|
|
174
|
+
const res = await api(`/api/personas/${id}`, { method: "DELETE" });
|
|
175
|
+
if (res.ok) {
|
|
176
|
+
toast("Deleted", "success");
|
|
177
|
+
loadPersonas();
|
|
178
|
+
} else {
|
|
179
|
+
toast(res.error || "Failed", "error");
|
|
180
|
+
}
|
|
208
181
|
};
|
|
209
182
|
|
|
210
183
|
// =============================================================================
|
|
@@ -212,532 +185,1039 @@ window.loadWorkspaceFile = async function(filename) {
|
|
|
212
185
|
// =============================================================================
|
|
213
186
|
|
|
214
187
|
async function loadMemories() {
|
|
215
|
-
const search =
|
|
216
|
-
const tier =
|
|
217
|
-
const archived =
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
188
|
+
const search = document.getElementById("memory-search").value;
|
|
189
|
+
const tier = document.getElementById("memory-tier-filter").value;
|
|
190
|
+
const archived = document.getElementById("memory-archived-filter").value;
|
|
191
|
+
|
|
192
|
+
const params = new URLSearchParams({
|
|
193
|
+
limit: memoryPage.limit,
|
|
194
|
+
offset: memoryPage.offset,
|
|
195
|
+
});
|
|
196
|
+
if (search) params.set("search", search);
|
|
197
|
+
if (tier) params.set("tier", tier);
|
|
198
|
+
if (archived) params.set("archived", archived);
|
|
199
|
+
|
|
200
|
+
const res = await api(`/api/memories?${params}`);
|
|
201
|
+
if (!res.ok) return;
|
|
202
|
+
|
|
203
|
+
const memories = res.data.memories || res.data;
|
|
204
|
+
memoryPage.total = res.data.total ?? memories.length;
|
|
205
|
+
const container = document.getElementById("memory-list");
|
|
206
|
+
|
|
207
|
+
if (memories.length === 0) {
|
|
208
|
+
container.innerHTML = '<p class="hint">No memories found.</p>';
|
|
209
|
+
document.getElementById("memory-pagination").innerHTML = "";
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
container.innerHTML = `
|
|
214
|
+
<div style="overflow-x: auto;">
|
|
215
|
+
<table class="data-table">
|
|
216
|
+
<thead><tr>
|
|
217
|
+
<th>ID</th><th>Agent ID</th><th>Access Scope</th><th>Content</th><th>Hash</th><th>Category</th><th>Source URI</th><th>Volatility</th><th>Pointer?</th><th>Embed</th><th>Model</th><th>Tokens</th><th>Conf.</th><th>Tier</th><th>Score</th><th>Inj.</th><th>Acc.</th><th>Last Inj.</th><th>Last Acc.</th><th>Created</th><th>Updated</th><th>Expires</th><th>Archived?</th><th>Metadata</th><th>Superseded</th><th>Actions</th>
|
|
218
|
+
</tr></thead>
|
|
219
|
+
<tbody>
|
|
220
|
+
${memories
|
|
221
|
+
.map(
|
|
222
|
+
(m) => {
|
|
223
|
+
const created = m.created_at ? new Date(m.created_at).toLocaleDateString() : '—';
|
|
224
|
+
const updated = m.updated_at ? new Date(m.updated_at).toLocaleDateString() : '—';
|
|
225
|
+
const conf = m.confidence != null ? m.confidence.toFixed(2) : '—';
|
|
226
|
+
const vol = m.volatility || '—';
|
|
227
|
+
const score = m.usefulness_score != null ? m.usefulness_score.toFixed(1) : '—';
|
|
228
|
+
const acc = m.access_count ?? 0;
|
|
229
|
+
const inj = m.injection_count ?? 0;
|
|
230
|
+
return `
|
|
231
|
+
<tr${m.is_pointer ? ' class="memory-pointer"' : ''}>
|
|
232
|
+
<td title="${m.id}">${truncate(m.id, 8)}</td>
|
|
233
|
+
<td>${m.agent_id}</td>
|
|
234
|
+
<td>${m.access_scope}</td>
|
|
235
|
+
<td title="${(m.content || '').replace(/"/g, '"')}">${truncate(m.content, 30)}</td>
|
|
236
|
+
<td title="${m.content_hash}">${truncate(m.content_hash, 8)}</td>
|
|
237
|
+
<td>${m.category || '—'}</td>
|
|
238
|
+
<td title="${m.source_uri}">${truncate(m.source_uri, 15)}</td>
|
|
239
|
+
<td><span class="vol-${vol}">${vol}</span></td>
|
|
240
|
+
<td>${m.is_pointer ? 'Y' : 'N'}</td>
|
|
241
|
+
<td title="${m.embedding ? 'Vector exists' : 'None'}">${m.embedding ? 'Vector' : 'None'}</td>
|
|
242
|
+
<td>${m.embedding_model}</td>
|
|
243
|
+
<td>${m.token_count}</td>
|
|
244
|
+
<td>${conf}</td>
|
|
245
|
+
<td>${badge(m.tier || 'daily')}</td>
|
|
246
|
+
<td>${score}</td>
|
|
247
|
+
<td>${inj}</td>
|
|
248
|
+
<td>${acc}</td>
|
|
249
|
+
<td>${m.last_injected_at ? new Date(m.last_injected_at).toLocaleDateString() : '—'}</td>
|
|
250
|
+
<td>${m.last_accessed_at ? new Date(m.last_accessed_at).toLocaleDateString() : '—'}</td>
|
|
251
|
+
<td>${created}</td>
|
|
252
|
+
<td>${updated}</td>
|
|
253
|
+
<td>${m.expires_at ? new Date(m.expires_at).toLocaleDateString() : '—'}</td>
|
|
254
|
+
<td>${m.is_archived ? 'Y' : 'N'}</td>
|
|
255
|
+
<td title="${m.metadata ? JSON.stringify(m.metadata).replace(/"/g, '"') : ''}">${m.metadata ? truncate(JSON.stringify(m.metadata), 15) : '—'}</td>
|
|
256
|
+
<td title="${m.superseded_by}">${truncate(m.superseded_by, 8)}</td>
|
|
257
|
+
<td class="actions" style="position: sticky; right: 0; background: var(--amb-c-surface);">
|
|
245
258
|
<button class="btn-sm btn-secondary" onclick="editMemory('${m.id}')">✏️</button>
|
|
246
|
-
<button class="btn-sm btn-danger" onclick="
|
|
259
|
+
<button class="btn-sm btn-danger" onclick="deleteMemory('${m.id}')">🗑️</button>
|
|
247
260
|
</td>
|
|
248
|
-
</tr
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
: `<span>${result.total} memories</span>`;
|
|
259
|
-
} catch (err) { toast(err.message, "error"); }
|
|
261
|
+
</tr>`;
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
.join('')}
|
|
265
|
+
</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
`;
|
|
269
|
+
|
|
270
|
+
renderPagination();
|
|
260
271
|
}
|
|
261
272
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
function renderPagination() {
|
|
274
|
+
const el = document.getElementById("memory-pagination");
|
|
275
|
+
const page = Math.floor(memoryPage.offset / memoryPage.limit) + 1;
|
|
276
|
+
const totalPages = Math.max(1, Math.ceil(memoryPage.total / memoryPage.limit));
|
|
277
|
+
el.innerHTML = `
|
|
278
|
+
<button class="btn-sm btn-secondary" onclick="prevMemoryPage()" ${memoryPage.offset === 0 ? "disabled" : ""}>← Prev</button>
|
|
279
|
+
<span>Page ${page} of ${totalPages} (${memoryPage.total} total)</span>
|
|
280
|
+
<button class="btn-sm btn-secondary" onclick="nextMemoryPage()" ${page >= totalPages ? "disabled" : ""}>Next →</button>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
window.prevMemoryPage = function () {
|
|
285
|
+
memoryPage.offset = Math.max(0, memoryPage.offset - memoryPage.limit);
|
|
286
|
+
loadMemories();
|
|
275
287
|
};
|
|
276
288
|
|
|
277
|
-
window.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
await api("DELETE", `/api/memories/${id}`);
|
|
281
|
-
toast("Memory archived", "success");
|
|
282
|
-
loadMemories();
|
|
283
|
-
} catch (err) { toast(err.message, "error"); }
|
|
289
|
+
window.nextMemoryPage = function () {
|
|
290
|
+
memoryPage.offset += memoryPage.limit;
|
|
291
|
+
loadMemories();
|
|
284
292
|
};
|
|
285
293
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
document.getElementById("memory-search").addEventListener(
|
|
295
|
+
"input",
|
|
296
|
+
debounce(() => {
|
|
297
|
+
memoryPage.offset = 0;
|
|
298
|
+
loadMemories();
|
|
299
|
+
}, 300),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
document.getElementById("memory-tier-filter").addEventListener("change", () => {
|
|
303
|
+
memoryPage.offset = 0;
|
|
304
|
+
loadMemories();
|
|
293
305
|
});
|
|
294
306
|
|
|
295
|
-
|
|
296
|
-
|
|
307
|
+
document.getElementById("memory-archived-filter").addEventListener("change", () => {
|
|
308
|
+
memoryPage.offset = 0;
|
|
309
|
+
loadMemories();
|
|
297
310
|
});
|
|
298
311
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
tier: $("memory-tier").value,
|
|
312
|
+
function debounce(fn, ms) {
|
|
313
|
+
let t;
|
|
314
|
+
return (...args) => {
|
|
315
|
+
clearTimeout(t);
|
|
316
|
+
t = setTimeout(() => fn(...args), ms);
|
|
305
317
|
};
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
document.getElementById("btn-new-memory").addEventListener("click", () => {
|
|
321
|
+
const fields = ['id', 'agent_id', 'access_scope', 'content', 'content_hash', 'category', 'source_uri', 'volatility', 'is_pointer', 'embedding', 'embedding_model', 'token_count', 'confidence', 'tier', 'usefulness_score', 'injection_count', 'access_count', 'last_injected_at', 'last_accessed_at', 'expires_at', 'created_at', 'updated_at', 'is_archived', 'metadata', 'superseded_by'];
|
|
322
|
+
for (const f of fields) {
|
|
323
|
+
const el = document.getElementById("memory-" + f);
|
|
324
|
+
if (el) {
|
|
325
|
+
if (el.tagName === 'SELECT') {
|
|
326
|
+
el.selectedIndex = 0;
|
|
327
|
+
if (f === 'is_archived' || f === 'is_pointer') el.value = "false";
|
|
328
|
+
if (f === 'tier') el.value = "daily";
|
|
329
|
+
if (f === 'volatility') el.value = "low";
|
|
330
|
+
if (f === 'access_scope') el.value = "private";
|
|
331
|
+
} else {
|
|
332
|
+
el.value = "";
|
|
333
|
+
}
|
|
313
334
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
335
|
+
}
|
|
336
|
+
document.getElementById("memory-form-id").value = "";
|
|
337
|
+
document.getElementById("memory-edge-list").innerHTML = "";
|
|
338
|
+
document.getElementById("memory-form").style.display = "block";
|
|
317
339
|
});
|
|
318
340
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
$("import-form").style.display = "block";
|
|
322
|
-
$("memory-form").style.display = "none";
|
|
323
|
-
});
|
|
324
|
-
$("btn-cancel-import").addEventListener("click", () => {
|
|
325
|
-
$("import-form").style.display = "none";
|
|
341
|
+
document.getElementById("btn-cancel-memory").addEventListener("click", () => {
|
|
342
|
+
document.getElementById("memory-form").style.display = "none";
|
|
326
343
|
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
344
|
+
|
|
345
|
+
document.getElementById("btn-save-memory").addEventListener("click", async () => {
|
|
346
|
+
const id = document.getElementById("memory-form-id").value;
|
|
347
|
+
|
|
348
|
+
const getVal = (f) => {
|
|
349
|
+
const el = document.getElementById("memory-" + f);
|
|
350
|
+
return el && el.value !== "" ? el.value : undefined;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const getNum = (f) => {
|
|
354
|
+
const el = document.getElementById("memory-" + f);
|
|
355
|
+
return el && el.value !== "" ? Number(el.value) : undefined;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const getBool = (f) => {
|
|
359
|
+
const el = document.getElementById("memory-" + f);
|
|
360
|
+
return el && el.value !== "" ? el.value === "true" : undefined;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
let metadataObj = undefined;
|
|
364
|
+
const rawMeta = getVal("metadata");
|
|
365
|
+
if (rawMeta) {
|
|
366
|
+
try {
|
|
367
|
+
metadataObj = JSON.parse(rawMeta);
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return toast("Invalid JSON in Metadata field", "error");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let embeddingObj = undefined;
|
|
374
|
+
const rawEmbed = getVal("embedding");
|
|
375
|
+
if (rawEmbed) {
|
|
376
|
+
try {
|
|
377
|
+
embeddingObj = JSON.parse(rawEmbed);
|
|
378
|
+
} catch (e) {
|
|
379
|
+
return toast("Invalid JSON in Embedding field", "error");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const body = {
|
|
384
|
+
content: getVal("content") || "",
|
|
385
|
+
agent_id: getVal("agent_id"),
|
|
386
|
+
access_scope: getVal("access_scope"),
|
|
387
|
+
content_hash: getVal("content_hash"),
|
|
388
|
+
category: getVal("category"),
|
|
389
|
+
source_uri: getVal("source_uri"),
|
|
390
|
+
volatility: getVal("volatility"),
|
|
391
|
+
is_pointer: getBool("is_pointer"),
|
|
392
|
+
embedding: embeddingObj,
|
|
393
|
+
embedding_model: getVal("embedding_model"),
|
|
394
|
+
token_count: getNum("token_count"),
|
|
395
|
+
confidence: getNum("confidence"),
|
|
396
|
+
tier: getVal("tier"),
|
|
397
|
+
usefulness_score: getNum("usefulness_score"),
|
|
398
|
+
injection_count: getNum("injection_count"),
|
|
399
|
+
access_count: getNum("access_count"),
|
|
400
|
+
last_injected_at: getVal("last_injected_at"),
|
|
401
|
+
last_accessed_at: getVal("last_accessed_at"),
|
|
402
|
+
created_at: getVal("created_at"),
|
|
403
|
+
updated_at: getVal("updated_at"),
|
|
404
|
+
expires_at: getVal("expires_at"),
|
|
405
|
+
is_archived: getBool("is_archived"),
|
|
406
|
+
metadata: metadataObj,
|
|
407
|
+
superseded_by: getVal("superseded_by")
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Clean up undefined properties to avoid sending huge sparse objects when creating
|
|
411
|
+
Object.keys(body).forEach(k => body[k] === undefined && delete body[k]);
|
|
412
|
+
|
|
413
|
+
const res = id
|
|
414
|
+
? await api(`/api/memories/${id}`, { method: "PUT", body: JSON.stringify(body) })
|
|
415
|
+
: await api("/api/memories", { method: "POST", body: JSON.stringify(body) });
|
|
416
|
+
|
|
417
|
+
if (res.ok) {
|
|
418
|
+
toast(id ? "Memory updated" : "Memory created", "success");
|
|
419
|
+
document.getElementById("memory-form").style.display = "none";
|
|
337
420
|
loadMemories();
|
|
338
|
-
}
|
|
421
|
+
} else {
|
|
422
|
+
toast(res.error || "Failed", "error");
|
|
423
|
+
}
|
|
339
424
|
});
|
|
340
425
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
426
|
+
window.editMemory = async function (id) {
|
|
427
|
+
const res = await api(`/api/memories/${id}`);
|
|
428
|
+
if (!res.ok) return toast("Not found", "error");
|
|
429
|
+
const m = res.data;
|
|
430
|
+
document.getElementById("memory-form-id").value = m.id;
|
|
345
431
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
432
|
+
const setVal = (f, v) => {
|
|
433
|
+
const el = document.getElementById("memory-" + f);
|
|
434
|
+
if (el) el.value = v != null ? String(v) : "";
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
setVal("agent_id", m.agent_id);
|
|
438
|
+
setVal("access_scope", m.access_scope);
|
|
439
|
+
setVal("content", m.content);
|
|
440
|
+
setVal("content_hash", m.content_hash);
|
|
441
|
+
setVal("category", m.category);
|
|
442
|
+
setVal("source_uri", m.source_uri);
|
|
443
|
+
setVal("volatility", m.volatility);
|
|
444
|
+
setVal("is_pointer", m.is_pointer);
|
|
445
|
+
setVal("embedding", m.embedding ? JSON.stringify(m.embedding) : "");
|
|
446
|
+
setVal("embedding_model", m.embedding_model);
|
|
447
|
+
setVal("token_count", m.token_count);
|
|
448
|
+
setVal("confidence", m.confidence);
|
|
449
|
+
setVal("tier", m.tier);
|
|
450
|
+
setVal("usefulness_score", m.usefulness_score);
|
|
451
|
+
setVal("injection_count", m.injection_count);
|
|
452
|
+
setVal("access_count", m.access_count);
|
|
453
|
+
setVal("last_injected_at", m.last_injected_at);
|
|
454
|
+
setVal("last_accessed_at", m.last_accessed_at);
|
|
455
|
+
setVal("created_at", m.created_at);
|
|
456
|
+
setVal("updated_at", m.updated_at);
|
|
457
|
+
setVal("expires_at", m.expires_at);
|
|
458
|
+
setVal("is_archived", m.is_archived);
|
|
459
|
+
setVal("metadata", m.metadata ? JSON.stringify(m.metadata) : "");
|
|
460
|
+
setVal("superseded_by", m.superseded_by);
|
|
461
|
+
|
|
462
|
+
document.getElementById("memory-form").style.display = "block";
|
|
463
|
+
loadMemoryEdges(id);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
window.deleteMemory = async function (id) {
|
|
467
|
+
if (!confirm("Archive this memory?")) return;
|
|
468
|
+
const res = await api(`/api/memories/${id}`, { method: "DELETE" });
|
|
469
|
+
if (res.ok) {
|
|
470
|
+
toast("Archived", "success");
|
|
471
|
+
loadMemories();
|
|
472
|
+
} else {
|
|
473
|
+
toast(res.error || "Failed", "error");
|
|
474
|
+
}
|
|
475
|
+
};
|
|
350
476
|
|
|
351
477
|
// =============================================================================
|
|
352
|
-
//
|
|
478
|
+
// MEMORY LINKING
|
|
353
479
|
// =============================================================================
|
|
354
480
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
481
|
+
async function loadMemoryEdges(memoryId) {
|
|
482
|
+
const res = await api(`/api/memories/${memoryId}/edges`);
|
|
483
|
+
const container = document.getElementById("memory-edge-list");
|
|
358
484
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
485
|
+
if (!res.ok || !res.data.length) {
|
|
486
|
+
container.innerHTML = '<p class="hint" style="font-size:0.8rem;">No links yet.</p>';
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
container.innerHTML = res.data
|
|
491
|
+
.map((e) => {
|
|
492
|
+
const isSrc = e.source_memory_id === memoryId;
|
|
493
|
+
const targetLabel = isSrc
|
|
494
|
+
? truncate(e.target_content || e.target_persona_category || "Unknown", 50)
|
|
495
|
+
: truncate(e.source_content || e.source_persona_category || "Unknown", 50);
|
|
496
|
+
const targetType = (isSrc ? (e.target_persona_id ? "persona" : "memory") : (e.source_persona_id ? "persona" : "memory"));
|
|
497
|
+
return `
|
|
498
|
+
<div class="edge-item">
|
|
499
|
+
<span class="edge-rel">${e.relationship_type}</span>
|
|
500
|
+
<span class="edge-type-badge">${targetType}</span>
|
|
501
|
+
<span class="edge-target">${isSrc ? "→" : "←"} ${targetLabel}</span>
|
|
502
|
+
<button class="btn-sm btn-danger" onclick="deleteEdge('${e.id}', '${memoryId}')">✕</button>
|
|
503
|
+
</div>
|
|
504
|
+
`;
|
|
505
|
+
})
|
|
506
|
+
.join("");
|
|
365
507
|
}
|
|
366
508
|
|
|
367
|
-
function
|
|
368
|
-
const
|
|
369
|
-
|
|
509
|
+
window.deleteEdge = async function (edgeId, memoryId) {
|
|
510
|
+
const res = await api(`/api/edges/${edgeId}`, { method: "DELETE" });
|
|
511
|
+
if (res.ok) {
|
|
512
|
+
toast("Link removed", "success");
|
|
513
|
+
loadMemoryEdges(memoryId);
|
|
514
|
+
} else {
|
|
515
|
+
toast(res.error || "Failed", "error");
|
|
516
|
+
}
|
|
517
|
+
};
|
|
370
518
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
519
|
+
// Link target search
|
|
520
|
+
const linkSearchInput = document.getElementById("link-target-search");
|
|
521
|
+
const linkSearchResults = document.getElementById("link-target-results");
|
|
374
522
|
|
|
375
|
-
|
|
523
|
+
linkSearchInput.addEventListener(
|
|
524
|
+
"input",
|
|
525
|
+
debounce(async () => {
|
|
526
|
+
const q = linkSearchInput.value.trim().toLowerCase();
|
|
527
|
+
if (q.length < 2) {
|
|
528
|
+
linkSearchResults.style.display = "none";
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
376
531
|
|
|
377
|
-
|
|
378
|
-
|
|
532
|
+
const res = await api(`/api/memories/search?search=${encodeURIComponent(q)}`);
|
|
533
|
+
if (!res.ok || !res.data || res.data.length === 0) {
|
|
534
|
+
linkSearchResults.style.display = "none";
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
379
537
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
538
|
+
linkSearchResults.innerHTML = res.data
|
|
539
|
+
.map(
|
|
540
|
+
(r) => `
|
|
541
|
+
<div class="link-search-item" data-id="${r.id}" data-type="${r.type}">
|
|
542
|
+
<span class="node-type ${r.type}">${r.type}</span>
|
|
543
|
+
<span>${truncate(r.content, 60)}</span>
|
|
544
|
+
</div>
|
|
545
|
+
`,
|
|
546
|
+
)
|
|
547
|
+
.join("");
|
|
548
|
+
linkSearchResults.style.display = "block";
|
|
549
|
+
}, 300),
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
linkSearchResults.addEventListener("click", (e) => {
|
|
553
|
+
const item = e.target.closest(".link-search-item");
|
|
554
|
+
if (!item) return;
|
|
555
|
+
document.getElementById("link-target-id").value = item.dataset.id;
|
|
556
|
+
document.getElementById("link-target-type").value = item.dataset.type;
|
|
557
|
+
linkSearchInput.value = item.querySelector("span:last-child").textContent;
|
|
558
|
+
linkSearchResults.style.display = "none";
|
|
559
|
+
});
|
|
392
560
|
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
561
|
+
document.getElementById("btn-add-link").addEventListener("click", async () => {
|
|
562
|
+
const memoryId = document.getElementById("memory-form-id").value;
|
|
563
|
+
const targetId = document.getElementById("link-target-id").value;
|
|
564
|
+
const targetType = document.getElementById("link-target-type").value;
|
|
565
|
+
const relationship = document.getElementById("link-relationship").value.trim();
|
|
566
|
+
|
|
567
|
+
if (!memoryId || !targetId || !relationship) {
|
|
568
|
+
return toast("Select a target and enter a relationship type", "error");
|
|
569
|
+
}
|
|
398
570
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
571
|
+
const body = { relationship_type: relationship };
|
|
572
|
+
if (targetType === "persona") {
|
|
573
|
+
body.source_memory_id = memoryId;
|
|
574
|
+
body.target_persona_id = targetId;
|
|
575
|
+
} else {
|
|
576
|
+
body.source_memory_id = memoryId;
|
|
577
|
+
body.target_memory_id = targetId;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const res = await api("/api/edges", { method: "POST", body: JSON.stringify(body) });
|
|
581
|
+
if (res.ok) {
|
|
582
|
+
toast("Link created", "success");
|
|
583
|
+
linkSearchInput.value = "";
|
|
584
|
+
document.getElementById("link-target-id").value = "";
|
|
585
|
+
document.getElementById("link-relationship").value = "";
|
|
586
|
+
loadMemoryEdges(memoryId);
|
|
587
|
+
} else {
|
|
588
|
+
toast(res.error || "Link failed", "error");
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// =============================================================================
|
|
593
|
+
// IMPORT (paste markdown)
|
|
594
|
+
// =============================================================================
|
|
595
|
+
|
|
596
|
+
document.getElementById("btn-import-memory").addEventListener("click", () => {
|
|
597
|
+
document.getElementById("import-form").style.display =
|
|
598
|
+
document.getElementById("import-form").style.display === "none" ? "block" : "none";
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
document.getElementById("btn-cancel-import").addEventListener("click", () => {
|
|
602
|
+
document.getElementById("import-form").style.display = "none";
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
document.getElementById("btn-run-import").addEventListener("click", async () => {
|
|
606
|
+
const content = document.getElementById("import-content").value;
|
|
607
|
+
const source_filename = document.getElementById("import-filename").value || undefined;
|
|
608
|
+
|
|
609
|
+
if (!content.trim()) return toast("Paste some content first", "error");
|
|
610
|
+
|
|
611
|
+
const res = await api("/api/memories/import", {
|
|
612
|
+
method: "POST",
|
|
613
|
+
body: JSON.stringify({ content, source_filename }),
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
if (res.ok) {
|
|
617
|
+
toast(`Imported ${res.data.imported} memories`, "success");
|
|
618
|
+
document.getElementById("import-form").style.display = "none";
|
|
619
|
+
document.getElementById("import-content").value = "";
|
|
620
|
+
loadMemories();
|
|
621
|
+
} else {
|
|
622
|
+
toast(res.error || "Import failed", "error");
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// =============================================================================
|
|
627
|
+
// WORKSPACE FILES
|
|
628
|
+
// =============================================================================
|
|
629
|
+
|
|
630
|
+
async function loadWorkspaceFiles() {
|
|
631
|
+
const res = await api("/api/workspace-files");
|
|
632
|
+
const container = document.getElementById("workspace-files");
|
|
633
|
+
|
|
634
|
+
if (!res.ok || !res.data.length) {
|
|
635
|
+
container.innerHTML = '<p class="hint">No .md files in workspace.</p>';
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
container.innerHTML = res.data
|
|
640
|
+
.map(
|
|
641
|
+
(f) => `
|
|
642
|
+
<div style="display:inline-flex;flex-direction:column;gap:0.15rem;">
|
|
643
|
+
<button class="workspace-file-btn" onclick="viewWorkspaceFile('${f.name}')">${f.name}</button>
|
|
644
|
+
<div class="workspace-import-actions">
|
|
645
|
+
<button class="workspace-import-btn" onclick="importWorkspaceTo('${f.name}', 'persona')" title="Import as persona entries via LLM">→ Persona</button>
|
|
646
|
+
<button class="workspace-import-btn" onclick="importWorkspaceTo('${f.name}', 'memory')" title="Import as memory facts via LLM">→ Memory</button>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
`,
|
|
650
|
+
)
|
|
651
|
+
.join("");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
window.viewWorkspaceFile = async function (filename) {
|
|
655
|
+
const res = await api(`/api/workspace-files/${encodeURIComponent(filename)}`);
|
|
656
|
+
if (!res.ok) return toast(res.error || "Failed to read", "error");
|
|
657
|
+
const el = document.getElementById("workspace-content");
|
|
658
|
+
el.textContent = res.data.content;
|
|
659
|
+
el.style.display = "block";
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
window.importWorkspaceTo = async function (filename, target) {
|
|
663
|
+
const tierPrompt = target === "memory" ? " (tier: stable)" : "";
|
|
664
|
+
if (!confirm(`Import "${filename}" as ${target} entries${tierPrompt}?\n\nThis uses LLM to semantically chunk the file.`)) return;
|
|
665
|
+
|
|
666
|
+
toast(`Importing ${filename} as ${target}… (LLM processing)`, "info");
|
|
667
|
+
|
|
668
|
+
const res = await api("/api/workspace-import", {
|
|
669
|
+
method: "POST",
|
|
670
|
+
body: JSON.stringify({ filename, target }),
|
|
413
671
|
});
|
|
414
672
|
|
|
415
|
-
|
|
416
|
-
|
|
673
|
+
if (res.ok) {
|
|
674
|
+
toast(`Imported ${res.data.imported}/${res.data.total} ${target} entries from ${filename}`, "success");
|
|
675
|
+
if (target === "persona") loadPersonas();
|
|
676
|
+
else loadMemories();
|
|
677
|
+
} else {
|
|
678
|
+
toast(res.error || "Import failed", "error");
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// =============================================================================
|
|
683
|
+
// KNOWLEDGE GRAPH (D3.js)
|
|
684
|
+
// =============================================================================
|
|
685
|
+
|
|
686
|
+
const TIER_COLORS = {
|
|
687
|
+
permanent: "#4ade80",
|
|
688
|
+
stable: "#4f9cf7",
|
|
689
|
+
daily: "#f7b955",
|
|
690
|
+
session: "#a78bfa",
|
|
691
|
+
volatile: "#f7555a",
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const PERSONA_COLOR = "#f7b955";
|
|
695
|
+
const MEMORY_DEFAULT_COLOR = "#4f9cf7";
|
|
417
696
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
world.attr("transform", event.transform);
|
|
422
|
-
});
|
|
697
|
+
async function loadGraph() {
|
|
698
|
+
const res = await api("/api/graph");
|
|
699
|
+
if (!res.ok) return;
|
|
423
700
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
svg.on("dblclick.zoom", null);
|
|
701
|
+
allNodes = res.data.nodes;
|
|
702
|
+
allEdges = res.data.edges;
|
|
427
703
|
|
|
428
|
-
|
|
429
|
-
|
|
704
|
+
const svg = d3.select("#graph-svg");
|
|
705
|
+
svg.selectAll("*").remove();
|
|
706
|
+
|
|
707
|
+
const container = document.getElementById("graph-container");
|
|
708
|
+
const width = container.clientWidth;
|
|
709
|
+
const height = container.clientHeight;
|
|
430
710
|
|
|
431
|
-
|
|
432
|
-
const chargeStrength = nodeCount > 100 ? -60 : nodeCount > 50 ? -80 : -100;
|
|
433
|
-
const linkDist = nodeCount > 100 ? 80 : 120;
|
|
711
|
+
svg.attr("width", width).attr("height", height);
|
|
434
712
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
713
|
+
// Defs for arrowheads
|
|
714
|
+
const defs = svg.append("defs");
|
|
715
|
+
defs
|
|
716
|
+
.append("marker")
|
|
717
|
+
.attr("id", "arrowhead")
|
|
718
|
+
.attr("viewBox", "0 -5 10 10")
|
|
719
|
+
.attr("refX", 20)
|
|
720
|
+
.attr("refY", 0)
|
|
721
|
+
.attr("markerWidth", 6)
|
|
722
|
+
.attr("markerHeight", 6)
|
|
723
|
+
.attr("orient", "auto")
|
|
724
|
+
.append("path")
|
|
725
|
+
.attr("d", "M0,-5L10,0L0,5")
|
|
726
|
+
.attr("fill", "#6b7280");
|
|
727
|
+
|
|
728
|
+
const g = svg.append("g");
|
|
729
|
+
|
|
730
|
+
// Zoom
|
|
731
|
+
const zoom = d3.zoom().scaleExtent([0.1, 5]).on("zoom", (event) => {
|
|
732
|
+
g.attr("transform", event.transform);
|
|
733
|
+
});
|
|
734
|
+
svg.call(zoom);
|
|
735
|
+
|
|
736
|
+
// Force simulation
|
|
737
|
+
const simulation = d3
|
|
738
|
+
.forceSimulation(res.data.nodes)
|
|
739
|
+
.force(
|
|
740
|
+
"link",
|
|
741
|
+
d3
|
|
742
|
+
.forceLink(res.data.edges)
|
|
743
|
+
.id((d) => d.id)
|
|
744
|
+
.distance(120),
|
|
745
|
+
)
|
|
746
|
+
.force("charge", d3.forceManyBody().strength(-200))
|
|
438
747
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
439
|
-
.force("collision", d3.forceCollide().radius(
|
|
440
|
-
.force("x", d3.forceX(width / 2).strength(0.06))
|
|
441
|
-
.force("y", d3.forceY(height / 2).strength(0.06))
|
|
442
|
-
.alphaDecay(0.03);
|
|
748
|
+
.force("collision", d3.forceCollide().radius(30));
|
|
443
749
|
|
|
444
|
-
//
|
|
445
|
-
const link =
|
|
750
|
+
// Edges
|
|
751
|
+
const link = g
|
|
752
|
+
.append("g")
|
|
446
753
|
.selectAll("line")
|
|
447
|
-
.data(data.edges)
|
|
754
|
+
.data(res.data.edges)
|
|
448
755
|
.join("line")
|
|
449
756
|
.attr("class", "graph-edge")
|
|
450
|
-
.attr("stroke", d =>
|
|
451
|
-
|
|
452
|
-
|
|
757
|
+
.attr("stroke", (d) => {
|
|
758
|
+
if (d.sourceType === "persona" || d.targetType === "persona") return PERSONA_COLOR;
|
|
759
|
+
return "#6b7280";
|
|
760
|
+
})
|
|
761
|
+
.attr("stroke-width", (d) => Math.max(1, d.weight))
|
|
762
|
+
.attr("marker-end", "url(#arrowhead)");
|
|
453
763
|
|
|
454
764
|
// Edge labels
|
|
455
|
-
const
|
|
765
|
+
const edgeLabel = g
|
|
766
|
+
.append("g")
|
|
456
767
|
.selectAll("text")
|
|
457
|
-
.data(data.edges)
|
|
768
|
+
.data(res.data.edges)
|
|
458
769
|
.join("text")
|
|
459
770
|
.attr("class", "graph-edge-label")
|
|
460
|
-
.text(d => d.relationship
|
|
771
|
+
.text((d) => d.relationship);
|
|
461
772
|
|
|
462
|
-
//
|
|
463
|
-
const node =
|
|
773
|
+
// Nodes
|
|
774
|
+
const node = g
|
|
775
|
+
.append("g")
|
|
464
776
|
.selectAll("g")
|
|
465
|
-
.data(data.nodes)
|
|
777
|
+
.data(res.data.nodes)
|
|
466
778
|
.join("g")
|
|
467
779
|
.attr("class", "graph-node")
|
|
468
|
-
.call(
|
|
469
|
-
.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
780
|
+
.call(
|
|
781
|
+
d3.drag()
|
|
782
|
+
.filter((event) => !event.shiftKey) // let shift+drag fall through to link handler
|
|
783
|
+
.on("start", dragStart)
|
|
784
|
+
.on("drag", dragging)
|
|
785
|
+
.on("end", dragEnd)
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
// Glow
|
|
789
|
+
node
|
|
790
|
+
.append("circle")
|
|
475
791
|
.attr("class", "node-glow")
|
|
476
|
-
.attr("r", d => nodeRadius(d) +
|
|
792
|
+
.attr("r", (d) => nodeRadius(d) + 6)
|
|
477
793
|
.attr("fill", "none")
|
|
478
|
-
.attr("stroke", d =>
|
|
479
|
-
.attr("stroke-width",
|
|
480
|
-
.attr("stroke-opacity", 0
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
794
|
+
.attr("stroke", (d) => nodeColor(d))
|
|
795
|
+
.attr("stroke-width", 3)
|
|
796
|
+
.attr("stroke-opacity", 0);
|
|
797
|
+
|
|
798
|
+
// Memory nodes = circles, Persona nodes = diamonds
|
|
799
|
+
node.each(function (d) {
|
|
800
|
+
const el = d3.select(this);
|
|
801
|
+
if (d.type === "persona") {
|
|
802
|
+
const size = 10;
|
|
803
|
+
el.append("path")
|
|
804
|
+
.attr("class", "node-diamond")
|
|
805
|
+
.attr("d", `M0,${-size} L${size},0 L0,${size} L${-size},0 Z`)
|
|
806
|
+
.attr("fill", PERSONA_COLOR)
|
|
807
|
+
.attr("stroke", "#0f1117")
|
|
808
|
+
.attr("stroke-width", 1.5);
|
|
809
|
+
} else {
|
|
810
|
+
el.append("circle")
|
|
811
|
+
.attr("class", "node-circle")
|
|
812
|
+
.attr("r", nodeRadius(d))
|
|
813
|
+
.attr("fill", nodeColor(d))
|
|
814
|
+
.attr("stroke", "#0f1117")
|
|
815
|
+
.attr("stroke-width", 1.5);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
490
818
|
|
|
491
819
|
// Labels
|
|
492
|
-
node
|
|
820
|
+
node
|
|
821
|
+
.append("text")
|
|
493
822
|
.attr("class", "node-label")
|
|
494
|
-
.
|
|
495
|
-
.attr("
|
|
496
|
-
.
|
|
497
|
-
.style("display",
|
|
498
|
-
|
|
499
|
-
// Click
|
|
500
|
-
node.on("click", (_event, d) =>
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
823
|
+
.attr("dy", (d) => nodeRadius(d) + 14)
|
|
824
|
+
.attr("text-anchor", "middle")
|
|
825
|
+
.text((d) => truncate(d.label, 20))
|
|
826
|
+
.style("display", showLabels ? "block" : "none");
|
|
827
|
+
|
|
828
|
+
// Click handler
|
|
829
|
+
node.on("click", (_event, d) => showNodeDetail(d));
|
|
830
|
+
|
|
831
|
+
// Shift+drag to link — use native mousedown since D3 drag ignores shift events
|
|
832
|
+
node.on("mousedown.link", function (event, d) {
|
|
833
|
+
if (!event.shiftKey) return;
|
|
834
|
+
event.stopPropagation();
|
|
835
|
+
event.preventDefault();
|
|
836
|
+
const svgEl = document.getElementById("graph-svg");
|
|
837
|
+
const pt = svgEl.createSVGPoint();
|
|
838
|
+
pt.x = event.clientX;
|
|
839
|
+
pt.y = event.clientY;
|
|
840
|
+
const ctm = g.node().getScreenCTM();
|
|
841
|
+
if (!ctm) return;
|
|
842
|
+
const svgPt = pt.matrixTransform(ctm.inverse());
|
|
843
|
+
|
|
844
|
+
const line = g
|
|
845
|
+
.append("line")
|
|
846
|
+
.attr("class", "graph-link-preview")
|
|
847
|
+
.attr("x1", d.x)
|
|
848
|
+
.attr("y1", d.y)
|
|
849
|
+
.attr("x2", d.x)
|
|
850
|
+
.attr("y2", d.y);
|
|
851
|
+
|
|
852
|
+
shiftLinkState = { sourceNode: d, line };
|
|
505
853
|
});
|
|
506
854
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
855
|
+
svg.on("mousemove", function (event) {
|
|
856
|
+
if (!shiftLinkState) return;
|
|
857
|
+
const pt = this.createSVGPoint();
|
|
858
|
+
pt.x = event.clientX;
|
|
859
|
+
pt.y = event.clientY;
|
|
860
|
+
const svgPt = pt.matrixTransform(g.node().getScreenCTM().inverse());
|
|
861
|
+
shiftLinkState.line.attr("x2", svgPt.x).attr("y2", svgPt.y);
|
|
512
862
|
});
|
|
513
863
|
|
|
514
|
-
|
|
515
|
-
|
|
864
|
+
svg.on("mouseup", async function (event) {
|
|
865
|
+
if (!shiftLinkState) return;
|
|
866
|
+
shiftLinkState.line.remove();
|
|
867
|
+
const source = shiftLinkState.sourceNode;
|
|
868
|
+
shiftLinkState = null;
|
|
869
|
+
|
|
870
|
+
// Find target node under cursor
|
|
871
|
+
const target = findNodeAt(event, res.data.nodes, g);
|
|
872
|
+
if (!target || target.id === source.id) return;
|
|
873
|
+
|
|
874
|
+
const relationship = prompt(`Link "${truncate(source.label, 30)}" → "${truncate(target.label, 30)}"\n\nRelationship type:`);
|
|
875
|
+
if (!relationship) return;
|
|
876
|
+
|
|
877
|
+
const body = { relationship_type: relationship };
|
|
878
|
+
if (source.type === "persona") body.source_persona_id = source.id;
|
|
879
|
+
else body.source_memory_id = source.id;
|
|
880
|
+
if (target.type === "persona") body.target_persona_id = target.id;
|
|
881
|
+
else body.target_memory_id = target.id;
|
|
882
|
+
|
|
883
|
+
const linkRes = await api("/api/edges", { method: "POST", body: JSON.stringify(body) });
|
|
884
|
+
if (linkRes.ok) {
|
|
885
|
+
toast("Link created", "success");
|
|
886
|
+
loadGraph();
|
|
887
|
+
} else {
|
|
888
|
+
toast(linkRes.error || "Link failed", "error");
|
|
889
|
+
}
|
|
890
|
+
});
|
|
516
891
|
|
|
517
|
-
//
|
|
518
|
-
|
|
892
|
+
// Tick
|
|
893
|
+
simulation.on("tick", () => {
|
|
519
894
|
link
|
|
520
|
-
.attr("x1", d => d.source.x)
|
|
521
|
-
.attr("
|
|
522
|
-
|
|
523
|
-
.attr("
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
895
|
+
.attr("x1", (d) => d.source.x)
|
|
896
|
+
.attr("y1", (d) => d.source.y)
|
|
897
|
+
.attr("x2", (d) => d.target.x)
|
|
898
|
+
.attr("y2", (d) => d.target.y);
|
|
899
|
+
|
|
900
|
+
edgeLabel
|
|
901
|
+
.attr("x", (d) => (d.source.x + d.target.x) / 2)
|
|
902
|
+
.attr("y", (d) => (d.source.y + d.target.y) / 2);
|
|
527
903
|
|
|
528
|
-
|
|
529
|
-
graphSimulation.on("end", () => {
|
|
530
|
-
fitGraphToView(data.nodes, width, height);
|
|
904
|
+
node.attr("transform", (d) => `translate(${d.x},${d.y})`);
|
|
531
905
|
});
|
|
532
906
|
|
|
907
|
+
// Store
|
|
908
|
+
graphInstances = { simulation, svg, g, zoom };
|
|
909
|
+
|
|
910
|
+
// Legend
|
|
911
|
+
renderLegend();
|
|
912
|
+
|
|
913
|
+
// Stats
|
|
914
|
+
document.getElementById("graph-stats").textContent =
|
|
915
|
+
`${res.data.nodes.length} nodes • ${res.data.edges.length} edges — Scroll to zoom • Drag to pan • Click node for details • Shift+drag to link`;
|
|
916
|
+
|
|
533
917
|
function dragStart(event, d) {
|
|
534
|
-
if (
|
|
535
|
-
|
|
918
|
+
if (event.sourceEvent && event.sourceEvent.shiftKey) return;
|
|
919
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
920
|
+
d.fx = d.x;
|
|
921
|
+
d.fy = d.y;
|
|
536
922
|
}
|
|
537
|
-
|
|
923
|
+
|
|
924
|
+
function dragging(event, d) {
|
|
925
|
+
if (event.sourceEvent && event.sourceEvent.shiftKey) return;
|
|
926
|
+
d.fx = event.x;
|
|
927
|
+
d.fy = event.y;
|
|
928
|
+
}
|
|
929
|
+
|
|
538
930
|
function dragEnd(event, d) {
|
|
539
|
-
if (!event.active)
|
|
540
|
-
d.fx = null;
|
|
931
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
932
|
+
d.fx = null;
|
|
933
|
+
d.fy = null;
|
|
541
934
|
}
|
|
542
935
|
}
|
|
543
936
|
|
|
544
|
-
function
|
|
545
|
-
|
|
937
|
+
function findNodeAt(event, nodes, gGroup) {
|
|
938
|
+
const svgEl = document.getElementById("graph-svg");
|
|
939
|
+
const pt = svgEl.createSVGPoint();
|
|
940
|
+
pt.x = event.clientX;
|
|
941
|
+
pt.y = event.clientY;
|
|
942
|
+
const svgPt = pt.matrixTransform(gGroup.node().getScreenCTM().inverse());
|
|
943
|
+
|
|
944
|
+
let closest = null;
|
|
945
|
+
let minDist = Infinity;
|
|
946
|
+
for (const n of nodes) {
|
|
947
|
+
const dx = n.x - svgPt.x;
|
|
948
|
+
const dy = n.y - svgPt.y;
|
|
949
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
950
|
+
if (dist < 20 && dist < minDist) {
|
|
951
|
+
minDist = dist;
|
|
952
|
+
closest = n;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return closest;
|
|
546
956
|
}
|
|
547
957
|
|
|
548
|
-
function
|
|
549
|
-
if (
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
nodes.forEach(d => {
|
|
554
|
-
if (d.x < minX) minX = d.x;
|
|
555
|
-
if (d.x > maxX) maxX = d.x;
|
|
556
|
-
if (d.y < minY) minY = d.y;
|
|
557
|
-
if (d.y > maxY) maxY = d.y;
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
const padding = 60;
|
|
561
|
-
const graphWidth = maxX - minX + padding * 2;
|
|
562
|
-
const graphHeight = maxY - minY + padding * 2;
|
|
563
|
-
const scale = Math.min(width / graphWidth, height / graphHeight, 2);
|
|
564
|
-
const tx = width / 2 - (minX + maxX) / 2 * scale;
|
|
565
|
-
const ty = height / 2 - (minY + maxY) / 2 * scale;
|
|
958
|
+
function nodeRadius(d) {
|
|
959
|
+
if (d.type === "persona") return 10;
|
|
960
|
+
const base = 6;
|
|
961
|
+
return Math.min(base + (d.accessCount || 0) * 0.5, 18);
|
|
962
|
+
}
|
|
566
963
|
|
|
567
|
-
|
|
568
|
-
|
|
964
|
+
function nodeColor(d) {
|
|
965
|
+
if (d.type === "persona") return PERSONA_COLOR;
|
|
966
|
+
return TIER_COLORS[d.tier] || MEMORY_DEFAULT_COLOR;
|
|
569
967
|
}
|
|
570
968
|
|
|
571
|
-
function
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
`<
|
|
576
|
-
|
|
969
|
+
function renderLegend() {
|
|
970
|
+
const legend = document.getElementById("graph-legend");
|
|
971
|
+
const items = [
|
|
972
|
+
...Object.entries(TIER_COLORS).map(([tier, color]) => `<div class="legend-item"><span class="legend-dot" style="background:${color}"></span>${tier}</div>`),
|
|
973
|
+
`<div class="legend-item"><span class="legend-diamond" style="background:${PERSONA_COLOR}"></span>persona</div>`,
|
|
974
|
+
];
|
|
975
|
+
legend.innerHTML = items.join("");
|
|
577
976
|
}
|
|
578
977
|
|
|
579
|
-
function
|
|
580
|
-
const panel =
|
|
581
|
-
if (!panel) return;
|
|
978
|
+
function showNodeDetail(d) {
|
|
979
|
+
const panel = document.getElementById("graph-detail");
|
|
582
980
|
panel.style.display = "block";
|
|
981
|
+
|
|
982
|
+
const typeLabel = d.type === "persona" ? "🎭 Persona Trait" : "🧠 Memory";
|
|
583
983
|
panel.innerHTML = `
|
|
584
984
|
<div class="detail-header">
|
|
585
|
-
<
|
|
985
|
+
<strong>${typeLabel}</strong>
|
|
586
986
|
<span class="detail-category">${d.category || "uncategorized"}</span>
|
|
587
|
-
|
|
987
|
+
${badge(d.tier || "—")}
|
|
988
|
+
<span class="detail-id">${d.id.substring(0, 8)}</span>
|
|
989
|
+
<button class="btn-sm btn-secondary" onclick="document.getElementById('graph-detail').style.display='none'">✕</button>
|
|
588
990
|
</div>
|
|
589
|
-
<
|
|
991
|
+
<div class="detail-content">${d.label}</div>
|
|
590
992
|
<div class="detail-stats">
|
|
591
|
-
<span>
|
|
592
|
-
<span>Score:
|
|
593
|
-
<span
|
|
993
|
+
<span>Accesses: ${d.accessCount || 0}</span>
|
|
994
|
+
<span>Score: ${d.score != null ? d.score.toFixed(1) : "—"}</span>
|
|
995
|
+
${d.isAlwaysActive ? "<span>Always Active ✅</span>" : ""}
|
|
594
996
|
</div>
|
|
595
997
|
`;
|
|
596
998
|
}
|
|
597
999
|
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
fitGraphToView(nodes, container.clientWidth, container.clientHeight);
|
|
1000
|
+
// Graph controls
|
|
1001
|
+
document.getElementById("btn-fit-graph").addEventListener("click", () => {
|
|
1002
|
+
if (!graphInstances.svg || !graphInstances.zoom) return;
|
|
1003
|
+
graphInstances.svg
|
|
1004
|
+
.transition()
|
|
1005
|
+
.duration(500)
|
|
1006
|
+
.call(graphInstances.zoom.transform, d3.zoomIdentity);
|
|
606
1007
|
});
|
|
607
1008
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
d3.selectAll(".node-label").style("display",
|
|
611
|
-
|
|
1009
|
+
document.getElementById("btn-toggle-labels").addEventListener("click", () => {
|
|
1010
|
+
showLabels = !showLabels;
|
|
1011
|
+
d3.selectAll(".node-label").style("display", showLabels ? "block" : "none");
|
|
1012
|
+
document.getElementById("btn-toggle-labels").textContent = showLabels ? "🏷️ Labels On" : "🏷️ Labels Off";
|
|
612
1013
|
});
|
|
613
1014
|
|
|
1015
|
+
document.getElementById("btn-refresh-graph").addEventListener("click", () => loadGraph());
|
|
614
1016
|
|
|
615
1017
|
// =============================================================================
|
|
616
1018
|
// SCRIPTS
|
|
617
1019
|
// =============================================================================
|
|
618
1020
|
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1021
|
+
document.getElementById("btn-run-sleep").addEventListener("click", async () => {
|
|
1022
|
+
const statusEl = document.getElementById("sleep-status");
|
|
1023
|
+
statusEl.textContent = "Running sleep cycle…";
|
|
1024
|
+
statusEl.className = "script-status running";
|
|
1025
|
+
|
|
1026
|
+
const res = await api("/api/scripts/sleep", { method: "POST", body: JSON.stringify({ agentId: currentAgent }) });
|
|
1027
|
+
|
|
1028
|
+
if (res.ok) {
|
|
1029
|
+
statusEl.textContent = "✅ " + (res.data?.message || "Complete");
|
|
1030
|
+
statusEl.className = "script-status success";
|
|
1031
|
+
} else {
|
|
1032
|
+
statusEl.textContent = "❌ " + (res.error || "Failed");
|
|
1033
|
+
statusEl.className = "script-status error";
|
|
630
1034
|
}
|
|
631
1035
|
});
|
|
632
1036
|
|
|
633
|
-
|
|
634
|
-
const file =
|
|
635
|
-
if (!file
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1037
|
+
document.getElementById("btn-run-persona-import").addEventListener("click", async () => {
|
|
1038
|
+
const file = document.getElementById("persona-import-file").value;
|
|
1039
|
+
if (!file) return toast("Enter a file path", "error");
|
|
1040
|
+
|
|
1041
|
+
const statusEl = document.getElementById("persona-import-status");
|
|
1042
|
+
statusEl.textContent = "Importing…";
|
|
1043
|
+
statusEl.className = "script-status running";
|
|
1044
|
+
|
|
1045
|
+
const res = await api("/api/scripts/persona-import", {
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
body: JSON.stringify({ file, agentId: currentAgent }),
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
if (res.ok) {
|
|
1051
|
+
statusEl.textContent = "✅ " + (res.data?.message || "Done");
|
|
1052
|
+
statusEl.className = "script-status success";
|
|
1053
|
+
} else {
|
|
1054
|
+
statusEl.textContent = "❌ " + (res.error || "Failed");
|
|
1055
|
+
statusEl.className = "script-status error";
|
|
646
1056
|
}
|
|
647
1057
|
});
|
|
648
1058
|
|
|
649
1059
|
// =============================================================================
|
|
650
|
-
//
|
|
1060
|
+
// CONFIG
|
|
651
1061
|
// =============================================================================
|
|
652
1062
|
|
|
1063
|
+
const CONFIG_MAP = [
|
|
1064
|
+
{ id: "cfg-rag-semantic-limit", path: "rag.semanticLimit", type: "number" },
|
|
1065
|
+
{ id: "cfg-rag-total-limit", path: "rag.totalLimit", type: "number" },
|
|
1066
|
+
{ id: "cfg-rag-linked-similarity", path: "rag.linkedSimilarity", type: "number" },
|
|
1067
|
+
{ id: "cfg-rag-max-traversal-depth", path: "rag.maxTraversalDepth", type: "number" },
|
|
1068
|
+
{ id: "cfg-persona-situational-limit", path: "persona.situationalLimit", type: "number" },
|
|
1069
|
+
{ id: "cfg-prompt-memory", path: "prompts.memoryRules", type: "text" },
|
|
1070
|
+
{ id: "cfg-prompt-persona", path: "prompts.personaRules", type: "text" },
|
|
1071
|
+
{ id: "cfg-prompt-heartbeat", path: "prompts.heartbeatRules", type: "text" },
|
|
1072
|
+
{ id: "cfg-prompt-heartbeat-path", path: "prompts.heartbeatFilePath", type: "text" },
|
|
1073
|
+
{ id: "cfg-sleep-dedup-threshold", path: "sleep.duplicateSimilarityThreshold", type: "number" },
|
|
1074
|
+
{ id: "cfg-sleep-low-value-age", path: "sleep.lowValueAgeDays", type: "number" },
|
|
1075
|
+
{ id: "cfg-sleep-link-min", path: "sleep.linkSimilarityMin", type: "number" },
|
|
1076
|
+
{ id: "cfg-sleep-link-max", path: "sleep.linkSimilarityMax", type: "number" },
|
|
1077
|
+
{ id: "cfg-sleep-episodic-batch", path: "sleep.episodicBatchLimit", type: "number" },
|
|
1078
|
+
{ id: "cfg-sleep-dedup-scan", path: "sleep.duplicateScanLimit", type: "number" },
|
|
1079
|
+
{ id: "cfg-sleep-link-candidates", path: "sleep.linkCandidatesPerMemory", type: "number" },
|
|
1080
|
+
{ id: "cfg-sleep-link-batch", path: "sleep.linkBatchSize", type: "number" },
|
|
1081
|
+
{ id: "cfg-sleep-link-scan", path: "sleep.linkScanLimit", type: "number" },
|
|
1082
|
+
{ id: "cfg-sleep-protected-tiers", path: "sleep.lowValueProtectedTiers", type: "csv" },
|
|
1083
|
+
{ id: "cfg-dedup-cache", path: "dedup.maxCacheSize", type: "number" },
|
|
1084
|
+
];
|
|
1085
|
+
|
|
1086
|
+
function getConfigValue(cfg, path) {
|
|
1087
|
+
return path.split(".").reduce((o, k) => o?.[k], cfg);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function setConfigValue(cfg, path, value) {
|
|
1091
|
+
const keys = path.split(".");
|
|
1092
|
+
let obj = cfg;
|
|
1093
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1094
|
+
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
1095
|
+
obj = obj[keys[i]];
|
|
1096
|
+
}
|
|
1097
|
+
obj[keys[keys.length - 1]] = value;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
653
1100
|
async function loadConfig() {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
$("cfg-prompt-heartbeat").value = config.prompts.heartbeatRules;
|
|
669
|
-
$("cfg-prompt-heartbeat-path").value = config.prompts.heartbeatFilePath;
|
|
670
|
-
|
|
671
|
-
// Sleep
|
|
672
|
-
$("cfg-sleep-dedup-threshold").value = config.sleep.duplicateSimilarityThreshold;
|
|
673
|
-
$("cfg-sleep-low-value-age").value = config.sleep.lowValueAgeDays;
|
|
674
|
-
$("cfg-sleep-link-min").value = config.sleep.linkSimilarityMin;
|
|
675
|
-
$("cfg-sleep-link-max").value = config.sleep.linkSimilarityMax;
|
|
676
|
-
|
|
677
|
-
// Technical
|
|
678
|
-
$("cfg-dedup-cache").value = config.dedup.maxCacheSize;
|
|
679
|
-
|
|
680
|
-
} catch (err) { toast(err.message, "error"); }
|
|
1101
|
+
const res = await api("/api/config");
|
|
1102
|
+
if (!res.ok) return;
|
|
1103
|
+
const cfg = res.data;
|
|
1104
|
+
|
|
1105
|
+
for (const item of CONFIG_MAP) {
|
|
1106
|
+
const el = document.getElementById(item.id);
|
|
1107
|
+
if (!el) continue;
|
|
1108
|
+
const val = getConfigValue(cfg, item.path);
|
|
1109
|
+
if (item.type === "csv" && Array.isArray(val)) {
|
|
1110
|
+
el.value = val.join(", ");
|
|
1111
|
+
} else {
|
|
1112
|
+
el.value = val ?? "";
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
681
1115
|
}
|
|
682
1116
|
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
sleep: {
|
|
700
|
-
// Keep other sleep settings at their defaults if not in UI
|
|
701
|
-
episodicBatchLimit: 100,
|
|
702
|
-
duplicateScanLimit: 200,
|
|
703
|
-
linkCandidatesPerMemory: 5,
|
|
704
|
-
linkBatchSize: 20,
|
|
705
|
-
linkScanLimit: 50,
|
|
706
|
-
lowValueProtectedTiers: ['permanent', 'stable'],
|
|
707
|
-
|
|
708
|
-
duplicateSimilarityThreshold: parseFloat($("cfg-sleep-dedup-threshold").value),
|
|
709
|
-
lowValueAgeDays: parseInt($("cfg-sleep-low-value-age").value),
|
|
710
|
-
linkSimilarityMin: parseFloat($("cfg-sleep-link-min").value),
|
|
711
|
-
linkSimilarityMax: parseFloat($("cfg-sleep-link-max").value),
|
|
712
|
-
},
|
|
713
|
-
dedup: {
|
|
714
|
-
maxCacheSize: parseInt($("cfg-dedup-cache").value)
|
|
1117
|
+
document.getElementById("btn-save-config").addEventListener("click", async () => {
|
|
1118
|
+
const res = await api("/api/config");
|
|
1119
|
+
if (!res.ok) return;
|
|
1120
|
+
const cfg = res.data;
|
|
1121
|
+
|
|
1122
|
+
for (const item of CONFIG_MAP) {
|
|
1123
|
+
const el = document.getElementById(item.id);
|
|
1124
|
+
if (!el) continue;
|
|
1125
|
+
let val;
|
|
1126
|
+
if (item.type === "number") {
|
|
1127
|
+
val = parseFloat(el.value);
|
|
1128
|
+
if (isNaN(val)) continue;
|
|
1129
|
+
} else if (item.type === "csv") {
|
|
1130
|
+
val = el.value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1131
|
+
} else {
|
|
1132
|
+
val = el.value;
|
|
715
1133
|
}
|
|
716
|
-
|
|
1134
|
+
setConfigValue(cfg, item.path, val);
|
|
1135
|
+
}
|
|
717
1136
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1137
|
+
const saveRes = await api("/api/config", {
|
|
1138
|
+
method: "POST",
|
|
1139
|
+
body: JSON.stringify(cfg),
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
if (saveRes.ok) {
|
|
1143
|
+
toast("Configuration saved", "success");
|
|
1144
|
+
} else {
|
|
1145
|
+
toast(saveRes.error || "Save failed", "error");
|
|
1146
|
+
}
|
|
723
1147
|
});
|
|
724
1148
|
|
|
725
|
-
|
|
1149
|
+
document.getElementById("btn-reset-config").addEventListener("click", async () => {
|
|
726
1150
|
if (!confirm("Reset all settings to defaults?")) return;
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
toast("
|
|
1151
|
+
const res = await api("/api/config/reset", { method: "POST" });
|
|
1152
|
+
if (res.ok) {
|
|
1153
|
+
toast("Reset to defaults", "success");
|
|
730
1154
|
loadConfig();
|
|
731
|
-
}
|
|
1155
|
+
} else {
|
|
1156
|
+
toast(res.error || "Reset failed", "error");
|
|
1157
|
+
}
|
|
732
1158
|
});
|
|
733
1159
|
|
|
1160
|
+
// =============================================================================
|
|
1161
|
+
// LIGHT SLIDER
|
|
1162
|
+
// =============================================================================
|
|
1163
|
+
|
|
1164
|
+
const FILL_LIGHT = 0.75;
|
|
1165
|
+
|
|
1166
|
+
function applyLighting(key) {
|
|
1167
|
+
const root = document.documentElement;
|
|
1168
|
+
const k = parseFloat(key);
|
|
1169
|
+
|
|
1170
|
+
root.style.setProperty('--amb-key-light-intensity', k);
|
|
1171
|
+
root.style.setProperty('--amb-fill-light-intensity', FILL_LIGHT);
|
|
1172
|
+
|
|
1173
|
+
// Text must always contrast against amb-surface (lightness ≈ key × 100%).
|
|
1174
|
+
// Step function: dark surface → light text, light surface → dark text.
|
|
1175
|
+
const s = k * 100;
|
|
1176
|
+
let textL, secL, mutL;
|
|
1177
|
+
|
|
1178
|
+
if (s < 50) {
|
|
1179
|
+
textL = 92;
|
|
1180
|
+
secL = 68;
|
|
1181
|
+
mutL = 55;
|
|
1182
|
+
} else {
|
|
1183
|
+
textL = 10;
|
|
1184
|
+
secL = 30;
|
|
1185
|
+
mutL = 40;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
root.style.setProperty('--text-primary', `hsl(0 0% ${textL}%)`);
|
|
1189
|
+
root.style.setProperty('--text-secondary', `hsl(0 0% ${secL}%)`);
|
|
1190
|
+
root.style.setProperty('--text-muted', `hsl(0 0% ${mutL}%)`);
|
|
1191
|
+
|
|
1192
|
+
const kDisp = document.getElementById('key-light-value');
|
|
1193
|
+
if (kDisp) kDisp.textContent = k.toFixed(2);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function initLightSlider() {
|
|
1197
|
+
const slider = document.getElementById('key-light-slider');
|
|
1198
|
+
if (!slider) return;
|
|
1199
|
+
|
|
1200
|
+
const stored = localStorage.getItem('postclaw-key-light');
|
|
1201
|
+
if (stored) slider.value = stored;
|
|
1202
|
+
|
|
1203
|
+
applyLighting(slider.value);
|
|
1204
|
+
|
|
1205
|
+
slider.addEventListener('input', () => {
|
|
1206
|
+
applyLighting(slider.value);
|
|
1207
|
+
localStorage.setItem('postclaw-key-light', slider.value);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
734
1211
|
// =============================================================================
|
|
735
1212
|
// INIT
|
|
736
1213
|
// =============================================================================
|
|
737
1214
|
|
|
738
|
-
|
|
1215
|
+
async function init() {
|
|
1216
|
+
initLightSlider();
|
|
739
1217
|
await loadAgents();
|
|
740
1218
|
loadPersonas();
|
|
1219
|
+
loadMemories();
|
|
741
1220
|
loadWorkspaceFiles();
|
|
742
|
-
|
|
743
|
-
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
init();
|