feat: drawer edits with manual_edit event capture
This commit is contained in:
+150
-7
@@ -1,21 +1,37 @@
|
||||
"""Read-only chat drawer (T24).
|
||||
"""Chat drawer — read view (T24) and inline edits (T25).
|
||||
|
||||
Returns an HTML partial rendered into the chat shell's `<aside id="drawer">`
|
||||
on first reveal. Shows the current scene + container, per-entity activity,
|
||||
host <-> you edges, pinned memories with an `n / cap` counter, and recent
|
||||
witnessed memories from the host's POV with significance markers.
|
||||
The GET endpoint renders an HTML partial showing the current scene +
|
||||
container, per-entity activity, host <-> you edges, pinned memories with
|
||||
an ``n / cap`` counter, and recent witnessed memories from the host's
|
||||
POV with significance markers.
|
||||
|
||||
Edit affordances are added in T25; this endpoint is intentionally read-only.
|
||||
T25 adds three POST endpoints for the most useful inline edits, each
|
||||
returning the refreshed drawer partial so HTMX can swap it in:
|
||||
|
||||
* affinity slider on an edge (emits ``manual_edit``);
|
||||
* significance dropdown on a memory (emits ``manual_edit``);
|
||||
* pin toggle on a memory (emits ``memory_pin_changed`` with
|
||||
``auto_pinned=0`` so a manual pin is not subject to auto-eviction).
|
||||
|
||||
Each ``manual_edit`` payload snapshots the prior value alongside the new
|
||||
one so a later inverse edit can restore state (§6.4 final paragraph).
|
||||
|
||||
Other §6.4 editable fields (activity verb/attention/posture, edge_trust,
|
||||
edge summary, knowledge_facts list, memory pov_summary) are deferred to
|
||||
a Phase 1.5 follow-up — the dispatch in :mod:`chat.state.manual_edit`
|
||||
already accepts more ``target_kind`` values, so adding their routes is a
|
||||
mechanical extension.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from chat.eventlog.log import append_and_apply
|
||||
from chat.state.edges import get_edge
|
||||
from chat.state.entities import get_bot, get_you
|
||||
from chat.state.memory import get_pinned
|
||||
@@ -104,3 +120,130 @@ async def drawer(chat_id: str, request: Request, conn=Depends(get_conn)):
|
||||
"pin_cap": PIN_CAP,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# --- T25 edit endpoints ---------------------------------------------------
|
||||
#
|
||||
# Each endpoint:
|
||||
# 1. Loads the chat (404 if missing) and the target row (404 if missing).
|
||||
# 2. Reads the prior value before mutating, so the event payload carries
|
||||
# it for §6.4 reversibility.
|
||||
# 3. Calls ``append_and_apply`` so the projected table updates atomically
|
||||
# with the event log append; full reprojection would re-add deltas
|
||||
# from earlier ``edge_update`` events.
|
||||
# 4. Returns the refreshed drawer partial via ``await drawer(...)``, which
|
||||
# HTMX swaps into ``#drawer``.
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chats/{chat_id}/drawer/edge/{source_id}/{target_id}/affinity",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def edit_edge_affinity(
|
||||
chat_id: str,
|
||||
source_id: str,
|
||||
target_id: str,
|
||||
request: Request,
|
||||
affinity: int = Form(...),
|
||||
conn=Depends(get_conn),
|
||||
):
|
||||
chat = get_chat(conn, chat_id)
|
||||
if chat is None:
|
||||
raise HTTPException(status_code=404, detail=f"chat not found: {chat_id}")
|
||||
|
||||
edge = get_edge(conn, source_id, target_id)
|
||||
if edge is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"edge not found: {source_id}->{target_id}",
|
||||
)
|
||||
|
||||
prior = int(edge["affinity"])
|
||||
new_value = max(0, min(100, int(affinity)))
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="manual_edit",
|
||||
payload={
|
||||
"target_kind": "edge_affinity",
|
||||
"target_id": {"source_id": source_id, "target_id": target_id},
|
||||
"prior_value": prior,
|
||||
"new_value": new_value,
|
||||
},
|
||||
)
|
||||
return await drawer(chat_id, request, conn)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chats/{chat_id}/drawer/memory/{memory_id}/significance",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def edit_memory_significance(
|
||||
chat_id: str,
|
||||
memory_id: int,
|
||||
request: Request,
|
||||
significance: int = Form(...),
|
||||
conn=Depends(get_conn),
|
||||
):
|
||||
chat = get_chat(conn, chat_id)
|
||||
if chat is None:
|
||||
raise HTTPException(status_code=404, detail=f"chat not found: {chat_id}")
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT significance FROM memories WHERE id = ?", (memory_id,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"memory not found: {memory_id}"
|
||||
)
|
||||
|
||||
prior = int(row[0])
|
||||
new_value = max(0, min(3, int(significance)))
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="manual_edit",
|
||||
payload={
|
||||
"target_kind": "memory_significance",
|
||||
"target_id": int(memory_id),
|
||||
"prior_value": prior,
|
||||
"new_value": new_value,
|
||||
},
|
||||
)
|
||||
return await drawer(chat_id, request, conn)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chats/{chat_id}/drawer/memory/{memory_id}/pin",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def toggle_memory_pin(
|
||||
chat_id: str,
|
||||
memory_id: int,
|
||||
request: Request,
|
||||
pinned: int = Form(...),
|
||||
conn=Depends(get_conn),
|
||||
):
|
||||
chat = get_chat(conn, chat_id)
|
||||
if chat is None:
|
||||
raise HTTPException(status_code=404, detail=f"chat not found: {chat_id}")
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT pinned FROM memories WHERE id = ?", (memory_id,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"memory not found: {memory_id}"
|
||||
)
|
||||
|
||||
new_pinned = 1 if int(pinned) else 0
|
||||
# Manual pin: ``auto_pinned=0`` so the §8.5 eviction query (which only
|
||||
# touches auto-pinned rows) leaves this alone.
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="memory_pin_changed",
|
||||
payload={
|
||||
"memory_id": int(memory_id),
|
||||
"pinned": new_pinned,
|
||||
"auto_pinned": 0,
|
||||
},
|
||||
)
|
||||
return await drawer(chat_id, request, conn)
|
||||
|
||||
Reference in New Issue
Block a user