superlocalmemory 3.0.18 → 3.0.20
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/cli/commands.py +68 -0
- package/src/superlocalmemory/cli/main.py +10 -2
- package/src/superlocalmemory/core/config.py +1 -0
- package/src/superlocalmemory/core/recall_worker.py +74 -0
- package/src/superlocalmemory/core/worker_pool.py +8 -0
- package/src/superlocalmemory/mcp/tools_core.py +61 -0
- package/src/superlocalmemory/server/routes/memories.py +58 -0
- package/src/superlocalmemory/storage/database.py +26 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +1 -1
- package/ui/js/modal.js +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.20",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -27,6 +27,8 @@ def dispatch(args: Namespace) -> None:
|
|
|
27
27
|
"remember": cmd_remember,
|
|
28
28
|
"recall": cmd_recall,
|
|
29
29
|
"forget": cmd_forget,
|
|
30
|
+
"delete": cmd_delete,
|
|
31
|
+
"update": cmd_update,
|
|
30
32
|
"status": cmd_status,
|
|
31
33
|
"health": cmd_health,
|
|
32
34
|
"trace": cmd_trace,
|
|
@@ -199,6 +201,72 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
199
201
|
print("Cancelled.")
|
|
200
202
|
|
|
201
203
|
|
|
204
|
+
def cmd_delete(args: Namespace) -> None:
|
|
205
|
+
"""Delete a specific memory by exact fact ID."""
|
|
206
|
+
from superlocalmemory.core.config import SLMConfig
|
|
207
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
208
|
+
|
|
209
|
+
config = SLMConfig.load()
|
|
210
|
+
engine = MemoryEngine(config)
|
|
211
|
+
engine.initialize()
|
|
212
|
+
|
|
213
|
+
fact_id = args.fact_id.strip()
|
|
214
|
+
# Look up the memory first so user can confirm
|
|
215
|
+
rows = engine._db.execute(
|
|
216
|
+
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
217
|
+
(fact_id, engine.profile_id),
|
|
218
|
+
)
|
|
219
|
+
if not rows:
|
|
220
|
+
print(f"Memory not found: {fact_id}")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
content_preview = dict(rows[0]).get("content", "")[:120]
|
|
224
|
+
print(f"Memory: {content_preview}")
|
|
225
|
+
|
|
226
|
+
if not getattr(args, "yes", False):
|
|
227
|
+
confirm = input("Delete this memory? [y/N] ").strip().lower()
|
|
228
|
+
if confirm not in ("y", "yes"):
|
|
229
|
+
print("Cancelled.")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
engine._db.delete_fact(fact_id)
|
|
233
|
+
print(f"Deleted: {fact_id}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def cmd_update(args: Namespace) -> None:
|
|
237
|
+
"""Update the content of a specific memory by exact fact ID."""
|
|
238
|
+
from superlocalmemory.core.config import SLMConfig
|
|
239
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
240
|
+
|
|
241
|
+
config = SLMConfig.load()
|
|
242
|
+
engine = MemoryEngine(config)
|
|
243
|
+
engine.initialize()
|
|
244
|
+
|
|
245
|
+
fact_id = args.fact_id.strip()
|
|
246
|
+
new_content = args.content.strip()
|
|
247
|
+
if not new_content:
|
|
248
|
+
print("Error: content cannot be empty")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
rows = engine._db.execute(
|
|
252
|
+
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
253
|
+
(fact_id, engine.profile_id),
|
|
254
|
+
)
|
|
255
|
+
if not rows:
|
|
256
|
+
print(f"Memory not found: {fact_id}")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
old_content = dict(rows[0]).get("content", "")
|
|
260
|
+
print(f"Old: {old_content[:100]}")
|
|
261
|
+
print(f"New: {new_content[:100]}")
|
|
262
|
+
|
|
263
|
+
engine._db.execute(
|
|
264
|
+
"UPDATE atomic_facts SET content = ? WHERE fact_id = ?",
|
|
265
|
+
(new_content, fact_id),
|
|
266
|
+
)
|
|
267
|
+
print(f"Updated: {fact_id}")
|
|
268
|
+
|
|
269
|
+
|
|
202
270
|
def cmd_status(_args: Namespace) -> None:
|
|
203
271
|
"""Show system status."""
|
|
204
272
|
from superlocalmemory.core.config import SLMConfig
|
|
@@ -119,10 +119,18 @@ def main() -> None:
|
|
|
119
119
|
recall_p.add_argument("query", help="Search query")
|
|
120
120
|
recall_p.add_argument("--limit", type=int, default=10, help="Max results (default 10)")
|
|
121
121
|
|
|
122
|
-
forget_p = sub.add_parser("forget", help="Delete memories matching a query")
|
|
122
|
+
forget_p = sub.add_parser("forget", help="Delete memories matching a query (fuzzy)")
|
|
123
123
|
forget_p.add_argument("query", help="Query to match for deletion")
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
delete_p = sub.add_parser("delete", help="Delete a specific memory by ID (precise)")
|
|
126
|
+
delete_p.add_argument("fact_id", help="Exact fact ID to delete")
|
|
127
|
+
delete_p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
|
|
128
|
+
|
|
129
|
+
update_p = sub.add_parser("update", help="Edit the content of a specific memory by ID")
|
|
130
|
+
update_p.add_argument("fact_id", help="Exact fact ID to update")
|
|
131
|
+
update_p.add_argument("content", help="New content for the memory")
|
|
132
|
+
|
|
133
|
+
list_p = sub.add_parser("list", help="List recent memories chronologically (shows IDs for delete/update)")
|
|
126
134
|
list_p.add_argument(
|
|
127
135
|
"--limit", "-n", type=int, default=20, help="Number of entries (default 20)",
|
|
128
136
|
)
|
|
@@ -78,6 +78,25 @@ def _handle_store(content: str, metadata: dict) -> dict:
|
|
|
78
78
|
engine = _get_engine()
|
|
79
79
|
session_id = metadata.pop("session_id", "")
|
|
80
80
|
fact_ids = engine.store(content, session_id=session_id, metadata=metadata)
|
|
81
|
+
|
|
82
|
+
# Generate and persist summary immediately after store (Mode A heuristic, B/C LLM)
|
|
83
|
+
if fact_ids:
|
|
84
|
+
try:
|
|
85
|
+
from superlocalmemory.core.summarizer import Summarizer
|
|
86
|
+
summarizer = Summarizer(engine._config)
|
|
87
|
+
summary = summarizer.summarize_cluster([{"content": content}])
|
|
88
|
+
if summary:
|
|
89
|
+
# Get the memory_id from the first stored fact
|
|
90
|
+
rows = engine._db.execute(
|
|
91
|
+
"SELECT memory_id FROM atomic_facts WHERE fact_id = ? LIMIT 1",
|
|
92
|
+
(fact_ids[0],),
|
|
93
|
+
)
|
|
94
|
+
if rows:
|
|
95
|
+
memory_id = dict(rows[0])["memory_id"]
|
|
96
|
+
engine._db.update_memory_summary(memory_id, summary)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass # Summary is non-critical
|
|
99
|
+
|
|
81
100
|
return {"ok": True, "fact_ids": fact_ids, "count": len(fact_ids)}
|
|
82
101
|
|
|
83
102
|
|
|
@@ -107,6 +126,49 @@ def _handle_get_memory_facts(memory_id: str) -> dict:
|
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
|
|
129
|
+
def _handle_delete_memory(fact_id: str, agent_id: str = "system") -> dict:
|
|
130
|
+
"""Delete a specific atomic fact by ID with audit logging."""
|
|
131
|
+
engine = _get_engine()
|
|
132
|
+
pid = engine.profile_id
|
|
133
|
+
rows = engine._db.execute(
|
|
134
|
+
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ? LIMIT 1",
|
|
135
|
+
(fact_id, pid),
|
|
136
|
+
)
|
|
137
|
+
if not rows:
|
|
138
|
+
return {"ok": False, "error": f"Memory {fact_id} not found"}
|
|
139
|
+
content_preview = dict(rows[0]).get("content", "")[:80]
|
|
140
|
+
engine._db.delete_fact(fact_id)
|
|
141
|
+
# Audit log
|
|
142
|
+
import logging as _logging
|
|
143
|
+
_logging.getLogger("superlocalmemory.audit").info(
|
|
144
|
+
"DELETE fact_id=%s by agent=%s content=%s", fact_id[:16], agent_id, content_preview,
|
|
145
|
+
)
|
|
146
|
+
return {"ok": True, "deleted": fact_id, "content_preview": content_preview}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _handle_update_memory(fact_id: str, content: str, agent_id: str = "system") -> dict:
|
|
150
|
+
"""Update content of a specific atomic fact with audit logging."""
|
|
151
|
+
engine = _get_engine()
|
|
152
|
+
pid = engine.profile_id
|
|
153
|
+
rows = engine._db.execute(
|
|
154
|
+
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ? LIMIT 1",
|
|
155
|
+
(fact_id, pid),
|
|
156
|
+
)
|
|
157
|
+
if not rows:
|
|
158
|
+
return {"ok": False, "error": f"Memory {fact_id} not found"}
|
|
159
|
+
old_content = dict(rows[0]).get("content", "")[:80]
|
|
160
|
+
engine._db.execute(
|
|
161
|
+
"UPDATE atomic_facts SET content = ? WHERE fact_id = ?",
|
|
162
|
+
(content, fact_id),
|
|
163
|
+
)
|
|
164
|
+
import logging as _logging
|
|
165
|
+
_logging.getLogger("superlocalmemory.audit").info(
|
|
166
|
+
"UPDATE fact_id=%s by agent=%s old=%s new=%s",
|
|
167
|
+
fact_id[:16], agent_id, old_content, content[:80],
|
|
168
|
+
)
|
|
169
|
+
return {"ok": True, "fact_id": fact_id, "content": content}
|
|
170
|
+
|
|
171
|
+
|
|
110
172
|
def _handle_summarize(texts: list[str], mode: str) -> dict:
|
|
111
173
|
"""Generate summary using heuristic (A) or LLM (B/C)."""
|
|
112
174
|
from superlocalmemory.core.summarizer import Summarizer
|
|
@@ -167,6 +229,18 @@ def _worker_main() -> None:
|
|
|
167
229
|
elif cmd == "store":
|
|
168
230
|
result = _handle_store(req.get("content", ""), req.get("metadata", {}))
|
|
169
231
|
_respond(result)
|
|
232
|
+
elif cmd == "delete_memory":
|
|
233
|
+
result = _handle_delete_memory(
|
|
234
|
+
req.get("fact_id", ""), req.get("agent_id", "system"),
|
|
235
|
+
)
|
|
236
|
+
_respond(result)
|
|
237
|
+
elif cmd == "update_memory":
|
|
238
|
+
result = _handle_update_memory(
|
|
239
|
+
req.get("fact_id", ""),
|
|
240
|
+
req.get("content", ""),
|
|
241
|
+
req.get("agent_id", "system"),
|
|
242
|
+
)
|
|
243
|
+
_respond(result)
|
|
170
244
|
elif cmd == "get_memory_facts":
|
|
171
245
|
result = _handle_get_memory_facts(req.get("memory_id", ""))
|
|
172
246
|
_respond(result)
|
|
@@ -73,6 +73,14 @@ class WorkerPool:
|
|
|
73
73
|
"metadata": metadata or {},
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
+
def delete_memory(self, fact_id: str, agent_id: str = "system") -> dict:
|
|
77
|
+
"""Delete a specific memory by fact_id. Logged for audit."""
|
|
78
|
+
return self._send({"cmd": "delete_memory", "fact_id": fact_id, "agent_id": agent_id})
|
|
79
|
+
|
|
80
|
+
def update_memory(self, fact_id: str, content: str, agent_id: str = "system") -> dict:
|
|
81
|
+
"""Update content of a specific memory. Logged for audit."""
|
|
82
|
+
return self._send({"cmd": "update_memory", "fact_id": fact_id, "content": content, "agent_id": agent_id})
|
|
83
|
+
|
|
76
84
|
def get_memory_facts(self, memory_id: str) -> dict:
|
|
77
85
|
"""Get original memory text + child atomic facts."""
|
|
78
86
|
return self._send({"cmd": "get_memory_facts", "memory_id": memory_id})
|
|
@@ -281,6 +281,67 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
281
281
|
logger.exception("correct_pattern failed")
|
|
282
282
|
return {"success": False, "error": str(exc)}
|
|
283
283
|
|
|
284
|
+
@server.tool()
|
|
285
|
+
async def delete_memory(fact_id: str, agent_id: str = "mcp_client") -> dict:
|
|
286
|
+
"""Delete a specific memory by exact fact ID.
|
|
287
|
+
|
|
288
|
+
Security note: This is a destructive operation. All deletions are
|
|
289
|
+
logged with the calling agent_id for audit trail. Use get_status or
|
|
290
|
+
list_recent to find fact_ids before deleting.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
fact_id: Exact fact ID to delete (from recall or list_recent results).
|
|
294
|
+
agent_id: Identifier of the calling agent (logged for audit).
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
from superlocalmemory.core.worker_pool import WorkerPool
|
|
298
|
+
pool = WorkerPool.shared()
|
|
299
|
+
result = pool._send({
|
|
300
|
+
"cmd": "delete_memory",
|
|
301
|
+
"fact_id": fact_id,
|
|
302
|
+
"agent_id": agent_id,
|
|
303
|
+
})
|
|
304
|
+
if result.get("ok"):
|
|
305
|
+
logger.info("Memory deleted: %s by agent: %s", fact_id[:16], agent_id)
|
|
306
|
+
return {"success": True, "deleted": fact_id, "agent_id": agent_id}
|
|
307
|
+
return {"success": False, "error": result.get("error", "Delete failed")}
|
|
308
|
+
except Exception as exc:
|
|
309
|
+
logger.exception("delete_memory failed")
|
|
310
|
+
return {"success": False, "error": str(exc)}
|
|
311
|
+
|
|
312
|
+
@server.tool()
|
|
313
|
+
async def update_memory(
|
|
314
|
+
fact_id: str, content: str, agent_id: str = "mcp_client",
|
|
315
|
+
) -> dict:
|
|
316
|
+
"""Update the content of a specific memory by exact fact ID.
|
|
317
|
+
|
|
318
|
+
Security note: All updates are logged with the calling agent_id.
|
|
319
|
+
The fact_id must belong to the active profile.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
fact_id: Exact fact ID to update.
|
|
323
|
+
content: New content for the memory (cannot be empty).
|
|
324
|
+
agent_id: Identifier of the calling agent (logged for audit).
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
if not content or not content.strip():
|
|
328
|
+
return {"success": False, "error": "content cannot be empty"}
|
|
329
|
+
from superlocalmemory.core.worker_pool import WorkerPool
|
|
330
|
+
pool = WorkerPool.shared()
|
|
331
|
+
result = pool._send({
|
|
332
|
+
"cmd": "update_memory",
|
|
333
|
+
"fact_id": fact_id,
|
|
334
|
+
"content": content.strip(),
|
|
335
|
+
"agent_id": agent_id,
|
|
336
|
+
})
|
|
337
|
+
if result.get("ok"):
|
|
338
|
+
logger.info("Memory updated: %s by agent: %s", fact_id[:16], agent_id)
|
|
339
|
+
return {"success": True, "fact_id": fact_id, "content": content.strip()}
|
|
340
|
+
return {"success": False, "error": result.get("error", "Update failed")}
|
|
341
|
+
except Exception as exc:
|
|
342
|
+
logger.exception("update_memory failed")
|
|
343
|
+
return {"success": False, "error": str(exc)}
|
|
344
|
+
|
|
284
345
|
@server.tool()
|
|
285
346
|
async def get_attribution() -> dict:
|
|
286
347
|
"""Get system attribution: author, version, license, and provenance metadata."""
|
|
@@ -455,3 +455,61 @@ async def get_memory_facts(request: Request, memory_id: str):
|
|
|
455
455
|
raise
|
|
456
456
|
except Exception as e:
|
|
457
457
|
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@router.delete("/api/memories/{fact_id}")
|
|
461
|
+
async def delete_memory(request: Request, fact_id: str):
|
|
462
|
+
"""Delete a specific memory (atomic fact) by ID."""
|
|
463
|
+
try:
|
|
464
|
+
conn = get_db_connection()
|
|
465
|
+
conn.row_factory = dict_factory
|
|
466
|
+
cursor = conn.cursor()
|
|
467
|
+
active_profile = get_active_profile()
|
|
468
|
+
# Verify it exists and belongs to this profile
|
|
469
|
+
cursor.execute(
|
|
470
|
+
"SELECT fact_id FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
471
|
+
(fact_id, active_profile),
|
|
472
|
+
)
|
|
473
|
+
if not cursor.fetchone():
|
|
474
|
+
conn.close()
|
|
475
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
476
|
+
cursor.execute("DELETE FROM atomic_facts WHERE fact_id = ?", (fact_id,))
|
|
477
|
+
conn.commit()
|
|
478
|
+
conn.close()
|
|
479
|
+
return {"success": True, "deleted": fact_id}
|
|
480
|
+
except HTTPException:
|
|
481
|
+
raise
|
|
482
|
+
except Exception as e:
|
|
483
|
+
raise HTTPException(status_code=500, detail=f"Delete error: {str(e)}")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@router.patch("/api/memories/{fact_id}")
|
|
487
|
+
async def edit_memory(request: Request, fact_id: str):
|
|
488
|
+
"""Edit the content of a specific memory (atomic fact)."""
|
|
489
|
+
try:
|
|
490
|
+
body = await request.json()
|
|
491
|
+
new_content = (body.get("content") or "").strip()
|
|
492
|
+
if not new_content:
|
|
493
|
+
raise HTTPException(status_code=400, detail="content is required")
|
|
494
|
+
conn = get_db_connection()
|
|
495
|
+
conn.row_factory = dict_factory
|
|
496
|
+
cursor = conn.cursor()
|
|
497
|
+
active_profile = get_active_profile()
|
|
498
|
+
cursor.execute(
|
|
499
|
+
"SELECT fact_id FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
500
|
+
(fact_id, active_profile),
|
|
501
|
+
)
|
|
502
|
+
if not cursor.fetchone():
|
|
503
|
+
conn.close()
|
|
504
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
505
|
+
cursor.execute(
|
|
506
|
+
"UPDATE atomic_facts SET content = ? WHERE fact_id = ?",
|
|
507
|
+
(new_content, fact_id),
|
|
508
|
+
)
|
|
509
|
+
conn.commit()
|
|
510
|
+
conn.close()
|
|
511
|
+
return {"success": True, "fact_id": fact_id, "content": new_content}
|
|
512
|
+
except HTTPException:
|
|
513
|
+
raise
|
|
514
|
+
except Exception as e:
|
|
515
|
+
raise HTTPException(status_code=500, detail=f"Edit error: {str(e)}")
|
|
@@ -128,6 +128,32 @@ class DatabaseManager:
|
|
|
128
128
|
)
|
|
129
129
|
return record.memory_id
|
|
130
130
|
|
|
131
|
+
def update_memory_summary(self, memory_id: str, summary: str) -> None:
|
|
132
|
+
"""Store a generated summary for a memory record."""
|
|
133
|
+
try:
|
|
134
|
+
self.execute(
|
|
135
|
+
"UPDATE memories SET metadata_json = json_set("
|
|
136
|
+
" COALESCE(metadata_json, '{}'), '$.summary', ?"
|
|
137
|
+
") WHERE memory_id = ?",
|
|
138
|
+
(summary, memory_id),
|
|
139
|
+
)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass # Non-critical — summary is enhancement only
|
|
142
|
+
|
|
143
|
+
def get_memory_summary(self, memory_id: str) -> str:
|
|
144
|
+
"""Retrieve stored summary for a memory, or empty string."""
|
|
145
|
+
try:
|
|
146
|
+
rows = self.execute(
|
|
147
|
+
"SELECT json_extract(metadata_json, '$.summary') as s "
|
|
148
|
+
"FROM memories WHERE memory_id = ?",
|
|
149
|
+
(memory_id,),
|
|
150
|
+
)
|
|
151
|
+
if rows:
|
|
152
|
+
return dict(rows[0]).get("s") or ""
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
return ""
|
|
156
|
+
|
|
131
157
|
def store_fact(self, fact: AtomicFact) -> str:
|
|
132
158
|
"""Persist an atomic fact. Returns fact_id."""
|
|
133
159
|
self.execute(
|
package/ui/js/modal.js
CHANGED
|
@@ -176,6 +176,63 @@ function openMemoryDetail(mem, source) {
|
|
|
176
176
|
actionsDiv.appendChild(filterBtn);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// Edit button — always available
|
|
180
|
+
var editBtn = document.createElement('button');
|
|
181
|
+
editBtn.className = 'btn btn-outline-warning btn-sm';
|
|
182
|
+
editBtn.innerHTML = '<i class="bi bi-pencil"></i> Edit';
|
|
183
|
+
editBtn.onclick = function() {
|
|
184
|
+
var currentText = contentDiv.textContent;
|
|
185
|
+
var textarea = document.createElement('textarea');
|
|
186
|
+
textarea.className = 'form-control mb-2';
|
|
187
|
+
textarea.rows = 4;
|
|
188
|
+
textarea.value = currentText;
|
|
189
|
+
contentDiv.textContent = '';
|
|
190
|
+
contentDiv.appendChild(textarea);
|
|
191
|
+
var saveBtn = document.createElement('button');
|
|
192
|
+
saveBtn.className = 'btn btn-sm btn-success me-1';
|
|
193
|
+
saveBtn.textContent = 'Save';
|
|
194
|
+
saveBtn.onclick = function() {
|
|
195
|
+
var newContent = textarea.value.trim();
|
|
196
|
+
if (!newContent) return;
|
|
197
|
+
fetch('/api/memories/' + encodeURIComponent(mem.id), {
|
|
198
|
+
method: 'PATCH',
|
|
199
|
+
headers: {'Content-Type': 'application/json'},
|
|
200
|
+
body: JSON.stringify({content: newContent})
|
|
201
|
+
}).then(function(r) { return r.json(); }).then(function(d) {
|
|
202
|
+
if (d.success) {
|
|
203
|
+
contentDiv.textContent = newContent;
|
|
204
|
+
mem.content = newContent;
|
|
205
|
+
if (typeof showToast === 'function') showToast('Memory updated');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
var cancelBtn = document.createElement('button');
|
|
210
|
+
cancelBtn.className = 'btn btn-sm btn-secondary';
|
|
211
|
+
cancelBtn.textContent = 'Cancel';
|
|
212
|
+
cancelBtn.onclick = function() { contentDiv.textContent = currentText; };
|
|
213
|
+
contentDiv.appendChild(saveBtn);
|
|
214
|
+
contentDiv.appendChild(cancelBtn);
|
|
215
|
+
};
|
|
216
|
+
actionsDiv.appendChild(editBtn);
|
|
217
|
+
|
|
218
|
+
// Delete button — always available
|
|
219
|
+
var deleteBtn = document.createElement('button');
|
|
220
|
+
deleteBtn.className = 'btn btn-outline-danger btn-sm';
|
|
221
|
+
deleteBtn.innerHTML = '<i class="bi bi-trash"></i> Delete';
|
|
222
|
+
deleteBtn.onclick = function() {
|
|
223
|
+
if (!confirm('Delete this memory? This cannot be undone.')) return;
|
|
224
|
+
fetch('/api/memories/' + encodeURIComponent(mem.id), {method: 'DELETE'})
|
|
225
|
+
.then(function(r) { return r.json(); })
|
|
226
|
+
.then(function(d) {
|
|
227
|
+
if (d.success) {
|
|
228
|
+
modal.hide();
|
|
229
|
+
if (typeof showToast === 'function') showToast('Memory deleted');
|
|
230
|
+
if (typeof loadMemories === 'function') setTimeout(loadMemories, 300);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
actionsDiv.appendChild(deleteBtn);
|
|
235
|
+
|
|
179
236
|
body.appendChild(actionsDiv);
|
|
180
237
|
}
|
|
181
238
|
|