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