diff --git a/chat/templates/_drawer.html b/chat/templates/_drawer.html index 621d4af..6c1b2c1 100644 --- a/chat/templates/_drawer.html +++ b/chat/templates/_drawer.html @@ -456,6 +456,52 @@ +
+

Significance review

+ {% set total_mem = significance_distribution.values()|sum %} + {% if total_mem %} + + {% else %} +

No memories yet.

+ {% endif %} + {% if recent_memories %} +
+ Edit significance (recent memories) + +
+ {% endif %} +
+

Pinned memories ({{ pinned|length }} / {{ pin_cap }})

{% if pinned %} diff --git a/chat/web/drawer.py b/chat/web/drawer.py index 93c017d..251e2ab 100644 --- a/chat/web/drawer.py +++ b/chat/web/drawer.py @@ -181,6 +181,21 @@ async def drawer(chat_id: str, request: Request, conn=Depends(get_conn)): # returns both flavours and the template highlights the active one). branches = list_branches_with_metadata(conn, chat_id) + # T98.2: significance distribution across this chat's memories. Powers + # the "Significance review" panel — a small histogram letting authors + # spot lopsided buckets (e.g. nothing significant=3 yet) and triage by + # editing individual memory significance values. + sig_rows = conn.execute( + "SELECT significance, COUNT(*) FROM memories " + "WHERE chat_id = ? GROUP BY significance ORDER BY significance", + (chat_id,), + ).fetchall() + significance_distribution = {int(r[0]): int(r[1]) for r in sig_rows} + # Ensure every bucket 0..3 is present so the bar-chart template can + # render a stable axis even when a level has zero rows. + for level in (0, 1, 2, 3): + significance_distribution.setdefault(level, 0) + return TEMPLATES.TemplateResponse( request, "_drawer.html", @@ -209,6 +224,7 @@ async def drawer(chat_id: str, request: Request, conn=Depends(get_conn)): "active_events": active_events, "open_threads": open_threads, "branches": branches, + "significance_distribution": significance_distribution, }, ) diff --git a/tests/test_drawer_phase4.py b/tests/test_drawer_phase4.py index 3e0f875..e20f01d 100644 --- a/tests/test_drawer_phase4.py +++ b/tests/test_drawer_phase4.py @@ -187,3 +187,96 @@ def test_t98_1_branch_from_turn_emits_branch_created(client, tmp_path): ) assert dup.status_code == 400 assert seed_id < turn_id # sanity: turn is after chat_created + + +# --------------------------------------------------------------------------- +# T98.2 — significance review panel. +# --------------------------------------------------------------------------- + + +def _seed_memories_for_significance(db: Path) -> list[int]: + """Seed three memories with significance levels 0, 1, 2. Returns ids. + + Uses ``append_and_apply`` (vs ``append_event`` + a final ``project``) + so each row is applied exactly once — the chat row was already + materialised by ``_seed_chat`` and a re-projection would conflict + on ``chats.id`` UNIQUE. + """ + ids: list[int] = [] + with open_db(db) as conn: + for sig in (0, 1, 2): + append_and_apply( + conn, + kind="memory_written", + payload={ + "owner_id": "bot_a", + "chat_id": "chat_bot_a", + "pov_summary": f"memory at significance {sig}", + "witness_you": 1, + "witness_host": 1, + "witness_guest": 0, + "significance": sig, + }, + ) + rows = conn.execute( + "SELECT id FROM memories WHERE chat_id = 'chat_bot_a' " + "ORDER BY id ASC" + ).fetchall() + ids = [int(r[0]) for r in rows] + return ids + + +def test_t98_2_distribution_renders_per_significance_bucket(client, tmp_path): + db = tmp_path / "test.db" + _seed_chat(db) + _seed_memories_for_significance(db) + + response = client.get("/chats/chat_bot_a/drawer") + assert response.status_code == 200 + body = response.text + + # Section heading + bar entries for each significance level. + assert "

Significance review

" in body + # All four buckets appear by their canonical label even when count=0. + assert ">★★ (3)<" in body or "(3)" in body + # The distribution markup names each level explicitly. + for level in (0, 1, 2, 3): + assert f"sig-bar sig-{level}" in body + # Three seeded memories (sigs 0, 1, 2) — each has a count = 1 bar. + # We don't pin exact text formatting, just verify the per-level bars + # are present. + + +def test_t98_2_edit_significance_via_existing_route_lands_manual_edit( + client, tmp_path +): + db = tmp_path / "test.db" + _seed_chat(db) + ids = _seed_memories_for_significance(db) + + target_id = ids[0] # initially significance=0 + response = client.post( + f"/chats/chat_bot_a/drawer/memory/{target_id}/significance", + data={"significance": "3"}, + ) + assert response.status_code == 200 + + with open_db(db) as conn: + # Significance updated in the projected table. + row = conn.execute( + "SELECT significance FROM memories WHERE id = ?", (target_id,) + ).fetchone() + assert int(row[0]) == 3 + + # manual_edit landed in the event log with the prior snapshot. + import json as _json + + log_rows = conn.execute( + "SELECT payload_json FROM event_log " + "WHERE kind = 'manual_edit' ORDER BY id DESC LIMIT 1" + ).fetchone() + payload = _json.loads(log_rows[0]) + assert payload["target_kind"] == "memory_significance" + assert int(payload["target_id"]) == target_id + assert payload["prior_value"] == 0 + assert payload["new_value"] == 3