@christopherlittle51/postclaw 1.1.0 → 1.2.0
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/dashboard/public/app.js +743 -0
- package/dashboard/public/index.html +350 -0
- package/dashboard/public/styles.css +571 -0
- package/dist/dashboard/helpers.d.ts +32 -0
- package/dist/dashboard/helpers.d.ts.map +1 -0
- package/dist/dashboard/helpers.js +122 -0
- package/dist/dashboard/helpers.js.map +1 -0
- package/dist/dashboard/router.d.ts +39 -0
- package/dist/dashboard/router.d.ts.map +1 -0
- package/dist/dashboard/router.js +94 -0
- package/dist/dashboard/router.js.map +1 -0
- package/dist/dashboard/routes/config.d.ts +3 -0
- package/dist/dashboard/routes/config.d.ts.map +1 -0
- package/dist/dashboard/routes/config.js +53 -0
- package/dist/dashboard/routes/config.js.map +1 -0
- package/dist/dashboard/routes/graph.d.ts +11 -0
- package/dist/dashboard/routes/graph.d.ts.map +1 -0
- package/dist/dashboard/routes/graph.js +131 -0
- package/dist/dashboard/routes/graph.js.map +1 -0
- package/dist/dashboard/routes/memories.d.ts +12 -0
- package/dist/dashboard/routes/memories.d.ts.map +1 -0
- package/dist/dashboard/routes/memories.js +199 -0
- package/dist/dashboard/routes/memories.js.map +1 -0
- package/dist/dashboard/routes/personas.d.ts +11 -0
- package/dist/dashboard/routes/personas.d.ts.map +1 -0
- package/dist/dashboard/routes/personas.js +71 -0
- package/dist/dashboard/routes/personas.js.map +1 -0
- package/dist/dashboard/routes/scripts.d.ts +9 -0
- package/dist/dashboard/routes/scripts.d.ts.map +1 -0
- package/dist/dashboard/routes/scripts.js +62 -0
- package/dist/dashboard/routes/scripts.js.map +1 -0
- package/dist/dashboard/routes/workspace.d.ts +10 -0
- package/dist/dashboard/routes/workspace.d.ts.map +1 -0
- package/dist/dashboard/routes/workspace.js +82 -0
- package/dist/dashboard/routes/workspace.js.map +1 -0
- package/dist/dashboard/server.d.ts +14 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +97 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -46
- package/dist/index.js.map +1 -1
- package/dist/schemas/validation.d.ts +123 -0
- package/dist/schemas/validation.d.ts.map +1 -1
- package/dist/schemas/validation.js +70 -1
- package/dist/schemas/validation.js.map +1 -1
- package/dist/scripts/setup-db.d.ts.map +1 -1
- package/dist/scripts/setup-db.js +24 -25
- package/dist/scripts/setup-db.js.map +1 -1
- package/dist/scripts/sleep_cycle.d.ts +12 -0
- package/dist/scripts/sleep_cycle.d.ts.map +1 -1
- package/dist/scripts/sleep_cycle.js +45 -33
- package/dist/scripts/sleep_cycle.js.map +1 -1
- package/dist/services/config.d.ts +31 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +125 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/memoryService.d.ts +10 -19
- package/dist/services/memoryService.d.ts.map +1 -1
- package/dist/services/memoryService.js +20 -76
- package/dist/services/memoryService.js.map +1 -1
- package/dist/services/personaService.d.ts +22 -0
- package/dist/services/personaService.d.ts.map +1 -0
- package/dist/services/personaService.js +148 -0
- package/dist/services/personaService.js.map +1 -0
- package/dist/tests/dashboard-schemas.test.d.ts +6 -0
- package/dist/tests/dashboard-schemas.test.d.ts.map +1 -0
- package/dist/tests/dashboard-schemas.test.js +171 -0
- package/dist/tests/dashboard-schemas.test.js.map +1 -0
- package/dist/tests/helpers.test.d.ts +5 -0
- package/dist/tests/helpers.test.d.ts.map +1 -0
- package/dist/tests/helpers.test.js +66 -0
- package/dist/tests/helpers.test.js.map +1 -0
- package/dist/tests/router.test.d.ts +5 -0
- package/dist/tests/router.test.d.ts.map +1 -0
- package/dist/tests/router.test.js +125 -0
- package/dist/tests/router.test.js.map +1 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +7 -2
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostClaw Dashboard — Frontend Application
|
|
3
|
+
*
|
|
4
|
+
* Tab management, API calls, DOM updates, D3 knowledge graph.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// STATE
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
let currentAgent = "main";
|
|
12
|
+
let memoryPage = 0;
|
|
13
|
+
const PAGE_SIZE = 50;
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// UTILITIES
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
function $(id) { return document.getElementById(id); }
|
|
20
|
+
|
|
21
|
+
async function api(method, path, body = null) {
|
|
22
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
23
|
+
const url = `${path}${sep}agentId=${encodeURIComponent(currentAgent)}`;
|
|
24
|
+
const opts = { method, headers: { "Content-Type": "application/json" } };
|
|
25
|
+
if (body) opts.body = JSON.stringify(body);
|
|
26
|
+
const res = await fetch(url, opts);
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
if (!data.ok) throw new Error(data.error || "API error");
|
|
29
|
+
return data.data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toast(message, type = "info") {
|
|
33
|
+
const container = $("toast-container");
|
|
34
|
+
const el = document.createElement("div");
|
|
35
|
+
el.className = `toast toast-${type}`;
|
|
36
|
+
el.textContent = message;
|
|
37
|
+
container.appendChild(el);
|
|
38
|
+
setTimeout(() => el.remove(), 3000);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tierBadge(tier) {
|
|
42
|
+
return `<span class="badge badge-${tier || 'daily'}">${tier || "daily"}</span>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function boolBadge(val) {
|
|
46
|
+
return `<span class="badge badge-${val}">${val ? "✓" : "✗"}</span>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function truncate(str, len = 80) {
|
|
50
|
+
if (!str) return "";
|
|
51
|
+
return str.length > len ? str.substring(0, len) + "…" : str;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// TAB MANAGEMENT
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
document.querySelectorAll(".tab-btn").forEach(btn => {
|
|
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"));
|
|
62
|
+
btn.classList.add("active");
|
|
63
|
+
$(`tab-${btn.dataset.tab}`).classList.add("active");
|
|
64
|
+
|
|
65
|
+
// Load data for the active tab
|
|
66
|
+
if (btn.dataset.tab === "personas") loadPersonas();
|
|
67
|
+
if (btn.dataset.tab === "memories") loadMemories();
|
|
68
|
+
if (btn.dataset.tab === "graph") loadGraph();
|
|
69
|
+
if (btn.dataset.tab === "config") loadConfig();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// AGENT SELECTOR
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
async function loadAgents() {
|
|
78
|
+
try {
|
|
79
|
+
const agents = await api("GET", "/api/agents");
|
|
80
|
+
const select = $("agent-select");
|
|
81
|
+
select.innerHTML = "";
|
|
82
|
+
agents.forEach(a => {
|
|
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 */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$("agent-select").addEventListener("change", (e) => {
|
|
93
|
+
currentAgent = e.target.value;
|
|
94
|
+
// Reload active tab
|
|
95
|
+
const activeTab = document.querySelector(".tab-btn.active");
|
|
96
|
+
if (activeTab) activeTab.click();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// PERSONAS
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
async function loadPersonas() {
|
|
104
|
+
try {
|
|
105
|
+
const personas = await api("GET", "/api/personas");
|
|
106
|
+
const container = $("persona-list");
|
|
107
|
+
if (personas.length === 0) {
|
|
108
|
+
container.innerHTML = '<p class="hint">No persona entries yet. Create one above.</p>';
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
container.innerHTML = `
|
|
112
|
+
<table class="data-table">
|
|
113
|
+
<thead><tr>
|
|
114
|
+
<th>Category</th><th>Content</th><th>Always Active</th><th>Actions</th>
|
|
115
|
+
</tr></thead>
|
|
116
|
+
<tbody>${personas.map(p => `
|
|
117
|
+
<tr>
|
|
118
|
+
<td><strong>${p.category}</strong></td>
|
|
119
|
+
<td title="${p.content.replace(/"/g, '"')}">${truncate(p.content, 120)}</td>
|
|
120
|
+
<td>${boolBadge(p.is_always_active)}</td>
|
|
121
|
+
<td class="actions">
|
|
122
|
+
<button class="btn-sm btn-secondary" onclick="editPersona('${p.id}')">✏️</button>
|
|
123
|
+
<button class="btn-sm btn-danger" onclick="deletePersonaRow('${p.id}', '${p.category}')">🗑️</button>
|
|
124
|
+
</td>
|
|
125
|
+
</tr>
|
|
126
|
+
`).join("")}</tbody>
|
|
127
|
+
</table>`;
|
|
128
|
+
} catch (err) { toast(err.message, "error"); }
|
|
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"); }
|
|
143
|
+
}
|
|
144
|
+
window.editPersona = editPersona;
|
|
145
|
+
|
|
146
|
+
async function deletePersonaRow(id, category) {
|
|
147
|
+
if (!confirm(`Delete persona "${category}"?`)) return;
|
|
148
|
+
try {
|
|
149
|
+
await api("DELETE", `/api/personas/${id}`);
|
|
150
|
+
toast("Persona deleted", "success");
|
|
151
|
+
loadPersonas();
|
|
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";
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
$("btn-cancel-persona").addEventListener("click", () => {
|
|
165
|
+
$("persona-form").style.display = "none";
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
$("btn-save-persona").addEventListener("click", async () => {
|
|
169
|
+
const id = $("persona-form-id").value;
|
|
170
|
+
const data = {
|
|
171
|
+
category: $("persona-category").value,
|
|
172
|
+
content: $("persona-content").value,
|
|
173
|
+
is_always_active: $("persona-always-active").checked,
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
if (id) {
|
|
177
|
+
await api("PUT", `/api/personas/${id}`, data);
|
|
178
|
+
toast("Persona updated", "success");
|
|
179
|
+
} else {
|
|
180
|
+
await api("POST", "/api/personas", data);
|
|
181
|
+
toast("Persona created", "success");
|
|
182
|
+
}
|
|
183
|
+
$("persona-form").style.display = "none";
|
|
184
|
+
loadPersonas();
|
|
185
|
+
} catch (err) { toast(err.message, "error"); }
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Workspace files
|
|
189
|
+
async function loadWorkspaceFiles() {
|
|
190
|
+
try {
|
|
191
|
+
const files = await api("GET", "/api/workspace-files");
|
|
192
|
+
const container = $("workspace-files");
|
|
193
|
+
if (files.length === 0) {
|
|
194
|
+
container.innerHTML = '<span class="hint">No .md files found in workspace</span>';
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
container.innerHTML = files.map(f =>
|
|
198
|
+
`<button class="workspace-file-btn" onclick="loadWorkspaceFile('${f.name}')">${f.name}</button>`
|
|
199
|
+
).join("");
|
|
200
|
+
} catch { /* workspace might not be configured */ }
|
|
201
|
+
}
|
|
202
|
+
window.loadWorkspaceFile = async function(filename) {
|
|
203
|
+
try {
|
|
204
|
+
const file = await api("GET", `/api/workspace-files/${filename}`);
|
|
205
|
+
$("workspace-content").textContent = file.content;
|
|
206
|
+
$("workspace-content").style.display = "block";
|
|
207
|
+
} catch (err) { toast(err.message, "error"); }
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// =============================================================================
|
|
211
|
+
// MEMORIES
|
|
212
|
+
// =============================================================================
|
|
213
|
+
|
|
214
|
+
async function loadMemories() {
|
|
215
|
+
const search = $("memory-search").value;
|
|
216
|
+
const tier = $("memory-tier-filter").value;
|
|
217
|
+
const archived = $("memory-archived-filter").value;
|
|
218
|
+
|
|
219
|
+
let params = `limit=${PAGE_SIZE}&offset=${memoryPage * PAGE_SIZE}`;
|
|
220
|
+
if (search) params += `&search=${encodeURIComponent(search)}`;
|
|
221
|
+
if (tier) params += `&tier=${tier}`;
|
|
222
|
+
if (archived) params += `&archived=${archived}`;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const result = await api("GET", `/api/memories?${params}`);
|
|
226
|
+
const container = $("memory-list");
|
|
227
|
+
const memories = result.memories;
|
|
228
|
+
if (memories.length === 0) {
|
|
229
|
+
container.innerHTML = '<p class="hint">No memories found.</p>';
|
|
230
|
+
$("memory-pagination").innerHTML = "";
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
container.innerHTML = `
|
|
234
|
+
<table class="data-table">
|
|
235
|
+
<thead><tr>
|
|
236
|
+
<th>Content</th><th>Category</th><th>Tier</th><th>Access</th><th>Actions</th>
|
|
237
|
+
</tr></thead>
|
|
238
|
+
<tbody>${memories.map(m => `
|
|
239
|
+
<tr>
|
|
240
|
+
<td title="${(m.content || '').replace(/"/g, '"')}">${truncate(m.content, 100)}</td>
|
|
241
|
+
<td>${m.category || "—"}</td>
|
|
242
|
+
<td>${tierBadge(m.tier)}</td>
|
|
243
|
+
<td>${m.access_count || 0}</td>
|
|
244
|
+
<td class="actions">
|
|
245
|
+
<button class="btn-sm btn-secondary" onclick="editMemory('${m.id}')">✏️</button>
|
|
246
|
+
<button class="btn-sm btn-danger" onclick="archiveMemory('${m.id}')">🗑️</button>
|
|
247
|
+
</td>
|
|
248
|
+
</tr>
|
|
249
|
+
`).join("")}</tbody>
|
|
250
|
+
</table>`;
|
|
251
|
+
|
|
252
|
+
// Pagination
|
|
253
|
+
const totalPages = Math.ceil(result.total / PAGE_SIZE);
|
|
254
|
+
$("memory-pagination").innerHTML = totalPages > 1
|
|
255
|
+
? `<button class="btn-sm btn-secondary" ${memoryPage === 0 ? "disabled" : ""} onclick="memPrev()">← Prev</button>
|
|
256
|
+
<span>Page ${memoryPage + 1} of ${totalPages} (${result.total} total)</span>
|
|
257
|
+
<button class="btn-sm btn-secondary" ${memoryPage >= totalPages - 1 ? "disabled" : ""} onclick="memNext()">Next →</button>`
|
|
258
|
+
: `<span>${result.total} memories</span>`;
|
|
259
|
+
} catch (err) { toast(err.message, "error"); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
window.memPrev = () => { if (memoryPage > 0) { memoryPage--; loadMemories(); } };
|
|
263
|
+
window.memNext = () => { memoryPage++; loadMemories(); };
|
|
264
|
+
|
|
265
|
+
window.editMemory = async function(id) {
|
|
266
|
+
try {
|
|
267
|
+
const mem = await api("GET", `/api/memories/${id}`);
|
|
268
|
+
if (!mem) return toast("Memory not found", "error");
|
|
269
|
+
$("memory-form-id").value = id;
|
|
270
|
+
$("memory-content").value = mem.content;
|
|
271
|
+
$("memory-category").value = mem.category || "";
|
|
272
|
+
$("memory-tier").value = mem.tier || "daily";
|
|
273
|
+
$("memory-form").style.display = "block";
|
|
274
|
+
} catch (err) { toast(err.message, "error"); }
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
window.archiveMemory = async function(id) {
|
|
278
|
+
if (!confirm("Archive this memory?")) return;
|
|
279
|
+
try {
|
|
280
|
+
await api("DELETE", `/api/memories/${id}`);
|
|
281
|
+
toast("Memory archived", "success");
|
|
282
|
+
loadMemories();
|
|
283
|
+
} catch (err) { toast(err.message, "error"); }
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
$("btn-new-memory").addEventListener("click", () => {
|
|
287
|
+
$("memory-form-id").value = "";
|
|
288
|
+
$("memory-content").value = "";
|
|
289
|
+
$("memory-category").value = "";
|
|
290
|
+
$("memory-tier").value = "daily";
|
|
291
|
+
$("memory-form").style.display = "block";
|
|
292
|
+
$("import-form").style.display = "none";
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
$("btn-cancel-memory").addEventListener("click", () => {
|
|
296
|
+
$("memory-form").style.display = "none";
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
$("btn-save-memory").addEventListener("click", async () => {
|
|
300
|
+
const id = $("memory-form-id").value;
|
|
301
|
+
const data = {
|
|
302
|
+
content: $("memory-content").value,
|
|
303
|
+
category: $("memory-category").value || undefined,
|
|
304
|
+
tier: $("memory-tier").value,
|
|
305
|
+
};
|
|
306
|
+
try {
|
|
307
|
+
if (id) {
|
|
308
|
+
await api("PUT", `/api/memories/${id}`, data);
|
|
309
|
+
toast("Memory updated", "success");
|
|
310
|
+
} else {
|
|
311
|
+
await api("POST", "/api/memories", data);
|
|
312
|
+
toast("Memory created", "success");
|
|
313
|
+
}
|
|
314
|
+
$("memory-form").style.display = "none";
|
|
315
|
+
loadMemories();
|
|
316
|
+
} catch (err) { toast(err.message, "error"); }
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Import
|
|
320
|
+
$("btn-import-memory").addEventListener("click", () => {
|
|
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";
|
|
326
|
+
});
|
|
327
|
+
$("btn-run-import").addEventListener("click", async () => {
|
|
328
|
+
const content = $("import-content").value;
|
|
329
|
+
const filename = $("import-filename").value;
|
|
330
|
+
if (!content.trim()) return toast("No content to import", "error");
|
|
331
|
+
try {
|
|
332
|
+
const result = await api("POST", "/api/memories/import", {
|
|
333
|
+
content, source_filename: filename || undefined,
|
|
334
|
+
});
|
|
335
|
+
toast(`Imported ${result.imported} chunks`, "success");
|
|
336
|
+
$("import-form").style.display = "none";
|
|
337
|
+
loadMemories();
|
|
338
|
+
} catch (err) { toast(err.message, "error"); }
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Filter listeners
|
|
342
|
+
$("memory-search").addEventListener("input", debounce(() => { memoryPage = 0; loadMemories(); }, 300));
|
|
343
|
+
$("memory-tier-filter").addEventListener("change", () => { memoryPage = 0; loadMemories(); });
|
|
344
|
+
$("memory-archived-filter").addEventListener("change", () => { memoryPage = 0; loadMemories(); });
|
|
345
|
+
|
|
346
|
+
function debounce(fn, ms) {
|
|
347
|
+
let timer;
|
|
348
|
+
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// =============================================================================
|
|
352
|
+
// KNOWLEDGE GRAPH (D3.js)
|
|
353
|
+
// =============================================================================
|
|
354
|
+
|
|
355
|
+
let graphSimulation = null;
|
|
356
|
+
let graphZoom = null;
|
|
357
|
+
let graphLabelsVisible = true;
|
|
358
|
+
|
|
359
|
+
async function loadGraph() {
|
|
360
|
+
try {
|
|
361
|
+
const data = await api("GET", "/api/graph");
|
|
362
|
+
renderGraph(data);
|
|
363
|
+
$("graph-stats").textContent = `${data.nodes.length} nodes, ${data.edges.length} edges`;
|
|
364
|
+
} catch (err) { toast(err.message, "error"); }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function renderGraph(data) {
|
|
368
|
+
const svg = d3.select("#graph-svg");
|
|
369
|
+
svg.selectAll("*").remove();
|
|
370
|
+
|
|
371
|
+
const container = $("graph-container");
|
|
372
|
+
const width = container.clientWidth;
|
|
373
|
+
const height = container.clientHeight;
|
|
374
|
+
|
|
375
|
+
svg.attr("viewBox", [0, 0, width, height]);
|
|
376
|
+
|
|
377
|
+
// ── Defs: glow filters + arrowhead markers ──
|
|
378
|
+
const defs = svg.append("defs");
|
|
379
|
+
|
|
380
|
+
// Glow filter
|
|
381
|
+
const glow = defs.append("filter").attr("id", "glow");
|
|
382
|
+
glow.append("feGaussianBlur").attr("stdDeviation", "3").attr("result", "color");
|
|
383
|
+
glow.append("feMerge").selectAll("feMergeNode")
|
|
384
|
+
.data(["color", "SourceGraphic"])
|
|
385
|
+
.join("feMergeNode").attr("in", d => d);
|
|
386
|
+
|
|
387
|
+
// Tier colors
|
|
388
|
+
const tierColor = {
|
|
389
|
+
permanent: "#4ade80", stable: "#4f9cf7", daily: "#f7b955",
|
|
390
|
+
session: "#a78bfa", volatile: "#f7555a",
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Relationship colors
|
|
394
|
+
const relColor = {
|
|
395
|
+
related_to: "#6b7280", elaborates: "#4f9cf7", contradicts: "#f7555a",
|
|
396
|
+
depends_on: "#f7b955", part_of: "#a78bfa",
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Arrowhead markers for each relationship
|
|
400
|
+
Object.entries(relColor).forEach(([rel, color]) => {
|
|
401
|
+
defs.append("marker")
|
|
402
|
+
.attr("id", `arrow-${rel}`)
|
|
403
|
+
.attr("viewBox", "0 -5 10 10")
|
|
404
|
+
.attr("refX", 20)
|
|
405
|
+
.attr("refY", 0)
|
|
406
|
+
.attr("markerWidth", 6)
|
|
407
|
+
.attr("markerHeight", 6)
|
|
408
|
+
.attr("orient", "auto")
|
|
409
|
+
.append("path")
|
|
410
|
+
.attr("d", "M0,-5L10,0L0,5")
|
|
411
|
+
.attr("fill", color)
|
|
412
|
+
.attr("opacity", 0.6);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// ── Zoom behavior ──
|
|
416
|
+
const world = svg.append("g").attr("class", "graph-world");
|
|
417
|
+
|
|
418
|
+
graphZoom = d3.zoom()
|
|
419
|
+
.scaleExtent([0.1, 6])
|
|
420
|
+
.on("zoom", (event) => {
|
|
421
|
+
world.attr("transform", event.transform);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
svg.call(graphZoom);
|
|
425
|
+
// Disable double-click zoom (conflicts with node interaction)
|
|
426
|
+
svg.on("dblclick.zoom", null);
|
|
427
|
+
|
|
428
|
+
// ── Force simulation ──
|
|
429
|
+
if (graphSimulation) graphSimulation.stop();
|
|
430
|
+
|
|
431
|
+
const nodeCount = data.nodes.length;
|
|
432
|
+
const chargeStrength = nodeCount > 100 ? -60 : nodeCount > 50 ? -80 : -100;
|
|
433
|
+
const linkDist = nodeCount > 100 ? 80 : 120;
|
|
434
|
+
|
|
435
|
+
graphSimulation = d3.forceSimulation(data.nodes)
|
|
436
|
+
.force("link", d3.forceLink(data.edges).id(d => d.id).distance(linkDist))
|
|
437
|
+
.force("charge", d3.forceManyBody().strength(chargeStrength))
|
|
438
|
+
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
439
|
+
.force("collision", d3.forceCollide().radius(d => nodeRadius(d) + 4))
|
|
440
|
+
.force("x", d3.forceX(width / 2).strength(0.06))
|
|
441
|
+
.force("y", d3.forceY(height / 2).strength(0.06))
|
|
442
|
+
.alphaDecay(0.03);
|
|
443
|
+
|
|
444
|
+
// ── Edges ──
|
|
445
|
+
const link = world.append("g").attr("class", "graph-edges")
|
|
446
|
+
.selectAll("line")
|
|
447
|
+
.data(data.edges)
|
|
448
|
+
.join("line")
|
|
449
|
+
.attr("class", "graph-edge")
|
|
450
|
+
.attr("stroke", d => relColor[d.relationship] || "#6b7280")
|
|
451
|
+
.attr("stroke-width", d => Math.max(0.5, Math.sqrt(d.weight || 1)))
|
|
452
|
+
.attr("marker-end", d => `url(#arrow-${d.relationship || "related_to"})`);
|
|
453
|
+
|
|
454
|
+
// Edge labels
|
|
455
|
+
const linkLabel = world.append("g").attr("class", "graph-edge-labels")
|
|
456
|
+
.selectAll("text")
|
|
457
|
+
.data(data.edges)
|
|
458
|
+
.join("text")
|
|
459
|
+
.attr("class", "graph-edge-label")
|
|
460
|
+
.text(d => d.relationship || "");
|
|
461
|
+
|
|
462
|
+
// ── Nodes ──
|
|
463
|
+
const node = world.append("g").attr("class", "graph-nodes")
|
|
464
|
+
.selectAll("g")
|
|
465
|
+
.data(data.nodes)
|
|
466
|
+
.join("g")
|
|
467
|
+
.attr("class", "graph-node")
|
|
468
|
+
.call(d3.drag()
|
|
469
|
+
.on("start", dragStart)
|
|
470
|
+
.on("drag", dragging)
|
|
471
|
+
.on("end", dragEnd));
|
|
472
|
+
|
|
473
|
+
// Outer glow ring
|
|
474
|
+
node.append("circle")
|
|
475
|
+
.attr("class", "node-glow")
|
|
476
|
+
.attr("r", d => nodeRadius(d) + 4)
|
|
477
|
+
.attr("fill", "none")
|
|
478
|
+
.attr("stroke", d => tierColor[d.tier] || "#6b7280")
|
|
479
|
+
.attr("stroke-width", 2)
|
|
480
|
+
.attr("stroke-opacity", 0.15)
|
|
481
|
+
.attr("filter", "url(#glow)");
|
|
482
|
+
|
|
483
|
+
// Main circle
|
|
484
|
+
node.append("circle")
|
|
485
|
+
.attr("class", "node-circle")
|
|
486
|
+
.attr("r", d => nodeRadius(d))
|
|
487
|
+
.attr("fill", d => tierColor[d.tier] || "#6b7280")
|
|
488
|
+
.attr("stroke", "rgba(255,255,255,0.15)")
|
|
489
|
+
.attr("stroke-width", 1.5);
|
|
490
|
+
|
|
491
|
+
// Labels
|
|
492
|
+
node.append("text")
|
|
493
|
+
.attr("class", "node-label")
|
|
494
|
+
.text(d => d.label ? d.label.substring(0, 30) : "")
|
|
495
|
+
.attr("dx", d => nodeRadius(d) + 5)
|
|
496
|
+
.attr("dy", 4)
|
|
497
|
+
.style("display", graphLabelsVisible ? "block" : "none");
|
|
498
|
+
|
|
499
|
+
// Click → detail panel
|
|
500
|
+
node.on("click", (_event, d) => {
|
|
501
|
+
showGraphDetail(d);
|
|
502
|
+
// Highlight selected
|
|
503
|
+
node.selectAll(".node-circle").attr("stroke", "rgba(255,255,255,0.15)").attr("stroke-width", 1.5);
|
|
504
|
+
d3.select(_event.currentTarget).select(".node-circle").attr("stroke", "#fff").attr("stroke-width", 3);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Hover tooltip
|
|
508
|
+
node.on("mouseenter", function(_event, d) {
|
|
509
|
+
d3.select(this).select(".node-label").style("display", "block");
|
|
510
|
+
}).on("mouseleave", function() {
|
|
511
|
+
if (!graphLabelsVisible) d3.select(this).select(".node-label").style("display", "none");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ── Legend ──
|
|
515
|
+
renderGraphLegend(tierColor);
|
|
516
|
+
|
|
517
|
+
// ── Tick ──
|
|
518
|
+
graphSimulation.on("tick", () => {
|
|
519
|
+
link
|
|
520
|
+
.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
|
|
521
|
+
.attr("x2", d => d.target.x).attr("y2", d => d.target.y);
|
|
522
|
+
linkLabel
|
|
523
|
+
.attr("x", d => (d.source.x + d.target.x) / 2)
|
|
524
|
+
.attr("y", d => (d.source.y + d.target.y) / 2);
|
|
525
|
+
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Auto-fit after simulation stabilizes
|
|
529
|
+
graphSimulation.on("end", () => {
|
|
530
|
+
fitGraphToView(data.nodes, width, height);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
function dragStart(event, d) {
|
|
534
|
+
if (!event.active) graphSimulation.alphaTarget(0.3).restart();
|
|
535
|
+
d.fx = d.x; d.fy = d.y;
|
|
536
|
+
}
|
|
537
|
+
function dragging(event, d) { d.fx = event.x; d.fy = event.y; }
|
|
538
|
+
function dragEnd(event, d) {
|
|
539
|
+
if (!event.active) graphSimulation.alphaTarget(0);
|
|
540
|
+
d.fx = null; d.fy = null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function nodeRadius(d) {
|
|
545
|
+
return Math.max(5, Math.min(14, 4 + Math.sqrt(d.accessCount || 1) * 2));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function fitGraphToView(nodes, width, height) {
|
|
549
|
+
if (!nodes || nodes.length === 0 || !graphZoom) return;
|
|
550
|
+
const svg = d3.select("#graph-svg");
|
|
551
|
+
|
|
552
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
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;
|
|
566
|
+
|
|
567
|
+
svg.transition().duration(500)
|
|
568
|
+
.call(graphZoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function renderGraphLegend(tierColor) {
|
|
572
|
+
const legendContainer = $("graph-legend");
|
|
573
|
+
if (!legendContainer) return;
|
|
574
|
+
legendContainer.innerHTML = Object.entries(tierColor).map(([tier, color]) =>
|
|
575
|
+
`<span class="legend-item"><span class="legend-dot" style="background:${color}"></span>${tier}</span>`
|
|
576
|
+
).join("");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function showGraphDetail(d) {
|
|
580
|
+
const panel = $("graph-detail");
|
|
581
|
+
if (!panel) return;
|
|
582
|
+
panel.style.display = "block";
|
|
583
|
+
panel.innerHTML = `
|
|
584
|
+
<div class="detail-header">
|
|
585
|
+
<span class="badge badge-${d.tier || 'daily'}">${d.tier || "daily"}</span>
|
|
586
|
+
<span class="detail-category">${d.category || "uncategorized"}</span>
|
|
587
|
+
<button class="btn-sm btn-secondary" onclick="this.closest('.graph-detail-panel').style.display='none'">✕</button>
|
|
588
|
+
</div>
|
|
589
|
+
<p class="detail-content">${d.label || "—"}</p>
|
|
590
|
+
<div class="detail-stats">
|
|
591
|
+
<span>Access: <strong>${d.accessCount || 0}</strong></span>
|
|
592
|
+
<span>Score: <strong>${(d.score || 0).toFixed(2)}</strong></span>
|
|
593
|
+
<span class="detail-id">${d.id}</span>
|
|
594
|
+
</div>
|
|
595
|
+
`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Controls
|
|
599
|
+
$("btn-refresh-graph").addEventListener("click", loadGraph);
|
|
600
|
+
|
|
601
|
+
$("btn-fit-graph").addEventListener("click", () => {
|
|
602
|
+
if (!graphSimulation) return;
|
|
603
|
+
const container = $("graph-container");
|
|
604
|
+
const nodes = graphSimulation.nodes();
|
|
605
|
+
fitGraphToView(nodes, container.clientWidth, container.clientHeight);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
$("btn-toggle-labels").addEventListener("click", () => {
|
|
609
|
+
graphLabelsVisible = !graphLabelsVisible;
|
|
610
|
+
d3.selectAll(".node-label").style("display", graphLabelsVisible ? "block" : "none");
|
|
611
|
+
$("btn-toggle-labels").textContent = graphLabelsVisible ? "🏷️ Labels On" : "🏷️ Labels Off";
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
// =============================================================================
|
|
616
|
+
// SCRIPTS
|
|
617
|
+
// =============================================================================
|
|
618
|
+
|
|
619
|
+
$("btn-run-sleep").addEventListener("click", async () => {
|
|
620
|
+
const status = $("sleep-status");
|
|
621
|
+
status.textContent = "Running...";
|
|
622
|
+
status.className = "script-status running";
|
|
623
|
+
try {
|
|
624
|
+
await api("POST", "/api/scripts/sleep", { agentId: currentAgent });
|
|
625
|
+
status.textContent = "Sleep cycle started! Check server logs for progress.";
|
|
626
|
+
status.className = "script-status success";
|
|
627
|
+
} catch (err) {
|
|
628
|
+
status.textContent = `Error: ${err.message}`;
|
|
629
|
+
status.className = "script-status error";
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
$("btn-run-persona-import").addEventListener("click", async () => {
|
|
634
|
+
const file = $("persona-import-file").value;
|
|
635
|
+
if (!file.trim()) return toast("Enter a file path", "error");
|
|
636
|
+
const status = $("persona-import-status");
|
|
637
|
+
status.textContent = "Running...";
|
|
638
|
+
status.className = "script-status running";
|
|
639
|
+
try {
|
|
640
|
+
await api("POST", "/api/scripts/persona-import", { agentId: currentAgent, file });
|
|
641
|
+
status.textContent = "Persona import started! Check server logs for progress.";
|
|
642
|
+
status.className = "script-status success";
|
|
643
|
+
} catch (err) {
|
|
644
|
+
status.textContent = `Error: ${err.message}`;
|
|
645
|
+
status.className = "script-status error";
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// =============================================================================
|
|
650
|
+
// CONFIGURATION
|
|
651
|
+
// =============================================================================
|
|
652
|
+
|
|
653
|
+
async function loadConfig() {
|
|
654
|
+
try {
|
|
655
|
+
const config = await api("GET", "/api/config");
|
|
656
|
+
|
|
657
|
+
// RAG
|
|
658
|
+
$("cfg-rag-semantic-limit").value = config.rag.semanticLimit;
|
|
659
|
+
$("cfg-rag-total-limit").value = config.rag.totalLimit;
|
|
660
|
+
$("cfg-rag-linked-similarity").value = config.rag.linkedSimilarity;
|
|
661
|
+
|
|
662
|
+
// Persona
|
|
663
|
+
$("cfg-persona-situational-limit").value = config.persona.situationalLimit;
|
|
664
|
+
|
|
665
|
+
// Prompts
|
|
666
|
+
$("cfg-prompt-memory").value = config.prompts.memoryRules;
|
|
667
|
+
$("cfg-prompt-persona").value = config.prompts.personaRules;
|
|
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"); }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
$("btn-save-config").addEventListener("click", async () => {
|
|
684
|
+
const data = {
|
|
685
|
+
rag: {
|
|
686
|
+
semanticLimit: parseInt($("cfg-rag-semantic-limit").value),
|
|
687
|
+
totalLimit: parseInt($("cfg-rag-total-limit").value),
|
|
688
|
+
linkedSimilarity: parseFloat($("cfg-rag-linked-similarity").value)
|
|
689
|
+
},
|
|
690
|
+
persona: {
|
|
691
|
+
situationalLimit: parseInt($("cfg-persona-situational-limit").value)
|
|
692
|
+
},
|
|
693
|
+
prompts: {
|
|
694
|
+
memoryRules: $("cfg-prompt-memory").value,
|
|
695
|
+
personaRules: $("cfg-prompt-persona").value,
|
|
696
|
+
heartbeatRules: $("cfg-prompt-heartbeat").value,
|
|
697
|
+
heartbeatFilePath: $("cfg-prompt-heartbeat-path").value
|
|
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)
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
await api("POST", "/api/config", data);
|
|
720
|
+
toast("Configuration saved!", "success");
|
|
721
|
+
loadConfig();
|
|
722
|
+
} catch (err) { toast(err.message, "error"); }
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
$("btn-reset-config").addEventListener("click", async () => {
|
|
726
|
+
if (!confirm("Reset all settings to defaults?")) return;
|
|
727
|
+
try {
|
|
728
|
+
await api("POST", "/api/config/reset");
|
|
729
|
+
toast("Configuration reset to defaults", "success");
|
|
730
|
+
loadConfig();
|
|
731
|
+
} catch (err) { toast(err.message, "error"); }
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// =============================================================================
|
|
735
|
+
// INIT
|
|
736
|
+
// =============================================================================
|
|
737
|
+
|
|
738
|
+
(async function init() {
|
|
739
|
+
await loadAgents();
|
|
740
|
+
loadPersonas();
|
|
741
|
+
loadWorkspaceFiles();
|
|
742
|
+
loadConfig(); // Pre-load config state
|
|
743
|
+
})();
|